diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..a19ade077 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +CHANGELOG.md merge=union diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7bdb4ecab..7a5c2ab46 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -3,3 +3,26 @@ # @temporalio/sdk will be requested for review when # someone opens a pull request. * @temporalio/sdk + + +# Below are owners for modules in the temporalio/contrib/ +# and tests/contrib/ directories that are owned by teams +# other than the SDK team. For each one, we add the owning team, +# as well as @temporalio/sdk, so the SDK team can continue to +# manage repo-wide concerns. +/temporalio/contrib/deepagents/ @temporalio/ai-sdk @temporalio/sdk +/temporalio/contrib/google_adk_agents/ @temporalio/ai-sdk @temporalio/sdk +/temporalio/contrib/google_genai/ @temporalio/ai-sdk @temporalio/sdk +/temporalio/contrib/langgraph/ @temporalio/ai-sdk @temporalio/sdk +/temporalio/contrib/langsmith/ @temporalio/ai-sdk @temporalio/sdk +/temporalio/contrib/openai_agents/ @temporalio/ai-sdk @temporalio/sdk +/temporalio/contrib/strands/ @temporalio/ai-sdk @temporalio/sdk +/temporalio/contrib/workflow_streams/ @temporalio/ai-sdk @temporalio/sdk +/tests/contrib/deepagents/ @temporalio/ai-sdk @temporalio/sdk +/tests/contrib/google_adk_agents/ @temporalio/ai-sdk @temporalio/sdk +/tests/contrib/google_genai/ @temporalio/ai-sdk @temporalio/sdk +/tests/contrib/langgraph/ @temporalio/ai-sdk @temporalio/sdk +/tests/contrib/langsmith/ @temporalio/ai-sdk @temporalio/sdk +/tests/contrib/openai_agents/ @temporalio/ai-sdk @temporalio/sdk +/tests/contrib/strands/ @temporalio/ai-sdk @temporalio/sdk +/tests/contrib/workflow_streams/ @temporalio/ai-sdk @temporalio/sdk diff --git a/.github/actions/alpine-package-smoke/action.yml b/.github/actions/alpine-package-smoke/action.yml new file mode 100644 index 000000000..ad72f2eb9 --- /dev/null +++ b/.github/actions/alpine-package-smoke/action.yml @@ -0,0 +1,72 @@ +name: Alpine package smoke test +description: Install a local or published temporalio package in Alpine and run a minimal SDK workflow. +inputs: + wheel-dir: + description: "Directory containing local wheel files to install" + required: false + default: "" + version: + description: "Published package version to install and verify" + required: false + default: "" + index-url: + description: "Primary package index URL for published package installs" + required: false + default: "" + dependency-index-url: + description: "Optional dependency package index URL for published package installs" + required: false + default: "" + python-image: + description: "Alpine Python Docker image" + required: false + default: "python:3.10-alpine" +runs: + using: composite + steps: + - name: Install package and run SDK smoke test + shell: bash + env: + WHEEL_DIR: ${{ inputs.wheel-dir }} + VERSION: ${{ inputs.version }} + INDEX_URL: ${{ inputs.index-url }} + DEPENDENCY_INDEX_URL: ${{ inputs.dependency-index-url }} + PYTHON_IMAGE: ${{ inputs.python-image }} + run: | + set -euo pipefail + + if [[ -z "$WHEEL_DIR" && ( -z "$VERSION" || -z "$INDEX_URL" ) ]]; then + echo "Either wheel-dir or both version and index-url must be provided" >&2 + exit 1 + fi + + docker run --rm -v "$(pwd):/workspace" -w /tmp \ + -e WHEEL_DIR="$WHEEL_DIR" \ + -e VERSION="$VERSION" \ + -e INDEX_URL="$INDEX_URL" \ + -e DEPENDENCY_INDEX_URL="$DEPENDENCY_INDEX_URL" \ + "$PYTHON_IMAGE" sh -c ' + set -eu + python -m venv /tmp/alpine-smoke + /tmp/alpine-smoke/bin/python -m pip install --upgrade pip + + if [ -n "$WHEEL_DIR" ]; then + /tmp/alpine-smoke/bin/python -m pip install --prefer-binary /workspace/$WHEEL_DIR/*.whl + elif [ -n "$DEPENDENCY_INDEX_URL" ]; then + /tmp/alpine-smoke/bin/python /workspace/.github/scripts/install_release_package.py \ + --version "$VERSION" \ + --index-url "$INDEX_URL" \ + --dependency-index-url "$DEPENDENCY_INDEX_URL" + else + /tmp/alpine-smoke/bin/python /workspace/.github/scripts/install_release_package.py \ + --version "$VERSION" \ + --index-url "$INDEX_URL" + fi + + if [ -z "$VERSION" ]; then + VERSION=$(/tmp/alpine-smoke/bin/python -c "import importlib.metadata; print(importlib.metadata.version(\"temporalio\"))") + export VERSION + fi + + /tmp/alpine-smoke/bin/python /workspace/.github/scripts/release_smoke_package.py + ' diff --git a/.github/actions/release-smoke-package/action.yml b/.github/actions/release-smoke-package/action.yml new file mode 100644 index 000000000..bd557cd2b --- /dev/null +++ b/.github/actions/release-smoke-package/action.yml @@ -0,0 +1,43 @@ +name: Release package smoke test +description: Install a published temporalio package and run a minimal SDK workflow. +inputs: + version: + description: "Package version to install and verify" + required: true + index-url: + description: "Primary package index URL" + required: true + dependency-index-url: + description: "Optional dependency package index URL" + required: false + default: "" +runs: + using: composite + steps: + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: "3.10" + - name: Install package + shell: bash + env: + VERSION: ${{ inputs.version }} + INDEX_URL: ${{ inputs.index-url }} + DEPENDENCY_INDEX_URL: ${{ inputs.dependency-index-url }} + run: | + set -euo pipefail + python -m venv .venv + .venv/bin/python -m pip install --upgrade pip + + install_args=(--version "$VERSION" --index-url "$INDEX_URL") + if [[ -n "$DEPENDENCY_INDEX_URL" ]]; then + install_args+=(--dependency-index-url "$DEPENDENCY_INDEX_URL") + fi + + .venv/bin/python .github/scripts/install_release_package.py "${install_args[@]}" + - name: Run SDK smoke test + shell: bash + env: + VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + .venv/bin/python .github/scripts/release_smoke_package.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..1f3a19d73 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +version: 2 +updates: + - package-ecosystem: "uv" + directory: "/" + schedule: + interval: "weekly" + cooldown: + default-days: 14 + open-pull-requests-limit: 0 + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + cooldown: + default-days: 14 + open-pull-requests-limit: 0 diff --git a/.github/scripts/cloud_namespace.py b/.github/scripts/cloud_namespace.py new file mode 100644 index 000000000..c13d1e65e --- /dev/null +++ b/.github/scripts/cloud_namespace.py @@ -0,0 +1,110 @@ +"""Create and delete an isolated Temporal Cloud namespace for CI.""" + +import asyncio +import os +import sys +import time +from pathlib import Path + +from temporalio.api.cloud.cloudservice.v1 import ( + CreateNamespaceRequest, + DeleteNamespaceRequest, + GetAsyncOperationRequest, + GetNamespaceRequest, +) +from temporalio.api.cloud.namespace.v1 import MtlsAuthSpec, NamespaceSpec +from temporalio.api.cloud.operation.v1 import AsyncOperation +from temporalio.client import CloudOperationsClient + + +async def wait_for_operation( + client: CloudOperationsClient, operation: AsyncOperation +) -> None: + deadline = time.monotonic() + 10 * 60 + while True: + operation = ( + await client.cloud_service.get_async_operation( + GetAsyncOperationRequest(async_operation_id=operation.id) + ) + ).async_operation + if operation.state == AsyncOperation.STATE_FULFILLED: + return + if operation.state in { + AsyncOperation.STATE_FAILED, + AsyncOperation.STATE_CANCELLED, + AsyncOperation.STATE_REJECTED, + }: + raise RuntimeError( + "Cloud operation " + f"{operation.id} {AsyncOperation.State.Name(operation.state).lower()}: " + f"{operation.failure_reason}" + ) + if time.monotonic() >= deadline: + raise TimeoutError(f"Timed out waiting for Cloud operation {operation.id}") + delay = max( + operation.check_duration.seconds + + operation.check_duration.nanos / 1_000_000_000, + 1, + ) + await asyncio.sleep(min(delay, deadline - time.monotonic())) + + +async def create() -> None: + client = await cloud_client() + namespace_name = "sdk-python-ci-{}-{}".format( + os.environ["GITHUB_RUN_ID"], os.environ["GITHUB_RUN_ATTEMPT"] + ) + result = await client.cloud_service.create_namespace( + CreateNamespaceRequest( + spec=NamespaceSpec( + name=namespace_name, + regions=["aws-ca-central-1"], + retention_days=1, + mtls_auth=MtlsAuthSpec( + accepted_client_ca=Path( + os.environ["TEMPORAL_CLOUD_CLIENT_CA_PATH"] + ).read_bytes(), + enabled=True, + ), + ) + ) + ) + # Make cleanup possible even if provisioning fails after Cloud accepts the request. + with open(os.environ["GITHUB_OUTPUT"], "a") as output: + output.write(f"namespace={result.namespace}\n") + await wait_for_operation(client, result.async_operation) + + +async def delete(namespace: str) -> None: + client = await cloud_client() + existing = await client.cloud_service.get_namespace( + GetNamespaceRequest(namespace=namespace) + ) + result = await client.cloud_service.delete_namespace( + DeleteNamespaceRequest( + namespace=namespace, + resource_version=existing.namespace.resource_version, + ) + ) + await wait_for_operation(client, result.async_operation) + + +async def cloud_client() -> CloudOperationsClient: + return await CloudOperationsClient.connect( + api_key=os.environ["TEMPORAL_CLIENT_CLOUD_API_KEY"], + version=os.environ["TEMPORAL_CLIENT_CLOUD_API_VERSION"], + ) + + +async def main() -> None: + match sys.argv[1:]: + case ["create"]: + await create() + case ["delete", namespace]: + await delete(namespace) + case _: + raise ValueError("Usage: cloud_namespace.py create|delete ") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/.github/scripts/install_release_package.py b/.github/scripts/install_release_package.py new file mode 100644 index 000000000..45f407c7f --- /dev/null +++ b/.github/scripts/install_release_package.py @@ -0,0 +1,84 @@ +"""Install a release package for smoke testing.""" + +from __future__ import annotations + +import argparse +import importlib.metadata +import subprocess +import sys +import time +from collections.abc import Sequence + +RELEASE_INSTALL_ATTEMPTS = 31 +RELEASE_INSTALL_RETRY_SECONDS = 30 + + +def _pip_install(args: Sequence[str]) -> None: + subprocess.check_call([sys.executable, "-m", "pip", "install", *args]) + + +def _pip_install_with_retries(args: Sequence[str]) -> None: + for attempt in range(1, RELEASE_INSTALL_ATTEMPTS + 1): + try: + _pip_install(args) + return + except subprocess.CalledProcessError: + if attempt == RELEASE_INSTALL_ATTEMPTS: + raise + print( + "Package was not installable yet; retrying in " + f"{RELEASE_INSTALL_RETRY_SECONDS}s " + f"({attempt}/{RELEASE_INSTALL_ATTEMPTS})", + flush=True, + ) + time.sleep(RELEASE_INSTALL_RETRY_SECONDS) + + +def install_package(args: argparse.Namespace) -> None: + package = f"temporalio=={args.version}" + if args.dependency_index_url: + _pip_install_with_retries( + [ + "--prefer-binary", + "--no-cache-dir", + "--index-url", + args.index_url, + "--no-deps", + package, + ] + ) + + requirements = importlib.metadata.requires("temporalio") or [] + if requirements: + _pip_install( + [ + "--prefer-binary", + "--index-url", + args.dependency_index_url, + *requirements, + ] + ) + else: + _pip_install_with_retries( + [ + "--prefer-binary", + "--no-cache-dir", + "--index-url", + args.index_url, + package, + ] + ) + + subprocess.check_call([sys.executable, "-m", "pip", "check"]) + + +def main(argv: Sequence[str] | None = None) -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--version", required=True) + parser.add_argument("--index-url", required=True) + parser.add_argument("--dependency-index-url") + install_package(parser.parse_args(argv)) + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/release_smoke_package.py b/.github/scripts/release_smoke_package.py new file mode 100644 index 000000000..00a192e85 --- /dev/null +++ b/.github/scripts/release_smoke_package.py @@ -0,0 +1,59 @@ +"""Smoke test an installed temporalio release package.""" + +from __future__ import annotations + +import asyncio +import os +import uuid +from datetime import timedelta + +import temporalio +from temporalio import activity, workflow +from temporalio.testing import WorkflowEnvironment +from temporalio.worker import UnsandboxedWorkflowRunner, Worker + + +@activity.defn +async def say_hello(name: str) -> str: + return f"Hello, {name}!" + + +@workflow.defn +class SmokeWorkflow: + @workflow.run + async def run(self, name: str) -> str: + return await workflow.execute_activity( + say_hello, + name, + start_to_close_timeout=timedelta(seconds=10), + ) + + +async def main() -> None: + expected_version = os.environ["VERSION"] + if temporalio.__version__ != expected_version: + raise RuntimeError( + f"Expected temporalio {expected_version}, got {temporalio.__version__}" + ) + + task_queue = f"release-smoke-{uuid.uuid4()}" + async with await WorkflowEnvironment.start_local() as env: + async with Worker( + env.client, + task_queue=task_queue, + workflows=[SmokeWorkflow], + activities=[say_hello], + workflow_runner=UnsandboxedWorkflowRunner(), + ): + result = await env.client.execute_workflow( + SmokeWorkflow.run, + "trusted publishing", + id=task_queue, + task_queue=task_queue, + ) + if result != "Hello, trusted publishing!": + raise RuntimeError(f"Unexpected workflow result: {result!r}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/.github/scripts/release_verify.py b/.github/scripts/release_verify.py new file mode 100644 index 000000000..23df9d5e3 --- /dev/null +++ b/.github/scripts/release_verify.py @@ -0,0 +1,391 @@ +"""Release workflow validation helpers.""" + +from __future__ import annotations + +import argparse +import ast +import dataclasses +import difflib +import pathlib +import re +import subprocess +from collections.abc import Sequence + +try: + import tomllib +except ModuleNotFoundError: + import toml as tomllib # type: ignore[no-redef] + + +def _checked_in_version() -> str: + pyproject_version = tomllib.loads(pathlib.Path("pyproject.toml").read_text())[ + "project" + ]["version"] + service_tree = ast.parse(pathlib.Path("temporalio/service.py").read_text()) + service_version = None + for stmt in service_tree.body: + if ( + isinstance(stmt, ast.Assign) + and any( + isinstance(target, ast.Name) and target.id == "__version__" + for target in stmt.targets + ) + and isinstance(stmt.value, ast.Constant) + and isinstance(stmt.value.value, str) + ): + service_version = stmt.value.value + break + + if pyproject_version != service_version: + raise RuntimeError( + f"pyproject.toml version {pyproject_version!r} does not match " + f"temporalio/service.py version {service_version!r}" + ) + if pyproject_version.startswith("v"): + raise RuntimeError("Checked-in version must not start with 'v'") + if not re.fullmatch(r"[0-9]+(?:\.[0-9]+)+(?:[a-zA-Z0-9_.+-]+)?", pyproject_version): + raise RuntimeError(f"Invalid checked-in version: {pyproject_version!r}") + return pyproject_version + + +def _write_github_output(path: pathlib.Path, *, version: str, sha: str) -> None: + with path.open("a", encoding="utf-8") as output: + print(f"version={version}", file=output) + print(f"sha={sha}", file=output) + + +def validate_version(args: argparse.Namespace) -> None: + version = _checked_in_version() + if args.github_output: + _write_github_output( + pathlib.Path(args.github_output), + version=version, + sha=args.sha, + ) + else: + print(version) + + +def verify_dist(args: argparse.Namespace) -> None: + dist_dir = pathlib.Path(args.dist_dir) + files = sorted(path.name for path in dist_dir.iterdir() if path.is_file()) + wheels = [name for name in files if name.endswith(".whl")] + sdists = [name for name in files if name.endswith(".tar.gz")] + + if len(files) != len(set(files)): + raise RuntimeError("Duplicate distribution filenames found") + expected_sdist = f"temporalio-{args.version}.tar.gz" + if sdists != [expected_sdist]: + raise RuntimeError(f"Expected only sdist {expected_sdist!r}, found {sdists!r}") + if len(wheels) != 7: + raise RuntimeError( + f"Expected 7 platform wheels, found {len(wheels)}: {wheels!r}" + ) + + for name in files: + if not name.startswith(f"temporalio-{args.version}"): + raise RuntimeError( + f"Distribution filename does not match requested version " + f"{args.version!r}: {name}" + ) + + expected_platforms = { + "linux-x86_64": lambda name: "manylinux" in name and "x86_64" in name, + "linux-aarch64": lambda name: "manylinux" in name and "aarch64" in name, + "linux-musl-x86_64": lambda name: "musllinux" in name and "x86_64" in name, + "linux-musl-aarch64": lambda name: "musllinux" in name and "aarch64" in name, + "macos-x86_64": lambda name: "macosx" in name and "x86_64" in name, + "macos-arm64": lambda name: "macosx" in name and "arm64" in name, + "windows-amd64": lambda name: "win_amd64" in name, + } + missing = [ + platform + for platform, predicate in expected_platforms.items() + if not any(predicate(name) for name in wheels) + ] + if missing: + raise RuntimeError( + f"Missing expected platform wheels: {missing!r}; found {wheels!r}" + ) + + print("Verified release artifacts:") + for name in files: + print(f" {name}") + + +def _git(args: Sequence[str], *, cwd: pathlib.Path | None = None) -> str: + return subprocess.check_output( + ["git", *args], + cwd=cwd, + encoding="utf-8", + stderr=subprocess.STDOUT, + ).strip() + + +def _version_tuple(version: str) -> tuple[int, ...] | None: + match = re.fullmatch(r"([0-9]+(?:\.[0-9]+)+)(?:[a-zA-Z0-9_.+-]+)?", version) + if not match: + return None + return tuple(int(part) for part in match.group(1).split(".")) + + +def _previous_release_tag(version: str) -> str: + current = _version_tuple(version) + if current is None: + raise RuntimeError(f"Cannot determine previous release for {version!r}") + + candidates: list[tuple[int, ...]] = [] + for tag in _git(["tag"]).splitlines(): + tag_version = _version_tuple(tag) + if tag_version is not None and tag_version < current: + candidates.append(tag_version) + if not candidates: + raise RuntimeError(f"Could not find a previous release tag before {version!r}") + return ".".join(str(part) for part in max(candidates)) + + +def _gitlink(rev: str, path: str) -> str: + output = _git(["ls-tree", rev, path]) + parts = output.split() + if len(parts) < 3 or parts[0] != "160000": + raise RuntimeError(f"Could not find submodule gitlink {path!r} at {rev!r}") + return parts[2] + + +def _clean_commit_subject(subject: str) -> str: + subject = subject.encode("ascii", "ignore").decode("ascii") + subject = re.sub(r"\s+", " ", subject).strip() + subject = re.sub(r"^:[a-z0-9_+-]+:\s*", "", subject) + return subject.replace(" : ", ": ") + + +def _link_sdk_core_prs(subject: str) -> str: + return re.sub( + r"\(#([0-9]+)\)", + r"([#\1](https://github.com/temporalio/sdk-rust/pull/\1))", + subject, + ) + + +def _changelog_entries(text: str) -> dict[str, list[list[str]]]: + entries: dict[str, list[list[str]]] = {} + header: str | None = None + entry: list[str] | None = None + + for line in text.splitlines(): + if line.startswith("### "): + if entry is not None: + entries.setdefault(header or "Other", []).append(entry) + entry = None + header = line.removeprefix("### ").strip() + elif line.startswith(("* ", "- ")): + if entry is not None: + entries.setdefault(header or "Other", []).append(entry) + entry = [line] + elif entry is not None: + if line.strip(): + entry.append(line) + else: + entries.setdefault(header or "Other", []).append(entry) + entry = None + + if entry is not None: + entries.setdefault(header or "Other", []).append(entry) + return entries + + +@dataclasses.dataclass +class _ChangelogEntry: + lines: list[str] + introduced_header: str | None = None + + +def _updated_changelog_entries( + previous_entries: dict[str, list[_ChangelogEntry]], + current_entries: dict[str, list[list[str]]], +) -> dict[str, list[_ChangelogEntry]]: + current = [ + (header, entry) + for header, header_entries in current_entries.items() + for entry in header_entries + ] + previous = [ + entry + for category_entries in previous_entries.values() + for entry in category_entries + ] + exact_matches: dict[tuple[str, ...], list[_ChangelogEntry]] = {} + for entry in previous: + exact_matches.setdefault(tuple(entry.lines), []).append(entry) + + matches: dict[int, _ChangelogEntry] = {} + matched_previous: set[int] = set() + for current_index, (_, entry) in enumerate(current): + exact = exact_matches.get(tuple(entry)) + if exact: + previous_entry = exact.pop(0) + matches[current_index] = previous_entry + matched_previous.add(id(previous_entry)) + + candidates: list[tuple[float, int, _ChangelogEntry]] = [] + for current_index, (_, current_entry) in enumerate(current): + if current_index in matches: + continue + for previous_entry in previous: + if id(previous_entry) in matched_previous: + continue + similarity = difflib.SequenceMatcher( + a="\n".join(previous_entry.lines), + b="\n".join(current_entry), + autojunk=False, + ).ratio() + if similarity >= 0.6: + candidates.append((similarity, current_index, previous_entry)) + for _, current_index, previous_entry in sorted( + candidates, key=lambda candidate: candidate[0], reverse=True + ): + if current_index not in matches and id(previous_entry) not in matched_previous: + matches[current_index] = previous_entry + matched_previous.add(id(previous_entry)) + + updated: dict[str, list[_ChangelogEntry]] = {} + for current_index, (header, entry) in enumerate(current): + previous_entry = matches.get(current_index) + updated.setdefault(header, []).append( + _ChangelogEntry( + entry, + previous_entry.introduced_header if previous_entry else header, + ) + ) + return updated + + +def _sdk_core_changelog_entries( + previous_commit: str, + current_commit: str, + path: pathlib.Path, +) -> list[str]: + output = subprocess.check_output( + [ + "cargo", + "run", + "--quiet", + "-p", + "temporalio-sdk-core", + "--bin", + "changelog-release-notes", + "--", + "--from", + previous_commit, + "--to", + current_commit, + ], + cwd=path, + encoding="utf-8", + stderr=subprocess.STDOUT, + ).strip() + return output.splitlines() if output else [] + + +def _sdk_core_release_notes(version: str, path: str) -> list[str]: + previous_tag = _previous_release_tag(version) + previous_commit = _gitlink(previous_tag, path) + current_commit = _gitlink("HEAD", path) + if previous_commit == current_commit: + return [] + + submodule_path = pathlib.Path(path) + if not (submodule_path / ".git").exists(): + raise RuntimeError( + f"Submodule {path!r} is not initialized; checkout with submodules" + ) + + try: + notes = _sdk_core_changelog_entries( + previous_commit, + current_commit, + submodule_path, + ) + except subprocess.CalledProcessError: + _git(["fetch", "--quiet", "origin", "main"], cwd=submodule_path) + notes = _sdk_core_changelog_entries( + previous_commit, + current_commit, + submodule_path, + ) + if not notes: + return [] + + return ["### SDK Core", "", *notes] + + +def changelog_notes(args: argparse.Namespace) -> None: + changelog_path = pathlib.Path(args.changelog) + lines = changelog_path.read_text(encoding="utf-8").splitlines() + heading = re.compile(r"^## \[(?P[^\]]+)\](?:\s+-\s+.*)?\s*$") + + start = None + for index, line in enumerate(lines): + match = heading.match(line) + if match and match.group("version") == args.version: + start = index + 1 + break + + if start is None: + raise RuntimeError( + f"Could not find changelog section for version {args.version!r}" + ) + + end = len(lines) + for index in range(start, len(lines)): + if lines[index].startswith("## "): + end = index + break + + section_lines = lines[start:end] + while section_lines and not section_lines[0].strip(): + section_lines.pop(0) + while section_lines and not section_lines[-1].strip(): + section_lines.pop() + + if not section_lines: + raise RuntimeError(f"Changelog section for {args.version!r} is empty") + + note_lines = ["## Notable Changes", "", *section_lines] + sdk_core_notes = _sdk_core_release_notes(args.version, args.sdk_core_path) + if sdk_core_notes: + note_lines.extend(["", *sdk_core_notes]) + + notes = "\n".join(note_lines) + "\n" + pathlib.Path(args.output).write_text(notes, encoding="utf-8") + + +def main(argv: Sequence[str] | None = None) -> None: + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers(required=True) + + validate_parser = subparsers.add_parser("validate-version") + validate_parser.add_argument("--sha", required=True) + validate_parser.add_argument("--github-output") + validate_parser.set_defaults(func=validate_version) + + verify_parser = subparsers.add_parser("verify-dist") + verify_parser.add_argument("--version", required=True) + verify_parser.add_argument("--dist-dir", default="dist") + verify_parser.set_defaults(func=verify_dist) + + changelog_parser = subparsers.add_parser("changelog-notes") + changelog_parser.add_argument("--version", required=True) + changelog_parser.add_argument("--changelog", default="CHANGELOG.md") + changelog_parser.add_argument("--output", required=True) + changelog_parser.add_argument( + "--sdk-core-path", default="temporalio/bridge/sdk-core" + ) + changelog_parser.set_defaults(func=changelog_notes) + + args = parser.parse_args(argv) + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml deleted file mode 100644 index b7e927573..000000000 --- a/.github/workflows/build-binaries.yml +++ /dev/null @@ -1,77 +0,0 @@ -name: Build Binaries -on: - push: - branches: - - main - - "releases/*" - - fix-build-binaries - -jobs: - # Compile the binaries and upload artifacts - compile-binaries: - strategy: - fail-fast: false - matrix: - include: - - os: ubuntu-latest - package-suffix: linux-amd64 - - os: ubuntu-arm - package-suffix: linux-aarch64 - runsOn: ubuntu-24.04-arm64-2-core - - os: macos-intel - package-suffix: macos-amd64 - runsOn: macos-13 - - os: macos-arm - package-suffix: macos-aarch64 - runsOn: macos-14 - - os: windows-latest - package-suffix: windows-amd64 - runs-on: ${{ matrix.runsOn || matrix.os }} - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - # Install Rust locally for non-Linux (Linux uses an internal docker - # command to build with cibuildwheel which uses rustup install defined - # in pyproject.toml) - - if: ${{ runner.os != 'Linux' }} - uses: dtolnay/rust-toolchain@stable - - if: ${{ runner.os != 'Linux' }} - uses: Swatinem/rust-cache@v2 - with: - workspaces: temporalio/bridge -> target - - uses: astral-sh/setup-uv@v5 - - run: uv sync --all-extras - - # Add the source dist only for Linux x64 for now - - if: ${{ matrix.package-suffix == 'linux-amd64' }} - run: uv build --sdist - - # Build the wheel - - run: uv run cibuildwheel --output-dir dist - - # Install the wheel in a new venv and run a test - - name: Test wheel - shell: bash - run: | - mkdir __test_wheel__ - cd __test_wheel__ - cp -r ../tests . - python -m venv .venv - bindir=bin - if [ "$RUNNER_OS" = "Windows" ]; then - bindir=Scripts - fi - ./.venv/$bindir/pip install 'protobuf>=3.20,<6' 'types-protobuf>=3.20,<6' 'typing-extensions>=4.2.0,<5' pytest pytest_asyncio grpcio 'nexus-rpc>=1.1.0' pydantic opentelemetry-api opentelemetry-sdk python-dateutil 'openai-agents>=0.2.3,<=0.2.9' - ./.venv/$bindir/pip install --no-index --find-links=../dist temporalio - ./.venv/$bindir/python -m pytest -s -k test_workflow_hello - - # Upload dist - - uses: actions/upload-artifact@v4 - with: - name: packages-${{ matrix.package-suffix }} - path: dist diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9637f8ad..b1098dcdc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,10 @@ on: - main - "releases/*" +permissions: + contents: read + actions: read + env: COLUMNS: 120 @@ -16,63 +20,47 @@ jobs: strategy: fail-fast: false matrix: - python: ["3.9", "3.13"] - os: [ubuntu-latest, ubuntu-arm, macos-intel, macos-arm, windows-latest] + python: ["3.10", "3.14"] + os: [ubuntu-latest, ubuntu-arm, macos-arm, windows-latest] include: - # On 3.9 there is a problem with import errors caused by pytests' loader that surface due - # to a bug in CPython (https://github.com/python/cpython/issues/91351), so we avoid using - # the assert rewriter. - - python: "3.9" - pytestExtraArgs: "--assert=plain" - os: ubuntu-latest - python: "3.13" + python: "3.14" docsTarget: true - cloudTestTarget: true openaiTestTarget: true clippyLinter: true - - os: ubuntu-latest - python: "3.9" - protoCheckTarget: true + - python: "3.10" + pytestExtraArgs: '--reruns 3 --only-rerun "RuntimeError: Failed validating workflow"' - os: ubuntu-arm runsOn: ubuntu-24.04-arm64-2-core - - os: macos-intel - runsOn: macos-13 - # On 3.13.3 there is some issue with macOS intel where it hangs after pytest with some - # test that may have a worker that cannot properly shutdown, but it does not occur on - # other versions, platforms, etc. See https://github.com/temporalio/sdk-python/issues/834. - - os: macos-intel - python: "3.13" - pythonOverride: "3.13.2" - os: macos-arm runsOn: macos-latest - # On 3.13.5, python3.lib is missing for the linker - - os: windows-latest - python: "3.13" - pythonOverride: "3.13.4" runs-on: ${{ matrix.runsOn || matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: submodules: recursive - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable with: - workspaces: temporalio/bridge -> target - - uses: actions/setup-python@v5 + components: "clippy" + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: ${{ matrix.pythonOverride || matrix.python }} - - uses: arduino/setup-protoc@v3 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + with: + workspaces: temporalio/bridge -> target + key: ${{ env.pythonLocation }} + - uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3 with: # TODO(cretz): Can upgrade proto when https://github.com/arduino/setup-protoc/issues/99 fixed version: "23.x" repo-token: ${{ secrets.GITHUB_TOKEN }} - - uses: astral-sh/setup-uv@v5 + - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8 - run: uv tool install poethepoet - run: uv sync --all-extras - run: poe bridge-lint if: ${{ matrix.clippyLinter }} - - run: poe lint - run: poe build-develop + - run: poe lint - run: mkdir junit-xml - run: poe test ${{matrix.pytestExtraArgs}} -s --junit-xml=junit-xml/${{ matrix.python }}--${{ matrix.os }}.xml timeout-minutes: 15 @@ -80,46 +68,19 @@ jobs: - if: ${{ !endsWith(matrix.os, '-arm') }} run: poe test ${{matrix.pytestExtraArgs}} -s --workflow-environment time-skipping --junit-xml=junit-xml/${{ matrix.python }}--${{ matrix.os }}--time-skipping.xml timeout-minutes: 10 - # Check cloud if proper target and not on fork - - if: ${{ matrix.cloudTestTarget && (github.event.pull_request.head.repo.full_name == '' || github.event.pull_request.head.repo.full_name == 'temporalio/sdk-python') }} - run: poe test ${{matrix.pytestExtraArgs}} -s -k test_cloud_client --junit-xml=junit-xml/${{ matrix.python }}--${{ matrix.os }}--cloud.xml - timeout-minutes: 10 - env: - TEMPORAL_CLIENT_CLOUD_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }} - TEMPORAL_CLIENT_CLOUD_API_VERSION: 2024-05-13-00 - TEMPORAL_CLIENT_CLOUD_NAMESPACE: sdk-ci.a2dd6 - if: ${{ matrix.openaiTestTarget && (github.event.pull_request.head.repo.full_name == '' || github.event.pull_request.head.repo.full_name == 'temporalio/sdk-python') }} run: poe test tests/contrib/openai_agents/test_openai.py ${{matrix.pytestExtraArgs}} -s --junit-xml=junit-xml/${{ matrix.python }}--${{ matrix.os }}--openai.xml timeout-minutes: 10 env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - name: "Upload junit-xml artifacts" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 if: always() with: name: junit-xml--${{github.run_id}}--${{github.run_attempt}}--${{ matrix.python }}--${{ matrix.os }} path: junit-xml retention-days: 14 - # Confirm protos are already generated properly with older protobuf - # library and run test with that older version. We must downgrade protobuf - # so we can generate 3.x and 4.x compatible API. We have to use older - # Python to run this check because the grpcio-tools version we use - # is <= 3.10. - - name: Check generated protos and test protobuf 3.x - if: ${{ matrix.protoCheckTarget }} - env: - TEMPORAL_TEST_PROTO3: 1 - run: | - uv add --python 3.9 "protobuf<4" - uv sync --all-extras - poe build-develop - poe gen-protos - poe format - [[ -z $(git status --porcelain temporalio) ]] || (git diff temporalio; echo "Protos changed"; exit 1) - poe test -s - timeout-minutes: 10 - # Do docs stuff (only on one host) - name: Build API docs if: ${{ matrix.docsTarget }} @@ -132,48 +93,191 @@ jobs: run: npx vercel deploy build/apidocs -t ${{ secrets.VERCEL_TOKEN }} --prod --yes # Confirm README ToC is generated properly - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - name: Check generated README ToC if: ${{ matrix.docsTarget }} run: | npx doctoc README.md [[ -z $(git status --porcelain README.md) ]] || (git diff README.md; echo "README changed"; exit 1) - test-latest-deps: + + alpine-package-test: + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + include: + - cibw-build: cp310-musllinux_x86_64 + runsOn: ubuntu-latest + - cibw-build: cp310-musllinux_aarch64 + runsOn: ubuntu-24.04-arm64-2-core + runs-on: ${{ matrix.runsOn }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + submodules: recursive + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: "3.14" + - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8 + - run: uv sync --all-extras + - name: Build Alpine wheel + run: uv run cibuildwheel --output-dir dist + env: + CIBW_BUILD: ${{ matrix.cibw-build }} + - name: Test Alpine wheel + uses: ./.github/actions/alpine-package-smoke + with: + wheel-dir: dist + + check-protos: timeout-minutes: 30 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: submodules: recursive - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: "3.10" + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: workspaces: temporalio/bridge -> target - - uses: actions/setup-python@v5 + key: ${{ env.pythonLocation }} + - uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3 + with: + # TODO(cretz): Can upgrade proto when https://github.com/arduino/setup-protoc/issues/99 fixed + version: "23.x" + repo-token: ${{ secrets.GITHUB_TOKEN }} + - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8 + - run: uv tool install poethepoet + - run: uv remove google-adk --optional google-adk + - run: uv add --dev --python 3.10 "googleapis-common-protos==1.70.0" + - run: uv add --python 3.10 "protobuf<4" + - run: uv sync --all-extras + - run: poe build-develop + - run: poe gen-protos + - name: Check generation unchanged + run: | + [[ -z $(git status --porcelain temporalio tests) ]] || (git diff temporalio tests; echo "Protos changed"; exit 1) + - name: Test with protobuf 3.x + run: poe test -s --ignore=tests/contrib/google_adk_agents/ + env: + TEMPORAL_TEST_PROTO3: 1 + + test-latest-deps: + timeout-minutes: 30 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + submodules: recursive + - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable + with: + components: "clippy" + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.13" - - uses: arduino/setup-protoc@v3 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + with: + workspaces: temporalio/bridge -> target + key: ${{ env.pythonLocation }} + - uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3 with: # TODO(cretz): Can upgrade proto when https://github.com/arduino/setup-protoc/issues/99 fixed version: "23.x" repo-token: ${{ secrets.GITHUB_TOKEN }} - - uses: astral-sh/setup-uv@v5 + - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8 - run: uv tool install poethepoet - run: uv lock --upgrade - run: uv sync --all-extras - - run: poe lint - run: poe build-develop + - run: poe lint - run: mkdir junit-xml - - run: poe test -s --junit-xml=junit-xml/latest-deps.xml - timeout-minutes: 10 + - run: poe test -s --junit-xml=junit-xml/latest-deps.xml + timeout-minutes: 15 - name: "Upload junit-xml artifacts" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 if: always() with: name: junit-xml--${{github.run_id}}--${{github.run_attempt}}--latest-deps--time-skipping path: junit-xml retention-days: 14 + # Run the test suite against Temporal Cloud (skipped on forks) + cloud-test: + if: ${{ github.event.pull_request.head.repo.full_name == '' || github.event.pull_request.head.repo.full_name == 'temporalio/sdk-python' }} + timeout-minutes: 30 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + submodules: recursive + - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: "3.14" + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + with: + workspaces: temporalio/bridge -> target + key: ${{ env.pythonLocation }} + - uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3 + with: + # TODO(cretz): Can upgrade proto when https://github.com/arduino/setup-protoc/issues/99 fixed + version: "23.x" + repo-token: ${{ secrets.GITHUB_TOKEN }} + - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8 + - run: uv tool install poethepoet + - run: uv sync --all-extras + - run: poe build-develop + - name: Generate Cloud test certificates + run: | + cert_dir="$RUNNER_TEMP/cloud-test-certs" + mkdir "$cert_dir" + openssl req -x509 -newkey rsa:2048 -nodes -days 1 \ + -keyout "$cert_dir/ca.key" -out "$cert_dir/ca.pem" \ + -subj '/CN=Temporal Python SDK Cloud CI CA' + openssl req -newkey rsa:2048 -nodes \ + -keyout "$cert_dir/client.key" -out "$cert_dir/client.csr" \ + -subj '/CN=Temporal Python SDK Cloud CI' + openssl x509 -req -days 1 -in "$cert_dir/client.csr" \ + -CA "$cert_dir/ca.pem" -CAkey "$cert_dir/ca.key" -CAcreateserial \ + -out "$cert_dir/client.pem" -extfile <(printf 'extendedKeyUsage=clientAuth') + { + echo "TEMPORAL_CLOUD_CLIENT_CA_PATH=$cert_dir/ca.pem" + echo "TEMPORAL_TLS_CLIENT_CERT_PATH=$cert_dir/client.pem" + echo "TEMPORAL_TLS_CLIENT_KEY_PATH=$cert_dir/client.key" + } >> "$GITHUB_ENV" + - name: Create Cloud namespace + id: create-cloud-namespace + run: uv run python .github/scripts/cloud_namespace.py create + env: + TEMPORAL_CLIENT_CLOUD_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }} + TEMPORAL_CLIENT_CLOUD_API_VERSION: v0.19.1 + - run: mkdir junit-xml + - run: poe test -s --workflow-environment envconfig --junit-xml=junit-xml/cloud.xml + timeout-minutes: 15 + env: + TEMPORAL_ADDRESS: ${{ steps.create-cloud-namespace.outputs.namespace }}.tmprl.cloud:7233 + TEMPORAL_NAMESPACE: ${{ steps.create-cloud-namespace.outputs.namespace }} + TEMPORAL_IS_CLOUD_TESTS: true + TEMPORAL_CLIENT_CLOUD_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }} + TEMPORAL_CLIENT_CLOUD_API_VERSION: v0.19.1 + TEMPORAL_CLIENT_CLOUD_NAMESPACE: ${{ steps.create-cloud-namespace.outputs.namespace }} + - name: Delete Cloud namespace + if: ${{ always() && steps.create-cloud-namespace.outputs.namespace != '' }} + run: uv run python .github/scripts/cloud_namespace.py delete "${{ steps.create-cloud-namespace.outputs.namespace }}" + env: + TEMPORAL_CLIENT_CLOUD_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }} + TEMPORAL_CLIENT_CLOUD_API_VERSION: v0.19.1 + - name: "Upload junit-xml artifacts" + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + if: always() + with: + name: junit-xml--${{github.run_id}}--${{github.run_attempt}}--cloud + path: junit-xml + retention-days: 14 + # Runs the sdk features repo tests with this repo's current SDK code features-tests: uses: temporalio/features/.github/workflows/python.yaml@main diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 7e1e30d68..505fd507d 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -5,6 +5,9 @@ on: # (12 AM PST) - cron: "00 07 * * *" +permissions: + contents: read + jobs: nightly: uses: ./.github/workflows/run-bench.yml diff --git a/.github/workflows/omes.yml b/.github/workflows/omes.yml deleted file mode 100644 index 6b1287739..000000000 --- a/.github/workflows/omes.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Omes Testing -on: - push: - branches: - - main - - "releases/*" - -jobs: - omes-image-build: - uses: temporalio/omes/.github/workflows/docker-images.yml@main - secrets: inherit - with: - lang: python - sdk-repo-url: ${{ github.event.pull_request.head.repo.full_name || 'temporalio/sdk-python' }} - sdk-repo-ref: ${{ github.event.pull_request.head.ref || github.ref }} - # TODO: Remove once we have a good way of cleaning up sha-based pushed images - docker-tag-ext: ci-latest - do-push: true diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml new file mode 100644 index 000000000..d745804a8 --- /dev/null +++ b/.github/workflows/release-publish.yml @@ -0,0 +1,356 @@ +name: Release Publish +run-name: Release from main + +on: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: release-publish-${{ github.ref }} + cancel-in-progress: false + +jobs: + build_binaries: + name: Build binaries (${{ matrix.package-suffix }}) + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + package-suffix: linux-amd64 + cibw-build: cp310-manylinux_x86_64 + - os: ubuntu-arm + package-suffix: linux-aarch64 + cibw-build: cp310-manylinux_aarch64 + runsOn: ubuntu-24.04-arm64-2-core + - os: ubuntu-latest + package-suffix: linux-musl-amd64 + cibw-build: cp310-musllinux_x86_64 + - os: ubuntu-arm + package-suffix: linux-musl-aarch64 + cibw-build: cp310-musllinux_aarch64 + runsOn: ubuntu-24.04-arm64-2-core + - os: macos-intel + package-suffix: macos-amd64 + cibw-build: cp310-macosx_x86_64 + runsOn: macos-15-intel + - os: macos-arm + package-suffix: macos-aarch64 + cibw-build: cp310-macosx_arm64 + runsOn: macos-14 + - os: windows-latest + package-suffix: windows-amd64 + cibw-build: cp310-win_amd64 + runs-on: ${{ matrix.runsOn || matrix.os }} + permissions: + contents: read + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.sha }} + submodules: recursive + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: "3.14" + + # Install Rust locally for non-Linux (Linux uses an internal docker + # command to build with cibuildwheel which uses rustup install defined + # in pyproject.toml) + - if: ${{ runner.os != 'Linux' }} + uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable + - if: ${{ runner.os != 'Linux' }} + uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + with: + cache-bin: false + workspaces: temporalio/bridge -> target + key: ${{ env.pythonLocation }} + - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8 + - run: uv sync --all-extras + + # Add the source dist only for Linux x64 for now + - if: ${{ matrix.package-suffix == 'linux-amd64' }} + run: uv build --sdist + + # Build the wheel + - run: uv run cibuildwheel --output-dir dist + env: + CIBW_BUILD: ${{ matrix.cibw-build }} + + # Install the wheel in a new venv and run a test + - name: Test wheel + if: ${{ !contains(matrix.package-suffix, 'musl') }} + shell: bash + run: | + mkdir __test_wheel__ + cd __test_wheel__ + cp -r ../tests . + python -m venv .venv + bindir=bin + if [ "$RUNNER_OS" = "Windows" ]; then + bindir=Scripts + fi + ./.venv/$bindir/pip install pytest pytest_asyncio grpcio pydantic opentelemetry-api opentelemetry-sdk + ./.venv/$bindir/pip install --prefer-binary ../dist/*.whl + ./.venv/$bindir/python -m pytest -s tests/worker/test_workflow.py -k test_workflow_hello + + - name: Test Alpine wheel + if: ${{ contains(matrix.package-suffix, 'musl') }} + uses: ./.github/actions/alpine-package-smoke + with: + wheel-dir: dist + + # Upload dist + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: packages-${{ matrix.package-suffix }} + path: dist + + verify_artifacts: + name: Verify release artifacts + needs: build_binaries + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + actions: read + contents: read + outputs: + release_sha: ${{ steps.validate_versions.outputs.sha }} + version: ${{ steps.validate_versions.outputs.version }} + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.sha }} + fetch-depth: 0 + submodules: recursive + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: "3.11" + - name: Validate checked-in versions + id: validate_versions + run: | + set -euo pipefail + python .github/scripts/release_verify.py validate-version \ + --sha "$(git rev-parse HEAD)" \ + --github-output "$GITHUB_OUTPUT" + - name: Validate changelog release notes + run: | + set -euo pipefail + python .github/scripts/release_verify.py changelog-notes \ + --version "${{ steps.validate_versions.outputs.version }}" \ + --output /tmp/release-notes.md + - name: Download and flatten artifacts + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + mkdir artifacts dist + gh run download "$GITHUB_RUN_ID" \ + --repo "$GITHUB_REPOSITORY" \ + --dir artifacts \ + --pattern 'packages-*' + + while IFS= read -r -d '' file; do + dest="dist/$(basename "$file")" + if [[ -e "$dest" ]]; then + echo "Duplicate distribution filename: $(basename "$file")" >&2 + exit 1 + fi + cp "$file" "$dest" + done < <(find artifacts -type f \( -name '*.whl' -o -name '*.tar.gz' \) -print0) + - name: Verify release artifacts + env: + VERSION: ${{ steps.validate_versions.outputs.version }} + run: | + set -euo pipefail + python .github/scripts/release_verify.py verify-dist --version "$VERSION" + - name: Upload verified release artifact + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: release-dist-${{ steps.validate_versions.outputs.version }} + path: dist + if-no-files-found: error + retention-days: 14 + + publish_testpypi: + name: Publish to TestPyPI + needs: verify_artifacts + runs-on: ubuntu-latest + timeout-minutes: 10 + environment: testpypi + permissions: + actions: read + contents: read + id-token: write + steps: + - name: Download verified release artifact + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ needs.verify_artifacts.outputs.version }} + run: | + set -euo pipefail + mkdir downloaded dist + gh run download "$GITHUB_RUN_ID" \ + --repo "$GITHUB_REPOSITORY" \ + --name "release-dist-$VERSION" \ + --dir downloaded + while IFS= read -r -d '' file; do + cp "$file" "dist/$(basename "$file")" + done < <(find downloaded -type f \( -name '*.whl' -o -name '*.tar.gz' \) -print0) + test "$(find dist -type f | wc -l)" -gt 0 + - name: Publish to TestPyPI + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1 + with: + packages-dir: dist/ + repository-url: https://test.pypi.org/legacy/ + skip-existing: true + + smoke_testpypi: + name: Smoke test TestPyPI package + needs: + - verify_artifacts + - publish_testpypi + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: ./.github/actions/release-smoke-package + with: + version: ${{ needs.verify_artifacts.outputs.version }} + index-url: https://test.pypi.org/simple/ + dependency-index-url: https://pypi.org/simple/ + + smoke_testpypi_alpine: + name: Smoke test TestPyPI package on Alpine (${{ matrix.arch }}) + needs: + - verify_artifacts + - publish_testpypi + strategy: + fail-fast: false + matrix: + include: + - arch: x64 + runsOn: ubuntu-latest + - arch: arm64 + runsOn: ubuntu-24.04-arm64-2-core + runs-on: ${{ matrix.runsOn }} + timeout-minutes: 10 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - name: Install and load package + uses: ./.github/actions/alpine-package-smoke + with: + version: ${{ needs.verify_artifacts.outputs.version }} + index-url: https://test.pypi.org/simple/ + dependency-index-url: https://pypi.org/simple/ + + publish_pypi: + name: Publish to PyPI + needs: + - verify_artifacts + - smoke_testpypi + - smoke_testpypi_alpine + runs-on: ubuntu-latest + timeout-minutes: 10 + environment: pypi + permissions: + actions: read + contents: read + id-token: write + steps: + - name: Download verified release artifact + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ needs.verify_artifacts.outputs.version }} + run: | + set -euo pipefail + mkdir downloaded dist + gh run download "$GITHUB_RUN_ID" \ + --repo "$GITHUB_REPOSITORY" \ + --name "release-dist-$VERSION" \ + --dir downloaded + while IFS= read -r -d '' file; do + cp "$file" "dist/$(basename "$file")" + done < <(find downloaded -type f \( -name '*.whl' -o -name '*.tar.gz' \) -print0) + test "$(find dist -type f | wc -l)" -gt 0 + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1 + with: + packages-dir: dist/ + + smoke_pypi: + name: Smoke test PyPI package + needs: + - verify_artifacts + - publish_pypi + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: ./.github/actions/release-smoke-package + with: + version: ${{ needs.verify_artifacts.outputs.version }} + index-url: https://pypi.org/simple/ + + smoke_pypi_alpine: + name: Smoke test PyPI package on Alpine (${{ matrix.arch }}) + needs: + - verify_artifacts + - publish_pypi + strategy: + fail-fast: false + matrix: + include: + - arch: x64 + runsOn: ubuntu-latest + - arch: arm64 + runsOn: ubuntu-24.04-arm64-2-core + runs-on: ${{ matrix.runsOn }} + timeout-minutes: 10 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - name: Install and load package + uses: ./.github/actions/alpine-package-smoke + with: + version: ${{ needs.verify_artifacts.outputs.version }} + index-url: https://pypi.org/simple/ + + create_draft_release: + name: Create draft GitHub Release + needs: + - verify_artifacts + - smoke_pypi + - smoke_pypi_alpine + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: write + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ needs.verify_artifacts.outputs.release_sha }} + fetch-depth: 0 + submodules: recursive + - name: Build changelog release notes + env: + VERSION: ${{ needs.verify_artifacts.outputs.version }} + run: | + set -euo pipefail + python3 .github/scripts/release_verify.py changelog-notes \ + --version "$VERSION" \ + --output release-notes.md + - name: Create draft release with generated notes + env: + GH_TOKEN: ${{ github.token }} + RELEASE_SHA: ${{ needs.verify_artifacts.outputs.release_sha }} + VERSION: ${{ needs.verify_artifacts.outputs.version }} + run: | + set -euo pipefail + gh release create "$VERSION" \ + --repo "$GITHUB_REPOSITORY" \ + --target "$RELEASE_SHA" \ + --title "$VERSION" \ + --draft \ + --notes-file release-notes.md \ + --generate-notes diff --git a/.github/workflows/run-bench.yml b/.github/workflows/run-bench.yml index 6dcad63e8..26e0b4759 100644 --- a/.github/workflows/run-bench.yml +++ b/.github/workflows/run-bench.yml @@ -18,6 +18,9 @@ on: - "--sandbox" - "--no-sandbox" +permissions: + contents: read + jobs: run-bench: strategy: @@ -26,25 +29,27 @@ jobs: runs-on: ${{ matrix.os }} steps: # Prepare - - uses: actions/checkout@v4 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: submodules: recursive - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable with: toolchain: stable - - uses: Swatinem/rust-cache@v2 - with: - workspaces: temporalio/bridge -> target - - uses: actions/setup-python@v5 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.13" - - uses: arduino/setup-protoc@v3 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + with: + cache-bin: false + workspaces: temporalio/bridge -> target + key: ${{ env.pythonLocation }} + - uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3 with: # TODO(cretz): Can upgrade proto when https://github.com/arduino/setup-protoc/issues/99 fixed version: "23.x" repo-token: ${{ secrets.GITHUB_TOKEN }} - - uses: astral-sh/setup-uv@v5 + - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8 # Build - run: uv tool install poethepoet - run: uv sync --all-extras @@ -68,4 +73,4 @@ jobs: - run: poe run-bench --workflow-count 10000 --max-cached-workflows 10000 --max-concurrent 10000 ${{ inputs.sandbox-arg }} - run: poe run-bench --workflow-count 10000 --max-cached-workflows 1000 --max-concurrent 1000 ${{ inputs.sandbox-arg }} - - run: poe run-bench --workflow-count 10000 --max-cached-workflows 1000 --max-concurrent 1000 ${{ inputs.sandbox-arg }} \ No newline at end of file + - run: poe run-bench --workflow-count 10000 --max-cached-workflows 1000 --max-concurrent 1000 ${{ inputs.sandbox-arg }} diff --git a/.gitignore b/.gitignore index c31f84940..8cd439e05 100644 --- a/.gitignore +++ b/.gitignore @@ -2,11 +2,16 @@ __pycache__ /build /dist +temporalio/bridge/libtemporal_sdk_bridge.dylib.dSYM/ temporalio/bridge/target/ temporalio/bridge/temporal_sdk_bridge* /tests/helpers/golangserver/golangserver /tests/helpers/golangworker/golangworker /.idea /sdk-python.iml +**/CLAUDE.md /.zed *.DS_Store +tags +/.claude +tmpclaude-* diff --git a/.gitmodules b/.gitmodules index ba2ba964a..a4c911631 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "sdk-core"] path = temporalio/bridge/sdk-core - url = https://github.com/temporalio/sdk-core.git + url = https://github.com/temporalio/sdk-rust.git diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..7ecbb40d5 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,117 @@ +# Contributor Guidance for `sdk-python` + +This repository provides the Temporal Python SDK, including Python packages, +tests, optional integrations, and the Rust bridge used by the SDK. Use this +document as a quick reference when submitting pull requests. + +## Requirements for coding agents + +* Prefer the repo's Poe tasks over invoking underlying tools directly. Use + `poe test`, `poe lint`, `poe format`, `poe build-develop`, and + `poe bridge-lint` unless there is a specific reason to run a lower-level + command. +* If you are about to run tests, you do not need to run a build separately first + unless Rust bridge changes need a fresh editable extension. For bridge changes, + run `poe build-develop` before Python tests that import `temporalio`. +* Use targeted tests while iterating. `poe test -s -k ` is preferred for + a small behavioral change; run broader tests only when the change affects + shared behavior. +* Do not use `--log-cli-level` by default. The pytest configuration shows logs + for failed tests at the end without streaming all logs for passing tests. +* Tests that use the workflow environment may start a local Temporal dev server + and may download a test server binary on first run. Unit tests that do not use + the workflow environment do not start a server. +* Time-skipping tests are run with `poe test -s --workflow-environment + time-skipping`. Time-skipping does not work on Linux ARM or Windows ARM. +* It is extremely important that comments explain why something is necessary, + not what the code already says. Avoid comments unless they clarify nonobvious + behavior. +* Avoid broad refactors, style churn, or unrelated cleanups in behavior changes. +* Avoid unqualified imports from `temporalio` packages except `temporalio.types`. + Relative imports are acceptable for private packages. +* Do not commit `uv.lock` or `pyproject.toml` changes created only for temporary + protobuf downgrade workflows. Prefer to use poe gen-protos-docker when possible. + +## Repo Specific Utilities + +* Poe tasks are defined in `pyproject.toml`: + * `poe build-develop` - build the Rust extension in editable debug mode. + * `poe test` - run pytest in parallel with the default workflow environment. + * `poe lint` - run import checks, formatting checks, type checks, and + docstyle. + * `poe lint-types` - run pyright, mypy, and basedpyright. + * `poe bridge-lint` - run clippy for the Rust bridge. + * `poe format` - run Ruff import sorting, Ruff formatting, and `cargo fmt` for + the bridge. + * `poe gen-protos-docker` - regenerate protobuf-related files using Docker. + * `poe gen-protos` - regenerate protobuf-related files without Docker, with + the Python/protobuf constraints documented in `README.md`. + +## Building and Testing + +The common local commands are: + +```bash +uv sync --all-extras +poe build-develop +poe lint +poe test +poe test -s --workflow-environment time-skipping +``` + +For focused iteration, prefer: + +```bash +poe test -s -k +uv run pytest tests/path/test_file.py::test_name +``` + +For release artifacts, use `uv build`. Documentation can be generated with +`poe gen-docs`. + +## Expectations for Pull Requests + +* Format and lint code before submitting. +* Include tests for behavior changes. +* Update public API documentation or doc comments for public behavior changes. +* Add a high-level changelog entry for user-facing changes according to the + existing `CHANGELOG.md` convention. +* Keep commit messages short and in the imperative mood. +* Provide a clear PR description outlining what changed, why it changed, and + what validation was run. + +## Review Checklist + +Reviewers will look for: + +* CI passing, including build, lint, type checks, unit tests, and workflow + environment tests. +* Tests covering behavior changes. +* Clear and concise code following existing style. +* Public API documentation updates when behavior changes. +* No unrelated generated files, lockfile churn, or broad rewrites. + +## Where Things Are + +* `temporalio/` - Python SDK source. + * `temporalio/worker/` - worker implementation. + * `temporalio/converter/` - payload and failure conversion. + * `temporalio/testing/` - testing utilities. + * `temporalio/nexus/` - Nexus support. + * `temporalio/contrib/` - optional integrations. + * `temporalio/bridge/` - Rust bridge and generated bridge bindings. +* `tests/` - pytest suites mirroring SDK areas. +* `scripts/` - generation, documentation, and helper scripts. +* `build/apidocs/` - generated API documentation. +* `dist/` - built wheels and source distributions. +* `temporalio/bridge/target/` - Rust build output. You should not need to inspect + this directory. + +## Notes + +* The SDK supports Python 3.10 and newer. +* Generated protobuf and bridge files have specific regeneration workflows; see + `README.md` before changing them. +* The Rust bridge uses SDK Core from `temporalio/bridge/sdk-core`. +* `__pycache__`, `build`, `dist`, and Rust `target` outputs are generated + artifacts and should not be reviewed as source changes. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..f5e22ce98 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,190 @@ + + +# Changelog + +## [Unreleased] + +### Added + +- Added the `Runtime(disable_environment_info=...)` option to control whether + runtime, hosting, and platform information is included in worker heartbeats. + +- `temporalio.workflow.uuid7()` generates a determinism-safe, time-sortable + UUIDv7 (RFC 9562) from workflow time and the workflow's deterministic random + generator, complementing the existing `workflow.uuid4()` + ([#1450](https://github.com/temporalio/sdk-python/issues/1450)). The + workflow sandbox now also restricts the non-deterministic `uuid.uuid7()` + added to the standard library in Python 3.14, matching the existing + `uuid.uuid1()`/`uuid.uuid4()` restrictions. +- **Experimental**: `TemporalOperationHandler` can now use Standalone Activities as asynchronous + Nexus Operation backing executions through `TemporalNexusClient.start_activity`. + +### Changed + +- `temporalio.contrib.pydantic` converters now reuse Pydantic type adapters + for repeated type hints instead of rebuilding their schemas for every + payload, greatly speeding up decode of non-model hints such as discriminated + unions ([#1695](https://github.com/temporalio/sdk-python/issues/1695)). Up + to 1024 type adapters are cached per converter instance by default, with + least-recently-used eviction. To change the bound, pass + ``max_cached_type_adapters`` to ``PydanticPayloadConverter`` (or + ``PydanticJSONPlainPayloadConverter``) from a nullary subclass used as the + ``DataConverter.payload_converter_class``; ``None`` makes the cache + unbounded and zero disables caching. + +### Deprecated + +### :boom: Breaking Changes + +### Fixed + +- The `google-adk` extra now depends on `mcp`, so fresh installs of + `temporalio[google-adk]` can import `temporalio.contrib.google_adk_agents` + without separately installing `mcp`. Previously the import failed with an + `ImportError` because `google.adk.tools.mcp_tool` only exports `McpToolset` + when `mcp` is installed. +- `temporalio.contrib.openai_agents` no longer crashes when a plain `dict` + is passed for `run_config`. (openai-agents >= 0.19.0 accepts `dict` run + configs at its public runner API) + +### Security + +## [1.31.0] - 2026-07-29 + +### Added + +- Added the `Worker` `max_eager_activity_reservations_per_workflow_task` option for configuring + the number of activity slots reserved for eager execution per workflow task. Configured values + must be positive; use `disable_eager_activity_execution` to disable eager activity execution. +- Added experimental SDK payload converter support for values and type hints + decorated with `@transfer_type_convertible(...)` using a `TransferTypeConverter` class. + This lets types with transfer type converters delegate their wire representation to the + configured payload converter, preserving SDK behavior such as serialization + contexts. +- Added `TLSConfig.verification_server_name` to verify the server certificate against a fixed name + instead of the connection's server name. Unlike `domain`, it does not change the TLS SNI or + HTTP/2 authority values, which keep following the connected host, so it can be used when the + server's certificate does not carry the dialed name but on-path infrastructure (e.g. an + SNI-inspecting egress proxy) needs the SNI to remain resolvable. Requires + `server_root_ca_cert`. + +- Added the experimental `Worker` `patch_activation_callback` option, allowing workers + to decide whether a first non-replay `workflow.patched` call should activate a patch + during rolling deployments. +- Added external storage support to Nexus task handling. + +### Changed + +- Prepared replay-safe workflow activation scheduling that prevents cancellation + from being lost when another event becomes ready in the same workflow task. The + behavior is guarded by internal workflow logic flag 2 and remains disabled by + default during its compatibility rollout. + **Maintainer reminder:** keep flag 2 default-disabled for the first two published + SDK releases that recognize it; enable it in the third release, remove the explicit + overrides for this flag from `tests/worker/test_workflow.py`, and replace this rollout + note with a `Fixed` entry announcing the behavior change. + +### :boom: Breaking Changes + +- Custom workflow runners that construct `WorkflowInstanceDetails` must now pass + `payload_converter_factory` instead of `payload_converter_class`. The factory + returns the already wrapped payload converter that workflow instances should + use. +- System Nexus payload converter helpers added for generated bindings are now + private implementation details, and the remaining public `temporalio.nexus.system` + APIs are marked experimental and subject to change. +- Payload size limits have moved from `DataConverter` to `Client.connect`. Pass + `payload_limits=PayloadLimitsConfig(...)` (now exported from + `temporalio.client`) instead of setting `payload_limits` on `DataConverter`. + Config fields were renamed to `payloads_warn_size` and `memo_warn_size`, and + the deprecated `PayloadSizeWarning` was removed. + +### Fixed + +- Marked system Nexus envelope payloads so nested payloads can be detected and + visited after the envelope is already stored as a payload. + +## [1.30.0] - 2026-07-01 + +### Added + +- Nexus operation link propagation for signals. When a Nexus operation handler signals a workflow + (including signal-with-start), the inbound Nexus request links are now forwarded onto the signaled + workflow so its history events link back to the caller, and the link the server returns for the + signaled event is attached to the caller workflow's Nexus operation history event. This makes the + caller and callee mutually navigable in the UI for signal-based Nexus operations. +- Exposed `backoff_start_interval` for continue-as-new, to allow the new workflow to start after a delay. + +### Changed + +- AWS Lambda worker `configure` parameter supports sync, async, and async + generator style functions. This callback is invoked on the asyncio event + loop. +- Relaxed the protobuf dependency bounds to allow protobuf 7 where compatible + with the selected optional dependencies. +- Standalone Nexus operation links are now forwarded on start workflow and signal requests. + +### :boom: Breaking Changes + +- AWS Lambda worker `configure` parameter has been changed to be invoked + per-invocation of the worker instead of only at startup. It is advised that + any shared, heavy-weight operations are performed outside of the callback + before `run_worker` is invoked. + +## [1.29.0] - 2026-06-17 + +### Added + +- Added experimental `temporalio.workflow.signal_with_start_workflow`, backed by + generated system Nexus bindings for + `WorkflowService.SignalWithStartWorkflowExecution`. +- Added OpenAI Agents plugin support for `CustomTool` dispatch, including lazy + tool discovery through `defer_loading`. + +### Changed + +- Client connections now use gzip transport-level gRPC compression by default. + Pass `grpc_compression=GrpcCompression.NONE` to `Client.connect` or + `CloudOperationsClient.connect` to disable it. + +### :boom: Breaking Changes + +- `StartWorkflowUpdateWithStartInput` now owns the authoritative + `rpc_metadata` and `rpc_timeout` fields for + `OutboundInterceptor.start_update_with_start_workflow`. These fields were + removed from the nested update-with-start input objects, so custom + interceptors that accessed them there should read or update the top-level + fields instead. + +### Fixed + +- Fixed `breakpoint()` and `pdb.set_trace()` inside workflow code when a worker + runs with `debug_mode=True` or `TEMPORAL_DEBUG=1`; sandboxed workflows without + debug mode now get a clearer error pointing to `debug_mode=True`. +- Fixed `start_update_with_start_workflow` interceptor handling so RPC metadata + and timeouts are forwarded to the underlying `execute_multi_operation` call. +- Fixed OpenAI Agents plugin streamed event serialization when pydantic had not + yet built deferred schemas, and fixed terminal sandbox errors retrying + forever. +- Removed the lazy-connect lock from the per-RPC hot path. It was previously + acquired on every RPC, putting an event-loop-bound primitive on the hot path; + it is now skipped once the client is connected. This reduces the client's + coupling to the event loop it connected on, which can help when reusing a + single long-lived `Client` across event loops or threads (e.g. the + dedicated-loop pattern used with gevent/gunicorn and synchronous services). + Note this does not make a `Client` fully thread- or loop-agnostic; reusing one + long-lived loop is still the recommended pattern. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..057901ff5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,128 @@ +# Contributing to Temporal SDKs + +Thanks for your interest in contributing to Temporal SDKs. + +This guide describes expectations that apply across Temporal SDK repositories. Each +repository may have additional local conventions, but the guidance below should help +you open issues and pull requests that maintainers can evaluate efficiently. + +## Before You Open an Issue + +Search the existing issues first. If you find an issue that describes the same bug, +feature request, or design topic, add any relevant details there instead of opening a +duplicate. Use an upvote on the issue to show that it affects you too. + +Issues are assigned to people when they are actively working on them. Before taking +on an issue, check whether it is already assigned so you do not duplicate someone +else's work. + +Use GitHub issues for actionable bugs and feature work. For usage questions, help +debugging an application, or general discussion, join the relevant +language-specific channel in the +[Temporal community Slack](https://temporal.io/slack) or use the support channel +available to you. + +## Bug Reports + +When reporting a bug, include enough detail for someone else to reproduce or +understand the problem: + +* A short summary of the problem. +* A minimal reproduction, preferably as code that can be copied into a small + project or test. +* What you expected to happen and what actually happened. +* The SDK version. +* The language runtime version. +* The operating system and architecture. +* Temporal Server or Temporal Cloud details, if the issue depends on service + behavior. +* Logs, stack traces, workflow histories, or other diagnostics that show the + failure. +* Whether the behavior is a regression, and the last version where it worked if + known. + +## Feature Requests and Design Changes + +Open or join a GitHub issue before starting substantial feature work, behavior +changes, or API design changes. This gives maintainers and other SDK users a chance +to discuss the approach before you invest in a larger implementation. + +The relevant language-specific channel in Temporal community Slack is also a good +place for early discussion, but important decisions should still be captured in a +GitHub issue so they are visible and searchable. + +Small bug fixes, documentation fixes, and narrowly scoped maintenance changes can go +straight to a pull request. + +## Pull Requests + +Good pull requests are focused and easy to review: + +* Keep each pull request scoped to one logical change. +* Include tests for behavior changes. +* Update public API documentation or doc comments when public behavior changes. +* Add a high-level changelog entry for user-facing changes according to the + repository's local changelog convention. +* Describe what changed, why it changed, and what validation you ran. + +Run the relevant local checks when practical. CI must pass before a pull request can +be merged. + +## Things to Avoid + +Avoid changes that make review harder without improving the contribution: + +* Unrelated refactors mixed into a behavior change. +* Style-only churn. +* Large feature pull requests that were not discussed first. +* License, copyright, or other legal changes without maintainer discussion. + +## AI-Generated Contributions + +Using AI tools while contributing is acceptable. You are responsible for the +correctness, quality, and maintainability of everything you submit. + +Contributors must fully understand the issue they are fixing and be able to explain +the proposed change. We expect that human understanding to be evident in pull +request responses and design discussions. If a contribution's interaction appears +entirely AI-driven, maintainers may close it: it does not provide a benefit over +maintainers using AI tooling themselves. + +Thoroughly self-review AI-generated code and documentation before opening a pull +request. Make sure it is correct, tested where appropriate, and consistent with the +style and patterns of the codebase. + +Keep AI-assisted changes concise and scoped. Avoid verbose generated prose, +unnecessary comments, or broad rewrites that make the change harder to review. + +## Contributor License Agreement + +All contributors must complete the Temporal Contributor License Agreement (CLA) +before changes can be merged. A link to the CLA will be posted in the pull request. + +## Security Issues + +Do not open public GitHub issues for suspected security vulnerabilities. Report them +to security@temporal.io instead. + +## Review and CI + +Maintainers review pull requests for correctness, compatibility, test coverage, +documentation, and long-term maintainability. Review may require changes before a +pull request can be merged, and it may take maintainers some time to review a +contribution. + +CI is the final validation gate. If CI fails, update the pull request or ask for help +if the failure appears unrelated to your change. Some CI gates may wait for a +maintainer to approve or run them. + +## Inactive Pull Requests + +Maintainers may close inactive pull requests after follow-up if they are no longer +moving forward. If that happens, you are welcome to reopen the pull request or open a +new one when you are ready to continue. + +## Community Conduct + +Keep discussions respectful, constructive, and focused on the work. Clear context, +specific examples, and patience with review feedback help everyone move faster. diff --git a/README.md b/README.md index 58cdeb055..bd3ac195c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -![Temporal Python SDK](https://assets.temporal.io/w/py-banner.svg) +![Temporal Python SDK](https://assets.temporal.io/w/py.png) -[![Python 3.9+](https://img.shields.io/pypi/pyversions/temporalio.svg?style=for-the-badge)](https://pypi.org/project/temporalio) +[![Python 3.10+](https://img.shields.io/pypi/pyversions/temporalio.svg?style=for-the-badge)](https://pypi.org/project/temporalio) [![PyPI](https://img.shields.io/pypi/v/temporalio.svg?style=for-the-badge)](https://pypi.org/project/temporalio) [![MIT](https://img.shields.io/pypi/l/temporalio.svg?style=for-the-badge)](LICENSE) @@ -54,6 +54,10 @@ informal introduction to the features and their implementation. - [Data Conversion](#data-conversion) - [Pydantic Support](#pydantic-support) - [Custom Type Data Conversion](#custom-type-data-conversion) + - [External Storage](#external-storage) + - [Driver Selection](#driver-selection) + - [Built-in Drivers](#built-in-drivers) + - [Custom Drivers](#custom-drivers) - [Workers](#workers) - [Workflows](#workflows) - [Definition](#definition) @@ -78,6 +82,7 @@ informal introduction to the features and their implementation. - [Customizing the Sandbox](#customizing-the-sandbox) - [Passthrough Modules](#passthrough-modules) - [Invalid Module Members](#invalid-module-members) + - [Debugging Workflows with `breakpoint()` / `pdb`](#debugging-workflows-with-breakpoint--pdb) - [Known Sandbox Issues](#known-sandbox-issues) - [Global Import/Builtins](#global-importbuiltins) - [Sandbox is not Secure](#sandbox-is-not-secure) @@ -99,8 +104,11 @@ informal introduction to the features and their implementation. - [Interceptors](#interceptors) - [Nexus](#nexus) - [Plugins](#plugins) - - [Client Plugins](#client-plugins) - - [Worker Plugins](#worker-plugins) + - [Usage](#usage-1) + - [Plugin Implementations](#plugin-implementations) + - [Advanced Plugin Implementations](#advanced-plugin-implementations) + - [Client Plugins](#client-plugins) + - [Worker Plugins](#worker-plugins) - [Workflow Replay](#workflow-replay) - [Observability](#observability) - [Metrics](#metrics) @@ -161,7 +169,7 @@ from temporalio import workflow # Import our activity, passing it through the sandbox with workflow.unsafe.imports_passed_through(): - from .activities import say_hello + from activities import say_hello @workflow.defn class SayHello: @@ -181,8 +189,8 @@ from temporalio.client import Client from temporalio.worker import Worker # Import the activity and workflow from our other files -from .activities import say_hello -from .workflows import SayHello +from activities import say_hello +from workflows import SayHello async def main(): # Create client connected to server at the given address @@ -217,7 +225,7 @@ import asyncio from temporalio.client import Client # Import the workflow from the previous code -from .workflows import SayHello +from workflows import SayHello async def main(): # Create client connected to server at the given address @@ -306,8 +314,9 @@ other_ns_client = Client(**config) Data converters are used to convert raw Temporal payloads to/from actual Python types. A custom data converter of type `temporalio.converter.DataConverter` can be set via the `data_converter` parameter of the `Client` constructor. Data -converters are a combination of payload converters, payload codecs, and failure converters. Payload converters convert -Python values to/from serialized bytes. Payload codecs convert bytes to bytes (e.g. for compression or encryption). +converters are a combination of payload converters, external storage, payload codecs, and failure converters. Payload +converters convert Python values to/from serialized bytes. External payload storage optionally stores and retrieves payloads +to/from external storage services using drivers. Payload codecs convert bytes to bytes (e.g. for compression or encryption). Failure converters convert exceptions to/from serialized failures. The default data converter supports converting multiple types including: @@ -424,7 +433,7 @@ class IPv4AddressJSONEncoder(AdvancedJSONEncoder): class IPv4AddressJSONTypeConverter(JSONTypeConverter): def to_typed_value( self, hint: Type, value: Any - ) -> Union[Optional[Any], _JSONTypeConverterUnhandled]: + ) -> Union[Optional[Any], JSONTypeConverterUnhandled]: if issubclass(hint, ipaddress.IPv4Address): return ipaddress.IPv4Address(value) return JSONTypeConverter.Unhandled @@ -452,6 +461,145 @@ my_data_converter = dataclasses.replace( Now `IPv4Address` can be used in type hints including collections, optionals, etc. +##### External Storage + +⚠️ **External storage support is currently at an experimental release stage.** ⚠️ + +External storage allows large payloads to be offloaded to an external storage service (such as Amazon S3) rather than stored inline in workflow history. This is useful when workflows or activities work with data that would otherwise exceed Temporal's payload size limits. + +External storage is configured via the `external_storage` parameter on `DataConverter`. It should be configured on the `Client` both for clients of your workflow as well as on the worker -- anywhere large payloads may be uploaded or downloaded. + +A `StorageDriver` handles uploading and downloading payloads. Temporal provides [built-in drivers](#built-in-drivers) for common storage solutions, or you may implement a [custom driver](#custom-drivers). Here's an example using the built-in `S3StorageDriver` with the SDK's `aioboto3` client: + +```python +import aioboto3 +import dataclasses +from temporalio.client import Client, ClientConfig +from temporalio.contrib.aws.s3driver import S3StorageDriver +from temporalio.contrib.aws.s3driver.aioboto3 import new_aioboto3_client +from temporalio.converter import DataConverter +from temporalio.converter import ExternalStorage + +client_config = ClientConfig.load_client_connect_config() + +session = aioboto3.Session() +async with session.client("s3") as s3_client: + driver = S3StorageDriver( + client=new_aioboto3_client(s3_client), + bucket="my-bucket", + ) + client = await Client.connect( + **client_config, + data_converter=dataclasses.replace( + DataConverter.default, + external_storage=ExternalStorage(drivers=[driver]), + ), + ) +``` + +See the [S3 driver README](temporalio/contrib/aws/s3driver/) for further details. + +Some things to note about external storage: + +* Only payloads that meet or exceed `ExternalStorage.payload_size_threshold` (default 256 KiB) are offloaded. Smaller payloads are stored inline as normal. +* External storage applies transparently to all payloads, whether they are workflow inputs/outputs, activity inputs/outputs, signal inputs, query outputs, update inputs/outputs, or failure details. +* The `DataConverter`'s `payload_codec` (if configured) is applied to the payload *before* it is handed to the storage driver, so the driver always stores encoded bytes. The reference payload written to workflow history is not encoded by the `DataConverter` codec. +* Setting `ExternalStorage.payload_size_threshold` to `0` causes every payload to be considered for external storage regardless of size. + +###### Driver Selection + +When multiple storage backends are needed, list all drivers in `ExternalStorage.drivers` and provide a `driver_selector` to control which driver stores new payloads. Any driver in the list not chosen for storing is still available for retrieval, which is useful when migrating between storage backends. + +```python +from temporalio.converter import ExternalStorage + +options = ExternalStorage( + drivers=[hot_driver, cold_driver], + driver_selector=lambda context, payload: ( + hot_driver if payload.ByteSize() < 5 * 1024 * 1024 else cold_driver + ), +) +``` + +For more complex selection logic, use a plain callable that reads from the `StorageDriverStoreContext`: + +```python +import temporalio.converter +from temporalio.api.common.v1 import Payload + +def feature_flag_is_on(workflow_id: str | None) -> bool: + """Check whether external storage is enabled for this workflow via a feature flag service.""" + return workflow_id is not None and len(workflow_id) % 2 == 0 + +def feature_flag_selector( + context: temporalio.converter.StorageDriverStoreContext, _payload: Payload +) -> temporalio.converter.StorageDriver | None: + workflow_id = ( + context.target.id + if isinstance(context.target, temporalio.converter.StorageDriverWorkflowInfo) + else None + ) + return my_driver if feature_flag_is_on(workflow_id) else None + +options = ExternalStorage( + drivers=[my_driver], + driver_selector=feature_flag_selector, +) +``` + +Some things to note about driver selection: + +* A `driver_selector` is required when more than one driver is registered. With a single driver, `driver_selector` may be omitted and that driver is used for all store operations. +* Returning `None` from a selector leaves the payload stored inline in workflow history rather than offloading it. +* The driver instance returned by the selector must be one of the instances registered in `ExternalStorage.drivers`. If it is not, an error is raised. + +###### Built-in Drivers + +- **[S3 Storage Driver](temporalio/contrib/aws/s3driver/)**: ⚠️ **Experimental** ⚠️ Amazon S3 driver. Ships with an aioboto3 client, or bring your own by subclassing `S3StorageDriverClient`. + +###### Custom Drivers + +Implement `temporalio.converter.StorageDriver` to integrate with an external storage system: + +```python +from collections.abc import Sequence +from temporalio.converter import StorageDriver, StorageDriverClaim, StorageDriverRetrieveContext, StorageDriverStoreContext +from temporalio.api.common.v1 import Payload + +class MyDriver(StorageDriver): + def __init__(self, driver_name: str | None = None): + self._driver_name = driver_name or "my-org:driver:my-driver" + + def name(self) -> str: + return self._driver_name + + async def store( + self, context: StorageDriverStoreContext, payloads: Sequence[Payload] + ) -> list[StorageDriverClaim]: + claims = [] + for payload in payloads: + key = await my_storage.put(payload.SerializeToString()) + claims.append(StorageDriverClaim(claim_data={"key": key})) + return claims + + async def retrieve( + self, context: StorageDriverRetrieveContext, claims: Sequence[StorageDriverClaim] + ) -> list[Payload]: + payloads = [] + for claim in claims: + data = await my_storage.get(claim.claim_data["key"]) + p = Payload() + p.ParseFromString(data) + payloads.append(p) + return payloads +``` + +Some things to note about implementing a custom driver: + +* `StorageDriver.name()` must return a string that is unique among all drivers in `ExternalStorage.drivers`. This name is embedded in the reference payload stored in workflow history and used to look up the correct driver during retrieval — changing it after payloads have been stored will break retrieval. +* `StorageDriver.type()` is automatically implemented to return the name of the class. This can be overridden in subclasses but must remain consistent across all instances of the subclass. +* Use `StorageDriverStoreContext.target` inside `store()` when you need workflow or activity identity (namespace, workflow ID, activity ID, etc.) to choose where or how to store payloads. + ### Workers Workers host workflows and/or activities. Here's how to run a worker: @@ -681,7 +829,6 @@ Some things to note about the above code: capabilities are needed. * Local activities work very similarly except the functions are `workflow.start_local_activity()` and `workflow.execute_local_activity()` - * ⚠️Local activities are currently experimental * Activities can be methods of a class. Invokers should use `workflow.start_activity_method()`, `workflow.execute_activity_method()`, `workflow.start_local_activity_method()`, and `workflow.execute_local_activity_method()` instead. @@ -748,9 +895,12 @@ protect tasks against cancellation. The following tasks, when cancelled, perform a Temporal cancellation: -* Activities - when the task executing an activity is cancelled, a cancellation request is sent to the activity -* Child workflows - when the task starting or executing a child workflow is cancelled, a cancellation request is sent to - cancel the child workflow +* Activities - when the task executing an activity is cancelled, a cancellation request may be sent to the activity + depending on cancellation type +* Child workflows - when the task starting or executing a child workflow is cancelled, a cancellation request may be + sent to cancel the child workflow depending on cancellation type +* Nexus operations - when the task starting or executing a Nexus operation is cancelled, a cancellation request may be + sent to cancel the Nexus operation depending on cancellation type * Timers - when the task executing a timer is cancelled (whether started via sleep or timeout), the timer is cancelled When the workflow itself is requested to cancel, `Task.cancel` is called on the main workflow task. Therefore, @@ -1095,6 +1245,80 @@ my_worker = Worker(..., workflow_runner=SandboxedWorkflowRunner(restrictions=my_ See the API for more details on exact fields and their meaning. +##### Debugging Workflows with `breakpoint()` / `pdb` + +Setting `debug_mode=True` on the `Worker` (or `TEMPORAL_DEBUG=1` in the environment) routes workflow activations +onto the asyncio main thread instead of a worker thread pool. This lets `breakpoint()` and `pdb.set_trace()` +inside workflow code open an interactive REPL — without it, pdb hangs because its `input()` call would run on a +thread that does not own the controlling TTY. + +A minimal runnable example: + +```python +import asyncio +from datetime import timedelta + +from temporalio import workflow +from temporalio.client import Client +from temporalio.worker import Worker + + +@workflow.defn +class DebugMeWorkflow: + @workflow.run + async def run(self) -> str: + x = 42 + breakpoint() # interactive pdb prompt opens at this line + return f"x was {x}" + + +async def main() -> None: + client = await Client.connect("localhost:7233") + async with Worker( + client, + task_queue="debug-me", + workflows=[DebugMeWorkflow], + debug_mode=True, + ): + result = await client.execute_workflow( + DebugMeWorkflow.run, + id="debug-me-wf", + task_queue="debug-me", + task_timeout=timedelta(minutes=10), # see caveat below + ) + print(result) + + +if __name__ == "__main__": + asyncio.run(main()) +``` + +Run with `python debug_me.py`, or under pytest with `pytest -s` (the `-s` flag disables pytest's stdin +capture). At the `(Pdb)` prompt you'll land at the line where `breakpoint()` was called, with workflow +locals in scope. Try `p x`, `n`, `c`, `q`. + +**Quitting cleanly.** Typing `q` or hitting Ctrl-D continues the workflow rather than raising `BdbQuit` +(which would fail the workflow task). To genuinely abort, kill the outer process with Ctrl-C. + +Two caveats when pausing at a breakpoint inside a workflow: + +1. **Workflow task timeout.** Temporal expires a workflow task after ~10 seconds by default. If you sit at the + `(Pdb)` prompt longer than that, the server reassigns the task and your workflow replays from the start when + you continue — re-hitting the breakpoint. Pass `task_timeout=timedelta(minutes=N)` to `execute_workflow` / + `start_workflow` to give yourself debugging headroom: + + ```python + await client.execute_workflow(MyWorkflow.run, ..., task_timeout=timedelta(minutes=10)) + ``` + +2. **Deterministic replay.** Workflows are deterministic and replay from history; any wall-clock pause violates + that contract. For post-mortem debugging without these caveats, use the [Replayer](#replayer) on a recorded + history instead of live debugging. + +Calling `breakpoint()` from sandboxed workflow code without `debug_mode` raises a sandbox +`RestrictedWorkflowAccessError` with a message pointing at `debug_mode=True`, so the failure mode is loud +and the fix is obvious. + ##### Known Sandbox Issues Below are known sandbox issues. As the sandbox is developed and matures, some may be resolved. @@ -1383,8 +1607,6 @@ code](https://github.com/temporalio/samples-python/blob/main/context_propagation ### Nexus -⚠️ **Nexus support is currently at an experimental release stage. Backwards-incompatible changes are anticipated until a stable release is announced.** ⚠️ - [Nexus](https://github.com/nexus-rpc/) is a synchronous RPC protocol. Arbitrary duration operations that can respond asynchronously are modeled on top of a set of pre-defined synchronous RPCs. @@ -1498,10 +1720,80 @@ configuration, and worker execution. Common customizations may include but are n 3. Workflows 4. Interceptors +**Important Notes:** + +- Client plugins that also implement worker plugin interfaces are automatically propagated to workers +- Avoid providing the same plugin to both client and worker to prevent double execution +- Each plugin's `name()` method returns a unique identifier for debugging purposes + +#### Usage + +Plugins can be provided to both `Client` and `Worker`. + +```python +# Use the plugin when connecting +client = await Client.connect( + "my-server.com:7233", + plugins=[SomePlugin()] +) +``` +```python +# Use the plugin when creating a worker +worker = Worker( + client, + plugins=[SomePlugin()] +) +``` +In the case of `Client`, any plugins will also be provided to any workers created with that client. +```python +# Create client with the unified plugin +client = await Client.connect( + "localhost:7233", + plugins=[SomePlugin()] +) + +# Worker will automatically inherit the plugin from the client +worker = Worker( + client, + task_queue="my-task-queue", + workflows=[MyWorkflow], + activities=[my_activity] +) +``` +#### Plugin Implementations + +The easiest way to create your own plugin is to use `SimplePlugin`. This takes a number of possible configurations to produce +a relatively straightforward plugin. + +```python +plugin = SimplePlugin( + "MyPlugin", + data_converter=converter, +) +``` + +It is also possible to subclass `SimplePlugin` for some additional controls. This is what we do for `OpenAIAgentsPlugin`. + +```python +class MediumPlugin(SimplePlugin): + def __init__(self): + super().__init__("MediumPlugin", data_converter=pydantic_data_converter) + + def configure_worker(self, config: WorkerConfig) -> WorkerConfig: + config = super().configure_worker(config) + config["task_queue"] = "override" + return config +``` + +#### Advanced Plugin Implementations + +`SimplePlugin` doesn't cover all possible uses of plugins. For more unusual use cases, an implementor can implement +the underlying plugin interfaces directly. + A single plugin class can implement both client and worker plugin interfaces to share common logic between both contexts. When used with a client, it will automatically be propagated to any workers created with that client. -#### Client Plugins +##### Client Plugins Client plugins can intercept and modify client configuration and service connections. They are useful for adding authentication, modifying connection parameters, or adding custom behavior during client creation. @@ -1516,29 +1808,21 @@ class AuthenticationPlugin(Plugin): def __init__(self, api_key: str): self.api_key = api_key - def init_client_plugin(self, next: Plugin) -> None: - self.next_client_plugin = next - def configure_client(self, config: ClientConfig) -> ClientConfig: # Modify client configuration config["namespace"] = "my-secure-namespace" - return self.next_client_plugin.configure_client(config) + return config async def connect_service_client( - self, config: temporalio.service.ConnectConfig + self, + config: temporalio.service.ConnectConfig, + next: Callable[[ConnectConfig], Awaitable[ServiceClient]] ) -> temporalio.service.ServiceClient: - # Add authentication to the connection config.api_key = self.api_key - return await self.next_client_plugin.connect_service_client(config) - -# Use the plugin when connecting -client = await Client.connect( - "my-server.com:7233", - plugins=[AuthenticationPlugin("my-api-key")] -) + return await next(config) ``` -#### Worker Plugins +##### Worker Plugins Worker plugins can modify worker configuration and intercept worker execution. They are useful for adding monitoring, custom lifecycle management, or modifying worker settings. Worker plugins can also configure replay. @@ -1558,47 +1842,39 @@ class MonitoringPlugin(Plugin): def __init__(self): self.logger = logging.getLogger(__name__) - def init_worker_plugin(self, next: Plugin) -> None: - self.next_worker_plugin = next - def configure_worker(self, config: WorkerConfig) -> WorkerConfig: # Modify worker configuration original_task_queue = config["task_queue"] config["task_queue"] = f"monitored-{original_task_queue}" self.logger.info(f"Worker created for task queue: {config['task_queue']}") - return self.next_worker_plugin.configure_worker(config) + return config - async def run_worker(self, worker: Worker) -> None: + async def run_worker(self, worker: Worker, next: Callable[[Worker], Awaitable[None]]) -> None: self.logger.info("Starting worker execution") try: - await self.next_worker_plugin.run_worker(worker) + await next(worker) finally: self.logger.info("Worker execution completed") def configure_replayer(self, config: ReplayerConfig) -> ReplayerConfig: - return self.next_worker_plugin.configure_replayer(config) + return config @asynccontextmanager async def run_replayer( self, replayer: Replayer, histories: AsyncIterator[temporalio.client.WorkflowHistory], + next: Callable[ + [Replayer, AsyncIterator[WorkflowHistory]], + AbstractAsyncContextManager[AsyncIterator[WorkflowReplayResult]], + ] ) -> AsyncIterator[AsyncIterator[WorkflowReplayResult]]: self.logger.info("Starting replay execution") try: - async with self.next_worker_plugin.run_replayer(replayer, histories) as results: - yield results + async with self.next_worker_plugin.run_replayer(replayer, histories) as results: + yield results finally: self.logger.info("Replay execution completed") - -# Use the plugin when creating a worker -worker = Worker( - client, - task_queue="my-task-queue", - workflows=[MyWorkflow], - activities=[my_activity], - plugins=[MonitoringPlugin()] -) ``` For plugins that need to work with both clients and workers, you can implement both interfaces in a single class: @@ -1612,72 +1888,48 @@ from temporalio.worker import Plugin as WorkerPlugin, WorkerConfig, ReplayerConf class UnifiedPlugin(ClientPlugin, WorkerPlugin): - def init_client_plugin(self, next: ClientPlugin) -> None: - self.next_client_plugin = next - - def init_worker_plugin(self, next: WorkerPlugin) -> None: - self.next_worker_plugin = next - def configure_client(self, config: ClientConfig) -> ClientConfig: # Client-side customization config["data_converter"] = pydantic_data_converter - return self.next_client_plugin.configure_client(config) + return config async def connect_service_client( - self, config: temporalio.service.ConnectConfig + self, + config: temporalio.service.ConnectConfig, + next: Callable[[ConnectConfig], Awaitable[ServiceClient]] ) -> temporalio.service.ServiceClient: - # Add authentication to the connection config.api_key = self.api_key - return await self.next_client_plugin.connect_service_client(config) + return await next(config) def configure_worker(self, config: WorkerConfig) -> WorkerConfig: # Worker-side customization - return self.next_worker_plugin.configure_worker(config) + return config - async def run_worker(self, worker: Worker) -> None: + async def run_worker(self, worker: Worker, next: Callable[[Worker], Awaitable[None]]) -> None: print("Starting unified worker") - await self.next_worker_plugin.run_worker(worker) + await next(worker) def configure_replayer(self, config: ReplayerConfig) -> ReplayerConfig: config["data_converter"] = pydantic_data_converter - return self.next_worker_plugin.configure_replayer(config) + return config async def run_replayer( self, replayer: Replayer, histories: AsyncIterator[temporalio.client.WorkflowHistory], + next: Callable[ + [Replayer, AsyncIterator[WorkflowHistory]], + AbstractAsyncContextManager[AsyncIterator[WorkflowReplayResult]], + ] ) -> AbstractAsyncContextManager[AsyncIterator[WorkflowReplayResult]]: - return self.next_worker_plugin.run_replayer(replayer, histories) - -# Create client with the unified plugin -client = await Client.connect( - "localhost:7233", - plugins=[UnifiedPlugin()] -) - -# Worker will automatically inherit the plugin from the client -worker = Worker( - client, - task_queue="my-task-queue", - workflows=[MyWorkflow], - activities=[my_activity] -) + return next(replayer, histories) ``` -**Important Notes:** - -- Plugins are executed in reverse order (last plugin wraps the first), forming a chain of responsibility -- Client plugins that also implement worker plugin interfaces are automatically propagated to workers -- Avoid providing the same plugin to both client and worker to prevent double execution -- Plugin methods should call the plugin provided during initialization to maintain the plugin chain -- Each plugin's `name()` method returns a unique identifier for debugging purposes - - ### Workflow Replay Given a workflow's history, it can be replayed locally to check for things like non-determinism errors. For example, -assuming `history_str` is populated with a JSON string history either exported from the web UI or from `tctl`, the -following function will replay it: +assuming `history_str` is populated with a JSON string history either exported from the web UI or from the +`Temporal CLI`, the following function will replay it: ```python from temporalio.client import WorkflowHistory @@ -1762,8 +2014,8 @@ users are encouraged to not use gevent in asyncio applications (including Tempor # Development -The Python SDK is built to work with Python 3.9 and newer. It is built using -[SDK Core](https://github.com/temporalio/sdk-core/) which is written in Rust. +The Python SDK is built to work with Python 3.10 and newer. It is built using +[SDK Core](https://github.com/temporalio/sdk-rust/) which is written in Rust. ### Building @@ -1885,21 +2137,33 @@ The environment is now ready to develop in. #### Testing -To execute tests: +To execute tests (in parallel if possible): ```bash poe test ``` -This runs against [Temporalite](https://github.com/temporalio/temporalite). To run against the time-skipping test -server, pass `--workflow-environment time-skipping`. To run against the `default` namespace of an already-running -server, pass the `host:port` to `--workflow-environment`. Can also use regular pytest arguments. For example, here's how -to run a single test with debug logs on the console: +To execute tests serially: + +```bash +uv run pytest +``` + +To execute a single test: ```bash poe test -s --log-cli-level=DEBUG -k test_sync_activity_thread_cancel_caught ``` +**Temporal Server** + +- Tests that use the workflow test environment run against the [Temporal CLI dev server](https://docs.temporal.io/cli#start-dev-server). +- By default, workflow-environment tests automatically start a local dev server. +- On first run, the dev server binary may be downloaded so network access is required if no server is currently running. +- To run workflow-environment tests against the time-skipping test server, pass `--workflow-environment time-skipping`. +- To run workflow-environment tests against the `default` namespace of an already-running server, pass the `host:port` to `--workflow-environment`. +- Unit tests that do not use the workflow environment do not start a dev server. + #### Proto Generation and Testing If you have docker available, run @@ -1917,6 +2181,11 @@ tests. ### Style +``` +# runs ruff + cargo fmt +poe format +``` + * Mostly [Google Style Guide](https://google.github.io/styleguide/pyguide.html). Notable exceptions: * We use [ruff](https://docs.astral.sh/ruff/) for formatting, so that takes precedence * In tests and example code, can import individual classes/functions to make it more readable. Can also do this for diff --git a/pyproject.toml b/pyproject.toml index 77f31e2ad..ab9f2638e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,34 +1,52 @@ [project] name = "temporalio" -version = "1.17.0" +version = "1.31.0" description = "Temporal.io Python SDK" authors = [{ name = "Temporal Technologies Inc", email = "sdk@temporal.io" }] -requires-python = ">=3.9" +requires-python = ">=3.10" readme = "README.md" -license = { file = "LICENSE" } -keywords = [ - "temporal", - "workflow", -] +license = "MIT" +license-files = ["LICENSE"] +keywords = ["temporal", "workflow"] dependencies = [ - "nexus-rpc==1.1.0", - "protobuf>=3.20,<6", - "python-dateutil>=2.8.2,<3 ; python_version < '3.11'", - "types-protobuf>=3.20", - "typing-extensions>=4.2.0,<5", + "nexus-rpc==1.4.0", + "protobuf>=3.20,<8.0.0", + "python-dateutil>=2.8.2,<3 ; python_version < '3.11'", + "types-protobuf>=3.20,<8.0.0", + "typing-extensions>=4.2.0,<5", +] +classifiers = [ + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ] [project.optional-dependencies] grpc = ["grpcio>=1.48.2,<2"] -opentelemetry = [ - "opentelemetry-api>=1.11.1,<2", - "opentelemetry-sdk>=1.11.1,<2", -] +opentelemetry = ["opentelemetry-api>=1.11.1,<2", "opentelemetry-sdk>=1.11.1,<2"] pydantic = ["pydantic>=2.0.0,<3"] -openai-agents = [ - "openai-agents>=0.2.3,<=0.2.9", # 0.2.10 doesn't work: https://github.com/openai/openai-agents-python/issues/1639 - "eval-type-backport>=0.2.2; python_version < '3.10'" +openai-agents = ["openai-agents>=0.17.5", "mcp>=1.9.4, <2"] +google-adk = ["google-adk>=2.2.0,<3", "mcp>=1.24,<2"] +langgraph = ["langgraph>=1.1.0"] +langsmith = ["langsmith>=0.7.34,<0.9"] +deepagents = [ + "deepagents>=0.6.12,<0.7; python_version >= '3.11'", + "langchain>=1.3.11,<2; python_version >= '3.11'", + "langchain-core>=1.4.8,<2; python_version >= '3.11'", ] +lambda-worker-otel = [ + "opentelemetry-api>=1.11.1,<2", + "opentelemetry-sdk>=1.11.1,<2", + "opentelemetry-exporter-otlp-proto-grpc>=1.11.1,<2", + "opentelemetry-semantic-conventions>=0.40b0,<1", + "opentelemetry-sdk-extension-aws>=2.0.0,<3", +] +aioboto3 = ["aioboto3>=10.4.0", "types-aioboto3[s3]>=10.4.0"] +google-genai = ["google-genai>=2.10.0,<3.0.0"] +strands-agents = ["strands-agents>=1.39.0"] [project.urls] Homepage = "https://github.com/temporalio/sdk-python" @@ -38,51 +56,93 @@ Documentation = "https://docs.temporal.io/docs/python" [dependency-groups] dev = [ - "cibuildwheel>=2.22.0,<3", - "grpcio-tools>=1.48.2,<2", - "mypy==1.4.1", - "mypy-protobuf>=3.3.0,<4", - "psutil>=5.9.3,<6", - "pydocstyle>=6.3.0,<7", - "pydoctor>=24.11.1,<25", - "pyright==1.1.403", - "pytest~=7.4", - "pytest-asyncio>=0.21,<0.22", - "pytest-timeout~=2.2", - "ruff>=0.5.0,<0.6", - "toml>=0.10.2,<0.11", - "twine>=4.0.1,<5", - "ruff>=0.5.0,<0.6", - "maturin>=1.8.2", - "pytest-cov>=6.1.1", - "httpx>=0.28.1", - "pytest-pretty>=1.3.0", - "openai-agents[litellm]>=0.2.3,<=0.2.9", # 0.2.10 doesn't work: https://github.com/openai/openai-agents-python/issues/1639 + "basedpyright==1.34.0", + "cibuildwheel>=2.22.0,<3", + "cryptography>=46", + "grpcio-tools>=1.48.2,<2", + "mypy==1.18.2", + "mypy-protobuf>=3.3.0,<4", + "psutil>=5.9.3,<6", + "pydocstyle>=6.3.0,<7", + "pydoctor>=25.10.1,<26", + "pyright==1.1.403", + "pytest~=9.0", + "pytest-asyncio>=0.21,<0.22", + "pytest-timeout~=2.2", + "ruff>=0.15.12,<0.16", + "toml>=0.10.2,<0.11", + "twine>=4.0.1,<5", + "maturin>=1.8.2", + "openinference-instrumentation-openai-agents>=0.1.0", + "pytest-cov>=6.1.1", + "httpx>=0.28.1", + "pytest-pretty>=1.3.0", + "openai-agents>=0.14.0; python_version >= '3.14'", + "openai-agents[litellm]>=0.14.0; python_version < '3.14'", + "litellm>=1.83.0", + "openinference-instrumentation-google-adk>=0.1.11", + "googleapis-common-protos>=1.75.0,<2", + "pytest-rerunfailures>=16.1", + "pytest-xdist>=3.6,<4", + "moto[s3,server]>=5", + "langgraph>=1.1.0", + "langsmith>=0.7.34,<0.9", + "deepagents>=0.6.12,<0.7; python_version >= '3.11'", + "langchain>=1.3.11,<2; python_version >= '3.11'", + "langchain-core>=1.4.8,<2; python_version >= '3.11'", + "langchain-anthropic>=1.4.7; python_version >= '3.11'", + "setuptools<82", + "opentelemetry-exporter-otlp-proto-grpc>=1.11.1,<2", + "opentelemetry-semantic-conventions>=0.40b0,<1", + "opentelemetry-sdk-extension-aws>=2.0.0,<3", + "pytest-flakefinder>=1.1.0", + "async-timeout>=4.0,<6; python_version < '3.11'", + "strands-agents>=1.39.0", + "strands-agents-tools>=0.5.2", + "mcp>=1.9.4,<2", ] [tool.poe.tasks] build-develop = "uv run maturin develop --uv" build-develop-with-release = { cmd = "uv run maturin develop --release --uv" } -format = [{cmd = "uv run ruff check --select I --fix"}, {cmd = "uv run ruff format"}, ] +format = [ + { cmd = "uv run ruff check --select I --fix" }, + { cmd = "uv run ruff format" }, + { cmd = "cargo fmt", cwd = "temporalio/bridge" }, +] gen-docs = "uv run scripts/gen_docs.py" -gen-protos = "uv run scripts/gen_protos.py" -gen-protos-docker = "uv run scripts/gen_protos_docker.py" +gen-nexus-system-api = "uv run scripts/gen_nexus_system_api.py" +gen-protos = [ + { cmd = "uv run scripts/gen_protos.py" }, + { ref = "gen-nexus-system-api" }, + { cmd = "uv run scripts/gen_payload_visitor.py" }, + { cmd = "uv run scripts/gen_bridge_client.py" }, + { ref = "format" }, +] +gen-protos-docker = [ + { cmd = "uv run scripts/gen_protos_docker.py" }, + { ref = "gen-nexus-system-api" }, + { cmd = "uv run scripts/gen_payload_visitor.py" }, + { cmd = "uv run scripts/gen_bridge_client.py" }, + { ref = "format" }, +] lint = [ - {cmd = "uv run ruff check --select I"}, - {cmd = "uv run ruff format --check"}, - {ref = "lint-types"}, - {ref = "lint-docs"}, + { cmd = "uv run ruff check --select I" }, + { cmd = "uv run ruff format --check" }, + { ref = "lint-types" }, + { ref = "lint-docs" }, ] bridge-lint = { cmd = "cargo clippy -- -D warnings", cwd = "temporalio/bridge" } # TODO(cretz): Why does pydocstyle complain about @overload missing docs after # https://github.com/PyCQA/pydocstyle/pull/511? lint-docs = "uv run pydocstyle --ignore-decorators=overload" lint-types = [ - { cmd = "uv run pyright"}, - { cmd = "uv run mypy --namespace-packages --check-untyped-defs ."}, + { cmd = "uv run pyright" }, + { cmd = "uv run mypy --namespace-packages --check-untyped-defs ." }, + { cmd = "uv run basedpyright" }, ] run-bench = "uv run python scripts/run_bench.py" -test = "uv run pytest" +test = "uv run pytest -n auto --dist=worksteal" [tool.pytest.ini_options] @@ -103,33 +163,38 @@ filterwarnings = [ [tool.cibuildwheel] before-all = "pip install protoc-wheel-0" -build = "cp39-win_amd64 cp39-manylinux_x86_64 cp39-manylinux_aarch64 cp39-macosx_x86_64 cp39-macosx_arm64" +build = "cp310-win_amd64 cp310-manylinux_x86_64 cp310-manylinux_aarch64 cp310-musllinux_x86_64 cp310-musllinux_aarch64 cp310-macosx_x86_64 cp310-macosx_arm64" build-verbosity = 1 [tool.cibuildwheel.macos] environment = { MACOSX_DEPLOYMENT_TARGET = "10.12" } [tool.cibuildwheel.linux] -before-all = "curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain stable -y && yum install -y openssl-devel" -before-build = "pip install protoc-wheel-0" +before-all = "sh scripts/cibuildwheel_before_all_linux.sh" +before-build = "sh scripts/cibuildwheel_before_build_linux.sh" environment = { PATH = "$PATH:$HOME/.cargo/bin", CARGO_NET_GIT_FETCH_WITH_CLI = "true" } +[[tool.cibuildwheel.overrides]] +select = "*musllinux*" +environment = { PATH = "$PATH:$HOME/.cargo/bin", CARGO_NET_GIT_FETCH_WITH_CLI = "true", RUSTFLAGS = "-C target-feature=-crt-static" } + [tool.mypy] ignore_missing_imports = true exclude = [ # Ignore generated code 'temporalio/api', - 'temporalio/bridge/proto' + 'temporalio/bridge/proto', ] [tool.pydocstyle] convention = "google" # https://github.com/PyCQA/pydocstyle/issues/363#issuecomment-625563088 -match_dir = "^(?!(docs|scripts|tests|api|proto|\\.)).*" +match_dir = "^(?!(docs|scripts|tests|api|proto|system|\\.)).*" add_ignore = [ # We like to wrap at a certain number of chars, even long summary sentences. # https://github.com/PyCQA/pydocstyle/issues/184 - "D205", "D415" + "D205", + "D415", ] [tool.pydoctor] @@ -141,6 +206,7 @@ intersphinx = [ "https://docs.python.org/3/objects.inv", "https://googleapis.dev/python/protobuf/latest/objects.inv", "https://opentelemetry-python.readthedocs.io/en/latest/objects.inv", + "https://nexus-rpc.github.io/sdk-python/objects.inv", ] privacy = [ "PRIVATE:temporalio.bridge", @@ -160,6 +226,7 @@ privacy = [ ] project-name = "Temporal Python" sidebar-expand-depth = 2 +warnings-as-errors = true [tool.pyright] enableTypeIgnoreComments = true @@ -181,40 +248,18 @@ reportUnknownVariableType = "none" reportUnnecessaryIsInstance = "none" reportUnnecessaryTypeIgnoreComment = "none" reportUnusedCallResult = "none" +reportUnknownLambdaType = "none" include = ["temporalio", "tests"] exclude = [ + # Exclude auto generated files "temporalio/api", "temporalio/bridge/proto", + "temporalio/bridge/_visitor.py", "tests/worker/workflow_sandbox/testmodules/proto", - "temporalio/bridge/worker.py", - "temporalio/worker/_replayer.py", - "temporalio/worker/_worker.py", - "temporalio/worker/workflow_sandbox/_importer.py", - "temporalio/worker/workflow_sandbox/_restrictions.py", - "temporalio/workflow.py", - "tests/api/test_grpc_stub.py", - "tests/conftest.py", - "tests/contrib/test_opentelemetry.py", - "tests/contrib/pydantic/models.py", - "tests/contrib/pydantic/models_2.py", - "tests/contrib/pydantic/test_pydantic.py", - "tests/contrib/pydantic/workflows.py", - "tests/test_converter.py", - "tests/test_service.py", - "tests/worker/test_activity.py", - "tests/worker/workflow_sandbox/test_importer.py", - "tests/worker/workflow_sandbox/test_restrictions.py", - # TODO: these pass locally but fail in CI with - # error: Import "temporalio.bridge.temporal_sdk_bridge" could not be resolved - "temporalio/bridge/client.py", - "temporalio/bridge/metric.py", - "temporalio/bridge/runtime.py", - "temporalio/bridge/testing.py", - "temporalio/envconfig.py", ] [tool.ruff] -target-version = "py39" +target-version = "py310" [build-system] requires = ["maturin>=1.0,<2.0"] @@ -225,10 +270,9 @@ manifest-path = "temporalio/bridge/Cargo.toml" module-name = "temporalio.bridge.temporal_sdk_bridge" python-packages = ["temporalio"] include = ["LICENSE"] -exclude = [ - "temporalio/bridge/target/**/*", -] +exclude = ["temporalio/bridge/target/**/*", "temporalio/bridge/sdk-core/.git"] [tool.uv] # Prevent uv commands from building the package by default package = false +exclude-newer = "2 weeks" diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 000000000..9f29c1099 --- /dev/null +++ b/scripts/__init__.py @@ -0,0 +1 @@ +"""Repository helper scripts.""" diff --git a/scripts/_proto/Dockerfile b/scripts/_proto/Dockerfile index 7617cbe1b..0bbe18bb3 100644 --- a/scripts/_proto/Dockerfile +++ b/scripts/_proto/Dockerfile @@ -8,8 +8,10 @@ VOLUME ["/api_new", "/bridge_new"] COPY ./ ./ RUN mkdir -p ./temporalio/api +RUN uv remove google-adk --optional google-adk +RUN uv add --dev "googleapis-common-protos==1.70.0" RUN uv add "protobuf<4" RUN uv sync --all-extras -RUN poe gen-protos +RUN uv run scripts/gen_protos.py CMD ["sh", "-c", "cp -r ./temporalio/api/* /api_new && cp -r ./temporalio/bridge/proto/* /bridge_new"] diff --git a/scripts/cibuildwheel_before_all_linux.sh b/scripts/cibuildwheel_before_all_linux.sh new file mode 100644 index 000000000..be9b08986 --- /dev/null +++ b/scripts/cibuildwheel_before_all_linux.sh @@ -0,0 +1,13 @@ +#!/bin/sh +set -eu + +if command -v apk >/dev/null 2>&1; then + apk add --no-cache build-base curl openssl-dev protobuf-dev +elif command -v yum >/dev/null 2>&1; then + yum install -y openssl-devel +else + echo "Unsupported Linux image: expected apk or yum" >&2 + exit 1 +fi + +curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain stable -y diff --git a/scripts/cibuildwheel_before_build_linux.sh b/scripts/cibuildwheel_before_build_linux.sh new file mode 100644 index 000000000..838c7dd72 --- /dev/null +++ b/scripts/cibuildwheel_before_build_linux.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -eu + +if command -v protoc >/dev/null 2>&1; then + protoc --version +else + pip install protoc-wheel-0 +fi diff --git a/scripts/gen_bridge_client.py b/scripts/gen_bridge_client.py new file mode 100644 index 000000000..f06dd29e6 --- /dev/null +++ b/scripts/gen_bridge_client.py @@ -0,0 +1,275 @@ +import re +from functools import partial +from string import Template + +from google.protobuf.descriptor import ( + FileDescriptor, + MethodDescriptor, + ServiceDescriptor, +) + +import temporalio.api.cloud.cloudservice.v1.service_pb2 as cloud_service +import temporalio.api.operatorservice.v1.service_pb2 as operator_service +import temporalio.api.testservice.v1.service_pb2 as test_service +import temporalio.api.workflowservice.v1.service_pb2 as workflow_service +import temporalio.bridge.proto.health.v1.health_pb2 as health_service + + +def generate_python_services( + file_descriptors: list[FileDescriptor], + output_file: str = "temporalio/bridge/services_generated.py", +): + print("generating python services") + + services_template = Template('''# Generated file. DO NOT EDIT +"""Generated RPC calls for Temporal services.""" + +from __future__ import annotations + +from datetime import timedelta +from typing import TYPE_CHECKING +from collections.abc import Mapping + +import google.protobuf.empty_pb2 + +$service_imports + + +if TYPE_CHECKING: + from temporalio.service import ServiceClient + +$service_defns +''') + + def service_name(s): + return f"import {sanitize_proto_name(s.full_name)[: -len(s.name) - 1]}" + + service_imports = [ + service_name(service_descriptor) + for file_descriptor in file_descriptors + for service_descriptor in file_descriptor.services_by_name.values() + ] + + service_defns = [ + generate_python_service(service_descriptor) + for file_descriptor in file_descriptors + for service_descriptor in file_descriptor.services_by_name.values() + ] + + with open(output_file, "w") as f: + f.write( + services_template.substitute( + service_imports="\n".join(service_imports), + service_defns="\n".join(service_defns), + ) + ) + + +def generate_python_service(service_descriptor: ServiceDescriptor) -> str: + service_template = Template(''' +class $service_name: + """RPC calls for the $service_name.""" + def __init__(self, client: ServiceClient): + """Initialize service with the provided ServiceClient.""" + self._client = client + self._service = "$rpc_service_name" +$method_calls +''') + + sanitized_service_name: str = service_descriptor.name + # The health service doesn't end in "Service" in the proto definition + # this check ensures that the proto descriptor name will match the format in core + if not sanitized_service_name.endswith("Service"): + sanitized_service_name += "Service" + + # remove "Service" and lowercase + rpc_name = sanitized_service_name[:-7].lower() + + # remove any streaming methods b/c we don't support them at the moment + methods = [ + method + for method in service_descriptor.methods + if not method.client_streaming and not method.server_streaming + ] + + method_calls = [ + generate_python_method_call(sanitized_service_name, method) + for method in sorted(methods, key=lambda m: m.name) + ] + + return service_template.substitute( + service_name=sanitized_service_name, + rpc_service_name=pascal_to_snake(rpc_name), + method_calls="\n".join(method_calls), + ) + + +def generate_python_method_call( + service_name: str, method_descriptor: MethodDescriptor +) -> str: + method_template = Template(''' + async def $method_name( + self, + req: $request_type, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> $response_type: + """Invokes the $service_name.$method_name rpc method.""" + return await self._client._rpc_call( + rpc="$method_name", + req=req, + service=self._service, + resp_type=$response_type, + retry=retry, + metadata=metadata, + timeout=timeout, + ) +''') + + return method_template.substitute( + service_name=service_name, + method_name=pascal_to_snake(method_descriptor.name), + request_type=sanitize_proto_name(method_descriptor.input_type.full_name), + response_type=sanitize_proto_name(method_descriptor.output_type.full_name), + ) + + +def generate_rust_client_impl( + file_descriptors: list[FileDescriptor], + output_file: str = "temporalio/bridge/src/client_rpc_generated.rs", +): + print("generating bridge rpc calls") + + service_calls = [ + generate_rust_service_call(service_descriptor) + for file_descriptor in file_descriptors + for service_descriptor in file_descriptor.services_by_name.values() + ] + + impl_template = Template("""// Generated file. DO NOT EDIT + +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; + +use super::{ + client::{rpc_req, rpc_resp, ClientRef, RpcCall}, + rpc_call, +}; + +#[pymethods] +impl ClientRef { +$service_calls +}""") + + with open(output_file, "w") as f: + f.write(impl_template.substitute(service_calls="\n".join(service_calls))) + + +def generate_rust_service_call(service_descriptor: ServiceDescriptor) -> str: + call_template = Template(""" +fn call_${service_name}<'p>( + &self, + py: Python<'p>, + call: RpcCall, + ) -> PyResult> { + self.runtime.assert_same_process("use client")?; + use temporalio_client::grpc::${descriptor_name}; + let mut connection = self.connection.clone(); + self.runtime.future_into_py(py, async move { + let bytes = match call.rpc.as_str() { +$match_arms + _ => { + return Err(PyValueError::new_err(format!( + "Unknown RPC call {}", + call.rpc + ))) + } + }?; + Ok(bytes) + }) + }""") + + sanitized_service_name: str = service_descriptor.name + # The health service doesn't end in "Service" in the proto definition + # this check ensures that the proto descriptor name will match the format in core + if not sanitized_service_name.endswith("Service"): + sanitized_service_name += "Service" + + # remove any streaming methods b/c we don't support them at the moment + methods = [ + method + for method in service_descriptor.methods + if not method.client_streaming and not method.server_streaming + ] + + service_method = pascal_to_snake(sanitized_service_name) + match_arms = [ + generate_rust_match_arm(sanitized_service_name, service_method, method) + for method in sorted(methods, key=lambda m: m.name) + ] + + return call_template.substitute( + service_name=service_method, + descriptor_name=sanitized_service_name, + match_arms="\n".join(match_arms), + ) + + +def generate_rust_match_arm( + trait_name: str, service_method: str, method: MethodDescriptor +) -> str: + match_template = Template("""\ + "$method_name" => { + rpc_call!(connection, call, $trait_name, $service_method, $method_name) + }""") + + return match_template.substitute( + method_name=pascal_to_snake(method.name), + trait_name=trait_name, + service_method=service_method, + ) + + +def pascal_to_snake(input: str) -> str: + return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", input).lower() + + +sanitize_import_fixes = [ + partial(re.compile(r"temporal\.api\.").sub, r"temporalio.api."), + partial( + re.compile(r"temporal\.grpc.health\.").sub, r"temporalio.bridge.proto.health." + ), + partial( + re.compile(r"google\.protobuf\.Empty").sub, r"google.protobuf.empty_pb2.Empty" + ), +] + + +def sanitize_proto_name(input: str) -> str: + content = input + for fix in sanitize_import_fixes: + content = fix(content) + return content + + +if __name__ == "__main__": + generate_rust_client_impl( + [ + workflow_service.DESCRIPTOR, + operator_service.DESCRIPTOR, + cloud_service.DESCRIPTOR, + test_service.DESCRIPTOR, + health_service.DESCRIPTOR, + ] + ) + + generate_python_services( + [ + workflow_service.DESCRIPTOR, + operator_service.DESCRIPTOR, + cloud_service.DESCRIPTOR, + test_service.DESCRIPTOR, + health_service.DESCRIPTOR, + ] + ) diff --git a/scripts/gen_docs.py b/scripts/gen_docs.py index 2a6955a15..eb6849986 100644 --- a/scripts/gen_docs.py +++ b/scripts/gen_docs.py @@ -8,7 +8,7 @@ print("Generating documentation...") # Run pydoctor - subprocess.check_call("pydoctor") + subprocess.check_call(["pydoctor", "--quiet"]) # Copy favicon shutil.copyfile( diff --git a/scripts/gen_nexus_system_api.py b/scripts/gen_nexus_system_api.py new file mode 100644 index 000000000..7ac1bd2de --- /dev/null +++ b/scripts/gen_nexus_system_api.py @@ -0,0 +1,163 @@ +import os +import shutil +import subprocess +import sys +import tempfile +from importlib.util import module_from_spec, spec_from_file_location +from pathlib import Path +from typing import cast + +import gen_protos + +base_dir = Path(__file__).parent.parent +sys.path.insert(0, str(base_dir)) +wit_input_dir = ( + base_dir + / "temporalio" + / "bridge" + / "sdk-core" + / "crates" + / "protos" + / "protos" + / "api_upstream" + / "nexus" +) +wit_path = wit_input_dir / "workflow-service.wit" +wit_deps_dir = wit_input_dir / "deps" +python_support_path = base_dir / "scripts" / "nex_gen_support.py" +output_dir = base_dir / "temporalio" / "nexus" / "system" / "workflow_service" +workflow_init_path = base_dir / "temporalio" / "workflow" / "__init__.py" +workflowservice_request_response_proto = ( + gen_protos.api_proto_dir + / "temporal" + / "api" + / "workflowservice" + / "v1" + / "request_response.proto" +) + + +def nex_gen_command() -> list[str]: + if bin_path := os.environ.get("NEX_GEN_BIN"): + return [bin_path] + + if shutil.which("nex-gen") is None: + subprocess.check_call(["cargo", "install", "--locked", "nex-gen", "--force"]) + return ["nex-gen"] + + +def build_descriptor_set(descriptor_path: Path) -> None: + subprocess.check_call( + [ + sys.executable, + "-mgrpc_tools.protoc", + f"--proto_path={gen_protos.api_proto_dir}", + f"--proto_path={gen_protos.proto_dir}", + "--include_imports", + f"--descriptor_set_out={descriptor_path}", + str(workflowservice_request_response_proto), + ] + ) + + +def generate_workflow_exports() -> None: + spec = spec_from_file_location( + "temporalio_nexus_system_workflow_service_exports", + output_dir / "__init__.py", + submodule_search_locations=[str(output_dir)], + ) + if spec is None or spec.loader is None: + raise RuntimeError(f"Cannot load generated workflow service from {output_dir}") + module = module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + exports = cast(list[str], module.__all__) + + import_block = [ + "# BEGIN GENERATED NEXUS SYSTEM EXPORTS\n", + "from temporalio.nexus.system.workflow_service import (\n", + *[f" {export},\n" for export in exports], + ")\n", + "# END GENERATED NEXUS SYSTEM EXPORTS\n", + ] + all_block = [ + " # BEGIN GENERATED NEXUS SYSTEM __ALL__\n", + *[f' "{export}",\n' for export in exports], + " # END GENERATED NEXUS SYSTEM __ALL__\n", + ] + content = workflow_init_path.read_text() + start = content.index("# BEGIN GENERATED NEXUS SYSTEM EXPORTS") + end = content.index("# END GENERATED NEXUS SYSTEM EXPORTS", start) + end = content.index("\n", end) + 1 + content = content[:start] + "".join(import_block) + content[end:] + start = content.index(" # BEGIN GENERATED NEXUS SYSTEM __ALL__") + end = content.index(" # END GENERATED NEXUS SYSTEM __ALL__", start) + end = content.index("\n", end) + 1 + workflow_init_path.write_text(content[:start] + "".join(all_block) + content[end:]) + + +def generate_nexus_system_api() -> None: + if not wit_path.exists(): + raise RuntimeError(f"missing WIT source: {wit_path}") + if not wit_deps_dir.exists(): + raise RuntimeError(f"missing WIT dependency directory: {wit_deps_dir}") + if not python_support_path.exists(): + raise RuntimeError(f"missing Python support source: {python_support_path}") + + with tempfile.TemporaryDirectory(dir=base_dir) as temp_dir: + descriptor_path = Path(temp_dir) / "temporal_api.bin" + build_descriptor_set(descriptor_path) + command = nex_gen_command() + + shutil.rmtree(output_dir, ignore_errors=True) + output_dir.parent.mkdir(parents=True, exist_ok=True) + subprocess.check_call( + [ + *command, + "generate", + "--lang", + "python", + "--input", + str(wit_path), + "--input", + str(wit_deps_dir), + "--support-file", + str(python_support_path), + "--descriptors", + str(descriptor_path), + "--output", + str(output_dir), + ] + ) + + (output_dir.parent / "__init__.py").touch() + generate_workflow_exports() + subprocess.check_call( + [ + sys.executable, + "-m", + "ruff", + "check", + "--select", + "I", + "--fix", + str(output_dir), + str(workflow_init_path), + ] + ) + subprocess.check_call( + [ + sys.executable, + "-m", + "ruff", + "format", + str(output_dir), + str(workflow_init_path), + ] + ) + + +if __name__ == "__main__": + print("Generating Nexus system API...", file=sys.stderr) + generate_nexus_system_api() + print("Done", file=sys.stderr) diff --git a/scripts/gen_payload_visitor.py b/scripts/gen_payload_visitor.py new file mode 100644 index 000000000..0001659f7 --- /dev/null +++ b/scripts/gen_payload_visitor.py @@ -0,0 +1,468 @@ +import subprocess +import sys +from importlib.util import module_from_spec, spec_from_file_location +from pathlib import Path +from typing import cast + +import google.protobuf.message +import nexusrpc +from google.protobuf.descriptor import Descriptor, FieldDescriptor + +base_dir = Path(__file__).parent.parent +sys.path.insert(0, str(base_dir)) + +from temporalio.api.common.v1.message_pb2 import Payload, Payloads, SearchAttributes +from temporalio.bridge.proto.nexus import NexusTaskCompletion +from temporalio.bridge.proto.workflow_activation.workflow_activation_pb2 import ( + WorkflowActivation, +) +from temporalio.bridge.proto.workflow_completion.workflow_completion_pb2 import ( + WorkflowActivationCompletion, +) + + +def discover_system_nexus_roots() -> list[Descriptor]: + module_path = ( + base_dir / "temporalio" / "nexus" / "system" / "workflow_service" / "service.py" + ) + spec = spec_from_file_location( + "temporalio_nexus_system_workflow_service", module_path + ) + if spec is None or spec.loader is None: + raise RuntimeError(f"Cannot load generated system service from {module_path}") + module = module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + + roots: list[Descriptor] = [] + for operation in vars(module.WorkflowService).values(): + if not isinstance(operation, nexusrpc.Operation): + continue + for proto_type in (operation.input_type, operation.output_type): + if isinstance(proto_type, type) and issubclass( + proto_type, google.protobuf.message.Message + ): + roots.append(cast(Descriptor, proto_type.DESCRIPTOR)) + deduped: list[Descriptor] = [] + seen: set[str] = set() + for root in roots: + if root.full_name not in seen: + seen.add(root.full_name) + deduped.append(root) + return deduped + + +def name_for(desc: Descriptor) -> str: + # Use fully-qualified name to avoid collisions; replace dots with underscores + return desc.full_name.replace(".", "_") + + +def field_is_repeated(field: FieldDescriptor) -> bool: + return bool( + getattr( + field, + "is_repeated", + getattr(field, "label") == FieldDescriptor.LABEL_REPEATED, + ) + ) + + +def emit_loop( + field_name: str, + iter_expr: str, + child_method: str, +) -> str: + # Emit a for-loop with direct await, with optional skip guard + inner = ( + f"for v in {iter_expr}:\n" + f" await self._visit_{child_method}(fs, v)" + ) + if field_name == "headers": + return f" if not self.skip_headers:\n {inner}" + elif field_name == "search_attributes": + return f" if not self.skip_search_attributes:\n {inner}" + else: + return f" {inner}" + + +def emit_singular( + field_name: str, access_expr: str, child_method: str, presence_word: str | None +) -> str: + # Emit a direct await self._visit_...() with optional HasField check and skip guard + if presence_word: + if field_name == "headers": + return ( + " if not self.skip_headers:\n" + f' {presence_word} o.HasField("{field_name}"):\n' + f" await self._visit_{child_method}(fs, {access_expr})" + ) + else: + return ( + f' {presence_word} o.HasField("{field_name}"):\n' + f" await self._visit_{child_method}(fs, {access_expr})" + ) + else: + if field_name == "headers": + return ( + " if not self.skip_headers:\n" + f" await self._visit_{child_method}(fs, {access_expr})" + ) + else: + return f" await self._visit_{child_method}(fs, {access_expr})" + + +class VisitorGenerator: + def generate(self, roots: list[Descriptor]) -> str: + """ + Generate Python source code that, given a function f(Payload) -> Payload, + applies it to every Payload contained within a WorkflowActivation tree. + + The generated code defines async visitor functions for each reachable + protobuf message type starting from WorkflowActivation, including support + for repeated fields and map entries, and a convenience entrypoint + function `visit`. + """ + + for r in roots: + self.walk(r) + + header = """ +from __future__ import annotations + +# This file is generated by gen_payload_visitor.py. Changes should be made there. +from typing import Any + +import temporalio.nexus.system +from temporalio.api.common.v1.message_pb2 import Payload +from temporalio.bridge._visitor_functions import ( + BoundedVisitorFunctions, + PayloadSequence, + VisitorFunctions, +) + + +class PayloadVisitor: + \"\"\"A visitor for payloads. + Applies a function to every payload in a tree of messages. + \"\"\" + + def __init__( + self, + *, + skip_search_attributes: bool = False, + skip_headers: bool = False, + concurrency_limit: int = 1, + ): + \"\"\"Creates a new payload visitor. + + Args: + skip_search_attributes: If True, search attributes are not visited. + skip_headers: If True, headers are not visited. + concurrency_limit: Maximum number of payload visits that may run + concurrently during a single call to visit(). Defaults to 1 + (sequential). + \"\"\" + if concurrency_limit < 1: + raise ValueError("concurrency_limit must be positive") + self.skip_search_attributes = skip_search_attributes + self.skip_headers = skip_headers + self._concurrency_limit = concurrency_limit + + async def visit( + self, fs: VisitorFunctions, root: Any + ) -> None: + \"\"\"Visits the given root message with the given function.\"\"\" + method_name = "_visit_" + root.DESCRIPTOR.full_name.replace(".", "_") + method = getattr(self, method_name, None) + if method is None: + raise ValueError(f"Unknown root message type: {root.DESCRIPTOR.full_name}") + if self._concurrency_limit == 1: + await method(fs, root) + return + + bounded = BoundedVisitorFunctions(fs, self._concurrency_limit) + try: + await method(bounded, root) + finally: + await bounded.drain() + + async def _visit_nexus_operation_input_payload( + self, + fs: VisitorFunctions, + payload: Payload, + ) -> None: + await self._visit_temporal_api_common_v1_Payload(fs, payload) + +""" + + return header + "\n".join(self.methods) + + def __init__(self): + # Track which message descriptors have visitor methods generated + self.generated: dict[str, bool] = { + Payload.DESCRIPTOR.full_name: True, + Payloads.DESCRIPTOR.full_name: True, + } + self.in_progress: set[str] = set() + self.methods: list[str] = [ + """\ + async def _visit_temporal_api_common_v1_Payload( + self, fs: VisitorFunctions, payload: Payload + ) -> None: + new_payload = await temporalio.nexus.system.maybe_visit_payload( + payload, + fs, + self.skip_search_attributes, + ) + if new_payload is None: + await fs.visit_payload(payload) + return + + if new_payload is not payload: + payload.CopyFrom(new_payload) + await fs.visit_system_nexus_envelope(payload) + """, + """\ + async def _visit_temporal_api_common_v1_Payloads(self, fs: VisitorFunctions, o: Any): + await fs.visit_payloads(o.payloads) + """, + """\ + async def _visit_payload_container(self, fs: VisitorFunctions, o: PayloadSequence): + await fs.visit_payloads(o) + """, + ] + + def _collect_repeated( + self, child_desc: Descriptor, field: FieldDescriptor, iter_expr: str + ) -> tuple | None: + """Collect emit item for a non-map repeated field. Returns tuple or None.""" + if child_desc.full_name == Payload.DESCRIPTOR.full_name: + return ("singular", field.name, iter_expr, "payload_container", None) + else: + child_needed = self.walk(child_desc) + if child_needed: + return ("loop", field.name, iter_expr, name_for(child_desc)) + else: + return None + + def walk(self, desc: Descriptor) -> bool: + key = desc.full_name + if key in self.generated: + return self.generated[key] + if key in self.in_progress: + # Break cycles; Assume the child will be needed (Used by Failure -> Cause) + return True + + has_payload = False + self.in_progress.add(key) + is_search_attrs = desc.full_name == SearchAttributes.DESCRIPTOR.full_name + + # Collect emit items before generating code. Each item is one of: + # ("loop", field_name, iter_expr, child_method) + # ("singular", field_name, access_expr, child_method, presence_word_or_None) + # ("oneof_group",[(field_name, access_expr, child_method, if_word), ...]) + emit_items: list = [] + + # Group fields by oneof to generate if/elif chains + oneof_fields: dict[int, list[FieldDescriptor]] = {} + regular_fields: list[FieldDescriptor] = [] + + for field in desc.fields: + if field.type != FieldDescriptor.TYPE_MESSAGE: + continue + + # Skip synthetic oneofs (proto3 optional fields) + if field.containing_oneof is not None: + oneof_idx = field.containing_oneof.index + if oneof_idx not in oneof_fields: + oneof_fields[oneof_idx] = [] + oneof_fields[oneof_idx].append(field) + else: + regular_fields.append(field) + + # Process regular fields first + for field in regular_fields: + if ( + desc.full_name == "coresdk.workflow_commands.ScheduleNexusOperation" + and field.name == "input" + ): + has_payload = True + emit_items.append( + ( + "system_nexus", + field.name, + "o.endpoint", + "o.input", + ) + ) + continue + + # Repeated fields (including maps which are represented as repeated messages) + if field_is_repeated(field): + message_type = field.message_type + if message_type is not None and message_type.GetOptions().map_entry: + val_fd = message_type.fields_by_name.get("value") + if ( + val_fd is not None + and val_fd.type == FieldDescriptor.TYPE_MESSAGE + ): + child_desc = val_fd.message_type + assert child_desc is not None + child_needed = self.walk(child_desc) + if child_needed: + has_payload = True + emit_items.append( + ( + "loop", + field.name, + f"o.{field.name}.values()", + name_for(child_desc), + ) + ) + + key_fd = message_type.fields_by_name.get("key") + if ( + key_fd is not None + and key_fd.type == FieldDescriptor.TYPE_MESSAGE + ): + child_desc = key_fd.message_type + assert child_desc is not None + child_needed = self.walk(child_desc) + if child_needed: + has_payload = True + emit_items.append( + ( + "loop", + field.name, + f"o.{field.name}.keys()", + name_for(child_desc), + ) + ) + else: + assert message_type is not None + item = self._collect_repeated( + message_type, field, f"o.{field.name}" + ) + if item is not None: + has_payload = True + emit_items.append(item) + else: + child_desc = field.message_type + assert child_desc is not None + child_has_payload = self.walk(child_desc) + has_payload |= child_has_payload + if child_has_payload: + emit_items.append( + ( + "singular", + field.name, + f"o.{field.name}", + name_for(child_desc), + "if", + ) + ) + + # Process oneof fields as if/elif chains + for oneof_idx, fields in oneof_fields.items(): + group = [] + first = True + for field in fields: + child_desc = field.message_type + assert child_desc is not None + child_has_payload = self.walk(child_desc) + has_payload |= child_has_payload + if child_has_payload: + if_word = "if" if first else "elif" + first = False + group.append( + (field.name, f"o.{field.name}", name_for(child_desc), if_word) + ) + if group: + emit_items.append(("oneof_group", group)) + + self.generated[key] = has_payload + self.in_progress.discard(key) + + if has_payload: + lines: list[str] = [ + f" async def _visit_{name_for(desc)}" + "(self, fs: VisitorFunctions, o: Any):" + ] + if is_search_attrs: + lines.append(" if self.skip_search_attributes:") + lines.append(" return") + + for item in emit_items: + if item[0] == "loop": + _, field_name, iter_expr, child_method = item + lines.append(emit_loop(field_name, iter_expr, child_method)) + elif item[0] == "singular": + _, field_name, access_expr, child_method, presence_word = item + lines.append( + emit_singular( + field_name, access_expr, child_method, presence_word + ) + ) + elif item[0] == "system_nexus": + _, field_name, _endpoint_expr, payload_expr = item + lines.append( + f' if o.HasField("{field_name}"):\n' + " await self._visit_nexus_operation_input_payload(\n" + f" fs, {payload_expr}\n" + " )" + ) + else: # oneof_group + for field_name, access_expr, child_method, presence_word in item[1]: + lines.append( + emit_singular( + field_name, access_expr, child_method, presence_word + ) + ) + + self.methods.append("\n".join(lines) + "\n") + return has_payload + + +def write_bridge_visitors() -> None: + out_path = base_dir / "temporalio" / "bridge" / "_visitor.py" + + # Build root descriptors: WorkflowActivation, WorkflowActivationCompletion, + # NexusTaskCompletion, and the system Nexus operation roots. + roots: list[Descriptor] = [ + WorkflowActivation.DESCRIPTOR, + WorkflowActivationCompletion.DESCRIPTOR, + NexusTaskCompletion.DESCRIPTOR, + ] + discover_system_nexus_roots() + + code = VisitorGenerator().generate(roots) + out_path.write_text(code) + + +if __name__ == "__main__": + print("Generating temporalio/bridge/_visitor.py...", file=sys.stderr) + write_bridge_visitors() + subprocess.run( + [ + "uv", + "run", + "ruff", + "check", + "--select", + "I", + "--fix", + "temporalio/bridge/_visitor.py", + ], + cwd=base_dir, + check=True, + ) + subprocess.run( + [ + "uv", + "run", + "ruff", + "format", + "temporalio/bridge/_visitor.py", + ], + cwd=base_dir, + check=True, + ) diff --git a/scripts/gen_protos.py b/scripts/gen_protos.py index 61d2709fe..4b2ea0456 100644 --- a/scripts/gen_protos.py +++ b/scripts/gen_protos.py @@ -4,13 +4,13 @@ import subprocess import sys import tempfile +from collections.abc import Mapping from functools import partial from pathlib import Path -from typing import List, Mapping, Optional base_dir = Path(__file__).parent.parent proto_dir = ( - base_dir / "temporalio" / "bridge" / "sdk-core" / "sdk-core-protos" / "protos" + base_dir / "temporalio" / "bridge" / "sdk-core" / "crates" / "protos" / "protos" ) api_proto_dir = proto_dir / "api_upstream" api_cloud_proto_dir = proto_dir / "api_cloud_upstream" @@ -39,13 +39,37 @@ partial( re.compile(r"from dependencies\.").sub, r"from temporalio.api.dependencies." ), + partial( + re.compile(r"from protoc_gen_openapiv2\.").sub, + r"from temporalio.api.dependencies.protoc_gen_openapiv2.", + ), + partial( + re.compile(r"from nexusannotations\.").sub, + r"from temporalio.api.dependencies.nexusannotations.", + ), partial( re.compile(r"from temporal\.sdk\.core\.").sub, r"from temporalio.bridge.proto." ), + partial( + re.compile(r"'__module__' : 'temporal\.api\.").sub, + r"'__module__' : 'temporalio.api.", + ), + partial( + re.compile(r"'__module__' : 'nexusannotations\.").sub, + r"'__module__' : 'temporalio.api.dependencies.nexusannotations.", + ), ] pyi_fixes = [ partial(re.compile(r"temporal\.api\.").sub, r"temporalio.api."), + partial( + re.compile(r"protoc_gen_openapiv2\.").sub, + r"temporalio.api.dependencies.protoc_gen_openapiv2.", + ), + partial( + re.compile(r"nexusannotations\.").sub, + r"temporalio.api.dependencies.nexusannotations.", + ), partial(re.compile(r"temporal\.sdk\.core\.").sub, r"temporalio.bridge.proto."), ] @@ -60,8 +84,7 @@ def fix_generated_output(base_path: Path): - protoc doesn't generate the correct import paths (https://github.com/protocolbuffers/protobuf/issues/1491) """ - - imports: Mapping[str, List[str]] = collections.defaultdict(list) + imports: Mapping[str, list[str]] = collections.defaultdict(list) for p in base_path.iterdir(): if p.is_dir(): fix_generated_output(p) @@ -142,12 +165,12 @@ def check_proto_toolchain_versions(): _, _, proto_version = line.partition("==") elif line.startswith("grpcio-tools"): _, _, grpcio_tools_version = line.partition("==") - assert proto_version.startswith( - "3." - ), f"expected 3.x protobuf, found {proto_version}" - assert grpcio_tools_version.startswith( - "1.48." - ), f"expected 1.48.x grpcio-tools, found {grpcio_tools_version}" + assert proto_version.startswith("3."), ( + f"expected 3.x protobuf, found {proto_version}" + ) + assert grpcio_tools_version.startswith("1.48."), ( + f"expected 1.48.x grpcio-tools, found {grpcio_tools_version}" + ) def generate_protos(output_dir: Path): @@ -160,6 +183,7 @@ def generate_protos(output_dir: Path): f"--proto_path={core_proto_dir}", f"--proto_path={testsrv_proto_dir}", f"--proto_path={health_proto_dir}", + f"--proto_path={proto_dir}", f"--proto_path={test_proto_dir}", f"--proto_path={additional_proto_dir}", f"--python_out={output_dir}", @@ -179,11 +203,18 @@ def generate_protos(output_dir: Path): grpc_file.unlink() # Apply fixes before moving code fix_generated_output(output_dir) + # Move dependency protos + deps_out_dir = api_out_dir / "dependencies" + shutil.rmtree(deps_out_dir / "protoc_gen_openapiv2", ignore_errors=True) + shutil.rmtree(deps_out_dir / "nexusannotations", ignore_errors=True) + deps_out_dir.mkdir(exist_ok=True) + (output_dir / "protoc_gen_openapiv2").replace(deps_out_dir / "protoc_gen_openapiv2") + (output_dir / "nexusannotations").replace(deps_out_dir / "nexusannotations") + (deps_out_dir / "__init__.py").touch() # Move protos for p in (output_dir / "temporal" / "api").iterdir(): shutil.rmtree(api_out_dir / p.name, ignore_errors=True) p.replace(api_out_dir / p.name) - shutil.rmtree(api_out_dir / "dependencies", ignore_errors=True) for p in (output_dir / "temporal" / "sdk" / "core").iterdir(): shutil.rmtree(sdk_out_dir / p.name, ignore_errors=True) p.replace(sdk_out_dir / p.name) diff --git a/scripts/gen_protos_docker.py b/scripts/gen_protos_docker.py index 7014022bc..6735bdf90 100644 --- a/scripts/gen_protos_docker.py +++ b/scripts/gen_protos_docker.py @@ -3,24 +3,42 @@ # Build the Docker image and capture its ID result = subprocess.run( - ["docker", "build", "-q", "-f", "scripts/_proto/Dockerfile", "."], - capture_output=True, + [ + "docker", + "build", + "-q", + "-f", + os.path.join("scripts", "_proto", "Dockerfile"), + ".", + ], + stdout=subprocess.PIPE, text=True, check=True, ) image_id = result.stdout.strip() -subprocess.run( +docker_run_command = [ + "docker", + "run", + "--rm", +] + +getuid = getattr(os, "getuid", None) +getgid = getattr(os, "getgid", None) +if callable(getuid) and callable(getgid): + docker_run_command.extend(["--user", f"{getuid()}:{getgid()}"]) + +docker_run_command.extend( [ - "docker", - "run", - "--rm", "-v", - f"{os.getcwd()}/temporalio/api:/api_new", + os.path.join(os.getcwd(), "temporalio", "api") + ":/api_new", "-v", - f"{os.getcwd()}/temporalio/bridge/proto:/bridge_new", + os.path.join(os.getcwd(), "temporalio", "bridge", "proto") + ":/bridge_new", image_id, - ], + ] +) + +subprocess.run( + docker_run_command, check=True, ) -subprocess.run(["uv", "run", "poe", "format"], check=True) diff --git a/scripts/nex_gen_support.py b/scripts/nex_gen_support.py new file mode 100644 index 000000000..58b51e263 --- /dev/null +++ b/scripts/nex_gen_support.py @@ -0,0 +1,195 @@ +import collections.abc +import typing +from datetime import timedelta + +import google.protobuf.duration_pb2 + +import temporalio.api.common.v1.message_pb2 as common_pb2 +import temporalio.api.enums.v1.workflow_pb2 as workflow_enums_pb2 +import temporalio.api.taskqueue.v1.message_pb2 as taskqueue_pb2 +import temporalio.api.workflow.v1 +import temporalio.common +import temporalio.converter + + +def retry_policy_from_proto( + proto: common_pb2.RetryPolicy, +) -> temporalio.common.RetryPolicy: + return temporalio.common.RetryPolicy.from_proto(proto) + + +def retry_policy_to_proto( + retry_policy: temporalio.common.RetryPolicy, +) -> common_pb2.RetryPolicy: + proto = common_pb2.RetryPolicy() + retry_policy.apply_to_proto(proto) + return proto + + +def workflow_function_name( + value: str | collections.abc.Callable[..., collections.abc.Awaitable[object]], +) -> str: + from temporalio.workflow import _Definition # pyright: ignore[reportPrivateUsage] + + name, _result_type = _Definition.get_name_and_result_type(value) + return name + + +def signal_function_to_proto( + value: str | collections.abc.Callable[..., typing.Any], +) -> str: + from temporalio.workflow import ( + _SignalDefinition, # pyright: ignore[reportPrivateUsage] + ) + + return _SignalDefinition.must_name_from_fn_or_str(value) # pyright: ignore[reportUnknownMemberType] + + +def workflow_type_to_proto( + workflow_type: str + | collections.abc.Callable[..., collections.abc.Awaitable[object]], +) -> common_pb2.WorkflowType: + return common_pb2.WorkflowType(name=workflow_function_name(workflow_type)) + + +def task_queue_from_proto( + proto: taskqueue_pb2.TaskQueue, +) -> str: + return proto.name + + +def task_queue_to_proto( + task_queue: str, +) -> taskqueue_pb2.TaskQueue: + return taskqueue_pb2.TaskQueue(name=task_queue) + + +def workflow_namespace() -> str: + from temporalio.workflow import info + + return info().namespace + + +def payloads_to_proto( + values: collections.abc.Sequence[typing.Any], +) -> common_pb2.Payloads: + from temporalio.workflow import payload_converter + + return payload_converter().to_payloads_wrapper(values) + + +def _clone_payload(payload: common_pb2.Payload) -> common_pb2.Payload: + clone = common_pb2.Payload() + clone.CopyFrom(payload) + return clone + + +def _value_to_payload(value: object | common_pb2.Payload) -> common_pb2.Payload: + if isinstance(value, common_pb2.Payload): + return _clone_payload(value) + from temporalio.workflow import payload_converter + + payloads = payload_converter().to_payloads_wrapper([value]) + return _clone_payload(payloads.payloads[0]) + + +def _payload_to_value(payload: common_pb2.Payload) -> object: + wrapper = common_pb2.Payloads() + wrapper.payloads.add().CopyFrom(payload) + from temporalio.workflow import payload_converter + + return typing.cast( + object, + payload_converter().from_payloads_wrapper(wrapper)[0], + ) + + +def payload_from_proto( + proto: common_pb2.Payload, +) -> object: + return _payload_to_value(proto) + + +def payload_to_proto( + payload: object, +) -> common_pb2.Payload: + return _value_to_payload(payload) + + +def memo_from_proto( + proto: common_pb2.Memo, +) -> collections.abc.Mapping[str, object]: + return {key: _payload_to_value(value) for key, value in proto.fields.items()} + + +def memo_to_proto( + memo: collections.abc.Mapping[str, object], +) -> common_pb2.Memo: + message = common_pb2.Memo() + for key, value in memo.items(): + message.fields[key].CopyFrom(_value_to_payload(value)) + return message + + +def duration_from_proto(proto: google.protobuf.duration_pb2.Duration) -> timedelta: + return proto.ToTimedelta() + + +def duration_to_proto( + duration: timedelta, +) -> google.protobuf.duration_pb2.Duration: + proto = google.protobuf.duration_pb2.Duration() + proto.FromTimedelta(duration) + return proto + + +def workflow_id_reuse_policy_from_proto( + policy: workflow_enums_pb2.WorkflowIdReusePolicy.ValueType, +) -> temporalio.common.WorkflowIDReusePolicy: + return temporalio.common.WorkflowIDReusePolicy(int(policy)) + + +def workflow_id_reuse_policy_to_proto( + policy: temporalio.common.WorkflowIDReusePolicy, +) -> workflow_enums_pb2.WorkflowIdReusePolicy.ValueType: + return typing.cast(workflow_enums_pb2.WorkflowIdReusePolicy.ValueType, int(policy)) + + +def workflow_id_conflict_policy_from_proto( + policy: workflow_enums_pb2.WorkflowIdConflictPolicy.ValueType, +) -> temporalio.common.WorkflowIDConflictPolicy: + return temporalio.common.WorkflowIDConflictPolicy(int(policy)) + + +def workflow_id_conflict_policy_to_proto( + policy: temporalio.common.WorkflowIDConflictPolicy, +) -> workflow_enums_pb2.WorkflowIdConflictPolicy.ValueType: + return typing.cast( + workflow_enums_pb2.WorkflowIdConflictPolicy.ValueType, int(policy) + ) + + +def search_attributes_to_proto( + search_attributes: temporalio.common.TypedSearchAttributes, +) -> common_pb2.SearchAttributes: + proto = common_pb2.SearchAttributes() + temporalio.converter.encode_search_attributes(search_attributes, proto) + return proto + + +def priority_from_proto( + proto: common_pb2.Priority, +) -> temporalio.common.Priority: + return temporalio.common.Priority._from_proto(proto) # pyright: ignore[reportPrivateUsage] + + +def priority_to_proto( + priority: temporalio.common.Priority, +) -> common_pb2.Priority: + return priority._to_proto() # pyright: ignore[reportPrivateUsage] + + +def versioning_override_to_proto( + versioning_override: temporalio.common.VersioningOverride, +) -> temporalio.api.workflow.v1.VersioningOverride: + return versioning_override._to_proto() # pyright: ignore[reportPrivateUsage] diff --git a/scripts/prepare_release.py b/scripts/prepare_release.py new file mode 100644 index 000000000..51dc34496 --- /dev/null +++ b/scripts/prepare_release.py @@ -0,0 +1,327 @@ +"""Prepare checked-in files for an SDK release.""" + +from __future__ import annotations + +import argparse +import datetime +import pathlib +import re +import subprocess +import sys +from collections.abc import Sequence + +if __package__ is None or __package__ == "": + sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1])) + +CHANGELOG_HEADERS = ( + "Added", + "Changed", + "Deprecated", + ":boom: Breaking Changes", + "Fixed", + "Security", +) +VERSION_RE = re.compile(r"[0-9]+(?:\.[0-9]+)+(?:[a-zA-Z0-9_.+-]+)?") +_CHANGELOG_HEADING_RE = re.compile(r"^## \[(?P[^\]]+)\](?:\s+-\s+.*)?\s*$") +_CHANGELOG_SUBHEADING_RE = re.compile(r"^### (?P
.+?)\s*$") +_RELEASE_FILES = ( + "CHANGELOG.md", + "pyproject.toml", + "temporalio/service.py", + "uv.lock", +) +_RELEASE_FILE_SET = frozenset(_RELEASE_FILES) + + +def validate_version(version: str) -> str: + if not VERSION_RE.fullmatch(version): + raise ValueError( + f"Invalid version {version!r}; expected a version like '1.30.0'" + ) + return version + + +def parse_date(date: str) -> datetime.date: + try: + return datetime.date.fromisoformat(date) + except ValueError as err: + raise ValueError(f"Invalid release date {date!r}; expected YYYY-MM-DD") from err + + +def finalize_changelog_release( + text: str, + *, + version: str, + release_date: datetime.date, +) -> str: + validate_version(version) + lines = text.splitlines() + + if _find_version_section(lines, version) is not None: + raise RuntimeError(f"Changelog already has a section for {version!r}") + + unreleased = _find_version_section(lines, "Unreleased") + if unreleased is None: + raise RuntimeError("Could not find changelog section for 'Unreleased'") + + heading_index, section_start, section_end = unreleased + unreleased_lines = _strip_empty_changelog_headers( + _strip_outer_blank_lines(lines[section_start:section_end]) + ) + if not unreleased_lines: + raise RuntimeError("Changelog section for 'Unreleased' is empty") + + next_lines = [ + *lines[:heading_index], + *_seeded_unreleased_lines(), + f"## [{version}] - {release_date.isoformat()}", + "", + *unreleased_lines, + "", + *lines[section_end:], + ] + return "\n".join(_collapse_blank_lines(next_lines)).rstrip() + "\n" + + +def replace_project_version(text: str, version: str) -> str: + return _replace_once( + r'(?m)^version = "[^"]+"[^\S\r\n]*$', + f'version = "{validate_version(version)}"', + text, + description="project version", + ) + + +def replace_service_version(text: str, version: str) -> str: + return _replace_once( + r'(?m)^__version__ = "[^"]+"[^\S\r\n]*$', + f'__version__ = "{validate_version(version)}"', + text, + description="service version", + ) + + +def create_release_branch(repo_root: pathlib.Path, version: str) -> None: + subprocess.run(["git", "fetch", "origin", "main"], cwd=repo_root, check=True) + subprocess.run( + ["git", "switch", "--create", f"chore/release-{version}", "origin/main"], + cwd=repo_root, + check=True, + ) + + +def changed_files(repo_root: pathlib.Path) -> set[str]: + result = subprocess.run( + ["git", "status", "--porcelain"], + cwd=repo_root, + check=True, + capture_output=True, + text=True, + ) + return {line[3:] for line in result.stdout.splitlines()} + + +def ensure_clean_worktree(repo_root: pathlib.Path) -> None: + changes = changed_files(repo_root) + if changes: + raise RuntimeError( + "Release preparation requires a clean worktree; found changes in " + + ", ".join(sorted(changes)) + ) + + +def ensure_only_release_changes(repo_root: pathlib.Path) -> None: + unexpected_files = changed_files(repo_root) - _RELEASE_FILE_SET + if unexpected_files: + raise RuntimeError( + "Release preparation changed unexpected files: " + + ", ".join(sorted(unexpected_files)) + ) + + +def commit_release_changes(repo_root: pathlib.Path, version: str) -> None: + subprocess.run( + ["git", "commit", "-m", f"Prepare release {version}", "--", *_RELEASE_FILES], + cwd=repo_root, + check=True, + ) + + +def push_release_branch(repo_root: pathlib.Path, version: str) -> None: + branch = f"chore/release-{version}" + subprocess.run( + ["git", "push", "--set-upstream", "origin", branch], + cwd=repo_root, + check=True, + ) + + +def create_release_pr(repo_root: pathlib.Path, version: str) -> None: + branch = f"chore/release-{version}" + subprocess.run( + [ + "gh", + "pr", + "create", + "--base", + "main", + "--head", + branch, + "--title", + f"Prepare release {version}", + "--body", + f"Prepare release {version}.", + ], + cwd=repo_root, + check=True, + ) + + +def _seeded_unreleased_lines() -> list[str]: + lines = ["## [Unreleased]", ""] + for header in CHANGELOG_HEADERS: + lines.extend([f"### {header}", ""]) + return lines + + +def _strip_empty_changelog_headers(lines: list[str]) -> list[str]: + filtered: list[str] = [] + index = 0 + while index < len(lines): + match = _CHANGELOG_SUBHEADING_RE.match(lines[index]) + if not match or match.group("header") not in CHANGELOG_HEADERS: + filtered.append(lines[index]) + index += 1 + continue + + next_index = index + 1 + while next_index < len(lines) and not lines[next_index].startswith("### "): + next_index += 1 + + content = lines[index + 1 : next_index] + if any(line.strip() for line in content): + filtered.append(lines[index]) + filtered.extend(content) + index = next_index + + return _strip_outer_blank_lines(filtered) + + +def _find_version_section( + lines: list[str], + version: str, +) -> tuple[int, int, int] | None: + for index, line in enumerate(lines): + match = _CHANGELOG_HEADING_RE.match(line) + if match and match.group("version") == version: + section_end = len(lines) + for end_index in range(index + 1, len(lines)): + if lines[end_index].startswith("## "): + section_end = end_index + break + return index, index + 1, section_end + return None + + +def _strip_outer_blank_lines(lines: list[str]) -> list[str]: + while lines and not lines[0].strip(): + lines.pop(0) + while lines and not lines[-1].strip(): + lines.pop() + return lines + + +def _collapse_blank_lines(lines: list[str]) -> list[str]: + collapsed: list[str] = [] + previous_blank = False + for line in lines: + blank = not line.strip() + if blank and previous_blank: + continue + collapsed.append(line) + previous_blank = blank + return collapsed + + +def _replace_once( + pattern: str, + replacement: str, + text: str, + *, + description: str, +) -> str: + updated, count = re.subn(pattern, replacement, text, count=1) + if count != 1: + raise RuntimeError(f"Could not find {description}") + return updated.rstrip("\n") + + +def main(argv: Sequence[str] | None = None) -> None: + parser = argparse.ArgumentParser( + description=( + "Bump the SDK version, roll CHANGELOG.md's Unreleased section into " + "a dated release section, seed a fresh Unreleased section, and " + "refresh uv.lock." + ) + ) + parser.add_argument("version", help="Release version, for example 1.30.0") + parser.add_argument( + "--date", + default=datetime.date.today().isoformat(), + help="Release date in YYYY-MM-DD format. Defaults to today.", + ) + parser.add_argument( + "--skip-lock", + action="store_true", + help="Do not run 'uv lock'. Intended only for local testing.", + ) + args = parser.parse_args(argv) + + repo_root = pathlib.Path(__file__).resolve().parents[1] + version = validate_version(args.version) + release_date = parse_date(args.date) + ensure_clean_worktree(repo_root) + create_release_branch(repo_root, version) + changelog_path = repo_root / "CHANGELOG.md" + pyproject_path = repo_root / "pyproject.toml" + service_path = repo_root / "temporalio" / "service.py" + + changelog_text = finalize_changelog_release( + changelog_path.read_text(encoding="utf-8"), + version=version, + release_date=release_date, + ) + pyproject_text = ( + replace_project_version( + pyproject_path.read_text(encoding="utf-8"), + version, + ) + + "\n" + ) + service_text = ( + replace_service_version( + service_path.read_text(encoding="utf-8"), + version, + ) + + "\n" + ) + + changelog_path.write_text(changelog_text, encoding="utf-8") + pyproject_path.write_text(pyproject_text, encoding="utf-8") + service_path.write_text(service_text, encoding="utf-8") + + if not args.skip_lock: + subprocess.run(["uv", "lock"], cwd=repo_root, check=True) + + ensure_only_release_changes(repo_root) + commit_release_changes(repo_root, version) + push_release_branch(repo_root, version) + create_release_pr(repo_root, version) + + print( + f"Prepared release {version} dated {release_date.isoformat()} and opened a PR" + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/run_bench.py b/scripts/run_bench.py index decbe4810..ba5a6923e 100644 --- a/scripts/run_bench.py +++ b/scripts/run_bench.py @@ -5,9 +5,9 @@ import sys import time import uuid +from collections.abc import AsyncIterator from contextlib import asynccontextmanager from datetime import timedelta -from typing import AsyncIterator from temporalio import activity, workflow from temporalio.testing import WorkflowEnvironment diff --git a/temporalio/activity.py b/temporalio/activity.py index e08081f19..3f69bc17f 100644 --- a/temporalio/activity.py +++ b/temporalio/activity.py @@ -9,29 +9,18 @@ from __future__ import annotations -import asyncio import contextvars import dataclasses import inspect import logging -import threading +from collections.abc import Callable, Iterator, Mapping, MutableMapping, Sequence from contextlib import AbstractContextManager, contextmanager from dataclasses import dataclass from datetime import datetime, timedelta from typing import ( TYPE_CHECKING, Any, - Callable, - Iterator, - List, - Mapping, - MutableMapping, NoReturn, - Optional, - Sequence, - Tuple, - Type, - Union, overload, ) @@ -40,6 +29,9 @@ import temporalio.bridge.proto.activity_task import temporalio.common import temporalio.converter +from temporalio.converter._payload_converter import ( + _TemporalTransferTypePayloadConverter, +) from .types import CallableType @@ -53,7 +45,7 @@ def defn(fn: CallableType) -> CallableType: ... @overload def defn( - *, name: Optional[str] = None, no_thread_cancel_exception: bool = False + *, name: str | None = None, no_thread_cancel_exception: bool = False ) -> Callable[[CallableType], CallableType]: ... @@ -64,9 +56,9 @@ def defn( def defn( - fn: Optional[CallableType] = None, + fn: CallableType | None = None, # type: ignore[reportInvalidTypeVarUse] *, - name: Optional[str] = None, + name: str | None = None, no_thread_cancel_exception: bool = False, dynamic: bool = False, ): @@ -104,6 +96,12 @@ class Info: """Information about the running activity. Retrieved inside an activity via :py:func:`info`. + + .. warning:: + Do not construct this class directly. For testing, use + :py:meth:`temporalio.testing.ActivityEnvironment.default_info` with + :py:func:`dataclasses.replace` to customize fields. This class may have + new required fields added in future versions. """ activity_id: str @@ -111,19 +109,43 @@ class Info: attempt: int current_attempt_scheduled_time: datetime heartbeat_details: Sequence[Any] - heartbeat_timeout: Optional[timedelta] + heartbeat_timeout: timedelta | None is_local: bool - schedule_to_close_timeout: Optional[timedelta] + namespace: str + schedule_to_close_timeout: timedelta | None scheduled_time: datetime - start_to_close_timeout: Optional[timedelta] + start_to_close_timeout: timedelta | None started_time: datetime task_queue: str task_token: bytes - workflow_id: str - workflow_namespace: str - workflow_run_id: str - workflow_type: str + workflow_id: str | None + """ID of the workflow. None if the activity was not started by a workflow.""" + workflow_namespace: str | None + """Namespace of the workflow. None if the activity was not started by a workflow. + + .. deprecated:: + Use :py:attr:`namespace` instead. + """ + workflow_run_id: str | None + """Run ID of the workflow. None if the activity was not started by a workflow.""" + workflow_type: str | None + """Type of the workflow. None if the activity was not started by a workflow.""" priority: temporalio.common.Priority + retry_policy: temporalio.common.RetryPolicy | None + """The retry policy of this activity. + + Note that the server may have set a different policy than the one provided when scheduling the activity. + If the value is None, it means the server didn't send information about retry policy (e.g. due to old server + version), but it may still be defined server-side.""" + + activity_run_id: str | None = None + """Run ID of this activity. None for workflow activities.""" + + @property + def in_workflow(self) -> bool: + """Was this activity started by a workflow?""" + return self.workflow_id is not None + # TODO(cretz): Consider putting identity on here for "worker_id" for logger? def _logger_details(self) -> Mapping[str, Any]: @@ -131,7 +153,7 @@ def _logger_details(self) -> Mapping[str, Any]: "activity_id": self.activity_id, "activity_type": self.activity_type, "attempt": self.attempt, - "namespace": self.workflow_namespace, + "namespace": self.namespace, "task_queue": self.task_queue, "workflow_id": self.workflow_id, "workflow_run_id": self.workflow_run_id, @@ -144,7 +166,7 @@ def _logger_details(self) -> Mapping[str, Any]: @dataclass class _ActivityCancellationDetailsHolder: - details: Optional[ActivityCancellationDetails] = None + details: ActivityCancellationDetails | None = None @dataclass(frozen=True) @@ -176,20 +198,20 @@ def _from_proto( class _Context: info: Callable[[], Info] # This is optional because during interceptor init it is not present - heartbeat: Optional[Callable[..., None]] - cancelled_event: _CompositeEvent - worker_shutdown_event: _CompositeEvent - shield_thread_cancel_exception: Optional[Callable[[], AbstractContextManager]] - payload_converter_class_or_instance: Union[ - Type[temporalio.converter.PayloadConverter], - temporalio.converter.PayloadConverter, - ] - runtime_metric_meter: Optional[temporalio.common.MetricMeter] - client: Optional[Client] + heartbeat: Callable[..., None] | None + cancelled_event: temporalio.common._CompositeEvent + worker_shutdown_event: temporalio.common._CompositeEvent + shield_thread_cancel_exception: Callable[[], AbstractContextManager] | None + payload_converter_class_or_instance: ( + type[temporalio.converter.PayloadConverter] + | temporalio.converter.PayloadConverter + ) + runtime_metric_meter: temporalio.common.MetricMeter | None + client: Client | None cancellation_details: _ActivityCancellationDetailsHolder - _logger_details: Optional[Mapping[str, Any]] = None - _payload_converter: Optional[temporalio.converter.PayloadConverter] = None - _metric_meter: Optional[temporalio.common.MetricMeter] = None + _logger_details: Mapping[str, Any] | None = None + _payload_converter: temporalio.converter.PayloadConverter | None = None + _metric_meter: temporalio.common.MetricMeter | None = None @staticmethod def current() -> _Context: @@ -219,9 +241,13 @@ def payload_converter(self) -> temporalio.converter.PayloadConverter: self.payload_converter_class_or_instance, temporalio.converter.PayloadConverter, ): - self._payload_converter = self.payload_converter_class_or_instance + self._payload_converter = _TemporalTransferTypePayloadConverter.wrap( + self.payload_converter_class_or_instance + ) else: - self._payload_converter = self.payload_converter_class_or_instance() + self._payload_converter = _TemporalTransferTypePayloadConverter.wrap( + self.payload_converter_class_or_instance() + ) return self._payload_converter @property @@ -240,7 +266,7 @@ def metric_meter(self) -> temporalio.common.MetricMeter: info = self.info() self._metric_meter = self.runtime_metric_meter.with_additional_attributes( { - "namespace": info.workflow_namespace, + "namespace": info.namespace, "task_queue": info.task_queue, "activity_type": info.activity_type, } @@ -248,40 +274,10 @@ def metric_meter(self) -> temporalio.common.MetricMeter: return self._metric_meter -@dataclass -class _CompositeEvent: - # This should always be present, but is sometimes lazily set internally - thread_event: Optional[threading.Event] - # Async event only for async activities - async_event: Optional[asyncio.Event] - - def set(self) -> None: - if not self.thread_event: - raise RuntimeError("Missing event") - self.thread_event.set() - if self.async_event: - self.async_event.set() - - def is_set(self) -> bool: - if not self.thread_event: - raise RuntimeError("Missing event") - return self.thread_event.is_set() - - async def wait(self) -> None: - if not self.async_event: - raise RuntimeError("not in async activity") - await self.async_event.wait() - - def wait_sync(self, timeout: Optional[float] = None) -> None: - if not self.thread_event: - raise RuntimeError("Missing event") - self.thread_event.wait(timeout) - - def client() -> Client: """Return a Temporal Client for use in the current activity. - The client is only available in `async def` activities. + The client is only available in ``async def`` activities. In tests it is not available automatically, but you can pass a client when creating a :py:class:`temporalio.testing.ActivityEnvironment`. @@ -323,7 +319,7 @@ def info() -> Info: return _Context.current().info() -def cancellation_details() -> Optional[ActivityCancellationDetails]: +def cancellation_details() -> ActivityCancellationDetails | None: """Cancellation details of the current activity, if any. Once set, cancellation details do not change.""" return _Context.current().cancellation_details.details @@ -391,7 +387,7 @@ async def wait_for_cancelled() -> None: await _Context.current().cancelled_event.wait() -def wait_for_cancelled_sync(timeout: Optional[Union[timedelta, float]] = None) -> None: +def wait_for_cancelled_sync(timeout: timedelta | float | None = None) -> None: """Synchronously block while waiting for a cancellation request on this activity. @@ -430,7 +426,7 @@ async def wait_for_worker_shutdown() -> None: def wait_for_worker_shutdown_sync( - timeout: Optional[Union[timedelta, float]] = None, + timeout: timedelta | float | None = None, ) -> None: """Synchronously block while waiting for shutdown to be called on the worker. @@ -463,6 +459,7 @@ class _CompleteAsyncError(BaseException): def payload_converter() -> temporalio.converter.PayloadConverter: """Get the payload converter for the current activity. + The returned converter has :py:class:`temporalio.converter.ActivitySerializationContext` set. This is often used for dynamic activities to convert payloads. """ return _Context.current().payload_converter @@ -503,9 +500,7 @@ class LoggerAdapter(logging.LoggerAdapter): use by others. Default is False. """ - def __init__( - self, logger: logging.Logger, extra: Optional[Mapping[str, Any]] - ) -> None: + def __init__(self, logger: logging.Logger, extra: Mapping[str, Any] | None) -> None: """Create the logger adapter.""" super().__init__(logger, extra or {}) self.activity_info_on_message = True @@ -514,7 +509,7 @@ def __init__( def process( self, msg: Any, kwargs: MutableMapping[str, Any] - ) -> Tuple[Any, MutableMapping[str, Any]]: + ) -> tuple[Any, MutableMapping[str, Any]]: """Override to add activity details.""" if ( self.activity_info_on_message @@ -525,15 +520,14 @@ def process( if context: if self.activity_info_on_message: msg = f"{msg} ({context.logger_details})" - if self.activity_info_on_extra: - # Extra can be absent or None, this handles both - extra = kwargs.get("extra", None) or {} - extra["temporal_activity"] = context.logger_details - kwargs["extra"] = extra - if self.full_activity_info_on_extra: - # Extra can be absent or None, this handles both - extra = kwargs.get("extra", None) or {} - extra["activity_info"] = context.info() + if self.activity_info_on_extra or self.full_activity_info_on_extra: + # Copy any caller-provided extra rather than mutating it in + # place (see https://github.com/temporalio/sdk-python/issues/503). + extra = dict(kwargs.get("extra") or {}) + if self.activity_info_on_extra: + extra["temporal_activity"] = context.logger_details + if self.full_activity_info_on_extra: + extra["activity_info"] = context.info() kwargs["extra"] = extra return (msg, kwargs) @@ -551,16 +545,16 @@ def base_logger(self) -> logging.Logger: @dataclass(frozen=True) class _Definition: - name: Optional[str] + name: str | None fn: Callable is_async: bool no_thread_cancel_exception: bool # Types loaded on post init if both are None - arg_types: Optional[List[Type]] = None - ret_type: Optional[Type] = None + arg_types: list[type] | None = None + ret_type: type | None = None @staticmethod - def from_callable(fn: Callable) -> Optional[_Definition]: + def from_callable(fn: Callable) -> _Definition | None: defn = getattr(fn, "__temporal_activity_definition", None) if isinstance(defn, _Definition): # We have to replace the function with the given callable here @@ -580,18 +574,32 @@ def must_from_callable(fn: Callable) -> _Definition: f"Activity {fn_name} missing attributes, was it decorated with @activity.defn?" ) + @classmethod + def get_name_and_result_type( + cls, name_or_run_fn: str | Callable[..., Any] + ) -> tuple[str, type | None]: + if isinstance(name_or_run_fn, str): + return name_or_run_fn, None + elif callable(name_or_run_fn): + defn = cls.must_from_callable(name_or_run_fn) + if not defn.name: + raise ValueError(f"Activity {name_or_run_fn} definition has no name") + return defn.name, defn.ret_type + else: + raise TypeError("Activity must be a string or callable") # type:ignore[reportUnreachable] + @staticmethod def _apply_to_callable( fn: Callable, *, - activity_name: Optional[str], + activity_name: str | None, no_thread_cancel_exception: bool = False, ) -> None: # Validate the activity if hasattr(fn, "__temporal_activity_definition"): raise ValueError("Function already contains activity definition") elif not callable(fn): - raise TypeError("Activity is not callable") + raise TypeError("Activity is not callable") # type:ignore[reportUnreachable] # We do not allow keyword only arguments in activities sig = inspect.signature(fn) for param in sig.parameters.values(): diff --git a/temporalio/api/activity/v1/__init__.py b/temporalio/api/activity/v1/__init__.py index a6e54842f..270022714 100644 --- a/temporalio/api/activity/v1/__init__.py +++ b/temporalio/api/activity/v1/__init__.py @@ -1,5 +1,15 @@ -from .message_pb2 import ActivityOptions +from .message_pb2 import ( + ActivityExecutionInfo, + ActivityExecutionListInfo, + ActivityExecutionOutcome, + ActivityOptions, + CallbackInfo, +) __all__ = [ + "ActivityExecutionInfo", + "ActivityExecutionListInfo", + "ActivityExecutionOutcome", "ActivityOptions", + "CallbackInfo", ] diff --git a/temporalio/api/activity/v1/message_pb2.py b/temporalio/api/activity/v1/message_pb2.py index 58f1d95cb..59f79a3c5 100644 --- a/temporalio/api/activity/v1/message_pb2.py +++ b/temporalio/api/activity/v1/message_pb2.py @@ -15,34 +15,137 @@ from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2 +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 +from temporalio.api.callback.v1 import ( + message_pb2 as temporal_dot_api_dot_callback_dot_v1_dot_message__pb2, +) from temporalio.api.common.v1 import ( message_pb2 as temporal_dot_api_dot_common_dot_v1_dot_message__pb2, ) +from temporalio.api.deployment.v1 import ( + message_pb2 as temporal_dot_api_dot_deployment_dot_v1_dot_message__pb2, +) +from temporalio.api.enums.v1 import ( + activity_pb2 as temporal_dot_api_dot_enums_dot_v1_dot_activity__pb2, +) +from temporalio.api.enums.v1 import ( + workflow_pb2 as temporal_dot_api_dot_enums_dot_v1_dot_workflow__pb2, +) +from temporalio.api.failure.v1 import ( + message_pb2 as temporal_dot_api_dot_failure_dot_v1_dot_message__pb2, +) +from temporalio.api.sdk.v1 import ( + user_metadata_pb2 as temporal_dot_api_dot_sdk_dot_v1_dot_user__metadata__pb2, +) from temporalio.api.taskqueue.v1 import ( message_pb2 as temporal_dot_api_dot_taskqueue_dot_v1_dot_message__pb2, ) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b"\n&temporal/api/activity/v1/message.proto\x12\x18temporal.api.activity.v1\x1a$temporal/api/common/v1/message.proto\x1a'temporal/api/taskqueue/v1/message.proto\x1a\x1egoogle/protobuf/duration.proto\"\xf3\x02\n\x0f\x41\x63tivityOptions\x12\x38\n\ntask_queue\x18\x01 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12<\n\x19schedule_to_close_timeout\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration\x12<\n\x19schedule_to_start_timeout\x18\x03 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x34\n\x11heartbeat_timeout\x18\x05 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x0cretry_policy\x18\x06 \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicyB\x93\x01\n\x1bio.temporal.api.activity.v1B\x0cMessageProtoP\x01Z'go.temporal.io/api/activity/v1;activity\xaa\x02\x1aTemporalio.Api.Activity.V1\xea\x02\x1dTemporalio::Api::Activity::V1b\x06proto3" + b'\n&temporal/api/activity/v1/message.proto\x12\x18temporal.api.activity.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\x1a(temporal/api/deployment/v1/message.proto\x1a$temporal/api/enums/v1/activity.proto\x1a&temporal/api/callback/v1/message.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a%temporal/api/failure/v1/message.proto\x1a\'temporal/api/taskqueue/v1/message.proto\x1a\'temporal/api/sdk/v1/user_metadata.proto"\xc4\x01\n\x18\x41\x63tivityExecutionOutcome\x12\x32\n\x06result\x18\x01 \x01(\x0b\x32 .temporal.api.common.v1.PayloadsH\x00\x12\x33\n\x07\x66\x61ilure\x18\x02 \x01(\x0b\x32 .temporal.api.failure.v1.FailureH\x00\x12\x36\n\x0bretry_state\x18\x03 \x01(\x0e\x32!.temporal.api.enums.v1.RetryStateB\x07\n\x05value"\xd7\x03\n\x0f\x41\x63tivityOptions\x12\x38\n\ntask_queue\x18\x01 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12<\n\x19schedule_to_close_timeout\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration\x12<\n\x19schedule_to_start_timeout\x18\x03 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x34\n\x11heartbeat_timeout\x18\x05 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x0cretry_policy\x18\x06 \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x32\n\x08priority\x18\x07 \x01(\x0b\x32 .temporal.api.common.v1.Priority\x12.\n\x0bstart_delay\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration"\xb6\x0e\n\x15\x41\x63tivityExecutionInfo\x12\x13\n\x0b\x61\x63tivity_id\x18\x01 \x01(\t\x12\x0e\n\x06run_id\x18\x02 \x01(\t\x12;\n\ractivity_type\x18\x03 \x01(\x0b\x32$.temporal.api.common.v1.ActivityType\x12>\n\x06status\x18\x04 \x01(\x0e\x32..temporal.api.enums.v1.ActivityExecutionStatus\x12>\n\trun_state\x18\x05 \x01(\x0e\x32+.temporal.api.enums.v1.PendingActivityState\x12\x12\n\ntask_queue\x18\x06 \x01(\t\x12<\n\x19schedule_to_close_timeout\x18\x07 \x01(\x0b\x32\x19.google.protobuf.Duration\x12<\n\x19schedule_to_start_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x34\n\x11heartbeat_timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x0cretry_policy\x18\x0b \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12;\n\x11heartbeat_details\x18\x0c \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x37\n\x13last_heartbeat_time\x18\r \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x35\n\x11last_started_time\x18\x0e \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0f\n\x07\x61ttempt\x18\x0f \x01(\x05\x12\x35\n\x12\x65xecution_duration\x18\x10 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x31\n\rschedule_time\x18\x11 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x33\n\x0f\x65xpiration_time\x18\x12 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12.\n\nclose_time\x18\x13 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x36\n\x0clast_failure\x18\x14 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x1c\n\x14last_worker_identity\x18\x15 \x01(\t\x12\x39\n\x16\x63urrent_retry_interval\x18\x16 \x01(\x0b\x32\x19.google.protobuf.Duration\x12>\n\x1alast_attempt_complete_time\x18\x17 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12>\n\x1anext_attempt_schedule_time\x18\x18 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12T\n\x17last_deployment_version\x18\x19 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12\x32\n\x08priority\x18\x1a \x01(\x0b\x32 .temporal.api.common.v1.Priority\x12\x1e\n\x16state_transition_count\x18\x1b \x01(\x03\x12\x18\n\x10state_size_bytes\x18\x1c \x01(\x03\x12\x43\n\x11search_attributes\x18\x1d \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12.\n\x06header\x18\x1e \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12\x38\n\ruser_metadata\x18\x1f \x01(\x0b\x32!.temporal.api.sdk.v1.UserMetadata\x12\x17\n\x0f\x63\x61nceled_reason\x18 \x01(\t\x12+\n\x05links\x18! \x03(\x0b\x32\x1c.temporal.api.common.v1.Link\x12\x1d\n\x15total_heartbeat_count\x18" \x01(\x03\x12\x10\n\x08sdk_name\x18# \x01(\t\x12\x13\n\x0bsdk_version\x18$ \x01(\t\x12.\n\x0bstart_delay\x18% \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x32\n\x0e\x65xecution_time\x18& \x01(\x0b\x32\x1a.google.protobuf.Timestamp"\x9e\x04\n\x19\x41\x63tivityExecutionListInfo\x12\x13\n\x0b\x61\x63tivity_id\x18\x01 \x01(\t\x12\x0e\n\x06run_id\x18\x02 \x01(\t\x12;\n\ractivity_type\x18\x03 \x01(\x0b\x32$.temporal.api.common.v1.ActivityType\x12\x31\n\rschedule_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12.\n\nclose_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12>\n\x06status\x18\x06 \x01(\x0e\x32..temporal.api.enums.v1.ActivityExecutionStatus\x12\x43\n\x11search_attributes\x18\x07 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12\x12\n\ntask_queue\x18\x08 \x01(\t\x12\x1e\n\x16state_transition_count\x18\t \x01(\x03\x12\x18\n\x10state_size_bytes\x18\n \x01(\x03\x12\x35\n\x12\x65xecution_duration\x18\x0b \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x32\n\x0e\x65xecution_time\x18\x0c \x01(\x0b\x32\x1a.google.protobuf.Timestamp"\xff\x01\n\x0c\x43\x61llbackInfo\x12?\n\x07trigger\x18\x01 \x01(\x0b\x32..temporal.api.activity.v1.CallbackInfo.Trigger\x12\x34\n\x04info\x18\x02 \x01(\x0b\x32&.temporal.api.callback.v1.CallbackInfo\x1a\x10\n\x0e\x41\x63tivityClosed\x1a\x66\n\x07Trigger\x12P\n\x0f\x61\x63tivity_closed\x18\x01 \x01(\x0b\x32\x35.temporal.api.activity.v1.CallbackInfo.ActivityClosedH\x00\x42\t\n\x07variantB\x93\x01\n\x1bio.temporal.api.activity.v1B\x0cMessageProtoP\x01Z\'go.temporal.io/api/activity/v1;activity\xaa\x02\x1aTemporalio.Api.Activity.V1\xea\x02\x1dTemporalio::Api::Activity::V1b\x06proto3' ) +_ACTIVITYEXECUTIONOUTCOME = DESCRIPTOR.message_types_by_name["ActivityExecutionOutcome"] _ACTIVITYOPTIONS = DESCRIPTOR.message_types_by_name["ActivityOptions"] +_ACTIVITYEXECUTIONINFO = DESCRIPTOR.message_types_by_name["ActivityExecutionInfo"] +_ACTIVITYEXECUTIONLISTINFO = DESCRIPTOR.message_types_by_name[ + "ActivityExecutionListInfo" +] +_CALLBACKINFO = DESCRIPTOR.message_types_by_name["CallbackInfo"] +_CALLBACKINFO_ACTIVITYCLOSED = _CALLBACKINFO.nested_types_by_name["ActivityClosed"] +_CALLBACKINFO_TRIGGER = _CALLBACKINFO.nested_types_by_name["Trigger"] +ActivityExecutionOutcome = _reflection.GeneratedProtocolMessageType( + "ActivityExecutionOutcome", + (_message.Message,), + { + "DESCRIPTOR": _ACTIVITYEXECUTIONOUTCOME, + "__module__": "temporalio.api.activity.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.activity.v1.ActivityExecutionOutcome) + }, +) +_sym_db.RegisterMessage(ActivityExecutionOutcome) + ActivityOptions = _reflection.GeneratedProtocolMessageType( "ActivityOptions", (_message.Message,), { "DESCRIPTOR": _ACTIVITYOPTIONS, - "__module__": "temporal.api.activity.v1.message_pb2", + "__module__": "temporalio.api.activity.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.activity.v1.ActivityOptions) }, ) _sym_db.RegisterMessage(ActivityOptions) +ActivityExecutionInfo = _reflection.GeneratedProtocolMessageType( + "ActivityExecutionInfo", + (_message.Message,), + { + "DESCRIPTOR": _ACTIVITYEXECUTIONINFO, + "__module__": "temporalio.api.activity.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.activity.v1.ActivityExecutionInfo) + }, +) +_sym_db.RegisterMessage(ActivityExecutionInfo) + +ActivityExecutionListInfo = _reflection.GeneratedProtocolMessageType( + "ActivityExecutionListInfo", + (_message.Message,), + { + "DESCRIPTOR": _ACTIVITYEXECUTIONLISTINFO, + "__module__": "temporalio.api.activity.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.activity.v1.ActivityExecutionListInfo) + }, +) +_sym_db.RegisterMessage(ActivityExecutionListInfo) + +CallbackInfo = _reflection.GeneratedProtocolMessageType( + "CallbackInfo", + (_message.Message,), + { + "ActivityClosed": _reflection.GeneratedProtocolMessageType( + "ActivityClosed", + (_message.Message,), + { + "DESCRIPTOR": _CALLBACKINFO_ACTIVITYCLOSED, + "__module__": "temporalio.api.activity.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.activity.v1.CallbackInfo.ActivityClosed) + }, + ), + "Trigger": _reflection.GeneratedProtocolMessageType( + "Trigger", + (_message.Message,), + { + "DESCRIPTOR": _CALLBACKINFO_TRIGGER, + "__module__": "temporalio.api.activity.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.activity.v1.CallbackInfo.Trigger) + }, + ), + "DESCRIPTOR": _CALLBACKINFO, + "__module__": "temporalio.api.activity.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.activity.v1.CallbackInfo) + }, +) +_sym_db.RegisterMessage(CallbackInfo) +_sym_db.RegisterMessage(CallbackInfo.ActivityClosed) +_sym_db.RegisterMessage(CallbackInfo.Trigger) + if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b"\n\033io.temporal.api.activity.v1B\014MessageProtoP\001Z'go.temporal.io/api/activity/v1;activity\252\002\032Temporalio.Api.Activity.V1\352\002\035Temporalio::Api::Activity::V1" - _ACTIVITYOPTIONS._serialized_start = 180 - _ACTIVITYOPTIONS._serialized_end = 551 + _ACTIVITYEXECUTIONOUTCOME._serialized_start = 451 + _ACTIVITYEXECUTIONOUTCOME._serialized_end = 647 + _ACTIVITYOPTIONS._serialized_start = 650 + _ACTIVITYOPTIONS._serialized_end = 1121 + _ACTIVITYEXECUTIONINFO._serialized_start = 1124 + _ACTIVITYEXECUTIONINFO._serialized_end = 2970 + _ACTIVITYEXECUTIONLISTINFO._serialized_start = 2973 + _ACTIVITYEXECUTIONLISTINFO._serialized_end = 3515 + _CALLBACKINFO._serialized_start = 3518 + _CALLBACKINFO._serialized_end = 3773 + _CALLBACKINFO_ACTIVITYCLOSED._serialized_start = 3653 + _CALLBACKINFO_ACTIVITYCLOSED._serialized_end = 3669 + _CALLBACKINFO_TRIGGER._serialized_start = 3671 + _CALLBACKINFO_TRIGGER._serialized_end = 3773 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/activity/v1/message_pb2.pyi b/temporalio/api/activity/v1/message_pb2.pyi index 373c5f18f..684233f95 100644 --- a/temporalio/api/activity/v1/message_pb2.pyi +++ b/temporalio/api/activity/v1/message_pb2.pyi @@ -4,13 +4,22 @@ isort:skip_file """ import builtins +import collections.abc import sys import google.protobuf.descriptor import google.protobuf.duration_pb2 +import google.protobuf.internal.containers import google.protobuf.message +import google.protobuf.timestamp_pb2 +import temporalio.api.callback.v1.message_pb2 import temporalio.api.common.v1.message_pb2 +import temporalio.api.deployment.v1.message_pb2 +import temporalio.api.enums.v1.activity_pb2 +import temporalio.api.enums.v1.workflow_pb2 +import temporalio.api.failure.v1.message_pb2 +import temporalio.api.sdk.v1.user_metadata_pb2 import temporalio.api.taskqueue.v1.message_pb2 if sys.version_info >= (3, 8): @@ -20,6 +29,56 @@ else: DESCRIPTOR: google.protobuf.descriptor.FileDescriptor +class ActivityExecutionOutcome(google.protobuf.message.Message): + """The outcome of a completed activity execution: either a successful result or a failure.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + RESULT_FIELD_NUMBER: builtins.int + FAILURE_FIELD_NUMBER: builtins.int + RETRY_STATE_FIELD_NUMBER: builtins.int + @property + def result(self) -> temporalio.api.common.v1.message_pb2.Payloads: + """The result if the activity completed successfully.""" + @property + def failure(self) -> temporalio.api.failure.v1.message_pb2.Failure: + """The failure if the activity completed unsuccessfully.""" + retry_state: temporalio.api.enums.v1.workflow_pb2.RetryState.ValueType + """The retry state associated with an unsuccessful activity execution. + This field is only meaningful when `failure` is set. + """ + def __init__( + self, + *, + result: temporalio.api.common.v1.message_pb2.Payloads | None = ..., + failure: temporalio.api.failure.v1.message_pb2.Failure | None = ..., + retry_state: temporalio.api.enums.v1.workflow_pb2.RetryState.ValueType = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "failure", b"failure", "result", b"result", "value", b"value" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "failure", + b"failure", + "result", + b"result", + "retry_state", + b"retry_state", + "value", + b"value", + ], + ) -> None: ... + def WhichOneof( + self, oneof_group: typing_extensions.Literal["value", b"value"] + ) -> typing_extensions.Literal["result", "failure"] | None: ... + +global___ActivityExecutionOutcome = ActivityExecutionOutcome + class ActivityOptions(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -29,6 +88,8 @@ class ActivityOptions(google.protobuf.message.Message): START_TO_CLOSE_TIMEOUT_FIELD_NUMBER: builtins.int HEARTBEAT_TIMEOUT_FIELD_NUMBER: builtins.int RETRY_POLICY_FIELD_NUMBER: builtins.int + PRIORITY_FIELD_NUMBER: builtins.int + START_DELAY_FIELD_NUMBER: builtins.int @property def task_queue(self) -> temporalio.api.taskqueue.v1.message_pb2.TaskQueue: ... @property @@ -62,7 +123,19 @@ class ActivityOptions(google.protobuf.message.Message): def heartbeat_timeout(self) -> google.protobuf.duration_pb2.Duration: """Maximum permitted time between successful worker heartbeats.""" @property - def retry_policy(self) -> temporalio.api.common.v1.message_pb2.RetryPolicy: ... + def retry_policy(self) -> temporalio.api.common.v1.message_pb2.RetryPolicy: + """The retry policy for the activity. Will never exceed `schedule_to_close_timeout`.""" + @property + def priority(self) -> temporalio.api.common.v1.message_pb2.Priority: + """Priority metadata. If this message is not present, or any fields are not + present, they inherit the values from the workflow. + """ + @property + def start_delay(self) -> google.protobuf.duration_pb2.Duration: + """Time to wait before making the first activity task available for dispatch. This delay is not applied to retry attempts. + When updated, the time is added to the original `schedule_time`, not to the current time. + If the resulting time is in the past, the task is made available for dispatch immediately. + """ def __init__( self, *, @@ -72,18 +145,24 @@ class ActivityOptions(google.protobuf.message.Message): start_to_close_timeout: google.protobuf.duration_pb2.Duration | None = ..., heartbeat_timeout: google.protobuf.duration_pb2.Duration | None = ..., retry_policy: temporalio.api.common.v1.message_pb2.RetryPolicy | None = ..., + priority: temporalio.api.common.v1.message_pb2.Priority | None = ..., + start_delay: google.protobuf.duration_pb2.Duration | None = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal[ "heartbeat_timeout", b"heartbeat_timeout", + "priority", + b"priority", "retry_policy", b"retry_policy", "schedule_to_close_timeout", b"schedule_to_close_timeout", "schedule_to_start_timeout", b"schedule_to_start_timeout", + "start_delay", + b"start_delay", "start_to_close_timeout", b"start_to_close_timeout", "task_queue", @@ -95,12 +174,16 @@ class ActivityOptions(google.protobuf.message.Message): field_name: typing_extensions.Literal[ "heartbeat_timeout", b"heartbeat_timeout", + "priority", + b"priority", "retry_policy", b"retry_policy", "schedule_to_close_timeout", b"schedule_to_close_timeout", "schedule_to_start_timeout", b"schedule_to_start_timeout", + "start_delay", + b"start_delay", "start_to_close_timeout", b"start_to_close_timeout", "task_queue", @@ -109,3 +192,563 @@ class ActivityOptions(google.protobuf.message.Message): ) -> None: ... global___ActivityOptions = ActivityOptions + +class ActivityExecutionInfo(google.protobuf.message.Message): + """Information about a standalone activity.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ACTIVITY_ID_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + ACTIVITY_TYPE_FIELD_NUMBER: builtins.int + STATUS_FIELD_NUMBER: builtins.int + RUN_STATE_FIELD_NUMBER: builtins.int + TASK_QUEUE_FIELD_NUMBER: builtins.int + SCHEDULE_TO_CLOSE_TIMEOUT_FIELD_NUMBER: builtins.int + SCHEDULE_TO_START_TIMEOUT_FIELD_NUMBER: builtins.int + START_TO_CLOSE_TIMEOUT_FIELD_NUMBER: builtins.int + HEARTBEAT_TIMEOUT_FIELD_NUMBER: builtins.int + RETRY_POLICY_FIELD_NUMBER: builtins.int + HEARTBEAT_DETAILS_FIELD_NUMBER: builtins.int + LAST_HEARTBEAT_TIME_FIELD_NUMBER: builtins.int + LAST_STARTED_TIME_FIELD_NUMBER: builtins.int + ATTEMPT_FIELD_NUMBER: builtins.int + EXECUTION_DURATION_FIELD_NUMBER: builtins.int + SCHEDULE_TIME_FIELD_NUMBER: builtins.int + EXPIRATION_TIME_FIELD_NUMBER: builtins.int + CLOSE_TIME_FIELD_NUMBER: builtins.int + LAST_FAILURE_FIELD_NUMBER: builtins.int + LAST_WORKER_IDENTITY_FIELD_NUMBER: builtins.int + CURRENT_RETRY_INTERVAL_FIELD_NUMBER: builtins.int + LAST_ATTEMPT_COMPLETE_TIME_FIELD_NUMBER: builtins.int + NEXT_ATTEMPT_SCHEDULE_TIME_FIELD_NUMBER: builtins.int + LAST_DEPLOYMENT_VERSION_FIELD_NUMBER: builtins.int + PRIORITY_FIELD_NUMBER: builtins.int + STATE_TRANSITION_COUNT_FIELD_NUMBER: builtins.int + STATE_SIZE_BYTES_FIELD_NUMBER: builtins.int + SEARCH_ATTRIBUTES_FIELD_NUMBER: builtins.int + HEADER_FIELD_NUMBER: builtins.int + USER_METADATA_FIELD_NUMBER: builtins.int + CANCELED_REASON_FIELD_NUMBER: builtins.int + LINKS_FIELD_NUMBER: builtins.int + TOTAL_HEARTBEAT_COUNT_FIELD_NUMBER: builtins.int + SDK_NAME_FIELD_NUMBER: builtins.int + SDK_VERSION_FIELD_NUMBER: builtins.int + START_DELAY_FIELD_NUMBER: builtins.int + EXECUTION_TIME_FIELD_NUMBER: builtins.int + activity_id: builtins.str + """Unique identifier of this activity within its namespace along with run ID (below).""" + run_id: builtins.str + @property + def activity_type(self) -> temporalio.api.common.v1.message_pb2.ActivityType: + """The type of the activity, a string that maps to a registered activity on a worker.""" + status: temporalio.api.enums.v1.activity_pb2.ActivityExecutionStatus.ValueType + """A general status for this activity, indicates whether it is currently running or in one of the terminal statuses.""" + run_state: temporalio.api.enums.v1.workflow_pb2.PendingActivityState.ValueType + """More detailed breakdown of ACTIVITY_EXECUTION_STATUS_RUNNING.""" + task_queue: builtins.str + @property + def schedule_to_close_timeout(self) -> google.protobuf.duration_pb2.Duration: + """Indicates how long the caller is willing to wait for an activity completion. Limits how long + retries will be attempted. + + (-- api-linter: core::0140::prepositions=disabled + aip.dev/not-precedent: "to" is used to indicate interval. --) + """ + @property + def schedule_to_start_timeout(self) -> google.protobuf.duration_pb2.Duration: + """Limits time an activity task can stay in a task queue before a worker picks it up. This + timeout is always non retryable, as all a retry would achieve is to put it back into the same + queue. Defaults to `schedule_to_close_timeout`. + + (-- api-linter: core::0140::prepositions=disabled + aip.dev/not-precedent: "to" is used to indicate interval. --) + """ + @property + def start_to_close_timeout(self) -> google.protobuf.duration_pb2.Duration: + """Maximum time a single activity attempt is allowed to execute after being picked up by a worker. This + timeout is always retryable. + + (-- api-linter: core::0140::prepositions=disabled + aip.dev/not-precedent: "to" is used to indicate interval. --) + """ + @property + def heartbeat_timeout(self) -> google.protobuf.duration_pb2.Duration: + """Maximum permitted time between successful worker heartbeats.""" + @property + def retry_policy(self) -> temporalio.api.common.v1.message_pb2.RetryPolicy: + """The retry policy for the activity. Will never exceed `schedule_to_close_timeout`.""" + @property + def heartbeat_details(self) -> temporalio.api.common.v1.message_pb2.Payloads: + """Details provided in the last recorded activity heartbeat. + DescribeActivityExecution does not set this field unless include_heartbeat_details was true in the request. + """ + @property + def last_heartbeat_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Time the last heartbeat was recorded.""" + @property + def last_started_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Time the last attempt was started.""" + attempt: builtins.int + """The attempt this activity is currently on. Incremented each time a new attempt is scheduled.""" + @property + def execution_duration(self) -> google.protobuf.duration_pb2.Duration: + """How long this activity has been running for, including all attempts and backoff between attempts.""" + @property + def schedule_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Time the activity was originally scheduled via a StartActivityExecution request.""" + @property + def expiration_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """The time at which the activity's Schedule-to-Close timeout expires. + Calculated as `schedule_time` + `start_delay` + `schedule_to_close_timeout`. + """ + @property + def close_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Time when the activity transitioned to a closed state.""" + @property + def last_failure(self) -> temporalio.api.failure.v1.message_pb2.Failure: + """Failure details from the last failed attempt. + DescribeActivityExecution does not set this field unless include_last_failure was true in the request. + """ + last_worker_identity: builtins.str + @property + def current_retry_interval(self) -> google.protobuf.duration_pb2.Duration: + """Time from the last attempt failure to the next activity retry. + If the activity is currently running, this represents the next retry interval in case the attempt fails. + If activity is currently backing off between attempt, this represents the current retry interval. + If there is no next retry allowed, this field will be null. + This interval is typically calculated from the specified retry policy, but may be modified if an activity fails + with a retryable application failure specifying a retry delay. + """ + @property + def last_attempt_complete_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """The time when the last activity attempt completed. If activity has not been completed yet, it will be null.""" + @property + def next_attempt_schedule_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """The time when the next activity attempt will be scheduled. + If activity is currently scheduled or started, this field will be null. + """ + @property + def last_deployment_version( + self, + ) -> temporalio.api.deployment.v1.message_pb2.WorkerDeploymentVersion: + """The Worker Deployment Version this activity was dispatched to most recently. + If nil, the activity has not yet been dispatched or was last dispatched to an unversioned worker. + """ + @property + def priority(self) -> temporalio.api.common.v1.message_pb2.Priority: + """Priority metadata.""" + state_transition_count: builtins.int + """Incremented each time the activity's state is mutated in persistence.""" + state_size_bytes: builtins.int + """Updated once on scheduled and once on terminal status.""" + @property + def search_attributes( + self, + ) -> temporalio.api.common.v1.message_pb2.SearchAttributes: ... + @property + def header(self) -> temporalio.api.common.v1.message_pb2.Header: ... + @property + def user_metadata(self) -> temporalio.api.sdk.v1.user_metadata_pb2.UserMetadata: + """Metadata for use by user interfaces to display the fixed as-of-start summary and details of the activity.""" + canceled_reason: builtins.str + """Set if activity cancelation was requested.""" + @property + def links( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.common.v1.message_pb2.Link + ]: + """Links to related entities, such as the entity that started this activity.""" + total_heartbeat_count: builtins.int + """Total number of heartbeats recorded across all attempts of this activity, including retries.""" + sdk_name: builtins.str + """The name of the SDK of the worker that most recently picked up an attempt of this activity. + Overwritten on each new attempt. Empty if unknown. + """ + sdk_version: builtins.str + """The version of the SDK of the worker that most recently picked up an attempt of this activity. + Overwritten on each new attempt. Empty if unknown. + """ + @property + def start_delay(self) -> google.protobuf.duration_pb2.Duration: + """Time to wait before making the first activity task available for dispatch. This delay is not applied to retry attempts.""" + @property + def execution_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """The time at which the first activity task is made available for dispatch, computed as + `schedule_time + start_delay`. Same as `schedule_time` if `start_delay` is not set. + """ + def __init__( + self, + *, + activity_id: builtins.str = ..., + run_id: builtins.str = ..., + activity_type: temporalio.api.common.v1.message_pb2.ActivityType | None = ..., + status: temporalio.api.enums.v1.activity_pb2.ActivityExecutionStatus.ValueType = ..., + run_state: temporalio.api.enums.v1.workflow_pb2.PendingActivityState.ValueType = ..., + task_queue: builtins.str = ..., + schedule_to_close_timeout: google.protobuf.duration_pb2.Duration | None = ..., + schedule_to_start_timeout: google.protobuf.duration_pb2.Duration | None = ..., + start_to_close_timeout: google.protobuf.duration_pb2.Duration | None = ..., + heartbeat_timeout: google.protobuf.duration_pb2.Duration | None = ..., + retry_policy: temporalio.api.common.v1.message_pb2.RetryPolicy | None = ..., + heartbeat_details: temporalio.api.common.v1.message_pb2.Payloads | None = ..., + last_heartbeat_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + last_started_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + attempt: builtins.int = ..., + execution_duration: google.protobuf.duration_pb2.Duration | None = ..., + schedule_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + expiration_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + close_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + last_failure: temporalio.api.failure.v1.message_pb2.Failure | None = ..., + last_worker_identity: builtins.str = ..., + current_retry_interval: google.protobuf.duration_pb2.Duration | None = ..., + last_attempt_complete_time: google.protobuf.timestamp_pb2.Timestamp + | None = ..., + next_attempt_schedule_time: google.protobuf.timestamp_pb2.Timestamp + | None = ..., + last_deployment_version: temporalio.api.deployment.v1.message_pb2.WorkerDeploymentVersion + | None = ..., + priority: temporalio.api.common.v1.message_pb2.Priority | None = ..., + state_transition_count: builtins.int = ..., + state_size_bytes: builtins.int = ..., + search_attributes: temporalio.api.common.v1.message_pb2.SearchAttributes + | None = ..., + header: temporalio.api.common.v1.message_pb2.Header | None = ..., + user_metadata: temporalio.api.sdk.v1.user_metadata_pb2.UserMetadata + | None = ..., + canceled_reason: builtins.str = ..., + links: collections.abc.Iterable[temporalio.api.common.v1.message_pb2.Link] + | None = ..., + total_heartbeat_count: builtins.int = ..., + sdk_name: builtins.str = ..., + sdk_version: builtins.str = ..., + start_delay: google.protobuf.duration_pb2.Duration | None = ..., + execution_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "activity_type", + b"activity_type", + "close_time", + b"close_time", + "current_retry_interval", + b"current_retry_interval", + "execution_duration", + b"execution_duration", + "execution_time", + b"execution_time", + "expiration_time", + b"expiration_time", + "header", + b"header", + "heartbeat_details", + b"heartbeat_details", + "heartbeat_timeout", + b"heartbeat_timeout", + "last_attempt_complete_time", + b"last_attempt_complete_time", + "last_deployment_version", + b"last_deployment_version", + "last_failure", + b"last_failure", + "last_heartbeat_time", + b"last_heartbeat_time", + "last_started_time", + b"last_started_time", + "next_attempt_schedule_time", + b"next_attempt_schedule_time", + "priority", + b"priority", + "retry_policy", + b"retry_policy", + "schedule_time", + b"schedule_time", + "schedule_to_close_timeout", + b"schedule_to_close_timeout", + "schedule_to_start_timeout", + b"schedule_to_start_timeout", + "search_attributes", + b"search_attributes", + "start_delay", + b"start_delay", + "start_to_close_timeout", + b"start_to_close_timeout", + "user_metadata", + b"user_metadata", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "activity_id", + b"activity_id", + "activity_type", + b"activity_type", + "attempt", + b"attempt", + "canceled_reason", + b"canceled_reason", + "close_time", + b"close_time", + "current_retry_interval", + b"current_retry_interval", + "execution_duration", + b"execution_duration", + "execution_time", + b"execution_time", + "expiration_time", + b"expiration_time", + "header", + b"header", + "heartbeat_details", + b"heartbeat_details", + "heartbeat_timeout", + b"heartbeat_timeout", + "last_attempt_complete_time", + b"last_attempt_complete_time", + "last_deployment_version", + b"last_deployment_version", + "last_failure", + b"last_failure", + "last_heartbeat_time", + b"last_heartbeat_time", + "last_started_time", + b"last_started_time", + "last_worker_identity", + b"last_worker_identity", + "links", + b"links", + "next_attempt_schedule_time", + b"next_attempt_schedule_time", + "priority", + b"priority", + "retry_policy", + b"retry_policy", + "run_id", + b"run_id", + "run_state", + b"run_state", + "schedule_time", + b"schedule_time", + "schedule_to_close_timeout", + b"schedule_to_close_timeout", + "schedule_to_start_timeout", + b"schedule_to_start_timeout", + "sdk_name", + b"sdk_name", + "sdk_version", + b"sdk_version", + "search_attributes", + b"search_attributes", + "start_delay", + b"start_delay", + "start_to_close_timeout", + b"start_to_close_timeout", + "state_size_bytes", + b"state_size_bytes", + "state_transition_count", + b"state_transition_count", + "status", + b"status", + "task_queue", + b"task_queue", + "total_heartbeat_count", + b"total_heartbeat_count", + "user_metadata", + b"user_metadata", + ], + ) -> None: ... + +global___ActivityExecutionInfo = ActivityExecutionInfo + +class ActivityExecutionListInfo(google.protobuf.message.Message): + """Limited activity information returned in the list response. + When adding fields here, ensure that it is also present in ActivityExecutionInfo (note that it + may already be present in ActivityExecutionInfo but not at the top-level). + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ACTIVITY_ID_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + ACTIVITY_TYPE_FIELD_NUMBER: builtins.int + SCHEDULE_TIME_FIELD_NUMBER: builtins.int + CLOSE_TIME_FIELD_NUMBER: builtins.int + STATUS_FIELD_NUMBER: builtins.int + SEARCH_ATTRIBUTES_FIELD_NUMBER: builtins.int + TASK_QUEUE_FIELD_NUMBER: builtins.int + STATE_TRANSITION_COUNT_FIELD_NUMBER: builtins.int + STATE_SIZE_BYTES_FIELD_NUMBER: builtins.int + EXECUTION_DURATION_FIELD_NUMBER: builtins.int + EXECUTION_TIME_FIELD_NUMBER: builtins.int + activity_id: builtins.str + """A unique identifier of this activity within its namespace along with run ID (below).""" + run_id: builtins.str + """The run ID of the standalone activity.""" + @property + def activity_type(self) -> temporalio.api.common.v1.message_pb2.ActivityType: + """The type of the activity, a string that maps to a registered activity on a worker.""" + @property + def schedule_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Time the activity was originally scheduled via a StartActivityExecution request.""" + @property + def close_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """If the activity is in a terminal status, this field represents the time the activity transitioned to that status.""" + status: temporalio.api.enums.v1.activity_pb2.ActivityExecutionStatus.ValueType + """Only scheduled and terminal statuses appear here. More detailed information in PendingActivityInfo but not + available in the list response. + """ + @property + def search_attributes( + self, + ) -> temporalio.api.common.v1.message_pb2.SearchAttributes: + """Search attributes from the start request.""" + task_queue: builtins.str + """The task queue this activity was scheduled on when it was originally started, updated on activity options update.""" + state_transition_count: builtins.int + """Updated on terminal status.""" + state_size_bytes: builtins.int + """Updated once on scheduled and once on terminal status.""" + @property + def execution_duration(self) -> google.protobuf.duration_pb2.Duration: + """The difference between close time and scheduled time. + This field is only populated if the activity is closed. + """ + @property + def execution_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """The time at which the first activity task is made available for dispatch, computed as + `schedule_time + start_delay`. Same as `schedule_time` if `start_delay` is not set. + """ + def __init__( + self, + *, + activity_id: builtins.str = ..., + run_id: builtins.str = ..., + activity_type: temporalio.api.common.v1.message_pb2.ActivityType | None = ..., + schedule_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + close_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + status: temporalio.api.enums.v1.activity_pb2.ActivityExecutionStatus.ValueType = ..., + search_attributes: temporalio.api.common.v1.message_pb2.SearchAttributes + | None = ..., + task_queue: builtins.str = ..., + state_transition_count: builtins.int = ..., + state_size_bytes: builtins.int = ..., + execution_duration: google.protobuf.duration_pb2.Duration | None = ..., + execution_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "activity_type", + b"activity_type", + "close_time", + b"close_time", + "execution_duration", + b"execution_duration", + "execution_time", + b"execution_time", + "schedule_time", + b"schedule_time", + "search_attributes", + b"search_attributes", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "activity_id", + b"activity_id", + "activity_type", + b"activity_type", + "close_time", + b"close_time", + "execution_duration", + b"execution_duration", + "execution_time", + b"execution_time", + "run_id", + b"run_id", + "schedule_time", + b"schedule_time", + "search_attributes", + b"search_attributes", + "state_size_bytes", + b"state_size_bytes", + "state_transition_count", + b"state_transition_count", + "status", + b"status", + "task_queue", + b"task_queue", + ], + ) -> None: ... + +global___ActivityExecutionListInfo = ActivityExecutionListInfo + +class CallbackInfo(google.protobuf.message.Message): + """CallbackInfo contains the state of an attached activity callback.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class ActivityClosed(google.protobuf.message.Message): + """Trigger for when the activity is closed.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + + class Trigger(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ACTIVITY_CLOSED_FIELD_NUMBER: builtins.int + @property + def activity_closed(self) -> global___CallbackInfo.ActivityClosed: ... + def __init__( + self, + *, + activity_closed: global___CallbackInfo.ActivityClosed | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "activity_closed", b"activity_closed", "variant", b"variant" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "activity_closed", b"activity_closed", "variant", b"variant" + ], + ) -> None: ... + def WhichOneof( + self, oneof_group: typing_extensions.Literal["variant", b"variant"] + ) -> typing_extensions.Literal["activity_closed"] | None: ... + + TRIGGER_FIELD_NUMBER: builtins.int + INFO_FIELD_NUMBER: builtins.int + @property + def trigger(self) -> global___CallbackInfo.Trigger: + """Trigger for this callback.""" + @property + def info(self) -> temporalio.api.callback.v1.message_pb2.CallbackInfo: + """Common callback info.""" + def __init__( + self, + *, + trigger: global___CallbackInfo.Trigger | None = ..., + info: temporalio.api.callback.v1.message_pb2.CallbackInfo | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["info", b"info", "trigger", b"trigger"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["info", b"info", "trigger", b"trigger"], + ) -> None: ... + +global___CallbackInfo = CallbackInfo diff --git a/temporalio/api/batch/v1/__init__.py b/temporalio/api/batch/v1/__init__.py index 5ec8ad63a..fea70b41f 100644 --- a/temporalio/api/batch/v1/__init__.py +++ b/temporalio/api/batch/v1/__init__.py @@ -1,10 +1,13 @@ from .message_pb2 import ( + BatchOperationCancelActivities, BatchOperationCancellation, + BatchOperationDeleteActivities, BatchOperationDeletion, BatchOperationInfo, BatchOperationReset, BatchOperationResetActivities, BatchOperationSignal, + BatchOperationTerminateActivities, BatchOperationTermination, BatchOperationTriggerWorkflowRule, BatchOperationUnpauseActivities, @@ -13,12 +16,15 @@ ) __all__ = [ + "BatchOperationCancelActivities", "BatchOperationCancellation", + "BatchOperationDeleteActivities", "BatchOperationDeletion", "BatchOperationInfo", "BatchOperationReset", "BatchOperationResetActivities", "BatchOperationSignal", + "BatchOperationTerminateActivities", "BatchOperationTermination", "BatchOperationTriggerWorkflowRule", "BatchOperationUnpauseActivities", diff --git a/temporalio/api/batch/v1/message_pb2.py b/temporalio/api/batch/v1/message_pb2.py index 6e412e7bd..292b2c182 100644 --- a/temporalio/api/batch/v1/message_pb2.py +++ b/temporalio/api/batch/v1/message_pb2.py @@ -38,7 +38,7 @@ ) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n#temporal/api/batch/v1/message.proto\x12\x15temporal.api.batch.v1\x1a\x1egoogle/protobuf/duration.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a&temporal/api/activity/v1/message.proto\x1a$temporal/api/common/v1/message.proto\x1a+temporal/api/enums/v1/batch_operation.proto\x1a!temporal/api/enums/v1/reset.proto\x1a#temporal/api/rules/v1/message.proto\x1a&temporal/api/workflow/v1/message.proto"\xbf\x01\n\x12\x42\x61tchOperationInfo\x12\x0e\n\x06job_id\x18\x01 \x01(\t\x12\x39\n\x05state\x18\x02 \x01(\x0e\x32*.temporal.api.enums.v1.BatchOperationState\x12.\n\nstart_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12.\n\nclose_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"`\n\x19\x42\x61tchOperationTermination\x12\x31\n\x07\x64\x65tails\x18\x01 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x10\n\x08identity\x18\x02 \x01(\t"\x99\x01\n\x14\x42\x61tchOperationSignal\x12\x0e\n\x06signal\x18\x01 \x01(\t\x12/\n\x05input\x18\x02 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12.\n\x06header\x18\x03 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12\x10\n\x08identity\x18\x04 \x01(\t".\n\x1a\x42\x61tchOperationCancellation\x12\x10\n\x08identity\x18\x01 \x01(\t"*\n\x16\x42\x61tchOperationDeletion\x12\x10\n\x08identity\x18\x01 \x01(\t"\xae\x02\n\x13\x42\x61tchOperationReset\x12\x10\n\x08identity\x18\x03 \x01(\t\x12\x35\n\x07options\x18\x04 \x01(\x0b\x32$.temporal.api.common.v1.ResetOptions\x12\x38\n\nreset_type\x18\x01 \x01(\x0e\x32 .temporal.api.enums.v1.ResetTypeB\x02\x18\x01\x12G\n\x12reset_reapply_type\x18\x02 \x01(\x0e\x32\'.temporal.api.enums.v1.ResetReapplyTypeB\x02\x18\x01\x12K\n\x15post_reset_operations\x18\x05 \x03(\x0b\x32,.temporal.api.workflow.v1.PostResetOperation"\xc9\x01\n,BatchOperationUpdateWorkflowExecutionOptions\x12\x10\n\x08identity\x18\x01 \x01(\t\x12V\n\x1aworkflow_execution_options\x18\x02 \x01(\x0b\x32\x32.temporal.api.workflow.v1.WorkflowExecutionOptions\x12/\n\x0bupdate_mask\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.FieldMask"\xc0\x01\n\x1f\x42\x61tchOperationUnpauseActivities\x12\x10\n\x08identity\x18\x01 \x01(\t\x12\x0e\n\x04type\x18\x02 \x01(\tH\x00\x12\x13\n\tmatch_all\x18\x03 \x01(\x08H\x00\x12\x16\n\x0ereset_attempts\x18\x04 \x01(\x08\x12\x17\n\x0freset_heartbeat\x18\x05 \x01(\x08\x12)\n\x06jitter\x18\x06 \x01(\x0b\x32\x19.google.protobuf.DurationB\n\n\x08\x61\x63tivity"\x84\x01\n!BatchOperationTriggerWorkflowRule\x12\x10\n\x08identity\x18\x01 \x01(\t\x12\x0c\n\x02id\x18\x02 \x01(\tH\x00\x12\x37\n\x04spec\x18\x03 \x01(\x0b\x32\'.temporal.api.rules.v1.WorkflowRuleSpecH\x00\x42\x06\n\x04rule"\xf5\x01\n\x1d\x42\x61tchOperationResetActivities\x12\x10\n\x08identity\x18\x01 \x01(\t\x12\x0e\n\x04type\x18\x02 \x01(\tH\x00\x12\x13\n\tmatch_all\x18\x03 \x01(\x08H\x00\x12\x16\n\x0ereset_attempts\x18\x04 \x01(\x08\x12\x17\n\x0freset_heartbeat\x18\x05 \x01(\x08\x12\x13\n\x0bkeep_paused\x18\x06 \x01(\x08\x12)\n\x06jitter\x18\x07 \x01(\x0b\x32\x19.google.protobuf.Duration\x12 \n\x18restore_original_options\x18\x08 \x01(\x08\x42\n\n\x08\x61\x63tivity"\xf8\x01\n#BatchOperationUpdateActivityOptions\x12\x10\n\x08identity\x18\x01 \x01(\t\x12\x0e\n\x04type\x18\x02 \x01(\tH\x00\x12\x13\n\tmatch_all\x18\x03 \x01(\x08H\x00\x12\x43\n\x10\x61\x63tivity_options\x18\x04 \x01(\x0b\x32).temporal.api.activity.v1.ActivityOptions\x12/\n\x0bupdate_mask\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.FieldMask\x12\x18\n\x10restore_original\x18\x06 \x01(\x08\x42\n\n\x08\x61\x63tivityB\x84\x01\n\x18io.temporal.api.batch.v1B\x0cMessageProtoP\x01Z!go.temporal.io/api/batch/v1;batch\xaa\x02\x17Temporalio.Api.Batch.V1\xea\x02\x1aTemporalio::Api::Batch::V1b\x06proto3' + b'\n#temporal/api/batch/v1/message.proto\x12\x15temporal.api.batch.v1\x1a\x1egoogle/protobuf/duration.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a&temporal/api/activity/v1/message.proto\x1a$temporal/api/common/v1/message.proto\x1a+temporal/api/enums/v1/batch_operation.proto\x1a!temporal/api/enums/v1/reset.proto\x1a#temporal/api/rules/v1/message.proto\x1a&temporal/api/workflow/v1/message.proto"\x82\x02\n\x12\x42\x61tchOperationInfo\x12\x0e\n\x06job_id\x18\x01 \x01(\t\x12\x39\n\x05state\x18\x02 \x01(\x0e\x32*.temporal.api.enums.v1.BatchOperationState\x12.\n\nstart_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12.\n\nclose_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x41\n\x0eoperation_type\x18\x05 \x01(\x0e\x32).temporal.api.enums.v1.BatchOperationType"`\n\x19\x42\x61tchOperationTermination\x12\x31\n\x07\x64\x65tails\x18\x01 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x10\n\x08identity\x18\x02 \x01(\t"E\n!BatchOperationTerminateActivities\x12\x10\n\x08identity\x18\x01 \x01(\t\x12\x0e\n\x06reason\x18\x02 \x01(\t"\x99\x01\n\x14\x42\x61tchOperationSignal\x12\x0e\n\x06signal\x18\x01 \x01(\t\x12/\n\x05input\x18\x02 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12.\n\x06header\x18\x03 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12\x10\n\x08identity\x18\x04 \x01(\t".\n\x1a\x42\x61tchOperationCancellation\x12\x10\n\x08identity\x18\x01 \x01(\t"B\n\x1e\x42\x61tchOperationCancelActivities\x12\x10\n\x08identity\x18\x01 \x01(\t\x12\x0e\n\x06reason\x18\x02 \x01(\t"*\n\x16\x42\x61tchOperationDeletion\x12\x10\n\x08identity\x18\x01 \x01(\t" \n\x1e\x42\x61tchOperationDeleteActivities"\xae\x02\n\x13\x42\x61tchOperationReset\x12\x10\n\x08identity\x18\x03 \x01(\t\x12\x35\n\x07options\x18\x04 \x01(\x0b\x32$.temporal.api.common.v1.ResetOptions\x12\x38\n\nreset_type\x18\x01 \x01(\x0e\x32 .temporal.api.enums.v1.ResetTypeB\x02\x18\x01\x12G\n\x12reset_reapply_type\x18\x02 \x01(\x0e\x32\'.temporal.api.enums.v1.ResetReapplyTypeB\x02\x18\x01\x12K\n\x15post_reset_operations\x18\x05 \x03(\x0b\x32,.temporal.api.workflow.v1.PostResetOperation"\xc9\x01\n,BatchOperationUpdateWorkflowExecutionOptions\x12\x10\n\x08identity\x18\x01 \x01(\t\x12V\n\x1aworkflow_execution_options\x18\x02 \x01(\x0b\x32\x32.temporal.api.workflow.v1.WorkflowExecutionOptions\x12/\n\x0bupdate_mask\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.FieldMask"\xc0\x01\n\x1f\x42\x61tchOperationUnpauseActivities\x12\x10\n\x08identity\x18\x01 \x01(\t\x12\x0e\n\x04type\x18\x02 \x01(\tH\x00\x12\x13\n\tmatch_all\x18\x03 \x01(\x08H\x00\x12\x16\n\x0ereset_attempts\x18\x04 \x01(\x08\x12\x17\n\x0freset_heartbeat\x18\x05 \x01(\x08\x12)\n\x06jitter\x18\x06 \x01(\x0b\x32\x19.google.protobuf.DurationB\n\n\x08\x61\x63tivity"\x84\x01\n!BatchOperationTriggerWorkflowRule\x12\x10\n\x08identity\x18\x01 \x01(\t\x12\x0c\n\x02id\x18\x02 \x01(\tH\x00\x12\x37\n\x04spec\x18\x03 \x01(\x0b\x32\'.temporal.api.rules.v1.WorkflowRuleSpecH\x00\x42\x06\n\x04rule"\xf5\x01\n\x1d\x42\x61tchOperationResetActivities\x12\x10\n\x08identity\x18\x01 \x01(\t\x12\x0e\n\x04type\x18\x02 \x01(\tH\x00\x12\x13\n\tmatch_all\x18\x03 \x01(\x08H\x00\x12\x16\n\x0ereset_attempts\x18\x04 \x01(\x08\x12\x17\n\x0freset_heartbeat\x18\x05 \x01(\x08\x12\x13\n\x0bkeep_paused\x18\x06 \x01(\x08\x12)\n\x06jitter\x18\x07 \x01(\x0b\x32\x19.google.protobuf.Duration\x12 \n\x18restore_original_options\x18\x08 \x01(\x08\x42\n\n\x08\x61\x63tivity"\xf8\x01\n#BatchOperationUpdateActivityOptions\x12\x10\n\x08identity\x18\x01 \x01(\t\x12\x0e\n\x04type\x18\x02 \x01(\tH\x00\x12\x13\n\tmatch_all\x18\x03 \x01(\x08H\x00\x12\x43\n\x10\x61\x63tivity_options\x18\x04 \x01(\x0b\x32).temporal.api.activity.v1.ActivityOptions\x12/\n\x0bupdate_mask\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.FieldMask\x12\x18\n\x10restore_original\x18\x06 \x01(\x08\x42\n\n\x08\x61\x63tivityB\x84\x01\n\x18io.temporal.api.batch.v1B\x0cMessageProtoP\x01Z!go.temporal.io/api/batch/v1;batch\xaa\x02\x17Temporalio.Api.Batch.V1\xea\x02\x1aTemporalio::Api::Batch::V1b\x06proto3' ) @@ -46,11 +46,20 @@ _BATCHOPERATIONTERMINATION = DESCRIPTOR.message_types_by_name[ "BatchOperationTermination" ] +_BATCHOPERATIONTERMINATEACTIVITIES = DESCRIPTOR.message_types_by_name[ + "BatchOperationTerminateActivities" +] _BATCHOPERATIONSIGNAL = DESCRIPTOR.message_types_by_name["BatchOperationSignal"] _BATCHOPERATIONCANCELLATION = DESCRIPTOR.message_types_by_name[ "BatchOperationCancellation" ] +_BATCHOPERATIONCANCELACTIVITIES = DESCRIPTOR.message_types_by_name[ + "BatchOperationCancelActivities" +] _BATCHOPERATIONDELETION = DESCRIPTOR.message_types_by_name["BatchOperationDeletion"] +_BATCHOPERATIONDELETEACTIVITIES = DESCRIPTOR.message_types_by_name[ + "BatchOperationDeleteActivities" +] _BATCHOPERATIONRESET = DESCRIPTOR.message_types_by_name["BatchOperationReset"] _BATCHOPERATIONUPDATEWORKFLOWEXECUTIONOPTIONS = DESCRIPTOR.message_types_by_name[ "BatchOperationUpdateWorkflowExecutionOptions" @@ -72,7 +81,7 @@ (_message.Message,), { "DESCRIPTOR": _BATCHOPERATIONINFO, - "__module__": "temporal.api.batch.v1.message_pb2", + "__module__": "temporalio.api.batch.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.batch.v1.BatchOperationInfo) }, ) @@ -83,18 +92,29 @@ (_message.Message,), { "DESCRIPTOR": _BATCHOPERATIONTERMINATION, - "__module__": "temporal.api.batch.v1.message_pb2", + "__module__": "temporalio.api.batch.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.batch.v1.BatchOperationTermination) }, ) _sym_db.RegisterMessage(BatchOperationTermination) +BatchOperationTerminateActivities = _reflection.GeneratedProtocolMessageType( + "BatchOperationTerminateActivities", + (_message.Message,), + { + "DESCRIPTOR": _BATCHOPERATIONTERMINATEACTIVITIES, + "__module__": "temporalio.api.batch.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.batch.v1.BatchOperationTerminateActivities) + }, +) +_sym_db.RegisterMessage(BatchOperationTerminateActivities) + BatchOperationSignal = _reflection.GeneratedProtocolMessageType( "BatchOperationSignal", (_message.Message,), { "DESCRIPTOR": _BATCHOPERATIONSIGNAL, - "__module__": "temporal.api.batch.v1.message_pb2", + "__module__": "temporalio.api.batch.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.batch.v1.BatchOperationSignal) }, ) @@ -105,29 +125,51 @@ (_message.Message,), { "DESCRIPTOR": _BATCHOPERATIONCANCELLATION, - "__module__": "temporal.api.batch.v1.message_pb2", + "__module__": "temporalio.api.batch.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.batch.v1.BatchOperationCancellation) }, ) _sym_db.RegisterMessage(BatchOperationCancellation) +BatchOperationCancelActivities = _reflection.GeneratedProtocolMessageType( + "BatchOperationCancelActivities", + (_message.Message,), + { + "DESCRIPTOR": _BATCHOPERATIONCANCELACTIVITIES, + "__module__": "temporalio.api.batch.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.batch.v1.BatchOperationCancelActivities) + }, +) +_sym_db.RegisterMessage(BatchOperationCancelActivities) + BatchOperationDeletion = _reflection.GeneratedProtocolMessageType( "BatchOperationDeletion", (_message.Message,), { "DESCRIPTOR": _BATCHOPERATIONDELETION, - "__module__": "temporal.api.batch.v1.message_pb2", + "__module__": "temporalio.api.batch.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.batch.v1.BatchOperationDeletion) }, ) _sym_db.RegisterMessage(BatchOperationDeletion) +BatchOperationDeleteActivities = _reflection.GeneratedProtocolMessageType( + "BatchOperationDeleteActivities", + (_message.Message,), + { + "DESCRIPTOR": _BATCHOPERATIONDELETEACTIVITIES, + "__module__": "temporalio.api.batch.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.batch.v1.BatchOperationDeleteActivities) + }, +) +_sym_db.RegisterMessage(BatchOperationDeleteActivities) + BatchOperationReset = _reflection.GeneratedProtocolMessageType( "BatchOperationReset", (_message.Message,), { "DESCRIPTOR": _BATCHOPERATIONRESET, - "__module__": "temporal.api.batch.v1.message_pb2", + "__module__": "temporalio.api.batch.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.batch.v1.BatchOperationReset) }, ) @@ -138,7 +180,7 @@ (_message.Message,), { "DESCRIPTOR": _BATCHOPERATIONUPDATEWORKFLOWEXECUTIONOPTIONS, - "__module__": "temporal.api.batch.v1.message_pb2", + "__module__": "temporalio.api.batch.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.batch.v1.BatchOperationUpdateWorkflowExecutionOptions) }, ) @@ -149,7 +191,7 @@ (_message.Message,), { "DESCRIPTOR": _BATCHOPERATIONUNPAUSEACTIVITIES, - "__module__": "temporal.api.batch.v1.message_pb2", + "__module__": "temporalio.api.batch.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.batch.v1.BatchOperationUnpauseActivities) }, ) @@ -160,7 +202,7 @@ (_message.Message,), { "DESCRIPTOR": _BATCHOPERATIONTRIGGERWORKFLOWRULE, - "__module__": "temporal.api.batch.v1.message_pb2", + "__module__": "temporalio.api.batch.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.batch.v1.BatchOperationTriggerWorkflowRule) }, ) @@ -171,7 +213,7 @@ (_message.Message,), { "DESCRIPTOR": _BATCHOPERATIONRESETACTIVITIES, - "__module__": "temporal.api.batch.v1.message_pb2", + "__module__": "temporalio.api.batch.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.batch.v1.BatchOperationResetActivities) }, ) @@ -182,7 +224,7 @@ (_message.Message,), { "DESCRIPTOR": _BATCHOPERATIONUPDATEACTIVITYOPTIONS, - "__module__": "temporal.api.batch.v1.message_pb2", + "__module__": "temporalio.api.batch.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.batch.v1.BatchOperationUpdateActivityOptions) }, ) @@ -198,25 +240,31 @@ "reset_reapply_type" ]._serialized_options = b"\030\001" _BATCHOPERATIONINFO._serialized_start = 397 - _BATCHOPERATIONINFO._serialized_end = 588 - _BATCHOPERATIONTERMINATION._serialized_start = 590 - _BATCHOPERATIONTERMINATION._serialized_end = 686 - _BATCHOPERATIONSIGNAL._serialized_start = 689 - _BATCHOPERATIONSIGNAL._serialized_end = 842 - _BATCHOPERATIONCANCELLATION._serialized_start = 844 - _BATCHOPERATIONCANCELLATION._serialized_end = 890 - _BATCHOPERATIONDELETION._serialized_start = 892 - _BATCHOPERATIONDELETION._serialized_end = 934 - _BATCHOPERATIONRESET._serialized_start = 937 - _BATCHOPERATIONRESET._serialized_end = 1239 - _BATCHOPERATIONUPDATEWORKFLOWEXECUTIONOPTIONS._serialized_start = 1242 - _BATCHOPERATIONUPDATEWORKFLOWEXECUTIONOPTIONS._serialized_end = 1443 - _BATCHOPERATIONUNPAUSEACTIVITIES._serialized_start = 1446 - _BATCHOPERATIONUNPAUSEACTIVITIES._serialized_end = 1638 - _BATCHOPERATIONTRIGGERWORKFLOWRULE._serialized_start = 1641 - _BATCHOPERATIONTRIGGERWORKFLOWRULE._serialized_end = 1773 - _BATCHOPERATIONRESETACTIVITIES._serialized_start = 1776 - _BATCHOPERATIONRESETACTIVITIES._serialized_end = 2021 - _BATCHOPERATIONUPDATEACTIVITYOPTIONS._serialized_start = 2024 - _BATCHOPERATIONUPDATEACTIVITYOPTIONS._serialized_end = 2272 + _BATCHOPERATIONINFO._serialized_end = 655 + _BATCHOPERATIONTERMINATION._serialized_start = 657 + _BATCHOPERATIONTERMINATION._serialized_end = 753 + _BATCHOPERATIONTERMINATEACTIVITIES._serialized_start = 755 + _BATCHOPERATIONTERMINATEACTIVITIES._serialized_end = 824 + _BATCHOPERATIONSIGNAL._serialized_start = 827 + _BATCHOPERATIONSIGNAL._serialized_end = 980 + _BATCHOPERATIONCANCELLATION._serialized_start = 982 + _BATCHOPERATIONCANCELLATION._serialized_end = 1028 + _BATCHOPERATIONCANCELACTIVITIES._serialized_start = 1030 + _BATCHOPERATIONCANCELACTIVITIES._serialized_end = 1096 + _BATCHOPERATIONDELETION._serialized_start = 1098 + _BATCHOPERATIONDELETION._serialized_end = 1140 + _BATCHOPERATIONDELETEACTIVITIES._serialized_start = 1142 + _BATCHOPERATIONDELETEACTIVITIES._serialized_end = 1174 + _BATCHOPERATIONRESET._serialized_start = 1177 + _BATCHOPERATIONRESET._serialized_end = 1479 + _BATCHOPERATIONUPDATEWORKFLOWEXECUTIONOPTIONS._serialized_start = 1482 + _BATCHOPERATIONUPDATEWORKFLOWEXECUTIONOPTIONS._serialized_end = 1683 + _BATCHOPERATIONUNPAUSEACTIVITIES._serialized_start = 1686 + _BATCHOPERATIONUNPAUSEACTIVITIES._serialized_end = 1878 + _BATCHOPERATIONTRIGGERWORKFLOWRULE._serialized_start = 1881 + _BATCHOPERATIONTRIGGERWORKFLOWRULE._serialized_end = 2013 + _BATCHOPERATIONRESETACTIVITIES._serialized_start = 2016 + _BATCHOPERATIONRESETACTIVITIES._serialized_end = 2261 + _BATCHOPERATIONUPDATEACTIVITYOPTIONS._serialized_start = 2264 + _BATCHOPERATIONUPDATEACTIVITYOPTIONS._serialized_end = 2512 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/batch/v1/message_pb2.pyi b/temporalio/api/batch/v1/message_pb2.pyi index 0f7e87da0..106b351ef 100644 --- a/temporalio/api/batch/v1/message_pb2.pyi +++ b/temporalio/api/batch/v1/message_pb2.pyi @@ -35,6 +35,7 @@ class BatchOperationInfo(google.protobuf.message.Message): STATE_FIELD_NUMBER: builtins.int START_TIME_FIELD_NUMBER: builtins.int CLOSE_TIME_FIELD_NUMBER: builtins.int + OPERATION_TYPE_FIELD_NUMBER: builtins.int job_id: builtins.str """Batch job ID""" state: temporalio.api.enums.v1.batch_operation_pb2.BatchOperationState.ValueType @@ -45,6 +46,10 @@ class BatchOperationInfo(google.protobuf.message.Message): @property def close_time(self) -> google.protobuf.timestamp_pb2.Timestamp: """Batch operation close time""" + operation_type: ( + temporalio.api.enums.v1.batch_operation_pb2.BatchOperationType.ValueType + ) + """Operation type""" def __init__( self, *, @@ -52,6 +57,7 @@ class BatchOperationInfo(google.protobuf.message.Message): state: temporalio.api.enums.v1.batch_operation_pb2.BatchOperationState.ValueType = ..., start_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., close_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + operation_type: temporalio.api.enums.v1.batch_operation_pb2.BatchOperationType.ValueType = ..., ) -> None: ... def HasField( self, @@ -66,6 +72,8 @@ class BatchOperationInfo(google.protobuf.message.Message): b"close_time", "job_id", b"job_id", + "operation_type", + b"operation_type", "start_time", b"start_time", "state", @@ -108,6 +116,36 @@ class BatchOperationTermination(google.protobuf.message.Message): global___BatchOperationTermination = BatchOperationTermination +class BatchOperationTerminateActivities(google.protobuf.message.Message): + """BatchOperationTerminateActivities sends terminate requests to a batch of activities. + Keep the parameter in sync with temporalio.api.workflowservice.v1.TerminateActivityExecutionRequest. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + IDENTITY_FIELD_NUMBER: builtins.int + REASON_FIELD_NUMBER: builtins.int + identity: builtins.str + """The identity of the worker/client""" + reason: builtins.str + """Reason for requesting the termination, recorded and available via the PollActivityExecution API. + Not propagated to a worker if an activity attempt is currently running. + """ + def __init__( + self, + *, + identity: builtins.str = ..., + reason: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "identity", b"identity", "reason", b"reason" + ], + ) -> None: ... + +global___BatchOperationTerminateActivities = BatchOperationTerminateActivities + class BatchOperationSignal(google.protobuf.message.Message): """BatchOperationSignal sends signals to batch workflows. Keep the parameter in sync with temporalio.api.workflowservice.v1.SignalWorkflowExecutionRequest. @@ -181,6 +219,36 @@ class BatchOperationCancellation(google.protobuf.message.Message): global___BatchOperationCancellation = BatchOperationCancellation +class BatchOperationCancelActivities(google.protobuf.message.Message): + """BatchOperationCancelActivities sends cancel requests to a batch of activities. + Keep the parameter in sync with temporalio.api.workflowservice.v1.RequestCancelActivityExecutionRequest. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + IDENTITY_FIELD_NUMBER: builtins.int + REASON_FIELD_NUMBER: builtins.int + identity: builtins.str + """The identity of the worker/client""" + reason: builtins.str + """Reason for requesting the cancellation, recorded and available via the PollActivityExecution API. + Not propagated to a worker if an activity attempt is currently running. + """ + def __init__( + self, + *, + identity: builtins.str = ..., + reason: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "identity", b"identity", "reason", b"reason" + ], + ) -> None: ... + +global___BatchOperationCancelActivities = BatchOperationCancelActivities + class BatchOperationDeletion(google.protobuf.message.Message): """BatchOperationDeletion sends deletion requests to batch workflows. Keep the parameter in sync with temporalio.api.workflowservice.v1.DeleteWorkflowExecutionRequest. @@ -202,6 +270,19 @@ class BatchOperationDeletion(google.protobuf.message.Message): global___BatchOperationDeletion = BatchOperationDeletion +class BatchOperationDeleteActivities(google.protobuf.message.Message): + """BatchOperationDeleteActivities sends deletion requests to a batch of activities. + Keep the parameter in sync with temporalio.api.workflowservice.v1.DeleteActivityExecutionRequest. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___BatchOperationDeleteActivities = BatchOperationDeleteActivities + class BatchOperationReset(google.protobuf.message.Message): """BatchOperationReset sends reset requests to batch workflows. Keep the parameter in sync with temporalio.api.workflowservice.v1.ResetWorkflowExecutionRequest. diff --git a/temporalio/api/callback/__init__.py b/temporalio/api/callback/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/temporalio/api/callback/v1/__init__.py b/temporalio/api/callback/v1/__init__.py new file mode 100644 index 000000000..4e0d8fcc6 --- /dev/null +++ b/temporalio/api/callback/v1/__init__.py @@ -0,0 +1,5 @@ +from .message_pb2 import CallbackInfo + +__all__ = [ + "CallbackInfo", +] diff --git a/temporalio/api/callback/v1/message_pb2.py b/temporalio/api/callback/v1/message_pb2.py new file mode 100644 index 000000000..5d4ccb917 --- /dev/null +++ b/temporalio/api/callback/v1/message_pb2.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: temporal/api/callback/v1/message.proto +"""Generated protocol buffer code.""" + +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 + +from temporalio.api.common.v1 import ( + message_pb2 as temporal_dot_api_dot_common_dot_v1_dot_message__pb2, +) +from temporalio.api.enums.v1 import ( + common_pb2 as temporal_dot_api_dot_enums_dot_v1_dot_common__pb2, +) +from temporalio.api.failure.v1 import ( + message_pb2 as temporal_dot_api_dot_failure_dot_v1_dot_message__pb2, +) + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n&temporal/api/callback/v1/message.proto\x12\x18temporal.api.callback.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\x1a"temporal/api/enums/v1/common.proto\x1a%temporal/api/failure/v1/message.proto"\x97\x03\n\x0c\x43\x61llbackInfo\x12\x32\n\x08\x63\x61llback\x18\x01 \x01(\x0b\x32 .temporal.api.common.v1.Callback\x12\x35\n\x11registration_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x33\n\x05state\x18\x03 \x01(\x0e\x32$.temporal.api.enums.v1.CallbackState\x12\x0f\n\x07\x61ttempt\x18\x04 \x01(\x05\x12>\n\x1alast_attempt_complete_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12>\n\x14last_attempt_failure\x18\x06 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12>\n\x1anext_attempt_schedule_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x16\n\x0e\x62locked_reason\x18\x08 \x01(\tB\x93\x01\n\x1bio.temporal.api.callback.v1B\x0cMessageProtoP\x01Z\'go.temporal.io/api/callback/v1;callback\xaa\x02\x1aTemporalio.Api.Callback.V1\xea\x02\x1dTemporalio::Api::Callback::V1b\x06proto3' +) + + +_CALLBACKINFO = DESCRIPTOR.message_types_by_name["CallbackInfo"] +CallbackInfo = _reflection.GeneratedProtocolMessageType( + "CallbackInfo", + (_message.Message,), + { + "DESCRIPTOR": _CALLBACKINFO, + "__module__": "temporalio.api.callback.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.callback.v1.CallbackInfo) + }, +) +_sym_db.RegisterMessage(CallbackInfo) + +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b"\n\033io.temporal.api.callback.v1B\014MessageProtoP\001Z'go.temporal.io/api/callback/v1;callback\252\002\032Temporalio.Api.Callback.V1\352\002\035Temporalio::Api::Callback::V1" + _CALLBACKINFO._serialized_start = 215 + _CALLBACKINFO._serialized_end = 622 +# @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/callback/v1/message_pb2.pyi b/temporalio/api/callback/v1/message_pb2.pyi new file mode 100644 index 000000000..ea7d6444b --- /dev/null +++ b/temporalio/api/callback/v1/message_pb2.pyi @@ -0,0 +1,112 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" + +import builtins +import sys + +import google.protobuf.descriptor +import google.protobuf.message +import google.protobuf.timestamp_pb2 + +import temporalio.api.common.v1.message_pb2 +import temporalio.api.enums.v1.common_pb2 +import temporalio.api.failure.v1.message_pb2 + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class CallbackInfo(google.protobuf.message.Message): + """Common callback information. Specific CallbackInfo messages should embed this and may include additional fields.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CALLBACK_FIELD_NUMBER: builtins.int + REGISTRATION_TIME_FIELD_NUMBER: builtins.int + STATE_FIELD_NUMBER: builtins.int + ATTEMPT_FIELD_NUMBER: builtins.int + LAST_ATTEMPT_COMPLETE_TIME_FIELD_NUMBER: builtins.int + LAST_ATTEMPT_FAILURE_FIELD_NUMBER: builtins.int + NEXT_ATTEMPT_SCHEDULE_TIME_FIELD_NUMBER: builtins.int + BLOCKED_REASON_FIELD_NUMBER: builtins.int + @property + def callback(self) -> temporalio.api.common.v1.message_pb2.Callback: + """Information on how this callback should be invoked (e.g. its URL and type).""" + @property + def registration_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """The time when the callback was registered.""" + state: temporalio.api.enums.v1.common_pb2.CallbackState.ValueType + """The current state of the callback.""" + attempt: builtins.int + """The number of attempts made to deliver the callback. + This number represents a minimum bound since the attempt is incremented after the callback request completes. + """ + @property + def last_attempt_complete_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """The time when the last attempt completed.""" + @property + def last_attempt_failure(self) -> temporalio.api.failure.v1.message_pb2.Failure: + """The last attempt's failure, if any.""" + @property + def next_attempt_schedule_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """The time when the next attempt is scheduled.""" + blocked_reason: builtins.str + """If the state is BLOCKED, blocked reason provides additional information.""" + def __init__( + self, + *, + callback: temporalio.api.common.v1.message_pb2.Callback | None = ..., + registration_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + state: temporalio.api.enums.v1.common_pb2.CallbackState.ValueType = ..., + attempt: builtins.int = ..., + last_attempt_complete_time: google.protobuf.timestamp_pb2.Timestamp + | None = ..., + last_attempt_failure: temporalio.api.failure.v1.message_pb2.Failure + | None = ..., + next_attempt_schedule_time: google.protobuf.timestamp_pb2.Timestamp + | None = ..., + blocked_reason: builtins.str = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "callback", + b"callback", + "last_attempt_complete_time", + b"last_attempt_complete_time", + "last_attempt_failure", + b"last_attempt_failure", + "next_attempt_schedule_time", + b"next_attempt_schedule_time", + "registration_time", + b"registration_time", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "attempt", + b"attempt", + "blocked_reason", + b"blocked_reason", + "callback", + b"callback", + "last_attempt_complete_time", + b"last_attempt_complete_time", + "last_attempt_failure", + b"last_attempt_failure", + "next_attempt_schedule_time", + b"next_attempt_schedule_time", + "registration_time", + b"registration_time", + "state", + b"state", + ], + ) -> None: ... + +global___CallbackInfo = CallbackInfo diff --git a/temporalio/api/cloud/account/v1/__init__.py b/temporalio/api/cloud/account/v1/__init__.py index fef9ff367..b1ef72ede 100644 --- a/temporalio/api/cloud/account/v1/__init__.py +++ b/temporalio/api/cloud/account/v1/__init__.py @@ -1,8 +1,17 @@ -from .message_pb2 import Account, AccountSpec, Metrics, MetricsSpec +from .message_pb2 import ( + Account, + AccountSpec, + AuditLogSink, + AuditLogSinkSpec, + Metrics, + MetricsSpec, +) __all__ = [ "Account", "AccountSpec", + "AuditLogSink", + "AuditLogSinkSpec", "Metrics", "MetricsSpec", ] diff --git a/temporalio/api/cloud/account/v1/message_pb2.py b/temporalio/api/cloud/account/v1/message_pb2.py index b212d7592..c31a2d3e7 100644 --- a/temporalio/api/cloud/account/v1/message_pb2.py +++ b/temporalio/api/cloud/account/v1/message_pb2.py @@ -14,12 +14,17 @@ _sym_db = _symbol_database.Default() +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 + from temporalio.api.cloud.resource.v1 import ( message_pb2 as temporal_dot_api_dot_cloud_dot_resource_dot_v1_dot_message__pb2, ) +from temporalio.api.cloud.sink.v1 import ( + message_pb2 as temporal_dot_api_dot_cloud_dot_sink_dot_v1_dot_message__pb2, +) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n+temporal/api/cloud/account/v1/message.proto\x12\x1dtemporal.api.cloud.account.v1\x1a,temporal/api/cloud/resource/v1/message.proto")\n\x0bMetricsSpec\x12\x1a\n\x12\x61\x63\x63\x65pted_client_ca\x18\x02 \x01(\x0c"J\n\x0b\x41\x63\x63ountSpec\x12;\n\x07metrics\x18\x01 \x01(\x0b\x32*.temporal.api.cloud.account.v1.MetricsSpec"\x16\n\x07Metrics\x12\x0b\n\x03uri\x18\x01 \x01(\t"\xfc\x01\n\x07\x41\x63\x63ount\x12\n\n\x02id\x18\x01 \x01(\t\x12\x38\n\x04spec\x18\x02 \x01(\x0b\x32*.temporal.api.cloud.account.v1.AccountSpec\x12\x18\n\x10resource_version\x18\x03 \x01(\t\x12<\n\x05state\x18\x04 \x01(\x0e\x32-.temporal.api.cloud.resource.v1.ResourceState\x12\x1a\n\x12\x61sync_operation_id\x18\x05 \x01(\t\x12\x37\n\x07metrics\x18\x06 \x01(\x0b\x32&.temporal.api.cloud.account.v1.MetricsB\xa7\x01\n io.temporal.api.cloud.account.v1B\x0cMessageProtoP\x01Z+go.temporal.io/api/cloud/account/v1;account\xaa\x02\x1fTemporalio.Api.Cloud.Account.V1\xea\x02#Temporalio::Api::Cloud::Account::V1b\x06proto3' + b'\n+temporal/api/cloud/account/v1/message.proto\x12\x1dtemporal.api.cloud.account.v1\x1a,temporal/api/cloud/resource/v1/message.proto\x1a(temporal/api/cloud/sink/v1/message.proto\x1a\x1fgoogle/protobuf/timestamp.proto")\n\x0bMetricsSpec\x12\x1a\n\x12\x61\x63\x63\x65pted_client_ca\x18\x02 \x01(\x0c"J\n\x0b\x41\x63\x63ountSpec\x12;\n\x07metrics\x18\x01 \x01(\x0b\x32*.temporal.api.cloud.account.v1.MetricsSpec"\x16\n\x07Metrics\x12\x0b\n\x03uri\x18\x01 \x01(\t"\xfc\x01\n\x07\x41\x63\x63ount\x12\n\n\x02id\x18\x01 \x01(\t\x12\x38\n\x04spec\x18\x02 \x01(\x0b\x32*.temporal.api.cloud.account.v1.AccountSpec\x12\x18\n\x10resource_version\x18\x03 \x01(\t\x12<\n\x05state\x18\x04 \x01(\x0e\x32-.temporal.api.cloud.resource.v1.ResourceState\x12\x1a\n\x12\x61sync_operation_id\x18\x05 \x01(\t\x12\x37\n\x07metrics\x18\x06 \x01(\x0b\x32&.temporal.api.cloud.account.v1.Metrics"\xbf\x01\n\x10\x41uditLogSinkSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12?\n\x0ckinesis_sink\x18\x02 \x01(\x0b\x32\'.temporal.api.cloud.sink.v1.KinesisSpecH\x00\x12>\n\x0cpub_sub_sink\x18\x03 \x01(\x0b\x32&.temporal.api.cloud.sink.v1.PubSubSpecH\x00\x12\x0f\n\x07\x65nabled\x18\x04 \x01(\x08\x42\x0b\n\tsink_type"\xb8\x03\n\x0c\x41uditLogSink\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12<\n\x05state\x18\x03 \x01(\x0e\x32-.temporal.api.cloud.resource.v1.ResourceState\x12=\n\x04spec\x18\x04 \x01(\x0b\x32/.temporal.api.cloud.account.v1.AuditLogSinkSpec\x12\x42\n\x06health\x18\x05 \x01(\x0e\x32\x32.temporal.api.cloud.account.v1.AuditLogSink.Health\x12\x15\n\rerror_message\x18\x06 \x01(\t\x12\x37\n\x13last_succeeded_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"o\n\x06Health\x12\x16\n\x12HEALTH_UNSPECIFIED\x10\x00\x12\r\n\tHEALTH_OK\x10\x01\x12\x19\n\x15HEALTH_ERROR_INTERNAL\x10\x02\x12#\n\x1fHEALTH_ERROR_USER_CONFIGURATION\x10\x03\x42\xa7\x01\n io.temporal.api.cloud.account.v1B\x0cMessageProtoP\x01Z+go.temporal.io/api/cloud/account/v1;account\xaa\x02\x1fTemporalio.Api.Cloud.Account.V1\xea\x02#Temporalio::Api::Cloud::Account::V1b\x06proto3' ) @@ -27,12 +32,15 @@ _ACCOUNTSPEC = DESCRIPTOR.message_types_by_name["AccountSpec"] _METRICS = DESCRIPTOR.message_types_by_name["Metrics"] _ACCOUNT = DESCRIPTOR.message_types_by_name["Account"] +_AUDITLOGSINKSPEC = DESCRIPTOR.message_types_by_name["AuditLogSinkSpec"] +_AUDITLOGSINK = DESCRIPTOR.message_types_by_name["AuditLogSink"] +_AUDITLOGSINK_HEALTH = _AUDITLOGSINK.enum_types_by_name["Health"] MetricsSpec = _reflection.GeneratedProtocolMessageType( "MetricsSpec", (_message.Message,), { "DESCRIPTOR": _METRICSSPEC, - "__module__": "temporal.api.cloud.account.v1.message_pb2", + "__module__": "temporalio.api.cloud.account.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.account.v1.MetricsSpec) }, ) @@ -43,7 +51,7 @@ (_message.Message,), { "DESCRIPTOR": _ACCOUNTSPEC, - "__module__": "temporal.api.cloud.account.v1.message_pb2", + "__module__": "temporalio.api.cloud.account.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.account.v1.AccountSpec) }, ) @@ -54,7 +62,7 @@ (_message.Message,), { "DESCRIPTOR": _METRICS, - "__module__": "temporal.api.cloud.account.v1.message_pb2", + "__module__": "temporalio.api.cloud.account.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.account.v1.Metrics) }, ) @@ -65,21 +73,49 @@ (_message.Message,), { "DESCRIPTOR": _ACCOUNT, - "__module__": "temporal.api.cloud.account.v1.message_pb2", + "__module__": "temporalio.api.cloud.account.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.account.v1.Account) }, ) _sym_db.RegisterMessage(Account) +AuditLogSinkSpec = _reflection.GeneratedProtocolMessageType( + "AuditLogSinkSpec", + (_message.Message,), + { + "DESCRIPTOR": _AUDITLOGSINKSPEC, + "__module__": "temporalio.api.cloud.account.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.account.v1.AuditLogSinkSpec) + }, +) +_sym_db.RegisterMessage(AuditLogSinkSpec) + +AuditLogSink = _reflection.GeneratedProtocolMessageType( + "AuditLogSink", + (_message.Message,), + { + "DESCRIPTOR": _AUDITLOGSINK, + "__module__": "temporalio.api.cloud.account.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.account.v1.AuditLogSink) + }, +) +_sym_db.RegisterMessage(AuditLogSink) + if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b"\n io.temporal.api.cloud.account.v1B\014MessageProtoP\001Z+go.temporal.io/api/cloud/account/v1;account\252\002\037Temporalio.Api.Cloud.Account.V1\352\002#Temporalio::Api::Cloud::Account::V1" - _METRICSSPEC._serialized_start = 124 - _METRICSSPEC._serialized_end = 165 - _ACCOUNTSPEC._serialized_start = 167 - _ACCOUNTSPEC._serialized_end = 241 - _METRICS._serialized_start = 243 - _METRICS._serialized_end = 265 - _ACCOUNT._serialized_start = 268 - _ACCOUNT._serialized_end = 520 + _METRICSSPEC._serialized_start = 199 + _METRICSSPEC._serialized_end = 240 + _ACCOUNTSPEC._serialized_start = 242 + _ACCOUNTSPEC._serialized_end = 316 + _METRICS._serialized_start = 318 + _METRICS._serialized_end = 340 + _ACCOUNT._serialized_start = 343 + _ACCOUNT._serialized_end = 595 + _AUDITLOGSINKSPEC._serialized_start = 598 + _AUDITLOGSINKSPEC._serialized_end = 789 + _AUDITLOGSINK._serialized_start = 792 + _AUDITLOGSINK._serialized_end = 1232 + _AUDITLOGSINK_HEALTH._serialized_start = 1121 + _AUDITLOGSINK_HEALTH._serialized_end = 1232 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/cloud/account/v1/message_pb2.pyi b/temporalio/api/cloud/account/v1/message_pb2.pyi index 34775c232..859cd6bd5 100644 --- a/temporalio/api/cloud/account/v1/message_pb2.pyi +++ b/temporalio/api/cloud/account/v1/message_pb2.pyi @@ -5,13 +5,17 @@ isort:skip_file import builtins import sys +import typing import google.protobuf.descriptor +import google.protobuf.internal.enum_type_wrapper import google.protobuf.message +import google.protobuf.timestamp_pb2 import temporalio.api.cloud.resource.v1.message_pb2 +import temporalio.api.cloud.sink.v1.message_pb2 -if sys.version_info >= (3, 8): +if sys.version_info >= (3, 10): import typing as typing_extensions else: import typing_extensions @@ -140,3 +144,159 @@ class Account(google.protobuf.message.Message): ) -> None: ... global___Account = Account + +class AuditLogSinkSpec(google.protobuf.message.Message): + """AuditLogSinkSpec is only used by Audit Log""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + KINESIS_SINK_FIELD_NUMBER: builtins.int + PUB_SUB_SINK_FIELD_NUMBER: builtins.int + ENABLED_FIELD_NUMBER: builtins.int + name: builtins.str + """Name of the sink e.g. "audit_log_01" """ + @property + def kinesis_sink(self) -> temporalio.api.cloud.sink.v1.message_pb2.KinesisSpec: + """The KinesisSpec when destination_type is Kinesis""" + @property + def pub_sub_sink(self) -> temporalio.api.cloud.sink.v1.message_pb2.PubSubSpec: + """The PubSubSpec when destination_type is PubSub""" + enabled: builtins.bool + """Enabled indicates whether the sink is enabled or not.""" + def __init__( + self, + *, + name: builtins.str = ..., + kinesis_sink: temporalio.api.cloud.sink.v1.message_pb2.KinesisSpec | None = ..., + pub_sub_sink: temporalio.api.cloud.sink.v1.message_pb2.PubSubSpec | None = ..., + enabled: builtins.bool = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "kinesis_sink", + b"kinesis_sink", + "pub_sub_sink", + b"pub_sub_sink", + "sink_type", + b"sink_type", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "enabled", + b"enabled", + "kinesis_sink", + b"kinesis_sink", + "name", + b"name", + "pub_sub_sink", + b"pub_sub_sink", + "sink_type", + b"sink_type", + ], + ) -> None: ... + def WhichOneof( + self, oneof_group: typing_extensions.Literal["sink_type", b"sink_type"] + ) -> typing_extensions.Literal["kinesis_sink", "pub_sub_sink"] | None: ... + +global___AuditLogSinkSpec = AuditLogSinkSpec + +class AuditLogSink(google.protobuf.message.Message): + """AuditLogSink is only used by Audit Log""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class _Health: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _HealthEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ + AuditLogSink._Health.ValueType + ], + builtins.type, + ): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + HEALTH_UNSPECIFIED: AuditLogSink._Health.ValueType # 0 + HEALTH_OK: AuditLogSink._Health.ValueType # 1 + """The audit log sink is healthy and functioning correctly.""" + HEALTH_ERROR_INTERNAL: AuditLogSink._Health.ValueType # 2 + """The audit log sink has an internal error.""" + HEALTH_ERROR_USER_CONFIGURATION: AuditLogSink._Health.ValueType # 3 + """The audit log sink has a configuration error.""" + + class Health(_Health, metaclass=_HealthEnumTypeWrapper): + """The health status of the audit log sink.""" + + HEALTH_UNSPECIFIED: AuditLogSink.Health.ValueType # 0 + HEALTH_OK: AuditLogSink.Health.ValueType # 1 + """The audit log sink is healthy and functioning correctly.""" + HEALTH_ERROR_INTERNAL: AuditLogSink.Health.ValueType # 2 + """The audit log sink has an internal error.""" + HEALTH_ERROR_USER_CONFIGURATION: AuditLogSink.Health.ValueType # 3 + """The audit log sink has a configuration error.""" + + NAME_FIELD_NUMBER: builtins.int + RESOURCE_VERSION_FIELD_NUMBER: builtins.int + STATE_FIELD_NUMBER: builtins.int + SPEC_FIELD_NUMBER: builtins.int + HEALTH_FIELD_NUMBER: builtins.int + ERROR_MESSAGE_FIELD_NUMBER: builtins.int + LAST_SUCCEEDED_TIME_FIELD_NUMBER: builtins.int + name: builtins.str + """Name of the sink e.g. "audit_log_01" """ + resource_version: builtins.str + """The version of the audit log sink resource.""" + state: temporalio.api.cloud.resource.v1.message_pb2.ResourceState.ValueType + """The current state of the audit log sink.""" + @property + def spec(self) -> global___AuditLogSinkSpec: + """The specification details of the audit log sink.""" + health: global___AuditLogSink.Health.ValueType + """The health status of the audit log sink.""" + error_message: builtins.str + """An error message describing any issues with the audit log sink, if applicable.""" + @property + def last_succeeded_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """The last succeeded timestamp for the internal workflow responsible for adding data to the sink.""" + def __init__( + self, + *, + name: builtins.str = ..., + resource_version: builtins.str = ..., + state: temporalio.api.cloud.resource.v1.message_pb2.ResourceState.ValueType = ..., + spec: global___AuditLogSinkSpec | None = ..., + health: global___AuditLogSink.Health.ValueType = ..., + error_message: builtins.str = ..., + last_succeeded_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "last_succeeded_time", b"last_succeeded_time", "spec", b"spec" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "error_message", + b"error_message", + "health", + b"health", + "last_succeeded_time", + b"last_succeeded_time", + "name", + b"name", + "resource_version", + b"resource_version", + "spec", + b"spec", + "state", + b"state", + ], + ) -> None: ... + +global___AuditLogSink = AuditLogSink diff --git a/temporalio/api/cloud/auditlog/__init__.py b/temporalio/api/cloud/auditlog/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/temporalio/api/cloud/auditlog/v1/__init__.py b/temporalio/api/cloud/auditlog/v1/__init__.py new file mode 100644 index 000000000..558f70660 --- /dev/null +++ b/temporalio/api/cloud/auditlog/v1/__init__.py @@ -0,0 +1,6 @@ +from .message_pb2 import LogRecord, Principal + +__all__ = [ + "LogRecord", + "Principal", +] diff --git a/temporalio/api/cloud/auditlog/v1/message_pb2.py b/temporalio/api/cloud/auditlog/v1/message_pb2.py new file mode 100644 index 000000000..5714d3d23 --- /dev/null +++ b/temporalio/api/cloud/auditlog/v1/message_pb2.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: temporal/api/cloud/auditlog/v1/message.proto +"""Generated protocol buffer code.""" + +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n,temporal/api/cloud/auditlog/v1/message.proto\x12\x1etemporal.api.cloud.auditlog.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1cgoogle/protobuf/struct.proto"\x9f\x02\n\tLogRecord\x12-\n\temit_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x11\n\toperation\x18\x05 \x01(\t\x12\x0e\n\x06status\x18\x07 \x01(\t\x12\x0f\n\x07version\x18\t \x01(\x05\x12\x0e\n\x06log_id\x18\n \x01(\t\x12<\n\tprincipal\x18\x0c \x01(\x0b\x32).temporal.api.cloud.auditlog.v1.Principal\x12,\n\x0braw_details\x18\r \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x17\n\x0fx_forwarded_for\x18\x0e \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x0f \x01(\t"G\n\tPrincipal\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\n\n\x02id\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x12\n\napi_key_id\x18\x04 \x01(\tB\xac\x01\n!io.temporal.api.cloud.auditlog.v1B\x0cMessageProtoP\x01Z-go.temporal.io/api/cloud/auditlog/v1;auditlog\xaa\x02 Temporalio.Api.Cloud.AuditLog.V1\xea\x02$Temporalio::Api::Cloud::AuditLog::V1b\x06proto3' +) + + +_LOGRECORD = DESCRIPTOR.message_types_by_name["LogRecord"] +_PRINCIPAL = DESCRIPTOR.message_types_by_name["Principal"] +LogRecord = _reflection.GeneratedProtocolMessageType( + "LogRecord", + (_message.Message,), + { + "DESCRIPTOR": _LOGRECORD, + "__module__": "temporalio.api.cloud.auditlog.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.auditlog.v1.LogRecord) + }, +) +_sym_db.RegisterMessage(LogRecord) + +Principal = _reflection.GeneratedProtocolMessageType( + "Principal", + (_message.Message,), + { + "DESCRIPTOR": _PRINCIPAL, + "__module__": "temporalio.api.cloud.auditlog.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.auditlog.v1.Principal) + }, +) +_sym_db.RegisterMessage(Principal) + +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b"\n!io.temporal.api.cloud.auditlog.v1B\014MessageProtoP\001Z-go.temporal.io/api/cloud/auditlog/v1;auditlog\252\002 Temporalio.Api.Cloud.AuditLog.V1\352\002$Temporalio::Api::Cloud::AuditLog::V1" + _LOGRECORD._serialized_start = 144 + _LOGRECORD._serialized_end = 431 + _PRINCIPAL._serialized_start = 433 + _PRINCIPAL._serialized_end = 504 +# @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/cloud/auditlog/v1/message_pb2.pyi b/temporalio/api/cloud/auditlog/v1/message_pb2.pyi new file mode 100644 index 000000000..4145313c2 --- /dev/null +++ b/temporalio/api/cloud/auditlog/v1/message_pb2.pyi @@ -0,0 +1,138 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" + +import builtins +import sys + +import google.protobuf.descriptor +import google.protobuf.message +import google.protobuf.struct_pb2 +import google.protobuf.timestamp_pb2 + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class LogRecord(google.protobuf.message.Message): + """LogRecord represents an audit log entry from Temporal, structured for easy parsing and analysis.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + EMIT_TIME_FIELD_NUMBER: builtins.int + OPERATION_FIELD_NUMBER: builtins.int + STATUS_FIELD_NUMBER: builtins.int + VERSION_FIELD_NUMBER: builtins.int + LOG_ID_FIELD_NUMBER: builtins.int + PRINCIPAL_FIELD_NUMBER: builtins.int + RAW_DETAILS_FIELD_NUMBER: builtins.int + X_FORWARDED_FOR_FIELD_NUMBER: builtins.int + ASYNC_OPERATION_ID_FIELD_NUMBER: builtins.int + @property + def emit_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Time when the log was emitted.""" + operation: builtins.str + """The operation performed.""" + status: builtins.str + """The status of the operation.""" + version: builtins.int + """The internal version of the log message. Can be used in deduplication if needed.""" + log_id: builtins.str + """Unique ID for the log record.""" + @property + def principal(self) -> global___Principal: + """The principal that performed the operation.""" + @property + def raw_details(self) -> google.protobuf.struct_pb2.Struct: + """The raw details of the operation.""" + x_forwarded_for: builtins.str + """The originating IP address of the request.""" + async_operation_id: builtins.str + """The ID of the async operation.""" + def __init__( + self, + *, + emit_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + operation: builtins.str = ..., + status: builtins.str = ..., + version: builtins.int = ..., + log_id: builtins.str = ..., + principal: global___Principal | None = ..., + raw_details: google.protobuf.struct_pb2.Struct | None = ..., + x_forwarded_for: builtins.str = ..., + async_operation_id: builtins.str = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "emit_time", + b"emit_time", + "principal", + b"principal", + "raw_details", + b"raw_details", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "async_operation_id", + b"async_operation_id", + "emit_time", + b"emit_time", + "log_id", + b"log_id", + "operation", + b"operation", + "principal", + b"principal", + "raw_details", + b"raw_details", + "status", + b"status", + "version", + b"version", + "x_forwarded_for", + b"x_forwarded_for", + ], + ) -> None: ... + +global___LogRecord = LogRecord + +class Principal(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TYPE_FIELD_NUMBER: builtins.int + ID_FIELD_NUMBER: builtins.int + NAME_FIELD_NUMBER: builtins.int + API_KEY_ID_FIELD_NUMBER: builtins.int + type: builtins.str + """The type of the principal. + Possible type values: user, serviceaccount. + """ + id: builtins.str + """The id of the principal.""" + name: builtins.str + """The name of the principal.""" + api_key_id: builtins.str + """The api key id of the principal if provided.""" + def __init__( + self, + *, + type: builtins.str = ..., + id: builtins.str = ..., + name: builtins.str = ..., + api_key_id: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "api_key_id", b"api_key_id", "id", b"id", "name", b"name", "type", b"type" + ], + ) -> None: ... + +global___Principal = Principal diff --git a/temporalio/api/cloud/billing/__init__.py b/temporalio/api/cloud/billing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/temporalio/api/cloud/billing/v1/__init__.py b/temporalio/api/cloud/billing/v1/__init__.py new file mode 100644 index 000000000..7159e38fc --- /dev/null +++ b/temporalio/api/cloud/billing/v1/__init__.py @@ -0,0 +1,6 @@ +from .message_pb2 import BillingReport, BillingReportSpec + +__all__ = [ + "BillingReport", + "BillingReportSpec", +] diff --git a/temporalio/api/cloud/billing/v1/message_pb2.py b/temporalio/api/cloud/billing/v1/message_pb2.py new file mode 100644 index 000000000..4cbf846f5 --- /dev/null +++ b/temporalio/api/cloud/billing/v1/message_pb2.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: temporal/api/cloud/billing/v1/message.proto +"""Generated protocol buffer code.""" + +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2 +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n+temporal/api/cloud/billing/v1/message.proto\x12\x1dtemporal.api.cloud.billing.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto"\xfd\x03\n\x11\x42illingReportSpec\x12\x38\n\x14start_time_inclusive\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x36\n\x12\x65nd_time_exclusive\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x43\n download_url_expiration_duration\x18\x03 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\x12^\n\x0bgranularity\x18\x05 \x01(\x0e\x32I.temporal.api.cloud.billing.v1.BillingReportSpec.BillingReportGranularity"\xbb\x01\n\x18\x42illingReportGranularity\x12*\n&BILLING_REPORT_GRANULARITY_UNSPECIFIED\x10\x00\x12%\n!BILLING_REPORT_GRANULARITY_HOURLY\x10\x01\x12$\n BILLING_REPORT_GRANULARITY_DAILY\x10\x02\x12&\n"BILLING_REPORT_GRANULARITY_MONTHLY\x10\x03"\xa8\x06\n\rBillingReport\x12\n\n\x02id\x18\x01 \x01(\t\x12N\n\x05state\x18\x02 \x01(\x0e\x32?.temporal.api.cloud.billing.v1.BillingReport.BillingReportState\x12>\n\x04spec\x18\x03 \x01(\x0b\x32\x30.temporal.api.cloud.billing.v1.BillingReportSpec\x12L\n\rdownload_info\x18\x04 \x03(\x0b\x32\x35.temporal.api.cloud.billing.v1.BillingReport.Download\x12\x32\n\x0erequested_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x32\n\x0egenerated_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x1a\n\x12\x61sync_operation_id\x18\x07 \x01(\t\x1a\x80\x02\n\x08\x44ownload\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\x37\n\x13url_expiration_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12U\n\x0b\x66ile_format\x18\x03 \x01(\x0e\x32@.temporal.api.cloud.billing.v1.BillingReport.Download.FileFormat\x12\x17\n\x0f\x66ile_size_bytes\x18\x04 \x01(\x03">\n\nFileFormat\x12\x1b\n\x17\x46ILE_FORMAT_UNSPECIFIED\x10\x00\x12\x13\n\x0f\x46ILE_FORMAT_CSV\x10\x01"\xa5\x01\n\x12\x42illingReportState\x12$\n BILLING_REPORT_STATE_UNSPECIFIED\x10\x00\x12$\n BILLING_REPORT_STATE_IN_PROGRESS\x10\x01\x12"\n\x1e\x42ILLING_REPORT_STATE_GENERATED\x10\x02\x12\x1f\n\x1b\x42ILLING_REPORT_STATE_FAILED\x10\x03\x42\xa7\x01\n io.temporal.api.cloud.billing.v1B\x0cMessageProtoP\x01Z+go.temporal.io/api/cloud/billing/v1;billing\xaa\x02\x1fTemporalio.Api.Cloud.Billing.V1\xea\x02#Temporalio::Api::Cloud::Billing::V1b\x06proto3' +) + + +_BILLINGREPORTSPEC = DESCRIPTOR.message_types_by_name["BillingReportSpec"] +_BILLINGREPORT = DESCRIPTOR.message_types_by_name["BillingReport"] +_BILLINGREPORT_DOWNLOAD = _BILLINGREPORT.nested_types_by_name["Download"] +_BILLINGREPORTSPEC_BILLINGREPORTGRANULARITY = _BILLINGREPORTSPEC.enum_types_by_name[ + "BillingReportGranularity" +] +_BILLINGREPORT_DOWNLOAD_FILEFORMAT = _BILLINGREPORT_DOWNLOAD.enum_types_by_name[ + "FileFormat" +] +_BILLINGREPORT_BILLINGREPORTSTATE = _BILLINGREPORT.enum_types_by_name[ + "BillingReportState" +] +BillingReportSpec = _reflection.GeneratedProtocolMessageType( + "BillingReportSpec", + (_message.Message,), + { + "DESCRIPTOR": _BILLINGREPORTSPEC, + "__module__": "temporalio.api.cloud.billing.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.billing.v1.BillingReportSpec) + }, +) +_sym_db.RegisterMessage(BillingReportSpec) + +BillingReport = _reflection.GeneratedProtocolMessageType( + "BillingReport", + (_message.Message,), + { + "Download": _reflection.GeneratedProtocolMessageType( + "Download", + (_message.Message,), + { + "DESCRIPTOR": _BILLINGREPORT_DOWNLOAD, + "__module__": "temporalio.api.cloud.billing.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.billing.v1.BillingReport.Download) + }, + ), + "DESCRIPTOR": _BILLINGREPORT, + "__module__": "temporalio.api.cloud.billing.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.billing.v1.BillingReport) + }, +) +_sym_db.RegisterMessage(BillingReport) +_sym_db.RegisterMessage(BillingReport.Download) + +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b"\n io.temporal.api.cloud.billing.v1B\014MessageProtoP\001Z+go.temporal.io/api/cloud/billing/v1;billing\252\002\037Temporalio.Api.Cloud.Billing.V1\352\002#Temporalio::Api::Cloud::Billing::V1" + _BILLINGREPORTSPEC._serialized_start = 144 + _BILLINGREPORTSPEC._serialized_end = 653 + _BILLINGREPORTSPEC_BILLINGREPORTGRANULARITY._serialized_start = 466 + _BILLINGREPORTSPEC_BILLINGREPORTGRANULARITY._serialized_end = 653 + _BILLINGREPORT._serialized_start = 656 + _BILLINGREPORT._serialized_end = 1464 + _BILLINGREPORT_DOWNLOAD._serialized_start = 1040 + _BILLINGREPORT_DOWNLOAD._serialized_end = 1296 + _BILLINGREPORT_DOWNLOAD_FILEFORMAT._serialized_start = 1234 + _BILLINGREPORT_DOWNLOAD_FILEFORMAT._serialized_end = 1296 + _BILLINGREPORT_BILLINGREPORTSTATE._serialized_start = 1299 + _BILLINGREPORT_BILLINGREPORTSTATE._serialized_end = 1464 +# @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/cloud/billing/v1/message_pb2.pyi b/temporalio/api/cloud/billing/v1/message_pb2.pyi new file mode 100644 index 000000000..7828afffa --- /dev/null +++ b/temporalio/api/cloud/billing/v1/message_pb2.pyi @@ -0,0 +1,299 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" + +import builtins +import collections.abc +import sys +import typing + +import google.protobuf.descriptor +import google.protobuf.duration_pb2 +import google.protobuf.internal.containers +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +import google.protobuf.timestamp_pb2 + +if sys.version_info >= (3, 10): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class BillingReportSpec(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class _BillingReportGranularity: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _BillingReportGranularityEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ + BillingReportSpec._BillingReportGranularity.ValueType + ], + builtins.type, + ): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + BILLING_REPORT_GRANULARITY_UNSPECIFIED: ( + BillingReportSpec._BillingReportGranularity.ValueType + ) # 0 + BILLING_REPORT_GRANULARITY_HOURLY: ( + BillingReportSpec._BillingReportGranularity.ValueType + ) # 1 + BILLING_REPORT_GRANULARITY_DAILY: ( + BillingReportSpec._BillingReportGranularity.ValueType + ) # 2 + BILLING_REPORT_GRANULARITY_MONTHLY: ( + BillingReportSpec._BillingReportGranularity.ValueType + ) # 3 + + class BillingReportGranularity( + _BillingReportGranularity, metaclass=_BillingReportGranularityEnumTypeWrapper + ): ... + BILLING_REPORT_GRANULARITY_UNSPECIFIED: ( + BillingReportSpec.BillingReportGranularity.ValueType + ) # 0 + BILLING_REPORT_GRANULARITY_HOURLY: ( + BillingReportSpec.BillingReportGranularity.ValueType + ) # 1 + BILLING_REPORT_GRANULARITY_DAILY: ( + BillingReportSpec.BillingReportGranularity.ValueType + ) # 2 + BILLING_REPORT_GRANULARITY_MONTHLY: ( + BillingReportSpec.BillingReportGranularity.ValueType + ) # 3 + + START_TIME_INCLUSIVE_FIELD_NUMBER: builtins.int + END_TIME_EXCLUSIVE_FIELD_NUMBER: builtins.int + DOWNLOAD_URL_EXPIRATION_DURATION_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + GRANULARITY_FIELD_NUMBER: builtins.int + @property + def start_time_inclusive(self) -> google.protobuf.timestamp_pb2.Timestamp: + """The start time of the billing report (in UTC).""" + @property + def end_time_exclusive(self) -> google.protobuf.timestamp_pb2.Timestamp: + """The end time of the billing report (in UTC).""" + @property + def download_url_expiration_duration(self) -> google.protobuf.duration_pb2.Duration: + """The duration after which the download url will expire. + Optional, default is 5 minutes and maximum is 1 hour. + """ + description: builtins.str + """The description for the billing report. + Optional, default is empty. + """ + granularity: global___BillingReportSpec.BillingReportGranularity.ValueType + """The data granularity of the billing report. + Optional, default is hourly. + temporal:versioning:min_version=v0.16.0 + """ + def __init__( + self, + *, + start_time_inclusive: google.protobuf.timestamp_pb2.Timestamp | None = ..., + end_time_exclusive: google.protobuf.timestamp_pb2.Timestamp | None = ..., + download_url_expiration_duration: google.protobuf.duration_pb2.Duration + | None = ..., + description: builtins.str = ..., + granularity: global___BillingReportSpec.BillingReportGranularity.ValueType = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "download_url_expiration_duration", + b"download_url_expiration_duration", + "end_time_exclusive", + b"end_time_exclusive", + "start_time_inclusive", + b"start_time_inclusive", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "description", + b"description", + "download_url_expiration_duration", + b"download_url_expiration_duration", + "end_time_exclusive", + b"end_time_exclusive", + "granularity", + b"granularity", + "start_time_inclusive", + b"start_time_inclusive", + ], + ) -> None: ... + +global___BillingReportSpec = BillingReportSpec + +class BillingReport(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class _BillingReportState: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _BillingReportStateEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ + BillingReport._BillingReportState.ValueType + ], + builtins.type, + ): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + BILLING_REPORT_STATE_UNSPECIFIED: ( + BillingReport._BillingReportState.ValueType + ) # 0 + BILLING_REPORT_STATE_IN_PROGRESS: ( + BillingReport._BillingReportState.ValueType + ) # 1 + BILLING_REPORT_STATE_GENERATED: BillingReport._BillingReportState.ValueType # 2 + BILLING_REPORT_STATE_FAILED: BillingReport._BillingReportState.ValueType # 3 + + class BillingReportState( + _BillingReportState, metaclass=_BillingReportStateEnumTypeWrapper + ): ... + BILLING_REPORT_STATE_UNSPECIFIED: BillingReport.BillingReportState.ValueType # 0 + BILLING_REPORT_STATE_IN_PROGRESS: BillingReport.BillingReportState.ValueType # 1 + BILLING_REPORT_STATE_GENERATED: BillingReport.BillingReportState.ValueType # 2 + BILLING_REPORT_STATE_FAILED: BillingReport.BillingReportState.ValueType # 3 + + class Download(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class _FileFormat: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _FileFormatEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ + BillingReport.Download._FileFormat.ValueType + ], + builtins.type, + ): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + FILE_FORMAT_UNSPECIFIED: BillingReport.Download._FileFormat.ValueType # 0 + FILE_FORMAT_CSV: BillingReport.Download._FileFormat.ValueType # 1 + + class FileFormat(_FileFormat, metaclass=_FileFormatEnumTypeWrapper): ... + FILE_FORMAT_UNSPECIFIED: BillingReport.Download.FileFormat.ValueType # 0 + FILE_FORMAT_CSV: BillingReport.Download.FileFormat.ValueType # 1 + + URL_FIELD_NUMBER: builtins.int + URL_EXPIRATION_TIME_FIELD_NUMBER: builtins.int + FILE_FORMAT_FIELD_NUMBER: builtins.int + FILE_SIZE_BYTES_FIELD_NUMBER: builtins.int + url: builtins.str + """The download url.""" + @property + def url_expiration_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """The time when the download url will expire.""" + file_format: global___BillingReport.Download.FileFormat.ValueType + """The file format of the billing report""" + file_size_bytes: builtins.int + """The size of the file in bytes. Useful for pre-allocating space, progress indicators, etc.""" + def __init__( + self, + *, + url: builtins.str = ..., + url_expiration_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + file_format: global___BillingReport.Download.FileFormat.ValueType = ..., + file_size_bytes: builtins.int = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "url_expiration_time", b"url_expiration_time" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "file_format", + b"file_format", + "file_size_bytes", + b"file_size_bytes", + "url", + b"url", + "url_expiration_time", + b"url_expiration_time", + ], + ) -> None: ... + + ID_FIELD_NUMBER: builtins.int + STATE_FIELD_NUMBER: builtins.int + SPEC_FIELD_NUMBER: builtins.int + DOWNLOAD_INFO_FIELD_NUMBER: builtins.int + REQUESTED_TIME_FIELD_NUMBER: builtins.int + GENERATED_TIME_FIELD_NUMBER: builtins.int + ASYNC_OPERATION_ID_FIELD_NUMBER: builtins.int + id: builtins.str + """The id of the billing report.""" + state: global___BillingReport.BillingReportState.ValueType + """The current state of the billing report.""" + @property + def spec(self) -> global___BillingReportSpec: + """The spec used to generate this billing report.""" + @property + def download_info( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + global___BillingReport.Download + ]: + """The download information for the billing report. + For future-proofness this is repeated as we may return multiple files (e.g. csv+meta/json, split by size/date, etc.) + """ + @property + def requested_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """The date and time when the billing report was requested.""" + @property + def generated_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """The date and time when the billing report generation completed.""" + async_operation_id: builtins.str + """The async operation id associated with the billing report generation.""" + def __init__( + self, + *, + id: builtins.str = ..., + state: global___BillingReport.BillingReportState.ValueType = ..., + spec: global___BillingReportSpec | None = ..., + download_info: collections.abc.Iterable[global___BillingReport.Download] + | None = ..., + requested_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + generated_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + async_operation_id: builtins.str = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "generated_time", + b"generated_time", + "requested_time", + b"requested_time", + "spec", + b"spec", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "async_operation_id", + b"async_operation_id", + "download_info", + b"download_info", + "generated_time", + b"generated_time", + "id", + b"id", + "requested_time", + b"requested_time", + "spec", + b"spec", + "state", + b"state", + ], + ) -> None: ... + +global___BillingReport = BillingReport diff --git a/temporalio/api/cloud/cloudservice/v1/__init__.py b/temporalio/api/cloud/cloudservice/v1/__init__.py index 53f3cb0e6..0586a752b 100644 --- a/temporalio/api/cloud/cloudservice/v1/__init__.py +++ b/temporalio/api/cloud/cloudservice/v1/__init__.py @@ -3,10 +3,16 @@ AddNamespaceRegionResponse, AddUserGroupMemberRequest, AddUserGroupMemberResponse, + CreateAccountAuditLogSinkRequest, + CreateAccountAuditLogSinkResponse, CreateApiKeyRequest, CreateApiKeyResponse, + CreateBillingReportRequest, + CreateBillingReportResponse, CreateConnectivityRuleRequest, CreateConnectivityRuleResponse, + CreateCustomRoleRequest, + CreateCustomRoleResponse, CreateNamespaceExportSinkRequest, CreateNamespaceExportSinkResponse, CreateNamespaceRequest, @@ -19,10 +25,14 @@ CreateUserGroupResponse, CreateUserRequest, CreateUserResponse, + DeleteAccountAuditLogSinkRequest, + DeleteAccountAuditLogSinkResponse, DeleteApiKeyRequest, DeleteApiKeyResponse, DeleteConnectivityRuleRequest, DeleteConnectivityRuleResponse, + DeleteCustomRoleRequest, + DeleteCustomRoleResponse, DeleteNamespaceExportSinkRequest, DeleteNamespaceExportSinkResponse, DeleteNamespaceRegionRequest, @@ -39,6 +49,10 @@ DeleteUserResponse, FailoverNamespaceRegionRequest, FailoverNamespaceRegionResponse, + GetAccountAuditLogSinkRequest, + GetAccountAuditLogSinkResponse, + GetAccountAuditLogSinksRequest, + GetAccountAuditLogSinksResponse, GetAccountRequest, GetAccountResponse, GetApiKeyRequest, @@ -47,10 +61,22 @@ GetApiKeysResponse, GetAsyncOperationRequest, GetAsyncOperationResponse, + GetAuditLogsRequest, + GetAuditLogsResponse, + GetBillingReportRequest, + GetBillingReportResponse, GetConnectivityRuleRequest, GetConnectivityRuleResponse, GetConnectivityRulesRequest, GetConnectivityRulesResponse, + GetCurrentIdentityRequest, + GetCurrentIdentityResponse, + GetCustomRoleRequest, + GetCustomRoleResponse, + GetCustomRolesRequest, + GetCustomRolesResponse, + GetNamespaceCapacityInfoRequest, + GetNamespaceCapacityInfoResponse, GetNamespaceExportSinkRequest, GetNamespaceExportSinkResponse, GetNamespaceExportSinksRequest, @@ -67,6 +93,8 @@ GetRegionResponse, GetRegionsRequest, GetRegionsResponse, + GetServiceAccountNamespaceAssignmentsRequest, + GetServiceAccountNamespaceAssignmentsResponse, GetServiceAccountRequest, GetServiceAccountResponse, GetServiceAccountsRequest, @@ -75,10 +103,14 @@ GetUsageResponse, GetUserGroupMembersRequest, GetUserGroupMembersResponse, + GetUserGroupNamespaceAssignmentsRequest, + GetUserGroupNamespaceAssignmentsResponse, GetUserGroupRequest, GetUserGroupResponse, GetUserGroupsRequest, GetUserGroupsResponse, + GetUserNamespaceAssignmentsRequest, + GetUserNamespaceAssignmentsResponse, GetUserRequest, GetUserResponse, GetUsersRequest, @@ -87,14 +119,20 @@ RemoveUserGroupMemberResponse, RenameCustomSearchAttributeRequest, RenameCustomSearchAttributeResponse, + SetServiceAccountNamespaceAccessRequest, + SetServiceAccountNamespaceAccessResponse, SetUserGroupNamespaceAccessRequest, SetUserGroupNamespaceAccessResponse, SetUserNamespaceAccessRequest, SetUserNamespaceAccessResponse, + UpdateAccountAuditLogSinkRequest, + UpdateAccountAuditLogSinkResponse, UpdateAccountRequest, UpdateAccountResponse, UpdateApiKeyRequest, UpdateApiKeyResponse, + UpdateCustomRoleRequest, + UpdateCustomRoleResponse, UpdateNamespaceExportSinkRequest, UpdateNamespaceExportSinkResponse, UpdateNamespaceRequest, @@ -109,6 +147,8 @@ UpdateUserGroupResponse, UpdateUserRequest, UpdateUserResponse, + ValidateAccountAuditLogSinkRequest, + ValidateAccountAuditLogSinkResponse, ValidateNamespaceExportSinkRequest, ValidateNamespaceExportSinkResponse, ) @@ -118,10 +158,16 @@ "AddNamespaceRegionResponse", "AddUserGroupMemberRequest", "AddUserGroupMemberResponse", + "CreateAccountAuditLogSinkRequest", + "CreateAccountAuditLogSinkResponse", "CreateApiKeyRequest", "CreateApiKeyResponse", + "CreateBillingReportRequest", + "CreateBillingReportResponse", "CreateConnectivityRuleRequest", "CreateConnectivityRuleResponse", + "CreateCustomRoleRequest", + "CreateCustomRoleResponse", "CreateNamespaceExportSinkRequest", "CreateNamespaceExportSinkResponse", "CreateNamespaceRequest", @@ -134,10 +180,14 @@ "CreateUserGroupResponse", "CreateUserRequest", "CreateUserResponse", + "DeleteAccountAuditLogSinkRequest", + "DeleteAccountAuditLogSinkResponse", "DeleteApiKeyRequest", "DeleteApiKeyResponse", "DeleteConnectivityRuleRequest", "DeleteConnectivityRuleResponse", + "DeleteCustomRoleRequest", + "DeleteCustomRoleResponse", "DeleteNamespaceExportSinkRequest", "DeleteNamespaceExportSinkResponse", "DeleteNamespaceRegionRequest", @@ -154,6 +204,10 @@ "DeleteUserResponse", "FailoverNamespaceRegionRequest", "FailoverNamespaceRegionResponse", + "GetAccountAuditLogSinkRequest", + "GetAccountAuditLogSinkResponse", + "GetAccountAuditLogSinksRequest", + "GetAccountAuditLogSinksResponse", "GetAccountRequest", "GetAccountResponse", "GetApiKeyRequest", @@ -162,10 +216,22 @@ "GetApiKeysResponse", "GetAsyncOperationRequest", "GetAsyncOperationResponse", + "GetAuditLogsRequest", + "GetAuditLogsResponse", + "GetBillingReportRequest", + "GetBillingReportResponse", "GetConnectivityRuleRequest", "GetConnectivityRuleResponse", "GetConnectivityRulesRequest", "GetConnectivityRulesResponse", + "GetCurrentIdentityRequest", + "GetCurrentIdentityResponse", + "GetCustomRoleRequest", + "GetCustomRoleResponse", + "GetCustomRolesRequest", + "GetCustomRolesResponse", + "GetNamespaceCapacityInfoRequest", + "GetNamespaceCapacityInfoResponse", "GetNamespaceExportSinkRequest", "GetNamespaceExportSinkResponse", "GetNamespaceExportSinksRequest", @@ -182,6 +248,8 @@ "GetRegionResponse", "GetRegionsRequest", "GetRegionsResponse", + "GetServiceAccountNamespaceAssignmentsRequest", + "GetServiceAccountNamespaceAssignmentsResponse", "GetServiceAccountRequest", "GetServiceAccountResponse", "GetServiceAccountsRequest", @@ -190,10 +258,14 @@ "GetUsageResponse", "GetUserGroupMembersRequest", "GetUserGroupMembersResponse", + "GetUserGroupNamespaceAssignmentsRequest", + "GetUserGroupNamespaceAssignmentsResponse", "GetUserGroupRequest", "GetUserGroupResponse", "GetUserGroupsRequest", "GetUserGroupsResponse", + "GetUserNamespaceAssignmentsRequest", + "GetUserNamespaceAssignmentsResponse", "GetUserRequest", "GetUserResponse", "GetUsersRequest", @@ -202,14 +274,20 @@ "RemoveUserGroupMemberResponse", "RenameCustomSearchAttributeRequest", "RenameCustomSearchAttributeResponse", + "SetServiceAccountNamespaceAccessRequest", + "SetServiceAccountNamespaceAccessResponse", "SetUserGroupNamespaceAccessRequest", "SetUserGroupNamespaceAccessResponse", "SetUserNamespaceAccessRequest", "SetUserNamespaceAccessResponse", + "UpdateAccountAuditLogSinkRequest", + "UpdateAccountAuditLogSinkResponse", "UpdateAccountRequest", "UpdateAccountResponse", "UpdateApiKeyRequest", "UpdateApiKeyResponse", + "UpdateCustomRoleRequest", + "UpdateCustomRoleResponse", "UpdateNamespaceExportSinkRequest", "UpdateNamespaceExportSinkResponse", "UpdateNamespaceRequest", @@ -224,6 +302,8 @@ "UpdateUserGroupResponse", "UpdateUserRequest", "UpdateUserResponse", + "ValidateAccountAuditLogSinkRequest", + "ValidateAccountAuditLogSinkResponse", "ValidateNamespaceExportSinkRequest", "ValidateNamespaceExportSinkResponse", ] diff --git a/temporalio/api/cloud/cloudservice/v1/request_response_pb2.py b/temporalio/api/cloud/cloudservice/v1/request_response_pb2.py index 718a075bb..5d7a6a28a 100644 --- a/temporalio/api/cloud/cloudservice/v1/request_response_pb2.py +++ b/temporalio/api/cloud/cloudservice/v1/request_response_pb2.py @@ -19,6 +19,12 @@ from temporalio.api.cloud.account.v1 import ( message_pb2 as temporal_dot_api_dot_cloud_dot_account_dot_v1_dot_message__pb2, ) +from temporalio.api.cloud.auditlog.v1 import ( + message_pb2 as temporal_dot_api_dot_cloud_dot_auditlog_dot_v1_dot_message__pb2, +) +from temporalio.api.cloud.billing.v1 import ( + message_pb2 as temporal_dot_api_dot_cloud_dot_billing_dot_v1_dot_message__pb2, +) from temporalio.api.cloud.connectivityrule.v1 import ( message_pb2 as temporal_dot_api_dot_cloud_dot_connectivityrule_dot_v1_dot_message__pb2, ) @@ -42,10 +48,16 @@ ) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n9temporal/api/cloud/cloudservice/v1/request_response.proto\x12"temporal.api.cloud.cloudservice.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a-temporal/api/cloud/operation/v1/message.proto\x1a,temporal/api/cloud/identity/v1/message.proto\x1a-temporal/api/cloud/namespace/v1/message.proto\x1a)temporal/api/cloud/nexus/v1/message.proto\x1a*temporal/api/cloud/region/v1/message.proto\x1a+temporal/api/cloud/account/v1/message.proto\x1a)temporal/api/cloud/usage/v1/message.proto\x1a\x34temporal/api/cloud/connectivityrule/v1/message.proto"Z\n\x0fGetUsersRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x12\n\npage_token\x18\x02 \x01(\t\x12\r\n\x05\x65mail\x18\x03 \x01(\t\x12\x11\n\tnamespace\x18\x04 \x01(\t"`\n\x10GetUsersResponse\x12\x33\n\x05users\x18\x01 \x03(\x0b\x32$.temporal.api.cloud.identity.v1.User\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t"!\n\x0eGetUserRequest\x12\x0f\n\x07user_id\x18\x01 \x01(\t"E\n\x0fGetUserResponse\x12\x32\n\x04user\x18\x01 \x01(\x0b\x32$.temporal.api.cloud.identity.v1.User"g\n\x11\x43reateUserRequest\x12\x36\n\x04spec\x18\x01 \x01(\x0b\x32(.temporal.api.cloud.identity.v1.UserSpec\x12\x1a\n\x12\x61sync_operation_id\x18\x02 \x01(\t"o\n\x12\x43reateUserResponse\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12H\n\x0f\x61sync_operation\x18\x02 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\x92\x01\n\x11UpdateUserRequest\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12\x36\n\x04spec\x18\x02 \x01(\x0b\x32(.temporal.api.cloud.identity.v1.UserSpec\x12\x18\n\x10resource_version\x18\x03 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x04 \x01(\t"^\n\x12UpdateUserResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"Z\n\x11\x44\x65leteUserRequest\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x03 \x01(\t"^\n\x12\x44\x65leteUserResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\xba\x01\n\x1dSetUserNamespaceAccessRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x0f\n\x07user_id\x18\x02 \x01(\t\x12?\n\x06\x61\x63\x63\x65ss\x18\x03 \x01(\x0b\x32/.temporal.api.cloud.identity.v1.NamespaceAccess\x12\x18\n\x10resource_version\x18\x04 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x05 \x01(\t"j\n\x1eSetUserNamespaceAccessResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"6\n\x18GetAsyncOperationRequest\x12\x1a\n\x12\x61sync_operation_id\x18\x01 \x01(\t"e\n\x19GetAsyncOperationResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\xf3\x01\n\x16\x43reateNamespaceRequest\x12<\n\x04spec\x18\x02 \x01(\x0b\x32..temporal.api.cloud.namespace.v1.NamespaceSpec\x12\x1a\n\x12\x61sync_operation_id\x18\x03 \x01(\t\x12R\n\x04tags\x18\x04 \x03(\x0b\x32\x44.temporal.api.cloud.cloudservice.v1.CreateNamespaceRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"v\n\x17\x43reateNamespaceResponse\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12H\n\x0f\x61sync_operation\x18\x02 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"K\n\x14GetNamespacesRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x12\n\npage_token\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t"p\n\x15GetNamespacesResponse\x12>\n\nnamespaces\x18\x01 \x03(\x0b\x32*.temporal.api.cloud.namespace.v1.Namespace\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t"(\n\x13GetNamespaceRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t"U\n\x14GetNamespaceResponse\x12=\n\tnamespace\x18\x01 \x01(\x0b\x32*.temporal.api.cloud.namespace.v1.Namespace"\x9f\x01\n\x16UpdateNamespaceRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12<\n\x04spec\x18\x02 \x01(\x0b\x32..temporal.api.cloud.namespace.v1.NamespaceSpec\x12\x18\n\x10resource_version\x18\x03 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x04 \x01(\t"c\n\x17UpdateNamespaceResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\xc6\x01\n"RenameCustomSearchAttributeRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12-\n%existing_custom_search_attribute_name\x18\x02 \x01(\t\x12(\n new_custom_search_attribute_name\x18\x03 \x01(\t\x12\x18\n\x10resource_version\x18\x04 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x05 \x01(\t"o\n#RenameCustomSearchAttributeResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"a\n\x16\x44\x65leteNamespaceRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x03 \x01(\t"c\n\x17\x44\x65leteNamespaceResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"_\n\x1e\x46\x61iloverNamespaceRegionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x0e\n\x06region\x18\x02 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x03 \x01(\t"k\n\x1f\x46\x61iloverNamespaceRegionResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"t\n\x19\x41\x64\x64NamespaceRegionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x0e\n\x06region\x18\x02 \x01(\t\x12\x18\n\x10resource_version\x18\x03 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x04 \x01(\t"f\n\x1a\x41\x64\x64NamespaceRegionResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"w\n\x1c\x44\x65leteNamespaceRegionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x0e\n\x06region\x18\x02 \x01(\t\x12\x18\n\x10resource_version\x18\x03 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x04 \x01(\t"i\n\x1d\x44\x65leteNamespaceRegionResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\x13\n\x11GetRegionsRequest"K\n\x12GetRegionsResponse\x12\x35\n\x07regions\x18\x01 \x03(\x0b\x32$.temporal.api.cloud.region.v1.Region""\n\x10GetRegionRequest\x12\x0e\n\x06region\x18\x01 \x01(\t"I\n\x11GetRegionResponse\x12\x34\n\x06region\x18\x01 \x01(\x0b\x32$.temporal.api.cloud.region.v1.Region"\xae\x01\n\x11GetApiKeysRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x12\n\npage_token\x18\x02 \x01(\t\x12\x10\n\x08owner_id\x18\x03 \x01(\t\x12!\n\x15owner_type_deprecated\x18\x04 \x01(\tB\x02\x18\x01\x12=\n\nowner_type\x18\x05 \x01(\x0e\x32).temporal.api.cloud.identity.v1.OwnerType"g\n\x12GetApiKeysResponse\x12\x38\n\x08\x61pi_keys\x18\x01 \x03(\x0b\x32&.temporal.api.cloud.identity.v1.ApiKey\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t""\n\x10GetApiKeyRequest\x12\x0e\n\x06key_id\x18\x01 \x01(\t"L\n\x11GetApiKeyResponse\x12\x37\n\x07\x61pi_key\x18\x01 \x01(\x0b\x32&.temporal.api.cloud.identity.v1.ApiKey"k\n\x13\x43reateApiKeyRequest\x12\x38\n\x04spec\x18\x01 \x01(\x0b\x32*.temporal.api.cloud.identity.v1.ApiKeySpec\x12\x1a\n\x12\x61sync_operation_id\x18\x02 \x01(\t"\x7f\n\x14\x43reateApiKeyResponse\x12\x0e\n\x06key_id\x18\x01 \x01(\t\x12\r\n\x05token\x18\x02 \x01(\t\x12H\n\x0f\x61sync_operation\x18\x03 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\x95\x01\n\x13UpdateApiKeyRequest\x12\x0e\n\x06key_id\x18\x01 \x01(\t\x12\x38\n\x04spec\x18\x02 \x01(\x0b\x32*.temporal.api.cloud.identity.v1.ApiKeySpec\x12\x18\n\x10resource_version\x18\x03 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x04 \x01(\t"`\n\x14UpdateApiKeyResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"[\n\x13\x44\x65leteApiKeyRequest\x12\x0e\n\x06key_id\x18\x01 \x01(\t\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x03 \x01(\t"`\n\x14\x44\x65leteApiKeyResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\x87\x01\n\x18GetNexusEndpointsRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x12\n\npage_token\x18\x02 \x01(\t\x12\x1b\n\x13target_namespace_id\x18\x03 \x01(\t\x12\x19\n\x11target_task_queue\x18\x04 \x01(\t\x12\x0c\n\x04name\x18\x05 \x01(\t"n\n\x19GetNexusEndpointsResponse\x12\x38\n\tendpoints\x18\x01 \x03(\x0b\x32%.temporal.api.cloud.nexus.v1.Endpoint\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t".\n\x17GetNexusEndpointRequest\x12\x13\n\x0b\x65ndpoint_id\x18\x01 \x01(\t"S\n\x18GetNexusEndpointResponse\x12\x37\n\x08\x65ndpoint\x18\x01 \x01(\x0b\x32%.temporal.api.cloud.nexus.v1.Endpoint"q\n\x1a\x43reateNexusEndpointRequest\x12\x37\n\x04spec\x18\x01 \x01(\x0b\x32).temporal.api.cloud.nexus.v1.EndpointSpec\x12\x1a\n\x12\x61sync_operation_id\x18\x02 \x01(\t"|\n\x1b\x43reateNexusEndpointResponse\x12\x13\n\x0b\x65ndpoint_id\x18\x01 \x01(\t\x12H\n\x0f\x61sync_operation\x18\x02 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\xa0\x01\n\x1aUpdateNexusEndpointRequest\x12\x13\n\x0b\x65ndpoint_id\x18\x01 \x01(\t\x12\x37\n\x04spec\x18\x02 \x01(\x0b\x32).temporal.api.cloud.nexus.v1.EndpointSpec\x12\x18\n\x10resource_version\x18\x03 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x04 \x01(\t"g\n\x1bUpdateNexusEndpointResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"g\n\x1a\x44\x65leteNexusEndpointRequest\x12\x13\n\x0b\x65ndpoint_id\x18\x01 \x01(\t\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x03 \x01(\t"g\n\x1b\x44\x65leteNexusEndpointResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\xf5\x02\n\x14GetUserGroupsRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x12\n\npage_token\x18\x02 \x01(\t\x12\x11\n\tnamespace\x18\x03 \x01(\t\x12\x14\n\x0c\x64isplay_name\x18\x04 \x01(\t\x12`\n\x0cgoogle_group\x18\x05 \x01(\x0b\x32J.temporal.api.cloud.cloudservice.v1.GetUserGroupsRequest.GoogleGroupFilter\x12\\\n\nscim_group\x18\x06 \x01(\x0b\x32H.temporal.api.cloud.cloudservice.v1.GetUserGroupsRequest.SCIMGroupFilter\x1a*\n\x11GoogleGroupFilter\x12\x15\n\remail_address\x18\x01 \x01(\t\x1a!\n\x0fSCIMGroupFilter\x12\x0e\n\x06idp_id\x18\x01 \x01(\t"k\n\x15GetUserGroupsResponse\x12\x39\n\x06groups\x18\x01 \x03(\x0b\x32).temporal.api.cloud.identity.v1.UserGroup\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t"\'\n\x13GetUserGroupRequest\x12\x10\n\x08group_id\x18\x01 \x01(\t"P\n\x14GetUserGroupResponse\x12\x38\n\x05group\x18\x01 \x01(\x0b\x32).temporal.api.cloud.identity.v1.UserGroup"q\n\x16\x43reateUserGroupRequest\x12;\n\x04spec\x18\x01 \x01(\x0b\x32-.temporal.api.cloud.identity.v1.UserGroupSpec\x12\x1a\n\x12\x61sync_operation_id\x18\x02 \x01(\t"u\n\x17\x43reateUserGroupResponse\x12\x10\n\x08group_id\x18\x01 \x01(\t\x12H\n\x0f\x61sync_operation\x18\x02 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\x9d\x01\n\x16UpdateUserGroupRequest\x12\x10\n\x08group_id\x18\x01 \x01(\t\x12;\n\x04spec\x18\x02 \x01(\x0b\x32-.temporal.api.cloud.identity.v1.UserGroupSpec\x12\x18\n\x10resource_version\x18\x03 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x04 \x01(\t"c\n\x17UpdateUserGroupResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"`\n\x16\x44\x65leteUserGroupRequest\x12\x10\n\x08group_id\x18\x01 \x01(\t\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x03 \x01(\t"c\n\x17\x44\x65leteUserGroupResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\xc0\x01\n"SetUserGroupNamespaceAccessRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x10\n\x08group_id\x18\x02 \x01(\t\x12?\n\x06\x61\x63\x63\x65ss\x18\x03 \x01(\x0b\x32/.temporal.api.cloud.identity.v1.NamespaceAccess\x12\x18\n\x10resource_version\x18\x04 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x05 \x01(\t"o\n#SetUserGroupNamespaceAccessResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\x8f\x01\n\x19\x41\x64\x64UserGroupMemberRequest\x12\x10\n\x08group_id\x18\x01 \x01(\t\x12\x44\n\tmember_id\x18\x02 \x01(\x0b\x32\x31.temporal.api.cloud.identity.v1.UserGroupMemberId\x12\x1a\n\x12\x61sync_operation_id\x18\x03 \x01(\t"f\n\x1a\x41\x64\x64UserGroupMemberResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\x92\x01\n\x1cRemoveUserGroupMemberRequest\x12\x10\n\x08group_id\x18\x01 \x01(\t\x12\x44\n\tmember_id\x18\x02 \x01(\x0b\x32\x31.temporal.api.cloud.identity.v1.UserGroupMemberId\x12\x1a\n\x12\x61sync_operation_id\x18\x03 \x01(\t"i\n\x1dRemoveUserGroupMemberResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"U\n\x1aGetUserGroupMembersRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x12\n\npage_token\x18\x02 \x01(\t\x12\x10\n\x08group_id\x18\x03 \x01(\t"x\n\x1bGetUserGroupMembersResponse\x12@\n\x07members\x18\x01 \x03(\x0b\x32/.temporal.api.cloud.identity.v1.UserGroupMember\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t"{\n\x1b\x43reateServiceAccountRequest\x12@\n\x04spec\x18\x01 \x01(\x0b\x32\x32.temporal.api.cloud.identity.v1.ServiceAccountSpec\x12\x1a\n\x12\x61sync_operation_id\x18\x02 \x01(\t"\x84\x01\n\x1c\x43reateServiceAccountResponse\x12\x1a\n\x12service_account_id\x18\x01 \x01(\t\x12H\n\x0f\x61sync_operation\x18\x02 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"6\n\x18GetServiceAccountRequest\x12\x1a\n\x12service_account_id\x18\x01 \x01(\t"d\n\x19GetServiceAccountResponse\x12G\n\x0fservice_account\x18\x01 \x01(\x0b\x32..temporal.api.cloud.identity.v1.ServiceAccount"B\n\x19GetServiceAccountsRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x12\n\npage_token\x18\x02 \x01(\t"~\n\x1aGetServiceAccountsResponse\x12G\n\x0fservice_account\x18\x01 \x03(\x0b\x32..temporal.api.cloud.identity.v1.ServiceAccount\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t"\xb1\x01\n\x1bUpdateServiceAccountRequest\x12\x1a\n\x12service_account_id\x18\x01 \x01(\t\x12@\n\x04spec\x18\x02 \x01(\x0b\x32\x32.temporal.api.cloud.identity.v1.ServiceAccountSpec\x12\x18\n\x10resource_version\x18\x03 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x04 \x01(\t"h\n\x1cUpdateServiceAccountResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"o\n\x1b\x44\x65leteServiceAccountRequest\x12\x1a\n\x12service_account_id\x18\x01 \x01(\t\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x03 \x01(\t"h\n\x1c\x44\x65leteServiceAccountResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\xaa\x01\n\x0fGetUsageRequest\x12\x38\n\x14start_time_inclusive\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x36\n\x12\x65nd_time_exclusive\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x11\n\tpage_size\x18\x03 \x01(\x05\x12\x12\n\npage_token\x18\x04 \x01(\t"d\n\x10GetUsageResponse\x12\x37\n\tsummaries\x18\x01 \x03(\x0b\x32$.temporal.api.cloud.usage.v1.Summary\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t"\x13\n\x11GetAccountRequest"M\n\x12GetAccountResponse\x12\x37\n\x07\x61\x63\x63ount\x18\x01 \x01(\x0b\x32&.temporal.api.cloud.account.v1.Account"\x86\x01\n\x14UpdateAccountRequest\x12\x38\n\x04spec\x18\x01 \x01(\x0b\x32*.temporal.api.cloud.account.v1.AccountSpec\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x03 \x01(\t"a\n\x15UpdateAccountResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\x90\x01\n CreateNamespaceExportSinkRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12=\n\x04spec\x18\x02 \x01(\x0b\x32/.temporal.api.cloud.namespace.v1.ExportSinkSpec\x12\x1a\n\x12\x61sync_operation_id\x18\x03 \x01(\t"m\n!CreateNamespaceExportSinkResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"@\n\x1dGetNamespaceExportSinkRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t"[\n\x1eGetNamespaceExportSinkResponse\x12\x39\n\x04sink\x18\x01 \x01(\x0b\x32+.temporal.api.cloud.namespace.v1.ExportSink"Z\n\x1eGetNamespaceExportSinksRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x12\n\npage_token\x18\x03 \x01(\t"v\n\x1fGetNamespaceExportSinksResponse\x12:\n\x05sinks\x18\x01 \x03(\x0b\x32+.temporal.api.cloud.namespace.v1.ExportSink\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t"\xaa\x01\n UpdateNamespaceExportSinkRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12=\n\x04spec\x18\x02 \x01(\x0b\x32/.temporal.api.cloud.namespace.v1.ExportSinkSpec\x12\x18\n\x10resource_version\x18\x03 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x04 \x01(\t"m\n!UpdateNamespaceExportSinkResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"y\n DeleteNamespaceExportSinkRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x18\n\x10resource_version\x18\x03 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x04 \x01(\t"m\n!DeleteNamespaceExportSinkResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"v\n"ValidateNamespaceExportSinkRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12=\n\x04spec\x18\x02 \x01(\x0b\x32/.temporal.api.cloud.namespace.v1.ExportSinkSpec"%\n#ValidateNamespaceExportSinkResponse"\x82\x02\n\x1aUpdateNamespaceTagsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12h\n\x0etags_to_upsert\x18\x02 \x03(\x0b\x32P.temporal.api.cloud.cloudservice.v1.UpdateNamespaceTagsRequest.TagsToUpsertEntry\x12\x16\n\x0etags_to_remove\x18\x03 \x03(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x04 \x01(\t\x1a\x33\n\x11TagsToUpsertEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"g\n\x1bUpdateNamespaceTagsResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\x87\x01\n\x1d\x43reateConnectivityRuleRequest\x12J\n\x04spec\x18\x01 \x01(\x0b\x32<.temporal.api.cloud.connectivityrule.v1.ConnectivityRuleSpec\x12\x1a\n\x12\x61sync_operation_id\x18\x02 \x01(\t"\x88\x01\n\x1e\x43reateConnectivityRuleResponse\x12\x1c\n\x14\x63onnectivity_rule_id\x18\x01 \x01(\t\x12H\n\x0f\x61sync_operation\x18\x02 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation":\n\x1aGetConnectivityRuleRequest\x12\x1c\n\x14\x63onnectivity_rule_id\x18\x01 \x01(\t"r\n\x1bGetConnectivityRuleResponse\x12S\n\x11\x63onnectivity_rule\x18\x01 \x01(\x0b\x32\x38.temporal.api.cloud.connectivityrule.v1.ConnectivityRule"W\n\x1bGetConnectivityRulesRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x12\n\npage_token\x18\x02 \x01(\t\x12\x11\n\tnamespace\x18\x03 \x01(\t"\x8d\x01\n\x1cGetConnectivityRulesResponse\x12T\n\x12\x63onnectivity_rules\x18\x01 \x03(\x0b\x32\x38.temporal.api.cloud.connectivityrule.v1.ConnectivityRule\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t"s\n\x1d\x44\x65leteConnectivityRuleRequest\x12\x1c\n\x14\x63onnectivity_rule_id\x18\x01 \x01(\t\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x03 \x01(\t"j\n\x1e\x44\x65leteConnectivityRuleResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperationB\xc8\x01\n%io.temporal.api.cloud.cloudservice.v1B\x14RequestResponseProtoP\x01Z5go.temporal.io/api/cloud/cloudservice/v1;cloudservice\xaa\x02$Temporalio.Api.Cloud.CloudService.V1\xea\x02(Temporalio::Api::Cloud::CloudService::V1b\x06proto3' + b'\n9temporal/api/cloud/cloudservice/v1/request_response.proto\x12"temporal.api.cloud.cloudservice.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a-temporal/api/cloud/operation/v1/message.proto\x1a,temporal/api/cloud/identity/v1/message.proto\x1a-temporal/api/cloud/namespace/v1/message.proto\x1a)temporal/api/cloud/nexus/v1/message.proto\x1a*temporal/api/cloud/region/v1/message.proto\x1a+temporal/api/cloud/account/v1/message.proto\x1a)temporal/api/cloud/usage/v1/message.proto\x1a\x34temporal/api/cloud/connectivityrule/v1/message.proto\x1a,temporal/api/cloud/auditlog/v1/message.proto\x1a+temporal/api/cloud/billing/v1/message.proto"\x1b\n\x19GetCurrentIdentityRequest"\xed\x01\n\x1aGetCurrentIdentityResponse\x12\x34\n\x04user\x18\x01 \x01(\x0b\x32$.temporal.api.cloud.identity.v1.UserH\x00\x12I\n\x0fservice_account\x18\x02 \x01(\x0b\x32..temporal.api.cloud.identity.v1.ServiceAccountH\x00\x12\x41\n\x11principal_api_key\x18\x03 \x01(\x0b\x32&.temporal.api.cloud.identity.v1.ApiKeyB\x0b\n\tprincipal"Z\n\x0fGetUsersRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x12\n\npage_token\x18\x02 \x01(\t\x12\r\n\x05\x65mail\x18\x03 \x01(\t\x12\x11\n\tnamespace\x18\x04 \x01(\t"`\n\x10GetUsersResponse\x12\x33\n\x05users\x18\x01 \x03(\x0b\x32$.temporal.api.cloud.identity.v1.User\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t"!\n\x0eGetUserRequest\x12\x0f\n\x07user_id\x18\x01 \x01(\t"E\n\x0fGetUserResponse\x12\x32\n\x04user\x18\x01 \x01(\x0b\x32$.temporal.api.cloud.identity.v1.User"g\n\x11\x43reateUserRequest\x12\x36\n\x04spec\x18\x01 \x01(\x0b\x32(.temporal.api.cloud.identity.v1.UserSpec\x12\x1a\n\x12\x61sync_operation_id\x18\x02 \x01(\t"o\n\x12\x43reateUserResponse\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12H\n\x0f\x61sync_operation\x18\x02 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\x92\x01\n\x11UpdateUserRequest\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12\x36\n\x04spec\x18\x02 \x01(\x0b\x32(.temporal.api.cloud.identity.v1.UserSpec\x12\x18\n\x10resource_version\x18\x03 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x04 \x01(\t"^\n\x12UpdateUserResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"Z\n\x11\x44\x65leteUserRequest\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x03 \x01(\t"^\n\x12\x44\x65leteUserResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\xba\x01\n\x1dSetUserNamespaceAccessRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x0f\n\x07user_id\x18\x02 \x01(\t\x12?\n\x06\x61\x63\x63\x65ss\x18\x03 \x01(\x0b\x32/.temporal.api.cloud.identity.v1.NamespaceAccess\x12\x18\n\x10resource_version\x18\x04 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x05 \x01(\t"j\n\x1eSetUserNamespaceAccessResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"6\n\x18GetAsyncOperationRequest\x12\x1a\n\x12\x61sync_operation_id\x18\x01 \x01(\t"e\n\x19GetAsyncOperationResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\xf3\x01\n\x16\x43reateNamespaceRequest\x12<\n\x04spec\x18\x02 \x01(\x0b\x32..temporal.api.cloud.namespace.v1.NamespaceSpec\x12\x1a\n\x12\x61sync_operation_id\x18\x03 \x01(\t\x12R\n\x04tags\x18\x04 \x03(\x0b\x32\x44.temporal.api.cloud.cloudservice.v1.CreateNamespaceRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"v\n\x17\x43reateNamespaceResponse\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12H\n\x0f\x61sync_operation\x18\x02 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"K\n\x14GetNamespacesRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x12\n\npage_token\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t"p\n\x15GetNamespacesResponse\x12>\n\nnamespaces\x18\x01 \x03(\x0b\x32*.temporal.api.cloud.namespace.v1.Namespace\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t"(\n\x13GetNamespaceRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t"U\n\x14GetNamespaceResponse\x12=\n\tnamespace\x18\x01 \x01(\x0b\x32*.temporal.api.cloud.namespace.v1.Namespace"\x9f\x01\n\x16UpdateNamespaceRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12<\n\x04spec\x18\x02 \x01(\x0b\x32..temporal.api.cloud.namespace.v1.NamespaceSpec\x12\x18\n\x10resource_version\x18\x03 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x04 \x01(\t"c\n\x17UpdateNamespaceResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\xc6\x01\n"RenameCustomSearchAttributeRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12-\n%existing_custom_search_attribute_name\x18\x02 \x01(\t\x12(\n new_custom_search_attribute_name\x18\x03 \x01(\t\x12\x18\n\x10resource_version\x18\x04 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x05 \x01(\t"o\n#RenameCustomSearchAttributeResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"a\n\x16\x44\x65leteNamespaceRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x03 \x01(\t"c\n\x17\x44\x65leteNamespaceResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"_\n\x1e\x46\x61iloverNamespaceRegionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x0e\n\x06region\x18\x02 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x03 \x01(\t"k\n\x1f\x46\x61iloverNamespaceRegionResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"t\n\x19\x41\x64\x64NamespaceRegionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x0e\n\x06region\x18\x02 \x01(\t\x12\x18\n\x10resource_version\x18\x03 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x04 \x01(\t"f\n\x1a\x41\x64\x64NamespaceRegionResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"w\n\x1c\x44\x65leteNamespaceRegionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x0e\n\x06region\x18\x02 \x01(\t\x12\x18\n\x10resource_version\x18\x03 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x04 \x01(\t"i\n\x1d\x44\x65leteNamespaceRegionResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\x13\n\x11GetRegionsRequest"K\n\x12GetRegionsResponse\x12\x35\n\x07regions\x18\x01 \x03(\x0b\x32$.temporal.api.cloud.region.v1.Region""\n\x10GetRegionRequest\x12\x0e\n\x06region\x18\x01 \x01(\t"I\n\x11GetRegionResponse\x12\x34\n\x06region\x18\x01 \x01(\x0b\x32$.temporal.api.cloud.region.v1.Region"\xae\x01\n\x11GetApiKeysRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x12\n\npage_token\x18\x02 \x01(\t\x12\x10\n\x08owner_id\x18\x03 \x01(\t\x12!\n\x15owner_type_deprecated\x18\x04 \x01(\tB\x02\x18\x01\x12=\n\nowner_type\x18\x05 \x01(\x0e\x32).temporal.api.cloud.identity.v1.OwnerType"g\n\x12GetApiKeysResponse\x12\x38\n\x08\x61pi_keys\x18\x01 \x03(\x0b\x32&.temporal.api.cloud.identity.v1.ApiKey\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t""\n\x10GetApiKeyRequest\x12\x0e\n\x06key_id\x18\x01 \x01(\t"L\n\x11GetApiKeyResponse\x12\x37\n\x07\x61pi_key\x18\x01 \x01(\x0b\x32&.temporal.api.cloud.identity.v1.ApiKey"k\n\x13\x43reateApiKeyRequest\x12\x38\n\x04spec\x18\x01 \x01(\x0b\x32*.temporal.api.cloud.identity.v1.ApiKeySpec\x12\x1a\n\x12\x61sync_operation_id\x18\x02 \x01(\t"\x7f\n\x14\x43reateApiKeyResponse\x12\x0e\n\x06key_id\x18\x01 \x01(\t\x12\r\n\x05token\x18\x02 \x01(\t\x12H\n\x0f\x61sync_operation\x18\x03 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\x95\x01\n\x13UpdateApiKeyRequest\x12\x0e\n\x06key_id\x18\x01 \x01(\t\x12\x38\n\x04spec\x18\x02 \x01(\x0b\x32*.temporal.api.cloud.identity.v1.ApiKeySpec\x12\x18\n\x10resource_version\x18\x03 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x04 \x01(\t"`\n\x14UpdateApiKeyResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"[\n\x13\x44\x65leteApiKeyRequest\x12\x0e\n\x06key_id\x18\x01 \x01(\t\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x03 \x01(\t"`\n\x14\x44\x65leteApiKeyResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\x87\x01\n\x18GetNexusEndpointsRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x12\n\npage_token\x18\x02 \x01(\t\x12\x1b\n\x13target_namespace_id\x18\x03 \x01(\t\x12\x19\n\x11target_task_queue\x18\x04 \x01(\t\x12\x0c\n\x04name\x18\x05 \x01(\t"n\n\x19GetNexusEndpointsResponse\x12\x38\n\tendpoints\x18\x01 \x03(\x0b\x32%.temporal.api.cloud.nexus.v1.Endpoint\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t".\n\x17GetNexusEndpointRequest\x12\x13\n\x0b\x65ndpoint_id\x18\x01 \x01(\t"S\n\x18GetNexusEndpointResponse\x12\x37\n\x08\x65ndpoint\x18\x01 \x01(\x0b\x32%.temporal.api.cloud.nexus.v1.Endpoint"q\n\x1a\x43reateNexusEndpointRequest\x12\x37\n\x04spec\x18\x01 \x01(\x0b\x32).temporal.api.cloud.nexus.v1.EndpointSpec\x12\x1a\n\x12\x61sync_operation_id\x18\x02 \x01(\t"|\n\x1b\x43reateNexusEndpointResponse\x12\x13\n\x0b\x65ndpoint_id\x18\x01 \x01(\t\x12H\n\x0f\x61sync_operation\x18\x02 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\xa0\x01\n\x1aUpdateNexusEndpointRequest\x12\x13\n\x0b\x65ndpoint_id\x18\x01 \x01(\t\x12\x37\n\x04spec\x18\x02 \x01(\x0b\x32).temporal.api.cloud.nexus.v1.EndpointSpec\x12\x18\n\x10resource_version\x18\x03 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x04 \x01(\t"g\n\x1bUpdateNexusEndpointResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"g\n\x1a\x44\x65leteNexusEndpointRequest\x12\x13\n\x0b\x65ndpoint_id\x18\x01 \x01(\t\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x03 \x01(\t"g\n\x1b\x44\x65leteNexusEndpointResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\xf5\x02\n\x14GetUserGroupsRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x12\n\npage_token\x18\x02 \x01(\t\x12\x11\n\tnamespace\x18\x03 \x01(\t\x12\x14\n\x0c\x64isplay_name\x18\x04 \x01(\t\x12`\n\x0cgoogle_group\x18\x05 \x01(\x0b\x32J.temporal.api.cloud.cloudservice.v1.GetUserGroupsRequest.GoogleGroupFilter\x12\\\n\nscim_group\x18\x06 \x01(\x0b\x32H.temporal.api.cloud.cloudservice.v1.GetUserGroupsRequest.SCIMGroupFilter\x1a*\n\x11GoogleGroupFilter\x12\x15\n\remail_address\x18\x01 \x01(\t\x1a!\n\x0fSCIMGroupFilter\x12\x0e\n\x06idp_id\x18\x01 \x01(\t"k\n\x15GetUserGroupsResponse\x12\x39\n\x06groups\x18\x01 \x03(\x0b\x32).temporal.api.cloud.identity.v1.UserGroup\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t"\'\n\x13GetUserGroupRequest\x12\x10\n\x08group_id\x18\x01 \x01(\t"P\n\x14GetUserGroupResponse\x12\x38\n\x05group\x18\x01 \x01(\x0b\x32).temporal.api.cloud.identity.v1.UserGroup"q\n\x16\x43reateUserGroupRequest\x12;\n\x04spec\x18\x01 \x01(\x0b\x32-.temporal.api.cloud.identity.v1.UserGroupSpec\x12\x1a\n\x12\x61sync_operation_id\x18\x02 \x01(\t"u\n\x17\x43reateUserGroupResponse\x12\x10\n\x08group_id\x18\x01 \x01(\t\x12H\n\x0f\x61sync_operation\x18\x02 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\x9d\x01\n\x16UpdateUserGroupRequest\x12\x10\n\x08group_id\x18\x01 \x01(\t\x12;\n\x04spec\x18\x02 \x01(\x0b\x32-.temporal.api.cloud.identity.v1.UserGroupSpec\x12\x18\n\x10resource_version\x18\x03 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x04 \x01(\t"c\n\x17UpdateUserGroupResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"`\n\x16\x44\x65leteUserGroupRequest\x12\x10\n\x08group_id\x18\x01 \x01(\t\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x03 \x01(\t"c\n\x17\x44\x65leteUserGroupResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\xc0\x01\n"SetUserGroupNamespaceAccessRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x10\n\x08group_id\x18\x02 \x01(\t\x12?\n\x06\x61\x63\x63\x65ss\x18\x03 \x01(\x0b\x32/.temporal.api.cloud.identity.v1.NamespaceAccess\x12\x18\n\x10resource_version\x18\x04 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x05 \x01(\t"o\n#SetUserGroupNamespaceAccessResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\x8f\x01\n\x19\x41\x64\x64UserGroupMemberRequest\x12\x10\n\x08group_id\x18\x01 \x01(\t\x12\x44\n\tmember_id\x18\x02 \x01(\x0b\x32\x31.temporal.api.cloud.identity.v1.UserGroupMemberId\x12\x1a\n\x12\x61sync_operation_id\x18\x03 \x01(\t"f\n\x1a\x41\x64\x64UserGroupMemberResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\x92\x01\n\x1cRemoveUserGroupMemberRequest\x12\x10\n\x08group_id\x18\x01 \x01(\t\x12\x44\n\tmember_id\x18\x02 \x01(\x0b\x32\x31.temporal.api.cloud.identity.v1.UserGroupMemberId\x12\x1a\n\x12\x61sync_operation_id\x18\x03 \x01(\t"i\n\x1dRemoveUserGroupMemberResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"U\n\x1aGetUserGroupMembersRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x12\n\npage_token\x18\x02 \x01(\t\x12\x10\n\x08group_id\x18\x03 \x01(\t"x\n\x1bGetUserGroupMembersResponse\x12@\n\x07members\x18\x01 \x03(\x0b\x32/.temporal.api.cloud.identity.v1.UserGroupMember\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t"{\n\x1b\x43reateServiceAccountRequest\x12@\n\x04spec\x18\x01 \x01(\x0b\x32\x32.temporal.api.cloud.identity.v1.ServiceAccountSpec\x12\x1a\n\x12\x61sync_operation_id\x18\x02 \x01(\t"\x84\x01\n\x1c\x43reateServiceAccountResponse\x12\x1a\n\x12service_account_id\x18\x01 \x01(\t\x12H\n\x0f\x61sync_operation\x18\x02 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"6\n\x18GetServiceAccountRequest\x12\x1a\n\x12service_account_id\x18\x01 \x01(\t"d\n\x19GetServiceAccountResponse\x12G\n\x0fservice_account\x18\x01 \x01(\x0b\x32..temporal.api.cloud.identity.v1.ServiceAccount"B\n\x19GetServiceAccountsRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x12\n\npage_token\x18\x02 \x01(\t"~\n\x1aGetServiceAccountsResponse\x12G\n\x0fservice_account\x18\x01 \x03(\x0b\x32..temporal.api.cloud.identity.v1.ServiceAccount\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t"\xb1\x01\n\x1bUpdateServiceAccountRequest\x12\x1a\n\x12service_account_id\x18\x01 \x01(\t\x12@\n\x04spec\x18\x02 \x01(\x0b\x32\x32.temporal.api.cloud.identity.v1.ServiceAccountSpec\x12\x18\n\x10resource_version\x18\x03 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x04 \x01(\t"h\n\x1cUpdateServiceAccountResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\xcf\x01\n\'SetServiceAccountNamespaceAccessRequest\x12\x1a\n\x12service_account_id\x18\x01 \x01(\t\x12\x11\n\tnamespace\x18\x02 \x01(\t\x12?\n\x06\x61\x63\x63\x65ss\x18\x03 \x01(\x0b\x32/.temporal.api.cloud.identity.v1.NamespaceAccess\x12\x18\n\x10resource_version\x18\x04 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x05 \x01(\t"t\n(SetServiceAccountNamespaceAccessResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"o\n\x1b\x44\x65leteServiceAccountRequest\x12\x1a\n\x12service_account_id\x18\x01 \x01(\t\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x03 \x01(\t"h\n\x1c\x44\x65leteServiceAccountResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\xaa\x01\n\x0fGetUsageRequest\x12\x38\n\x14start_time_inclusive\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x36\n\x12\x65nd_time_exclusive\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x11\n\tpage_size\x18\x03 \x01(\x05\x12\x12\n\npage_token\x18\x04 \x01(\t"d\n\x10GetUsageResponse\x12\x37\n\tsummaries\x18\x01 \x03(\x0b\x32$.temporal.api.cloud.usage.v1.Summary\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t"\x13\n\x11GetAccountRequest"M\n\x12GetAccountResponse\x12\x37\n\x07\x61\x63\x63ount\x18\x01 \x01(\x0b\x32&.temporal.api.cloud.account.v1.Account"\x86\x01\n\x14UpdateAccountRequest\x12\x38\n\x04spec\x18\x01 \x01(\x0b\x32*.temporal.api.cloud.account.v1.AccountSpec\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x03 \x01(\t"a\n\x15UpdateAccountResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\x90\x01\n CreateNamespaceExportSinkRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12=\n\x04spec\x18\x02 \x01(\x0b\x32/.temporal.api.cloud.namespace.v1.ExportSinkSpec\x12\x1a\n\x12\x61sync_operation_id\x18\x03 \x01(\t"m\n!CreateNamespaceExportSinkResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"@\n\x1dGetNamespaceExportSinkRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t"[\n\x1eGetNamespaceExportSinkResponse\x12\x39\n\x04sink\x18\x01 \x01(\x0b\x32+.temporal.api.cloud.namespace.v1.ExportSink"Z\n\x1eGetNamespaceExportSinksRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x12\n\npage_token\x18\x03 \x01(\t"v\n\x1fGetNamespaceExportSinksResponse\x12:\n\x05sinks\x18\x01 \x03(\x0b\x32+.temporal.api.cloud.namespace.v1.ExportSink\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t"\xaa\x01\n UpdateNamespaceExportSinkRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12=\n\x04spec\x18\x02 \x01(\x0b\x32/.temporal.api.cloud.namespace.v1.ExportSinkSpec\x12\x18\n\x10resource_version\x18\x03 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x04 \x01(\t"m\n!UpdateNamespaceExportSinkResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"y\n DeleteNamespaceExportSinkRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x18\n\x10resource_version\x18\x03 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x04 \x01(\t"m\n!DeleteNamespaceExportSinkResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"v\n"ValidateNamespaceExportSinkRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12=\n\x04spec\x18\x02 \x01(\x0b\x32/.temporal.api.cloud.namespace.v1.ExportSinkSpec"%\n#ValidateNamespaceExportSinkResponse"\x82\x02\n\x1aUpdateNamespaceTagsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12h\n\x0etags_to_upsert\x18\x02 \x03(\x0b\x32P.temporal.api.cloud.cloudservice.v1.UpdateNamespaceTagsRequest.TagsToUpsertEntry\x12\x16\n\x0etags_to_remove\x18\x03 \x03(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x04 \x01(\t\x1a\x33\n\x11TagsToUpsertEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"g\n\x1bUpdateNamespaceTagsResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\x87\x01\n\x1d\x43reateConnectivityRuleRequest\x12J\n\x04spec\x18\x01 \x01(\x0b\x32<.temporal.api.cloud.connectivityrule.v1.ConnectivityRuleSpec\x12\x1a\n\x12\x61sync_operation_id\x18\x02 \x01(\t"\x88\x01\n\x1e\x43reateConnectivityRuleResponse\x12\x1c\n\x14\x63onnectivity_rule_id\x18\x01 \x01(\t\x12H\n\x0f\x61sync_operation\x18\x02 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation":\n\x1aGetConnectivityRuleRequest\x12\x1c\n\x14\x63onnectivity_rule_id\x18\x01 \x01(\t"r\n\x1bGetConnectivityRuleResponse\x12S\n\x11\x63onnectivity_rule\x18\x01 \x01(\x0b\x32\x38.temporal.api.cloud.connectivityrule.v1.ConnectivityRule"W\n\x1bGetConnectivityRulesRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x12\n\npage_token\x18\x02 \x01(\t\x12\x11\n\tnamespace\x18\x03 \x01(\t"\x8d\x01\n\x1cGetConnectivityRulesResponse\x12T\n\x12\x63onnectivity_rules\x18\x01 \x03(\x0b\x32\x38.temporal.api.cloud.connectivityrule.v1.ConnectivityRule\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t"s\n\x1d\x44\x65leteConnectivityRuleRequest\x12\x1c\n\x14\x63onnectivity_rule_id\x18\x01 \x01(\t\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x03 \x01(\t"j\n\x1e\x44\x65leteConnectivityRuleResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\xae\x01\n\x13GetAuditLogsRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x12\n\npage_token\x18\x02 \x01(\t\x12\x38\n\x14start_time_inclusive\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x36\n\x12\x65nd_time_exclusive\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"h\n\x14GetAuditLogsResponse\x12\x37\n\x04logs\x18\x01 \x03(\x0b\x32).temporal.api.cloud.auditlog.v1.LogRecord\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t"c\n"ValidateAccountAuditLogSinkRequest\x12=\n\x04spec\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.account.v1.AuditLogSinkSpec"%\n#ValidateAccountAuditLogSinkResponse"}\n CreateAccountAuditLogSinkRequest\x12=\n\x04spec\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.account.v1.AuditLogSinkSpec\x12\x1a\n\x12\x61sync_operation_id\x18\x02 \x01(\t"m\n!CreateAccountAuditLogSinkResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"-\n\x1dGetAccountAuditLogSinkRequest\x12\x0c\n\x04name\x18\x01 \x01(\t"[\n\x1eGetAccountAuditLogSinkResponse\x12\x39\n\x04sink\x18\x01 \x01(\x0b\x32+.temporal.api.cloud.account.v1.AuditLogSink"G\n\x1eGetAccountAuditLogSinksRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x12\n\npage_token\x18\x02 \x01(\t"v\n\x1fGetAccountAuditLogSinksResponse\x12:\n\x05sinks\x18\x01 \x03(\x0b\x32+.temporal.api.cloud.account.v1.AuditLogSink\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t"\x97\x01\n UpdateAccountAuditLogSinkRequest\x12=\n\x04spec\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.account.v1.AuditLogSinkSpec\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x03 \x01(\t"m\n!UpdateAccountAuditLogSinkResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"f\n DeleteAccountAuditLogSinkRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x03 \x01(\t"m\n!DeleteAccountAuditLogSinkResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"4\n\x1fGetNamespaceCapacityInfoRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t"q\n GetNamespaceCapacityInfoResponse\x12M\n\rcapacity_info\x18\x01 \x01(\x0b\x32\x36.temporal.api.cloud.namespace.v1.NamespaceCapacityInfo"x\n\x1a\x43reateBillingReportRequest\x12>\n\x04spec\x18\x01 \x01(\x0b\x32\x30.temporal.api.cloud.billing.v1.BillingReportSpec\x12\x1a\n\x12\x61sync_operation_id\x18\x02 \x01(\t"\x82\x01\n\x1b\x43reateBillingReportResponse\x12\x19\n\x11\x62illing_report_id\x18\x01 \x01(\t\x12H\n\x0f\x61sync_operation\x18\x02 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"4\n\x17GetBillingReportRequest\x12\x19\n\x11\x62illing_report_id\x18\x01 \x01(\t"`\n\x18GetBillingReportResponse\x12\x44\n\x0e\x62illing_report\x18\x01 \x01(\x0b\x32,.temporal.api.cloud.billing.v1.BillingReport">\n\x15GetCustomRolesRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x12\n\npage_token\x18\x02 \x01(\t"s\n\x16GetCustomRolesResponse\x12@\n\x0c\x63ustom_roles\x18\x01 \x03(\x0b\x32*.temporal.api.cloud.identity.v1.CustomRole\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t"\'\n\x14GetCustomRoleRequest\x12\x0f\n\x07role_id\x18\x01 \x01(\t"X\n\x15GetCustomRoleResponse\x12?\n\x0b\x63ustom_role\x18\x01 \x01(\x0b\x32*.temporal.api.cloud.identity.v1.CustomRole"s\n\x17\x43reateCustomRoleRequest\x12<\n\x04spec\x18\x01 \x01(\x0b\x32..temporal.api.cloud.identity.v1.CustomRoleSpec\x12\x1a\n\x12\x61sync_operation_id\x18\x02 \x01(\t"u\n\x18\x43reateCustomRoleResponse\x12\x0f\n\x07role_id\x18\x01 \x01(\t\x12H\n\x0f\x61sync_operation\x18\x02 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"\x9e\x01\n\x17UpdateCustomRoleRequest\x12\x0f\n\x07role_id\x18\x01 \x01(\t\x12<\n\x04spec\x18\x02 \x01(\x0b\x32..temporal.api.cloud.identity.v1.CustomRoleSpec\x12\x18\n\x10resource_version\x18\x03 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x04 \x01(\t"d\n\x18UpdateCustomRoleResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"`\n\x17\x44\x65leteCustomRoleRequest\x12\x0f\n\x07role_id\x18\x01 \x01(\t\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12\x1a\n\x12\x61sync_operation_id\x18\x03 \x01(\t"d\n\x18\x44\x65leteCustomRoleResponse\x12H\n\x0f\x61sync_operation\x18\x01 \x01(\x0b\x32/.temporal.api.cloud.operation.v1.AsyncOperation"^\n"GetUserNamespaceAssignmentsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x12\n\npage_token\x18\x03 \x01(\t"\x86\x01\n#GetUserNamespaceAssignmentsResponse\x12\x46\n\x05users\x18\x01 \x03(\x0b\x32\x37.temporal.api.cloud.identity.v1.UserNamespaceAssignment\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t"h\n,GetServiceAccountNamespaceAssignmentsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x12\n\npage_token\x18\x03 \x01(\t"\xa5\x01\n-GetServiceAccountNamespaceAssignmentsResponse\x12[\n\x10service_accounts\x18\x01 \x03(\x0b\x32\x41.temporal.api.cloud.identity.v1.ServiceAccountNamespaceAssignment\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t"c\n\'GetUserGroupNamespaceAssignmentsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x12\n\npage_token\x18\x03 \x01(\t"\x91\x01\n(GetUserGroupNamespaceAssignmentsResponse\x12L\n\x06groups\x18\x01 \x03(\x0b\x32<.temporal.api.cloud.identity.v1.UserGroupNamespaceAssignment\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\tB\xc8\x01\n%io.temporal.api.cloud.cloudservice.v1B\x14RequestResponseProtoP\x01Z5go.temporal.io/api/cloud/cloudservice/v1;cloudservice\xaa\x02$Temporalio.Api.Cloud.CloudService.V1\xea\x02(Temporalio::Api::Cloud::CloudService::V1b\x06proto3' ) +_GETCURRENTIDENTITYREQUEST = DESCRIPTOR.message_types_by_name[ + "GetCurrentIdentityRequest" +] +_GETCURRENTIDENTITYRESPONSE = DESCRIPTOR.message_types_by_name[ + "GetCurrentIdentityResponse" +] _GETUSERSREQUEST = DESCRIPTOR.message_types_by_name["GetUsersRequest"] _GETUSERSRESPONSE = DESCRIPTOR.message_types_by_name["GetUsersResponse"] _GETUSERREQUEST = DESCRIPTOR.message_types_by_name["GetUserRequest"] @@ -203,6 +215,12 @@ _UPDATESERVICEACCOUNTRESPONSE = DESCRIPTOR.message_types_by_name[ "UpdateServiceAccountResponse" ] +_SETSERVICEACCOUNTNAMESPACEACCESSREQUEST = DESCRIPTOR.message_types_by_name[ + "SetServiceAccountNamespaceAccessRequest" +] +_SETSERVICEACCOUNTNAMESPACEACCESSRESPONSE = DESCRIPTOR.message_types_by_name[ + "SetServiceAccountNamespaceAccessResponse" +] _DELETESERVICEACCOUNTREQUEST = DESCRIPTOR.message_types_by_name[ "DeleteServiceAccountRequest" ] @@ -284,12 +302,114 @@ _DELETECONNECTIVITYRULERESPONSE = DESCRIPTOR.message_types_by_name[ "DeleteConnectivityRuleResponse" ] +_GETAUDITLOGSREQUEST = DESCRIPTOR.message_types_by_name["GetAuditLogsRequest"] +_GETAUDITLOGSRESPONSE = DESCRIPTOR.message_types_by_name["GetAuditLogsResponse"] +_VALIDATEACCOUNTAUDITLOGSINKREQUEST = DESCRIPTOR.message_types_by_name[ + "ValidateAccountAuditLogSinkRequest" +] +_VALIDATEACCOUNTAUDITLOGSINKRESPONSE = DESCRIPTOR.message_types_by_name[ + "ValidateAccountAuditLogSinkResponse" +] +_CREATEACCOUNTAUDITLOGSINKREQUEST = DESCRIPTOR.message_types_by_name[ + "CreateAccountAuditLogSinkRequest" +] +_CREATEACCOUNTAUDITLOGSINKRESPONSE = DESCRIPTOR.message_types_by_name[ + "CreateAccountAuditLogSinkResponse" +] +_GETACCOUNTAUDITLOGSINKREQUEST = DESCRIPTOR.message_types_by_name[ + "GetAccountAuditLogSinkRequest" +] +_GETACCOUNTAUDITLOGSINKRESPONSE = DESCRIPTOR.message_types_by_name[ + "GetAccountAuditLogSinkResponse" +] +_GETACCOUNTAUDITLOGSINKSREQUEST = DESCRIPTOR.message_types_by_name[ + "GetAccountAuditLogSinksRequest" +] +_GETACCOUNTAUDITLOGSINKSRESPONSE = DESCRIPTOR.message_types_by_name[ + "GetAccountAuditLogSinksResponse" +] +_UPDATEACCOUNTAUDITLOGSINKREQUEST = DESCRIPTOR.message_types_by_name[ + "UpdateAccountAuditLogSinkRequest" +] +_UPDATEACCOUNTAUDITLOGSINKRESPONSE = DESCRIPTOR.message_types_by_name[ + "UpdateAccountAuditLogSinkResponse" +] +_DELETEACCOUNTAUDITLOGSINKREQUEST = DESCRIPTOR.message_types_by_name[ + "DeleteAccountAuditLogSinkRequest" +] +_DELETEACCOUNTAUDITLOGSINKRESPONSE = DESCRIPTOR.message_types_by_name[ + "DeleteAccountAuditLogSinkResponse" +] +_GETNAMESPACECAPACITYINFOREQUEST = DESCRIPTOR.message_types_by_name[ + "GetNamespaceCapacityInfoRequest" +] +_GETNAMESPACECAPACITYINFORESPONSE = DESCRIPTOR.message_types_by_name[ + "GetNamespaceCapacityInfoResponse" +] +_CREATEBILLINGREPORTREQUEST = DESCRIPTOR.message_types_by_name[ + "CreateBillingReportRequest" +] +_CREATEBILLINGREPORTRESPONSE = DESCRIPTOR.message_types_by_name[ + "CreateBillingReportResponse" +] +_GETBILLINGREPORTREQUEST = DESCRIPTOR.message_types_by_name["GetBillingReportRequest"] +_GETBILLINGREPORTRESPONSE = DESCRIPTOR.message_types_by_name["GetBillingReportResponse"] +_GETCUSTOMROLESREQUEST = DESCRIPTOR.message_types_by_name["GetCustomRolesRequest"] +_GETCUSTOMROLESRESPONSE = DESCRIPTOR.message_types_by_name["GetCustomRolesResponse"] +_GETCUSTOMROLEREQUEST = DESCRIPTOR.message_types_by_name["GetCustomRoleRequest"] +_GETCUSTOMROLERESPONSE = DESCRIPTOR.message_types_by_name["GetCustomRoleResponse"] +_CREATECUSTOMROLEREQUEST = DESCRIPTOR.message_types_by_name["CreateCustomRoleRequest"] +_CREATECUSTOMROLERESPONSE = DESCRIPTOR.message_types_by_name["CreateCustomRoleResponse"] +_UPDATECUSTOMROLEREQUEST = DESCRIPTOR.message_types_by_name["UpdateCustomRoleRequest"] +_UPDATECUSTOMROLERESPONSE = DESCRIPTOR.message_types_by_name["UpdateCustomRoleResponse"] +_DELETECUSTOMROLEREQUEST = DESCRIPTOR.message_types_by_name["DeleteCustomRoleRequest"] +_DELETECUSTOMROLERESPONSE = DESCRIPTOR.message_types_by_name["DeleteCustomRoleResponse"] +_GETUSERNAMESPACEASSIGNMENTSREQUEST = DESCRIPTOR.message_types_by_name[ + "GetUserNamespaceAssignmentsRequest" +] +_GETUSERNAMESPACEASSIGNMENTSRESPONSE = DESCRIPTOR.message_types_by_name[ + "GetUserNamespaceAssignmentsResponse" +] +_GETSERVICEACCOUNTNAMESPACEASSIGNMENTSREQUEST = DESCRIPTOR.message_types_by_name[ + "GetServiceAccountNamespaceAssignmentsRequest" +] +_GETSERVICEACCOUNTNAMESPACEASSIGNMENTSRESPONSE = DESCRIPTOR.message_types_by_name[ + "GetServiceAccountNamespaceAssignmentsResponse" +] +_GETUSERGROUPNAMESPACEASSIGNMENTSREQUEST = DESCRIPTOR.message_types_by_name[ + "GetUserGroupNamespaceAssignmentsRequest" +] +_GETUSERGROUPNAMESPACEASSIGNMENTSRESPONSE = DESCRIPTOR.message_types_by_name[ + "GetUserGroupNamespaceAssignmentsResponse" +] +GetCurrentIdentityRequest = _reflection.GeneratedProtocolMessageType( + "GetCurrentIdentityRequest", + (_message.Message,), + { + "DESCRIPTOR": _GETCURRENTIDENTITYREQUEST, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetCurrentIdentityRequest) + }, +) +_sym_db.RegisterMessage(GetCurrentIdentityRequest) + +GetCurrentIdentityResponse = _reflection.GeneratedProtocolMessageType( + "GetCurrentIdentityResponse", + (_message.Message,), + { + "DESCRIPTOR": _GETCURRENTIDENTITYRESPONSE, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetCurrentIdentityResponse) + }, +) +_sym_db.RegisterMessage(GetCurrentIdentityResponse) + GetUsersRequest = _reflection.GeneratedProtocolMessageType( "GetUsersRequest", (_message.Message,), { "DESCRIPTOR": _GETUSERSREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetUsersRequest) }, ) @@ -300,7 +420,7 @@ (_message.Message,), { "DESCRIPTOR": _GETUSERSRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetUsersResponse) }, ) @@ -311,7 +431,7 @@ (_message.Message,), { "DESCRIPTOR": _GETUSERREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetUserRequest) }, ) @@ -322,7 +442,7 @@ (_message.Message,), { "DESCRIPTOR": _GETUSERRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetUserResponse) }, ) @@ -333,7 +453,7 @@ (_message.Message,), { "DESCRIPTOR": _CREATEUSERREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.CreateUserRequest) }, ) @@ -344,7 +464,7 @@ (_message.Message,), { "DESCRIPTOR": _CREATEUSERRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.CreateUserResponse) }, ) @@ -355,7 +475,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATEUSERREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.UpdateUserRequest) }, ) @@ -366,7 +486,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATEUSERRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.UpdateUserResponse) }, ) @@ -377,7 +497,7 @@ (_message.Message,), { "DESCRIPTOR": _DELETEUSERREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.DeleteUserRequest) }, ) @@ -388,7 +508,7 @@ (_message.Message,), { "DESCRIPTOR": _DELETEUSERRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.DeleteUserResponse) }, ) @@ -399,7 +519,7 @@ (_message.Message,), { "DESCRIPTOR": _SETUSERNAMESPACEACCESSREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.SetUserNamespaceAccessRequest) }, ) @@ -410,7 +530,7 @@ (_message.Message,), { "DESCRIPTOR": _SETUSERNAMESPACEACCESSRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.SetUserNamespaceAccessResponse) }, ) @@ -421,7 +541,7 @@ (_message.Message,), { "DESCRIPTOR": _GETASYNCOPERATIONREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetAsyncOperationRequest) }, ) @@ -432,7 +552,7 @@ (_message.Message,), { "DESCRIPTOR": _GETASYNCOPERATIONRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetAsyncOperationResponse) }, ) @@ -447,12 +567,12 @@ (_message.Message,), { "DESCRIPTOR": _CREATENAMESPACEREQUEST_TAGSENTRY, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.CreateNamespaceRequest.TagsEntry) }, ), "DESCRIPTOR": _CREATENAMESPACEREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.CreateNamespaceRequest) }, ) @@ -464,7 +584,7 @@ (_message.Message,), { "DESCRIPTOR": _CREATENAMESPACERESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.CreateNamespaceResponse) }, ) @@ -475,7 +595,7 @@ (_message.Message,), { "DESCRIPTOR": _GETNAMESPACESREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetNamespacesRequest) }, ) @@ -486,7 +606,7 @@ (_message.Message,), { "DESCRIPTOR": _GETNAMESPACESRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetNamespacesResponse) }, ) @@ -497,7 +617,7 @@ (_message.Message,), { "DESCRIPTOR": _GETNAMESPACEREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetNamespaceRequest) }, ) @@ -508,7 +628,7 @@ (_message.Message,), { "DESCRIPTOR": _GETNAMESPACERESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetNamespaceResponse) }, ) @@ -519,7 +639,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATENAMESPACEREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.UpdateNamespaceRequest) }, ) @@ -530,7 +650,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATENAMESPACERESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.UpdateNamespaceResponse) }, ) @@ -541,7 +661,7 @@ (_message.Message,), { "DESCRIPTOR": _RENAMECUSTOMSEARCHATTRIBUTEREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.RenameCustomSearchAttributeRequest) }, ) @@ -552,7 +672,7 @@ (_message.Message,), { "DESCRIPTOR": _RENAMECUSTOMSEARCHATTRIBUTERESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.RenameCustomSearchAttributeResponse) }, ) @@ -563,7 +683,7 @@ (_message.Message,), { "DESCRIPTOR": _DELETENAMESPACEREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.DeleteNamespaceRequest) }, ) @@ -574,7 +694,7 @@ (_message.Message,), { "DESCRIPTOR": _DELETENAMESPACERESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.DeleteNamespaceResponse) }, ) @@ -585,7 +705,7 @@ (_message.Message,), { "DESCRIPTOR": _FAILOVERNAMESPACEREGIONREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.FailoverNamespaceRegionRequest) }, ) @@ -596,7 +716,7 @@ (_message.Message,), { "DESCRIPTOR": _FAILOVERNAMESPACEREGIONRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.FailoverNamespaceRegionResponse) }, ) @@ -607,7 +727,7 @@ (_message.Message,), { "DESCRIPTOR": _ADDNAMESPACEREGIONREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.AddNamespaceRegionRequest) }, ) @@ -618,7 +738,7 @@ (_message.Message,), { "DESCRIPTOR": _ADDNAMESPACEREGIONRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.AddNamespaceRegionResponse) }, ) @@ -629,7 +749,7 @@ (_message.Message,), { "DESCRIPTOR": _DELETENAMESPACEREGIONREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.DeleteNamespaceRegionRequest) }, ) @@ -640,7 +760,7 @@ (_message.Message,), { "DESCRIPTOR": _DELETENAMESPACEREGIONRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.DeleteNamespaceRegionResponse) }, ) @@ -651,7 +771,7 @@ (_message.Message,), { "DESCRIPTOR": _GETREGIONSREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetRegionsRequest) }, ) @@ -662,7 +782,7 @@ (_message.Message,), { "DESCRIPTOR": _GETREGIONSRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetRegionsResponse) }, ) @@ -673,7 +793,7 @@ (_message.Message,), { "DESCRIPTOR": _GETREGIONREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetRegionRequest) }, ) @@ -684,7 +804,7 @@ (_message.Message,), { "DESCRIPTOR": _GETREGIONRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetRegionResponse) }, ) @@ -695,7 +815,7 @@ (_message.Message,), { "DESCRIPTOR": _GETAPIKEYSREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetApiKeysRequest) }, ) @@ -706,7 +826,7 @@ (_message.Message,), { "DESCRIPTOR": _GETAPIKEYSRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetApiKeysResponse) }, ) @@ -717,7 +837,7 @@ (_message.Message,), { "DESCRIPTOR": _GETAPIKEYREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetApiKeyRequest) }, ) @@ -728,7 +848,7 @@ (_message.Message,), { "DESCRIPTOR": _GETAPIKEYRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetApiKeyResponse) }, ) @@ -739,7 +859,7 @@ (_message.Message,), { "DESCRIPTOR": _CREATEAPIKEYREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.CreateApiKeyRequest) }, ) @@ -750,7 +870,7 @@ (_message.Message,), { "DESCRIPTOR": _CREATEAPIKEYRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.CreateApiKeyResponse) }, ) @@ -761,7 +881,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATEAPIKEYREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.UpdateApiKeyRequest) }, ) @@ -772,7 +892,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATEAPIKEYRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.UpdateApiKeyResponse) }, ) @@ -783,7 +903,7 @@ (_message.Message,), { "DESCRIPTOR": _DELETEAPIKEYREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.DeleteApiKeyRequest) }, ) @@ -794,7 +914,7 @@ (_message.Message,), { "DESCRIPTOR": _DELETEAPIKEYRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.DeleteApiKeyResponse) }, ) @@ -805,7 +925,7 @@ (_message.Message,), { "DESCRIPTOR": _GETNEXUSENDPOINTSREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetNexusEndpointsRequest) }, ) @@ -816,7 +936,7 @@ (_message.Message,), { "DESCRIPTOR": _GETNEXUSENDPOINTSRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetNexusEndpointsResponse) }, ) @@ -827,7 +947,7 @@ (_message.Message,), { "DESCRIPTOR": _GETNEXUSENDPOINTREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetNexusEndpointRequest) }, ) @@ -838,7 +958,7 @@ (_message.Message,), { "DESCRIPTOR": _GETNEXUSENDPOINTRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetNexusEndpointResponse) }, ) @@ -849,7 +969,7 @@ (_message.Message,), { "DESCRIPTOR": _CREATENEXUSENDPOINTREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.CreateNexusEndpointRequest) }, ) @@ -860,7 +980,7 @@ (_message.Message,), { "DESCRIPTOR": _CREATENEXUSENDPOINTRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.CreateNexusEndpointResponse) }, ) @@ -871,7 +991,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATENEXUSENDPOINTREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.UpdateNexusEndpointRequest) }, ) @@ -882,7 +1002,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATENEXUSENDPOINTRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.UpdateNexusEndpointResponse) }, ) @@ -893,7 +1013,7 @@ (_message.Message,), { "DESCRIPTOR": _DELETENEXUSENDPOINTREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.DeleteNexusEndpointRequest) }, ) @@ -904,7 +1024,7 @@ (_message.Message,), { "DESCRIPTOR": _DELETENEXUSENDPOINTRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.DeleteNexusEndpointResponse) }, ) @@ -919,7 +1039,7 @@ (_message.Message,), { "DESCRIPTOR": _GETUSERGROUPSREQUEST_GOOGLEGROUPFILTER, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetUserGroupsRequest.GoogleGroupFilter) }, ), @@ -928,12 +1048,12 @@ (_message.Message,), { "DESCRIPTOR": _GETUSERGROUPSREQUEST_SCIMGROUPFILTER, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetUserGroupsRequest.SCIMGroupFilter) }, ), "DESCRIPTOR": _GETUSERGROUPSREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetUserGroupsRequest) }, ) @@ -946,7 +1066,7 @@ (_message.Message,), { "DESCRIPTOR": _GETUSERGROUPSRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetUserGroupsResponse) }, ) @@ -957,7 +1077,7 @@ (_message.Message,), { "DESCRIPTOR": _GETUSERGROUPREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetUserGroupRequest) }, ) @@ -968,7 +1088,7 @@ (_message.Message,), { "DESCRIPTOR": _GETUSERGROUPRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetUserGroupResponse) }, ) @@ -979,7 +1099,7 @@ (_message.Message,), { "DESCRIPTOR": _CREATEUSERGROUPREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.CreateUserGroupRequest) }, ) @@ -990,7 +1110,7 @@ (_message.Message,), { "DESCRIPTOR": _CREATEUSERGROUPRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.CreateUserGroupResponse) }, ) @@ -1001,7 +1121,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATEUSERGROUPREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.UpdateUserGroupRequest) }, ) @@ -1012,7 +1132,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATEUSERGROUPRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.UpdateUserGroupResponse) }, ) @@ -1023,7 +1143,7 @@ (_message.Message,), { "DESCRIPTOR": _DELETEUSERGROUPREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.DeleteUserGroupRequest) }, ) @@ -1034,7 +1154,7 @@ (_message.Message,), { "DESCRIPTOR": _DELETEUSERGROUPRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.DeleteUserGroupResponse) }, ) @@ -1045,7 +1165,7 @@ (_message.Message,), { "DESCRIPTOR": _SETUSERGROUPNAMESPACEACCESSREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.SetUserGroupNamespaceAccessRequest) }, ) @@ -1056,7 +1176,7 @@ (_message.Message,), { "DESCRIPTOR": _SETUSERGROUPNAMESPACEACCESSRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.SetUserGroupNamespaceAccessResponse) }, ) @@ -1067,7 +1187,7 @@ (_message.Message,), { "DESCRIPTOR": _ADDUSERGROUPMEMBERREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.AddUserGroupMemberRequest) }, ) @@ -1078,7 +1198,7 @@ (_message.Message,), { "DESCRIPTOR": _ADDUSERGROUPMEMBERRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.AddUserGroupMemberResponse) }, ) @@ -1089,7 +1209,7 @@ (_message.Message,), { "DESCRIPTOR": _REMOVEUSERGROUPMEMBERREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.RemoveUserGroupMemberRequest) }, ) @@ -1100,7 +1220,7 @@ (_message.Message,), { "DESCRIPTOR": _REMOVEUSERGROUPMEMBERRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.RemoveUserGroupMemberResponse) }, ) @@ -1111,7 +1231,7 @@ (_message.Message,), { "DESCRIPTOR": _GETUSERGROUPMEMBERSREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetUserGroupMembersRequest) }, ) @@ -1122,7 +1242,7 @@ (_message.Message,), { "DESCRIPTOR": _GETUSERGROUPMEMBERSRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetUserGroupMembersResponse) }, ) @@ -1133,7 +1253,7 @@ (_message.Message,), { "DESCRIPTOR": _CREATESERVICEACCOUNTREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.CreateServiceAccountRequest) }, ) @@ -1144,7 +1264,7 @@ (_message.Message,), { "DESCRIPTOR": _CREATESERVICEACCOUNTRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.CreateServiceAccountResponse) }, ) @@ -1155,7 +1275,7 @@ (_message.Message,), { "DESCRIPTOR": _GETSERVICEACCOUNTREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetServiceAccountRequest) }, ) @@ -1166,7 +1286,7 @@ (_message.Message,), { "DESCRIPTOR": _GETSERVICEACCOUNTRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetServiceAccountResponse) }, ) @@ -1177,7 +1297,7 @@ (_message.Message,), { "DESCRIPTOR": _GETSERVICEACCOUNTSREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetServiceAccountsRequest) }, ) @@ -1188,7 +1308,7 @@ (_message.Message,), { "DESCRIPTOR": _GETSERVICEACCOUNTSRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetServiceAccountsResponse) }, ) @@ -1199,7 +1319,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATESERVICEACCOUNTREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.UpdateServiceAccountRequest) }, ) @@ -1210,18 +1330,40 @@ (_message.Message,), { "DESCRIPTOR": _UPDATESERVICEACCOUNTRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.UpdateServiceAccountResponse) }, ) _sym_db.RegisterMessage(UpdateServiceAccountResponse) +SetServiceAccountNamespaceAccessRequest = _reflection.GeneratedProtocolMessageType( + "SetServiceAccountNamespaceAccessRequest", + (_message.Message,), + { + "DESCRIPTOR": _SETSERVICEACCOUNTNAMESPACEACCESSREQUEST, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.SetServiceAccountNamespaceAccessRequest) + }, +) +_sym_db.RegisterMessage(SetServiceAccountNamespaceAccessRequest) + +SetServiceAccountNamespaceAccessResponse = _reflection.GeneratedProtocolMessageType( + "SetServiceAccountNamespaceAccessResponse", + (_message.Message,), + { + "DESCRIPTOR": _SETSERVICEACCOUNTNAMESPACEACCESSRESPONSE, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.SetServiceAccountNamespaceAccessResponse) + }, +) +_sym_db.RegisterMessage(SetServiceAccountNamespaceAccessResponse) + DeleteServiceAccountRequest = _reflection.GeneratedProtocolMessageType( "DeleteServiceAccountRequest", (_message.Message,), { "DESCRIPTOR": _DELETESERVICEACCOUNTREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.DeleteServiceAccountRequest) }, ) @@ -1232,7 +1374,7 @@ (_message.Message,), { "DESCRIPTOR": _DELETESERVICEACCOUNTRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.DeleteServiceAccountResponse) }, ) @@ -1243,7 +1385,7 @@ (_message.Message,), { "DESCRIPTOR": _GETUSAGEREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetUsageRequest) }, ) @@ -1254,7 +1396,7 @@ (_message.Message,), { "DESCRIPTOR": _GETUSAGERESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetUsageResponse) }, ) @@ -1265,7 +1407,7 @@ (_message.Message,), { "DESCRIPTOR": _GETACCOUNTREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetAccountRequest) }, ) @@ -1276,7 +1418,7 @@ (_message.Message,), { "DESCRIPTOR": _GETACCOUNTRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetAccountResponse) }, ) @@ -1287,7 +1429,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATEACCOUNTREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.UpdateAccountRequest) }, ) @@ -1298,7 +1440,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATEACCOUNTRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.UpdateAccountResponse) }, ) @@ -1309,7 +1451,7 @@ (_message.Message,), { "DESCRIPTOR": _CREATENAMESPACEEXPORTSINKREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.CreateNamespaceExportSinkRequest) }, ) @@ -1320,7 +1462,7 @@ (_message.Message,), { "DESCRIPTOR": _CREATENAMESPACEEXPORTSINKRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.CreateNamespaceExportSinkResponse) }, ) @@ -1331,7 +1473,7 @@ (_message.Message,), { "DESCRIPTOR": _GETNAMESPACEEXPORTSINKREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetNamespaceExportSinkRequest) }, ) @@ -1342,7 +1484,7 @@ (_message.Message,), { "DESCRIPTOR": _GETNAMESPACEEXPORTSINKRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetNamespaceExportSinkResponse) }, ) @@ -1353,7 +1495,7 @@ (_message.Message,), { "DESCRIPTOR": _GETNAMESPACEEXPORTSINKSREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetNamespaceExportSinksRequest) }, ) @@ -1364,7 +1506,7 @@ (_message.Message,), { "DESCRIPTOR": _GETNAMESPACEEXPORTSINKSRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetNamespaceExportSinksResponse) }, ) @@ -1375,7 +1517,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATENAMESPACEEXPORTSINKREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.UpdateNamespaceExportSinkRequest) }, ) @@ -1386,7 +1528,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATENAMESPACEEXPORTSINKRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.UpdateNamespaceExportSinkResponse) }, ) @@ -1397,7 +1539,7 @@ (_message.Message,), { "DESCRIPTOR": _DELETENAMESPACEEXPORTSINKREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.DeleteNamespaceExportSinkRequest) }, ) @@ -1408,7 +1550,7 @@ (_message.Message,), { "DESCRIPTOR": _DELETENAMESPACEEXPORTSINKRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.DeleteNamespaceExportSinkResponse) }, ) @@ -1419,7 +1561,7 @@ (_message.Message,), { "DESCRIPTOR": _VALIDATENAMESPACEEXPORTSINKREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.ValidateNamespaceExportSinkRequest) }, ) @@ -1430,7 +1572,7 @@ (_message.Message,), { "DESCRIPTOR": _VALIDATENAMESPACEEXPORTSINKRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.ValidateNamespaceExportSinkResponse) }, ) @@ -1445,12 +1587,12 @@ (_message.Message,), { "DESCRIPTOR": _UPDATENAMESPACETAGSREQUEST_TAGSTOUPSERTENTRY, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.UpdateNamespaceTagsRequest.TagsToUpsertEntry) }, ), "DESCRIPTOR": _UPDATENAMESPACETAGSREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.UpdateNamespaceTagsRequest) }, ) @@ -1462,7 +1604,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATENAMESPACETAGSRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.UpdateNamespaceTagsResponse) }, ) @@ -1473,7 +1615,7 @@ (_message.Message,), { "DESCRIPTOR": _CREATECONNECTIVITYRULEREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.CreateConnectivityRuleRequest) }, ) @@ -1484,7 +1626,7 @@ (_message.Message,), { "DESCRIPTOR": _CREATECONNECTIVITYRULERESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.CreateConnectivityRuleResponse) }, ) @@ -1495,7 +1637,7 @@ (_message.Message,), { "DESCRIPTOR": _GETCONNECTIVITYRULEREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetConnectivityRuleRequest) }, ) @@ -1506,7 +1648,7 @@ (_message.Message,), { "DESCRIPTOR": _GETCONNECTIVITYRULERESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetConnectivityRuleResponse) }, ) @@ -1517,7 +1659,7 @@ (_message.Message,), { "DESCRIPTOR": _GETCONNECTIVITYRULESREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetConnectivityRulesRequest) }, ) @@ -1528,7 +1670,7 @@ (_message.Message,), { "DESCRIPTOR": _GETCONNECTIVITYRULESRESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetConnectivityRulesResponse) }, ) @@ -1539,7 +1681,7 @@ (_message.Message,), { "DESCRIPTOR": _DELETECONNECTIVITYRULEREQUEST, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.DeleteConnectivityRuleRequest) }, ) @@ -1550,12 +1692,410 @@ (_message.Message,), { "DESCRIPTOR": _DELETECONNECTIVITYRULERESPONSE, - "__module__": "temporal.api.cloud.cloudservice.v1.request_response_pb2", + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.DeleteConnectivityRuleResponse) }, ) _sym_db.RegisterMessage(DeleteConnectivityRuleResponse) +GetAuditLogsRequest = _reflection.GeneratedProtocolMessageType( + "GetAuditLogsRequest", + (_message.Message,), + { + "DESCRIPTOR": _GETAUDITLOGSREQUEST, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetAuditLogsRequest) + }, +) +_sym_db.RegisterMessage(GetAuditLogsRequest) + +GetAuditLogsResponse = _reflection.GeneratedProtocolMessageType( + "GetAuditLogsResponse", + (_message.Message,), + { + "DESCRIPTOR": _GETAUDITLOGSRESPONSE, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetAuditLogsResponse) + }, +) +_sym_db.RegisterMessage(GetAuditLogsResponse) + +ValidateAccountAuditLogSinkRequest = _reflection.GeneratedProtocolMessageType( + "ValidateAccountAuditLogSinkRequest", + (_message.Message,), + { + "DESCRIPTOR": _VALIDATEACCOUNTAUDITLOGSINKREQUEST, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.ValidateAccountAuditLogSinkRequest) + }, +) +_sym_db.RegisterMessage(ValidateAccountAuditLogSinkRequest) + +ValidateAccountAuditLogSinkResponse = _reflection.GeneratedProtocolMessageType( + "ValidateAccountAuditLogSinkResponse", + (_message.Message,), + { + "DESCRIPTOR": _VALIDATEACCOUNTAUDITLOGSINKRESPONSE, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.ValidateAccountAuditLogSinkResponse) + }, +) +_sym_db.RegisterMessage(ValidateAccountAuditLogSinkResponse) + +CreateAccountAuditLogSinkRequest = _reflection.GeneratedProtocolMessageType( + "CreateAccountAuditLogSinkRequest", + (_message.Message,), + { + "DESCRIPTOR": _CREATEACCOUNTAUDITLOGSINKREQUEST, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.CreateAccountAuditLogSinkRequest) + }, +) +_sym_db.RegisterMessage(CreateAccountAuditLogSinkRequest) + +CreateAccountAuditLogSinkResponse = _reflection.GeneratedProtocolMessageType( + "CreateAccountAuditLogSinkResponse", + (_message.Message,), + { + "DESCRIPTOR": _CREATEACCOUNTAUDITLOGSINKRESPONSE, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.CreateAccountAuditLogSinkResponse) + }, +) +_sym_db.RegisterMessage(CreateAccountAuditLogSinkResponse) + +GetAccountAuditLogSinkRequest = _reflection.GeneratedProtocolMessageType( + "GetAccountAuditLogSinkRequest", + (_message.Message,), + { + "DESCRIPTOR": _GETACCOUNTAUDITLOGSINKREQUEST, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetAccountAuditLogSinkRequest) + }, +) +_sym_db.RegisterMessage(GetAccountAuditLogSinkRequest) + +GetAccountAuditLogSinkResponse = _reflection.GeneratedProtocolMessageType( + "GetAccountAuditLogSinkResponse", + (_message.Message,), + { + "DESCRIPTOR": _GETACCOUNTAUDITLOGSINKRESPONSE, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetAccountAuditLogSinkResponse) + }, +) +_sym_db.RegisterMessage(GetAccountAuditLogSinkResponse) + +GetAccountAuditLogSinksRequest = _reflection.GeneratedProtocolMessageType( + "GetAccountAuditLogSinksRequest", + (_message.Message,), + { + "DESCRIPTOR": _GETACCOUNTAUDITLOGSINKSREQUEST, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetAccountAuditLogSinksRequest) + }, +) +_sym_db.RegisterMessage(GetAccountAuditLogSinksRequest) + +GetAccountAuditLogSinksResponse = _reflection.GeneratedProtocolMessageType( + "GetAccountAuditLogSinksResponse", + (_message.Message,), + { + "DESCRIPTOR": _GETACCOUNTAUDITLOGSINKSRESPONSE, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetAccountAuditLogSinksResponse) + }, +) +_sym_db.RegisterMessage(GetAccountAuditLogSinksResponse) + +UpdateAccountAuditLogSinkRequest = _reflection.GeneratedProtocolMessageType( + "UpdateAccountAuditLogSinkRequest", + (_message.Message,), + { + "DESCRIPTOR": _UPDATEACCOUNTAUDITLOGSINKREQUEST, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.UpdateAccountAuditLogSinkRequest) + }, +) +_sym_db.RegisterMessage(UpdateAccountAuditLogSinkRequest) + +UpdateAccountAuditLogSinkResponse = _reflection.GeneratedProtocolMessageType( + "UpdateAccountAuditLogSinkResponse", + (_message.Message,), + { + "DESCRIPTOR": _UPDATEACCOUNTAUDITLOGSINKRESPONSE, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.UpdateAccountAuditLogSinkResponse) + }, +) +_sym_db.RegisterMessage(UpdateAccountAuditLogSinkResponse) + +DeleteAccountAuditLogSinkRequest = _reflection.GeneratedProtocolMessageType( + "DeleteAccountAuditLogSinkRequest", + (_message.Message,), + { + "DESCRIPTOR": _DELETEACCOUNTAUDITLOGSINKREQUEST, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.DeleteAccountAuditLogSinkRequest) + }, +) +_sym_db.RegisterMessage(DeleteAccountAuditLogSinkRequest) + +DeleteAccountAuditLogSinkResponse = _reflection.GeneratedProtocolMessageType( + "DeleteAccountAuditLogSinkResponse", + (_message.Message,), + { + "DESCRIPTOR": _DELETEACCOUNTAUDITLOGSINKRESPONSE, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.DeleteAccountAuditLogSinkResponse) + }, +) +_sym_db.RegisterMessage(DeleteAccountAuditLogSinkResponse) + +GetNamespaceCapacityInfoRequest = _reflection.GeneratedProtocolMessageType( + "GetNamespaceCapacityInfoRequest", + (_message.Message,), + { + "DESCRIPTOR": _GETNAMESPACECAPACITYINFOREQUEST, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetNamespaceCapacityInfoRequest) + }, +) +_sym_db.RegisterMessage(GetNamespaceCapacityInfoRequest) + +GetNamespaceCapacityInfoResponse = _reflection.GeneratedProtocolMessageType( + "GetNamespaceCapacityInfoResponse", + (_message.Message,), + { + "DESCRIPTOR": _GETNAMESPACECAPACITYINFORESPONSE, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetNamespaceCapacityInfoResponse) + }, +) +_sym_db.RegisterMessage(GetNamespaceCapacityInfoResponse) + +CreateBillingReportRequest = _reflection.GeneratedProtocolMessageType( + "CreateBillingReportRequest", + (_message.Message,), + { + "DESCRIPTOR": _CREATEBILLINGREPORTREQUEST, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.CreateBillingReportRequest) + }, +) +_sym_db.RegisterMessage(CreateBillingReportRequest) + +CreateBillingReportResponse = _reflection.GeneratedProtocolMessageType( + "CreateBillingReportResponse", + (_message.Message,), + { + "DESCRIPTOR": _CREATEBILLINGREPORTRESPONSE, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.CreateBillingReportResponse) + }, +) +_sym_db.RegisterMessage(CreateBillingReportResponse) + +GetBillingReportRequest = _reflection.GeneratedProtocolMessageType( + "GetBillingReportRequest", + (_message.Message,), + { + "DESCRIPTOR": _GETBILLINGREPORTREQUEST, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetBillingReportRequest) + }, +) +_sym_db.RegisterMessage(GetBillingReportRequest) + +GetBillingReportResponse = _reflection.GeneratedProtocolMessageType( + "GetBillingReportResponse", + (_message.Message,), + { + "DESCRIPTOR": _GETBILLINGREPORTRESPONSE, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetBillingReportResponse) + }, +) +_sym_db.RegisterMessage(GetBillingReportResponse) + +GetCustomRolesRequest = _reflection.GeneratedProtocolMessageType( + "GetCustomRolesRequest", + (_message.Message,), + { + "DESCRIPTOR": _GETCUSTOMROLESREQUEST, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetCustomRolesRequest) + }, +) +_sym_db.RegisterMessage(GetCustomRolesRequest) + +GetCustomRolesResponse = _reflection.GeneratedProtocolMessageType( + "GetCustomRolesResponse", + (_message.Message,), + { + "DESCRIPTOR": _GETCUSTOMROLESRESPONSE, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetCustomRolesResponse) + }, +) +_sym_db.RegisterMessage(GetCustomRolesResponse) + +GetCustomRoleRequest = _reflection.GeneratedProtocolMessageType( + "GetCustomRoleRequest", + (_message.Message,), + { + "DESCRIPTOR": _GETCUSTOMROLEREQUEST, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetCustomRoleRequest) + }, +) +_sym_db.RegisterMessage(GetCustomRoleRequest) + +GetCustomRoleResponse = _reflection.GeneratedProtocolMessageType( + "GetCustomRoleResponse", + (_message.Message,), + { + "DESCRIPTOR": _GETCUSTOMROLERESPONSE, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetCustomRoleResponse) + }, +) +_sym_db.RegisterMessage(GetCustomRoleResponse) + +CreateCustomRoleRequest = _reflection.GeneratedProtocolMessageType( + "CreateCustomRoleRequest", + (_message.Message,), + { + "DESCRIPTOR": _CREATECUSTOMROLEREQUEST, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.CreateCustomRoleRequest) + }, +) +_sym_db.RegisterMessage(CreateCustomRoleRequest) + +CreateCustomRoleResponse = _reflection.GeneratedProtocolMessageType( + "CreateCustomRoleResponse", + (_message.Message,), + { + "DESCRIPTOR": _CREATECUSTOMROLERESPONSE, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.CreateCustomRoleResponse) + }, +) +_sym_db.RegisterMessage(CreateCustomRoleResponse) + +UpdateCustomRoleRequest = _reflection.GeneratedProtocolMessageType( + "UpdateCustomRoleRequest", + (_message.Message,), + { + "DESCRIPTOR": _UPDATECUSTOMROLEREQUEST, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.UpdateCustomRoleRequest) + }, +) +_sym_db.RegisterMessage(UpdateCustomRoleRequest) + +UpdateCustomRoleResponse = _reflection.GeneratedProtocolMessageType( + "UpdateCustomRoleResponse", + (_message.Message,), + { + "DESCRIPTOR": _UPDATECUSTOMROLERESPONSE, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.UpdateCustomRoleResponse) + }, +) +_sym_db.RegisterMessage(UpdateCustomRoleResponse) + +DeleteCustomRoleRequest = _reflection.GeneratedProtocolMessageType( + "DeleteCustomRoleRequest", + (_message.Message,), + { + "DESCRIPTOR": _DELETECUSTOMROLEREQUEST, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.DeleteCustomRoleRequest) + }, +) +_sym_db.RegisterMessage(DeleteCustomRoleRequest) + +DeleteCustomRoleResponse = _reflection.GeneratedProtocolMessageType( + "DeleteCustomRoleResponse", + (_message.Message,), + { + "DESCRIPTOR": _DELETECUSTOMROLERESPONSE, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.DeleteCustomRoleResponse) + }, +) +_sym_db.RegisterMessage(DeleteCustomRoleResponse) + +GetUserNamespaceAssignmentsRequest = _reflection.GeneratedProtocolMessageType( + "GetUserNamespaceAssignmentsRequest", + (_message.Message,), + { + "DESCRIPTOR": _GETUSERNAMESPACEASSIGNMENTSREQUEST, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetUserNamespaceAssignmentsRequest) + }, +) +_sym_db.RegisterMessage(GetUserNamespaceAssignmentsRequest) + +GetUserNamespaceAssignmentsResponse = _reflection.GeneratedProtocolMessageType( + "GetUserNamespaceAssignmentsResponse", + (_message.Message,), + { + "DESCRIPTOR": _GETUSERNAMESPACEASSIGNMENTSRESPONSE, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetUserNamespaceAssignmentsResponse) + }, +) +_sym_db.RegisterMessage(GetUserNamespaceAssignmentsResponse) + +GetServiceAccountNamespaceAssignmentsRequest = _reflection.GeneratedProtocolMessageType( + "GetServiceAccountNamespaceAssignmentsRequest", + (_message.Message,), + { + "DESCRIPTOR": _GETSERVICEACCOUNTNAMESPACEASSIGNMENTSREQUEST, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetServiceAccountNamespaceAssignmentsRequest) + }, +) +_sym_db.RegisterMessage(GetServiceAccountNamespaceAssignmentsRequest) + +GetServiceAccountNamespaceAssignmentsResponse = ( + _reflection.GeneratedProtocolMessageType( + "GetServiceAccountNamespaceAssignmentsResponse", + (_message.Message,), + { + "DESCRIPTOR": _GETSERVICEACCOUNTNAMESPACEASSIGNMENTSRESPONSE, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetServiceAccountNamespaceAssignmentsResponse) + }, + ) +) +_sym_db.RegisterMessage(GetServiceAccountNamespaceAssignmentsResponse) + +GetUserGroupNamespaceAssignmentsRequest = _reflection.GeneratedProtocolMessageType( + "GetUserGroupNamespaceAssignmentsRequest", + (_message.Message,), + { + "DESCRIPTOR": _GETUSERGROUPNAMESPACEASSIGNMENTSREQUEST, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetUserGroupNamespaceAssignmentsRequest) + }, +) +_sym_db.RegisterMessage(GetUserGroupNamespaceAssignmentsRequest) + +GetUserGroupNamespaceAssignmentsResponse = _reflection.GeneratedProtocolMessageType( + "GetUserGroupNamespaceAssignmentsResponse", + (_message.Message,), + { + "DESCRIPTOR": _GETUSERGROUPNAMESPACEASSIGNMENTSRESPONSE, + "__module__": "temporalio.api.cloud.cloudservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.cloudservice.v1.GetUserGroupNamespaceAssignmentsResponse) + }, +) +_sym_db.RegisterMessage(GetUserGroupNamespaceAssignmentsResponse) + if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b"\n%io.temporal.api.cloud.cloudservice.v1B\024RequestResponseProtoP\001Z5go.temporal.io/api/cloud/cloudservice/v1;cloudservice\252\002$Temporalio.Api.Cloud.CloudService.V1\352\002(Temporalio::Api::Cloud::CloudService::V1" @@ -1567,236 +2107,316 @@ ]._serialized_options = b"\030\001" _UPDATENAMESPACETAGSREQUEST_TAGSTOUPSERTENTRY._options = None _UPDATENAMESPACETAGSREQUEST_TAGSTOUPSERTENTRY._serialized_options = b"8\001" - _GETUSERSREQUEST._serialized_start = 499 - _GETUSERSREQUEST._serialized_end = 589 - _GETUSERSRESPONSE._serialized_start = 591 - _GETUSERSRESPONSE._serialized_end = 687 - _GETUSERREQUEST._serialized_start = 689 - _GETUSERREQUEST._serialized_end = 722 - _GETUSERRESPONSE._serialized_start = 724 - _GETUSERRESPONSE._serialized_end = 793 - _CREATEUSERREQUEST._serialized_start = 795 - _CREATEUSERREQUEST._serialized_end = 898 - _CREATEUSERRESPONSE._serialized_start = 900 - _CREATEUSERRESPONSE._serialized_end = 1011 - _UPDATEUSERREQUEST._serialized_start = 1014 - _UPDATEUSERREQUEST._serialized_end = 1160 - _UPDATEUSERRESPONSE._serialized_start = 1162 - _UPDATEUSERRESPONSE._serialized_end = 1256 - _DELETEUSERREQUEST._serialized_start = 1258 - _DELETEUSERREQUEST._serialized_end = 1348 - _DELETEUSERRESPONSE._serialized_start = 1350 - _DELETEUSERRESPONSE._serialized_end = 1444 - _SETUSERNAMESPACEACCESSREQUEST._serialized_start = 1447 - _SETUSERNAMESPACEACCESSREQUEST._serialized_end = 1633 - _SETUSERNAMESPACEACCESSRESPONSE._serialized_start = 1635 - _SETUSERNAMESPACEACCESSRESPONSE._serialized_end = 1741 - _GETASYNCOPERATIONREQUEST._serialized_start = 1743 - _GETASYNCOPERATIONREQUEST._serialized_end = 1797 - _GETASYNCOPERATIONRESPONSE._serialized_start = 1799 - _GETASYNCOPERATIONRESPONSE._serialized_end = 1900 - _CREATENAMESPACEREQUEST._serialized_start = 1903 - _CREATENAMESPACEREQUEST._serialized_end = 2146 - _CREATENAMESPACEREQUEST_TAGSENTRY._serialized_start = 2103 - _CREATENAMESPACEREQUEST_TAGSENTRY._serialized_end = 2146 - _CREATENAMESPACERESPONSE._serialized_start = 2148 - _CREATENAMESPACERESPONSE._serialized_end = 2266 - _GETNAMESPACESREQUEST._serialized_start = 2268 - _GETNAMESPACESREQUEST._serialized_end = 2343 - _GETNAMESPACESRESPONSE._serialized_start = 2345 - _GETNAMESPACESRESPONSE._serialized_end = 2457 - _GETNAMESPACEREQUEST._serialized_start = 2459 - _GETNAMESPACEREQUEST._serialized_end = 2499 - _GETNAMESPACERESPONSE._serialized_start = 2501 - _GETNAMESPACERESPONSE._serialized_end = 2586 - _UPDATENAMESPACEREQUEST._serialized_start = 2589 - _UPDATENAMESPACEREQUEST._serialized_end = 2748 - _UPDATENAMESPACERESPONSE._serialized_start = 2750 - _UPDATENAMESPACERESPONSE._serialized_end = 2849 - _RENAMECUSTOMSEARCHATTRIBUTEREQUEST._serialized_start = 2852 - _RENAMECUSTOMSEARCHATTRIBUTEREQUEST._serialized_end = 3050 - _RENAMECUSTOMSEARCHATTRIBUTERESPONSE._serialized_start = 3052 - _RENAMECUSTOMSEARCHATTRIBUTERESPONSE._serialized_end = 3163 - _DELETENAMESPACEREQUEST._serialized_start = 3165 - _DELETENAMESPACEREQUEST._serialized_end = 3262 - _DELETENAMESPACERESPONSE._serialized_start = 3264 - _DELETENAMESPACERESPONSE._serialized_end = 3363 - _FAILOVERNAMESPACEREGIONREQUEST._serialized_start = 3365 - _FAILOVERNAMESPACEREGIONREQUEST._serialized_end = 3460 - _FAILOVERNAMESPACEREGIONRESPONSE._serialized_start = 3462 - _FAILOVERNAMESPACEREGIONRESPONSE._serialized_end = 3569 - _ADDNAMESPACEREGIONREQUEST._serialized_start = 3571 - _ADDNAMESPACEREGIONREQUEST._serialized_end = 3687 - _ADDNAMESPACEREGIONRESPONSE._serialized_start = 3689 - _ADDNAMESPACEREGIONRESPONSE._serialized_end = 3791 - _DELETENAMESPACEREGIONREQUEST._serialized_start = 3793 - _DELETENAMESPACEREGIONREQUEST._serialized_end = 3912 - _DELETENAMESPACEREGIONRESPONSE._serialized_start = 3914 - _DELETENAMESPACEREGIONRESPONSE._serialized_end = 4019 - _GETREGIONSREQUEST._serialized_start = 4021 - _GETREGIONSREQUEST._serialized_end = 4040 - _GETREGIONSRESPONSE._serialized_start = 4042 - _GETREGIONSRESPONSE._serialized_end = 4117 - _GETREGIONREQUEST._serialized_start = 4119 - _GETREGIONREQUEST._serialized_end = 4153 - _GETREGIONRESPONSE._serialized_start = 4155 - _GETREGIONRESPONSE._serialized_end = 4228 - _GETAPIKEYSREQUEST._serialized_start = 4231 - _GETAPIKEYSREQUEST._serialized_end = 4405 - _GETAPIKEYSRESPONSE._serialized_start = 4407 - _GETAPIKEYSRESPONSE._serialized_end = 4510 - _GETAPIKEYREQUEST._serialized_start = 4512 - _GETAPIKEYREQUEST._serialized_end = 4546 - _GETAPIKEYRESPONSE._serialized_start = 4548 - _GETAPIKEYRESPONSE._serialized_end = 4624 - _CREATEAPIKEYREQUEST._serialized_start = 4626 - _CREATEAPIKEYREQUEST._serialized_end = 4733 - _CREATEAPIKEYRESPONSE._serialized_start = 4735 - _CREATEAPIKEYRESPONSE._serialized_end = 4862 - _UPDATEAPIKEYREQUEST._serialized_start = 4865 - _UPDATEAPIKEYREQUEST._serialized_end = 5014 - _UPDATEAPIKEYRESPONSE._serialized_start = 5016 - _UPDATEAPIKEYRESPONSE._serialized_end = 5112 - _DELETEAPIKEYREQUEST._serialized_start = 5114 - _DELETEAPIKEYREQUEST._serialized_end = 5205 - _DELETEAPIKEYRESPONSE._serialized_start = 5207 - _DELETEAPIKEYRESPONSE._serialized_end = 5303 - _GETNEXUSENDPOINTSREQUEST._serialized_start = 5306 - _GETNEXUSENDPOINTSREQUEST._serialized_end = 5441 - _GETNEXUSENDPOINTSRESPONSE._serialized_start = 5443 - _GETNEXUSENDPOINTSRESPONSE._serialized_end = 5553 - _GETNEXUSENDPOINTREQUEST._serialized_start = 5555 - _GETNEXUSENDPOINTREQUEST._serialized_end = 5601 - _GETNEXUSENDPOINTRESPONSE._serialized_start = 5603 - _GETNEXUSENDPOINTRESPONSE._serialized_end = 5686 - _CREATENEXUSENDPOINTREQUEST._serialized_start = 5688 - _CREATENEXUSENDPOINTREQUEST._serialized_end = 5801 - _CREATENEXUSENDPOINTRESPONSE._serialized_start = 5803 - _CREATENEXUSENDPOINTRESPONSE._serialized_end = 5927 - _UPDATENEXUSENDPOINTREQUEST._serialized_start = 5930 - _UPDATENEXUSENDPOINTREQUEST._serialized_end = 6090 - _UPDATENEXUSENDPOINTRESPONSE._serialized_start = 6092 - _UPDATENEXUSENDPOINTRESPONSE._serialized_end = 6195 - _DELETENEXUSENDPOINTREQUEST._serialized_start = 6197 - _DELETENEXUSENDPOINTREQUEST._serialized_end = 6300 - _DELETENEXUSENDPOINTRESPONSE._serialized_start = 6302 - _DELETENEXUSENDPOINTRESPONSE._serialized_end = 6405 - _GETUSERGROUPSREQUEST._serialized_start = 6408 - _GETUSERGROUPSREQUEST._serialized_end = 6781 - _GETUSERGROUPSREQUEST_GOOGLEGROUPFILTER._serialized_start = 6704 - _GETUSERGROUPSREQUEST_GOOGLEGROUPFILTER._serialized_end = 6746 - _GETUSERGROUPSREQUEST_SCIMGROUPFILTER._serialized_start = 6748 - _GETUSERGROUPSREQUEST_SCIMGROUPFILTER._serialized_end = 6781 - _GETUSERGROUPSRESPONSE._serialized_start = 6783 - _GETUSERGROUPSRESPONSE._serialized_end = 6890 - _GETUSERGROUPREQUEST._serialized_start = 6892 - _GETUSERGROUPREQUEST._serialized_end = 6931 - _GETUSERGROUPRESPONSE._serialized_start = 6933 - _GETUSERGROUPRESPONSE._serialized_end = 7013 - _CREATEUSERGROUPREQUEST._serialized_start = 7015 - _CREATEUSERGROUPREQUEST._serialized_end = 7128 - _CREATEUSERGROUPRESPONSE._serialized_start = 7130 - _CREATEUSERGROUPRESPONSE._serialized_end = 7247 - _UPDATEUSERGROUPREQUEST._serialized_start = 7250 - _UPDATEUSERGROUPREQUEST._serialized_end = 7407 - _UPDATEUSERGROUPRESPONSE._serialized_start = 7409 - _UPDATEUSERGROUPRESPONSE._serialized_end = 7508 - _DELETEUSERGROUPREQUEST._serialized_start = 7510 - _DELETEUSERGROUPREQUEST._serialized_end = 7606 - _DELETEUSERGROUPRESPONSE._serialized_start = 7608 - _DELETEUSERGROUPRESPONSE._serialized_end = 7707 - _SETUSERGROUPNAMESPACEACCESSREQUEST._serialized_start = 7710 - _SETUSERGROUPNAMESPACEACCESSREQUEST._serialized_end = 7902 - _SETUSERGROUPNAMESPACEACCESSRESPONSE._serialized_start = 7904 - _SETUSERGROUPNAMESPACEACCESSRESPONSE._serialized_end = 8015 - _ADDUSERGROUPMEMBERREQUEST._serialized_start = 8018 - _ADDUSERGROUPMEMBERREQUEST._serialized_end = 8161 - _ADDUSERGROUPMEMBERRESPONSE._serialized_start = 8163 - _ADDUSERGROUPMEMBERRESPONSE._serialized_end = 8265 - _REMOVEUSERGROUPMEMBERREQUEST._serialized_start = 8268 - _REMOVEUSERGROUPMEMBERREQUEST._serialized_end = 8414 - _REMOVEUSERGROUPMEMBERRESPONSE._serialized_start = 8416 - _REMOVEUSERGROUPMEMBERRESPONSE._serialized_end = 8521 - _GETUSERGROUPMEMBERSREQUEST._serialized_start = 8523 - _GETUSERGROUPMEMBERSREQUEST._serialized_end = 8608 - _GETUSERGROUPMEMBERSRESPONSE._serialized_start = 8610 - _GETUSERGROUPMEMBERSRESPONSE._serialized_end = 8730 - _CREATESERVICEACCOUNTREQUEST._serialized_start = 8732 - _CREATESERVICEACCOUNTREQUEST._serialized_end = 8855 - _CREATESERVICEACCOUNTRESPONSE._serialized_start = 8858 - _CREATESERVICEACCOUNTRESPONSE._serialized_end = 8990 - _GETSERVICEACCOUNTREQUEST._serialized_start = 8992 - _GETSERVICEACCOUNTREQUEST._serialized_end = 9046 - _GETSERVICEACCOUNTRESPONSE._serialized_start = 9048 - _GETSERVICEACCOUNTRESPONSE._serialized_end = 9148 - _GETSERVICEACCOUNTSREQUEST._serialized_start = 9150 - _GETSERVICEACCOUNTSREQUEST._serialized_end = 9216 - _GETSERVICEACCOUNTSRESPONSE._serialized_start = 9218 - _GETSERVICEACCOUNTSRESPONSE._serialized_end = 9344 - _UPDATESERVICEACCOUNTREQUEST._serialized_start = 9347 - _UPDATESERVICEACCOUNTREQUEST._serialized_end = 9524 - _UPDATESERVICEACCOUNTRESPONSE._serialized_start = 9526 - _UPDATESERVICEACCOUNTRESPONSE._serialized_end = 9630 - _DELETESERVICEACCOUNTREQUEST._serialized_start = 9632 - _DELETESERVICEACCOUNTREQUEST._serialized_end = 9743 - _DELETESERVICEACCOUNTRESPONSE._serialized_start = 9745 - _DELETESERVICEACCOUNTRESPONSE._serialized_end = 9849 - _GETUSAGEREQUEST._serialized_start = 9852 - _GETUSAGEREQUEST._serialized_end = 10022 - _GETUSAGERESPONSE._serialized_start = 10024 - _GETUSAGERESPONSE._serialized_end = 10124 - _GETACCOUNTREQUEST._serialized_start = 10126 - _GETACCOUNTREQUEST._serialized_end = 10145 - _GETACCOUNTRESPONSE._serialized_start = 10147 - _GETACCOUNTRESPONSE._serialized_end = 10224 - _UPDATEACCOUNTREQUEST._serialized_start = 10227 - _UPDATEACCOUNTREQUEST._serialized_end = 10361 - _UPDATEACCOUNTRESPONSE._serialized_start = 10363 - _UPDATEACCOUNTRESPONSE._serialized_end = 10460 - _CREATENAMESPACEEXPORTSINKREQUEST._serialized_start = 10463 - _CREATENAMESPACEEXPORTSINKREQUEST._serialized_end = 10607 - _CREATENAMESPACEEXPORTSINKRESPONSE._serialized_start = 10609 - _CREATENAMESPACEEXPORTSINKRESPONSE._serialized_end = 10718 - _GETNAMESPACEEXPORTSINKREQUEST._serialized_start = 10720 - _GETNAMESPACEEXPORTSINKREQUEST._serialized_end = 10784 - _GETNAMESPACEEXPORTSINKRESPONSE._serialized_start = 10786 - _GETNAMESPACEEXPORTSINKRESPONSE._serialized_end = 10877 - _GETNAMESPACEEXPORTSINKSREQUEST._serialized_start = 10879 - _GETNAMESPACEEXPORTSINKSREQUEST._serialized_end = 10969 - _GETNAMESPACEEXPORTSINKSRESPONSE._serialized_start = 10971 - _GETNAMESPACEEXPORTSINKSRESPONSE._serialized_end = 11089 - _UPDATENAMESPACEEXPORTSINKREQUEST._serialized_start = 11092 - _UPDATENAMESPACEEXPORTSINKREQUEST._serialized_end = 11262 - _UPDATENAMESPACEEXPORTSINKRESPONSE._serialized_start = 11264 - _UPDATENAMESPACEEXPORTSINKRESPONSE._serialized_end = 11373 - _DELETENAMESPACEEXPORTSINKREQUEST._serialized_start = 11375 - _DELETENAMESPACEEXPORTSINKREQUEST._serialized_end = 11496 - _DELETENAMESPACEEXPORTSINKRESPONSE._serialized_start = 11498 - _DELETENAMESPACEEXPORTSINKRESPONSE._serialized_end = 11607 - _VALIDATENAMESPACEEXPORTSINKREQUEST._serialized_start = 11609 - _VALIDATENAMESPACEEXPORTSINKREQUEST._serialized_end = 11727 - _VALIDATENAMESPACEEXPORTSINKRESPONSE._serialized_start = 11729 - _VALIDATENAMESPACEEXPORTSINKRESPONSE._serialized_end = 11766 - _UPDATENAMESPACETAGSREQUEST._serialized_start = 11769 - _UPDATENAMESPACETAGSREQUEST._serialized_end = 12027 - _UPDATENAMESPACETAGSREQUEST_TAGSTOUPSERTENTRY._serialized_start = 11976 - _UPDATENAMESPACETAGSREQUEST_TAGSTOUPSERTENTRY._serialized_end = 12027 - _UPDATENAMESPACETAGSRESPONSE._serialized_start = 12029 - _UPDATENAMESPACETAGSRESPONSE._serialized_end = 12132 - _CREATECONNECTIVITYRULEREQUEST._serialized_start = 12135 - _CREATECONNECTIVITYRULEREQUEST._serialized_end = 12270 - _CREATECONNECTIVITYRULERESPONSE._serialized_start = 12273 - _CREATECONNECTIVITYRULERESPONSE._serialized_end = 12409 - _GETCONNECTIVITYRULEREQUEST._serialized_start = 12411 - _GETCONNECTIVITYRULEREQUEST._serialized_end = 12469 - _GETCONNECTIVITYRULERESPONSE._serialized_start = 12471 - _GETCONNECTIVITYRULERESPONSE._serialized_end = 12585 - _GETCONNECTIVITYRULESREQUEST._serialized_start = 12587 - _GETCONNECTIVITYRULESREQUEST._serialized_end = 12674 - _GETCONNECTIVITYRULESRESPONSE._serialized_start = 12677 - _GETCONNECTIVITYRULESRESPONSE._serialized_end = 12818 - _DELETECONNECTIVITYRULEREQUEST._serialized_start = 12820 - _DELETECONNECTIVITYRULEREQUEST._serialized_end = 12935 - _DELETECONNECTIVITYRULERESPONSE._serialized_start = 12937 - _DELETECONNECTIVITYRULERESPONSE._serialized_end = 13043 + _GETCURRENTIDENTITYREQUEST._serialized_start = 590 + _GETCURRENTIDENTITYREQUEST._serialized_end = 617 + _GETCURRENTIDENTITYRESPONSE._serialized_start = 620 + _GETCURRENTIDENTITYRESPONSE._serialized_end = 857 + _GETUSERSREQUEST._serialized_start = 859 + _GETUSERSREQUEST._serialized_end = 949 + _GETUSERSRESPONSE._serialized_start = 951 + _GETUSERSRESPONSE._serialized_end = 1047 + _GETUSERREQUEST._serialized_start = 1049 + _GETUSERREQUEST._serialized_end = 1082 + _GETUSERRESPONSE._serialized_start = 1084 + _GETUSERRESPONSE._serialized_end = 1153 + _CREATEUSERREQUEST._serialized_start = 1155 + _CREATEUSERREQUEST._serialized_end = 1258 + _CREATEUSERRESPONSE._serialized_start = 1260 + _CREATEUSERRESPONSE._serialized_end = 1371 + _UPDATEUSERREQUEST._serialized_start = 1374 + _UPDATEUSERREQUEST._serialized_end = 1520 + _UPDATEUSERRESPONSE._serialized_start = 1522 + _UPDATEUSERRESPONSE._serialized_end = 1616 + _DELETEUSERREQUEST._serialized_start = 1618 + _DELETEUSERREQUEST._serialized_end = 1708 + _DELETEUSERRESPONSE._serialized_start = 1710 + _DELETEUSERRESPONSE._serialized_end = 1804 + _SETUSERNAMESPACEACCESSREQUEST._serialized_start = 1807 + _SETUSERNAMESPACEACCESSREQUEST._serialized_end = 1993 + _SETUSERNAMESPACEACCESSRESPONSE._serialized_start = 1995 + _SETUSERNAMESPACEACCESSRESPONSE._serialized_end = 2101 + _GETASYNCOPERATIONREQUEST._serialized_start = 2103 + _GETASYNCOPERATIONREQUEST._serialized_end = 2157 + _GETASYNCOPERATIONRESPONSE._serialized_start = 2159 + _GETASYNCOPERATIONRESPONSE._serialized_end = 2260 + _CREATENAMESPACEREQUEST._serialized_start = 2263 + _CREATENAMESPACEREQUEST._serialized_end = 2506 + _CREATENAMESPACEREQUEST_TAGSENTRY._serialized_start = 2463 + _CREATENAMESPACEREQUEST_TAGSENTRY._serialized_end = 2506 + _CREATENAMESPACERESPONSE._serialized_start = 2508 + _CREATENAMESPACERESPONSE._serialized_end = 2626 + _GETNAMESPACESREQUEST._serialized_start = 2628 + _GETNAMESPACESREQUEST._serialized_end = 2703 + _GETNAMESPACESRESPONSE._serialized_start = 2705 + _GETNAMESPACESRESPONSE._serialized_end = 2817 + _GETNAMESPACEREQUEST._serialized_start = 2819 + _GETNAMESPACEREQUEST._serialized_end = 2859 + _GETNAMESPACERESPONSE._serialized_start = 2861 + _GETNAMESPACERESPONSE._serialized_end = 2946 + _UPDATENAMESPACEREQUEST._serialized_start = 2949 + _UPDATENAMESPACEREQUEST._serialized_end = 3108 + _UPDATENAMESPACERESPONSE._serialized_start = 3110 + _UPDATENAMESPACERESPONSE._serialized_end = 3209 + _RENAMECUSTOMSEARCHATTRIBUTEREQUEST._serialized_start = 3212 + _RENAMECUSTOMSEARCHATTRIBUTEREQUEST._serialized_end = 3410 + _RENAMECUSTOMSEARCHATTRIBUTERESPONSE._serialized_start = 3412 + _RENAMECUSTOMSEARCHATTRIBUTERESPONSE._serialized_end = 3523 + _DELETENAMESPACEREQUEST._serialized_start = 3525 + _DELETENAMESPACEREQUEST._serialized_end = 3622 + _DELETENAMESPACERESPONSE._serialized_start = 3624 + _DELETENAMESPACERESPONSE._serialized_end = 3723 + _FAILOVERNAMESPACEREGIONREQUEST._serialized_start = 3725 + _FAILOVERNAMESPACEREGIONREQUEST._serialized_end = 3820 + _FAILOVERNAMESPACEREGIONRESPONSE._serialized_start = 3822 + _FAILOVERNAMESPACEREGIONRESPONSE._serialized_end = 3929 + _ADDNAMESPACEREGIONREQUEST._serialized_start = 3931 + _ADDNAMESPACEREGIONREQUEST._serialized_end = 4047 + _ADDNAMESPACEREGIONRESPONSE._serialized_start = 4049 + _ADDNAMESPACEREGIONRESPONSE._serialized_end = 4151 + _DELETENAMESPACEREGIONREQUEST._serialized_start = 4153 + _DELETENAMESPACEREGIONREQUEST._serialized_end = 4272 + _DELETENAMESPACEREGIONRESPONSE._serialized_start = 4274 + _DELETENAMESPACEREGIONRESPONSE._serialized_end = 4379 + _GETREGIONSREQUEST._serialized_start = 4381 + _GETREGIONSREQUEST._serialized_end = 4400 + _GETREGIONSRESPONSE._serialized_start = 4402 + _GETREGIONSRESPONSE._serialized_end = 4477 + _GETREGIONREQUEST._serialized_start = 4479 + _GETREGIONREQUEST._serialized_end = 4513 + _GETREGIONRESPONSE._serialized_start = 4515 + _GETREGIONRESPONSE._serialized_end = 4588 + _GETAPIKEYSREQUEST._serialized_start = 4591 + _GETAPIKEYSREQUEST._serialized_end = 4765 + _GETAPIKEYSRESPONSE._serialized_start = 4767 + _GETAPIKEYSRESPONSE._serialized_end = 4870 + _GETAPIKEYREQUEST._serialized_start = 4872 + _GETAPIKEYREQUEST._serialized_end = 4906 + _GETAPIKEYRESPONSE._serialized_start = 4908 + _GETAPIKEYRESPONSE._serialized_end = 4984 + _CREATEAPIKEYREQUEST._serialized_start = 4986 + _CREATEAPIKEYREQUEST._serialized_end = 5093 + _CREATEAPIKEYRESPONSE._serialized_start = 5095 + _CREATEAPIKEYRESPONSE._serialized_end = 5222 + _UPDATEAPIKEYREQUEST._serialized_start = 5225 + _UPDATEAPIKEYREQUEST._serialized_end = 5374 + _UPDATEAPIKEYRESPONSE._serialized_start = 5376 + _UPDATEAPIKEYRESPONSE._serialized_end = 5472 + _DELETEAPIKEYREQUEST._serialized_start = 5474 + _DELETEAPIKEYREQUEST._serialized_end = 5565 + _DELETEAPIKEYRESPONSE._serialized_start = 5567 + _DELETEAPIKEYRESPONSE._serialized_end = 5663 + _GETNEXUSENDPOINTSREQUEST._serialized_start = 5666 + _GETNEXUSENDPOINTSREQUEST._serialized_end = 5801 + _GETNEXUSENDPOINTSRESPONSE._serialized_start = 5803 + _GETNEXUSENDPOINTSRESPONSE._serialized_end = 5913 + _GETNEXUSENDPOINTREQUEST._serialized_start = 5915 + _GETNEXUSENDPOINTREQUEST._serialized_end = 5961 + _GETNEXUSENDPOINTRESPONSE._serialized_start = 5963 + _GETNEXUSENDPOINTRESPONSE._serialized_end = 6046 + _CREATENEXUSENDPOINTREQUEST._serialized_start = 6048 + _CREATENEXUSENDPOINTREQUEST._serialized_end = 6161 + _CREATENEXUSENDPOINTRESPONSE._serialized_start = 6163 + _CREATENEXUSENDPOINTRESPONSE._serialized_end = 6287 + _UPDATENEXUSENDPOINTREQUEST._serialized_start = 6290 + _UPDATENEXUSENDPOINTREQUEST._serialized_end = 6450 + _UPDATENEXUSENDPOINTRESPONSE._serialized_start = 6452 + _UPDATENEXUSENDPOINTRESPONSE._serialized_end = 6555 + _DELETENEXUSENDPOINTREQUEST._serialized_start = 6557 + _DELETENEXUSENDPOINTREQUEST._serialized_end = 6660 + _DELETENEXUSENDPOINTRESPONSE._serialized_start = 6662 + _DELETENEXUSENDPOINTRESPONSE._serialized_end = 6765 + _GETUSERGROUPSREQUEST._serialized_start = 6768 + _GETUSERGROUPSREQUEST._serialized_end = 7141 + _GETUSERGROUPSREQUEST_GOOGLEGROUPFILTER._serialized_start = 7064 + _GETUSERGROUPSREQUEST_GOOGLEGROUPFILTER._serialized_end = 7106 + _GETUSERGROUPSREQUEST_SCIMGROUPFILTER._serialized_start = 7108 + _GETUSERGROUPSREQUEST_SCIMGROUPFILTER._serialized_end = 7141 + _GETUSERGROUPSRESPONSE._serialized_start = 7143 + _GETUSERGROUPSRESPONSE._serialized_end = 7250 + _GETUSERGROUPREQUEST._serialized_start = 7252 + _GETUSERGROUPREQUEST._serialized_end = 7291 + _GETUSERGROUPRESPONSE._serialized_start = 7293 + _GETUSERGROUPRESPONSE._serialized_end = 7373 + _CREATEUSERGROUPREQUEST._serialized_start = 7375 + _CREATEUSERGROUPREQUEST._serialized_end = 7488 + _CREATEUSERGROUPRESPONSE._serialized_start = 7490 + _CREATEUSERGROUPRESPONSE._serialized_end = 7607 + _UPDATEUSERGROUPREQUEST._serialized_start = 7610 + _UPDATEUSERGROUPREQUEST._serialized_end = 7767 + _UPDATEUSERGROUPRESPONSE._serialized_start = 7769 + _UPDATEUSERGROUPRESPONSE._serialized_end = 7868 + _DELETEUSERGROUPREQUEST._serialized_start = 7870 + _DELETEUSERGROUPREQUEST._serialized_end = 7966 + _DELETEUSERGROUPRESPONSE._serialized_start = 7968 + _DELETEUSERGROUPRESPONSE._serialized_end = 8067 + _SETUSERGROUPNAMESPACEACCESSREQUEST._serialized_start = 8070 + _SETUSERGROUPNAMESPACEACCESSREQUEST._serialized_end = 8262 + _SETUSERGROUPNAMESPACEACCESSRESPONSE._serialized_start = 8264 + _SETUSERGROUPNAMESPACEACCESSRESPONSE._serialized_end = 8375 + _ADDUSERGROUPMEMBERREQUEST._serialized_start = 8378 + _ADDUSERGROUPMEMBERREQUEST._serialized_end = 8521 + _ADDUSERGROUPMEMBERRESPONSE._serialized_start = 8523 + _ADDUSERGROUPMEMBERRESPONSE._serialized_end = 8625 + _REMOVEUSERGROUPMEMBERREQUEST._serialized_start = 8628 + _REMOVEUSERGROUPMEMBERREQUEST._serialized_end = 8774 + _REMOVEUSERGROUPMEMBERRESPONSE._serialized_start = 8776 + _REMOVEUSERGROUPMEMBERRESPONSE._serialized_end = 8881 + _GETUSERGROUPMEMBERSREQUEST._serialized_start = 8883 + _GETUSERGROUPMEMBERSREQUEST._serialized_end = 8968 + _GETUSERGROUPMEMBERSRESPONSE._serialized_start = 8970 + _GETUSERGROUPMEMBERSRESPONSE._serialized_end = 9090 + _CREATESERVICEACCOUNTREQUEST._serialized_start = 9092 + _CREATESERVICEACCOUNTREQUEST._serialized_end = 9215 + _CREATESERVICEACCOUNTRESPONSE._serialized_start = 9218 + _CREATESERVICEACCOUNTRESPONSE._serialized_end = 9350 + _GETSERVICEACCOUNTREQUEST._serialized_start = 9352 + _GETSERVICEACCOUNTREQUEST._serialized_end = 9406 + _GETSERVICEACCOUNTRESPONSE._serialized_start = 9408 + _GETSERVICEACCOUNTRESPONSE._serialized_end = 9508 + _GETSERVICEACCOUNTSREQUEST._serialized_start = 9510 + _GETSERVICEACCOUNTSREQUEST._serialized_end = 9576 + _GETSERVICEACCOUNTSRESPONSE._serialized_start = 9578 + _GETSERVICEACCOUNTSRESPONSE._serialized_end = 9704 + _UPDATESERVICEACCOUNTREQUEST._serialized_start = 9707 + _UPDATESERVICEACCOUNTREQUEST._serialized_end = 9884 + _UPDATESERVICEACCOUNTRESPONSE._serialized_start = 9886 + _UPDATESERVICEACCOUNTRESPONSE._serialized_end = 9990 + _SETSERVICEACCOUNTNAMESPACEACCESSREQUEST._serialized_start = 9993 + _SETSERVICEACCOUNTNAMESPACEACCESSREQUEST._serialized_end = 10200 + _SETSERVICEACCOUNTNAMESPACEACCESSRESPONSE._serialized_start = 10202 + _SETSERVICEACCOUNTNAMESPACEACCESSRESPONSE._serialized_end = 10318 + _DELETESERVICEACCOUNTREQUEST._serialized_start = 10320 + _DELETESERVICEACCOUNTREQUEST._serialized_end = 10431 + _DELETESERVICEACCOUNTRESPONSE._serialized_start = 10433 + _DELETESERVICEACCOUNTRESPONSE._serialized_end = 10537 + _GETUSAGEREQUEST._serialized_start = 10540 + _GETUSAGEREQUEST._serialized_end = 10710 + _GETUSAGERESPONSE._serialized_start = 10712 + _GETUSAGERESPONSE._serialized_end = 10812 + _GETACCOUNTREQUEST._serialized_start = 10814 + _GETACCOUNTREQUEST._serialized_end = 10833 + _GETACCOUNTRESPONSE._serialized_start = 10835 + _GETACCOUNTRESPONSE._serialized_end = 10912 + _UPDATEACCOUNTREQUEST._serialized_start = 10915 + _UPDATEACCOUNTREQUEST._serialized_end = 11049 + _UPDATEACCOUNTRESPONSE._serialized_start = 11051 + _UPDATEACCOUNTRESPONSE._serialized_end = 11148 + _CREATENAMESPACEEXPORTSINKREQUEST._serialized_start = 11151 + _CREATENAMESPACEEXPORTSINKREQUEST._serialized_end = 11295 + _CREATENAMESPACEEXPORTSINKRESPONSE._serialized_start = 11297 + _CREATENAMESPACEEXPORTSINKRESPONSE._serialized_end = 11406 + _GETNAMESPACEEXPORTSINKREQUEST._serialized_start = 11408 + _GETNAMESPACEEXPORTSINKREQUEST._serialized_end = 11472 + _GETNAMESPACEEXPORTSINKRESPONSE._serialized_start = 11474 + _GETNAMESPACEEXPORTSINKRESPONSE._serialized_end = 11565 + _GETNAMESPACEEXPORTSINKSREQUEST._serialized_start = 11567 + _GETNAMESPACEEXPORTSINKSREQUEST._serialized_end = 11657 + _GETNAMESPACEEXPORTSINKSRESPONSE._serialized_start = 11659 + _GETNAMESPACEEXPORTSINKSRESPONSE._serialized_end = 11777 + _UPDATENAMESPACEEXPORTSINKREQUEST._serialized_start = 11780 + _UPDATENAMESPACEEXPORTSINKREQUEST._serialized_end = 11950 + _UPDATENAMESPACEEXPORTSINKRESPONSE._serialized_start = 11952 + _UPDATENAMESPACEEXPORTSINKRESPONSE._serialized_end = 12061 + _DELETENAMESPACEEXPORTSINKREQUEST._serialized_start = 12063 + _DELETENAMESPACEEXPORTSINKREQUEST._serialized_end = 12184 + _DELETENAMESPACEEXPORTSINKRESPONSE._serialized_start = 12186 + _DELETENAMESPACEEXPORTSINKRESPONSE._serialized_end = 12295 + _VALIDATENAMESPACEEXPORTSINKREQUEST._serialized_start = 12297 + _VALIDATENAMESPACEEXPORTSINKREQUEST._serialized_end = 12415 + _VALIDATENAMESPACEEXPORTSINKRESPONSE._serialized_start = 12417 + _VALIDATENAMESPACEEXPORTSINKRESPONSE._serialized_end = 12454 + _UPDATENAMESPACETAGSREQUEST._serialized_start = 12457 + _UPDATENAMESPACETAGSREQUEST._serialized_end = 12715 + _UPDATENAMESPACETAGSREQUEST_TAGSTOUPSERTENTRY._serialized_start = 12664 + _UPDATENAMESPACETAGSREQUEST_TAGSTOUPSERTENTRY._serialized_end = 12715 + _UPDATENAMESPACETAGSRESPONSE._serialized_start = 12717 + _UPDATENAMESPACETAGSRESPONSE._serialized_end = 12820 + _CREATECONNECTIVITYRULEREQUEST._serialized_start = 12823 + _CREATECONNECTIVITYRULEREQUEST._serialized_end = 12958 + _CREATECONNECTIVITYRULERESPONSE._serialized_start = 12961 + _CREATECONNECTIVITYRULERESPONSE._serialized_end = 13097 + _GETCONNECTIVITYRULEREQUEST._serialized_start = 13099 + _GETCONNECTIVITYRULEREQUEST._serialized_end = 13157 + _GETCONNECTIVITYRULERESPONSE._serialized_start = 13159 + _GETCONNECTIVITYRULERESPONSE._serialized_end = 13273 + _GETCONNECTIVITYRULESREQUEST._serialized_start = 13275 + _GETCONNECTIVITYRULESREQUEST._serialized_end = 13362 + _GETCONNECTIVITYRULESRESPONSE._serialized_start = 13365 + _GETCONNECTIVITYRULESRESPONSE._serialized_end = 13506 + _DELETECONNECTIVITYRULEREQUEST._serialized_start = 13508 + _DELETECONNECTIVITYRULEREQUEST._serialized_end = 13623 + _DELETECONNECTIVITYRULERESPONSE._serialized_start = 13625 + _DELETECONNECTIVITYRULERESPONSE._serialized_end = 13731 + _GETAUDITLOGSREQUEST._serialized_start = 13734 + _GETAUDITLOGSREQUEST._serialized_end = 13908 + _GETAUDITLOGSRESPONSE._serialized_start = 13910 + _GETAUDITLOGSRESPONSE._serialized_end = 14014 + _VALIDATEACCOUNTAUDITLOGSINKREQUEST._serialized_start = 14016 + _VALIDATEACCOUNTAUDITLOGSINKREQUEST._serialized_end = 14115 + _VALIDATEACCOUNTAUDITLOGSINKRESPONSE._serialized_start = 14117 + _VALIDATEACCOUNTAUDITLOGSINKRESPONSE._serialized_end = 14154 + _CREATEACCOUNTAUDITLOGSINKREQUEST._serialized_start = 14156 + _CREATEACCOUNTAUDITLOGSINKREQUEST._serialized_end = 14281 + _CREATEACCOUNTAUDITLOGSINKRESPONSE._serialized_start = 14283 + _CREATEACCOUNTAUDITLOGSINKRESPONSE._serialized_end = 14392 + _GETACCOUNTAUDITLOGSINKREQUEST._serialized_start = 14394 + _GETACCOUNTAUDITLOGSINKREQUEST._serialized_end = 14439 + _GETACCOUNTAUDITLOGSINKRESPONSE._serialized_start = 14441 + _GETACCOUNTAUDITLOGSINKRESPONSE._serialized_end = 14532 + _GETACCOUNTAUDITLOGSINKSREQUEST._serialized_start = 14534 + _GETACCOUNTAUDITLOGSINKSREQUEST._serialized_end = 14605 + _GETACCOUNTAUDITLOGSINKSRESPONSE._serialized_start = 14607 + _GETACCOUNTAUDITLOGSINKSRESPONSE._serialized_end = 14725 + _UPDATEACCOUNTAUDITLOGSINKREQUEST._serialized_start = 14728 + _UPDATEACCOUNTAUDITLOGSINKREQUEST._serialized_end = 14879 + _UPDATEACCOUNTAUDITLOGSINKRESPONSE._serialized_start = 14881 + _UPDATEACCOUNTAUDITLOGSINKRESPONSE._serialized_end = 14990 + _DELETEACCOUNTAUDITLOGSINKREQUEST._serialized_start = 14992 + _DELETEACCOUNTAUDITLOGSINKREQUEST._serialized_end = 15094 + _DELETEACCOUNTAUDITLOGSINKRESPONSE._serialized_start = 15096 + _DELETEACCOUNTAUDITLOGSINKRESPONSE._serialized_end = 15205 + _GETNAMESPACECAPACITYINFOREQUEST._serialized_start = 15207 + _GETNAMESPACECAPACITYINFOREQUEST._serialized_end = 15259 + _GETNAMESPACECAPACITYINFORESPONSE._serialized_start = 15261 + _GETNAMESPACECAPACITYINFORESPONSE._serialized_end = 15374 + _CREATEBILLINGREPORTREQUEST._serialized_start = 15376 + _CREATEBILLINGREPORTREQUEST._serialized_end = 15496 + _CREATEBILLINGREPORTRESPONSE._serialized_start = 15499 + _CREATEBILLINGREPORTRESPONSE._serialized_end = 15629 + _GETBILLINGREPORTREQUEST._serialized_start = 15631 + _GETBILLINGREPORTREQUEST._serialized_end = 15683 + _GETBILLINGREPORTRESPONSE._serialized_start = 15685 + _GETBILLINGREPORTRESPONSE._serialized_end = 15781 + _GETCUSTOMROLESREQUEST._serialized_start = 15783 + _GETCUSTOMROLESREQUEST._serialized_end = 15845 + _GETCUSTOMROLESRESPONSE._serialized_start = 15847 + _GETCUSTOMROLESRESPONSE._serialized_end = 15962 + _GETCUSTOMROLEREQUEST._serialized_start = 15964 + _GETCUSTOMROLEREQUEST._serialized_end = 16003 + _GETCUSTOMROLERESPONSE._serialized_start = 16005 + _GETCUSTOMROLERESPONSE._serialized_end = 16093 + _CREATECUSTOMROLEREQUEST._serialized_start = 16095 + _CREATECUSTOMROLEREQUEST._serialized_end = 16210 + _CREATECUSTOMROLERESPONSE._serialized_start = 16212 + _CREATECUSTOMROLERESPONSE._serialized_end = 16329 + _UPDATECUSTOMROLEREQUEST._serialized_start = 16332 + _UPDATECUSTOMROLEREQUEST._serialized_end = 16490 + _UPDATECUSTOMROLERESPONSE._serialized_start = 16492 + _UPDATECUSTOMROLERESPONSE._serialized_end = 16592 + _DELETECUSTOMROLEREQUEST._serialized_start = 16594 + _DELETECUSTOMROLEREQUEST._serialized_end = 16690 + _DELETECUSTOMROLERESPONSE._serialized_start = 16692 + _DELETECUSTOMROLERESPONSE._serialized_end = 16792 + _GETUSERNAMESPACEASSIGNMENTSREQUEST._serialized_start = 16794 + _GETUSERNAMESPACEASSIGNMENTSREQUEST._serialized_end = 16888 + _GETUSERNAMESPACEASSIGNMENTSRESPONSE._serialized_start = 16891 + _GETUSERNAMESPACEASSIGNMENTSRESPONSE._serialized_end = 17025 + _GETSERVICEACCOUNTNAMESPACEASSIGNMENTSREQUEST._serialized_start = 17027 + _GETSERVICEACCOUNTNAMESPACEASSIGNMENTSREQUEST._serialized_end = 17131 + _GETSERVICEACCOUNTNAMESPACEASSIGNMENTSRESPONSE._serialized_start = 17134 + _GETSERVICEACCOUNTNAMESPACEASSIGNMENTSRESPONSE._serialized_end = 17299 + _GETUSERGROUPNAMESPACEASSIGNMENTSREQUEST._serialized_start = 17301 + _GETUSERGROUPNAMESPACEASSIGNMENTSREQUEST._serialized_end = 17400 + _GETUSERGROUPNAMESPACEASSIGNMENTSRESPONSE._serialized_start = 17403 + _GETUSERGROUPNAMESPACEASSIGNMENTSRESPONSE._serialized_end = 17548 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/cloud/cloudservice/v1/request_response_pb2.pyi b/temporalio/api/cloud/cloudservice/v1/request_response_pb2.pyi index 9202f86bb..149020039 100644 --- a/temporalio/api/cloud/cloudservice/v1/request_response_pb2.pyi +++ b/temporalio/api/cloud/cloudservice/v1/request_response_pb2.pyi @@ -13,6 +13,8 @@ import google.protobuf.message import google.protobuf.timestamp_pb2 import temporalio.api.cloud.account.v1.message_pb2 +import temporalio.api.cloud.auditlog.v1.message_pb2 +import temporalio.api.cloud.billing.v1.message_pb2 import temporalio.api.cloud.connectivityrule.v1.message_pb2 import temporalio.api.cloud.identity.v1.message_pb2 import temporalio.api.cloud.namespace.v1.message_pb2 @@ -28,6 +30,73 @@ else: DESCRIPTOR: google.protobuf.descriptor.FileDescriptor +class GetCurrentIdentityRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___GetCurrentIdentityRequest = GetCurrentIdentityRequest + +class GetCurrentIdentityResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + USER_FIELD_NUMBER: builtins.int + SERVICE_ACCOUNT_FIELD_NUMBER: builtins.int + PRINCIPAL_API_KEY_FIELD_NUMBER: builtins.int + @property + def user(self) -> temporalio.api.cloud.identity.v1.message_pb2.User: + """The user is a regular user""" + @property + def service_account( + self, + ) -> temporalio.api.cloud.identity.v1.message_pb2.ServiceAccount: + """The user is a service account""" + @property + def principal_api_key(self) -> temporalio.api.cloud.identity.v1.message_pb2.ApiKey: + """The API key info used to authenticate the request, if any""" + def __init__( + self, + *, + user: temporalio.api.cloud.identity.v1.message_pb2.User | None = ..., + service_account: temporalio.api.cloud.identity.v1.message_pb2.ServiceAccount + | None = ..., + principal_api_key: temporalio.api.cloud.identity.v1.message_pb2.ApiKey + | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "principal", + b"principal", + "principal_api_key", + b"principal_api_key", + "service_account", + b"service_account", + "user", + b"user", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "principal", + b"principal", + "principal_api_key", + b"principal_api_key", + "service_account", + b"service_account", + "user", + b"user", + ], + ) -> None: ... + def WhichOneof( + self, oneof_group: typing_extensions.Literal["principal", b"principal"] + ) -> typing_extensions.Literal["user", "service_account"] | None: ... + +global___GetCurrentIdentityResponse = GetCurrentIdentityResponse + class GetUsersRequest(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -557,7 +626,7 @@ class GetNamespacesRequest(google.protobuf.message.Message): NAME_FIELD_NUMBER: builtins.int page_size: builtins.int """The requested size of the page to retrieve. - Cannot exceed 1000. + Cannot exceed 1000. Optional, defaults to 100. """ page_token: builtins.str @@ -1157,6 +1226,7 @@ class GetApiKeysRequest(google.protobuf.message.Message): owner_type_deprecated: builtins.str """Filter api keys by owner type - optional. Possible values: user, service-account + temporal:versioning:max_version=v0.3.0 """ owner_type: temporalio.api.cloud.identity.v1.message_pb2.OwnerType.ValueType """Filter api keys by owner type - optional. @@ -2655,6 +2725,88 @@ class UpdateServiceAccountResponse(google.protobuf.message.Message): global___UpdateServiceAccountResponse = UpdateServiceAccountResponse +class SetServiceAccountNamespaceAccessRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SERVICE_ACCOUNT_ID_FIELD_NUMBER: builtins.int + NAMESPACE_FIELD_NUMBER: builtins.int + ACCESS_FIELD_NUMBER: builtins.int + RESOURCE_VERSION_FIELD_NUMBER: builtins.int + ASYNC_OPERATION_ID_FIELD_NUMBER: builtins.int + service_account_id: builtins.str + """The ID of the service account to update.""" + namespace: builtins.str + """The namespace to set permissions for.""" + @property + def access(self) -> temporalio.api.cloud.identity.v1.message_pb2.NamespaceAccess: + """The namespace access to assign the service account.""" + resource_version: builtins.str + """The version of the service account for which this update is intended for. + The latest version can be found in the GetServiceAccount response. + """ + async_operation_id: builtins.str + """The ID to use for this async operation - optional.""" + def __init__( + self, + *, + service_account_id: builtins.str = ..., + namespace: builtins.str = ..., + access: temporalio.api.cloud.identity.v1.message_pb2.NamespaceAccess + | None = ..., + resource_version: builtins.str = ..., + async_operation_id: builtins.str = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["access", b"access"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "access", + b"access", + "async_operation_id", + b"async_operation_id", + "namespace", + b"namespace", + "resource_version", + b"resource_version", + "service_account_id", + b"service_account_id", + ], + ) -> None: ... + +global___SetServiceAccountNamespaceAccessRequest = ( + SetServiceAccountNamespaceAccessRequest +) + +class SetServiceAccountNamespaceAccessResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ASYNC_OPERATION_FIELD_NUMBER: builtins.int + @property + def async_operation( + self, + ) -> temporalio.api.cloud.operation.v1.message_pb2.AsyncOperation: + """The async operation.""" + def __init__( + self, + *, + async_operation: temporalio.api.cloud.operation.v1.message_pb2.AsyncOperation + | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["async_operation", b"async_operation"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["async_operation", b"async_operation"], + ) -> None: ... + +global___SetServiceAccountNamespaceAccessResponse = ( + SetServiceAccountNamespaceAccessResponse +) + class DeleteServiceAccountRequest(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -3605,3 +3757,1051 @@ class DeleteConnectivityRuleResponse(google.protobuf.message.Message): ) -> None: ... global___DeleteConnectivityRuleResponse = DeleteConnectivityRuleResponse + +class GetAuditLogsRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + PAGE_SIZE_FIELD_NUMBER: builtins.int + PAGE_TOKEN_FIELD_NUMBER: builtins.int + START_TIME_INCLUSIVE_FIELD_NUMBER: builtins.int + END_TIME_EXCLUSIVE_FIELD_NUMBER: builtins.int + page_size: builtins.int + """The requested size of the page to retrieve - optional. + Cannot exceed 1000. Defaults to 100. + """ + page_token: builtins.str + """The page token if this is continuing from another response - optional.""" + @property + def start_time_inclusive(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Filter for UTC time >= (defaults to 30 days ago) - optional.""" + @property + def end_time_exclusive(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Filter for UTC time < (defaults to current time) - optional.""" + def __init__( + self, + *, + page_size: builtins.int = ..., + page_token: builtins.str = ..., + start_time_inclusive: google.protobuf.timestamp_pb2.Timestamp | None = ..., + end_time_exclusive: google.protobuf.timestamp_pb2.Timestamp | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "end_time_exclusive", + b"end_time_exclusive", + "start_time_inclusive", + b"start_time_inclusive", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "end_time_exclusive", + b"end_time_exclusive", + "page_size", + b"page_size", + "page_token", + b"page_token", + "start_time_inclusive", + b"start_time_inclusive", + ], + ) -> None: ... + +global___GetAuditLogsRequest = GetAuditLogsRequest + +class GetAuditLogsResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + LOGS_FIELD_NUMBER: builtins.int + NEXT_PAGE_TOKEN_FIELD_NUMBER: builtins.int + @property + def logs( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.cloud.auditlog.v1.message_pb2.LogRecord + ]: + """The list of audit logs ordered by emit time, log_id""" + next_page_token: builtins.str + """The next page's token.""" + def __init__( + self, + *, + logs: collections.abc.Iterable[ + temporalio.api.cloud.auditlog.v1.message_pb2.LogRecord + ] + | None = ..., + next_page_token: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "logs", b"logs", "next_page_token", b"next_page_token" + ], + ) -> None: ... + +global___GetAuditLogsResponse = GetAuditLogsResponse + +class ValidateAccountAuditLogSinkRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SPEC_FIELD_NUMBER: builtins.int + @property + def spec(self) -> temporalio.api.cloud.account.v1.message_pb2.AuditLogSinkSpec: + """The audit log sink spec that will be validated""" + def __init__( + self, + *, + spec: temporalio.api.cloud.account.v1.message_pb2.AuditLogSinkSpec | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["spec", b"spec"] + ) -> builtins.bool: ... + def ClearField( + self, field_name: typing_extensions.Literal["spec", b"spec"] + ) -> None: ... + +global___ValidateAccountAuditLogSinkRequest = ValidateAccountAuditLogSinkRequest + +class ValidateAccountAuditLogSinkResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___ValidateAccountAuditLogSinkResponse = ValidateAccountAuditLogSinkResponse + +class CreateAccountAuditLogSinkRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SPEC_FIELD_NUMBER: builtins.int + ASYNC_OPERATION_ID_FIELD_NUMBER: builtins.int + @property + def spec(self) -> temporalio.api.cloud.account.v1.message_pb2.AuditLogSinkSpec: + """The specification for the audit log sink.""" + async_operation_id: builtins.str + """Optional. The ID to use for this async operation.""" + def __init__( + self, + *, + spec: temporalio.api.cloud.account.v1.message_pb2.AuditLogSinkSpec | None = ..., + async_operation_id: builtins.str = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["spec", b"spec"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "async_operation_id", b"async_operation_id", "spec", b"spec" + ], + ) -> None: ... + +global___CreateAccountAuditLogSinkRequest = CreateAccountAuditLogSinkRequest + +class CreateAccountAuditLogSinkResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ASYNC_OPERATION_FIELD_NUMBER: builtins.int + @property + def async_operation( + self, + ) -> temporalio.api.cloud.operation.v1.message_pb2.AsyncOperation: + """The async operation.""" + def __init__( + self, + *, + async_operation: temporalio.api.cloud.operation.v1.message_pb2.AsyncOperation + | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["async_operation", b"async_operation"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["async_operation", b"async_operation"], + ) -> None: ... + +global___CreateAccountAuditLogSinkResponse = CreateAccountAuditLogSinkResponse + +class GetAccountAuditLogSinkRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + name: builtins.str + """The name of the sink to retrieve.""" + def __init__( + self, + *, + name: builtins.str = ..., + ) -> None: ... + def ClearField( + self, field_name: typing_extensions.Literal["name", b"name"] + ) -> None: ... + +global___GetAccountAuditLogSinkRequest = GetAccountAuditLogSinkRequest + +class GetAccountAuditLogSinkResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SINK_FIELD_NUMBER: builtins.int + @property + def sink(self) -> temporalio.api.cloud.account.v1.message_pb2.AuditLogSink: + """The audit log sink retrieved.""" + def __init__( + self, + *, + sink: temporalio.api.cloud.account.v1.message_pb2.AuditLogSink | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["sink", b"sink"] + ) -> builtins.bool: ... + def ClearField( + self, field_name: typing_extensions.Literal["sink", b"sink"] + ) -> None: ... + +global___GetAccountAuditLogSinkResponse = GetAccountAuditLogSinkResponse + +class GetAccountAuditLogSinksRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + PAGE_SIZE_FIELD_NUMBER: builtins.int + PAGE_TOKEN_FIELD_NUMBER: builtins.int + page_size: builtins.int + """The requested size of the page to retrieve. Cannot exceed 1000. + Defaults to 100 if not specified. + """ + page_token: builtins.str + """The page token if this is continuing from another response - optional.""" + def __init__( + self, + *, + page_size: builtins.int = ..., + page_token: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "page_size", b"page_size", "page_token", b"page_token" + ], + ) -> None: ... + +global___GetAccountAuditLogSinksRequest = GetAccountAuditLogSinksRequest + +class GetAccountAuditLogSinksResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SINKS_FIELD_NUMBER: builtins.int + NEXT_PAGE_TOKEN_FIELD_NUMBER: builtins.int + @property + def sinks( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.cloud.account.v1.message_pb2.AuditLogSink + ]: + """The list of audit log sinks retrieved.""" + next_page_token: builtins.str + """The next page token, set if there is another page.""" + def __init__( + self, + *, + sinks: collections.abc.Iterable[ + temporalio.api.cloud.account.v1.message_pb2.AuditLogSink + ] + | None = ..., + next_page_token: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "next_page_token", b"next_page_token", "sinks", b"sinks" + ], + ) -> None: ... + +global___GetAccountAuditLogSinksResponse = GetAccountAuditLogSinksResponse + +class UpdateAccountAuditLogSinkRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SPEC_FIELD_NUMBER: builtins.int + RESOURCE_VERSION_FIELD_NUMBER: builtins.int + ASYNC_OPERATION_ID_FIELD_NUMBER: builtins.int + @property + def spec(self) -> temporalio.api.cloud.account.v1.message_pb2.AuditLogSinkSpec: + """The updated audit log sink specification.""" + resource_version: builtins.str + """The version of the audit log sink to update. The latest version can be + retrieved using the GetAuditLogSink call. + """ + async_operation_id: builtins.str + """The ID to use for this async operation - optional.""" + def __init__( + self, + *, + spec: temporalio.api.cloud.account.v1.message_pb2.AuditLogSinkSpec | None = ..., + resource_version: builtins.str = ..., + async_operation_id: builtins.str = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["spec", b"spec"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "async_operation_id", + b"async_operation_id", + "resource_version", + b"resource_version", + "spec", + b"spec", + ], + ) -> None: ... + +global___UpdateAccountAuditLogSinkRequest = UpdateAccountAuditLogSinkRequest + +class UpdateAccountAuditLogSinkResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ASYNC_OPERATION_FIELD_NUMBER: builtins.int + @property + def async_operation( + self, + ) -> temporalio.api.cloud.operation.v1.message_pb2.AsyncOperation: + """The async operation.""" + def __init__( + self, + *, + async_operation: temporalio.api.cloud.operation.v1.message_pb2.AsyncOperation + | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["async_operation", b"async_operation"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["async_operation", b"async_operation"], + ) -> None: ... + +global___UpdateAccountAuditLogSinkResponse = UpdateAccountAuditLogSinkResponse + +class DeleteAccountAuditLogSinkRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + RESOURCE_VERSION_FIELD_NUMBER: builtins.int + ASYNC_OPERATION_ID_FIELD_NUMBER: builtins.int + name: builtins.str + """The name of the sink to delete.""" + resource_version: builtins.str + """The version of the sink to delete. The latest version can be + retrieved using the GetAccountAuditLogSink call. + """ + async_operation_id: builtins.str + """The ID to use for this async operation - optional.""" + def __init__( + self, + *, + name: builtins.str = ..., + resource_version: builtins.str = ..., + async_operation_id: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "async_operation_id", + b"async_operation_id", + "name", + b"name", + "resource_version", + b"resource_version", + ], + ) -> None: ... + +global___DeleteAccountAuditLogSinkRequest = DeleteAccountAuditLogSinkRequest + +class DeleteAccountAuditLogSinkResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ASYNC_OPERATION_FIELD_NUMBER: builtins.int + @property + def async_operation( + self, + ) -> temporalio.api.cloud.operation.v1.message_pb2.AsyncOperation: + """The async operation.""" + def __init__( + self, + *, + async_operation: temporalio.api.cloud.operation.v1.message_pb2.AsyncOperation + | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["async_operation", b"async_operation"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["async_operation", b"async_operation"], + ) -> None: ... + +global___DeleteAccountAuditLogSinkResponse = DeleteAccountAuditLogSinkResponse + +class GetNamespaceCapacityInfoRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + namespace: builtins.str + """The namespace identifier. + Required. + """ + def __init__( + self, + *, + namespace: builtins.str = ..., + ) -> None: ... + def ClearField( + self, field_name: typing_extensions.Literal["namespace", b"namespace"] + ) -> None: ... + +global___GetNamespaceCapacityInfoRequest = GetNamespaceCapacityInfoRequest + +class GetNamespaceCapacityInfoResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CAPACITY_INFO_FIELD_NUMBER: builtins.int + @property + def capacity_info( + self, + ) -> temporalio.api.cloud.namespace.v1.message_pb2.NamespaceCapacityInfo: + """Capacity information for the namespace.""" + def __init__( + self, + *, + capacity_info: temporalio.api.cloud.namespace.v1.message_pb2.NamespaceCapacityInfo + | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["capacity_info", b"capacity_info"] + ) -> builtins.bool: ... + def ClearField( + self, field_name: typing_extensions.Literal["capacity_info", b"capacity_info"] + ) -> None: ... + +global___GetNamespaceCapacityInfoResponse = GetNamespaceCapacityInfoResponse + +class CreateBillingReportRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SPEC_FIELD_NUMBER: builtins.int + ASYNC_OPERATION_ID_FIELD_NUMBER: builtins.int + @property + def spec(self) -> temporalio.api.cloud.billing.v1.message_pb2.BillingReportSpec: + """The specification for the billing report.""" + async_operation_id: builtins.str + """Optional, if not provided a random id will be generated.""" + def __init__( + self, + *, + spec: temporalio.api.cloud.billing.v1.message_pb2.BillingReportSpec + | None = ..., + async_operation_id: builtins.str = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["spec", b"spec"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "async_operation_id", b"async_operation_id", "spec", b"spec" + ], + ) -> None: ... + +global___CreateBillingReportRequest = CreateBillingReportRequest + +class CreateBillingReportResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + BILLING_REPORT_ID_FIELD_NUMBER: builtins.int + ASYNC_OPERATION_FIELD_NUMBER: builtins.int + billing_report_id: builtins.str + """The id of the billing report created.""" + @property + def async_operation( + self, + ) -> temporalio.api.cloud.operation.v1.message_pb2.AsyncOperation: + """The async operation.""" + def __init__( + self, + *, + billing_report_id: builtins.str = ..., + async_operation: temporalio.api.cloud.operation.v1.message_pb2.AsyncOperation + | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["async_operation", b"async_operation"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "async_operation", + b"async_operation", + "billing_report_id", + b"billing_report_id", + ], + ) -> None: ... + +global___CreateBillingReportResponse = CreateBillingReportResponse + +class GetBillingReportRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + BILLING_REPORT_ID_FIELD_NUMBER: builtins.int + billing_report_id: builtins.str + """The id of the billing report to retrieve.""" + def __init__( + self, + *, + billing_report_id: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "billing_report_id", b"billing_report_id" + ], + ) -> None: ... + +global___GetBillingReportRequest = GetBillingReportRequest + +class GetBillingReportResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + BILLING_REPORT_FIELD_NUMBER: builtins.int + @property + def billing_report( + self, + ) -> temporalio.api.cloud.billing.v1.message_pb2.BillingReport: + """The billing report retrieved.""" + def __init__( + self, + *, + billing_report: temporalio.api.cloud.billing.v1.message_pb2.BillingReport + | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["billing_report", b"billing_report"] + ) -> builtins.bool: ... + def ClearField( + self, field_name: typing_extensions.Literal["billing_report", b"billing_report"] + ) -> None: ... + +global___GetBillingReportResponse = GetBillingReportResponse + +class GetCustomRolesRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + PAGE_SIZE_FIELD_NUMBER: builtins.int + PAGE_TOKEN_FIELD_NUMBER: builtins.int + page_size: builtins.int + """The requested size of the page to retrieve. + Cannot exceed 1000. Defaults to 100. + """ + page_token: builtins.str + """The page token if this is continuing from another response.""" + def __init__( + self, + *, + page_size: builtins.int = ..., + page_token: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "page_size", b"page_size", "page_token", b"page_token" + ], + ) -> None: ... + +global___GetCustomRolesRequest = GetCustomRolesRequest + +class GetCustomRolesResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CUSTOM_ROLES_FIELD_NUMBER: builtins.int + NEXT_PAGE_TOKEN_FIELD_NUMBER: builtins.int + @property + def custom_roles( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.cloud.identity.v1.message_pb2.CustomRole + ]: + """The list of custom roles in ascending ID order.""" + next_page_token: builtins.str + """The next page token.""" + def __init__( + self, + *, + custom_roles: collections.abc.Iterable[ + temporalio.api.cloud.identity.v1.message_pb2.CustomRole + ] + | None = ..., + next_page_token: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "custom_roles", b"custom_roles", "next_page_token", b"next_page_token" + ], + ) -> None: ... + +global___GetCustomRolesResponse = GetCustomRolesResponse + +class GetCustomRoleRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ROLE_ID_FIELD_NUMBER: builtins.int + role_id: builtins.str + """The ID of the custom role to retrieve.""" + def __init__( + self, + *, + role_id: builtins.str = ..., + ) -> None: ... + def ClearField( + self, field_name: typing_extensions.Literal["role_id", b"role_id"] + ) -> None: ... + +global___GetCustomRoleRequest = GetCustomRoleRequest + +class GetCustomRoleResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CUSTOM_ROLE_FIELD_NUMBER: builtins.int + @property + def custom_role(self) -> temporalio.api.cloud.identity.v1.message_pb2.CustomRole: + """The custom role retrieved.""" + def __init__( + self, + *, + custom_role: temporalio.api.cloud.identity.v1.message_pb2.CustomRole + | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["custom_role", b"custom_role"] + ) -> builtins.bool: ... + def ClearField( + self, field_name: typing_extensions.Literal["custom_role", b"custom_role"] + ) -> None: ... + +global___GetCustomRoleResponse = GetCustomRoleResponse + +class CreateCustomRoleRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SPEC_FIELD_NUMBER: builtins.int + ASYNC_OPERATION_ID_FIELD_NUMBER: builtins.int + @property + def spec(self) -> temporalio.api.cloud.identity.v1.message_pb2.CustomRoleSpec: + """The specification for the custom role to create.""" + async_operation_id: builtins.str + """The ID to use for this async operation. + Optional, if not provided a random ID will be generated. + """ + def __init__( + self, + *, + spec: temporalio.api.cloud.identity.v1.message_pb2.CustomRoleSpec | None = ..., + async_operation_id: builtins.str = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["spec", b"spec"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "async_operation_id", b"async_operation_id", "spec", b"spec" + ], + ) -> None: ... + +global___CreateCustomRoleRequest = CreateCustomRoleRequest + +class CreateCustomRoleResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ROLE_ID_FIELD_NUMBER: builtins.int + ASYNC_OPERATION_FIELD_NUMBER: builtins.int + role_id: builtins.str + """The ID of the custom role created.""" + @property + def async_operation( + self, + ) -> temporalio.api.cloud.operation.v1.message_pb2.AsyncOperation: + """The async operation.""" + def __init__( + self, + *, + role_id: builtins.str = ..., + async_operation: temporalio.api.cloud.operation.v1.message_pb2.AsyncOperation + | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["async_operation", b"async_operation"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "async_operation", b"async_operation", "role_id", b"role_id" + ], + ) -> None: ... + +global___CreateCustomRoleResponse = CreateCustomRoleResponse + +class UpdateCustomRoleRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ROLE_ID_FIELD_NUMBER: builtins.int + SPEC_FIELD_NUMBER: builtins.int + RESOURCE_VERSION_FIELD_NUMBER: builtins.int + ASYNC_OPERATION_ID_FIELD_NUMBER: builtins.int + role_id: builtins.str + """The ID of the custom role to update.""" + @property + def spec(self) -> temporalio.api.cloud.identity.v1.message_pb2.CustomRoleSpec: + """The new custom role specification.""" + resource_version: builtins.str + """The version of the custom role for which this update is intended. + The latest version can be found in the GetCustomRole operation response. + """ + async_operation_id: builtins.str + """The ID to use for this async operation. + Optional, if not provided a random ID will be generated. + """ + def __init__( + self, + *, + role_id: builtins.str = ..., + spec: temporalio.api.cloud.identity.v1.message_pb2.CustomRoleSpec | None = ..., + resource_version: builtins.str = ..., + async_operation_id: builtins.str = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["spec", b"spec"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "async_operation_id", + b"async_operation_id", + "resource_version", + b"resource_version", + "role_id", + b"role_id", + "spec", + b"spec", + ], + ) -> None: ... + +global___UpdateCustomRoleRequest = UpdateCustomRoleRequest + +class UpdateCustomRoleResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ASYNC_OPERATION_FIELD_NUMBER: builtins.int + @property + def async_operation( + self, + ) -> temporalio.api.cloud.operation.v1.message_pb2.AsyncOperation: + """The async operation.""" + def __init__( + self, + *, + async_operation: temporalio.api.cloud.operation.v1.message_pb2.AsyncOperation + | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["async_operation", b"async_operation"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["async_operation", b"async_operation"], + ) -> None: ... + +global___UpdateCustomRoleResponse = UpdateCustomRoleResponse + +class DeleteCustomRoleRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ROLE_ID_FIELD_NUMBER: builtins.int + RESOURCE_VERSION_FIELD_NUMBER: builtins.int + ASYNC_OPERATION_ID_FIELD_NUMBER: builtins.int + role_id: builtins.str + """The ID of the custom role to delete.""" + resource_version: builtins.str + """The version of the custom role for which this delete is intended. + The latest version can be found in the GetCustomRole operation response. + """ + async_operation_id: builtins.str + """The ID to use for this async operation. + Optional, if not provided a random ID will be generated. + """ + def __init__( + self, + *, + role_id: builtins.str = ..., + resource_version: builtins.str = ..., + async_operation_id: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "async_operation_id", + b"async_operation_id", + "resource_version", + b"resource_version", + "role_id", + b"role_id", + ], + ) -> None: ... + +global___DeleteCustomRoleRequest = DeleteCustomRoleRequest + +class DeleteCustomRoleResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ASYNC_OPERATION_FIELD_NUMBER: builtins.int + @property + def async_operation( + self, + ) -> temporalio.api.cloud.operation.v1.message_pb2.AsyncOperation: + """The async operation.""" + def __init__( + self, + *, + async_operation: temporalio.api.cloud.operation.v1.message_pb2.AsyncOperation + | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["async_operation", b"async_operation"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["async_operation", b"async_operation"], + ) -> None: ... + +global___DeleteCustomRoleResponse = DeleteCustomRoleResponse + +class GetUserNamespaceAssignmentsRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + PAGE_SIZE_FIELD_NUMBER: builtins.int + PAGE_TOKEN_FIELD_NUMBER: builtins.int + namespace: builtins.str + """The namespace to get users for.""" + page_size: builtins.int + """The requested size of the page to retrieve - optional. + Cannot exceed 1000. Defaults to 100. + """ + page_token: builtins.str + """The page token if this is continuing from another response - optional.""" + def __init__( + self, + *, + namespace: builtins.str = ..., + page_size: builtins.int = ..., + page_token: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "namespace", + b"namespace", + "page_size", + b"page_size", + "page_token", + b"page_token", + ], + ) -> None: ... + +global___GetUserNamespaceAssignmentsRequest = GetUserNamespaceAssignmentsRequest + +class GetUserNamespaceAssignmentsResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + USERS_FIELD_NUMBER: builtins.int + NEXT_PAGE_TOKEN_FIELD_NUMBER: builtins.int + @property + def users( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.cloud.identity.v1.message_pb2.UserNamespaceAssignment + ]: + """The list of users with access to the namespace.""" + next_page_token: builtins.str + """The next page's token.""" + def __init__( + self, + *, + users: collections.abc.Iterable[ + temporalio.api.cloud.identity.v1.message_pb2.UserNamespaceAssignment + ] + | None = ..., + next_page_token: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "next_page_token", b"next_page_token", "users", b"users" + ], + ) -> None: ... + +global___GetUserNamespaceAssignmentsResponse = GetUserNamespaceAssignmentsResponse + +class GetServiceAccountNamespaceAssignmentsRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + PAGE_SIZE_FIELD_NUMBER: builtins.int + PAGE_TOKEN_FIELD_NUMBER: builtins.int + namespace: builtins.str + """The namespace to get service accounts for.""" + page_size: builtins.int + """The requested size of the page to retrieve - optional. + Cannot exceed 1000. Defaults to 100. + """ + page_token: builtins.str + """The page token if this is continuing from another response - optional.""" + def __init__( + self, + *, + namespace: builtins.str = ..., + page_size: builtins.int = ..., + page_token: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "namespace", + b"namespace", + "page_size", + b"page_size", + "page_token", + b"page_token", + ], + ) -> None: ... + +global___GetServiceAccountNamespaceAssignmentsRequest = ( + GetServiceAccountNamespaceAssignmentsRequest +) + +class GetServiceAccountNamespaceAssignmentsResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SERVICE_ACCOUNTS_FIELD_NUMBER: builtins.int + NEXT_PAGE_TOKEN_FIELD_NUMBER: builtins.int + @property + def service_accounts( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.cloud.identity.v1.message_pb2.ServiceAccountNamespaceAssignment + ]: + """The list of service accounts with access to the namespace.""" + next_page_token: builtins.str + """The next page's token.""" + def __init__( + self, + *, + service_accounts: collections.abc.Iterable[ + temporalio.api.cloud.identity.v1.message_pb2.ServiceAccountNamespaceAssignment + ] + | None = ..., + next_page_token: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "next_page_token", + b"next_page_token", + "service_accounts", + b"service_accounts", + ], + ) -> None: ... + +global___GetServiceAccountNamespaceAssignmentsResponse = ( + GetServiceAccountNamespaceAssignmentsResponse +) + +class GetUserGroupNamespaceAssignmentsRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + PAGE_SIZE_FIELD_NUMBER: builtins.int + PAGE_TOKEN_FIELD_NUMBER: builtins.int + namespace: builtins.str + """The namespace to get user groups for.""" + page_size: builtins.int + """The requested size of the page to retrieve - optional. + Cannot exceed 1000. Defaults to 100. + """ + page_token: builtins.str + """The page token if this is continuing from another response - optional.""" + def __init__( + self, + *, + namespace: builtins.str = ..., + page_size: builtins.int = ..., + page_token: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "namespace", + b"namespace", + "page_size", + b"page_size", + "page_token", + b"page_token", + ], + ) -> None: ... + +global___GetUserGroupNamespaceAssignmentsRequest = ( + GetUserGroupNamespaceAssignmentsRequest +) + +class GetUserGroupNamespaceAssignmentsResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + GROUPS_FIELD_NUMBER: builtins.int + NEXT_PAGE_TOKEN_FIELD_NUMBER: builtins.int + @property + def groups( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.cloud.identity.v1.message_pb2.UserGroupNamespaceAssignment + ]: + """The list of user groups with access to the namespace.""" + next_page_token: builtins.str + """The next page's token.""" + def __init__( + self, + *, + groups: collections.abc.Iterable[ + temporalio.api.cloud.identity.v1.message_pb2.UserGroupNamespaceAssignment + ] + | None = ..., + next_page_token: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "groups", b"groups", "next_page_token", b"next_page_token" + ], + ) -> None: ... + +global___GetUserGroupNamespaceAssignmentsResponse = ( + GetUserGroupNamespaceAssignmentsResponse +) diff --git a/temporalio/api/cloud/cloudservice/v1/service_pb2.py b/temporalio/api/cloud/cloudservice/v1/service_pb2.py index 7ff284911..aca6662c2 100644 --- a/temporalio/api/cloud/cloudservice/v1/service_pb2.py +++ b/temporalio/api/cloud/cloudservice/v1/service_pb2.py @@ -19,298 +19,325 @@ from temporalio.api.cloud.cloudservice.v1 import ( request_response_pb2 as temporal_dot_api_dot_cloud_dot_cloudservice_dot_v1_dot_request__response__pb2, ) +from temporalio.api.dependencies.protoc_gen_openapiv2.options import ( + annotations_pb2 as protoc__gen__openapiv2_dot_options_dot_annotations__pb2, +) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n0temporal/api/cloud/cloudservice/v1/service.proto\x12"temporal.api.cloud.cloudservice.v1\x1a\x39temporal/api/cloud/cloudservice/v1/request_response.proto\x1a\x1cgoogle/api/annotations.proto2\xf2R\n\x0c\x43loudService\x12\x8b\x01\n\x08GetUsers\x12\x33.temporal.api.cloud.cloudservice.v1.GetUsersRequest\x1a\x34.temporal.api.cloud.cloudservice.v1.GetUsersResponse"\x14\x82\xd3\xe4\x93\x02\x0e\x12\x0c/cloud/users\x12\x92\x01\n\x07GetUser\x12\x32.temporal.api.cloud.cloudservice.v1.GetUserRequest\x1a\x33.temporal.api.cloud.cloudservice.v1.GetUserResponse"\x1e\x82\xd3\xe4\x93\x02\x18\x12\x16/cloud/users/{user_id}\x12\x94\x01\n\nCreateUser\x12\x35.temporal.api.cloud.cloudservice.v1.CreateUserRequest\x1a\x36.temporal.api.cloud.cloudservice.v1.CreateUserResponse"\x17\x82\xd3\xe4\x93\x02\x11"\x0c/cloud/users:\x01*\x12\x9e\x01\n\nUpdateUser\x12\x35.temporal.api.cloud.cloudservice.v1.UpdateUserRequest\x1a\x36.temporal.api.cloud.cloudservice.v1.UpdateUserResponse"!\x82\xd3\xe4\x93\x02\x1b"\x16/cloud/users/{user_id}:\x01*\x12\x9b\x01\n\nDeleteUser\x12\x35.temporal.api.cloud.cloudservice.v1.DeleteUserRequest\x1a\x36.temporal.api.cloud.cloudservice.v1.DeleteUserResponse"\x1e\x82\xd3\xe4\x93\x02\x18*\x16/cloud/users/{user_id}\x12\xe0\x01\n\x16SetUserNamespaceAccess\x12\x41.temporal.api.cloud.cloudservice.v1.SetUserNamespaceAccessRequest\x1a\x42.temporal.api.cloud.cloudservice.v1.SetUserNamespaceAccessResponse"?\x82\xd3\xe4\x93\x02\x39"4/cloud/namespaces/{namespace}/users/{user_id}/access:\x01*\x12\xc0\x01\n\x11GetAsyncOperation\x12<.temporal.api.cloud.cloudservice.v1.GetAsyncOperationRequest\x1a=.temporal.api.cloud.cloudservice.v1.GetAsyncOperationResponse".\x82\xd3\xe4\x93\x02(\x12&/cloud/operations/{async_operation_id}\x12\xa8\x01\n\x0f\x43reateNamespace\x12:.temporal.api.cloud.cloudservice.v1.CreateNamespaceRequest\x1a;.temporal.api.cloud.cloudservice.v1.CreateNamespaceResponse"\x1c\x82\xd3\xe4\x93\x02\x16"\x11/cloud/namespaces:\x01*\x12\x9f\x01\n\rGetNamespaces\x12\x38.temporal.api.cloud.cloudservice.v1.GetNamespacesRequest\x1a\x39.temporal.api.cloud.cloudservice.v1.GetNamespacesResponse"\x19\x82\xd3\xe4\x93\x02\x13\x12\x11/cloud/namespaces\x12\xa8\x01\n\x0cGetNamespace\x12\x37.temporal.api.cloud.cloudservice.v1.GetNamespaceRequest\x1a\x38.temporal.api.cloud.cloudservice.v1.GetNamespaceResponse"%\x82\xd3\xe4\x93\x02\x1f\x12\x1d/cloud/namespaces/{namespace}\x12\xb4\x01\n\x0fUpdateNamespace\x12:.temporal.api.cloud.cloudservice.v1.UpdateNamespaceRequest\x1a;.temporal.api.cloud.cloudservice.v1.UpdateNamespaceResponse"(\x82\xd3\xe4\x93\x02""\x1d/cloud/namespaces/{namespace}:\x01*\x12\xf7\x01\n\x1bRenameCustomSearchAttribute\x12\x46.temporal.api.cloud.cloudservice.v1.RenameCustomSearchAttributeRequest\x1aG.temporal.api.cloud.cloudservice.v1.RenameCustomSearchAttributeResponse"G\x82\xd3\xe4\x93\x02\x41".temporal.api.cloud.cloudservice.v1.AddNamespaceRegionResponse"3\x82\xd3\xe4\x93\x02-"(/cloud/namespaces/{namespace}/add-region:\x01*\x12\xd4\x01\n\x15\x44\x65leteNamespaceRegion\x12@.temporal.api.cloud.cloudservice.v1.DeleteNamespaceRegionRequest\x1a\x41.temporal.api.cloud.cloudservice.v1.DeleteNamespaceRegionResponse"6\x82\xd3\xe4\x93\x02\x30*./cloud/namespaces/{namespace}/regions/{region}\x12\x93\x01\n\nGetRegions\x12\x35.temporal.api.cloud.cloudservice.v1.GetRegionsRequest\x1a\x36.temporal.api.cloud.cloudservice.v1.GetRegionsResponse"\x16\x82\xd3\xe4\x93\x02\x10\x12\x0e/cloud/regions\x12\x99\x01\n\tGetRegion\x12\x34.temporal.api.cloud.cloudservice.v1.GetRegionRequest\x1a\x35.temporal.api.cloud.cloudservice.v1.GetRegionResponse"\x1f\x82\xd3\xe4\x93\x02\x19\x12\x17/cloud/regions/{region}\x12\x94\x01\n\nGetApiKeys\x12\x35.temporal.api.cloud.cloudservice.v1.GetApiKeysRequest\x1a\x36.temporal.api.cloud.cloudservice.v1.GetApiKeysResponse"\x17\x82\xd3\xe4\x93\x02\x11\x12\x0f/cloud/api-keys\x12\x9a\x01\n\tGetApiKey\x12\x34.temporal.api.cloud.cloudservice.v1.GetApiKeyRequest\x1a\x35.temporal.api.cloud.cloudservice.v1.GetApiKeyResponse" \x82\xd3\xe4\x93\x02\x1a\x12\x18/cloud/api-keys/{key_id}\x12\x9d\x01\n\x0c\x43reateApiKey\x12\x37.temporal.api.cloud.cloudservice.v1.CreateApiKeyRequest\x1a\x38.temporal.api.cloud.cloudservice.v1.CreateApiKeyResponse"\x1a\x82\xd3\xe4\x93\x02\x14"\x0f/cloud/api-keys:\x01*\x12\xa6\x01\n\x0cUpdateApiKey\x12\x37.temporal.api.cloud.cloudservice.v1.UpdateApiKeyRequest\x1a\x38.temporal.api.cloud.cloudservice.v1.UpdateApiKeyResponse"#\x82\xd3\xe4\x93\x02\x1d"\x18/cloud/api-keys/{key_id}:\x01*\x12\xa3\x01\n\x0c\x44\x65leteApiKey\x12\x37.temporal.api.cloud.cloudservice.v1.DeleteApiKeyRequest\x1a\x38.temporal.api.cloud.cloudservice.v1.DeleteApiKeyResponse" \x82\xd3\xe4\x93\x02\x1a*\x18/cloud/api-keys/{key_id}\x12\xb0\x01\n\x11GetNexusEndpoints\x12<.temporal.api.cloud.cloudservice.v1.GetNexusEndpointsRequest\x1a=.temporal.api.cloud.cloudservice.v1.GetNexusEndpointsResponse"\x1e\x82\xd3\xe4\x93\x02\x18\x12\x16/cloud/nexus/endpoints\x12\xbb\x01\n\x10GetNexusEndpoint\x12;.temporal.api.cloud.cloudservice.v1.GetNexusEndpointRequest\x1a<.temporal.api.cloud.cloudservice.v1.GetNexusEndpointResponse",\x82\xd3\xe4\x93\x02&\x12$/cloud/nexus/endpoints/{endpoint_id}\x12\xb9\x01\n\x13\x43reateNexusEndpoint\x12>.temporal.api.cloud.cloudservice.v1.CreateNexusEndpointRequest\x1a?.temporal.api.cloud.cloudservice.v1.CreateNexusEndpointResponse"!\x82\xd3\xe4\x93\x02\x1b"\x16/cloud/nexus/endpoints:\x01*\x12\xc7\x01\n\x13UpdateNexusEndpoint\x12>.temporal.api.cloud.cloudservice.v1.UpdateNexusEndpointRequest\x1a?.temporal.api.cloud.cloudservice.v1.UpdateNexusEndpointResponse"/\x82\xd3\xe4\x93\x02)"$/cloud/nexus/endpoints/{endpoint_id}:\x01*\x12\xc4\x01\n\x13\x44\x65leteNexusEndpoint\x12>.temporal.api.cloud.cloudservice.v1.DeleteNexusEndpointRequest\x1a?.temporal.api.cloud.cloudservice.v1.DeleteNexusEndpointResponse",\x82\xd3\xe4\x93\x02&*$/cloud/nexus/endpoints/{endpoint_id}\x12\xa0\x01\n\rGetUserGroups\x12\x38.temporal.api.cloud.cloudservice.v1.GetUserGroupsRequest\x1a\x39.temporal.api.cloud.cloudservice.v1.GetUserGroupsResponse"\x1a\x82\xd3\xe4\x93\x02\x14\x12\x12/cloud/user-groups\x12\xa8\x01\n\x0cGetUserGroup\x12\x37.temporal.api.cloud.cloudservice.v1.GetUserGroupRequest\x1a\x38.temporal.api.cloud.cloudservice.v1.GetUserGroupResponse"%\x82\xd3\xe4\x93\x02\x1f\x12\x1d/cloud/user-groups/{group_id}\x12\xa9\x01\n\x0f\x43reateUserGroup\x12:.temporal.api.cloud.cloudservice.v1.CreateUserGroupRequest\x1a;.temporal.api.cloud.cloudservice.v1.CreateUserGroupResponse"\x1d\x82\xd3\xe4\x93\x02\x17"\x12/cloud/user-groups:\x01*\x12\xb4\x01\n\x0fUpdateUserGroup\x12:.temporal.api.cloud.cloudservice.v1.UpdateUserGroupRequest\x1a;.temporal.api.cloud.cloudservice.v1.UpdateUserGroupResponse"(\x82\xd3\xe4\x93\x02""\x1d/cloud/user-groups/{group_id}:\x01*\x12\xb1\x01\n\x0f\x44\x65leteUserGroup\x12:.temporal.api.cloud.cloudservice.v1.DeleteUserGroupRequest\x1a;.temporal.api.cloud.cloudservice.v1.DeleteUserGroupResponse"%\x82\xd3\xe4\x93\x02\x1f*\x1d/cloud/user-groups/{group_id}\x12\xf6\x01\n\x1bSetUserGroupNamespaceAccess\x12\x46.temporal.api.cloud.cloudservice.v1.SetUserGroupNamespaceAccessRequest\x1aG.temporal.api.cloud.cloudservice.v1.SetUserGroupNamespaceAccessResponse"F\x82\xd3\xe4\x93\x02@";/cloud/namespaces/{namespace}/user-groups/{group_id}/access:\x01*\x12\xc5\x01\n\x12\x41\x64\x64UserGroupMember\x12=.temporal.api.cloud.cloudservice.v1.AddUserGroupMemberRequest\x1a>.temporal.api.cloud.cloudservice.v1.AddUserGroupMemberResponse"0\x82\xd3\xe4\x93\x02*"%/cloud/user-groups/{group_id}/members:\x01*\x12\xd4\x01\n\x15RemoveUserGroupMember\x12@.temporal.api.cloud.cloudservice.v1.RemoveUserGroupMemberRequest\x1a\x41.temporal.api.cloud.cloudservice.v1.RemoveUserGroupMemberResponse"6\x82\xd3\xe4\x93\x02\x30"+/cloud/user-groups/{group_id}/remove-member:\x01*\x12\xc5\x01\n\x13GetUserGroupMembers\x12>.temporal.api.cloud.cloudservice.v1.GetUserGroupMembersRequest\x1a?.temporal.api.cloud.cloudservice.v1.GetUserGroupMembersResponse"-\x82\xd3\xe4\x93\x02\'\x12%/cloud/user-groups/{group_id}/members\x12\xbd\x01\n\x14\x43reateServiceAccount\x12?.temporal.api.cloud.cloudservice.v1.CreateServiceAccountRequest\x1a@.temporal.api.cloud.cloudservice.v1.CreateServiceAccountResponse""\x82\xd3\xe4\x93\x02\x1c"\x17/cloud/service-accounts:\x01*\x12\xc6\x01\n\x11GetServiceAccount\x12<.temporal.api.cloud.cloudservice.v1.GetServiceAccountRequest\x1a=.temporal.api.cloud.cloudservice.v1.GetServiceAccountResponse"4\x82\xd3\xe4\x93\x02.\x12,/cloud/service-accounts/{service_account_id}\x12\xb4\x01\n\x12GetServiceAccounts\x12=.temporal.api.cloud.cloudservice.v1.GetServiceAccountsRequest\x1a>.temporal.api.cloud.cloudservice.v1.GetServiceAccountsResponse"\x1f\x82\xd3\xe4\x93\x02\x19\x12\x17/cloud/service-accounts\x12\xd2\x01\n\x14UpdateServiceAccount\x12?.temporal.api.cloud.cloudservice.v1.UpdateServiceAccountRequest\x1a@.temporal.api.cloud.cloudservice.v1.UpdateServiceAccountResponse"7\x82\xd3\xe4\x93\x02\x31",/cloud/service-accounts/{service_account_id}:\x01*\x12\xcf\x01\n\x14\x44\x65leteServiceAccount\x12?.temporal.api.cloud.cloudservice.v1.DeleteServiceAccountRequest\x1a@.temporal.api.cloud.cloudservice.v1.DeleteServiceAccountResponse"4\x82\xd3\xe4\x93\x02.*,/cloud/service-accounts/{service_account_id}\x12\x8b\x01\n\x08GetUsage\x12\x33.temporal.api.cloud.cloudservice.v1.GetUsageRequest\x1a\x34.temporal.api.cloud.cloudservice.v1.GetUsageResponse"\x14\x82\xd3\xe4\x93\x02\x0e\x12\x0c/cloud/usage\x12\x93\x01\n\nGetAccount\x12\x35.temporal.api.cloud.cloudservice.v1.GetAccountRequest\x1a\x36.temporal.api.cloud.cloudservice.v1.GetAccountResponse"\x16\x82\xd3\xe4\x93\x02\x10\x12\x0e/cloud/account\x12\x9f\x01\n\rUpdateAccount\x12\x38.temporal.api.cloud.cloudservice.v1.UpdateAccountRequest\x1a\x39.temporal.api.cloud.cloudservice.v1.UpdateAccountResponse"\x19\x82\xd3\xe4\x93\x02\x13"\x0e/cloud/account:\x01*\x12\xdf\x01\n\x19\x43reateNamespaceExportSink\x12\x44.temporal.api.cloud.cloudservice.v1.CreateNamespaceExportSinkRequest\x1a\x45.temporal.api.cloud.cloudservice.v1.CreateNamespaceExportSinkResponse"5\x82\xd3\xe4\x93\x02/"*/cloud/namespaces/{namespace}/export-sinks:\x01*\x12\xda\x01\n\x16GetNamespaceExportSink\x12\x41.temporal.api.cloud.cloudservice.v1.GetNamespaceExportSinkRequest\x1a\x42.temporal.api.cloud.cloudservice.v1.GetNamespaceExportSinkResponse"9\x82\xd3\xe4\x93\x02\x33\x12\x31/cloud/namespaces/{namespace}/export-sinks/{name}\x12\xd6\x01\n\x17GetNamespaceExportSinks\x12\x42.temporal.api.cloud.cloudservice.v1.GetNamespaceExportSinksRequest\x1a\x43.temporal.api.cloud.cloudservice.v1.GetNamespaceExportSinksResponse"2\x82\xd3\xe4\x93\x02,\x12*/cloud/namespaces/{namespace}/export-sinks\x12\xeb\x01\n\x19UpdateNamespaceExportSink\x12\x44.temporal.api.cloud.cloudservice.v1.UpdateNamespaceExportSinkRequest\x1a\x45.temporal.api.cloud.cloudservice.v1.UpdateNamespaceExportSinkResponse"A\x82\xd3\xe4\x93\x02;"6/cloud/namespaces/{namespace}/export-sinks/{spec.name}:\x01*\x12\xe3\x01\n\x19\x44\x65leteNamespaceExportSink\x12\x44.temporal.api.cloud.cloudservice.v1.DeleteNamespaceExportSinkRequest\x1a\x45.temporal.api.cloud.cloudservice.v1.DeleteNamespaceExportSinkResponse"9\x82\xd3\xe4\x93\x02\x33*1/cloud/namespaces/{namespace}/export-sinks/{name}\x12\xee\x01\n\x1bValidateNamespaceExportSink\x12\x46.temporal.api.cloud.cloudservice.v1.ValidateNamespaceExportSinkRequest\x1aG.temporal.api.cloud.cloudservice.v1.ValidateNamespaceExportSinkResponse">\x82\xd3\xe4\x93\x02\x38"3/cloud/namespaces/{namespace}/export-sinks/validate:\x01*\x12\xcc\x01\n\x13UpdateNamespaceTags\x12>.temporal.api.cloud.cloudservice.v1.UpdateNamespaceTagsRequest\x1a?.temporal.api.cloud.cloudservice.v1.UpdateNamespaceTagsResponse"4\x82\xd3\xe4\x93\x02.")/cloud/namespaces/{namespace}/update-tags:\x01*\x12\xc5\x01\n\x16\x43reateConnectivityRule\x12\x41.temporal.api.cloud.cloudservice.v1.CreateConnectivityRuleRequest\x1a\x42.temporal.api.cloud.cloudservice.v1.CreateConnectivityRuleResponse"$\x82\xd3\xe4\x93\x02\x1e"\x19/cloud/connectivity-rules:\x01*\x12\xd0\x01\n\x13GetConnectivityRule\x12>.temporal.api.cloud.cloudservice.v1.GetConnectivityRuleRequest\x1a?.temporal.api.cloud.cloudservice.v1.GetConnectivityRuleResponse"8\x82\xd3\xe4\x93\x02\x32\x12\x30/cloud/connectivity-rules/{connectivity_rule_id}\x12\xbc\x01\n\x14GetConnectivityRules\x12?.temporal.api.cloud.cloudservice.v1.GetConnectivityRulesRequest\x1a@.temporal.api.cloud.cloudservice.v1.GetConnectivityRulesResponse"!\x82\xd3\xe4\x93\x02\x1b\x12\x19/cloud/connectivity-rules\x12\xd9\x01\n\x16\x44\x65leteConnectivityRule\x12\x41.temporal.api.cloud.cloudservice.v1.DeleteConnectivityRuleRequest\x1a\x42.temporal.api.cloud.cloudservice.v1.DeleteConnectivityRuleResponse"8\x82\xd3\xe4\x93\x02\x32*0/cloud/connectivity-rules/{connectivity_rule_id}B\xc0\x01\n%io.temporal.api.cloud.cloudservice.v1B\x0cServiceProtoP\x01Z5go.temporal.io/api/cloud/cloudservice/v1;cloudservice\xaa\x02$Temporalio.Api.Cloud.CloudService.V1\xea\x02(Temporalio::Api::Cloud::CloudService::V1b\x06proto3' + b'\n0temporal/api/cloud/cloudservice/v1/service.proto\x12"temporal.api.cloud.cloudservice.v1\x1a\x39temporal/api/cloud/cloudservice/v1/request_response.proto\x1a\x1cgoogle/api/annotations.proto\x1a.protoc-gen-openapiv2/options/annotations.proto2\x92\xd7\x01\n\x0c\x43loudService\x12\xb0\x02\n\x12GetCurrentIdentity\x12=.temporal.api.cloud.cloudservice.v1.GetCurrentIdentityRequest\x1a>.temporal.api.cloud.cloudservice.v1.GetCurrentIdentityResponse"\x9a\x01\x82\xd3\xe4\x93\x02\x19\x12\x17/cloud/current-identity\x92\x41x\n\x07\x41\x63\x63ount\x12\x14Get current identity\x1aWReturns information about the currently authenticated user or service account principal\x12\xa5\x02\n\x08GetUsers\x12\x33.temporal.api.cloud.cloudservice.v1.GetUsersRequest\x1a\x34.temporal.api.cloud.cloudservice.v1.GetUsersResponse"\xad\x01\x82\xd3\xe4\x93\x02\x0e\x12\x0c/cloud/users\x92\x41\x95\x01\n\x05Users\x12\x0eList all users\x1a*Returns a list of all users in the account"E\n\x1dUser management documentation\x12$https://docs.temporal.io/cloud/users*\tlistUsers\x12\x9c\x02\n\x07GetUser\x12\x32.temporal.api.cloud.cloudservice.v1.GetUserRequest\x1a\x33.temporal.api.cloud.cloudservice.v1.GetUserResponse"\xa7\x01\x82\xd3\xe4\x93\x02\x18\x12\x16/cloud/users/{user_id}\x92\x41\x85\x01\n\x05Users\x12\x0eGet user by ID\x1a%Takes a user ID, returns user details"E\n\x1dUser management documentation\x12$https://docs.temporal.io/cloud/users\x12\xd0\x01\n\nCreateUser\x12\x35.temporal.api.cloud.cloudservice.v1.CreateUserRequest\x1a\x36.temporal.api.cloud.cloudservice.v1.CreateUserResponse"S\x82\xd3\xe4\x93\x02\x11"\x0c/cloud/users:\x01*\x92\x41\x39\n\x05Users\x12\rCreate a user\x1a!Creates a new user in the account\x12\xdb\x01\n\nUpdateUser\x12\x35.temporal.api.cloud.cloudservice.v1.UpdateUserRequest\x1a\x36.temporal.api.cloud.cloudservice.v1.UpdateUserResponse"^\x82\xd3\xe4\x93\x02\x1b"\x16/cloud/users/{user_id}:\x01*\x92\x41:\n\x05Users\x12\rUpdate a user\x1a"Updates an existing user\'s details\x12\xd5\x01\n\nDeleteUser\x12\x35.temporal.api.cloud.cloudservice.v1.DeleteUserRequest\x1a\x36.temporal.api.cloud.cloudservice.v1.DeleteUserResponse"X\x82\xd3\xe4\x93\x02\x18*\x16/cloud/users/{user_id}\x92\x41\x37\n\x05Users\x12\rDelete a user\x1a\x1fRemoves a user from the account\x12\xaa\x03\n\x16SetUserNamespaceAccess\x12\x41.temporal.api.cloud.cloudservice.v1.SetUserNamespaceAccessRequest\x1a\x42.temporal.api.cloud.cloudservice.v1.SetUserNamespaceAccessResponse"\x88\x02\x82\xd3\xe4\x93\x02\x39"4/cloud/namespaces/{namespace}/users/{user_id}/access:\x01*\x92\x41\xc5\x01\n\x05Users\x12\x19Set user namespace access\x1a\x38\x43onfigures a user\'s permissions for a specific namespace"g\n#Namespace permissions documentation\x12@https://docs.temporal.io/cloud/users-namespace-level-permissions\x12\xb1\x02\n\x11GetAsyncOperation\x12<.temporal.api.cloud.cloudservice.v1.GetAsyncOperationRequest\x1a=.temporal.api.cloud.cloudservice.v1.GetAsyncOperationResponse"\x9e\x01\x82\xd3\xe4\x93\x02(\x12&/cloud/operations/{async_operation_id}\x92\x41m\n\nOperations\x12\x1aGet async operation status\x1a\x43Returns the current status and details of an asynchronous operation\x12\xc6\x02\n\x0f\x43reateNamespace\x12:.temporal.api.cloud.cloudservice.v1.CreateNamespaceRequest\x1a;.temporal.api.cloud.cloudservice.v1.CreateNamespaceResponse"\xb9\x01\x82\xd3\xe4\x93\x02\x16"\x11/cloud/namespaces:\x01*\x92\x41\x99\x01\n\nNamespaces\x12\x12\x43reate a namespace\x1a&Creates a new namespace in the account"O\n"Namespace management documentation\x12)https://docs.temporal.io/cloud/namespaces\x12\xc7\x02\n\rGetNamespaces\x12\x38.temporal.api.cloud.cloudservice.v1.GetNamespacesRequest\x1a\x39.temporal.api.cloud.cloudservice.v1.GetNamespacesResponse"\xc0\x01\x82\xd3\xe4\x93\x02\x13\x12\x11/cloud/namespaces\x92\x41\xa3\x01\n\nNamespaces\x12\x13List all namespaces\x1a/Returns a list of all namespaces in the account"O\n"Namespace management documentation\x12)https://docs.temporal.io/cloud/namespaces\x12\xda\x02\n\x0cGetNamespace\x12\x37.temporal.api.cloud.cloudservice.v1.GetNamespaceRequest\x1a\x38.temporal.api.cloud.cloudservice.v1.GetNamespaceResponse"\xd6\x01\x82\xd3\xe4\x93\x02\x1f\x12\x1d/cloud/namespaces/{namespace}\x92\x41\xad\x01\n\nNamespaces\x12\x15Get namespace details\x1a\x37Returns detailed information about a specific namespace"O\n"Namespace management documentation\x12)https://docs.temporal.io/cloud/namespaces\x12\xdb\x02\n\x0fUpdateNamespace\x12:.temporal.api.cloud.cloudservice.v1.UpdateNamespaceRequest\x1a;.temporal.api.cloud.cloudservice.v1.UpdateNamespaceResponse"\xce\x01\x82\xd3\xe4\x93\x02""\x1d/cloud/namespaces/{namespace}:\x01*\x92\x41\xa2\x01\n\nNamespaces\x12\x12Update a namespace\x1a/Updates configuration for an existing namespace"O\n"Namespace management documentation\x12)https://docs.temporal.io/cloud/namespaces\x12\xc7\x03\n\x1bRenameCustomSearchAttribute\x12\x46.temporal.api.cloud.cloudservice.v1.RenameCustomSearchAttributeRequest\x1aG.temporal.api.cloud.cloudservice.v1.RenameCustomSearchAttributeResponse"\x96\x02\x82\xd3\xe4\x93\x02\x41".temporal.api.cloud.cloudservice.v1.AddNamespaceRegionResponse"\xf0\x01\x88\x02\x01\x82\xd3\xe4\x93\x02-"(/cloud/namespaces/{namespace}/add-region:\x01*\x92\x41\xb6\x01\n\x11High Availability\x12\x15\x41\x64\x64 namespace replica\x1a+Adds a new replica to an existing namespace"]\n)High availability namespace documentation\x12\x30https://docs.temporal.io/cloud/high-availability\x12\x9e\x03\n\x15\x44\x65leteNamespaceRegion\x12@.temporal.api.cloud.cloudservice.v1.DeleteNamespaceRegionRequest\x1a\x41.temporal.api.cloud.cloudservice.v1.DeleteNamespaceRegionResponse"\xff\x01\x88\x02\x01\x82\xd3\xe4\x93\x02\x30*./cloud/namespaces/{namespace}/regions/{region}\x92\x41\xc2\x01\n\x11High Availability\x12\x18Remove namespace replica\x1a\x34Removes a replica from a high availability namespace"]\n)High availability namespace documentation\x12\x30https://docs.temporal.io/cloud/high-availability\x12\xa3\x02\n\nGetRegions\x12\x35.temporal.api.cloud.cloudservice.v1.GetRegionsRequest\x1a\x36.temporal.api.cloud.cloudservice.v1.GetRegionsResponse"\xa5\x01\x82\xd3\xe4\x93\x02\x10\x12\x0e/cloud/regions\x92\x41\x8b\x01\n\x07Regions\x12\x10List all regions\x1a-Returns a list of all available cloud regions"?\n\x15Regions documentation\x12&https://docs.temporal.io/cloud/regions\x12\xb2\x02\n\tGetRegion\x12\x34.temporal.api.cloud.cloudservice.v1.GetRegionRequest\x1a\x35.temporal.api.cloud.cloudservice.v1.GetRegionResponse"\xb7\x01\x82\xd3\xe4\x93\x02\x19\x12\x17/cloud/regions/{region}\x92\x41\x94\x01\n\x07Regions\x12\x12Get region details\x1a\x34Returns detailed information about a specific region"?\n\x15Regions documentation\x12&https://docs.temporal.io/cloud/regions\x12\xa8\x02\n\nGetApiKeys\x12\x35.temporal.api.cloud.cloudservice.v1.GetApiKeysRequest\x1a\x36.temporal.api.cloud.cloudservice.v1.GetApiKeysResponse"\xaa\x01\x82\xd3\xe4\x93\x02\x11\x12\x0f/cloud/api-keys\x92\x41\x8f\x01\n\x08\x41PI Keys\x12\x11List all API keys\x1a-Returns a list of all API keys in the account"A\n\x16\x41PI Keys documentation\x12\'https://docs.temporal.io/cloud/api-keys\x12\xb8\x02\n\tGetApiKey\x12\x34.temporal.api.cloud.cloudservice.v1.GetApiKeyRequest\x1a\x35.temporal.api.cloud.cloudservice.v1.GetApiKeyResponse"\xbd\x01\x82\xd3\xe4\x93\x02\x1a\x12\x18/cloud/api-keys/{key_id}\x92\x41\x99\x01\n\x08\x41PI Keys\x12\x13Get API key details\x1a\x35Returns detailed information about a specific API key"A\n\x16\x41PI Keys documentation\x12\'https://docs.temporal.io/cloud/api-keys\x12\xb1\x02\n\x0c\x43reateApiKey\x12\x37.temporal.api.cloud.cloudservice.v1.CreateApiKeyRequest\x1a\x38.temporal.api.cloud.cloudservice.v1.CreateApiKeyResponse"\xad\x01\x82\xd3\xe4\x93\x02\x14"\x0f/cloud/api-keys:\x01*\x92\x41\x8f\x01\n\x08\x41PI Keys\x12\x11\x43reate an API key\x1a-Creates a new API key for programmatic access"A\n\x16\x41PI Keys documentation\x12\'https://docs.temporal.io/cloud/api-keys\x12\xb5\x02\n\x0cUpdateApiKey\x12\x37.temporal.api.cloud.cloudservice.v1.UpdateApiKeyRequest\x1a\x38.temporal.api.cloud.cloudservice.v1.UpdateApiKeyResponse"\xb1\x01\x82\xd3\xe4\x93\x02\x1d"\x18/cloud/api-keys/{key_id}:\x01*\x92\x41\x8a\x01\n\x08\x41PI Keys\x12\x11Update an API key\x1a(Updates an existing API key\'s properties"A\n\x16\x41PI Keys documentation\x12\'https://docs.temporal.io/cloud/api-keys\x12\xa8\x02\n\x0c\x44\x65leteApiKey\x12\x37.temporal.api.cloud.cloudservice.v1.DeleteApiKeyRequest\x1a\x38.temporal.api.cloud.cloudservice.v1.DeleteApiKeyResponse"\xa4\x01\x82\xd3\xe4\x93\x02\x1a*\x18/cloud/api-keys/{key_id}\x92\x41\x80\x01\n\x08\x41PI Keys\x12\x11\x44\x65lete an API key\x1a\x1eRevokes and deletes an API key"A\n\x16\x41PI Keys documentation\x12\'https://docs.temporal.io/cloud/api-keys\x12\xc3\x02\n\x11GetNexusEndpoints\x12<.temporal.api.cloud.cloudservice.v1.GetNexusEndpointsRequest\x1a=.temporal.api.cloud.cloudservice.v1.GetNexusEndpointsResponse"\xb0\x01\x82\xd3\xe4\x93\x02\x18\x12\x16/cloud/nexus/endpoints\x92\x41\x8e\x01\n\x05Nexus\x12\x18List all Nexus endpoints\x1a\x34Returns a list of all Nexus endpoints in the account"5\n\x13Nexus documentation\x12\x1ehttps://docs.temporal.io/nexus\x12\xd8\x02\n\x10GetNexusEndpoint\x12;.temporal.api.cloud.cloudservice.v1.GetNexusEndpointRequest\x1a<.temporal.api.cloud.cloudservice.v1.GetNexusEndpointResponse"\xc8\x01\x82\xd3\xe4\x93\x02&\x12$/cloud/nexus/endpoints/{endpoint_id}\x92\x41\x98\x01\n\x05Nexus\x12\x1aGet Nexus endpoint details\x1a.temporal.api.cloud.cloudservice.v1.CreateNexusEndpointRequest\x1a?.temporal.api.cloud.cloudservice.v1.CreateNexusEndpointResponse"\xbc\x01\x82\xd3\xe4\x93\x02\x1b"\x16/cloud/nexus/endpoints:\x01*\x92\x41\x97\x01\n\x05Nexus\x12\x17\x43reate a Nexus endpoint\x1a>Creates a new Nexus endpoint for cross-namespace communication"5\n\x13Nexus documentation\x12\x1ehttps://docs.temporal.io/nexus\x12\xd7\x02\n\x13UpdateNexusEndpoint\x12>.temporal.api.cloud.cloudservice.v1.UpdateNexusEndpointRequest\x1a?.temporal.api.cloud.cloudservice.v1.UpdateNexusEndpointResponse"\xbe\x01\x82\xd3\xe4\x93\x02)"$/cloud/nexus/endpoints/{endpoint_id}:\x01*\x92\x41\x8b\x01\n\x05Nexus\x12\x17Update a Nexus endpoint\x1a\x32Updates an existing Nexus endpoint\'s configuration"5\n\x13Nexus documentation\x12\x1ehttps://docs.temporal.io/nexus\x12\xcb\x02\n\x13\x44\x65leteNexusEndpoint\x12>.temporal.api.cloud.cloudservice.v1.DeleteNexusEndpointRequest\x1a?.temporal.api.cloud.cloudservice.v1.DeleteNexusEndpointResponse"\xb2\x01\x82\xd3\xe4\x93\x02&*$/cloud/nexus/endpoints/{endpoint_id}\x92\x41\x82\x01\n\x05Nexus\x12\x17\x44\x65lete a Nexus endpoint\x1a)Removes a Nexus endpoint from the account"5\n\x13Nexus documentation\x12\x1ehttps://docs.temporal.io/nexus\x12\xcc\x02\n\rGetUserGroups\x12\x38.temporal.api.cloud.cloudservice.v1.GetUserGroupsRequest\x1a\x39.temporal.api.cloud.cloudservice.v1.GetUserGroupsResponse"\xc5\x01\x82\xd3\xe4\x93\x02\x14\x12\x12/cloud/user-groups\x92\x41\xa7\x01\n\x06Groups\x12\x14List all user groups\x1a\x30Returns a list of all user groups in the account"U\n\x19User groups documentation\x12\x38https://docs.temporal.io/cloud/users-account-level-roles\x12\xd0\x02\n\x0cGetUserGroup\x12\x37.temporal.api.cloud.cloudservice.v1.GetUserGroupRequest\x1a\x38.temporal.api.cloud.cloudservice.v1.GetUserGroupResponse"\xcc\x01\x82\xd3\xe4\x93\x02\x1f\x12\x1d/cloud/user-groups/{group_id}\x92\x41\xa3\x01\n\x06Groups\x12\x16Get user group details\x1a\x38Returns detailed information about a specific user group"G\n\x19User groups documentation\x12*https://docs.temporal.io/cloud/user-groups\x12\xc7\x02\n\x0f\x43reateUserGroup\x12:.temporal.api.cloud.cloudservice.v1.CreateUserGroupRequest\x1a;.temporal.api.cloud.cloudservice.v1.CreateUserGroupResponse"\xba\x01\x82\xd3\xe4\x93\x02\x17"\x12/cloud/user-groups:\x01*\x92\x41\x99\x01\n\x06Groups\x12\x13\x43reate a user group\x1a\x31\x43reates a new user group for managing permissions"G\n\x19User groups documentation\x12*https://docs.temporal.io/cloud/user-groups\x12\xcc\x02\n\x0fUpdateUserGroup\x12:.temporal.api.cloud.cloudservice.v1.UpdateUserGroupRequest\x1a;.temporal.api.cloud.cloudservice.v1.UpdateUserGroupResponse"\xbf\x01\x82\xd3\xe4\x93\x02""\x1d/cloud/user-groups/{group_id}:\x01*\x92\x41\x93\x01\n\x06Groups\x12\x13Update a user group\x1a+Updates an existing user group\'s properties"G\n\x19User groups documentation\x12*https://docs.temporal.io/cloud/user-groups\x12\xc3\x02\n\x0f\x44\x65leteUserGroup\x12:.temporal.api.cloud.cloudservice.v1.DeleteUserGroupRequest\x1a;.temporal.api.cloud.cloudservice.v1.DeleteUserGroupResponse"\xb6\x01\x82\xd3\xe4\x93\x02\x1f*\x1d/cloud/user-groups/{group_id}\x92\x41\x8d\x01\n\x06Groups\x12\x13\x44\x65lete a user group\x1a%Removes a user group from the account"G\n\x19User groups documentation\x12*https://docs.temporal.io/cloud/user-groups\x12\xad\x03\n\x1bSetUserGroupNamespaceAccess\x12\x46.temporal.api.cloud.cloudservice.v1.SetUserGroupNamespaceAccessRequest\x1aG.temporal.api.cloud.cloudservice.v1.SetUserGroupNamespaceAccessResponse"\xfc\x01\x82\xd3\xe4\x93\x02@";/cloud/namespaces/{namespace}/user-groups/{group_id}/access:\x01*\x92\x41\xb2\x01\n\x06Groups\x12\x1fSet user group namespace access\x1a>Configures a user group\'s permissions for a specific namespace"G\n\x19User groups documentation\x12*https://docs.temporal.io/cloud/user-groups\x12\xdf\x02\n\x12\x41\x64\x64UserGroupMember\x12=.temporal.api.cloud.cloudservice.v1.AddUserGroupMemberRequest\x1a>.temporal.api.cloud.cloudservice.v1.AddUserGroupMemberResponse"\xc9\x01\x82\xd3\xe4\x93\x02*"%/cloud/user-groups/{group_id}/members:\x01*\x92\x41\x95\x01\n\x06Groups\x12\x11\x41\x64\x64 user to group\x1a/Adds a user to a user group (Cloud groups only)"G\n\x19User groups documentation\x12*https://docs.temporal.io/cloud/user-groups\x12\xf8\x02\n\x15RemoveUserGroupMember\x12@.temporal.api.cloud.cloudservice.v1.RemoveUserGroupMemberRequest\x1a\x41.temporal.api.cloud.cloudservice.v1.RemoveUserGroupMemberResponse"\xd9\x01\x82\xd3\xe4\x93\x02\x30"+/cloud/user-groups/{group_id}/remove-member:\x01*\x92\x41\x9f\x01\n\x06Groups\x12\x16Remove user from group\x1a\x34Removes a user from a user group (Cloud groups only)"G\n\x19User groups documentation\x12*https://docs.temporal.io/cloud/user-groups\x12\xdf\x02\n\x13GetUserGroupMembers\x12>.temporal.api.cloud.cloudservice.v1.GetUserGroupMembersRequest\x1a?.temporal.api.cloud.cloudservice.v1.GetUserGroupMembersResponse"\xc6\x01\x82\xd3\xe4\x93\x02\'\x12%/cloud/user-groups/{group_id}/members\x92\x41\x95\x01\n\x06Groups\x12\x15List users in a group\x1a+Returns a list of all users in a user group"G\n\x19User groups documentation\x12*https://docs.temporal.io/cloud/user-groups\x12\xf5\x02\n\x14\x43reateServiceAccount\x12?.temporal.api.cloud.cloudservice.v1.CreateServiceAccountRequest\x1a@.temporal.api.cloud.cloudservice.v1.CreateServiceAccountResponse"\xd9\x01\x82\xd3\xe4\x93\x02\x1c"\x17/cloud/service-accounts:\x01*\x92\x41\xb3\x01\n\x10Service Accounts\x12\x18\x43reate a service account\x1a\x32\x43reates a new service account for automated access"Q\n\x1eService Accounts documentation\x12/https://docs.temporal.io/cloud/service-accounts\x12\x8c\x03\n\x11GetServiceAccount\x12<.temporal.api.cloud.cloudservice.v1.GetServiceAccountRequest\x1a=.temporal.api.cloud.cloudservice.v1.GetServiceAccountResponse"\xf9\x01\x82\xd3\xe4\x93\x02.\x12,/cloud/service-accounts/{service_account_id}\x92\x41\xc1\x01\n\x10Service Accounts\x12\x1bGet service account details\x1a=Returns detailed information about a specific service account"Q\n\x1eService Accounts documentation\x12/https://docs.temporal.io/cloud/service-accounts\x12\xf0\x02\n\x12GetServiceAccounts\x12=.temporal.api.cloud.cloudservice.v1.GetServiceAccountsRequest\x1a>.temporal.api.cloud.cloudservice.v1.GetServiceAccountsResponse"\xda\x01\x82\xd3\xe4\x93\x02\x19\x12\x17/cloud/service-accounts\x92\x41\xb7\x01\n\x10Service Accounts\x12\x19List all service accounts\x1a\x35Returns a list of all service accounts in the account"Q\n\x1eService Accounts documentation\x12/https://docs.temporal.io/cloud/service-accounts\x12\x88\x03\n\x14UpdateServiceAccount\x12?.temporal.api.cloud.cloudservice.v1.UpdateServiceAccountRequest\x1a@.temporal.api.cloud.cloudservice.v1.UpdateServiceAccountResponse"\xec\x01\x82\xd3\xe4\x93\x02\x31",/cloud/service-accounts/{service_account_id}:\x01*\x92\x41\xb1\x01\n\x10Service Accounts\x12\x18Update a service account\x1a\x30Updates an existing service account\'s properties"Q\n\x1eService Accounts documentation\x12/https://docs.temporal.io/cloud/service-accounts\x12\xe9\x03\n SetServiceAccountNamespaceAccess\x12K.temporal.api.cloud.cloudservice.v1.SetServiceAccountNamespaceAccessRequest\x1aL.temporal.api.cloud.cloudservice.v1.SetServiceAccountNamespaceAccessResponse"\xa9\x02\x82\xd3\xe4\x93\x02O"J/cloud/namespaces/{namespace}/service-accounts/{service_account_id}/access:\x01*\x92\x41\xd0\x01\n\x10Service Accounts\x12$Set service account namespace access\x1a\x43\x43onfigures a service account\'s permissions for a specific namespace"Q\n\x1eService Accounts documentation\x12/https://docs.temporal.io/cloud/service-accounts\x12\xff\x02\n\x14\x44\x65leteServiceAccount\x12?.temporal.api.cloud.cloudservice.v1.DeleteServiceAccountRequest\x1a@.temporal.api.cloud.cloudservice.v1.DeleteServiceAccountResponse"\xe3\x01\x82\xd3\xe4\x93\x02.*,/cloud/service-accounts/{service_account_id}\x92\x41\xab\x01\n\x10Service Accounts\x12\x18\x44\x65lete a service account\x1a*Removes a service account from the account"Q\n\x1eService Accounts documentation\x12/https://docs.temporal.io/cloud/service-accounts\x12\xcb\x01\n\x08GetUsage\x12\x33.temporal.api.cloud.cloudservice.v1.GetUsageRequest\x1a\x34.temporal.api.cloud.cloudservice.v1.GetUsageResponse"T\x82\xd3\xe4\x93\x02\x0e\x12\x0c/cloud/usage\x92\x41=\n\x07\x41\x63\x63ount\x12\x0eGet usage data\x1a Get usage data across namespacesX\x01\x12\xb0\x02\n\nGetAccount\x12\x35.temporal.api.cloud.cloudservice.v1.GetAccountRequest\x1a\x36.temporal.api.cloud.cloudservice.v1.GetAccountResponse"\xb2\x01\x82\xd3\xe4\x93\x02\x10\x12\x0e/cloud/account\x92\x41\x98\x01\n\x07\x41\x63\x63ount\x12\x13Get account details\x1a.Returns detailed information about the account"H\n\x15\x42illing documentation\x12/https://docs.temporal.io/cloud/billing-and-cost\x12\xbb\x02\n\rUpdateAccount\x12\x38.temporal.api.cloud.cloudservice.v1.UpdateAccountRequest\x1a\x39.temporal.api.cloud.cloudservice.v1.UpdateAccountResponse"\xb4\x01\x82\xd3\xe4\x93\x02\x13"\x0e/cloud/account:\x01*\x92\x41\x97\x01\n\x07\x41\x63\x63ount\x12\x16Update account details\x1a*Updates account configuration and settings"H\n\x15\x42illing documentation\x12/https://docs.temporal.io/cloud/billing-and-cost\x12\xf3\x02\n\x19\x43reateNamespaceExportSink\x12\x44.temporal.api.cloud.cloudservice.v1.CreateNamespaceExportSinkRequest\x1a\x45.temporal.api.cloud.cloudservice.v1.CreateNamespaceExportSinkResponse"\xc8\x01\x82\xd3\xe4\x93\x02/"*/cloud/namespaces/{namespace}/export-sinks:\x01*\x92\x41\x8f\x01\n\x06\x45xport\x12\x1a\x43reate history export sink\x1a*Creates a new workflow history export sink"=\n\x14\x45xport documentation\x12%https://docs.temporal.io/cloud/export\x12\x8c\x03\n\x16GetNamespaceExportSink\x12\x41.temporal.api.cloud.cloudservice.v1.GetNamespaceExportSinkRequest\x1a\x42.temporal.api.cloud.cloudservice.v1.GetNamespaceExportSinkResponse"\xea\x01\x82\xd3\xe4\x93\x02\x33\x12\x31/cloud/namespaces/{namespace}/export-sinks/{name}\x92\x41\xad\x01\n\x06\x45xport\x12\x18Get history sink details\x1aJReturns detailed information about a specific workflow history export sink"=\n\x14\x45xport documentation\x12%https://docs.temporal.io/cloud/export\x12\x82\x03\n\x17GetNamespaceExportSinks\x12\x42.temporal.api.cloud.cloudservice.v1.GetNamespaceExportSinksRequest\x1a\x43.temporal.api.cloud.cloudservice.v1.GetNamespaceExportSinksResponse"\xdd\x01\x82\xd3\xe4\x93\x02,\x12*/cloud/namespaces/{namespace}/export-sinks\x92\x41\xa7\x01\n\x06\x45xport\x12\x19List history export sinks\x1a\x43Returns a list of all workflow history export sinks for a namespace"=\n\x14\x45xport documentation\x12%https://docs.temporal.io/cloud/export\x12\x95\x03\n\x19UpdateNamespaceExportSink\x12\x44.temporal.api.cloud.cloudservice.v1.UpdateNamespaceExportSinkRequest\x1a\x45.temporal.api.cloud.cloudservice.v1.UpdateNamespaceExportSinkResponse"\xea\x01\x82\xd3\xe4\x93\x02;"6/cloud/namespaces/{namespace}/export-sinks/{spec.name}:\x01*\x92\x41\xa5\x01\n\x06\x45xport\x12\x1aUpdate history export sink\x1a@Updates an existing workflow history export sink\'s configuration"=\n\x14\x45xport documentation\x12%https://docs.temporal.io/cloud/export\x12\x84\x03\n\x19\x44\x65leteNamespaceExportSink\x12\x44.temporal.api.cloud.cloudservice.v1.DeleteNamespaceExportSinkRequest\x1a\x45.temporal.api.cloud.cloudservice.v1.DeleteNamespaceExportSinkResponse"\xd9\x01\x82\xd3\xe4\x93\x02\x33*1/cloud/namespaces/{namespace}/export-sinks/{name}\x92\x41\x9c\x01\n\x06\x45xport\x12\x1a\x44\x65lete history export sink\x1a\x37Removes a workflow history export sink from a namespace"=\n\x14\x45xport documentation\x12%https://docs.temporal.io/cloud/export\x12\xc9\x03\n\x1bValidateNamespaceExportSink\x12\x46.temporal.api.cloud.cloudservice.v1.ValidateNamespaceExportSinkRequest\x1aG.temporal.api.cloud.cloudservice.v1.ValidateNamespaceExportSinkResponse"\x98\x02\x82\xd3\xe4\x93\x02\x37"2/cloud/namespaces/{namespace}/export-sink-validate:\x01*\x92\x41\xd7\x01\n\x06\x45xport\x12*Validate history export sink configuration\x1a\x62Tests workflow history export sink configuration by delivering a test file to verify accessibility"=\n\x14\x45xport documentation\x12%https://docs.temporal.io/cloud/export\x12\xfc\x02\n\x13UpdateNamespaceTags\x12>.temporal.api.cloud.cloudservice.v1.UpdateNamespaceTagsRequest\x1a?.temporal.api.cloud.cloudservice.v1.UpdateNamespaceTagsResponse"\xe3\x01\x82\xd3\xe4\x93\x02.")/cloud/namespaces/{namespace}/update-tags:\x01*\x92\x41\xab\x01\n\nNamespaces\x12\x15Update namespace tags\x1a,Updates the tags associated with a namespace"X\n\x1bNamespace tag documentation\x12\x39https://docs.temporal.io/cloud/namespaces#tag-a-namespace\x12\xff\x02\n\x16\x43reateConnectivityRule\x12\x41.temporal.api.cloud.cloudservice.v1.CreateConnectivityRuleRequest\x1a\x42.temporal.api.cloud.cloudservice.v1.CreateConnectivityRuleResponse"\xdd\x01\x82\xd3\xe4\x93\x02\x1e"\x19/cloud/connectivity-rules:\x01*\x92\x41\xb5\x01\n\x12\x43onnectivity Rules\x12\x18\x43reate connectivity rule\x1a:Creates a new connectivity rule for network access control"I\n\x1a\x43onnectivity documentation\x12+https://docs.temporal.io/cloud/connectivity\x12\x94\x03\n\x13GetConnectivityRule\x12>.temporal.api.cloud.cloudservice.v1.GetConnectivityRuleRequest\x1a?.temporal.api.cloud.cloudservice.v1.GetConnectivityRuleResponse"\xfb\x01\x82\xd3\xe4\x93\x02\x32\x12\x30/cloud/connectivity-rules/{connectivity_rule_id}\x92\x41\xbf\x01\n\x12\x43onnectivity Rules\x12\x1dGet connectivity rule details\x1a?Returns detailed information about a specific connectivity rule"I\n\x1a\x43onnectivity documentation\x12+https://docs.temporal.io/cloud/connectivity\x12\xf6\x02\n\x14GetConnectivityRules\x12?.temporal.api.cloud.cloudservice.v1.GetConnectivityRulesRequest\x1a@.temporal.api.cloud.cloudservice.v1.GetConnectivityRulesResponse"\xda\x01\x82\xd3\xe4\x93\x02\x1b\x12\x19/cloud/connectivity-rules\x92\x41\xb5\x01\n\x12\x43onnectivity Rules\x12\x1bList all connectivity rules\x1a\x37Returns a list of all connectivity rules in the account"I\n\x1a\x43onnectivity documentation\x12+https://docs.temporal.io/cloud/connectivity\x12\x85\x03\n\x16\x44\x65leteConnectivityRule\x12\x41.temporal.api.cloud.cloudservice.v1.DeleteConnectivityRuleRequest\x1a\x42.temporal.api.cloud.cloudservice.v1.DeleteConnectivityRuleResponse"\xe3\x01\x82\xd3\xe4\x93\x02\x32*0/cloud/connectivity-rules/{connectivity_rule_id}\x92\x41\xa7\x01\n\x12\x43onnectivity Rules\x12\x18\x44\x65lete connectivity rule\x1a,Removes a connectivity rule from the account"I\n\x1a\x43onnectivity documentation\x12+https://docs.temporal.io/cloud/connectivity\x12\xe2\x02\n\x0cGetAuditLogs\x12\x37.temporal.api.cloud.cloudservice.v1.GetAuditLogsRequest\x1a\x38.temporal.api.cloud.cloudservice.v1.GetAuditLogsResponse"\xde\x01\x82\xd3\xe4\x93\x02\x13\x12\x11/cloud/audit-logs\x92\x41\xc1\x01\n\x07\x41\x63\x63ount\x12\x0eGet audit logs\x1aYReturns a paginated list of audit logs for the account, optionally filtered by time range"K\n\x1b\x41udit logging documentation\x12,https://docs.temporal.io/cloud/audit-logging\x12\xb4\x04\n\x1bValidateAccountAuditLogSink\x12\x46.temporal.api.cloud.cloudservice.v1.ValidateAccountAuditLogSinkRequest\x1aG.temporal.api.cloud.cloudservice.v1.ValidateAccountAuditLogSinkResponse"\x83\x03\x82\xd3\xe4\x93\x02#"\x1e/cloud/audit-log-sink-validate:\x01*\x92\x41\xd6\x02\n\x07\x41\x63\x63ount\x12\x17Validate audit log sink\x1a\xe4\x01Validate customer audit log sink is accessible from Temporal\'s workflow by delivering an empty file to the specified sink. The operation verifies that the sink is correctly configured, accessible and ready to receive audit logs."K\n\x1b\x41udit logging documentation\x12,https://docs.temporal.io/cloud/audit-logging\x12\xf4\x02\n\x19\x43reateAccountAuditLogSink\x12\x44.temporal.api.cloud.cloudservice.v1.CreateAccountAuditLogSinkRequest\x1a\x45.temporal.api.cloud.cloudservice.v1.CreateAccountAuditLogSinkResponse"\xc9\x01\x82\xd3\xe4\x93\x02\x1b"\x16/cloud/audit-log-sinks:\x01*\x92\x41\xa4\x01\n\x07\x41\x63\x63ount\x12\x15\x43reate audit log sink\x1a\x35\x43reates a new audit log sink for exporting audit logs"K\n\x1b\x41udit logging documentation\x12,https://docs.temporal.io/cloud/audit-logging\x12\xfb\x02\n\x16GetAccountAuditLogSink\x12\x41.temporal.api.cloud.cloudservice.v1.GetAccountAuditLogSinkRequest\x1a\x42.temporal.api.cloud.cloudservice.v1.GetAccountAuditLogSinkResponse"\xd9\x01\x82\xd3\xe4\x93\x02\x1f\x12\x1d/cloud/audit-log-sinks/{name}\x92\x41\xb0\x01\n\x07\x41\x63\x63ount\x12\x1aGet audit log sink details\x1a.temporal.api.cloud.cloudservice.v1.CreateBillingReportRequest\x1a?.temporal.api.cloud.cloudservice.v1.CreateBillingReportResponse"\xc1\x01\x82\xd3\xe4\x93\x02\x1b"\x16/cloud/billing-reports:\x01*\x92\x41\x9c\x01\n\x07\x42illing\x12\x17\x43reate a billing report\x1a(Creates a billing report for the account"N\n\x1c\x42illing report documentation\x12.https://docs.temporal.io/cloud/billing-reports\x12\xe6\x02\n\x10GetBillingReport\x12;.temporal.api.cloud.cloudservice.v1.GetBillingReportRequest\x1a<.temporal.api.cloud.cloudservice.v1.GetBillingReportResponse"\xd6\x01\x82\xd3\xe4\x93\x02,\x12*/cloud/billing-reports/{billing_report_id}\x92\x41\xa0\x01\n\x07\x42illing\x12\x14Get a billing report\x1a/Gets an existing billing report for the account"N\n\x1c\x42illing report documentation\x12.https://docs.temporal.io/cloud/billing-reports\x12\xc4\x02\n\x0eGetCustomRoles\x12\x39.temporal.api.cloud.cloudservice.v1.GetCustomRolesRequest\x1a:.temporal.api.cloud.cloudservice.v1.GetCustomRolesResponse"\xba\x01\x82\xd3\xe4\x93\x02\x15\x12\x13/cloud/custom-roles\x92\x41\x9b\x01\n\x0c\x43ustom Roles\x12\x11List custom roles\x1a-Returns a list of custom roles in the account"I\n\x1a\x43ustom roles documentation\x12+https://docs.temporal.io/cloud/custom-roles\x12\xcc\x02\n\rGetCustomRole\x12\x38.temporal.api.cloud.cloudservice.v1.GetCustomRoleRequest\x1a\x39.temporal.api.cloud.cloudservice.v1.GetCustomRoleResponse"\xc5\x01\x82\xd3\xe4\x93\x02\x1f\x12\x1d/cloud/custom-roles/{role_id}\x92\x41\x9c\x01\n\x0c\x43ustom Roles\x12\x15Get custom role by ID\x1a*Returns details for a specific custom role"I\n\x1a\x43ustom roles documentation\x12+https://docs.temporal.io/cloud/custom-roles\x12\xcb\x02\n\x10\x43reateCustomRole\x12;.temporal.api.cloud.cloudservice.v1.CreateCustomRoleRequest\x1a<.temporal.api.cloud.cloudservice.v1.CreateCustomRoleResponse"\xbb\x01\x82\xd3\xe4\x93\x02\x18"\x13/cloud/custom-roles:\x01*\x92\x41\x99\x01\n\x0c\x43ustom Roles\x12\x14\x43reate a custom role\x1a(Creates a new custom role in the account"I\n\x1a\x43ustom roles documentation\x12+https://docs.temporal.io/cloud/custom-roles\x12\xcc\x02\n\x10UpdateCustomRole\x12;.temporal.api.cloud.cloudservice.v1.UpdateCustomRoleRequest\x1a<.temporal.api.cloud.cloudservice.v1.UpdateCustomRoleResponse"\xbc\x01\x82\xd3\xe4\x93\x02""\x1d/cloud/custom-roles/{role_id}:\x01*\x92\x41\x90\x01\n\x0c\x43ustom Roles\x12\x14Update a custom role\x1a\x1fUpdates an existing custom role"I\n\x1a\x43ustom roles documentation\x12+https://docs.temporal.io/cloud/custom-roles\x12\xd0\x02\n\x10\x44\x65leteCustomRole\x12;.temporal.api.cloud.cloudservice.v1.DeleteCustomRoleRequest\x1a<.temporal.api.cloud.cloudservice.v1.DeleteCustomRoleResponse"\xc0\x01\x82\xd3\xe4\x93\x02\x1f*\x1d/cloud/custom-roles/{role_id}\x92\x41\x97\x01\n\x0c\x43ustom Roles\x12\x14\x44\x65lete a custom role\x1a&Deletes a custom role from the account"I\n\x1a\x43ustom roles documentation\x12+https://docs.temporal.io/cloud/custom-roles\x12\xb9\x03\n\x1bGetUserNamespaceAssignments\x12\x46.temporal.api.cloud.cloudservice.v1.GetUserNamespaceAssignmentsRequest\x1aG.temporal.api.cloud.cloudservice.v1.GetUserNamespaceAssignmentsResponse"\x88\x02\x82\xd3\xe4\x93\x02\x30\x12./cloud/namespaces/{namespace}/user-assignments\x92\x41\xce\x01\n\x05Users\x12$Get users with access to a namespace\x1a\x62Returns the users that have access to the namespace, including each user\'s namespace-level access.";\n\x13Users documentation\x12$https://docs.temporal.io/cloud/users\x12\xa5\x04\n%GetServiceAccountNamespaceAssignments\x12P.temporal.api.cloud.cloudservice.v1.GetServiceAccountNamespaceAssignmentsRequest\x1aQ.temporal.api.cloud.cloudservice.v1.GetServiceAccountNamespaceAssignmentsResponse"\xd6\x02\x82\xd3\xe4\x93\x02;\x12\x39/cloud/namespaces/{namespace}/service-account-assignments\x92\x41\x91\x02\n\x10Service Accounts\x12\x30List service accounts with access to a namespace\x1axReturns the service accounts that have access to the namespace, including each service account\'s namespace-level access."Q\n\x1eService Accounts documentation\x12/https://docs.temporal.io/cloud/service-accounts\x12\xe9\x03\n GetUserGroupNamespaceAssignments\x12K.temporal.api.cloud.cloudservice.v1.GetUserGroupNamespaceAssignmentsRequest\x1aL.temporal.api.cloud.cloudservice.v1.GetUserGroupNamespaceAssignmentsResponse"\xa9\x02\x82\xd3\xe4\x93\x02\x36\x12\x34/cloud/namespaces/{namespace}/user-group-assignments\x92\x41\xe9\x01\n\x06Groups\x12+List user groups with access to a namespace\x1aiReturns the user groups that have access to the namespace, including each group\'s namespace-level access."G\n\x19User groups documentation\x12*https://docs.temporal.io/cloud/user-groupsB\xc1\x15\n%io.temporal.api.cloud.cloudservice.v1B\x0cServiceProtoP\x01Z5go.temporal.io/api/cloud/cloudservice/v1;cloudservice\xaa\x02$Temporalio.Api.Cloud.CloudService.V1\xea\x02(Temporalio::Api::Cloud::CloudService::V1\x92\x41\xfd\x13\x12\xe0\r\n\x16Temporal Cloud Ops API\x12\x96\x0cProgrammatic access to manage Temporal Cloud control plane resources including namespaces, users, service accounts, and more.\n\n## Authentication\n\nAll API requests require authentication using an API Key. Include your API key in the `Authorization` header using the Bearer scheme:\n\n```\nAuthorization: Bearer YOUR_API_KEY\n```\n\nAPI keys can be created and managed through the [API Keys endpoints](#tag/API-Keys) or via the Temporal Cloud UI. For more information, see [API Keys Documentation](https://docs.temporal.io/cloud/api-keys).\n\n## Authorization\n\nThe API uses Role-Based Access Control (RBAC) to manage permissions. Each operation requires specific role-based permissions in addition to a valid API key.\n\n### Account-Level Roles\n\n- **Account Owner** - Full account administration access\n- **Account Admin** - Manage namespaces, users, and service accounts \n- **Account Developer** - Create namespaces and manage Nexus endpoints\n- **Finance Admin** - View usage and billing information\n- **Account Read** - Read-only access to account resources\n\n### Namespace-Level Roles\n\n- **Namespace Admin** - Full access to namespace configuration and data\n- **Namespace Write** - Execute workflows and modify workflow data\n- **Namespace Read** - Read-only access to namespace data\n\nNamespace-level permissions are scoped to specific namespaces. A user or service account may have different permission levels across different namespaces.\n\nFor detailed information about roles and permissions, see [Access Control Documentation](https://docs.temporal.io/cloud/users).2\x03\x31.0:\xa7\x01\n\x06x-logo\x12\x9c\x01*\x99\x01\n\x96\x01\n\x03url\x12\x8e\x01\x1a\x8b\x01https://images.ctfassets.net/0uuz8ydxyd9p/4YGUnEoCaH9SyoUDhlJkau/e1600205d17eeee3033d926ef06664a9/Temporal_LogoLockup_Horizontal_dark_1.svgj.\n\nNamespaces\x12 Manage Temporal Cloud namespacesj0\n\x05Users\x12\'Manage users and their namespace accessjF\n\x10Service Accounts\x12\x32Manage service accounts and their namespace accessj.\n\x08\x41PI Keys\x12"Manage API keys for authenticationj1\n\x06Groups\x12\'Manage user groups and group membershipj\x1f\n\x05Nexus\x12\x16Manage Nexus endpointsj\x7f\n\x11High Availability\x12jManage high availability (multi-region, multi-cloud, and same-region replication) namespace configurationsj7\n\x06\x45xport\x12-Manage workflow history export configurationsj7\n\x12\x43onnectivity Rules\x12!Manage network connectivity rulesj"\n\x07Regions\x12\x17Query available regionsj,\n\x07\x41\x63\x63ount\x12!Manage account settings and usagej9\n\x0c\x43ustom Roles\x12)Manage custom roles and their permissionsj*\n\nOperations\x12\x1cQuery async operation statusr>\n\x1cTemporal Cloud Documentation\x12\x1ehttps://docs.temporal.io/cloudb\x06proto3' ) _CLOUDSERVICE = DESCRIPTOR.services_by_name["CloudService"] if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None - DESCRIPTOR._serialized_options = b"\n%io.temporal.api.cloud.cloudservice.v1B\014ServiceProtoP\001Z5go.temporal.io/api/cloud/cloudservice/v1;cloudservice\252\002$Temporalio.Api.Cloud.CloudService.V1\352\002(Temporalio::Api::Cloud::CloudService::V1" + DESCRIPTOR._serialized_options = b"\n%io.temporal.api.cloud.cloudservice.v1B\014ServiceProtoP\001Z5go.temporal.io/api/cloud/cloudservice/v1;cloudservice\252\002$Temporalio.Api.Cloud.CloudService.V1\352\002(Temporalio::Api::Cloud::CloudService::V1\222A\375\023\022\340\r\n\026Temporal Cloud Ops API\022\226\014Programmatic access to manage Temporal Cloud control plane resources including namespaces, users, service accounts, and more.\n\n## Authentication\n\nAll API requests require authentication using an API Key. Include your API key in the `Authorization` header using the Bearer scheme:\n\n```\nAuthorization: Bearer YOUR_API_KEY\n```\n\nAPI keys can be created and managed through the [API Keys endpoints](#tag/API-Keys) or via the Temporal Cloud UI. For more information, see [API Keys Documentation](https://docs.temporal.io/cloud/api-keys).\n\n## Authorization\n\nThe API uses Role-Based Access Control (RBAC) to manage permissions. Each operation requires specific role-based permissions in addition to a valid API key.\n\n### Account-Level Roles\n\n- **Account Owner** - Full account administration access\n- **Account Admin** - Manage namespaces, users, and service accounts \n- **Account Developer** - Create namespaces and manage Nexus endpoints\n- **Finance Admin** - View usage and billing information\n- **Account Read** - Read-only access to account resources\n\n### Namespace-Level Roles\n\n- **Namespace Admin** - Full access to namespace configuration and data\n- **Namespace Write** - Execute workflows and modify workflow data\n- **Namespace Read** - Read-only access to namespace data\n\nNamespace-level permissions are scoped to specific namespaces. A user or service account may have different permission levels across different namespaces.\n\nFor detailed information about roles and permissions, see [Access Control Documentation](https://docs.temporal.io/cloud/users).2\0031.0:\247\001\n\006x-logo\022\234\001*\231\001\n\226\001\n\003url\022\216\001\032\213\001https://images.ctfassets.net/0uuz8ydxyd9p/4YGUnEoCaH9SyoUDhlJkau/e1600205d17eeee3033d926ef06664a9/Temporal_LogoLockup_Horizontal_dark_1.svgj.\n\nNamespaces\022 Manage Temporal Cloud namespacesj0\n\005Users\022'Manage users and their namespace accessjF\n\020Service Accounts\0222Manage service accounts and their namespace accessj.\n\010API Keys\022\"Manage API keys for authenticationj1\n\006Groups\022'Manage user groups and group membershipj\037\n\005Nexus\022\026Manage Nexus endpointsj\177\n\021High Availability\022jManage high availability (multi-region, multi-cloud, and same-region replication) namespace configurationsj7\n\006Export\022-Manage workflow history export configurationsj7\n\022Connectivity Rules\022!Manage network connectivity rulesj\"\n\007Regions\022\027Query available regionsj,\n\007Account\022!Manage account settings and usagej9\n\014Custom Roles\022)Manage custom roles and their permissionsj*\n\nOperations\022\034Query async operation statusr>\n\034Temporal Cloud Documentation\022\036https://docs.temporal.io/cloud" + _CLOUDSERVICE.methods_by_name["GetCurrentIdentity"]._options = None + _CLOUDSERVICE.methods_by_name[ + "GetCurrentIdentity" + ]._serialized_options = b"\202\323\344\223\002\031\022\027/cloud/current-identity\222Ax\n\007Account\022\024Get current identity\032WReturns information about the currently authenticated user or service account principal" _CLOUDSERVICE.methods_by_name["GetUsers"]._options = None _CLOUDSERVICE.methods_by_name[ "GetUsers" - ]._serialized_options = b"\202\323\344\223\002\016\022\014/cloud/users" + ]._serialized_options = b'\202\323\344\223\002\016\022\014/cloud/users\222A\225\001\n\005Users\022\016List all users\032*Returns a list of all users in the account"E\n\035User management documentation\022$https://docs.temporal.io/cloud/users*\tlistUsers' _CLOUDSERVICE.methods_by_name["GetUser"]._options = None _CLOUDSERVICE.methods_by_name[ "GetUser" - ]._serialized_options = b"\202\323\344\223\002\030\022\026/cloud/users/{user_id}" + ]._serialized_options = b'\202\323\344\223\002\030\022\026/cloud/users/{user_id}\222A\205\001\n\005Users\022\016Get user by ID\032%Takes a user ID, returns user details"E\n\035User management documentation\022$https://docs.temporal.io/cloud/users' _CLOUDSERVICE.methods_by_name["CreateUser"]._options = None _CLOUDSERVICE.methods_by_name[ "CreateUser" - ]._serialized_options = b'\202\323\344\223\002\021"\014/cloud/users:\001*' + ]._serialized_options = b'\202\323\344\223\002\021"\014/cloud/users:\001*\222A9\n\005Users\022\rCreate a user\032!Creates a new user in the account' _CLOUDSERVICE.methods_by_name["UpdateUser"]._options = None _CLOUDSERVICE.methods_by_name[ "UpdateUser" - ]._serialized_options = b'\202\323\344\223\002\033"\026/cloud/users/{user_id}:\001*' + ]._serialized_options = b'\202\323\344\223\002\033"\026/cloud/users/{user_id}:\001*\222A:\n\005Users\022\rUpdate a user\032"Updates an existing user\'s details' _CLOUDSERVICE.methods_by_name["DeleteUser"]._options = None _CLOUDSERVICE.methods_by_name[ "DeleteUser" - ]._serialized_options = b"\202\323\344\223\002\030*\026/cloud/users/{user_id}" + ]._serialized_options = b"\202\323\344\223\002\030*\026/cloud/users/{user_id}\222A7\n\005Users\022\rDelete a user\032\037Removes a user from the account" _CLOUDSERVICE.methods_by_name["SetUserNamespaceAccess"]._options = None _CLOUDSERVICE.methods_by_name[ "SetUserNamespaceAccess" - ]._serialized_options = b'\202\323\344\223\0029"4/cloud/namespaces/{namespace}/users/{user_id}/access:\001*' + ]._serialized_options = b'\202\323\344\223\0029"4/cloud/namespaces/{namespace}/users/{user_id}/access:\001*\222A\305\001\n\005Users\022\031Set user namespace access\0328Configures a user\'s permissions for a specific namespace"g\n#Namespace permissions documentation\022@https://docs.temporal.io/cloud/users-namespace-level-permissions' _CLOUDSERVICE.methods_by_name["GetAsyncOperation"]._options = None _CLOUDSERVICE.methods_by_name[ "GetAsyncOperation" - ]._serialized_options = ( - b"\202\323\344\223\002(\022&/cloud/operations/{async_operation_id}" - ) + ]._serialized_options = b"\202\323\344\223\002(\022&/cloud/operations/{async_operation_id}\222Am\n\nOperations\022\032Get async operation status\032CReturns the current status and details of an asynchronous operation" _CLOUDSERVICE.methods_by_name["CreateNamespace"]._options = None _CLOUDSERVICE.methods_by_name[ "CreateNamespace" - ]._serialized_options = b'\202\323\344\223\002\026"\021/cloud/namespaces:\001*' + ]._serialized_options = b'\202\323\344\223\002\026"\021/cloud/namespaces:\001*\222A\231\001\n\nNamespaces\022\022Create a namespace\032&Creates a new namespace in the account"O\n"Namespace management documentation\022)https://docs.temporal.io/cloud/namespaces' _CLOUDSERVICE.methods_by_name["GetNamespaces"]._options = None _CLOUDSERVICE.methods_by_name[ "GetNamespaces" - ]._serialized_options = b"\202\323\344\223\002\023\022\021/cloud/namespaces" + ]._serialized_options = b'\202\323\344\223\002\023\022\021/cloud/namespaces\222A\243\001\n\nNamespaces\022\023List all namespaces\032/Returns a list of all namespaces in the account"O\n"Namespace management documentation\022)https://docs.temporal.io/cloud/namespaces' _CLOUDSERVICE.methods_by_name["GetNamespace"]._options = None _CLOUDSERVICE.methods_by_name[ "GetNamespace" - ]._serialized_options = ( - b"\202\323\344\223\002\037\022\035/cloud/namespaces/{namespace}" - ) + ]._serialized_options = b'\202\323\344\223\002\037\022\035/cloud/namespaces/{namespace}\222A\255\001\n\nNamespaces\022\025Get namespace details\0327Returns detailed information about a specific namespace"O\n"Namespace management documentation\022)https://docs.temporal.io/cloud/namespaces' _CLOUDSERVICE.methods_by_name["UpdateNamespace"]._options = None _CLOUDSERVICE.methods_by_name[ "UpdateNamespace" - ]._serialized_options = ( - b'\202\323\344\223\002""\035/cloud/namespaces/{namespace}:\001*' - ) + ]._serialized_options = b'\202\323\344\223\002""\035/cloud/namespaces/{namespace}:\001*\222A\242\001\n\nNamespaces\022\022Update a namespace\032/Updates configuration for an existing namespace"O\n"Namespace management documentation\022)https://docs.temporal.io/cloud/namespaces' _CLOUDSERVICE.methods_by_name["RenameCustomSearchAttribute"]._options = None _CLOUDSERVICE.methods_by_name[ "RenameCustomSearchAttribute" - ]._serialized_options = b'\202\323\344\223\002A"Creates a new Nexus endpoint for cross-namespace communication"5\n\023Nexus documentation\022\036https://docs.temporal.io/nexus' _CLOUDSERVICE.methods_by_name["UpdateNexusEndpoint"]._options = None _CLOUDSERVICE.methods_by_name[ "UpdateNexusEndpoint" - ]._serialized_options = ( - b'\202\323\344\223\002)"$/cloud/nexus/endpoints/{endpoint_id}:\001*' - ) + ]._serialized_options = b'\202\323\344\223\002)"$/cloud/nexus/endpoints/{endpoint_id}:\001*\222A\213\001\n\005Nexus\022\027Update a Nexus endpoint\0322Updates an existing Nexus endpoint\'s configuration"5\n\023Nexus documentation\022\036https://docs.temporal.io/nexus' _CLOUDSERVICE.methods_by_name["DeleteNexusEndpoint"]._options = None _CLOUDSERVICE.methods_by_name[ "DeleteNexusEndpoint" - ]._serialized_options = ( - b"\202\323\344\223\002&*$/cloud/nexus/endpoints/{endpoint_id}" - ) + ]._serialized_options = b'\202\323\344\223\002&*$/cloud/nexus/endpoints/{endpoint_id}\222A\202\001\n\005Nexus\022\027Delete a Nexus endpoint\032)Removes a Nexus endpoint from the account"5\n\023Nexus documentation\022\036https://docs.temporal.io/nexus' _CLOUDSERVICE.methods_by_name["GetUserGroups"]._options = None _CLOUDSERVICE.methods_by_name[ "GetUserGroups" - ]._serialized_options = b"\202\323\344\223\002\024\022\022/cloud/user-groups" + ]._serialized_options = b'\202\323\344\223\002\024\022\022/cloud/user-groups\222A\247\001\n\006Groups\022\024List all user groups\0320Returns a list of all user groups in the account"U\n\031User groups documentation\0228https://docs.temporal.io/cloud/users-account-level-roles' _CLOUDSERVICE.methods_by_name["GetUserGroup"]._options = None _CLOUDSERVICE.methods_by_name[ "GetUserGroup" - ]._serialized_options = ( - b"\202\323\344\223\002\037\022\035/cloud/user-groups/{group_id}" - ) + ]._serialized_options = b'\202\323\344\223\002\037\022\035/cloud/user-groups/{group_id}\222A\243\001\n\006Groups\022\026Get user group details\0328Returns detailed information about a specific user group"G\n\031User groups documentation\022*https://docs.temporal.io/cloud/user-groups' _CLOUDSERVICE.methods_by_name["CreateUserGroup"]._options = None _CLOUDSERVICE.methods_by_name[ "CreateUserGroup" - ]._serialized_options = b'\202\323\344\223\002\027"\022/cloud/user-groups:\001*' + ]._serialized_options = b'\202\323\344\223\002\027"\022/cloud/user-groups:\001*\222A\231\001\n\006Groups\022\023Create a user group\0321Creates a new user group for managing permissions"G\n\031User groups documentation\022*https://docs.temporal.io/cloud/user-groups' _CLOUDSERVICE.methods_by_name["UpdateUserGroup"]._options = None _CLOUDSERVICE.methods_by_name[ "UpdateUserGroup" - ]._serialized_options = ( - b'\202\323\344\223\002""\035/cloud/user-groups/{group_id}:\001*' - ) + ]._serialized_options = b'\202\323\344\223\002""\035/cloud/user-groups/{group_id}:\001*\222A\223\001\n\006Groups\022\023Update a user group\032+Updates an existing user group\'s properties"G\n\031User groups documentation\022*https://docs.temporal.io/cloud/user-groups' _CLOUDSERVICE.methods_by_name["DeleteUserGroup"]._options = None _CLOUDSERVICE.methods_by_name[ "DeleteUserGroup" - ]._serialized_options = ( - b"\202\323\344\223\002\037*\035/cloud/user-groups/{group_id}" - ) + ]._serialized_options = b'\202\323\344\223\002\037*\035/cloud/user-groups/{group_id}\222A\215\001\n\006Groups\022\023Delete a user group\032%Removes a user group from the account"G\n\031User groups documentation\022*https://docs.temporal.io/cloud/user-groups' _CLOUDSERVICE.methods_by_name["SetUserGroupNamespaceAccess"]._options = None _CLOUDSERVICE.methods_by_name[ "SetUserGroupNamespaceAccess" - ]._serialized_options = b'\202\323\344\223\002@";/cloud/namespaces/{namespace}/user-groups/{group_id}/access:\001*' + ]._serialized_options = b'\202\323\344\223\002@";/cloud/namespaces/{namespace}/user-groups/{group_id}/access:\001*\222A\262\001\n\006Groups\022\037Set user group namespace access\032>Configures a user group\'s permissions for a specific namespace"G\n\031User groups documentation\022*https://docs.temporal.io/cloud/user-groups' _CLOUDSERVICE.methods_by_name["AddUserGroupMember"]._options = None _CLOUDSERVICE.methods_by_name[ "AddUserGroupMember" - ]._serialized_options = ( - b'\202\323\344\223\002*"%/cloud/user-groups/{group_id}/members:\001*' - ) + ]._serialized_options = b'\202\323\344\223\002*"%/cloud/user-groups/{group_id}/members:\001*\222A\225\001\n\006Groups\022\021Add user to group\032/Adds a user to a user group (Cloud groups only)"G\n\031User groups documentation\022*https://docs.temporal.io/cloud/user-groups' _CLOUDSERVICE.methods_by_name["RemoveUserGroupMember"]._options = None _CLOUDSERVICE.methods_by_name[ "RemoveUserGroupMember" - ]._serialized_options = ( - b'\202\323\344\223\0020"+/cloud/user-groups/{group_id}/remove-member:\001*' - ) + ]._serialized_options = b'\202\323\344\223\0020"+/cloud/user-groups/{group_id}/remove-member:\001*\222A\237\001\n\006Groups\022\026Remove user from group\0324Removes a user from a user group (Cloud groups only)"G\n\031User groups documentation\022*https://docs.temporal.io/cloud/user-groups' _CLOUDSERVICE.methods_by_name["GetUserGroupMembers"]._options = None _CLOUDSERVICE.methods_by_name[ "GetUserGroupMembers" - ]._serialized_options = ( - b"\202\323\344\223\002'\022%/cloud/user-groups/{group_id}/members" - ) + ]._serialized_options = b"\202\323\344\223\002'\022%/cloud/user-groups/{group_id}/members\222A\225\001\n\006Groups\022\025List users in a group\032+Returns a list of all users in a user group\"G\n\031User groups documentation\022*https://docs.temporal.io/cloud/user-groups" _CLOUDSERVICE.methods_by_name["CreateServiceAccount"]._options = None _CLOUDSERVICE.methods_by_name[ "CreateServiceAccount" - ]._serialized_options = ( - b'\202\323\344\223\002\034"\027/cloud/service-accounts:\001*' - ) + ]._serialized_options = b'\202\323\344\223\002\034"\027/cloud/service-accounts:\001*\222A\263\001\n\020Service Accounts\022\030Create a service account\0322Creates a new service account for automated access"Q\n\036Service Accounts documentation\022/https://docs.temporal.io/cloud/service-accounts' _CLOUDSERVICE.methods_by_name["GetServiceAccount"]._options = None _CLOUDSERVICE.methods_by_name[ "GetServiceAccount" - ]._serialized_options = ( - b"\202\323\344\223\002.\022,/cloud/service-accounts/{service_account_id}" - ) + ]._serialized_options = b'\202\323\344\223\002.\022,/cloud/service-accounts/{service_account_id}\222A\301\001\n\020Service Accounts\022\033Get service account details\032=Returns detailed information about a specific service account"Q\n\036Service Accounts documentation\022/https://docs.temporal.io/cloud/service-accounts' _CLOUDSERVICE.methods_by_name["GetServiceAccounts"]._options = None _CLOUDSERVICE.methods_by_name[ "GetServiceAccounts" - ]._serialized_options = b"\202\323\344\223\002\031\022\027/cloud/service-accounts" + ]._serialized_options = b'\202\323\344\223\002\031\022\027/cloud/service-accounts\222A\267\001\n\020Service Accounts\022\031List all service accounts\0325Returns a list of all service accounts in the account"Q\n\036Service Accounts documentation\022/https://docs.temporal.io/cloud/service-accounts' _CLOUDSERVICE.methods_by_name["UpdateServiceAccount"]._options = None _CLOUDSERVICE.methods_by_name[ "UpdateServiceAccount" - ]._serialized_options = ( - b'\202\323\344\223\0021",/cloud/service-accounts/{service_account_id}:\001*' - ) + ]._serialized_options = b'\202\323\344\223\0021",/cloud/service-accounts/{service_account_id}:\001*\222A\261\001\n\020Service Accounts\022\030Update a service account\0320Updates an existing service account\'s properties"Q\n\036Service Accounts documentation\022/https://docs.temporal.io/cloud/service-accounts' + _CLOUDSERVICE.methods_by_name["SetServiceAccountNamespaceAccess"]._options = None + _CLOUDSERVICE.methods_by_name[ + "SetServiceAccountNamespaceAccess" + ]._serialized_options = b'\202\323\344\223\002O"J/cloud/namespaces/{namespace}/service-accounts/{service_account_id}/access:\001*\222A\320\001\n\020Service Accounts\022$Set service account namespace access\032CConfigures a service account\'s permissions for a specific namespace"Q\n\036Service Accounts documentation\022/https://docs.temporal.io/cloud/service-accounts' _CLOUDSERVICE.methods_by_name["DeleteServiceAccount"]._options = None _CLOUDSERVICE.methods_by_name[ "DeleteServiceAccount" - ]._serialized_options = ( - b"\202\323\344\223\002.*,/cloud/service-accounts/{service_account_id}" - ) + ]._serialized_options = b'\202\323\344\223\002.*,/cloud/service-accounts/{service_account_id}\222A\253\001\n\020Service Accounts\022\030Delete a service account\032*Removes a service account from the account"Q\n\036Service Accounts documentation\022/https://docs.temporal.io/cloud/service-accounts' _CLOUDSERVICE.methods_by_name["GetUsage"]._options = None _CLOUDSERVICE.methods_by_name[ "GetUsage" - ]._serialized_options = b"\202\323\344\223\002\016\022\014/cloud/usage" + ]._serialized_options = b"\202\323\344\223\002\016\022\014/cloud/usage\222A=\n\007Account\022\016Get usage data\032 Get usage data across namespacesX\001" _CLOUDSERVICE.methods_by_name["GetAccount"]._options = None _CLOUDSERVICE.methods_by_name[ "GetAccount" - ]._serialized_options = b"\202\323\344\223\002\020\022\016/cloud/account" + ]._serialized_options = b'\202\323\344\223\002\020\022\016/cloud/account\222A\230\001\n\007Account\022\023Get account details\032.Returns detailed information about the account"H\n\025Billing documentation\022/https://docs.temporal.io/cloud/billing-and-cost' _CLOUDSERVICE.methods_by_name["UpdateAccount"]._options = None _CLOUDSERVICE.methods_by_name[ "UpdateAccount" - ]._serialized_options = b'\202\323\344\223\002\023"\016/cloud/account:\001*' + ]._serialized_options = b'\202\323\344\223\002\023"\016/cloud/account:\001*\222A\227\001\n\007Account\022\026Update account details\032*Updates account configuration and settings"H\n\025Billing documentation\022/https://docs.temporal.io/cloud/billing-and-cost' _CLOUDSERVICE.methods_by_name["CreateNamespaceExportSink"]._options = None _CLOUDSERVICE.methods_by_name[ "CreateNamespaceExportSink" - ]._serialized_options = ( - b'\202\323\344\223\002/"*/cloud/namespaces/{namespace}/export-sinks:\001*' - ) + ]._serialized_options = b'\202\323\344\223\002/"*/cloud/namespaces/{namespace}/export-sinks:\001*\222A\217\001\n\006Export\022\032Create history export sink\032*Creates a new workflow history export sink"=\n\024Export documentation\022%https://docs.temporal.io/cloud/export' _CLOUDSERVICE.methods_by_name["GetNamespaceExportSink"]._options = None _CLOUDSERVICE.methods_by_name[ "GetNamespaceExportSink" - ]._serialized_options = ( - b"\202\323\344\223\0023\0221/cloud/namespaces/{namespace}/export-sinks/{name}" - ) + ]._serialized_options = b'\202\323\344\223\0023\0221/cloud/namespaces/{namespace}/export-sinks/{name}\222A\255\001\n\006Export\022\030Get history sink details\032JReturns detailed information about a specific workflow history export sink"=\n\024Export documentation\022%https://docs.temporal.io/cloud/export' _CLOUDSERVICE.methods_by_name["GetNamespaceExportSinks"]._options = None _CLOUDSERVICE.methods_by_name[ "GetNamespaceExportSinks" - ]._serialized_options = ( - b"\202\323\344\223\002,\022*/cloud/namespaces/{namespace}/export-sinks" - ) + ]._serialized_options = b'\202\323\344\223\002,\022*/cloud/namespaces/{namespace}/export-sinks\222A\247\001\n\006Export\022\031List history export sinks\032CReturns a list of all workflow history export sinks for a namespace"=\n\024Export documentation\022%https://docs.temporal.io/cloud/export' _CLOUDSERVICE.methods_by_name["UpdateNamespaceExportSink"]._options = None _CLOUDSERVICE.methods_by_name[ "UpdateNamespaceExportSink" - ]._serialized_options = b'\202\323\344\223\002;"6/cloud/namespaces/{namespace}/export-sinks/{spec.name}:\001*' + ]._serialized_options = b'\202\323\344\223\002;"6/cloud/namespaces/{namespace}/export-sinks/{spec.name}:\001*\222A\245\001\n\006Export\022\032Update history export sink\032@Updates an existing workflow history export sink\'s configuration"=\n\024Export documentation\022%https://docs.temporal.io/cloud/export' _CLOUDSERVICE.methods_by_name["DeleteNamespaceExportSink"]._options = None _CLOUDSERVICE.methods_by_name[ "DeleteNamespaceExportSink" - ]._serialized_options = ( - b"\202\323\344\223\0023*1/cloud/namespaces/{namespace}/export-sinks/{name}" - ) + ]._serialized_options = b'\202\323\344\223\0023*1/cloud/namespaces/{namespace}/export-sinks/{name}\222A\234\001\n\006Export\022\032Delete history export sink\0327Removes a workflow history export sink from a namespace"=\n\024Export documentation\022%https://docs.temporal.io/cloud/export' _CLOUDSERVICE.methods_by_name["ValidateNamespaceExportSink"]._options = None _CLOUDSERVICE.methods_by_name[ "ValidateNamespaceExportSink" - ]._serialized_options = b'\202\323\344\223\0028"3/cloud/namespaces/{namespace}/export-sinks/validate:\001*' + ]._serialized_options = b'\202\323\344\223\0027"2/cloud/namespaces/{namespace}/export-sink-validate:\001*\222A\327\001\n\006Export\022*Validate history export sink configuration\032bTests workflow history export sink configuration by delivering a test file to verify accessibility"=\n\024Export documentation\022%https://docs.temporal.io/cloud/export' _CLOUDSERVICE.methods_by_name["UpdateNamespaceTags"]._options = None _CLOUDSERVICE.methods_by_name[ "UpdateNamespaceTags" - ]._serialized_options = ( - b'\202\323\344\223\002.")/cloud/namespaces/{namespace}/update-tags:\001*' - ) + ]._serialized_options = b'\202\323\344\223\002.")/cloud/namespaces/{namespace}/update-tags:\001*\222A\253\001\n\nNamespaces\022\025Update namespace tags\032,Updates the tags associated with a namespace"X\n\033Namespace tag documentation\0229https://docs.temporal.io/cloud/namespaces#tag-a-namespace' _CLOUDSERVICE.methods_by_name["CreateConnectivityRule"]._options = None _CLOUDSERVICE.methods_by_name[ "CreateConnectivityRule" - ]._serialized_options = ( - b'\202\323\344\223\002\036"\031/cloud/connectivity-rules:\001*' - ) + ]._serialized_options = b'\202\323\344\223\002\036"\031/cloud/connectivity-rules:\001*\222A\265\001\n\022Connectivity Rules\022\030Create connectivity rule\032:Creates a new connectivity rule for network access control"I\n\032Connectivity documentation\022+https://docs.temporal.io/cloud/connectivity' _CLOUDSERVICE.methods_by_name["GetConnectivityRule"]._options = None _CLOUDSERVICE.methods_by_name[ "GetConnectivityRule" - ]._serialized_options = ( - b"\202\323\344\223\0022\0220/cloud/connectivity-rules/{connectivity_rule_id}" - ) + ]._serialized_options = b'\202\323\344\223\0022\0220/cloud/connectivity-rules/{connectivity_rule_id}\222A\277\001\n\022Connectivity Rules\022\035Get connectivity rule details\032?Returns detailed information about a specific connectivity rule"I\n\032Connectivity documentation\022+https://docs.temporal.io/cloud/connectivity' _CLOUDSERVICE.methods_by_name["GetConnectivityRules"]._options = None _CLOUDSERVICE.methods_by_name[ "GetConnectivityRules" - ]._serialized_options = b"\202\323\344\223\002\033\022\031/cloud/connectivity-rules" + ]._serialized_options = b'\202\323\344\223\002\033\022\031/cloud/connectivity-rules\222A\265\001\n\022Connectivity Rules\022\033List all connectivity rules\0327Returns a list of all connectivity rules in the account"I\n\032Connectivity documentation\022+https://docs.temporal.io/cloud/connectivity' _CLOUDSERVICE.methods_by_name["DeleteConnectivityRule"]._options = None _CLOUDSERVICE.methods_by_name[ "DeleteConnectivityRule" - ]._serialized_options = ( - b"\202\323\344\223\0022*0/cloud/connectivity-rules/{connectivity_rule_id}" - ) - _CLOUDSERVICE._serialized_start = 178 - _CLOUDSERVICE._serialized_end = 10788 + ]._serialized_options = b'\202\323\344\223\0022*0/cloud/connectivity-rules/{connectivity_rule_id}\222A\247\001\n\022Connectivity Rules\022\030Delete connectivity rule\032,Removes a connectivity rule from the account"I\n\032Connectivity documentation\022+https://docs.temporal.io/cloud/connectivity' + _CLOUDSERVICE.methods_by_name["GetAuditLogs"]._options = None + _CLOUDSERVICE.methods_by_name[ + "GetAuditLogs" + ]._serialized_options = b'\202\323\344\223\002\023\022\021/cloud/audit-logs\222A\301\001\n\007Account\022\016Get audit logs\032YReturns a paginated list of audit logs for the account, optionally filtered by time range"K\n\033Audit logging documentation\022,https://docs.temporal.io/cloud/audit-logging' + _CLOUDSERVICE.methods_by_name["ValidateAccountAuditLogSink"]._options = None + _CLOUDSERVICE.methods_by_name[ + "ValidateAccountAuditLogSink" + ]._serialized_options = b'\202\323\344\223\002#"\036/cloud/audit-log-sink-validate:\001*\222A\326\002\n\007Account\022\027Validate audit log sink\032\344\001Validate customer audit log sink is accessible from Temporal\'s workflow by delivering an empty file to the specified sink. The operation verifies that the sink is correctly configured, accessible and ready to receive audit logs."K\n\033Audit logging documentation\022,https://docs.temporal.io/cloud/audit-logging' + _CLOUDSERVICE.methods_by_name["CreateAccountAuditLogSink"]._options = None + _CLOUDSERVICE.methods_by_name[ + "CreateAccountAuditLogSink" + ]._serialized_options = b'\202\323\344\223\002\033"\026/cloud/audit-log-sinks:\001*\222A\244\001\n\007Account\022\025Create audit log sink\0325Creates a new audit log sink for exporting audit logs"K\n\033Audit logging documentation\022,https://docs.temporal.io/cloud/audit-logging' + _CLOUDSERVICE.methods_by_name["GetAccountAuditLogSink"]._options = None + _CLOUDSERVICE.methods_by_name[ + "GetAccountAuditLogSink" + ]._serialized_options = b'\202\323\344\223\002\037\022\035/cloud/audit-log-sinks/{name}\222A\260\001\n\007Account\022\032Get audit log sink details\032 None: ... + GetCurrentIdentity: grpc.UnaryUnaryMultiCallable[ + temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetCurrentIdentityRequest, + temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetCurrentIdentityResponse, + ] + """Get information about the current authenticated user or service account principal""" GetUsers: grpc.UnaryUnaryMultiCallable[ temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetUsersRequest, temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetUsersResponse, @@ -89,12 +94,16 @@ class CloudServiceStub: temporalio.api.cloud.cloudservice.v1.request_response_pb2.AddNamespaceRegionRequest, temporalio.api.cloud.cloudservice.v1.request_response_pb2.AddNamespaceRegionResponse, ] - """Add a new region to a namespace""" + """Add a new region to a namespace + Deprecated: Use the UpdateNamespace() to add new replica in the namespace spec instead. + """ DeleteNamespaceRegion: grpc.UnaryUnaryMultiCallable[ temporalio.api.cloud.cloudservice.v1.request_response_pb2.DeleteNamespaceRegionRequest, temporalio.api.cloud.cloudservice.v1.request_response_pb2.DeleteNamespaceRegionResponse, ] - """Delete a region from a namespace""" + """Delete a region from a namespace + Deprecated: Use the UpdateNamespace() to delete a replica in the namespace spec instead. + """ GetRegions: grpc.UnaryUnaryMultiCallable[ temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetRegionsRequest, temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetRegionsResponse, @@ -219,6 +228,11 @@ class CloudServiceStub: temporalio.api.cloud.cloudservice.v1.request_response_pb2.UpdateServiceAccountResponse, ] """Update a service account.""" + SetServiceAccountNamespaceAccess: grpc.UnaryUnaryMultiCallable[ + temporalio.api.cloud.cloudservice.v1.request_response_pb2.SetServiceAccountNamespaceAccessRequest, + temporalio.api.cloud.cloudservice.v1.request_response_pb2.SetServiceAccountNamespaceAccessResponse, + ] + """Set a service account's access to a namespace.""" DeleteServiceAccount: grpc.UnaryUnaryMultiCallable[ temporalio.api.cloud.cloudservice.v1.request_response_pb2.DeleteServiceAccountRequest, temporalio.api.cloud.cloudservice.v1.request_response_pb2.DeleteServiceAccountResponse, @@ -298,12 +312,111 @@ class CloudServiceStub: temporalio.api.cloud.cloudservice.v1.request_response_pb2.DeleteConnectivityRuleResponse, ] """Deletes a connectivity rule by id""" + GetAuditLogs: grpc.UnaryUnaryMultiCallable[ + temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetAuditLogsRequest, + temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetAuditLogsResponse, + ] + """Get audit logs""" + ValidateAccountAuditLogSink: grpc.UnaryUnaryMultiCallable[ + temporalio.api.cloud.cloudservice.v1.request_response_pb2.ValidateAccountAuditLogSinkRequest, + temporalio.api.cloud.cloudservice.v1.request_response_pb2.ValidateAccountAuditLogSinkResponse, + ] + """Validate customer audit log sink is accessible from Temporal's workflow by delivering an empty file to the specified sink. + The operation verifies that the sink is correctly configured, accessible and ready to receive audit logs. + """ + CreateAccountAuditLogSink: grpc.UnaryUnaryMultiCallable[ + temporalio.api.cloud.cloudservice.v1.request_response_pb2.CreateAccountAuditLogSinkRequest, + temporalio.api.cloud.cloudservice.v1.request_response_pb2.CreateAccountAuditLogSinkResponse, + ] + """Create an audit log sink""" + GetAccountAuditLogSink: grpc.UnaryUnaryMultiCallable[ + temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetAccountAuditLogSinkRequest, + temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetAccountAuditLogSinkResponse, + ] + """Get an audit log sink""" + GetAccountAuditLogSinks: grpc.UnaryUnaryMultiCallable[ + temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetAccountAuditLogSinksRequest, + temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetAccountAuditLogSinksResponse, + ] + """Get audit log sinks""" + UpdateAccountAuditLogSink: grpc.UnaryUnaryMultiCallable[ + temporalio.api.cloud.cloudservice.v1.request_response_pb2.UpdateAccountAuditLogSinkRequest, + temporalio.api.cloud.cloudservice.v1.request_response_pb2.UpdateAccountAuditLogSinkResponse, + ] + """Update an audit log sink""" + DeleteAccountAuditLogSink: grpc.UnaryUnaryMultiCallable[ + temporalio.api.cloud.cloudservice.v1.request_response_pb2.DeleteAccountAuditLogSinkRequest, + temporalio.api.cloud.cloudservice.v1.request_response_pb2.DeleteAccountAuditLogSinkResponse, + ] + """Delete an audit log sink""" + GetNamespaceCapacityInfo: grpc.UnaryUnaryMultiCallable[ + temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetNamespaceCapacityInfoRequest, + temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetNamespaceCapacityInfoResponse, + ] + """Get namespace capacity information""" + CreateBillingReport: grpc.UnaryUnaryMultiCallable[ + temporalio.api.cloud.cloudservice.v1.request_response_pb2.CreateBillingReportRequest, + temporalio.api.cloud.cloudservice.v1.request_response_pb2.CreateBillingReportResponse, + ] + """Create a billing report""" + GetBillingReport: grpc.UnaryUnaryMultiCallable[ + temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetBillingReportRequest, + temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetBillingReportResponse, + ] + """Get a billing report""" + GetCustomRoles: grpc.UnaryUnaryMultiCallable[ + temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetCustomRolesRequest, + temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetCustomRolesResponse, + ] + """Get custom roles""" + GetCustomRole: grpc.UnaryUnaryMultiCallable[ + temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetCustomRoleRequest, + temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetCustomRoleResponse, + ] + """Get a custom role""" + CreateCustomRole: grpc.UnaryUnaryMultiCallable[ + temporalio.api.cloud.cloudservice.v1.request_response_pb2.CreateCustomRoleRequest, + temporalio.api.cloud.cloudservice.v1.request_response_pb2.CreateCustomRoleResponse, + ] + """Create a custom role""" + UpdateCustomRole: grpc.UnaryUnaryMultiCallable[ + temporalio.api.cloud.cloudservice.v1.request_response_pb2.UpdateCustomRoleRequest, + temporalio.api.cloud.cloudservice.v1.request_response_pb2.UpdateCustomRoleResponse, + ] + """Update a custom role""" + DeleteCustomRole: grpc.UnaryUnaryMultiCallable[ + temporalio.api.cloud.cloudservice.v1.request_response_pb2.DeleteCustomRoleRequest, + temporalio.api.cloud.cloudservice.v1.request_response_pb2.DeleteCustomRoleResponse, + ] + """Delete a custom role""" + GetUserNamespaceAssignments: grpc.UnaryUnaryMultiCallable[ + temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetUserNamespaceAssignmentsRequest, + temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetUserNamespaceAssignmentsResponse, + ] + """Get users with access to a namespace""" + GetServiceAccountNamespaceAssignments: grpc.UnaryUnaryMultiCallable[ + temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetServiceAccountNamespaceAssignmentsRequest, + temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetServiceAccountNamespaceAssignmentsResponse, + ] + """Get service accounts with access to a namespace""" + GetUserGroupNamespaceAssignments: grpc.UnaryUnaryMultiCallable[ + temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetUserGroupNamespaceAssignmentsRequest, + temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetUserGroupNamespaceAssignmentsResponse, + ] + """Get user groups with access to a namespace""" class CloudServiceServicer(metaclass=abc.ABCMeta): """WARNING: This service is currently experimental and may change in incompatible ways. """ + @abc.abstractmethod + def GetCurrentIdentity( + self, + request: temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetCurrentIdentityRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetCurrentIdentityResponse: + """Get information about the current authenticated user or service account principal""" @abc.abstractmethod def GetUsers( self, @@ -410,14 +523,18 @@ class CloudServiceServicer(metaclass=abc.ABCMeta): request: temporalio.api.cloud.cloudservice.v1.request_response_pb2.AddNamespaceRegionRequest, context: grpc.ServicerContext, ) -> temporalio.api.cloud.cloudservice.v1.request_response_pb2.AddNamespaceRegionResponse: - """Add a new region to a namespace""" + """Add a new region to a namespace + Deprecated: Use the UpdateNamespace() to add new replica in the namespace spec instead. + """ @abc.abstractmethod def DeleteNamespaceRegion( self, request: temporalio.api.cloud.cloudservice.v1.request_response_pb2.DeleteNamespaceRegionRequest, context: grpc.ServicerContext, ) -> temporalio.api.cloud.cloudservice.v1.request_response_pb2.DeleteNamespaceRegionResponse: - """Delete a region from a namespace""" + """Delete a region from a namespace + Deprecated: Use the UpdateNamespace() to delete a replica in the namespace spec instead. + """ @abc.abstractmethod def GetRegions( self, @@ -595,6 +712,13 @@ class CloudServiceServicer(metaclass=abc.ABCMeta): ) -> temporalio.api.cloud.cloudservice.v1.request_response_pb2.UpdateServiceAccountResponse: """Update a service account.""" @abc.abstractmethod + def SetServiceAccountNamespaceAccess( + self, + request: temporalio.api.cloud.cloudservice.v1.request_response_pb2.SetServiceAccountNamespaceAccessRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.cloud.cloudservice.v1.request_response_pb2.SetServiceAccountNamespaceAccessResponse: + """Set a service account's access to a namespace.""" + @abc.abstractmethod def DeleteServiceAccount( self, request: temporalio.api.cloud.cloudservice.v1.request_response_pb2.DeleteServiceAccountRequest, @@ -705,6 +829,138 @@ class CloudServiceServicer(metaclass=abc.ABCMeta): context: grpc.ServicerContext, ) -> temporalio.api.cloud.cloudservice.v1.request_response_pb2.DeleteConnectivityRuleResponse: """Deletes a connectivity rule by id""" + @abc.abstractmethod + def GetAuditLogs( + self, + request: temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetAuditLogsRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetAuditLogsResponse: + """Get audit logs""" + @abc.abstractmethod + def ValidateAccountAuditLogSink( + self, + request: temporalio.api.cloud.cloudservice.v1.request_response_pb2.ValidateAccountAuditLogSinkRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.cloud.cloudservice.v1.request_response_pb2.ValidateAccountAuditLogSinkResponse: + """Validate customer audit log sink is accessible from Temporal's workflow by delivering an empty file to the specified sink. + The operation verifies that the sink is correctly configured, accessible and ready to receive audit logs. + """ + @abc.abstractmethod + def CreateAccountAuditLogSink( + self, + request: temporalio.api.cloud.cloudservice.v1.request_response_pb2.CreateAccountAuditLogSinkRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.cloud.cloudservice.v1.request_response_pb2.CreateAccountAuditLogSinkResponse: + """Create an audit log sink""" + @abc.abstractmethod + def GetAccountAuditLogSink( + self, + request: temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetAccountAuditLogSinkRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetAccountAuditLogSinkResponse: + """Get an audit log sink""" + @abc.abstractmethod + def GetAccountAuditLogSinks( + self, + request: temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetAccountAuditLogSinksRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetAccountAuditLogSinksResponse: + """Get audit log sinks""" + @abc.abstractmethod + def UpdateAccountAuditLogSink( + self, + request: temporalio.api.cloud.cloudservice.v1.request_response_pb2.UpdateAccountAuditLogSinkRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.cloud.cloudservice.v1.request_response_pb2.UpdateAccountAuditLogSinkResponse: + """Update an audit log sink""" + @abc.abstractmethod + def DeleteAccountAuditLogSink( + self, + request: temporalio.api.cloud.cloudservice.v1.request_response_pb2.DeleteAccountAuditLogSinkRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.cloud.cloudservice.v1.request_response_pb2.DeleteAccountAuditLogSinkResponse: + """Delete an audit log sink""" + @abc.abstractmethod + def GetNamespaceCapacityInfo( + self, + request: temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetNamespaceCapacityInfoRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetNamespaceCapacityInfoResponse: + """Get namespace capacity information""" + @abc.abstractmethod + def CreateBillingReport( + self, + request: temporalio.api.cloud.cloudservice.v1.request_response_pb2.CreateBillingReportRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.cloud.cloudservice.v1.request_response_pb2.CreateBillingReportResponse: + """Create a billing report""" + @abc.abstractmethod + def GetBillingReport( + self, + request: temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetBillingReportRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetBillingReportResponse: + """Get a billing report""" + @abc.abstractmethod + def GetCustomRoles( + self, + request: temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetCustomRolesRequest, + context: grpc.ServicerContext, + ) -> ( + temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetCustomRolesResponse + ): + """Get custom roles""" + @abc.abstractmethod + def GetCustomRole( + self, + request: temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetCustomRoleRequest, + context: grpc.ServicerContext, + ) -> ( + temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetCustomRoleResponse + ): + """Get a custom role""" + @abc.abstractmethod + def CreateCustomRole( + self, + request: temporalio.api.cloud.cloudservice.v1.request_response_pb2.CreateCustomRoleRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.cloud.cloudservice.v1.request_response_pb2.CreateCustomRoleResponse: + """Create a custom role""" + @abc.abstractmethod + def UpdateCustomRole( + self, + request: temporalio.api.cloud.cloudservice.v1.request_response_pb2.UpdateCustomRoleRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.cloud.cloudservice.v1.request_response_pb2.UpdateCustomRoleResponse: + """Update a custom role""" + @abc.abstractmethod + def DeleteCustomRole( + self, + request: temporalio.api.cloud.cloudservice.v1.request_response_pb2.DeleteCustomRoleRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.cloud.cloudservice.v1.request_response_pb2.DeleteCustomRoleResponse: + """Delete a custom role""" + @abc.abstractmethod + def GetUserNamespaceAssignments( + self, + request: temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetUserNamespaceAssignmentsRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetUserNamespaceAssignmentsResponse: + """Get users with access to a namespace""" + @abc.abstractmethod + def GetServiceAccountNamespaceAssignments( + self, + request: temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetServiceAccountNamespaceAssignmentsRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetServiceAccountNamespaceAssignmentsResponse: + """Get service accounts with access to a namespace""" + @abc.abstractmethod + def GetUserGroupNamespaceAssignments( + self, + request: temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetUserGroupNamespaceAssignmentsRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.cloud.cloudservice.v1.request_response_pb2.GetUserGroupNamespaceAssignmentsResponse: + """Get user groups with access to a namespace""" def add_CloudServiceServicer_to_server( servicer: CloudServiceServicer, server: grpc.Server diff --git a/temporalio/api/cloud/connectivityrule/v1/message_pb2.py b/temporalio/api/cloud/connectivityrule/v1/message_pb2.py index 86820b3bc..2d39aec83 100644 --- a/temporalio/api/cloud/connectivityrule/v1/message_pb2.py +++ b/temporalio/api/cloud/connectivityrule/v1/message_pb2.py @@ -21,7 +21,7 @@ ) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n4temporal/api/cloud/connectivityrule/v1/message.proto\x12&temporal.api.cloud.connectivityrule.v1\x1a,temporal/api/cloud/resource/v1/message.proto\x1a\x1fgoogle/protobuf/timestamp.proto"\x96\x02\n\x10\x43onnectivityRule\x12\n\n\x02id\x18\x01 \x01(\t\x12J\n\x04spec\x18\x02 \x01(\x0b\x32<.temporal.api.cloud.connectivityrule.v1.ConnectivityRuleSpec\x12\x18\n\x10resource_version\x18\x04 \x01(\t\x12<\n\x05state\x18\x05 \x01(\x0e\x32-.temporal.api.cloud.resource.v1.ResourceState\x12\x1a\n\x12\x61sync_operation_id\x18\x06 \x01(\t\x12\x30\n\x0c\x63reated_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.TimestampJ\x04\x08\x03\x10\x04"\xd9\x01\n\x14\x43onnectivityRuleSpec\x12U\n\x0bpublic_rule\x18\x01 \x01(\x0b\x32>.temporal.api.cloud.connectivityrule.v1.PublicConnectivityRuleH\x00\x12W\n\x0cprivate_rule\x18\x02 \x01(\x0b\x32?.temporal.api.cloud.connectivityrule.v1.PrivateConnectivityRuleH\x00\x42\x11\n\x0f\x63onnection_type"\x18\n\x16PublicConnectivityRule"^\n\x17PrivateConnectivityRule\x12\x15\n\rconnection_id\x18\x01 \x01(\t\x12\x16\n\x0egcp_project_id\x18\x02 \x01(\t\x12\x0e\n\x06region\x18\x03 \x01(\tJ\x04\x08\x04\x10\x05\x42\xd4\x01\n)io.temporal.api.cloud.connectivityrule.v1B\x0cMessageProtoP\x01Z=go.temporal.io/api/cloud/connectivityrule/v1;connectivityrule\xaa\x02(Temporalio.Api.Cloud.ConnectivityRule.V1\xea\x02,Temporalio::Api::Cloud::ConnectivityRule::V1b\x06proto3' + b'\n4temporal/api/cloud/connectivityrule/v1/message.proto\x12&temporal.api.cloud.connectivityrule.v1\x1a,temporal/api/cloud/resource/v1/message.proto\x1a\x1fgoogle/protobuf/timestamp.proto"\x96\x02\n\x10\x43onnectivityRule\x12\n\n\x02id\x18\x01 \x01(\t\x12J\n\x04spec\x18\x02 \x01(\x0b\x32<.temporal.api.cloud.connectivityrule.v1.ConnectivityRuleSpec\x12\x18\n\x10resource_version\x18\x04 \x01(\t\x12<\n\x05state\x18\x05 \x01(\x0e\x32-.temporal.api.cloud.resource.v1.ResourceState\x12\x1a\n\x12\x61sync_operation_id\x18\x06 \x01(\t\x12\x30\n\x0c\x63reated_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.TimestampJ\x04\x08\x03\x10\x04"\xd9\x01\n\x14\x43onnectivityRuleSpec\x12U\n\x0bpublic_rule\x18\x01 \x01(\x0b\x32>.temporal.api.cloud.connectivityrule.v1.PublicConnectivityRuleH\x00\x12W\n\x0cprivate_rule\x18\x02 \x01(\x0b\x32?.temporal.api.cloud.connectivityrule.v1.PrivateConnectivityRuleH\x00\x42\x11\n\x0f\x63onnection_type"3\n\x16PublicConnectivityRule\x12\x19\n\x11\x65nable_stable_ips\x18\x01 \x01(\x08"^\n\x17PrivateConnectivityRule\x12\x15\n\rconnection_id\x18\x01 \x01(\t\x12\x16\n\x0egcp_project_id\x18\x02 \x01(\t\x12\x0e\n\x06region\x18\x03 \x01(\tJ\x04\x08\x04\x10\x05\x42\xd4\x01\n)io.temporal.api.cloud.connectivityrule.v1B\x0cMessageProtoP\x01Z=go.temporal.io/api/cloud/connectivityrule/v1;connectivityrule\xaa\x02(Temporalio.Api.Cloud.ConnectivityRule.V1\xea\x02,Temporalio::Api::Cloud::ConnectivityRule::V1b\x06proto3' ) @@ -34,7 +34,7 @@ (_message.Message,), { "DESCRIPTOR": _CONNECTIVITYRULE, - "__module__": "temporal.api.cloud.connectivityrule.v1.message_pb2", + "__module__": "temporalio.api.cloud.connectivityrule.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.connectivityrule.v1.ConnectivityRule) }, ) @@ -45,7 +45,7 @@ (_message.Message,), { "DESCRIPTOR": _CONNECTIVITYRULESPEC, - "__module__": "temporal.api.cloud.connectivityrule.v1.message_pb2", + "__module__": "temporalio.api.cloud.connectivityrule.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.connectivityrule.v1.ConnectivityRuleSpec) }, ) @@ -56,7 +56,7 @@ (_message.Message,), { "DESCRIPTOR": _PUBLICCONNECTIVITYRULE, - "__module__": "temporal.api.cloud.connectivityrule.v1.message_pb2", + "__module__": "temporalio.api.cloud.connectivityrule.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.connectivityrule.v1.PublicConnectivityRule) }, ) @@ -67,7 +67,7 @@ (_message.Message,), { "DESCRIPTOR": _PRIVATECONNECTIVITYRULE, - "__module__": "temporal.api.cloud.connectivityrule.v1.message_pb2", + "__module__": "temporalio.api.cloud.connectivityrule.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.connectivityrule.v1.PrivateConnectivityRule) }, ) @@ -81,7 +81,7 @@ _CONNECTIVITYRULESPEC._serialized_start = 457 _CONNECTIVITYRULESPEC._serialized_end = 674 _PUBLICCONNECTIVITYRULE._serialized_start = 676 - _PUBLICCONNECTIVITYRULE._serialized_end = 700 - _PRIVATECONNECTIVITYRULE._serialized_start = 702 - _PRIVATECONNECTIVITYRULE._serialized_end = 796 + _PUBLICCONNECTIVITYRULE._serialized_end = 727 + _PRIVATECONNECTIVITYRULE._serialized_start = 729 + _PRIVATECONNECTIVITYRULE._serialized_end = 823 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/cloud/connectivityrule/v1/message_pb2.pyi b/temporalio/api/cloud/connectivityrule/v1/message_pb2.pyi index 06d8850d4..629704d6f 100644 --- a/temporalio/api/cloud/connectivityrule/v1/message_pb2.pyi +++ b/temporalio/api/cloud/connectivityrule/v1/message_pb2.pyi @@ -132,8 +132,21 @@ class PublicConnectivityRule(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor + ENABLE_STABLE_IPS_FIELD_NUMBER: builtins.int + enable_stable_ips: builtins.bool + """Flag to determine namespace is connected via a predictable set of IPs on public internet + temporal:versioning:min_version=v0.15.0 + """ def __init__( self, + *, + enable_stable_ips: builtins.bool = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "enable_stable_ips", b"enable_stable_ips" + ], ) -> None: ... global___PublicConnectivityRule = PublicConnectivityRule diff --git a/temporalio/api/cloud/identity/v1/__init__.py b/temporalio/api/cloud/identity/v1/__init__.py index 6b477fbef..48e9ad21d 100644 --- a/temporalio/api/cloud/identity/v1/__init__.py +++ b/temporalio/api/cloud/identity/v1/__init__.py @@ -4,6 +4,8 @@ ApiKey, ApiKeySpec, CloudGroupSpec, + CustomRole, + CustomRoleSpec, GoogleGroupSpec, Invitation, NamespaceAccess, @@ -11,12 +13,15 @@ OwnerType, SCIMGroupSpec, ServiceAccount, + ServiceAccountNamespaceAssignment, ServiceAccountSpec, User, UserGroup, UserGroupMember, UserGroupMemberId, + UserGroupNamespaceAssignment, UserGroupSpec, + UserNamespaceAssignment, UserSpec, ) @@ -26,6 +31,8 @@ "ApiKey", "ApiKeySpec", "CloudGroupSpec", + "CustomRole", + "CustomRoleSpec", "GoogleGroupSpec", "Invitation", "NamespaceAccess", @@ -33,11 +40,14 @@ "OwnerType", "SCIMGroupSpec", "ServiceAccount", + "ServiceAccountNamespaceAssignment", "ServiceAccountSpec", "User", "UserGroup", "UserGroupMember", "UserGroupMemberId", + "UserGroupNamespaceAssignment", "UserGroupSpec", + "UserNamespaceAssignment", "UserSpec", ] diff --git a/temporalio/api/cloud/identity/v1/message_pb2.py b/temporalio/api/cloud/identity/v1/message_pb2.py index cd834be03..439a22202 100644 --- a/temporalio/api/cloud/identity/v1/message_pb2.py +++ b/temporalio/api/cloud/identity/v1/message_pb2.py @@ -22,7 +22,7 @@ ) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n,temporal/api/cloud/identity/v1/message.proto\x12\x1etemporal.api.cloud.identity.v1\x1a,temporal/api/cloud/resource/v1/message.proto\x1a\x1fgoogle/protobuf/timestamp.proto"\xff\x01\n\rAccountAccess\x12\x1b\n\x0frole_deprecated\x18\x01 \x01(\tB\x02\x18\x01\x12@\n\x04role\x18\x02 \x01(\x0e\x32\x32.temporal.api.cloud.identity.v1.AccountAccess.Role"\x8e\x01\n\x04Role\x12\x14\n\x10ROLE_UNSPECIFIED\x10\x00\x12\x0e\n\nROLE_OWNER\x10\x01\x12\x0e\n\nROLE_ADMIN\x10\x02\x12\x12\n\x0eROLE_DEVELOPER\x10\x03\x12\x16\n\x12ROLE_FINANCE_ADMIN\x10\x04\x12\r\n\tROLE_READ\x10\x05\x12\x15\n\x11ROLE_METRICS_READ\x10\x06"\xef\x01\n\x0fNamespaceAccess\x12!\n\x15permission_deprecated\x18\x01 \x01(\tB\x02\x18\x01\x12N\n\npermission\x18\x02 \x01(\x0e\x32:.temporal.api.cloud.identity.v1.NamespaceAccess.Permission"i\n\nPermission\x12\x1a\n\x16PERMISSION_UNSPECIFIED\x10\x00\x12\x14\n\x10PERMISSION_ADMIN\x10\x01\x12\x14\n\x10PERMISSION_WRITE\x10\x02\x12\x13\n\x0fPERMISSION_READ\x10\x03"\x95\x02\n\x06\x41\x63\x63\x65ss\x12\x45\n\x0e\x61\x63\x63ount_access\x18\x01 \x01(\x0b\x32-.temporal.api.cloud.identity.v1.AccountAccess\x12Y\n\x12namespace_accesses\x18\x02 \x03(\x0b\x32=.temporal.api.cloud.identity.v1.Access.NamespaceAccessesEntry\x1ai\n\x16NamespaceAccessesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12>\n\x05value\x18\x02 \x01(\x0b\x32/.temporal.api.cloud.identity.v1.NamespaceAccess:\x02\x38\x01"k\n\x15NamespaceScopedAccess\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12?\n\x06\x61\x63\x63\x65ss\x18\x02 \x01(\x0b\x32/.temporal.api.cloud.identity.v1.NamespaceAccess"Q\n\x08UserSpec\x12\r\n\x05\x65mail\x18\x01 \x01(\t\x12\x36\n\x06\x61\x63\x63\x65ss\x18\x02 \x01(\x0b\x32&.temporal.api.cloud.identity.v1.Access"p\n\nInvitation\x12\x30\n\x0c\x63reated_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x30\n\x0c\x65xpired_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"\x86\x03\n\x04User\x12\n\n\x02id\x18\x01 \x01(\t\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12\x36\n\x04spec\x18\x03 \x01(\x0b\x32(.temporal.api.cloud.identity.v1.UserSpec\x12\x1c\n\x10state_deprecated\x18\x04 \x01(\tB\x02\x18\x01\x12<\n\x05state\x18\t \x01(\x0e\x32-.temporal.api.cloud.resource.v1.ResourceState\x12\x1a\n\x12\x61sync_operation_id\x18\x05 \x01(\t\x12>\n\ninvitation\x18\x06 \x01(\x0b\x32*.temporal.api.cloud.identity.v1.Invitation\x12\x30\n\x0c\x63reated_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x36\n\x12last_modified_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"(\n\x0fGoogleGroupSpec\x12\x15\n\remail_address\x18\x01 \x01(\t"\x1f\n\rSCIMGroupSpec\x12\x0e\n\x06idp_id\x18\x01 \x01(\t"\x10\n\x0e\x43loudGroupSpec"\xc0\x02\n\rUserGroupSpec\x12\x14\n\x0c\x64isplay_name\x18\x01 \x01(\t\x12\x36\n\x06\x61\x63\x63\x65ss\x18\x02 \x01(\x0b\x32&.temporal.api.cloud.identity.v1.Access\x12G\n\x0cgoogle_group\x18\x03 \x01(\x0b\x32/.temporal.api.cloud.identity.v1.GoogleGroupSpecH\x00\x12\x43\n\nscim_group\x18\x04 \x01(\x0b\x32-.temporal.api.cloud.identity.v1.SCIMGroupSpecH\x00\x12\x45\n\x0b\x63loud_group\x18\x05 \x01(\x0b\x32..temporal.api.cloud.identity.v1.CloudGroupSpecH\x00\x42\x0c\n\ngroup_type"\xd0\x02\n\tUserGroup\x12\n\n\x02id\x18\x01 \x01(\t\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12;\n\x04spec\x18\x03 \x01(\x0b\x32-.temporal.api.cloud.identity.v1.UserGroupSpec\x12\x1c\n\x10state_deprecated\x18\x04 \x01(\tB\x02\x18\x01\x12<\n\x05state\x18\x08 \x01(\x0e\x32-.temporal.api.cloud.resource.v1.ResourceState\x12\x1a\n\x12\x61sync_operation_id\x18\x05 \x01(\t\x12\x30\n\x0c\x63reated_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x36\n\x12last_modified_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"5\n\x11UserGroupMemberId\x12\x11\n\x07user_id\x18\x01 \x01(\tH\x00\x42\r\n\x0bmember_type"\x89\x01\n\x0fUserGroupMember\x12\x44\n\tmember_id\x18\x01 \x01(\x0b\x32\x31.temporal.api.cloud.identity.v1.UserGroupMemberId\x12\x30\n\x0c\x63reated_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"\xda\x02\n\x0eServiceAccount\x12\n\n\x02id\x18\x01 \x01(\t\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12@\n\x04spec\x18\x03 \x01(\x0b\x32\x32.temporal.api.cloud.identity.v1.ServiceAccountSpec\x12\x1c\n\x10state_deprecated\x18\x04 \x01(\tB\x02\x18\x01\x12<\n\x05state\x18\x08 \x01(\x0e\x32-.temporal.api.cloud.resource.v1.ResourceState\x12\x1a\n\x12\x61sync_operation_id\x18\x05 \x01(\t\x12\x30\n\x0c\x63reated_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x36\n\x12last_modified_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"\xc7\x01\n\x12ServiceAccountSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x36\n\x06\x61\x63\x63\x65ss\x18\x02 \x01(\x0b\x32&.temporal.api.cloud.identity.v1.Access\x12V\n\x17namespace_scoped_access\x18\x04 \x01(\x0b\x32\x35.temporal.api.cloud.identity.v1.NamespaceScopedAccess\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t"\xca\x02\n\x06\x41piKey\x12\n\n\x02id\x18\x01 \x01(\t\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12\x38\n\x04spec\x18\x03 \x01(\x0b\x32*.temporal.api.cloud.identity.v1.ApiKeySpec\x12\x1c\n\x10state_deprecated\x18\x04 \x01(\tB\x02\x18\x01\x12<\n\x05state\x18\x08 \x01(\x0e\x32-.temporal.api.cloud.resource.v1.ResourceState\x12\x1a\n\x12\x61sync_operation_id\x18\x05 \x01(\t\x12\x30\n\x0c\x63reated_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x36\n\x12last_modified_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"\xee\x01\n\nApiKeySpec\x12\x10\n\x08owner_id\x18\x01 \x01(\t\x12!\n\x15owner_type_deprecated\x18\x02 \x01(\tB\x02\x18\x01\x12=\n\nowner_type\x18\x07 \x01(\x0e\x32).temporal.api.cloud.identity.v1.OwnerType\x12\x14\n\x0c\x64isplay_name\x18\x03 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\x12/\n\x0b\x65xpiry_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x10\n\x08\x64isabled\x18\x06 \x01(\x08*\\\n\tOwnerType\x12\x1a\n\x16OWNER_TYPE_UNSPECIFIED\x10\x00\x12\x13\n\x0fOWNER_TYPE_USER\x10\x01\x12\x1e\n\x1aOWNER_TYPE_SERVICE_ACCOUNT\x10\x02\x42\xac\x01\n!io.temporal.api.cloud.identity.v1B\x0cMessageProtoP\x01Z-go.temporal.io/api/cloud/identity/v1;identity\xaa\x02 Temporalio.Api.Cloud.Identity.V1\xea\x02$Temporalio::Api::Cloud::Identity::V1b\x06proto3' + b'\n,temporal/api/cloud/identity/v1/message.proto\x12\x1etemporal.api.cloud.identity.v1\x1a,temporal/api/cloud/resource/v1/message.proto\x1a\x1fgoogle/protobuf/timestamp.proto"\x95\x02\n\rAccountAccess\x12\x1b\n\x0frole_deprecated\x18\x01 \x01(\tB\x02\x18\x01\x12@\n\x04role\x18\x02 \x01(\x0e\x32\x32.temporal.api.cloud.identity.v1.AccountAccess.Role\x12\x14\n\x0c\x63ustom_roles\x18\x03 \x03(\t"\x8e\x01\n\x04Role\x12\x14\n\x10ROLE_UNSPECIFIED\x10\x00\x12\x0e\n\nROLE_OWNER\x10\x01\x12\x0e\n\nROLE_ADMIN\x10\x02\x12\x12\n\x0eROLE_DEVELOPER\x10\x03\x12\x16\n\x12ROLE_FINANCE_ADMIN\x10\x04\x12\r\n\tROLE_READ\x10\x05\x12\x15\n\x11ROLE_METRICS_READ\x10\x06"\xef\x01\n\x0fNamespaceAccess\x12!\n\x15permission_deprecated\x18\x01 \x01(\tB\x02\x18\x01\x12N\n\npermission\x18\x02 \x01(\x0e\x32:.temporal.api.cloud.identity.v1.NamespaceAccess.Permission"i\n\nPermission\x12\x1a\n\x16PERMISSION_UNSPECIFIED\x10\x00\x12\x14\n\x10PERMISSION_ADMIN\x10\x01\x12\x14\n\x10PERMISSION_WRITE\x10\x02\x12\x13\n\x0fPERMISSION_READ\x10\x03"\xba\x02\n\x06\x41\x63\x63\x65ss\x12\x45\n\x0e\x61\x63\x63ount_access\x18\x01 \x01(\x0b\x32-.temporal.api.cloud.identity.v1.AccountAccess\x12Y\n\x12namespace_accesses\x18\x02 \x03(\x0b\x32=.temporal.api.cloud.identity.v1.Access.NamespaceAccessesEntry\x12#\n\x17\x63ustom_roles_deprecated\x18\x04 \x03(\tB\x02\x18\x01\x1ai\n\x16NamespaceAccessesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12>\n\x05value\x18\x02 \x01(\x0b\x32/.temporal.api.cloud.identity.v1.NamespaceAccess:\x02\x38\x01"k\n\x15NamespaceScopedAccess\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12?\n\x06\x61\x63\x63\x65ss\x18\x02 \x01(\x0b\x32/.temporal.api.cloud.identity.v1.NamespaceAccess"Q\n\x08UserSpec\x12\r\n\x05\x65mail\x18\x01 \x01(\t\x12\x36\n\x06\x61\x63\x63\x65ss\x18\x02 \x01(\x0b\x32&.temporal.api.cloud.identity.v1.Access"p\n\nInvitation\x12\x30\n\x0c\x63reated_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x30\n\x0c\x65xpired_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"\x86\x03\n\x04User\x12\n\n\x02id\x18\x01 \x01(\t\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12\x36\n\x04spec\x18\x03 \x01(\x0b\x32(.temporal.api.cloud.identity.v1.UserSpec\x12\x1c\n\x10state_deprecated\x18\x04 \x01(\tB\x02\x18\x01\x12<\n\x05state\x18\t \x01(\x0e\x32-.temporal.api.cloud.resource.v1.ResourceState\x12\x1a\n\x12\x61sync_operation_id\x18\x05 \x01(\t\x12>\n\ninvitation\x18\x06 \x01(\x0b\x32*.temporal.api.cloud.identity.v1.Invitation\x12\x30\n\x0c\x63reated_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x36\n\x12last_modified_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"(\n\x0fGoogleGroupSpec\x12\x15\n\remail_address\x18\x01 \x01(\t"\x1f\n\rSCIMGroupSpec\x12\x0e\n\x06idp_id\x18\x01 \x01(\t"\x10\n\x0e\x43loudGroupSpec"\xc0\x02\n\rUserGroupSpec\x12\x14\n\x0c\x64isplay_name\x18\x01 \x01(\t\x12\x36\n\x06\x61\x63\x63\x65ss\x18\x02 \x01(\x0b\x32&.temporal.api.cloud.identity.v1.Access\x12G\n\x0cgoogle_group\x18\x03 \x01(\x0b\x32/.temporal.api.cloud.identity.v1.GoogleGroupSpecH\x00\x12\x43\n\nscim_group\x18\x04 \x01(\x0b\x32-.temporal.api.cloud.identity.v1.SCIMGroupSpecH\x00\x12\x45\n\x0b\x63loud_group\x18\x05 \x01(\x0b\x32..temporal.api.cloud.identity.v1.CloudGroupSpecH\x00\x42\x0c\n\ngroup_type"\xd0\x02\n\tUserGroup\x12\n\n\x02id\x18\x01 \x01(\t\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12;\n\x04spec\x18\x03 \x01(\x0b\x32-.temporal.api.cloud.identity.v1.UserGroupSpec\x12\x1c\n\x10state_deprecated\x18\x04 \x01(\tB\x02\x18\x01\x12<\n\x05state\x18\x08 \x01(\x0e\x32-.temporal.api.cloud.resource.v1.ResourceState\x12\x1a\n\x12\x61sync_operation_id\x18\x05 \x01(\t\x12\x30\n\x0c\x63reated_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x36\n\x12last_modified_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"5\n\x11UserGroupMemberId\x12\x11\n\x07user_id\x18\x01 \x01(\tH\x00\x42\r\n\x0bmember_type"\x89\x01\n\x0fUserGroupMember\x12\x44\n\tmember_id\x18\x01 \x01(\x0b\x32\x31.temporal.api.cloud.identity.v1.UserGroupMemberId\x12\x30\n\x0c\x63reated_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"\xda\x02\n\x0eServiceAccount\x12\n\n\x02id\x18\x01 \x01(\t\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12@\n\x04spec\x18\x03 \x01(\x0b\x32\x32.temporal.api.cloud.identity.v1.ServiceAccountSpec\x12\x1c\n\x10state_deprecated\x18\x04 \x01(\tB\x02\x18\x01\x12<\n\x05state\x18\x08 \x01(\x0e\x32-.temporal.api.cloud.resource.v1.ResourceState\x12\x1a\n\x12\x61sync_operation_id\x18\x05 \x01(\t\x12\x30\n\x0c\x63reated_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x36\n\x12last_modified_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"\xc7\x01\n\x12ServiceAccountSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x36\n\x06\x61\x63\x63\x65ss\x18\x02 \x01(\x0b\x32&.temporal.api.cloud.identity.v1.Access\x12V\n\x17namespace_scoped_access\x18\x04 \x01(\x0b\x32\x35.temporal.api.cloud.identity.v1.NamespaceScopedAccess\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t"\xca\x02\n\x06\x41piKey\x12\n\n\x02id\x18\x01 \x01(\t\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12\x38\n\x04spec\x18\x03 \x01(\x0b\x32*.temporal.api.cloud.identity.v1.ApiKeySpec\x12\x1c\n\x10state_deprecated\x18\x04 \x01(\tB\x02\x18\x01\x12<\n\x05state\x18\x08 \x01(\x0e\x32-.temporal.api.cloud.resource.v1.ResourceState\x12\x1a\n\x12\x61sync_operation_id\x18\x05 \x01(\t\x12\x30\n\x0c\x63reated_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x36\n\x12last_modified_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"\xee\x01\n\nApiKeySpec\x12\x10\n\x08owner_id\x18\x01 \x01(\t\x12!\n\x15owner_type_deprecated\x18\x02 \x01(\tB\x02\x18\x01\x12=\n\nowner_type\x18\x07 \x01(\x0e\x32).temporal.api.cloud.identity.v1.OwnerType\x12\x14\n\x0c\x64isplay_name\x18\x03 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\x12/\n\x0b\x65xpiry_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x10\n\x08\x64isabled\x18\x06 \x01(\x08"\xbc\x02\n\x0e\x43ustomRoleSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12N\n\x0bpermissions\x18\x03 \x03(\x0b\x32\x39.temporal.api.cloud.identity.v1.CustomRoleSpec.Permission\x1aK\n\tResources\x12\x15\n\rresource_type\x18\x01 \x01(\t\x12\x14\n\x0cresource_ids\x18\x02 \x03(\t\x12\x11\n\tallow_all\x18\x03 \x01(\x08\x1aj\n\nPermission\x12K\n\tresources\x18\x01 \x01(\x0b\x32\x38.temporal.api.cloud.identity.v1.CustomRoleSpec.Resources\x12\x0f\n\x07\x61\x63tions\x18\x02 \x03(\t"\xb4\x02\n\nCustomRole\x12\n\n\x02id\x18\x01 \x01(\t\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12<\n\x04spec\x18\x03 \x01(\x0b\x32..temporal.api.cloud.identity.v1.CustomRoleSpec\x12<\n\x05state\x18\x04 \x01(\x0e\x32-.temporal.api.cloud.resource.v1.ResourceState\x12\x1a\n\x12\x61sync_operation_id\x18\x05 \x01(\t\x12\x30\n\x0c\x63reated_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x36\n\x12last_modified_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"\xb3\x01\n\x17UserNamespaceAssignment\x12\n\n\x02id\x18\x01 \x01(\t\x12\r\n\x05\x65mail\x18\x02 \x01(\t\x12I\n\x10namespace_access\x18\x03 \x01(\x0b\x32/.temporal.api.cloud.identity.v1.NamespaceAccess\x12\x18\n\x10inherited_access\x18\x04 \x01(\x08\x12\x18\n\x10resource_version\x18\x05 \x01(\t"\xbc\x01\n!ServiceAccountNamespaceAssignment\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12I\n\x10namespace_access\x18\x03 \x01(\x0b\x32/.temporal.api.cloud.identity.v1.NamespaceAccess\x12\x18\n\x10inherited_access\x18\x04 \x01(\x08\x12\x18\n\x10resource_version\x18\x05 \x01(\t"\xbf\x01\n\x1cUserGroupNamespaceAssignment\x12\n\n\x02id\x18\x01 \x01(\t\x12\x14\n\x0c\x64isplay_name\x18\x02 \x01(\t\x12I\n\x10namespace_access\x18\x03 \x01(\x0b\x32/.temporal.api.cloud.identity.v1.NamespaceAccess\x12\x18\n\x10inherited_access\x18\x04 \x01(\x08\x12\x18\n\x10resource_version\x18\x05 \x01(\t*\\\n\tOwnerType\x12\x1a\n\x16OWNER_TYPE_UNSPECIFIED\x10\x00\x12\x13\n\x0fOWNER_TYPE_USER\x10\x01\x12\x1e\n\x1aOWNER_TYPE_SERVICE_ACCOUNT\x10\x02\x42\xac\x01\n!io.temporal.api.cloud.identity.v1B\x0cMessageProtoP\x01Z-go.temporal.io/api/cloud/identity/v1;identity\xaa\x02 Temporalio.Api.Cloud.Identity.V1\xea\x02$Temporalio::Api::Cloud::Identity::V1b\x06proto3' ) _OWNERTYPE = DESCRIPTOR.enum_types_by_name["OwnerType"] @@ -51,6 +51,17 @@ _SERVICEACCOUNTSPEC = DESCRIPTOR.message_types_by_name["ServiceAccountSpec"] _APIKEY = DESCRIPTOR.message_types_by_name["ApiKey"] _APIKEYSPEC = DESCRIPTOR.message_types_by_name["ApiKeySpec"] +_CUSTOMROLESPEC = DESCRIPTOR.message_types_by_name["CustomRoleSpec"] +_CUSTOMROLESPEC_RESOURCES = _CUSTOMROLESPEC.nested_types_by_name["Resources"] +_CUSTOMROLESPEC_PERMISSION = _CUSTOMROLESPEC.nested_types_by_name["Permission"] +_CUSTOMROLE = DESCRIPTOR.message_types_by_name["CustomRole"] +_USERNAMESPACEASSIGNMENT = DESCRIPTOR.message_types_by_name["UserNamespaceAssignment"] +_SERVICEACCOUNTNAMESPACEASSIGNMENT = DESCRIPTOR.message_types_by_name[ + "ServiceAccountNamespaceAssignment" +] +_USERGROUPNAMESPACEASSIGNMENT = DESCRIPTOR.message_types_by_name[ + "UserGroupNamespaceAssignment" +] _ACCOUNTACCESS_ROLE = _ACCOUNTACCESS.enum_types_by_name["Role"] _NAMESPACEACCESS_PERMISSION = _NAMESPACEACCESS.enum_types_by_name["Permission"] AccountAccess = _reflection.GeneratedProtocolMessageType( @@ -58,7 +69,7 @@ (_message.Message,), { "DESCRIPTOR": _ACCOUNTACCESS, - "__module__": "temporal.api.cloud.identity.v1.message_pb2", + "__module__": "temporalio.api.cloud.identity.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.identity.v1.AccountAccess) }, ) @@ -69,7 +80,7 @@ (_message.Message,), { "DESCRIPTOR": _NAMESPACEACCESS, - "__module__": "temporal.api.cloud.identity.v1.message_pb2", + "__module__": "temporalio.api.cloud.identity.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.identity.v1.NamespaceAccess) }, ) @@ -84,12 +95,12 @@ (_message.Message,), { "DESCRIPTOR": _ACCESS_NAMESPACEACCESSESENTRY, - "__module__": "temporal.api.cloud.identity.v1.message_pb2", + "__module__": "temporalio.api.cloud.identity.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.identity.v1.Access.NamespaceAccessesEntry) }, ), "DESCRIPTOR": _ACCESS, - "__module__": "temporal.api.cloud.identity.v1.message_pb2", + "__module__": "temporalio.api.cloud.identity.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.identity.v1.Access) }, ) @@ -101,7 +112,7 @@ (_message.Message,), { "DESCRIPTOR": _NAMESPACESCOPEDACCESS, - "__module__": "temporal.api.cloud.identity.v1.message_pb2", + "__module__": "temporalio.api.cloud.identity.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.identity.v1.NamespaceScopedAccess) }, ) @@ -112,7 +123,7 @@ (_message.Message,), { "DESCRIPTOR": _USERSPEC, - "__module__": "temporal.api.cloud.identity.v1.message_pb2", + "__module__": "temporalio.api.cloud.identity.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.identity.v1.UserSpec) }, ) @@ -123,7 +134,7 @@ (_message.Message,), { "DESCRIPTOR": _INVITATION, - "__module__": "temporal.api.cloud.identity.v1.message_pb2", + "__module__": "temporalio.api.cloud.identity.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.identity.v1.Invitation) }, ) @@ -134,7 +145,7 @@ (_message.Message,), { "DESCRIPTOR": _USER, - "__module__": "temporal.api.cloud.identity.v1.message_pb2", + "__module__": "temporalio.api.cloud.identity.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.identity.v1.User) }, ) @@ -145,7 +156,7 @@ (_message.Message,), { "DESCRIPTOR": _GOOGLEGROUPSPEC, - "__module__": "temporal.api.cloud.identity.v1.message_pb2", + "__module__": "temporalio.api.cloud.identity.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.identity.v1.GoogleGroupSpec) }, ) @@ -156,7 +167,7 @@ (_message.Message,), { "DESCRIPTOR": _SCIMGROUPSPEC, - "__module__": "temporal.api.cloud.identity.v1.message_pb2", + "__module__": "temporalio.api.cloud.identity.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.identity.v1.SCIMGroupSpec) }, ) @@ -167,7 +178,7 @@ (_message.Message,), { "DESCRIPTOR": _CLOUDGROUPSPEC, - "__module__": "temporal.api.cloud.identity.v1.message_pb2", + "__module__": "temporalio.api.cloud.identity.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.identity.v1.CloudGroupSpec) }, ) @@ -178,7 +189,7 @@ (_message.Message,), { "DESCRIPTOR": _USERGROUPSPEC, - "__module__": "temporal.api.cloud.identity.v1.message_pb2", + "__module__": "temporalio.api.cloud.identity.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.identity.v1.UserGroupSpec) }, ) @@ -189,7 +200,7 @@ (_message.Message,), { "DESCRIPTOR": _USERGROUP, - "__module__": "temporal.api.cloud.identity.v1.message_pb2", + "__module__": "temporalio.api.cloud.identity.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.identity.v1.UserGroup) }, ) @@ -200,7 +211,7 @@ (_message.Message,), { "DESCRIPTOR": _USERGROUPMEMBERID, - "__module__": "temporal.api.cloud.identity.v1.message_pb2", + "__module__": "temporalio.api.cloud.identity.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.identity.v1.UserGroupMemberId) }, ) @@ -211,7 +222,7 @@ (_message.Message,), { "DESCRIPTOR": _USERGROUPMEMBER, - "__module__": "temporal.api.cloud.identity.v1.message_pb2", + "__module__": "temporalio.api.cloud.identity.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.identity.v1.UserGroupMember) }, ) @@ -222,7 +233,7 @@ (_message.Message,), { "DESCRIPTOR": _SERVICEACCOUNT, - "__module__": "temporal.api.cloud.identity.v1.message_pb2", + "__module__": "temporalio.api.cloud.identity.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.identity.v1.ServiceAccount) }, ) @@ -233,7 +244,7 @@ (_message.Message,), { "DESCRIPTOR": _SERVICEACCOUNTSPEC, - "__module__": "temporal.api.cloud.identity.v1.message_pb2", + "__module__": "temporalio.api.cloud.identity.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.identity.v1.ServiceAccountSpec) }, ) @@ -244,7 +255,7 @@ (_message.Message,), { "DESCRIPTOR": _APIKEY, - "__module__": "temporal.api.cloud.identity.v1.message_pb2", + "__module__": "temporalio.api.cloud.identity.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.identity.v1.ApiKey) }, ) @@ -255,12 +266,87 @@ (_message.Message,), { "DESCRIPTOR": _APIKEYSPEC, - "__module__": "temporal.api.cloud.identity.v1.message_pb2", + "__module__": "temporalio.api.cloud.identity.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.identity.v1.ApiKeySpec) }, ) _sym_db.RegisterMessage(ApiKeySpec) +CustomRoleSpec = _reflection.GeneratedProtocolMessageType( + "CustomRoleSpec", + (_message.Message,), + { + "Resources": _reflection.GeneratedProtocolMessageType( + "Resources", + (_message.Message,), + { + "DESCRIPTOR": _CUSTOMROLESPEC_RESOURCES, + "__module__": "temporalio.api.cloud.identity.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.identity.v1.CustomRoleSpec.Resources) + }, + ), + "Permission": _reflection.GeneratedProtocolMessageType( + "Permission", + (_message.Message,), + { + "DESCRIPTOR": _CUSTOMROLESPEC_PERMISSION, + "__module__": "temporalio.api.cloud.identity.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.identity.v1.CustomRoleSpec.Permission) + }, + ), + "DESCRIPTOR": _CUSTOMROLESPEC, + "__module__": "temporalio.api.cloud.identity.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.identity.v1.CustomRoleSpec) + }, +) +_sym_db.RegisterMessage(CustomRoleSpec) +_sym_db.RegisterMessage(CustomRoleSpec.Resources) +_sym_db.RegisterMessage(CustomRoleSpec.Permission) + +CustomRole = _reflection.GeneratedProtocolMessageType( + "CustomRole", + (_message.Message,), + { + "DESCRIPTOR": _CUSTOMROLE, + "__module__": "temporalio.api.cloud.identity.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.identity.v1.CustomRole) + }, +) +_sym_db.RegisterMessage(CustomRole) + +UserNamespaceAssignment = _reflection.GeneratedProtocolMessageType( + "UserNamespaceAssignment", + (_message.Message,), + { + "DESCRIPTOR": _USERNAMESPACEASSIGNMENT, + "__module__": "temporalio.api.cloud.identity.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.identity.v1.UserNamespaceAssignment) + }, +) +_sym_db.RegisterMessage(UserNamespaceAssignment) + +ServiceAccountNamespaceAssignment = _reflection.GeneratedProtocolMessageType( + "ServiceAccountNamespaceAssignment", + (_message.Message,), + { + "DESCRIPTOR": _SERVICEACCOUNTNAMESPACEASSIGNMENT, + "__module__": "temporalio.api.cloud.identity.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.identity.v1.ServiceAccountNamespaceAssignment) + }, +) +_sym_db.RegisterMessage(ServiceAccountNamespaceAssignment) + +UserGroupNamespaceAssignment = _reflection.GeneratedProtocolMessageType( + "UserGroupNamespaceAssignment", + (_message.Message,), + { + "DESCRIPTOR": _USERGROUPNAMESPACEASSIGNMENT, + "__module__": "temporalio.api.cloud.identity.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.identity.v1.UserGroupNamespaceAssignment) + }, +) +_sym_db.RegisterMessage(UserGroupNamespaceAssignment) + if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b"\n!io.temporal.api.cloud.identity.v1B\014MessageProtoP\001Z-go.temporal.io/api/cloud/identity/v1;identity\252\002 Temporalio.Api.Cloud.Identity.V1\352\002$Temporalio::Api::Cloud::Identity::V1" @@ -272,6 +358,8 @@ ]._serialized_options = b"\030\001" _ACCESS_NAMESPACEACCESSESENTRY._options = None _ACCESS_NAMESPACEACCESSESENTRY._serialized_options = b"8\001" + _ACCESS.fields_by_name["custom_roles_deprecated"]._options = None + _ACCESS.fields_by_name["custom_roles_deprecated"]._serialized_options = b"\030\001" _USER.fields_by_name["state_deprecated"]._options = None _USER.fields_by_name["state_deprecated"]._serialized_options = b"\030\001" _USERGROUP.fields_by_name["state_deprecated"]._options = None @@ -284,48 +372,62 @@ _APIKEYSPEC.fields_by_name[ "owner_type_deprecated" ]._serialized_options = b"\030\001" - _OWNERTYPE._serialized_start = 3713 - _OWNERTYPE._serialized_end = 3805 + _OWNERTYPE._serialized_start = 4969 + _OWNERTYPE._serialized_end = 5061 _ACCOUNTACCESS._serialized_start = 160 - _ACCOUNTACCESS._serialized_end = 415 - _ACCOUNTACCESS_ROLE._serialized_start = 273 - _ACCOUNTACCESS_ROLE._serialized_end = 415 - _NAMESPACEACCESS._serialized_start = 418 - _NAMESPACEACCESS._serialized_end = 657 - _NAMESPACEACCESS_PERMISSION._serialized_start = 552 - _NAMESPACEACCESS_PERMISSION._serialized_end = 657 - _ACCESS._serialized_start = 660 - _ACCESS._serialized_end = 937 - _ACCESS_NAMESPACEACCESSESENTRY._serialized_start = 832 - _ACCESS_NAMESPACEACCESSESENTRY._serialized_end = 937 - _NAMESPACESCOPEDACCESS._serialized_start = 939 - _NAMESPACESCOPEDACCESS._serialized_end = 1046 - _USERSPEC._serialized_start = 1048 - _USERSPEC._serialized_end = 1129 - _INVITATION._serialized_start = 1131 - _INVITATION._serialized_end = 1243 - _USER._serialized_start = 1246 - _USER._serialized_end = 1636 - _GOOGLEGROUPSPEC._serialized_start = 1638 - _GOOGLEGROUPSPEC._serialized_end = 1678 - _SCIMGROUPSPEC._serialized_start = 1680 - _SCIMGROUPSPEC._serialized_end = 1711 - _CLOUDGROUPSPEC._serialized_start = 1713 - _CLOUDGROUPSPEC._serialized_end = 1729 - _USERGROUPSPEC._serialized_start = 1732 - _USERGROUPSPEC._serialized_end = 2052 - _USERGROUP._serialized_start = 2055 - _USERGROUP._serialized_end = 2391 - _USERGROUPMEMBERID._serialized_start = 2393 - _USERGROUPMEMBERID._serialized_end = 2446 - _USERGROUPMEMBER._serialized_start = 2449 - _USERGROUPMEMBER._serialized_end = 2586 - _SERVICEACCOUNT._serialized_start = 2589 - _SERVICEACCOUNT._serialized_end = 2935 - _SERVICEACCOUNTSPEC._serialized_start = 2938 - _SERVICEACCOUNTSPEC._serialized_end = 3137 - _APIKEY._serialized_start = 3140 - _APIKEY._serialized_end = 3470 - _APIKEYSPEC._serialized_start = 3473 - _APIKEYSPEC._serialized_end = 3711 + _ACCOUNTACCESS._serialized_end = 437 + _ACCOUNTACCESS_ROLE._serialized_start = 295 + _ACCOUNTACCESS_ROLE._serialized_end = 437 + _NAMESPACEACCESS._serialized_start = 440 + _NAMESPACEACCESS._serialized_end = 679 + _NAMESPACEACCESS_PERMISSION._serialized_start = 574 + _NAMESPACEACCESS_PERMISSION._serialized_end = 679 + _ACCESS._serialized_start = 682 + _ACCESS._serialized_end = 996 + _ACCESS_NAMESPACEACCESSESENTRY._serialized_start = 891 + _ACCESS_NAMESPACEACCESSESENTRY._serialized_end = 996 + _NAMESPACESCOPEDACCESS._serialized_start = 998 + _NAMESPACESCOPEDACCESS._serialized_end = 1105 + _USERSPEC._serialized_start = 1107 + _USERSPEC._serialized_end = 1188 + _INVITATION._serialized_start = 1190 + _INVITATION._serialized_end = 1302 + _USER._serialized_start = 1305 + _USER._serialized_end = 1695 + _GOOGLEGROUPSPEC._serialized_start = 1697 + _GOOGLEGROUPSPEC._serialized_end = 1737 + _SCIMGROUPSPEC._serialized_start = 1739 + _SCIMGROUPSPEC._serialized_end = 1770 + _CLOUDGROUPSPEC._serialized_start = 1772 + _CLOUDGROUPSPEC._serialized_end = 1788 + _USERGROUPSPEC._serialized_start = 1791 + _USERGROUPSPEC._serialized_end = 2111 + _USERGROUP._serialized_start = 2114 + _USERGROUP._serialized_end = 2450 + _USERGROUPMEMBERID._serialized_start = 2452 + _USERGROUPMEMBERID._serialized_end = 2505 + _USERGROUPMEMBER._serialized_start = 2508 + _USERGROUPMEMBER._serialized_end = 2645 + _SERVICEACCOUNT._serialized_start = 2648 + _SERVICEACCOUNT._serialized_end = 2994 + _SERVICEACCOUNTSPEC._serialized_start = 2997 + _SERVICEACCOUNTSPEC._serialized_end = 3196 + _APIKEY._serialized_start = 3199 + _APIKEY._serialized_end = 3529 + _APIKEYSPEC._serialized_start = 3532 + _APIKEYSPEC._serialized_end = 3770 + _CUSTOMROLESPEC._serialized_start = 3773 + _CUSTOMROLESPEC._serialized_end = 4089 + _CUSTOMROLESPEC_RESOURCES._serialized_start = 3906 + _CUSTOMROLESPEC_RESOURCES._serialized_end = 3981 + _CUSTOMROLESPEC_PERMISSION._serialized_start = 3983 + _CUSTOMROLESPEC_PERMISSION._serialized_end = 4089 + _CUSTOMROLE._serialized_start = 4092 + _CUSTOMROLE._serialized_end = 4400 + _USERNAMESPACEASSIGNMENT._serialized_start = 4403 + _USERNAMESPACEASSIGNMENT._serialized_end = 4582 + _SERVICEACCOUNTNAMESPACEASSIGNMENT._serialized_start = 4585 + _SERVICEACCOUNTNAMESPACEASSIGNMENT._serialized_end = 4773 + _USERGROUPNAMESPACEASSIGNMENT._serialized_start = 4776 + _USERGROUPNAMESPACEASSIGNMENT._serialized_end = 4967 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/cloud/identity/v1/message_pb2.pyi b/temporalio/api/cloud/identity/v1/message_pb2.pyi index ced77ff78..5471475f6 100644 --- a/temporalio/api/cloud/identity/v1/message_pb2.pyi +++ b/temporalio/api/cloud/identity/v1/message_pb2.pyi @@ -92,6 +92,7 @@ class AccountAccess(google.protobuf.message.Message): ROLE_DEPRECATED_FIELD_NUMBER: builtins.int ROLE_FIELD_NUMBER: builtins.int + CUSTOM_ROLES_FIELD_NUMBER: builtins.int role_deprecated: builtins.str """The role on the account, should be one of [owner, admin, developer, financeadmin, read, metricsread] owner - gives full access to the account, including users, namespaces, and billing @@ -108,16 +109,29 @@ class AccountAccess(google.protobuf.message.Message): temporal:versioning:min_version=v0.3.0 temporal:enums:replaces=role_deprecated """ + @property + def custom_roles( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """List of custom role IDs assigned to the user or service account. + temporal:versioning:min_version=v0.13.0 + """ def __init__( self, *, role_deprecated: builtins.str = ..., role: global___AccountAccess.Role.ValueType = ..., + custom_roles: collections.abc.Iterable[builtins.str] | None = ..., ) -> None: ... def ClearField( self, field_name: typing_extensions.Literal[ - "role", b"role", "role_deprecated", b"role_deprecated" + "custom_roles", + b"custom_roles", + "role", + b"role", + "role_deprecated", + b"role_deprecated", ], ) -> None: ... @@ -214,6 +228,7 @@ class Access(google.protobuf.message.Message): ACCOUNT_ACCESS_FIELD_NUMBER: builtins.int NAMESPACE_ACCESSES_FIELD_NUMBER: builtins.int + CUSTOM_ROLES_DEPRECATED_FIELD_NUMBER: builtins.int @property def account_access(self) -> global___AccountAccess: """The account access""" @@ -226,6 +241,14 @@ class Access(google.protobuf.message.Message): """The map of namespace accesses The key is the namespace name and the value is the access to the namespace """ + @property + def custom_roles_deprecated( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """List of custom role IDs assigned to the user or service account. + Deprecated: Not supported after v0.12.0 api version. Use account_access.custom_roles instead. + temporal:versioning:max_version=v0.12.0 + """ def __init__( self, *, @@ -234,6 +257,7 @@ class Access(google.protobuf.message.Message): builtins.str, global___NamespaceAccess ] | None = ..., + custom_roles_deprecated: collections.abc.Iterable[builtins.str] | None = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal["account_access", b"account_access"] @@ -243,6 +267,8 @@ class Access(google.protobuf.message.Message): field_name: typing_extensions.Literal[ "account_access", b"account_access", + "custom_roles_deprecated", + b"custom_roles_deprecated", "namespace_accesses", b"namespace_accesses", ], @@ -1009,3 +1035,336 @@ class ApiKeySpec(google.protobuf.message.Message): ) -> None: ... global___ApiKeySpec = ApiKeySpec + +class CustomRoleSpec(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class Resources(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + RESOURCE_TYPE_FIELD_NUMBER: builtins.int + RESOURCE_IDS_FIELD_NUMBER: builtins.int + ALLOW_ALL_FIELD_NUMBER: builtins.int + resource_type: builtins.str + """The resource type the permission applies to.""" + @property + def resource_ids( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[ + builtins.str + ]: + """The resource IDs the permission applies to. Can be empty if allow_all is true.""" + allow_all: builtins.bool + """Whether the permission applies to all resources of the given type.""" + def __init__( + self, + *, + resource_type: builtins.str = ..., + resource_ids: collections.abc.Iterable[builtins.str] | None = ..., + allow_all: builtins.bool = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "allow_all", + b"allow_all", + "resource_ids", + b"resource_ids", + "resource_type", + b"resource_type", + ], + ) -> None: ... + + class Permission(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + RESOURCES_FIELD_NUMBER: builtins.int + ACTIONS_FIELD_NUMBER: builtins.int + @property + def resources(self) -> global___CustomRoleSpec.Resources: + """The resources the permission applies to.""" + @property + def actions( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[ + builtins.str + ]: + """The actions allowed by the permission.""" + def __init__( + self, + *, + resources: global___CustomRoleSpec.Resources | None = ..., + actions: collections.abc.Iterable[builtins.str] | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["resources", b"resources"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "actions", b"actions", "resources", b"resources" + ], + ) -> None: ... + + NAME_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + PERMISSIONS_FIELD_NUMBER: builtins.int + name: builtins.str + """The name of the custom role.""" + description: builtins.str + """The description of the custom role.""" + @property + def permissions( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + global___CustomRoleSpec.Permission + ]: + """The permissions assigned to the custom role.""" + def __init__( + self, + *, + name: builtins.str = ..., + description: builtins.str = ..., + permissions: collections.abc.Iterable[global___CustomRoleSpec.Permission] + | None = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "description", + b"description", + "name", + b"name", + "permissions", + b"permissions", + ], + ) -> None: ... + +global___CustomRoleSpec = CustomRoleSpec + +class CustomRole(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ID_FIELD_NUMBER: builtins.int + RESOURCE_VERSION_FIELD_NUMBER: builtins.int + SPEC_FIELD_NUMBER: builtins.int + STATE_FIELD_NUMBER: builtins.int + ASYNC_OPERATION_ID_FIELD_NUMBER: builtins.int + CREATED_TIME_FIELD_NUMBER: builtins.int + LAST_MODIFIED_TIME_FIELD_NUMBER: builtins.int + id: builtins.str + """The id of the custom role.""" + resource_version: builtins.str + """The current version of the custom role specification. + The next update operation will have to include this version. + """ + @property + def spec(self) -> global___CustomRoleSpec: + """The custom role specification.""" + state: temporalio.api.cloud.resource.v1.message_pb2.ResourceState.ValueType + """The current state of the custom role. + For any failed state, reach out to Temporal Cloud support for remediation. + """ + async_operation_id: builtins.str + """The id of the async operation that is creating/updating/deleting the custom role, if any.""" + @property + def created_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """The date and time when the custom role was created.""" + @property + def last_modified_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """The date and time when the custom role was last modified. + Will not be set if the custom role has never been modified. + """ + def __init__( + self, + *, + id: builtins.str = ..., + resource_version: builtins.str = ..., + spec: global___CustomRoleSpec | None = ..., + state: temporalio.api.cloud.resource.v1.message_pb2.ResourceState.ValueType = ..., + async_operation_id: builtins.str = ..., + created_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + last_modified_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "created_time", + b"created_time", + "last_modified_time", + b"last_modified_time", + "spec", + b"spec", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "async_operation_id", + b"async_operation_id", + "created_time", + b"created_time", + "id", + b"id", + "last_modified_time", + b"last_modified_time", + "resource_version", + b"resource_version", + "spec", + b"spec", + "state", + b"state", + ], + ) -> None: ... + +global___CustomRole = CustomRole + +class UserNamespaceAssignment(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ID_FIELD_NUMBER: builtins.int + EMAIL_FIELD_NUMBER: builtins.int + NAMESPACE_ACCESS_FIELD_NUMBER: builtins.int + INHERITED_ACCESS_FIELD_NUMBER: builtins.int + RESOURCE_VERSION_FIELD_NUMBER: builtins.int + id: builtins.str + """The ID of the user.""" + email: builtins.str + """The email of the user.""" + @property + def namespace_access(self) -> global___NamespaceAccess: + """The access assigned to the user at the namespace level.""" + inherited_access: builtins.bool + """True if the user has inherited access to the namespace through an account or project role.""" + resource_version: builtins.str + """The current resource version of the user.""" + def __init__( + self, + *, + id: builtins.str = ..., + email: builtins.str = ..., + namespace_access: global___NamespaceAccess | None = ..., + inherited_access: builtins.bool = ..., + resource_version: builtins.str = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["namespace_access", b"namespace_access"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "email", + b"email", + "id", + b"id", + "inherited_access", + b"inherited_access", + "namespace_access", + b"namespace_access", + "resource_version", + b"resource_version", + ], + ) -> None: ... + +global___UserNamespaceAssignment = UserNamespaceAssignment + +class ServiceAccountNamespaceAssignment(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ID_FIELD_NUMBER: builtins.int + NAME_FIELD_NUMBER: builtins.int + NAMESPACE_ACCESS_FIELD_NUMBER: builtins.int + INHERITED_ACCESS_FIELD_NUMBER: builtins.int + RESOURCE_VERSION_FIELD_NUMBER: builtins.int + id: builtins.str + """The ID of the service account.""" + name: builtins.str + """The name of the service account.""" + @property + def namespace_access(self) -> global___NamespaceAccess: + """The access assigned to the service account at the namespace level.""" + inherited_access: builtins.bool + """True if the service account has inherited access to the namespace through an account or project role.""" + resource_version: builtins.str + """The current resource version of the service account.""" + def __init__( + self, + *, + id: builtins.str = ..., + name: builtins.str = ..., + namespace_access: global___NamespaceAccess | None = ..., + inherited_access: builtins.bool = ..., + resource_version: builtins.str = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["namespace_access", b"namespace_access"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "id", + b"id", + "inherited_access", + b"inherited_access", + "name", + b"name", + "namespace_access", + b"namespace_access", + "resource_version", + b"resource_version", + ], + ) -> None: ... + +global___ServiceAccountNamespaceAssignment = ServiceAccountNamespaceAssignment + +class UserGroupNamespaceAssignment(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ID_FIELD_NUMBER: builtins.int + DISPLAY_NAME_FIELD_NUMBER: builtins.int + NAMESPACE_ACCESS_FIELD_NUMBER: builtins.int + INHERITED_ACCESS_FIELD_NUMBER: builtins.int + RESOURCE_VERSION_FIELD_NUMBER: builtins.int + id: builtins.str + """The ID of the group.""" + display_name: builtins.str + """The display name of the group.""" + @property + def namespace_access(self) -> global___NamespaceAccess: + """The access assigned to the group at the namespace level.""" + inherited_access: builtins.bool + """True if the group has inherited access to the namespace through an account or project role.""" + resource_version: builtins.str + """The current resource version of the group.""" + def __init__( + self, + *, + id: builtins.str = ..., + display_name: builtins.str = ..., + namespace_access: global___NamespaceAccess | None = ..., + inherited_access: builtins.bool = ..., + resource_version: builtins.str = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["namespace_access", b"namespace_access"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "display_name", + b"display_name", + "id", + b"id", + "inherited_access", + b"inherited_access", + "namespace_access", + b"namespace_access", + "resource_version", + b"resource_version", + ], + ) -> None: ... + +global___UserGroupNamespaceAssignment = UserGroupNamespaceAssignment diff --git a/temporalio/api/cloud/namespace/v1/__init__.py b/temporalio/api/cloud/namespace/v1/__init__.py index ed81cf865..04e5e98f8 100644 --- a/temporalio/api/cloud/namespace/v1/__init__.py +++ b/temporalio/api/cloud/namespace/v1/__init__.py @@ -1,35 +1,47 @@ from .message_pb2 import ( ApiKeyAuthSpec, AWSPrivateLinkInfo, + Capacity, + CapacitySpec, CertificateFilterSpec, CodecServerSpec, Endpoints, ExportSink, ExportSinkSpec, + FairnessSpec, HighAvailabilitySpec, LifecycleSpec, Limits, MtlsAuthSpec, Namespace, + NamespaceCapacityInfo, NamespaceRegionStatus, NamespaceSpec, PrivateConnectivity, + Replica, + ReplicaSpec, ) __all__ = [ "AWSPrivateLinkInfo", "ApiKeyAuthSpec", + "Capacity", + "CapacitySpec", "CertificateFilterSpec", "CodecServerSpec", "Endpoints", "ExportSink", "ExportSinkSpec", + "FairnessSpec", "HighAvailabilitySpec", "LifecycleSpec", "Limits", "MtlsAuthSpec", "Namespace", + "NamespaceCapacityInfo", "NamespaceRegionStatus", "NamespaceSpec", "PrivateConnectivity", + "Replica", + "ReplicaSpec", ] diff --git a/temporalio/api/cloud/namespace/v1/message_pb2.py b/temporalio/api/cloud/namespace/v1/message_pb2.py index 827ad10f4..33eb2f789 100644 --- a/temporalio/api/cloud/namespace/v1/message_pb2.py +++ b/temporalio/api/cloud/namespace/v1/message_pb2.py @@ -27,13 +27,14 @@ ) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n-temporal/api/cloud/namespace/v1/message.proto\x12\x1ftemporal.api.cloud.namespace.v1\x1a,temporal/api/cloud/resource/v1/message.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a(temporal/api/cloud/sink/v1/message.proto\x1a\x34temporal/api/cloud/connectivityrule/v1/message.proto"\x81\x01\n\x15\x43\x65rtificateFilterSpec\x12\x13\n\x0b\x63ommon_name\x18\x01 \x01(\t\x12\x14\n\x0corganization\x18\x02 \x01(\t\x12\x1b\n\x13organizational_unit\x18\x03 \x01(\t\x12 \n\x18subject_alternative_name\x18\x04 \x01(\t"\xb7\x01\n\x0cMtlsAuthSpec\x12%\n\x1d\x61\x63\x63\x65pted_client_ca_deprecated\x18\x01 \x01(\t\x12\x1a\n\x12\x61\x63\x63\x65pted_client_ca\x18\x04 \x01(\x0c\x12S\n\x13\x63\x65rtificate_filters\x18\x02 \x03(\x0b\x32\x36.temporal.api.cloud.namespace.v1.CertificateFilterSpec\x12\x0f\n\x07\x65nabled\x18\x03 \x01(\x08"!\n\x0e\x41piKeyAuthSpec\x12\x0f\n\x07\x65nabled\x18\x01 \x01(\x08"\xf4\x02\n\x0f\x43odecServerSpec\x12\x10\n\x08\x65ndpoint\x18\x01 \x01(\t\x12\x19\n\x11pass_access_token\x18\x02 \x01(\x08\x12(\n include_cross_origin_credentials\x18\x03 \x01(\x08\x12\x61\n\x14\x63ustom_error_message\x18\x04 \x01(\x0b\x32\x43.temporal.api.cloud.namespace.v1.CodecServerSpec.CustomErrorMessage\x1a\xa6\x01\n\x12\x43ustomErrorMessage\x12\x61\n\x07\x64\x65\x66\x61ult\x18\x01 \x01(\x0b\x32P.temporal.api.cloud.namespace.v1.CodecServerSpec.CustomErrorMessage.ErrorMessage\x1a-\n\x0c\x45rrorMessage\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x0c\n\x04link\x18\x02 \x01(\t"1\n\rLifecycleSpec\x12 \n\x18\x65nable_delete_protection\x18\x01 \x01(\x08"8\n\x14HighAvailabilitySpec\x12 \n\x18\x64isable_managed_failover\x18\x01 \x01(\x08"\x89\t\n\rNamespaceSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07regions\x18\x02 \x03(\t\x12\x16\n\x0eretention_days\x18\x03 \x01(\x05\x12@\n\tmtls_auth\x18\x04 \x01(\x0b\x32-.temporal.api.cloud.namespace.v1.MtlsAuthSpec\x12\x45\n\x0c\x61pi_key_auth\x18\x07 \x01(\x0b\x32/.temporal.api.cloud.namespace.v1.ApiKeyAuthSpec\x12p\n\x18\x63ustom_search_attributes\x18\x05 \x03(\x0b\x32J.temporal.api.cloud.namespace.v1.NamespaceSpec.CustomSearchAttributesEntryB\x02\x18\x01\x12_\n\x11search_attributes\x18\x08 \x03(\x0b\x32\x44.temporal.api.cloud.namespace.v1.NamespaceSpec.SearchAttributesEntry\x12\x46\n\x0c\x63odec_server\x18\x06 \x01(\x0b\x32\x30.temporal.api.cloud.namespace.v1.CodecServerSpec\x12\x41\n\tlifecycle\x18\t \x01(\x0b\x32..temporal.api.cloud.namespace.v1.LifecycleSpec\x12P\n\x11high_availability\x18\n \x01(\x0b\x32\x35.temporal.api.cloud.namespace.v1.HighAvailabilitySpec\x12\x1d\n\x15\x63onnectivity_rule_ids\x18\x0b \x03(\t\x1a=\n\x1b\x43ustomSearchAttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a{\n\x15SearchAttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12Q\n\x05value\x18\x02 \x01(\x0e\x32\x42.temporal.api.cloud.namespace.v1.NamespaceSpec.SearchAttributeType:\x02\x38\x01"\xac\x02\n\x13SearchAttributeType\x12%\n!SEARCH_ATTRIBUTE_TYPE_UNSPECIFIED\x10\x00\x12\x1e\n\x1aSEARCH_ATTRIBUTE_TYPE_TEXT\x10\x01\x12!\n\x1dSEARCH_ATTRIBUTE_TYPE_KEYWORD\x10\x02\x12\x1d\n\x19SEARCH_ATTRIBUTE_TYPE_INT\x10\x03\x12 \n\x1cSEARCH_ATTRIBUTE_TYPE_DOUBLE\x10\x04\x12\x1e\n\x1aSEARCH_ATTRIBUTE_TYPE_BOOL\x10\x05\x12"\n\x1eSEARCH_ATTRIBUTE_TYPE_DATETIME\x10\x06\x12&\n"SEARCH_ATTRIBUTE_TYPE_KEYWORD_LIST\x10\x07"Q\n\tEndpoints\x12\x13\n\x0bweb_address\x18\x01 \x01(\t\x12\x19\n\x11mtls_grpc_address\x18\x02 \x01(\t\x12\x14\n\x0cgrpc_address\x18\x03 \x01(\t"*\n\x06Limits\x12 \n\x18\x61\x63tions_per_second_limit\x18\x01 \x01(\x05"X\n\x12\x41WSPrivateLinkInfo\x12\x1e\n\x16\x61llowed_principal_arns\x18\x01 \x03(\t\x12"\n\x1avpc_endpoint_service_names\x18\x02 \x03(\t"t\n\x13PrivateConnectivity\x12\x0e\n\x06region\x18\x01 \x01(\t\x12M\n\x10\x61ws_private_link\x18\x02 \x01(\x0b\x32\x33.temporal.api.cloud.namespace.v1.AWSPrivateLinkInfo"\xc6\x07\n\tNamespace\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12<\n\x04spec\x18\x03 \x01(\x0b\x32..temporal.api.cloud.namespace.v1.NamespaceSpec\x12\x1c\n\x10state_deprecated\x18\x04 \x01(\tB\x02\x18\x01\x12<\n\x05state\x18\r \x01(\x0e\x32-.temporal.api.cloud.resource.v1.ResourceState\x12\x1a\n\x12\x61sync_operation_id\x18\x05 \x01(\t\x12=\n\tendpoints\x18\x06 \x01(\x0b\x32*.temporal.api.cloud.namespace.v1.Endpoints\x12\x15\n\ractive_region\x18\x07 \x01(\t\x12\x37\n\x06limits\x18\x08 \x01(\x0b\x32\'.temporal.api.cloud.namespace.v1.Limits\x12T\n\x16private_connectivities\x18\t \x03(\x0b\x32\x34.temporal.api.cloud.namespace.v1.PrivateConnectivity\x12\x30\n\x0c\x63reated_time\x18\n \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x36\n\x12last_modified_time\x18\x0b \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12S\n\rregion_status\x18\x0c \x03(\x0b\x32<.temporal.api.cloud.namespace.v1.Namespace.RegionStatusEntry\x12T\n\x12\x63onnectivity_rules\x18\x0e \x03(\x0b\x32\x38.temporal.api.cloud.connectivityrule.v1.ConnectivityRule\x12\x42\n\x04tags\x18\x0f \x03(\x0b\x32\x34.temporal.api.cloud.namespace.v1.Namespace.TagsEntry\x1ak\n\x11RegionStatusEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x45\n\x05value\x18\x02 \x01(\x0b\x32\x36.temporal.api.cloud.namespace.v1.NamespaceRegionStatus:\x02\x38\x01\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"\x9b\x02\n\x15NamespaceRegionStatus\x12\x1c\n\x10state_deprecated\x18\x01 \x01(\tB\x02\x18\x01\x12K\n\x05state\x18\x03 \x01(\x0e\x32<.temporal.api.cloud.namespace.v1.NamespaceRegionStatus.State\x12\x1a\n\x12\x61sync_operation_id\x18\x02 \x01(\t"{\n\x05State\x12\x15\n\x11STATE_UNSPECIFIED\x10\x00\x12\x10\n\x0cSTATE_ADDING\x10\x01\x12\x10\n\x0cSTATE_ACTIVE\x10\x02\x12\x11\n\rSTATE_PASSIVE\x10\x03\x12\x12\n\x0eSTATE_REMOVING\x10\x04\x12\x10\n\x0cSTATE_FAILED\x10\x05"\x91\x01\n\x0e\x45xportSinkSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07\x65nabled\x18\x02 \x01(\x08\x12.\n\x02s3\x18\x03 \x01(\x0b\x32".temporal.api.cloud.sink.v1.S3Spec\x12\x30\n\x03gcs\x18\x04 \x01(\x0b\x32#.temporal.api.cloud.sink.v1.GCSSpec"\xf6\x03\n\nExportSink\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12<\n\x05state\x18\x03 \x01(\x0e\x32-.temporal.api.cloud.resource.v1.ResourceState\x12=\n\x04spec\x18\x04 \x01(\x0b\x32/.temporal.api.cloud.namespace.v1.ExportSinkSpec\x12\x42\n\x06health\x18\x05 \x01(\x0e\x32\x32.temporal.api.cloud.namespace.v1.ExportSink.Health\x12\x15\n\rerror_message\x18\x06 \x01(\t\x12;\n\x17latest_data_export_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_health_check_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"o\n\x06Health\x12\x16\n\x12HEALTH_UNSPECIFIED\x10\x00\x12\r\n\tHEALTH_OK\x10\x01\x12\x19\n\x15HEALTH_ERROR_INTERNAL\x10\x02\x12#\n\x1fHEALTH_ERROR_USER_CONFIGURATION\x10\x03\x42\xb1\x01\n"io.temporal.api.cloud.namespace.v1B\x0cMessageProtoP\x01Z/go.temporal.io/api/cloud/namespace/v1;namespace\xaa\x02!Temporalio.Api.Cloud.Namespace.V1\xea\x02%Temporalio::Api::Cloud::Namespace::V1b\x06proto3' + b'\n-temporal/api/cloud/namespace/v1/message.proto\x12\x1ftemporal.api.cloud.namespace.v1\x1a,temporal/api/cloud/resource/v1/message.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a(temporal/api/cloud/sink/v1/message.proto\x1a\x34temporal/api/cloud/connectivityrule/v1/message.proto"\x81\x01\n\x15\x43\x65rtificateFilterSpec\x12\x13\n\x0b\x63ommon_name\x18\x01 \x01(\t\x12\x14\n\x0corganization\x18\x02 \x01(\t\x12\x1b\n\x13organizational_unit\x18\x03 \x01(\t\x12 \n\x18subject_alternative_name\x18\x04 \x01(\t"\xbb\x01\n\x0cMtlsAuthSpec\x12)\n\x1d\x61\x63\x63\x65pted_client_ca_deprecated\x18\x01 \x01(\tB\x02\x18\x01\x12\x1a\n\x12\x61\x63\x63\x65pted_client_ca\x18\x04 \x01(\x0c\x12S\n\x13\x63\x65rtificate_filters\x18\x02 \x03(\x0b\x32\x36.temporal.api.cloud.namespace.v1.CertificateFilterSpec\x12\x0f\n\x07\x65nabled\x18\x03 \x01(\x08"!\n\x0e\x41piKeyAuthSpec\x12\x0f\n\x07\x65nabled\x18\x01 \x01(\x08"1\n\rLifecycleSpec\x12 \n\x18\x65nable_delete_protection\x18\x01 \x01(\x08"\xf4\x02\n\x0f\x43odecServerSpec\x12\x10\n\x08\x65ndpoint\x18\x01 \x01(\t\x12\x19\n\x11pass_access_token\x18\x02 \x01(\x08\x12(\n include_cross_origin_credentials\x18\x03 \x01(\x08\x12\x61\n\x14\x63ustom_error_message\x18\x04 \x01(\x0b\x32\x43.temporal.api.cloud.namespace.v1.CodecServerSpec.CustomErrorMessage\x1a\xa6\x01\n\x12\x43ustomErrorMessage\x12\x61\n\x07\x64\x65\x66\x61ult\x18\x01 \x01(\x0b\x32P.temporal.api.cloud.namespace.v1.CodecServerSpec.CustomErrorMessage.ErrorMessage\x1a-\n\x0c\x45rrorMessage\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x0c\n\x04link\x18\x02 \x01(\t"c\n\x14HighAvailabilitySpec\x12 \n\x18\x64isable_managed_failover\x18\x01 \x01(\x08\x12)\n!disable_passive_poller_forwarding\x18\x02 \x01(\x08"\x1d\n\x0bReplicaSpec\x12\x0e\n\x06region\x18\x01 \x01(\t"\x99\x02\n\x07Replica\x12\n\n\x02id\x18\x01 \x01(\t\x12\x12\n\nis_primary\x18\x02 \x01(\x08\x12\x44\n\x05state\x18\x03 \x01(\x0e\x32\x35.temporal.api.cloud.namespace.v1.Replica.ReplicaState\x12\x0e\n\x06region\x18\x04 \x01(\t"\x97\x01\n\x0cReplicaState\x12\x1d\n\x19REPLICA_STATE_UNSPECIFIED\x10\x00\x12\x18\n\x14REPLICA_STATE_ADDING\x10\x01\x12\x18\n\x14REPLICA_STATE_ACTIVE\x10\x02\x12\x1a\n\x16REPLICA_STATE_REMOVING\x10\x03\x12\x18\n\x14REPLICA_STATE_FAILED\x10\x05"\xdf\x01\n\x0c\x43\x61pacitySpec\x12K\n\ton_demand\x18\x01 \x01(\x0b\x32\x36.temporal.api.cloud.namespace.v1.CapacitySpec.OnDemandH\x00\x12P\n\x0bprovisioned\x18\x02 \x01(\x0b\x32\x39.temporal.api.cloud.namespace.v1.CapacitySpec.ProvisionedH\x00\x1a\n\n\x08OnDemand\x1a\x1c\n\x0bProvisioned\x12\r\n\x05value\x18\x01 \x01(\x01\x42\x06\n\x04spec"\xdc\x05\n\x08\x43\x61pacity\x12G\n\ton_demand\x18\x01 \x01(\x0b\x32\x32.temporal.api.cloud.namespace.v1.Capacity.OnDemandH\x00\x12L\n\x0bprovisioned\x18\x02 \x01(\x0b\x32\x35.temporal.api.cloud.namespace.v1.Capacity.ProvisionedH\x00\x12I\n\x0elatest_request\x18\x03 \x01(\x0b\x32\x31.temporal.api.cloud.namespace.v1.Capacity.Request\x1a\n\n\x08OnDemand\x1a$\n\x0bProvisioned\x12\x15\n\rcurrent_value\x18\x01 \x01(\x01\x1a\xab\x03\n\x07Request\x12\x46\n\x05state\x18\x01 \x01(\x0e\x32\x37.temporal.api.cloud.namespace.v1.Capacity.Request.State\x12.\n\nstart_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12,\n\x08\x65nd_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x1a\n\x12\x61sync_operation_id\x18\x04 \x01(\t\x12;\n\x04spec\x18\x05 \x01(\x0b\x32-.temporal.api.cloud.namespace.v1.CapacitySpec"\xa0\x01\n\x05State\x12&\n"STATE_CAPACITY_REQUEST_UNSPECIFIED\x10\x00\x12$\n STATE_CAPACITY_REQUEST_COMPLETED\x10\x01\x12&\n"STATE_CAPACITY_REQUEST_IN_PROGRESS\x10\x02\x12!\n\x1dSTATE_CAPACITY_REQUEST_FAILED\x10\x03\x42\x0e\n\x0c\x63urrent_mode"3\n\x0c\x46\x61irnessSpec\x12#\n\x1btask_queue_fairness_enabled\x18\x01 \x01(\x08"\xd4\n\n\rNamespaceSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x07regions\x18\x02 \x03(\tB\x02\x18\x01\x12\x16\n\x0eretention_days\x18\x03 \x01(\x05\x12@\n\tmtls_auth\x18\x04 \x01(\x0b\x32-.temporal.api.cloud.namespace.v1.MtlsAuthSpec\x12\x45\n\x0c\x61pi_key_auth\x18\x07 \x01(\x0b\x32/.temporal.api.cloud.namespace.v1.ApiKeyAuthSpec\x12p\n\x18\x63ustom_search_attributes\x18\x05 \x03(\x0b\x32J.temporal.api.cloud.namespace.v1.NamespaceSpec.CustomSearchAttributesEntryB\x02\x18\x01\x12_\n\x11search_attributes\x18\x08 \x03(\x0b\x32\x44.temporal.api.cloud.namespace.v1.NamespaceSpec.SearchAttributesEntry\x12\x46\n\x0c\x63odec_server\x18\x06 \x01(\x0b\x32\x30.temporal.api.cloud.namespace.v1.CodecServerSpec\x12\x41\n\tlifecycle\x18\t \x01(\x0b\x32..temporal.api.cloud.namespace.v1.LifecycleSpec\x12P\n\x11high_availability\x18\n \x01(\x0b\x32\x35.temporal.api.cloud.namespace.v1.HighAvailabilitySpec\x12\x1d\n\x15\x63onnectivity_rule_ids\x18\x0b \x03(\t\x12\x44\n\rcapacity_spec\x18\x0c \x01(\x0b\x32-.temporal.api.cloud.namespace.v1.CapacitySpec\x12>\n\x08replicas\x18\r \x03(\x0b\x32,.temporal.api.cloud.namespace.v1.ReplicaSpec\x12?\n\x08\x66\x61irness\x18\x0e \x01(\x0b\x32-.temporal.api.cloud.namespace.v1.FairnessSpec\x1a=\n\x1b\x43ustomSearchAttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a{\n\x15SearchAttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12Q\n\x05value\x18\x02 \x01(\x0e\x32\x42.temporal.api.cloud.namespace.v1.NamespaceSpec.SearchAttributeType:\x02\x38\x01"\xac\x02\n\x13SearchAttributeType\x12%\n!SEARCH_ATTRIBUTE_TYPE_UNSPECIFIED\x10\x00\x12\x1e\n\x1aSEARCH_ATTRIBUTE_TYPE_TEXT\x10\x01\x12!\n\x1dSEARCH_ATTRIBUTE_TYPE_KEYWORD\x10\x02\x12\x1d\n\x19SEARCH_ATTRIBUTE_TYPE_INT\x10\x03\x12 \n\x1cSEARCH_ATTRIBUTE_TYPE_DOUBLE\x10\x04\x12\x1e\n\x1aSEARCH_ATTRIBUTE_TYPE_BOOL\x10\x05\x12"\n\x1eSEARCH_ATTRIBUTE_TYPE_DATETIME\x10\x06\x12&\n"SEARCH_ATTRIBUTE_TYPE_KEYWORD_LIST\x10\x07"Q\n\tEndpoints\x12\x13\n\x0bweb_address\x18\x01 \x01(\t\x12\x19\n\x11mtls_grpc_address\x18\x02 \x01(\t\x12\x14\n\x0cgrpc_address\x18\x03 \x01(\t"*\n\x06Limits\x12 \n\x18\x61\x63tions_per_second_limit\x18\x01 \x01(\x05"X\n\x12\x41WSPrivateLinkInfo\x12\x1e\n\x16\x61llowed_principal_arns\x18\x01 \x03(\t\x12"\n\x1avpc_endpoint_service_names\x18\x02 \x03(\t"t\n\x13PrivateConnectivity\x12\x0e\n\x06region\x18\x01 \x01(\t\x12M\n\x10\x61ws_private_link\x18\x02 \x01(\x0b\x32\x33.temporal.api.cloud.namespace.v1.AWSPrivateLinkInfo"\xc3\x08\n\tNamespace\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12<\n\x04spec\x18\x03 \x01(\x0b\x32..temporal.api.cloud.namespace.v1.NamespaceSpec\x12\x1c\n\x10state_deprecated\x18\x04 \x01(\tB\x02\x18\x01\x12<\n\x05state\x18\r \x01(\x0e\x32-.temporal.api.cloud.resource.v1.ResourceState\x12\x1a\n\x12\x61sync_operation_id\x18\x05 \x01(\t\x12=\n\tendpoints\x18\x06 \x01(\x0b\x32*.temporal.api.cloud.namespace.v1.Endpoints\x12\x15\n\ractive_region\x18\x07 \x01(\t\x12\x37\n\x06limits\x18\x08 \x01(\x0b\x32\'.temporal.api.cloud.namespace.v1.Limits\x12T\n\x16private_connectivities\x18\t \x03(\x0b\x32\x34.temporal.api.cloud.namespace.v1.PrivateConnectivity\x12\x30\n\x0c\x63reated_time\x18\n \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x36\n\x12last_modified_time\x18\x0b \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12W\n\rregion_status\x18\x0c \x03(\x0b\x32<.temporal.api.cloud.namespace.v1.Namespace.RegionStatusEntryB\x02\x18\x01\x12T\n\x12\x63onnectivity_rules\x18\x0e \x03(\x0b\x32\x38.temporal.api.cloud.connectivityrule.v1.ConnectivityRule\x12\x42\n\x04tags\x18\x0f \x03(\x0b\x32\x34.temporal.api.cloud.namespace.v1.Namespace.TagsEntry\x12;\n\x08\x63\x61pacity\x18\x10 \x01(\x0b\x32).temporal.api.cloud.namespace.v1.Capacity\x12:\n\x08replicas\x18\x12 \x03(\x0b\x32(.temporal.api.cloud.namespace.v1.Replica\x1ak\n\x11RegionStatusEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x45\n\x05value\x18\x02 \x01(\x0b\x32\x36.temporal.api.cloud.namespace.v1.NamespaceRegionStatus:\x02\x38\x01\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"\x9b\x02\n\x15NamespaceRegionStatus\x12\x1c\n\x10state_deprecated\x18\x01 \x01(\tB\x02\x18\x01\x12K\n\x05state\x18\x03 \x01(\x0e\x32<.temporal.api.cloud.namespace.v1.NamespaceRegionStatus.State\x12\x1a\n\x12\x61sync_operation_id\x18\x02 \x01(\t"{\n\x05State\x12\x15\n\x11STATE_UNSPECIFIED\x10\x00\x12\x10\n\x0cSTATE_ADDING\x10\x01\x12\x10\n\x0cSTATE_ACTIVE\x10\x02\x12\x11\n\rSTATE_PASSIVE\x10\x03\x12\x12\n\x0eSTATE_REMOVING\x10\x04\x12\x10\n\x0cSTATE_FAILED\x10\x05"\x91\x01\n\x0e\x45xportSinkSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07\x65nabled\x18\x02 \x01(\x08\x12.\n\x02s3\x18\x03 \x01(\x0b\x32".temporal.api.cloud.sink.v1.S3Spec\x12\x30\n\x03gcs\x18\x04 \x01(\x0b\x32#.temporal.api.cloud.sink.v1.GCSSpec"\xf6\x03\n\nExportSink\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x18\n\x10resource_version\x18\x02 \x01(\t\x12<\n\x05state\x18\x03 \x01(\x0e\x32-.temporal.api.cloud.resource.v1.ResourceState\x12=\n\x04spec\x18\x04 \x01(\x0b\x32/.temporal.api.cloud.namespace.v1.ExportSinkSpec\x12\x42\n\x06health\x18\x05 \x01(\x0e\x32\x32.temporal.api.cloud.namespace.v1.ExportSink.Health\x12\x15\n\rerror_message\x18\x06 \x01(\t\x12;\n\x17latest_data_export_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_health_check_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"o\n\x06Health\x12\x16\n\x12HEALTH_UNSPECIFIED\x10\x00\x12\r\n\tHEALTH_OK\x10\x01\x12\x19\n\x15HEALTH_ERROR_INTERNAL\x10\x02\x12#\n\x1fHEALTH_ERROR_USER_CONFIGURATION\x10\x03"\x9f\x06\n\x15NamespaceCapacityInfo\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x19\n\x11has_legacy_limits\x18\x02 \x01(\x08\x12\x43\n\x10\x63urrent_capacity\x18\x03 \x01(\x0b\x32).temporal.api.cloud.namespace.v1.Capacity\x12`\n\x0cmode_options\x18\x04 \x01(\x0b\x32J.temporal.api.cloud.namespace.v1.NamespaceCapacityInfo.CapacityModeOptions\x12K\n\x05stats\x18\x05 \x01(\x0b\x32<.temporal.api.cloud.namespace.v1.NamespaceCapacityInfo.Stats\x1a\xd3\x02\n\x13\x43\x61pacityModeOptions\x12k\n\x0bprovisioned\x18\x01 \x01(\x0b\x32V.temporal.api.cloud.namespace.v1.NamespaceCapacityInfo.CapacityModeOptions.Provisioned\x12\x66\n\ton_demand\x18\x02 \x01(\x0b\x32S.temporal.api.cloud.namespace.v1.NamespaceCapacityInfo.CapacityModeOptions.OnDemand\x1aH\n\x0bProvisioned\x12\x18\n\x10valid_tru_values\x18\x01 \x03(\x01\x12\x1f\n\x17max_available_tru_value\x18\x02 \x01(\x01\x1a\x1d\n\x08OnDemand\x12\x11\n\taps_limit\x18\x01 \x01(\x01\x1a\x8d\x01\n\x05Stats\x12Q\n\x03\x61ps\x18\x01 \x01(\x0b\x32\x44.temporal.api.cloud.namespace.v1.NamespaceCapacityInfo.Stats.Summary\x1a\x31\n\x07Summary\x12\x0c\n\x04mean\x18\x01 \x01(\x01\x12\x0b\n\x03p90\x18\x02 \x01(\x01\x12\x0b\n\x03p99\x18\x03 \x01(\x01\x42\xb1\x01\n"io.temporal.api.cloud.namespace.v1B\x0cMessageProtoP\x01Z/go.temporal.io/api/cloud/namespace/v1;namespace\xaa\x02!Temporalio.Api.Cloud.Namespace.V1\xea\x02%Temporalio::Api::Cloud::Namespace::V1b\x06proto3' ) _CERTIFICATEFILTERSPEC = DESCRIPTOR.message_types_by_name["CertificateFilterSpec"] _MTLSAUTHSPEC = DESCRIPTOR.message_types_by_name["MtlsAuthSpec"] _APIKEYAUTHSPEC = DESCRIPTOR.message_types_by_name["ApiKeyAuthSpec"] +_LIFECYCLESPEC = DESCRIPTOR.message_types_by_name["LifecycleSpec"] _CODECSERVERSPEC = DESCRIPTOR.message_types_by_name["CodecServerSpec"] _CODECSERVERSPEC_CUSTOMERRORMESSAGE = _CODECSERVERSPEC.nested_types_by_name[ "CustomErrorMessage" @@ -41,8 +42,17 @@ _CODECSERVERSPEC_CUSTOMERRORMESSAGE_ERRORMESSAGE = ( _CODECSERVERSPEC_CUSTOMERRORMESSAGE.nested_types_by_name["ErrorMessage"] ) -_LIFECYCLESPEC = DESCRIPTOR.message_types_by_name["LifecycleSpec"] _HIGHAVAILABILITYSPEC = DESCRIPTOR.message_types_by_name["HighAvailabilitySpec"] +_REPLICASPEC = DESCRIPTOR.message_types_by_name["ReplicaSpec"] +_REPLICA = DESCRIPTOR.message_types_by_name["Replica"] +_CAPACITYSPEC = DESCRIPTOR.message_types_by_name["CapacitySpec"] +_CAPACITYSPEC_ONDEMAND = _CAPACITYSPEC.nested_types_by_name["OnDemand"] +_CAPACITYSPEC_PROVISIONED = _CAPACITYSPEC.nested_types_by_name["Provisioned"] +_CAPACITY = DESCRIPTOR.message_types_by_name["Capacity"] +_CAPACITY_ONDEMAND = _CAPACITY.nested_types_by_name["OnDemand"] +_CAPACITY_PROVISIONED = _CAPACITY.nested_types_by_name["Provisioned"] +_CAPACITY_REQUEST = _CAPACITY.nested_types_by_name["Request"] +_FAIRNESSSPEC = DESCRIPTOR.message_types_by_name["FairnessSpec"] _NAMESPACESPEC = DESCRIPTOR.message_types_by_name["NamespaceSpec"] _NAMESPACESPEC_CUSTOMSEARCHATTRIBUTESENTRY = _NAMESPACESPEC.nested_types_by_name[ "CustomSearchAttributesEntry" @@ -60,6 +70,22 @@ _NAMESPACEREGIONSTATUS = DESCRIPTOR.message_types_by_name["NamespaceRegionStatus"] _EXPORTSINKSPEC = DESCRIPTOR.message_types_by_name["ExportSinkSpec"] _EXPORTSINK = DESCRIPTOR.message_types_by_name["ExportSink"] +_NAMESPACECAPACITYINFO = DESCRIPTOR.message_types_by_name["NamespaceCapacityInfo"] +_NAMESPACECAPACITYINFO_CAPACITYMODEOPTIONS = ( + _NAMESPACECAPACITYINFO.nested_types_by_name["CapacityModeOptions"] +) +_NAMESPACECAPACITYINFO_CAPACITYMODEOPTIONS_PROVISIONED = ( + _NAMESPACECAPACITYINFO_CAPACITYMODEOPTIONS.nested_types_by_name["Provisioned"] +) +_NAMESPACECAPACITYINFO_CAPACITYMODEOPTIONS_ONDEMAND = ( + _NAMESPACECAPACITYINFO_CAPACITYMODEOPTIONS.nested_types_by_name["OnDemand"] +) +_NAMESPACECAPACITYINFO_STATS = _NAMESPACECAPACITYINFO.nested_types_by_name["Stats"] +_NAMESPACECAPACITYINFO_STATS_SUMMARY = ( + _NAMESPACECAPACITYINFO_STATS.nested_types_by_name["Summary"] +) +_REPLICA_REPLICASTATE = _REPLICA.enum_types_by_name["ReplicaState"] +_CAPACITY_REQUEST_STATE = _CAPACITY_REQUEST.enum_types_by_name["State"] _NAMESPACESPEC_SEARCHATTRIBUTETYPE = _NAMESPACESPEC.enum_types_by_name[ "SearchAttributeType" ] @@ -70,7 +96,7 @@ (_message.Message,), { "DESCRIPTOR": _CERTIFICATEFILTERSPEC, - "__module__": "temporal.api.cloud.namespace.v1.message_pb2", + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.CertificateFilterSpec) }, ) @@ -81,7 +107,7 @@ (_message.Message,), { "DESCRIPTOR": _MTLSAUTHSPEC, - "__module__": "temporal.api.cloud.namespace.v1.message_pb2", + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.MtlsAuthSpec) }, ) @@ -92,12 +118,23 @@ (_message.Message,), { "DESCRIPTOR": _APIKEYAUTHSPEC, - "__module__": "temporal.api.cloud.namespace.v1.message_pb2", + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.ApiKeyAuthSpec) }, ) _sym_db.RegisterMessage(ApiKeyAuthSpec) +LifecycleSpec = _reflection.GeneratedProtocolMessageType( + "LifecycleSpec", + (_message.Message,), + { + "DESCRIPTOR": _LIFECYCLESPEC, + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.LifecycleSpec) + }, +) +_sym_db.RegisterMessage(LifecycleSpec) + CodecServerSpec = _reflection.GeneratedProtocolMessageType( "CodecServerSpec", (_message.Message,), @@ -111,17 +148,17 @@ (_message.Message,), { "DESCRIPTOR": _CODECSERVERSPEC_CUSTOMERRORMESSAGE_ERRORMESSAGE, - "__module__": "temporal.api.cloud.namespace.v1.message_pb2", + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.CodecServerSpec.CustomErrorMessage.ErrorMessage) }, ), "DESCRIPTOR": _CODECSERVERSPEC_CUSTOMERRORMESSAGE, - "__module__": "temporal.api.cloud.namespace.v1.message_pb2", + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.CodecServerSpec.CustomErrorMessage) }, ), "DESCRIPTOR": _CODECSERVERSPEC, - "__module__": "temporal.api.cloud.namespace.v1.message_pb2", + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.CodecServerSpec) }, ) @@ -129,28 +166,122 @@ _sym_db.RegisterMessage(CodecServerSpec.CustomErrorMessage) _sym_db.RegisterMessage(CodecServerSpec.CustomErrorMessage.ErrorMessage) -LifecycleSpec = _reflection.GeneratedProtocolMessageType( - "LifecycleSpec", - (_message.Message,), - { - "DESCRIPTOR": _LIFECYCLESPEC, - "__module__": "temporal.api.cloud.namespace.v1.message_pb2", - # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.LifecycleSpec) - }, -) -_sym_db.RegisterMessage(LifecycleSpec) - HighAvailabilitySpec = _reflection.GeneratedProtocolMessageType( "HighAvailabilitySpec", (_message.Message,), { "DESCRIPTOR": _HIGHAVAILABILITYSPEC, - "__module__": "temporal.api.cloud.namespace.v1.message_pb2", + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.HighAvailabilitySpec) }, ) _sym_db.RegisterMessage(HighAvailabilitySpec) +ReplicaSpec = _reflection.GeneratedProtocolMessageType( + "ReplicaSpec", + (_message.Message,), + { + "DESCRIPTOR": _REPLICASPEC, + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.ReplicaSpec) + }, +) +_sym_db.RegisterMessage(ReplicaSpec) + +Replica = _reflection.GeneratedProtocolMessageType( + "Replica", + (_message.Message,), + { + "DESCRIPTOR": _REPLICA, + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.Replica) + }, +) +_sym_db.RegisterMessage(Replica) + +CapacitySpec = _reflection.GeneratedProtocolMessageType( + "CapacitySpec", + (_message.Message,), + { + "OnDemand": _reflection.GeneratedProtocolMessageType( + "OnDemand", + (_message.Message,), + { + "DESCRIPTOR": _CAPACITYSPEC_ONDEMAND, + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.CapacitySpec.OnDemand) + }, + ), + "Provisioned": _reflection.GeneratedProtocolMessageType( + "Provisioned", + (_message.Message,), + { + "DESCRIPTOR": _CAPACITYSPEC_PROVISIONED, + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.CapacitySpec.Provisioned) + }, + ), + "DESCRIPTOR": _CAPACITYSPEC, + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.CapacitySpec) + }, +) +_sym_db.RegisterMessage(CapacitySpec) +_sym_db.RegisterMessage(CapacitySpec.OnDemand) +_sym_db.RegisterMessage(CapacitySpec.Provisioned) + +Capacity = _reflection.GeneratedProtocolMessageType( + "Capacity", + (_message.Message,), + { + "OnDemand": _reflection.GeneratedProtocolMessageType( + "OnDemand", + (_message.Message,), + { + "DESCRIPTOR": _CAPACITY_ONDEMAND, + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.Capacity.OnDemand) + }, + ), + "Provisioned": _reflection.GeneratedProtocolMessageType( + "Provisioned", + (_message.Message,), + { + "DESCRIPTOR": _CAPACITY_PROVISIONED, + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.Capacity.Provisioned) + }, + ), + "Request": _reflection.GeneratedProtocolMessageType( + "Request", + (_message.Message,), + { + "DESCRIPTOR": _CAPACITY_REQUEST, + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.Capacity.Request) + }, + ), + "DESCRIPTOR": _CAPACITY, + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.Capacity) + }, +) +_sym_db.RegisterMessage(Capacity) +_sym_db.RegisterMessage(Capacity.OnDemand) +_sym_db.RegisterMessage(Capacity.Provisioned) +_sym_db.RegisterMessage(Capacity.Request) + +FairnessSpec = _reflection.GeneratedProtocolMessageType( + "FairnessSpec", + (_message.Message,), + { + "DESCRIPTOR": _FAIRNESSSPEC, + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.FairnessSpec) + }, +) +_sym_db.RegisterMessage(FairnessSpec) + NamespaceSpec = _reflection.GeneratedProtocolMessageType( "NamespaceSpec", (_message.Message,), @@ -160,7 +291,7 @@ (_message.Message,), { "DESCRIPTOR": _NAMESPACESPEC_CUSTOMSEARCHATTRIBUTESENTRY, - "__module__": "temporal.api.cloud.namespace.v1.message_pb2", + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.NamespaceSpec.CustomSearchAttributesEntry) }, ), @@ -169,12 +300,12 @@ (_message.Message,), { "DESCRIPTOR": _NAMESPACESPEC_SEARCHATTRIBUTESENTRY, - "__module__": "temporal.api.cloud.namespace.v1.message_pb2", + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.NamespaceSpec.SearchAttributesEntry) }, ), "DESCRIPTOR": _NAMESPACESPEC, - "__module__": "temporal.api.cloud.namespace.v1.message_pb2", + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.NamespaceSpec) }, ) @@ -187,7 +318,7 @@ (_message.Message,), { "DESCRIPTOR": _ENDPOINTS, - "__module__": "temporal.api.cloud.namespace.v1.message_pb2", + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.Endpoints) }, ) @@ -198,7 +329,7 @@ (_message.Message,), { "DESCRIPTOR": _LIMITS, - "__module__": "temporal.api.cloud.namespace.v1.message_pb2", + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.Limits) }, ) @@ -209,7 +340,7 @@ (_message.Message,), { "DESCRIPTOR": _AWSPRIVATELINKINFO, - "__module__": "temporal.api.cloud.namespace.v1.message_pb2", + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.AWSPrivateLinkInfo) }, ) @@ -220,7 +351,7 @@ (_message.Message,), { "DESCRIPTOR": _PRIVATECONNECTIVITY, - "__module__": "temporal.api.cloud.namespace.v1.message_pb2", + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.PrivateConnectivity) }, ) @@ -235,7 +366,7 @@ (_message.Message,), { "DESCRIPTOR": _NAMESPACE_REGIONSTATUSENTRY, - "__module__": "temporal.api.cloud.namespace.v1.message_pb2", + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.Namespace.RegionStatusEntry) }, ), @@ -244,12 +375,12 @@ (_message.Message,), { "DESCRIPTOR": _NAMESPACE_TAGSENTRY, - "__module__": "temporal.api.cloud.namespace.v1.message_pb2", + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.Namespace.TagsEntry) }, ), "DESCRIPTOR": _NAMESPACE, - "__module__": "temporal.api.cloud.namespace.v1.message_pb2", + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.Namespace) }, ) @@ -262,7 +393,7 @@ (_message.Message,), { "DESCRIPTOR": _NAMESPACEREGIONSTATUS, - "__module__": "temporal.api.cloud.namespace.v1.message_pb2", + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.NamespaceRegionStatus) }, ) @@ -273,7 +404,7 @@ (_message.Message,), { "DESCRIPTOR": _EXPORTSINKSPEC, - "__module__": "temporal.api.cloud.namespace.v1.message_pb2", + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.ExportSinkSpec) }, ) @@ -284,19 +415,86 @@ (_message.Message,), { "DESCRIPTOR": _EXPORTSINK, - "__module__": "temporal.api.cloud.namespace.v1.message_pb2", + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.ExportSink) }, ) _sym_db.RegisterMessage(ExportSink) +NamespaceCapacityInfo = _reflection.GeneratedProtocolMessageType( + "NamespaceCapacityInfo", + (_message.Message,), + { + "CapacityModeOptions": _reflection.GeneratedProtocolMessageType( + "CapacityModeOptions", + (_message.Message,), + { + "Provisioned": _reflection.GeneratedProtocolMessageType( + "Provisioned", + (_message.Message,), + { + "DESCRIPTOR": _NAMESPACECAPACITYINFO_CAPACITYMODEOPTIONS_PROVISIONED, + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.NamespaceCapacityInfo.CapacityModeOptions.Provisioned) + }, + ), + "OnDemand": _reflection.GeneratedProtocolMessageType( + "OnDemand", + (_message.Message,), + { + "DESCRIPTOR": _NAMESPACECAPACITYINFO_CAPACITYMODEOPTIONS_ONDEMAND, + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.NamespaceCapacityInfo.CapacityModeOptions.OnDemand) + }, + ), + "DESCRIPTOR": _NAMESPACECAPACITYINFO_CAPACITYMODEOPTIONS, + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.NamespaceCapacityInfo.CapacityModeOptions) + }, + ), + "Stats": _reflection.GeneratedProtocolMessageType( + "Stats", + (_message.Message,), + { + "Summary": _reflection.GeneratedProtocolMessageType( + "Summary", + (_message.Message,), + { + "DESCRIPTOR": _NAMESPACECAPACITYINFO_STATS_SUMMARY, + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.NamespaceCapacityInfo.Stats.Summary) + }, + ), + "DESCRIPTOR": _NAMESPACECAPACITYINFO_STATS, + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.NamespaceCapacityInfo.Stats) + }, + ), + "DESCRIPTOR": _NAMESPACECAPACITYINFO, + "__module__": "temporalio.api.cloud.namespace.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.namespace.v1.NamespaceCapacityInfo) + }, +) +_sym_db.RegisterMessage(NamespaceCapacityInfo) +_sym_db.RegisterMessage(NamespaceCapacityInfo.CapacityModeOptions) +_sym_db.RegisterMessage(NamespaceCapacityInfo.CapacityModeOptions.Provisioned) +_sym_db.RegisterMessage(NamespaceCapacityInfo.CapacityModeOptions.OnDemand) +_sym_db.RegisterMessage(NamespaceCapacityInfo.Stats) +_sym_db.RegisterMessage(NamespaceCapacityInfo.Stats.Summary) + if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'\n"io.temporal.api.cloud.namespace.v1B\014MessageProtoP\001Z/go.temporal.io/api/cloud/namespace/v1;namespace\252\002!Temporalio.Api.Cloud.Namespace.V1\352\002%Temporalio::Api::Cloud::Namespace::V1' + _MTLSAUTHSPEC.fields_by_name["accepted_client_ca_deprecated"]._options = None + _MTLSAUTHSPEC.fields_by_name[ + "accepted_client_ca_deprecated" + ]._serialized_options = b"\030\001" _NAMESPACESPEC_CUSTOMSEARCHATTRIBUTESENTRY._options = None _NAMESPACESPEC_CUSTOMSEARCHATTRIBUTESENTRY._serialized_options = b"8\001" _NAMESPACESPEC_SEARCHATTRIBUTESENTRY._options = None _NAMESPACESPEC_SEARCHATTRIBUTESENTRY._serialized_options = b"8\001" + _NAMESPACESPEC.fields_by_name["regions"]._options = None + _NAMESPACESPEC.fields_by_name["regions"]._serialized_options = b"\030\001" _NAMESPACESPEC.fields_by_name["custom_search_attributes"]._options = None _NAMESPACESPEC.fields_by_name[ "custom_search_attributes" @@ -307,6 +505,8 @@ _NAMESPACE_TAGSENTRY._serialized_options = b"8\001" _NAMESPACE.fields_by_name["state_deprecated"]._options = None _NAMESPACE.fields_by_name["state_deprecated"]._serialized_options = b"\030\001" + _NAMESPACE.fields_by_name["region_status"]._options = None + _NAMESPACE.fields_by_name["region_status"]._serialized_options = b"\030\001" _NAMESPACEREGIONSTATUS.fields_by_name["state_deprecated"]._options = None _NAMESPACEREGIONSTATUS.fields_by_name[ "state_deprecated" @@ -314,49 +514,85 @@ _CERTIFICATEFILTERSPEC._serialized_start = 258 _CERTIFICATEFILTERSPEC._serialized_end = 387 _MTLSAUTHSPEC._serialized_start = 390 - _MTLSAUTHSPEC._serialized_end = 573 - _APIKEYAUTHSPEC._serialized_start = 575 - _APIKEYAUTHSPEC._serialized_end = 608 - _CODECSERVERSPEC._serialized_start = 611 - _CODECSERVERSPEC._serialized_end = 983 - _CODECSERVERSPEC_CUSTOMERRORMESSAGE._serialized_start = 817 - _CODECSERVERSPEC_CUSTOMERRORMESSAGE._serialized_end = 983 - _CODECSERVERSPEC_CUSTOMERRORMESSAGE_ERRORMESSAGE._serialized_start = 938 - _CODECSERVERSPEC_CUSTOMERRORMESSAGE_ERRORMESSAGE._serialized_end = 983 - _LIFECYCLESPEC._serialized_start = 985 - _LIFECYCLESPEC._serialized_end = 1034 - _HIGHAVAILABILITYSPEC._serialized_start = 1036 - _HIGHAVAILABILITYSPEC._serialized_end = 1092 - _NAMESPACESPEC._serialized_start = 1095 - _NAMESPACESPEC._serialized_end = 2256 - _NAMESPACESPEC_CUSTOMSEARCHATTRIBUTESENTRY._serialized_start = 1767 - _NAMESPACESPEC_CUSTOMSEARCHATTRIBUTESENTRY._serialized_end = 1828 - _NAMESPACESPEC_SEARCHATTRIBUTESENTRY._serialized_start = 1830 - _NAMESPACESPEC_SEARCHATTRIBUTESENTRY._serialized_end = 1953 - _NAMESPACESPEC_SEARCHATTRIBUTETYPE._serialized_start = 1956 - _NAMESPACESPEC_SEARCHATTRIBUTETYPE._serialized_end = 2256 - _ENDPOINTS._serialized_start = 2258 - _ENDPOINTS._serialized_end = 2339 - _LIMITS._serialized_start = 2341 - _LIMITS._serialized_end = 2383 - _AWSPRIVATELINKINFO._serialized_start = 2385 - _AWSPRIVATELINKINFO._serialized_end = 2473 - _PRIVATECONNECTIVITY._serialized_start = 2475 - _PRIVATECONNECTIVITY._serialized_end = 2591 - _NAMESPACE._serialized_start = 2594 - _NAMESPACE._serialized_end = 3560 - _NAMESPACE_REGIONSTATUSENTRY._serialized_start = 3408 - _NAMESPACE_REGIONSTATUSENTRY._serialized_end = 3515 - _NAMESPACE_TAGSENTRY._serialized_start = 3517 - _NAMESPACE_TAGSENTRY._serialized_end = 3560 - _NAMESPACEREGIONSTATUS._serialized_start = 3563 - _NAMESPACEREGIONSTATUS._serialized_end = 3846 - _NAMESPACEREGIONSTATUS_STATE._serialized_start = 3723 - _NAMESPACEREGIONSTATUS_STATE._serialized_end = 3846 - _EXPORTSINKSPEC._serialized_start = 3849 - _EXPORTSINKSPEC._serialized_end = 3994 - _EXPORTSINK._serialized_start = 3997 - _EXPORTSINK._serialized_end = 4499 - _EXPORTSINK_HEALTH._serialized_start = 4388 - _EXPORTSINK_HEALTH._serialized_end = 4499 + _MTLSAUTHSPEC._serialized_end = 577 + _APIKEYAUTHSPEC._serialized_start = 579 + _APIKEYAUTHSPEC._serialized_end = 612 + _LIFECYCLESPEC._serialized_start = 614 + _LIFECYCLESPEC._serialized_end = 663 + _CODECSERVERSPEC._serialized_start = 666 + _CODECSERVERSPEC._serialized_end = 1038 + _CODECSERVERSPEC_CUSTOMERRORMESSAGE._serialized_start = 872 + _CODECSERVERSPEC_CUSTOMERRORMESSAGE._serialized_end = 1038 + _CODECSERVERSPEC_CUSTOMERRORMESSAGE_ERRORMESSAGE._serialized_start = 993 + _CODECSERVERSPEC_CUSTOMERRORMESSAGE_ERRORMESSAGE._serialized_end = 1038 + _HIGHAVAILABILITYSPEC._serialized_start = 1040 + _HIGHAVAILABILITYSPEC._serialized_end = 1139 + _REPLICASPEC._serialized_start = 1141 + _REPLICASPEC._serialized_end = 1170 + _REPLICA._serialized_start = 1173 + _REPLICA._serialized_end = 1454 + _REPLICA_REPLICASTATE._serialized_start = 1303 + _REPLICA_REPLICASTATE._serialized_end = 1454 + _CAPACITYSPEC._serialized_start = 1457 + _CAPACITYSPEC._serialized_end = 1680 + _CAPACITYSPEC_ONDEMAND._serialized_start = 1632 + _CAPACITYSPEC_ONDEMAND._serialized_end = 1642 + _CAPACITYSPEC_PROVISIONED._serialized_start = 1644 + _CAPACITYSPEC_PROVISIONED._serialized_end = 1672 + _CAPACITY._serialized_start = 1683 + _CAPACITY._serialized_end = 2415 + _CAPACITY_ONDEMAND._serialized_start = 1632 + _CAPACITY_ONDEMAND._serialized_end = 1642 + _CAPACITY_PROVISIONED._serialized_start = 1933 + _CAPACITY_PROVISIONED._serialized_end = 1969 + _CAPACITY_REQUEST._serialized_start = 1972 + _CAPACITY_REQUEST._serialized_end = 2399 + _CAPACITY_REQUEST_STATE._serialized_start = 2239 + _CAPACITY_REQUEST_STATE._serialized_end = 2399 + _FAIRNESSSPEC._serialized_start = 2417 + _FAIRNESSSPEC._serialized_end = 2468 + _NAMESPACESPEC._serialized_start = 2471 + _NAMESPACESPEC._serialized_end = 3835 + _NAMESPACESPEC_CUSTOMSEARCHATTRIBUTESENTRY._serialized_start = 3346 + _NAMESPACESPEC_CUSTOMSEARCHATTRIBUTESENTRY._serialized_end = 3407 + _NAMESPACESPEC_SEARCHATTRIBUTESENTRY._serialized_start = 3409 + _NAMESPACESPEC_SEARCHATTRIBUTESENTRY._serialized_end = 3532 + _NAMESPACESPEC_SEARCHATTRIBUTETYPE._serialized_start = 3535 + _NAMESPACESPEC_SEARCHATTRIBUTETYPE._serialized_end = 3835 + _ENDPOINTS._serialized_start = 3837 + _ENDPOINTS._serialized_end = 3918 + _LIMITS._serialized_start = 3920 + _LIMITS._serialized_end = 3962 + _AWSPRIVATELINKINFO._serialized_start = 3964 + _AWSPRIVATELINKINFO._serialized_end = 4052 + _PRIVATECONNECTIVITY._serialized_start = 4054 + _PRIVATECONNECTIVITY._serialized_end = 4170 + _NAMESPACE._serialized_start = 4173 + _NAMESPACE._serialized_end = 5264 + _NAMESPACE_REGIONSTATUSENTRY._serialized_start = 5112 + _NAMESPACE_REGIONSTATUSENTRY._serialized_end = 5219 + _NAMESPACE_TAGSENTRY._serialized_start = 5221 + _NAMESPACE_TAGSENTRY._serialized_end = 5264 + _NAMESPACEREGIONSTATUS._serialized_start = 5267 + _NAMESPACEREGIONSTATUS._serialized_end = 5550 + _NAMESPACEREGIONSTATUS_STATE._serialized_start = 5427 + _NAMESPACEREGIONSTATUS_STATE._serialized_end = 5550 + _EXPORTSINKSPEC._serialized_start = 5553 + _EXPORTSINKSPEC._serialized_end = 5698 + _EXPORTSINK._serialized_start = 5701 + _EXPORTSINK._serialized_end = 6203 + _EXPORTSINK_HEALTH._serialized_start = 6092 + _EXPORTSINK_HEALTH._serialized_end = 6203 + _NAMESPACECAPACITYINFO._serialized_start = 6206 + _NAMESPACECAPACITYINFO._serialized_end = 7005 + _NAMESPACECAPACITYINFO_CAPACITYMODEOPTIONS._serialized_start = 6522 + _NAMESPACECAPACITYINFO_CAPACITYMODEOPTIONS._serialized_end = 6861 + _NAMESPACECAPACITYINFO_CAPACITYMODEOPTIONS_PROVISIONED._serialized_start = 6758 + _NAMESPACECAPACITYINFO_CAPACITYMODEOPTIONS_PROVISIONED._serialized_end = 6830 + _NAMESPACECAPACITYINFO_CAPACITYMODEOPTIONS_ONDEMAND._serialized_start = 6832 + _NAMESPACECAPACITYINFO_CAPACITYMODEOPTIONS_ONDEMAND._serialized_end = 6861 + _NAMESPACECAPACITYINFO_STATS._serialized_start = 6864 + _NAMESPACECAPACITYINFO_STATS._serialized_end = 7005 + _NAMESPACECAPACITYINFO_STATS_SUMMARY._serialized_start = 6956 + _NAMESPACECAPACITYINFO_STATS_SUMMARY._serialized_end = 7005 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/cloud/namespace/v1/message_pb2.pyi b/temporalio/api/cloud/namespace/v1/message_pb2.pyi index ef666d6e5..b2cbe49d8 100644 --- a/temporalio/api/cloud/namespace/v1/message_pb2.pyi +++ b/temporalio/api/cloud/namespace/v1/message_pb2.pyi @@ -151,6 +151,26 @@ class ApiKeyAuthSpec(google.protobuf.message.Message): global___ApiKeyAuthSpec = ApiKeyAuthSpec +class LifecycleSpec(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ENABLE_DELETE_PROTECTION_FIELD_NUMBER: builtins.int + enable_delete_protection: builtins.bool + """Flag to enable delete protection for the namespace.""" + def __init__( + self, + *, + enable_delete_protection: builtins.bool = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "enable_delete_protection", b"enable_delete_protection" + ], + ) -> None: ... + +global___LifecycleSpec = LifecycleSpec + class CodecServerSpec(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -241,45 +261,365 @@ class CodecServerSpec(google.protobuf.message.Message): global___CodecServerSpec = CodecServerSpec -class LifecycleSpec(google.protobuf.message.Message): +class HighAvailabilitySpec(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor - ENABLE_DELETE_PROTECTION_FIELD_NUMBER: builtins.int - enable_delete_protection: builtins.bool - """Flag to enable delete protection for the namespace.""" + DISABLE_MANAGED_FAILOVER_FIELD_NUMBER: builtins.int + DISABLE_PASSIVE_POLLER_FORWARDING_FIELD_NUMBER: builtins.int + disable_managed_failover: builtins.bool + """Flag to disable managed failover for the namespace.""" + disable_passive_poller_forwarding: builtins.bool + """Flag to disable passive poller forwarding for this namespace. + temporal:versioning:min_version=v0.13.0 + """ def __init__( self, *, - enable_delete_protection: builtins.bool = ..., + disable_managed_failover: builtins.bool = ..., + disable_passive_poller_forwarding: builtins.bool = ..., ) -> None: ... def ClearField( self, field_name: typing_extensions.Literal[ - "enable_delete_protection", b"enable_delete_protection" + "disable_managed_failover", + b"disable_managed_failover", + "disable_passive_poller_forwarding", + b"disable_passive_poller_forwarding", ], ) -> None: ... -global___LifecycleSpec = LifecycleSpec +global___HighAvailabilitySpec = HighAvailabilitySpec + +class ReplicaSpec(google.protobuf.message.Message): + """temporal:versioning:min_version=v0.13.0""" -class HighAvailabilitySpec(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor - DISABLE_MANAGED_FAILOVER_FIELD_NUMBER: builtins.int - disable_managed_failover: builtins.bool - """Flag to disable managed failover for the namespace.""" + REGION_FIELD_NUMBER: builtins.int + region: builtins.str + """The id of the region where the replica should be placed. + The GetRegions API can be used to get the list of valid region ids. + All the replicas must adhere to the region's max_in_region_replicas limit and connectable_region_ids. + Required. Immutable. + Example: "aws-us-west-2". + """ def __init__( self, *, - disable_managed_failover: builtins.bool = ..., + region: builtins.str = ..., + ) -> None: ... + def ClearField( + self, field_name: typing_extensions.Literal["region", b"region"] + ) -> None: ... + +global___ReplicaSpec = ReplicaSpec + +class Replica(google.protobuf.message.Message): + """temporal:versioning:min_version=v0.13.0""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class _ReplicaState: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _ReplicaStateEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ + Replica._ReplicaState.ValueType + ], + builtins.type, + ): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + REPLICA_STATE_UNSPECIFIED: Replica._ReplicaState.ValueType # 0 + REPLICA_STATE_ADDING: Replica._ReplicaState.ValueType # 1 + """This replica is currently being added to the namespace.""" + REPLICA_STATE_ACTIVE: Replica._ReplicaState.ValueType # 2 + """This replica is healthy and active.""" + REPLICA_STATE_REMOVING: Replica._ReplicaState.ValueType # 3 + """This replica is currently being removed from the namespace.""" + REPLICA_STATE_FAILED: Replica._ReplicaState.ValueType # 5 + """This replica is in a failed state, reach out to Temporal Cloud support for remediation.""" + + class ReplicaState(_ReplicaState, metaclass=_ReplicaStateEnumTypeWrapper): ... + REPLICA_STATE_UNSPECIFIED: Replica.ReplicaState.ValueType # 0 + REPLICA_STATE_ADDING: Replica.ReplicaState.ValueType # 1 + """This replica is currently being added to the namespace.""" + REPLICA_STATE_ACTIVE: Replica.ReplicaState.ValueType # 2 + """This replica is healthy and active.""" + REPLICA_STATE_REMOVING: Replica.ReplicaState.ValueType # 3 + """This replica is currently being removed from the namespace.""" + REPLICA_STATE_FAILED: Replica.ReplicaState.ValueType # 5 + """This replica is in a failed state, reach out to Temporal Cloud support for remediation.""" + + ID_FIELD_NUMBER: builtins.int + IS_PRIMARY_FIELD_NUMBER: builtins.int + STATE_FIELD_NUMBER: builtins.int + REGION_FIELD_NUMBER: builtins.int + id: builtins.str + """The id of the replica. This is generated by Temporal after a replica is created.""" + is_primary: builtins.bool + """Whether this replica is currently the primary one.""" + state: global___Replica.ReplicaState.ValueType + """The current state of this replica.""" + region: builtins.str + """The cloud provider and region of this replica.""" + def __init__( + self, + *, + id: builtins.str = ..., + is_primary: builtins.bool = ..., + state: global___Replica.ReplicaState.ValueType = ..., + region: builtins.str = ..., ) -> None: ... def ClearField( self, field_name: typing_extensions.Literal[ - "disable_managed_failover", b"disable_managed_failover" + "id", + b"id", + "is_primary", + b"is_primary", + "region", + b"region", + "state", + b"state", ], ) -> None: ... -global___HighAvailabilitySpec = HighAvailabilitySpec +global___Replica = Replica + +class CapacitySpec(google.protobuf.message.Message): + """temporal:versioning:min_version=v0.10.0""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class OnDemand(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + + class Provisioned(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VALUE_FIELD_NUMBER: builtins.int + value: builtins.float + """The units of provisioned capacity in TRU (Temporal Resource Units). + Each TRU unit assigned to the namespace will entitle it with additional APS limits as specified in the documentation. + """ + def __init__( + self, + *, + value: builtins.float = ..., + ) -> None: ... + def ClearField( + self, field_name: typing_extensions.Literal["value", b"value"] + ) -> None: ... + + ON_DEMAND_FIELD_NUMBER: builtins.int + PROVISIONED_FIELD_NUMBER: builtins.int + @property + def on_demand(self) -> global___CapacitySpec.OnDemand: + """The on-demand capacity mode configuration.""" + @property + def provisioned(self) -> global___CapacitySpec.Provisioned: + """The provisioned capacity mode configuration.""" + def __init__( + self, + *, + on_demand: global___CapacitySpec.OnDemand | None = ..., + provisioned: global___CapacitySpec.Provisioned | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "on_demand", b"on_demand", "provisioned", b"provisioned", "spec", b"spec" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "on_demand", b"on_demand", "provisioned", b"provisioned", "spec", b"spec" + ], + ) -> None: ... + def WhichOneof( + self, oneof_group: typing_extensions.Literal["spec", b"spec"] + ) -> typing_extensions.Literal["on_demand", "provisioned"] | None: ... + +global___CapacitySpec = CapacitySpec + +class Capacity(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class OnDemand(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + + class Provisioned(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CURRENT_VALUE_FIELD_NUMBER: builtins.int + current_value: builtins.float + """The current provisioned capacity for the namespace in Temporal Resource Units. + Can be different from the requested capacity in latest_request if the request is still in progress. + """ + def __init__( + self, + *, + current_value: builtins.float = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal["current_value", b"current_value"], + ) -> None: ... + + class Request(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class _State: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _StateEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ + Capacity.Request._State.ValueType + ], + builtins.type, + ): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + STATE_CAPACITY_REQUEST_UNSPECIFIED: Capacity.Request._State.ValueType # 0 + STATE_CAPACITY_REQUEST_COMPLETED: Capacity.Request._State.ValueType # 1 + STATE_CAPACITY_REQUEST_IN_PROGRESS: Capacity.Request._State.ValueType # 2 + STATE_CAPACITY_REQUEST_FAILED: Capacity.Request._State.ValueType # 3 + + class State(_State, metaclass=_StateEnumTypeWrapper): ... + STATE_CAPACITY_REQUEST_UNSPECIFIED: Capacity.Request.State.ValueType # 0 + STATE_CAPACITY_REQUEST_COMPLETED: Capacity.Request.State.ValueType # 1 + STATE_CAPACITY_REQUEST_IN_PROGRESS: Capacity.Request.State.ValueType # 2 + STATE_CAPACITY_REQUEST_FAILED: Capacity.Request.State.ValueType # 3 + + STATE_FIELD_NUMBER: builtins.int + START_TIME_FIELD_NUMBER: builtins.int + END_TIME_FIELD_NUMBER: builtins.int + ASYNC_OPERATION_ID_FIELD_NUMBER: builtins.int + SPEC_FIELD_NUMBER: builtins.int + state: global___Capacity.Request.State.ValueType + """The current state of the capacity request (e.g. in-progress, completed, failed).""" + @property + def start_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """The date and time when the capacity request was created.""" + @property + def end_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """The date and time when the capacity request was completed or failed.""" + async_operation_id: builtins.str + """The id of the async operation that is creating/updating/deleting the capacity, if any.""" + @property + def spec(self) -> global___CapacitySpec: + """The requested capacity specification.""" + def __init__( + self, + *, + state: global___Capacity.Request.State.ValueType = ..., + start_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + end_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + async_operation_id: builtins.str = ..., + spec: global___CapacitySpec | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "end_time", b"end_time", "spec", b"spec", "start_time", b"start_time" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "async_operation_id", + b"async_operation_id", + "end_time", + b"end_time", + "spec", + b"spec", + "start_time", + b"start_time", + "state", + b"state", + ], + ) -> None: ... + + ON_DEMAND_FIELD_NUMBER: builtins.int + PROVISIONED_FIELD_NUMBER: builtins.int + LATEST_REQUEST_FIELD_NUMBER: builtins.int + @property + def on_demand(self) -> global___Capacity.OnDemand: + """The status of on-demand capacity mode.""" + @property + def provisioned(self) -> global___Capacity.Provisioned: + """The status of provisioned capacity mode.""" + @property + def latest_request(self) -> global___Capacity.Request: + """The latest requested capacity for the namespace, if any.""" + def __init__( + self, + *, + on_demand: global___Capacity.OnDemand | None = ..., + provisioned: global___Capacity.Provisioned | None = ..., + latest_request: global___Capacity.Request | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "current_mode", + b"current_mode", + "latest_request", + b"latest_request", + "on_demand", + b"on_demand", + "provisioned", + b"provisioned", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "current_mode", + b"current_mode", + "latest_request", + b"latest_request", + "on_demand", + b"on_demand", + "provisioned", + b"provisioned", + ], + ) -> None: ... + def WhichOneof( + self, oneof_group: typing_extensions.Literal["current_mode", b"current_mode"] + ) -> typing_extensions.Literal["on_demand", "provisioned"] | None: ... + +global___Capacity = Capacity + +class FairnessSpec(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TASK_QUEUE_FAIRNESS_ENABLED_FIELD_NUMBER: builtins.int + task_queue_fairness_enabled: builtins.bool + """Flag to enable task queue fairness for the namespace (default: disabled).""" + def __init__( + self, + *, + task_queue_fairness_enabled: builtins.bool = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "task_queue_fairness_enabled", b"task_queue_fairness_enabled" + ], + ) -> None: ... + +global___FairnessSpec = FairnessSpec class NamespaceSpec(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -369,6 +709,9 @@ class NamespaceSpec(google.protobuf.message.Message): LIFECYCLE_FIELD_NUMBER: builtins.int HIGH_AVAILABILITY_FIELD_NUMBER: builtins.int CONNECTIVITY_RULE_IDS_FIELD_NUMBER: builtins.int + CAPACITY_SPEC_FIELD_NUMBER: builtins.int + REPLICAS_FIELD_NUMBER: builtins.int + FAIRNESS_FIELD_NUMBER: builtins.int name: builtins.str """The name to use for the namespace. This will create a namespace that's available at '..tmprl.cloud:7233'. @@ -386,6 +729,8 @@ class NamespaceSpec(google.protobuf.message.Message): Number of supported regions is 2. The regions is immutable. Once set, it cannot be changed. Example: ["aws-us-west-2"]. + Deprecated: Use replicas field instead. + temporal:versioning:max_version=v0.15.0 """ retention_days: builtins.int """The number of days the workflows data will be retained for. @@ -451,6 +796,35 @@ class NamespaceSpec(google.protobuf.message.Message): This will apply the connectivity rules specified to the namespace. temporal:versioning:min_version=v0.6.0 """ + @property + def capacity_spec(self) -> global___CapacitySpec: + """The capacity configuration for the namespace. + There are two capacity modes: on-demand and provisioned. + On-demand capacity mode allows the namespace to scale automatically based on usage. + Provisioned capacity mode allows the user to specify a fixed amount of capacity (in TRUs) for the namespace. + Can be changed only when the last capacity request is not in progress. + temporal:versioning:min_version=v0.10.0 + """ + @property + def replicas( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + global___ReplicaSpec + ]: + """The replication configuration for the namespace. + At least one replica must be specified. + Only one replica can be marked to be the desired active one. + The status of each replica is available in the Namespace.replicas field. + Use HighAvailabilitySpec to set the preferred primary replica ID. + If the preferred primary replica ID is not set, the first replica in this replicas list will be the preferred primary. + temporal:versioning:min_version=v0.13.0 + """ + @property + def fairness(self) -> global___FairnessSpec: + """The fairness configuration for the namespace. + If unspecified, fairness features will be disabled. + temporal:versioning:min_version=v0.14.0 + """ def __init__( self, *, @@ -469,14 +843,21 @@ class NamespaceSpec(google.protobuf.message.Message): lifecycle: global___LifecycleSpec | None = ..., high_availability: global___HighAvailabilitySpec | None = ..., connectivity_rule_ids: collections.abc.Iterable[builtins.str] | None = ..., + capacity_spec: global___CapacitySpec | None = ..., + replicas: collections.abc.Iterable[global___ReplicaSpec] | None = ..., + fairness: global___FairnessSpec | None = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal[ "api_key_auth", b"api_key_auth", + "capacity_spec", + b"capacity_spec", "codec_server", b"codec_server", + "fairness", + b"fairness", "high_availability", b"high_availability", "lifecycle", @@ -490,12 +871,16 @@ class NamespaceSpec(google.protobuf.message.Message): field_name: typing_extensions.Literal[ "api_key_auth", b"api_key_auth", + "capacity_spec", + b"capacity_spec", "codec_server", b"codec_server", "connectivity_rule_ids", b"connectivity_rule_ids", "custom_search_attributes", b"custom_search_attributes", + "fairness", + b"fairness", "high_availability", b"high_availability", "lifecycle", @@ -506,6 +891,8 @@ class NamespaceSpec(google.protobuf.message.Message): b"name", "regions", b"regions", + "replicas", + b"replicas", "retention_days", b"retention_days", "search_attributes", @@ -692,6 +1079,8 @@ class Namespace(google.protobuf.message.Message): REGION_STATUS_FIELD_NUMBER: builtins.int CONNECTIVITY_RULES_FIELD_NUMBER: builtins.int TAGS_FIELD_NUMBER: builtins.int + CAPACITY_FIELD_NUMBER: builtins.int + REPLICAS_FIELD_NUMBER: builtins.int namespace: builtins.str """The namespace identifier.""" resource_version: builtins.str @@ -745,6 +1134,8 @@ class Namespace(google.protobuf.message.Message): ]: """The status of each region where the namespace is available. The id of the region is the key and the status is the value of the map. + deprecated: Use replicas field instead. + temporal:versioning:max_version=v0.15.0 """ @property def connectivity_rules( @@ -758,6 +1149,18 @@ class Namespace(google.protobuf.message.Message): self, ) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: """The tags for the namespace.""" + @property + def capacity(self) -> global___Capacity: + """The status of namespace's capacity, if any.""" + @property + def replicas( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + global___Replica + ]: + """The status of each replica where the namespace is available. + temporal:versioning:min_version=v0.13.0 + """ def __init__( self, *, @@ -783,10 +1186,14 @@ class Namespace(google.protobuf.message.Message): ] | None = ..., tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + capacity: global___Capacity | None = ..., + replicas: collections.abc.Iterable[global___Replica] | None = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal[ + "capacity", + b"capacity", "created_time", b"created_time", "endpoints", @@ -806,6 +1213,8 @@ class Namespace(google.protobuf.message.Message): b"active_region", "async_operation_id", b"async_operation_id", + "capacity", + b"capacity", "connectivity_rules", b"connectivity_rules", "created_time", @@ -822,6 +1231,8 @@ class Namespace(google.protobuf.message.Message): b"private_connectivities", "region_status", b"region_status", + "replicas", + b"replicas", "resource_version", b"resource_version", "spec", @@ -930,7 +1341,9 @@ class ExportSinkSpec(google.protobuf.message.Message): """The S3 configuration details when destination_type is S3.""" @property def gcs(self) -> temporalio.api.cloud.sink.v1.message_pb2.GCSSpec: - """The GCS configuration details when destination_type is GCS.""" + """This is a feature under development. We will allow GCS sink support for GCP Namespaces. + The GCS configuration details when destination_type is GCS. + """ def __init__( self, *, @@ -985,7 +1398,7 @@ class ExportSink(google.protobuf.message.Message): LATEST_DATA_EXPORT_TIME_FIELD_NUMBER: builtins.int LAST_HEALTH_CHECK_TIME_FIELD_NUMBER: builtins.int name: builtins.str - """The unique name of the export sink.""" + """The unique name of the export sink, once set it can't be changed""" resource_version: builtins.str """The version of the export sink resource.""" state: temporalio.api.cloud.resource.v1.message_pb2.ResourceState.ValueType @@ -1049,3 +1462,202 @@ class ExportSink(google.protobuf.message.Message): ) -> None: ... global___ExportSink = ExportSink + +class NamespaceCapacityInfo(google.protobuf.message.Message): + """NamespaceCapacityInfo contains detailed capacity information for a namespace.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class CapacityModeOptions(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class Provisioned(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VALID_TRU_VALUES_FIELD_NUMBER: builtins.int + MAX_AVAILABLE_TRU_VALUE_FIELD_NUMBER: builtins.int + @property + def valid_tru_values( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[ + builtins.float + ]: + """The valid TRU (Temporal Resource Unit) values that can be set. + These are the discrete capacity tiers available for selection. + """ + max_available_tru_value: builtins.float + """The maximum TRU value that can currently be set for this namespace. + This may be lower than the highest value in valid_tru_values due to + inventory constraints. + """ + def __init__( + self, + *, + valid_tru_values: collections.abc.Iterable[builtins.float] | None = ..., + max_available_tru_value: builtins.float = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "max_available_tru_value", + b"max_available_tru_value", + "valid_tru_values", + b"valid_tru_values", + ], + ) -> None: ... + + class OnDemand(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + APS_LIMIT_FIELD_NUMBER: builtins.int + aps_limit: builtins.float + """The APS limit that would apply to this namespace in on-demand mode. + See: https://docs.temporal.io/cloud/limits#actions-per-second + """ + def __init__( + self, + *, + aps_limit: builtins.float = ..., + ) -> None: ... + def ClearField( + self, field_name: typing_extensions.Literal["aps_limit", b"aps_limit"] + ) -> None: ... + + PROVISIONED_FIELD_NUMBER: builtins.int + ON_DEMAND_FIELD_NUMBER: builtins.int + @property + def provisioned( + self, + ) -> global___NamespaceCapacityInfo.CapacityModeOptions.Provisioned: + """Provisioned capacity options and entitlements.""" + @property + def on_demand( + self, + ) -> global___NamespaceCapacityInfo.CapacityModeOptions.OnDemand: + """On-Demand capacity information.""" + def __init__( + self, + *, + provisioned: global___NamespaceCapacityInfo.CapacityModeOptions.Provisioned + | None = ..., + on_demand: global___NamespaceCapacityInfo.CapacityModeOptions.OnDemand + | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "on_demand", b"on_demand", "provisioned", b"provisioned" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "on_demand", b"on_demand", "provisioned", b"provisioned" + ], + ) -> None: ... + + class Stats(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class Summary(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + MEAN_FIELD_NUMBER: builtins.int + P90_FIELD_NUMBER: builtins.int + P99_FIELD_NUMBER: builtins.int + mean: builtins.float + p90: builtins.float + p99: builtins.float + def __init__( + self, + *, + mean: builtins.float = ..., + p90: builtins.float = ..., + p99: builtins.float = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "mean", b"mean", "p90", b"p90", "p99", b"p99" + ], + ) -> None: ... + + APS_FIELD_NUMBER: builtins.int + @property + def aps(self) -> global___NamespaceCapacityInfo.Stats.Summary: + """Actions-per-second measurements summarized over the last 7 days.""" + def __init__( + self, + *, + aps: global___NamespaceCapacityInfo.Stats.Summary | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["aps", b"aps"] + ) -> builtins.bool: ... + def ClearField( + self, field_name: typing_extensions.Literal["aps", b"aps"] + ) -> None: ... + + NAMESPACE_FIELD_NUMBER: builtins.int + HAS_LEGACY_LIMITS_FIELD_NUMBER: builtins.int + CURRENT_CAPACITY_FIELD_NUMBER: builtins.int + MODE_OPTIONS_FIELD_NUMBER: builtins.int + STATS_FIELD_NUMBER: builtins.int + namespace: builtins.str + """The namespace identifier.""" + has_legacy_limits: builtins.bool + """Whether the namespace's APS limit was set by Temporal Support. + When true, adjusting the namespace's capacity will reset this limit. + """ + @property + def current_capacity(self) -> global___Capacity: + """The current capacity of the namespace. + Includes the current mode (on-demand or provisioned) and latest request status. + """ + @property + def mode_options(self) -> global___NamespaceCapacityInfo.CapacityModeOptions: + """Available capacity mode options for this namespace. + Contains configuration limits for both provisioned and on-demand modes. + """ + @property + def stats(self) -> global___NamespaceCapacityInfo.Stats: + """Usage statistics for the namespace over the last 7 days. + Used to calculate On-Demand capacity limits, also useful for capacity planning. + """ + def __init__( + self, + *, + namespace: builtins.str = ..., + has_legacy_limits: builtins.bool = ..., + current_capacity: global___Capacity | None = ..., + mode_options: global___NamespaceCapacityInfo.CapacityModeOptions | None = ..., + stats: global___NamespaceCapacityInfo.Stats | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "current_capacity", + b"current_capacity", + "mode_options", + b"mode_options", + "stats", + b"stats", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "current_capacity", + b"current_capacity", + "has_legacy_limits", + b"has_legacy_limits", + "mode_options", + b"mode_options", + "namespace", + b"namespace", + "stats", + b"stats", + ], + ) -> None: ... + +global___NamespaceCapacityInfo = NamespaceCapacityInfo diff --git a/temporalio/api/cloud/nexus/v1/message_pb2.py b/temporalio/api/cloud/nexus/v1/message_pb2.py index e7c3f6ea5..f34cac2cc 100644 --- a/temporalio/api/cloud/nexus/v1/message_pb2.py +++ b/temporalio/api/cloud/nexus/v1/message_pb2.py @@ -41,7 +41,7 @@ (_message.Message,), { "DESCRIPTOR": _ENDPOINTSPEC, - "__module__": "temporal.api.cloud.nexus.v1.message_pb2", + "__module__": "temporalio.api.cloud.nexus.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.nexus.v1.EndpointSpec) }, ) @@ -52,7 +52,7 @@ (_message.Message,), { "DESCRIPTOR": _ENDPOINTTARGETSPEC, - "__module__": "temporal.api.cloud.nexus.v1.message_pb2", + "__module__": "temporalio.api.cloud.nexus.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.nexus.v1.EndpointTargetSpec) }, ) @@ -63,7 +63,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKERTARGETSPEC, - "__module__": "temporal.api.cloud.nexus.v1.message_pb2", + "__module__": "temporalio.api.cloud.nexus.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.nexus.v1.WorkerTargetSpec) }, ) @@ -74,7 +74,7 @@ (_message.Message,), { "DESCRIPTOR": _ENDPOINTPOLICYSPEC, - "__module__": "temporal.api.cloud.nexus.v1.message_pb2", + "__module__": "temporalio.api.cloud.nexus.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.nexus.v1.EndpointPolicySpec) }, ) @@ -85,7 +85,7 @@ (_message.Message,), { "DESCRIPTOR": _ALLOWEDCLOUDNAMESPACEPOLICYSPEC, - "__module__": "temporal.api.cloud.nexus.v1.message_pb2", + "__module__": "temporalio.api.cloud.nexus.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.nexus.v1.AllowedCloudNamespacePolicySpec) }, ) @@ -96,7 +96,7 @@ (_message.Message,), { "DESCRIPTOR": _ENDPOINT, - "__module__": "temporal.api.cloud.nexus.v1.message_pb2", + "__module__": "temporalio.api.cloud.nexus.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.nexus.v1.Endpoint) }, ) diff --git a/temporalio/api/cloud/operation/v1/message_pb2.py b/temporalio/api/cloud/operation/v1/message_pb2.py index 230e00583..8d4664ed5 100644 --- a/temporalio/api/cloud/operation/v1/message_pb2.py +++ b/temporalio/api/cloud/operation/v1/message_pb2.py @@ -30,7 +30,7 @@ (_message.Message,), { "DESCRIPTOR": _ASYNCOPERATION, - "__module__": "temporal.api.cloud.operation.v1.message_pb2", + "__module__": "temporalio.api.cloud.operation.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.operation.v1.AsyncOperation) }, ) diff --git a/temporalio/api/cloud/region/v1/message_pb2.py b/temporalio/api/cloud/region/v1/message_pb2.py index edcb844b9..17a5461a8 100644 --- a/temporalio/api/cloud/region/v1/message_pb2.py +++ b/temporalio/api/cloud/region/v1/message_pb2.py @@ -26,7 +26,7 @@ (_message.Message,), { "DESCRIPTOR": _REGION, - "__module__": "temporal.api.cloud.region.v1.message_pb2", + "__module__": "temporalio.api.cloud.region.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.region.v1.Region) }, ) diff --git a/temporalio/api/cloud/sink/v1/__init__.py b/temporalio/api/cloud/sink/v1/__init__.py index 659be6d28..9bf62a798 100644 --- a/temporalio/api/cloud/sink/v1/__init__.py +++ b/temporalio/api/cloud/sink/v1/__init__.py @@ -1,6 +1,8 @@ -from .message_pb2 import GCSSpec, S3Spec +from .message_pb2 import GCSSpec, KinesisSpec, PubSubSpec, S3Spec __all__ = [ "GCSSpec", + "KinesisSpec", + "PubSubSpec", "S3Spec", ] diff --git a/temporalio/api/cloud/sink/v1/message_pb2.py b/temporalio/api/cloud/sink/v1/message_pb2.py index d50bf6bee..ef22569f1 100644 --- a/temporalio/api/cloud/sink/v1/message_pb2.py +++ b/temporalio/api/cloud/sink/v1/message_pb2.py @@ -15,18 +15,20 @@ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n(temporal/api/cloud/sink/v1/message.proto\x12\x1atemporal.api.cloud.sink.v1"i\n\x06S3Spec\x12\x11\n\trole_name\x18\x01 \x01(\t\x12\x13\n\x0b\x62ucket_name\x18\x02 \x01(\t\x12\x0e\n\x06region\x18\x03 \x01(\t\x12\x0f\n\x07kms_arn\x18\x04 \x01(\t\x12\x16\n\x0e\x61ws_account_id\x18\x05 \x01(\t"U\n\x07GCSSpec\x12\r\n\x05sa_id\x18\x01 \x01(\t\x12\x13\n\x0b\x62ucket_name\x18\x02 \x01(\t\x12\x16\n\x0egcp_project_id\x18\x03 \x01(\t\x12\x0e\n\x06region\x18\x04 \x01(\tB\x98\x01\n\x1dio.temporal.api.cloud.sink.v1B\x0cMessageProtoP\x01Z%go.temporal.io/api/cloud/sink/v1;sink\xaa\x02\x1cTemporalio.Api.Cloud.Sink.V1\xea\x02 Temporalio::Api::Cloud::Sink::V1b\x06proto3' + b'\n(temporal/api/cloud/sink/v1/message.proto\x12\x1atemporal.api.cloud.sink.v1"i\n\x06S3Spec\x12\x11\n\trole_name\x18\x01 \x01(\t\x12\x13\n\x0b\x62ucket_name\x18\x02 \x01(\t\x12\x0e\n\x06region\x18\x03 \x01(\t\x12\x0f\n\x07kms_arn\x18\x04 \x01(\t\x12\x16\n\x0e\x61ws_account_id\x18\x05 \x01(\t"U\n\x07GCSSpec\x12\r\n\x05sa_id\x18\x01 \x01(\t\x12\x13\n\x0b\x62ucket_name\x18\x02 \x01(\t\x12\x16\n\x0egcp_project_id\x18\x03 \x01(\t\x12\x0e\n\x06region\x18\x04 \x01(\t"I\n\x0bKinesisSpec\x12\x11\n\trole_name\x18\x01 \x01(\t\x12\x17\n\x0f\x64\x65stination_uri\x18\x02 \x01(\t\x12\x0e\n\x06region\x18\x03 \x01(\t"T\n\nPubSubSpec\x12\x1a\n\x12service_account_id\x18\x01 \x01(\t\x12\x12\n\ntopic_name\x18\x02 \x01(\t\x12\x16\n\x0egcp_project_id\x18\x03 \x01(\tB\x98\x01\n\x1dio.temporal.api.cloud.sink.v1B\x0cMessageProtoP\x01Z%go.temporal.io/api/cloud/sink/v1;sink\xaa\x02\x1cTemporalio.Api.Cloud.Sink.V1\xea\x02 Temporalio::Api::Cloud::Sink::V1b\x06proto3' ) _S3SPEC = DESCRIPTOR.message_types_by_name["S3Spec"] _GCSSPEC = DESCRIPTOR.message_types_by_name["GCSSpec"] +_KINESISSPEC = DESCRIPTOR.message_types_by_name["KinesisSpec"] +_PUBSUBSPEC = DESCRIPTOR.message_types_by_name["PubSubSpec"] S3Spec = _reflection.GeneratedProtocolMessageType( "S3Spec", (_message.Message,), { "DESCRIPTOR": _S3SPEC, - "__module__": "temporal.api.cloud.sink.v1.message_pb2", + "__module__": "temporalio.api.cloud.sink.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.sink.v1.S3Spec) }, ) @@ -37,12 +39,34 @@ (_message.Message,), { "DESCRIPTOR": _GCSSPEC, - "__module__": "temporal.api.cloud.sink.v1.message_pb2", + "__module__": "temporalio.api.cloud.sink.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.sink.v1.GCSSpec) }, ) _sym_db.RegisterMessage(GCSSpec) +KinesisSpec = _reflection.GeneratedProtocolMessageType( + "KinesisSpec", + (_message.Message,), + { + "DESCRIPTOR": _KINESISSPEC, + "__module__": "temporalio.api.cloud.sink.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.sink.v1.KinesisSpec) + }, +) +_sym_db.RegisterMessage(KinesisSpec) + +PubSubSpec = _reflection.GeneratedProtocolMessageType( + "PubSubSpec", + (_message.Message,), + { + "DESCRIPTOR": _PUBSUBSPEC, + "__module__": "temporalio.api.cloud.sink.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.cloud.sink.v1.PubSubSpec) + }, +) +_sym_db.RegisterMessage(PubSubSpec) + if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b"\n\035io.temporal.api.cloud.sink.v1B\014MessageProtoP\001Z%go.temporal.io/api/cloud/sink/v1;sink\252\002\034Temporalio.Api.Cloud.Sink.V1\352\002 Temporalio::Api::Cloud::Sink::V1" @@ -50,4 +74,8 @@ _S3SPEC._serialized_end = 177 _GCSSPEC._serialized_start = 179 _GCSSPEC._serialized_end = 264 + _KINESISSPEC._serialized_start = 266 + _KINESISSPEC._serialized_end = 339 + _PUBSUBSPEC._serialized_start = 341 + _PUBSUBSPEC._serialized_end = 425 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/cloud/sink/v1/message_pb2.pyi b/temporalio/api/cloud/sink/v1/message_pb2.pyi index d93987af1..30e6ff099 100644 --- a/temporalio/api/cloud/sink/v1/message_pb2.pyi +++ b/temporalio/api/cloud/sink/v1/message_pb2.pyi @@ -99,3 +99,69 @@ class GCSSpec(google.protobuf.message.Message): ) -> None: ... global___GCSSpec = GCSSpec + +class KinesisSpec(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ROLE_NAME_FIELD_NUMBER: builtins.int + DESTINATION_URI_FIELD_NUMBER: builtins.int + REGION_FIELD_NUMBER: builtins.int + role_name: builtins.str + """The role Temporal Cloud assumes when writing records to Kinesis""" + destination_uri: builtins.str + """Destination Kinesis endpoint arn for temporal to send data to.""" + region: builtins.str + """The sink's region.""" + def __init__( + self, + *, + role_name: builtins.str = ..., + destination_uri: builtins.str = ..., + region: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "destination_uri", + b"destination_uri", + "region", + b"region", + "role_name", + b"role_name", + ], + ) -> None: ... + +global___KinesisSpec = KinesisSpec + +class PubSubSpec(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SERVICE_ACCOUNT_ID_FIELD_NUMBER: builtins.int + TOPIC_NAME_FIELD_NUMBER: builtins.int + GCP_PROJECT_ID_FIELD_NUMBER: builtins.int + service_account_id: builtins.str + """The customer service account id that Temporal Cloud impersonates for writing records to customer's pubsub topic""" + topic_name: builtins.str + """Destination pubsub topic name for us""" + gcp_project_id: builtins.str + """The gcp project id of pubsub topic and service account""" + def __init__( + self, + *, + service_account_id: builtins.str = ..., + topic_name: builtins.str = ..., + gcp_project_id: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "gcp_project_id", + b"gcp_project_id", + "service_account_id", + b"service_account_id", + "topic_name", + b"topic_name", + ], + ) -> None: ... + +global___PubSubSpec = PubSubSpec diff --git a/temporalio/api/cloud/usage/v1/message_pb2.py b/temporalio/api/cloud/usage/v1/message_pb2.py index 2cb309c32..84b635bba 100644 --- a/temporalio/api/cloud/usage/v1/message_pb2.py +++ b/temporalio/api/cloud/usage/v1/message_pb2.py @@ -47,7 +47,7 @@ (_message.Message,), { "DESCRIPTOR": _SUMMARY, - "__module__": "temporal.api.cloud.usage.v1.message_pb2", + "__module__": "temporalio.api.cloud.usage.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.usage.v1.Summary) }, ) @@ -58,7 +58,7 @@ (_message.Message,), { "DESCRIPTOR": _RECORDGROUP, - "__module__": "temporal.api.cloud.usage.v1.message_pb2", + "__module__": "temporalio.api.cloud.usage.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.usage.v1.RecordGroup) }, ) @@ -69,7 +69,7 @@ (_message.Message,), { "DESCRIPTOR": _GROUPBY, - "__module__": "temporal.api.cloud.usage.v1.message_pb2", + "__module__": "temporalio.api.cloud.usage.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.usage.v1.GroupBy) }, ) @@ -80,7 +80,7 @@ (_message.Message,), { "DESCRIPTOR": _RECORD, - "__module__": "temporal.api.cloud.usage.v1.message_pb2", + "__module__": "temporalio.api.cloud.usage.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.cloud.usage.v1.Record) }, ) diff --git a/temporalio/api/command/v1/message_pb2.py b/temporalio/api/command/v1/message_pb2.py index a5435dd2e..ad004ae03 100644 --- a/temporalio/api/command/v1/message_pb2.py +++ b/temporalio/api/command/v1/message_pb2.py @@ -28,15 +28,21 @@ from temporalio.api.failure.v1 import ( message_pb2 as temporal_dot_api_dot_failure_dot_v1_dot_message__pb2, ) +from temporalio.api.sdk.v1 import ( + event_group_marker_pb2 as temporal_dot_api_dot_sdk_dot_v1_dot_event__group__marker__pb2, +) from temporalio.api.sdk.v1 import ( user_metadata_pb2 as temporal_dot_api_dot_sdk_dot_v1_dot_user__metadata__pb2, ) from temporalio.api.taskqueue.v1 import ( message_pb2 as temporal_dot_api_dot_taskqueue_dot_v1_dot_message__pb2, ) +from temporalio.api.workflow.v1 import ( + message_pb2 as temporal_dot_api_dot_workflow_dot_v1_dot_message__pb2, +) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n%temporal/api/command/v1/message.proto\x12\x17temporal.api.command.v1\x1a\x1egoogle/protobuf/duration.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a(temporal/api/enums/v1/command_type.proto\x1a$temporal/api/common/v1/message.proto\x1a%temporal/api/failure/v1/message.proto\x1a\'temporal/api/taskqueue/v1/message.proto\x1a\'temporal/api/sdk/v1/user_metadata.proto"\xb6\x05\n%ScheduleActivityTaskCommandAttributes\x12\x13\n\x0b\x61\x63tivity_id\x18\x01 \x01(\t\x12;\n\ractivity_type\x18\x02 \x01(\x0b\x32$.temporal.api.common.v1.ActivityType\x12\x38\n\ntask_queue\x18\x04 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12.\n\x06header\x18\x05 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12/\n\x05input\x18\x06 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12<\n\x19schedule_to_close_timeout\x18\x07 \x01(\x0b\x32\x19.google.protobuf.Duration\x12<\n\x19schedule_to_start_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x34\n\x11heartbeat_timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x0cretry_policy\x18\x0b \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x1f\n\x17request_eager_execution\x18\x0c \x01(\x08\x12\x1d\n\x15use_workflow_build_id\x18\r \x01(\x08\x12\x32\n\x08priority\x18\x0e \x01(\x0b\x32 .temporal.api.common.v1.PriorityJ\x04\x08\x03\x10\x04"H\n*RequestCancelActivityTaskCommandAttributes\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03"i\n\x1bStartTimerCommandAttributes\x12\x10\n\x08timer_id\x18\x01 \x01(\t\x12\x38\n\x15start_to_fire_timeout\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration"^\n*CompleteWorkflowExecutionCommandAttributes\x12\x30\n\x06result\x18\x01 \x01(\x0b\x32 .temporal.api.common.v1.Payloads"[\n&FailWorkflowExecutionCommandAttributes\x12\x31\n\x07\x66\x61ilure\x18\x01 \x01(\x0b\x32 .temporal.api.failure.v1.Failure"0\n\x1c\x43\x61ncelTimerCommandAttributes\x12\x10\n\x08timer_id\x18\x01 \x01(\t"]\n(CancelWorkflowExecutionCommandAttributes\x12\x31\n\x07\x64\x65tails\x18\x01 \x01(\x0b\x32 .temporal.api.common.v1.Payloads"\xb3\x01\n7RequestCancelExternalWorkflowExecutionCommandAttributes\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\t\x12\x13\n\x07\x63ontrol\x18\x04 \x01(\tB\x02\x18\x01\x12\x1b\n\x13\x63hild_workflow_only\x18\x05 \x01(\x08\x12\x0e\n\x06reason\x18\x06 \x01(\t"\xab\x02\n0SignalExternalWorkflowExecutionCommandAttributes\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12<\n\texecution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x13\n\x0bsignal_name\x18\x03 \x01(\t\x12/\n\x05input\x18\x04 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x13\n\x07\x63ontrol\x18\x05 \x01(\tB\x02\x18\x01\x12\x1b\n\x13\x63hild_workflow_only\x18\x06 \x01(\x08\x12.\n\x06header\x18\x07 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header"v\n/UpsertWorkflowSearchAttributesCommandAttributes\x12\x43\n\x11search_attributes\x18\x01 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes"`\n)ModifyWorkflowPropertiesCommandAttributes\x12\x33\n\rupserted_memo\x18\x01 \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo"\xbf\x02\n\x1dRecordMarkerCommandAttributes\x12\x13\n\x0bmarker_name\x18\x01 \x01(\t\x12T\n\x07\x64\x65tails\x18\x02 \x03(\x0b\x32\x43.temporal.api.command.v1.RecordMarkerCommandAttributes.DetailsEntry\x12.\n\x06header\x18\x03 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12\x31\n\x07\x66\x61ilure\x18\x04 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x1aP\n\x0c\x44\x65tailsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12/\n\x05value\x18\x02 \x01(\x0b\x32 .temporal.api.common.v1.Payloads:\x02\x38\x01"\xcf\x06\n/ContinueAsNewWorkflowExecutionCommandAttributes\x12;\n\rworkflow_type\x18\x01 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12\x38\n\ntask_queue\x18\x02 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12/\n\x05input\x18\x03 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x37\n\x14workflow_run_timeout\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\x05 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16\x62\x61\x63koff_start_interval\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x0cretry_policy\x18\x07 \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12@\n\tinitiator\x18\x08 \x01(\x0e\x32-.temporal.api.enums.v1.ContinueAsNewInitiator\x12\x31\n\x07\x66\x61ilure\x18\t \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12@\n\x16last_completion_result\x18\n \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x15\n\rcron_schedule\x18\x0b \x01(\t\x12.\n\x06header\x18\x0c \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12*\n\x04memo\x18\r \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo\x12\x43\n\x11search_attributes\x18\x0e \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12\x1c\n\x10inherit_build_id\x18\x0f \x01(\x08\x42\x02\x18\x01"\x9d\x07\n,StartChildWorkflowExecutionCommandAttributes\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12;\n\rworkflow_type\x18\x03 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12\x38\n\ntask_queue\x18\x04 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12/\n\x05input\x18\x05 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12=\n\x1aworkflow_execution_timeout\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x37\n\x14workflow_run_timeout\x18\x07 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x45\n\x13parent_close_policy\x18\t \x01(\x0e\x32(.temporal.api.enums.v1.ParentClosePolicy\x12\x0f\n\x07\x63ontrol\x18\n \x01(\t\x12N\n\x18workflow_id_reuse_policy\x18\x0b \x01(\x0e\x32,.temporal.api.enums.v1.WorkflowIdReusePolicy\x12\x39\n\x0cretry_policy\x18\x0c \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x15\n\rcron_schedule\x18\r \x01(\t\x12.\n\x06header\x18\x0e \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12*\n\x04memo\x18\x0f \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo\x12\x43\n\x11search_attributes\x18\x10 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12\x1c\n\x10inherit_build_id\x18\x11 \x01(\x08\x42\x02\x18\x01\x12\x32\n\x08priority\x18\x12 \x01(\x0b\x32 .temporal.api.common.v1.Priority"6\n ProtocolMessageCommandAttributes\x12\x12\n\nmessage_id\x18\x01 \x01(\t"\xea\x02\n\'ScheduleNexusOperationCommandAttributes\x12\x10\n\x08\x65ndpoint\x18\x01 \x01(\t\x12\x0f\n\x07service\x18\x02 \x01(\t\x12\x11\n\toperation\x18\x03 \x01(\t\x12.\n\x05input\x18\x04 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12<\n\x19schedule_to_close_timeout\x18\x05 \x01(\x0b\x32\x19.google.protobuf.Duration\x12g\n\x0cnexus_header\x18\x06 \x03(\x0b\x32Q.temporal.api.command.v1.ScheduleNexusOperationCommandAttributes.NexusHeaderEntry\x1a\x32\n\x10NexusHeaderEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"J\n,RequestCancelNexusOperationCommandAttributes\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03"\xc2\x11\n\x07\x43ommand\x12\x38\n\x0c\x63ommand_type\x18\x01 \x01(\x0e\x32".temporal.api.enums.v1.CommandType\x12\x39\n\ruser_metadata\x18\xad\x02 \x01(\x0b\x32!.temporal.api.sdk.v1.UserMetadata\x12s\n)schedule_activity_task_command_attributes\x18\x02 \x01(\x0b\x32>.temporal.api.command.v1.ScheduleActivityTaskCommandAttributesH\x00\x12^\n\x1estart_timer_command_attributes\x18\x03 \x01(\x0b\x32\x34.temporal.api.command.v1.StartTimerCommandAttributesH\x00\x12}\n.complete_workflow_execution_command_attributes\x18\x04 \x01(\x0b\x32\x43.temporal.api.command.v1.CompleteWorkflowExecutionCommandAttributesH\x00\x12u\n*fail_workflow_execution_command_attributes\x18\x05 \x01(\x0b\x32?.temporal.api.command.v1.FailWorkflowExecutionCommandAttributesH\x00\x12~\n/request_cancel_activity_task_command_attributes\x18\x06 \x01(\x0b\x32\x43.temporal.api.command.v1.RequestCancelActivityTaskCommandAttributesH\x00\x12`\n\x1f\x63\x61ncel_timer_command_attributes\x18\x07 \x01(\x0b\x32\x35.temporal.api.command.v1.CancelTimerCommandAttributesH\x00\x12y\n,cancel_workflow_execution_command_attributes\x18\x08 \x01(\x0b\x32\x41.temporal.api.command.v1.CancelWorkflowExecutionCommandAttributesH\x00\x12\x99\x01\n=request_cancel_external_workflow_execution_command_attributes\x18\t \x01(\x0b\x32P.temporal.api.command.v1.RequestCancelExternalWorkflowExecutionCommandAttributesH\x00\x12\x62\n record_marker_command_attributes\x18\n \x01(\x0b\x32\x36.temporal.api.command.v1.RecordMarkerCommandAttributesH\x00\x12\x89\x01\n5continue_as_new_workflow_execution_command_attributes\x18\x0b \x01(\x0b\x32H.temporal.api.command.v1.ContinueAsNewWorkflowExecutionCommandAttributesH\x00\x12\x82\x01\n1start_child_workflow_execution_command_attributes\x18\x0c \x01(\x0b\x32\x45.temporal.api.command.v1.StartChildWorkflowExecutionCommandAttributesH\x00\x12\x8a\x01\n5signal_external_workflow_execution_command_attributes\x18\r \x01(\x0b\x32I.temporal.api.command.v1.SignalExternalWorkflowExecutionCommandAttributesH\x00\x12\x88\x01\n4upsert_workflow_search_attributes_command_attributes\x18\x0e \x01(\x0b\x32H.temporal.api.command.v1.UpsertWorkflowSearchAttributesCommandAttributesH\x00\x12h\n#protocol_message_command_attributes\x18\x0f \x01(\x0b\x32\x39.temporal.api.command.v1.ProtocolMessageCommandAttributesH\x00\x12{\n-modify_workflow_properties_command_attributes\x18\x11 \x01(\x0b\x32\x42.temporal.api.command.v1.ModifyWorkflowPropertiesCommandAttributesH\x00\x12w\n+schedule_nexus_operation_command_attributes\x18\x12 \x01(\x0b\x32@.temporal.api.command.v1.ScheduleNexusOperationCommandAttributesH\x00\x12\x82\x01\n1request_cancel_nexus_operation_command_attributes\x18\x13 \x01(\x0b\x32\x45.temporal.api.command.v1.RequestCancelNexusOperationCommandAttributesH\x00\x42\x0c\n\nattributesB\x8e\x01\n\x1aio.temporal.api.command.v1B\x0cMessageProtoP\x01Z%go.temporal.io/api/command/v1;command\xaa\x02\x19Temporalio.Api.Command.V1\xea\x02\x1cTemporalio::Api::Command::V1b\x06proto3' + b'\n%temporal/api/command/v1/message.proto\x12\x17temporal.api.command.v1\x1a\x1egoogle/protobuf/duration.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a(temporal/api/enums/v1/command_type.proto\x1a$temporal/api/common/v1/message.proto\x1a%temporal/api/failure/v1/message.proto\x1a\'temporal/api/taskqueue/v1/message.proto\x1a&temporal/api/workflow/v1/message.proto\x1a\'temporal/api/sdk/v1/user_metadata.proto\x1a,temporal/api/sdk/v1/event_group_marker.proto"\xb6\x05\n%ScheduleActivityTaskCommandAttributes\x12\x13\n\x0b\x61\x63tivity_id\x18\x01 \x01(\t\x12;\n\ractivity_type\x18\x02 \x01(\x0b\x32$.temporal.api.common.v1.ActivityType\x12\x38\n\ntask_queue\x18\x04 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12.\n\x06header\x18\x05 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12/\n\x05input\x18\x06 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12<\n\x19schedule_to_close_timeout\x18\x07 \x01(\x0b\x32\x19.google.protobuf.Duration\x12<\n\x19schedule_to_start_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x34\n\x11heartbeat_timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x0cretry_policy\x18\x0b \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x1f\n\x17request_eager_execution\x18\x0c \x01(\x08\x12\x1d\n\x15use_workflow_build_id\x18\r \x01(\x08\x12\x32\n\x08priority\x18\x0e \x01(\x0b\x32 .temporal.api.common.v1.PriorityJ\x04\x08\x03\x10\x04"H\n*RequestCancelActivityTaskCommandAttributes\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03"i\n\x1bStartTimerCommandAttributes\x12\x10\n\x08timer_id\x18\x01 \x01(\t\x12\x38\n\x15start_to_fire_timeout\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration"^\n*CompleteWorkflowExecutionCommandAttributes\x12\x30\n\x06result\x18\x01 \x01(\x0b\x32 .temporal.api.common.v1.Payloads"[\n&FailWorkflowExecutionCommandAttributes\x12\x31\n\x07\x66\x61ilure\x18\x01 \x01(\x0b\x32 .temporal.api.failure.v1.Failure"0\n\x1c\x43\x61ncelTimerCommandAttributes\x12\x10\n\x08timer_id\x18\x01 \x01(\t"]\n(CancelWorkflowExecutionCommandAttributes\x12\x31\n\x07\x64\x65tails\x18\x01 \x01(\x0b\x32 .temporal.api.common.v1.Payloads"\xb7\x01\n7RequestCancelExternalWorkflowExecutionCommandAttributes\x12\x15\n\tnamespace\x18\x01 \x01(\tB\x02\x18\x01\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\t\x12\x13\n\x07\x63ontrol\x18\x04 \x01(\tB\x02\x18\x01\x12\x1b\n\x13\x63hild_workflow_only\x18\x05 \x01(\x08\x12\x0e\n\x06reason\x18\x06 \x01(\t"\xaf\x02\n0SignalExternalWorkflowExecutionCommandAttributes\x12\x15\n\tnamespace\x18\x01 \x01(\tB\x02\x18\x01\x12<\n\texecution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x13\n\x0bsignal_name\x18\x03 \x01(\t\x12/\n\x05input\x18\x04 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x13\n\x07\x63ontrol\x18\x05 \x01(\tB\x02\x18\x01\x12\x1b\n\x13\x63hild_workflow_only\x18\x06 \x01(\x08\x12.\n\x06header\x18\x07 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header"v\n/UpsertWorkflowSearchAttributesCommandAttributes\x12\x43\n\x11search_attributes\x18\x01 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes"`\n)ModifyWorkflowPropertiesCommandAttributes\x12\x33\n\rupserted_memo\x18\x01 \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo"\xbf\x02\n\x1dRecordMarkerCommandAttributes\x12\x13\n\x0bmarker_name\x18\x01 \x01(\t\x12T\n\x07\x64\x65tails\x18\x02 \x03(\x0b\x32\x43.temporal.api.command.v1.RecordMarkerCommandAttributes.DetailsEntry\x12.\n\x06header\x18\x03 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12\x31\n\x07\x66\x61ilure\x18\x04 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x1aP\n\x0c\x44\x65tailsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12/\n\x05value\x18\x02 \x01(\x0b\x32 .temporal.api.common.v1.Payloads:\x02\x38\x01"\xac\x07\n/ContinueAsNewWorkflowExecutionCommandAttributes\x12;\n\rworkflow_type\x18\x01 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12\x38\n\ntask_queue\x18\x02 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12/\n\x05input\x18\x03 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x37\n\x14workflow_run_timeout\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\x05 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16\x62\x61\x63koff_start_interval\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x0cretry_policy\x18\x07 \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12@\n\tinitiator\x18\x08 \x01(\x0e\x32-.temporal.api.enums.v1.ContinueAsNewInitiator\x12\x31\n\x07\x66\x61ilure\x18\t \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12@\n\x16last_completion_result\x18\n \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x15\n\rcron_schedule\x18\x0b \x01(\t\x12.\n\x06header\x18\x0c \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12*\n\x04memo\x18\r \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo\x12\x43\n\x11search_attributes\x18\x0e \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12\x1c\n\x10inherit_build_id\x18\x0f \x01(\x08\x42\x02\x18\x01\x12[\n\x1binitial_versioning_behavior\x18\x10 \x01(\x0e\x32\x36.temporal.api.enums.v1.ContinueAsNewVersioningBehavior"\xec\x07\n,StartChildWorkflowExecutionCommandAttributes\x12\x15\n\tnamespace\x18\x01 \x01(\tB\x02\x18\x01\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12;\n\rworkflow_type\x18\x03 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12\x38\n\ntask_queue\x18\x04 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12/\n\x05input\x18\x05 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12=\n\x1aworkflow_execution_timeout\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x37\n\x14workflow_run_timeout\x18\x07 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x45\n\x13parent_close_policy\x18\t \x01(\x0e\x32(.temporal.api.enums.v1.ParentClosePolicy\x12\x0f\n\x07\x63ontrol\x18\n \x01(\t\x12N\n\x18workflow_id_reuse_policy\x18\x0b \x01(\x0e\x32,.temporal.api.enums.v1.WorkflowIdReusePolicy\x12\x39\n\x0cretry_policy\x18\x0c \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x15\n\rcron_schedule\x18\r \x01(\t\x12.\n\x06header\x18\x0e \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12*\n\x04memo\x18\x0f \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo\x12\x43\n\x11search_attributes\x18\x10 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12\x1c\n\x10inherit_build_id\x18\x11 \x01(\x08\x42\x02\x18\x01\x12\x32\n\x08priority\x18\x12 \x01(\x0b\x32 .temporal.api.common.v1.Priority\x12I\n\x13versioning_override\x18\x13 \x01(\x0b\x32,.temporal.api.workflow.v1.VersioningOverride"6\n ProtocolMessageCommandAttributes\x12\x12\n\nmessage_id\x18\x01 \x01(\t"\xe3\x03\n\'ScheduleNexusOperationCommandAttributes\x12\x10\n\x08\x65ndpoint\x18\x01 \x01(\t\x12\x0f\n\x07service\x18\x02 \x01(\t\x12\x11\n\toperation\x18\x03 \x01(\t\x12.\n\x05input\x18\x04 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12<\n\x19schedule_to_close_timeout\x18\x05 \x01(\x0b\x32\x19.google.protobuf.Duration\x12g\n\x0cnexus_header\x18\x06 \x03(\x0b\x32Q.temporal.api.command.v1.ScheduleNexusOperationCommandAttributes.NexusHeaderEntry\x12<\n\x19schedule_to_start_timeout\x18\x07 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x1a\x32\n\x10NexusHeaderEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"J\n,RequestCancelNexusOperationCommandAttributes\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03"\x87\x12\n\x07\x43ommand\x12\x38\n\x0c\x63ommand_type\x18\x01 \x01(\x0e\x32".temporal.api.enums.v1.CommandType\x12\x39\n\ruser_metadata\x18\xad\x02 \x01(\x0b\x32!.temporal.api.sdk.v1.UserMetadata\x12\x43\n\x13\x65vent_group_markers\x18\xae\x02 \x03(\x0b\x32%.temporal.api.sdk.v1.EventGroupMarker\x12s\n)schedule_activity_task_command_attributes\x18\x02 \x01(\x0b\x32>.temporal.api.command.v1.ScheduleActivityTaskCommandAttributesH\x00\x12^\n\x1estart_timer_command_attributes\x18\x03 \x01(\x0b\x32\x34.temporal.api.command.v1.StartTimerCommandAttributesH\x00\x12}\n.complete_workflow_execution_command_attributes\x18\x04 \x01(\x0b\x32\x43.temporal.api.command.v1.CompleteWorkflowExecutionCommandAttributesH\x00\x12u\n*fail_workflow_execution_command_attributes\x18\x05 \x01(\x0b\x32?.temporal.api.command.v1.FailWorkflowExecutionCommandAttributesH\x00\x12~\n/request_cancel_activity_task_command_attributes\x18\x06 \x01(\x0b\x32\x43.temporal.api.command.v1.RequestCancelActivityTaskCommandAttributesH\x00\x12`\n\x1f\x63\x61ncel_timer_command_attributes\x18\x07 \x01(\x0b\x32\x35.temporal.api.command.v1.CancelTimerCommandAttributesH\x00\x12y\n,cancel_workflow_execution_command_attributes\x18\x08 \x01(\x0b\x32\x41.temporal.api.command.v1.CancelWorkflowExecutionCommandAttributesH\x00\x12\x99\x01\n=request_cancel_external_workflow_execution_command_attributes\x18\t \x01(\x0b\x32P.temporal.api.command.v1.RequestCancelExternalWorkflowExecutionCommandAttributesH\x00\x12\x62\n record_marker_command_attributes\x18\n \x01(\x0b\x32\x36.temporal.api.command.v1.RecordMarkerCommandAttributesH\x00\x12\x89\x01\n5continue_as_new_workflow_execution_command_attributes\x18\x0b \x01(\x0b\x32H.temporal.api.command.v1.ContinueAsNewWorkflowExecutionCommandAttributesH\x00\x12\x82\x01\n1start_child_workflow_execution_command_attributes\x18\x0c \x01(\x0b\x32\x45.temporal.api.command.v1.StartChildWorkflowExecutionCommandAttributesH\x00\x12\x8a\x01\n5signal_external_workflow_execution_command_attributes\x18\r \x01(\x0b\x32I.temporal.api.command.v1.SignalExternalWorkflowExecutionCommandAttributesH\x00\x12\x88\x01\n4upsert_workflow_search_attributes_command_attributes\x18\x0e \x01(\x0b\x32H.temporal.api.command.v1.UpsertWorkflowSearchAttributesCommandAttributesH\x00\x12h\n#protocol_message_command_attributes\x18\x0f \x01(\x0b\x32\x39.temporal.api.command.v1.ProtocolMessageCommandAttributesH\x00\x12{\n-modify_workflow_properties_command_attributes\x18\x11 \x01(\x0b\x32\x42.temporal.api.command.v1.ModifyWorkflowPropertiesCommandAttributesH\x00\x12w\n+schedule_nexus_operation_command_attributes\x18\x12 \x01(\x0b\x32@.temporal.api.command.v1.ScheduleNexusOperationCommandAttributesH\x00\x12\x82\x01\n1request_cancel_nexus_operation_command_attributes\x18\x13 \x01(\x0b\x32\x45.temporal.api.command.v1.RequestCancelNexusOperationCommandAttributesH\x00\x42\x0c\n\nattributesB\x8e\x01\n\x1aio.temporal.api.command.v1B\x0cMessageProtoP\x01Z%go.temporal.io/api/command/v1;command\xaa\x02\x19Temporalio.Api.Command.V1\xea\x02\x1cTemporalio::Api::Command::V1b\x06proto3' ) @@ -105,7 +111,7 @@ (_message.Message,), { "DESCRIPTOR": _SCHEDULEACTIVITYTASKCOMMANDATTRIBUTES, - "__module__": "temporal.api.command.v1.message_pb2", + "__module__": "temporalio.api.command.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.command.v1.ScheduleActivityTaskCommandAttributes) }, ) @@ -116,7 +122,7 @@ (_message.Message,), { "DESCRIPTOR": _REQUESTCANCELACTIVITYTASKCOMMANDATTRIBUTES, - "__module__": "temporal.api.command.v1.message_pb2", + "__module__": "temporalio.api.command.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.command.v1.RequestCancelActivityTaskCommandAttributes) }, ) @@ -127,7 +133,7 @@ (_message.Message,), { "DESCRIPTOR": _STARTTIMERCOMMANDATTRIBUTES, - "__module__": "temporal.api.command.v1.message_pb2", + "__module__": "temporalio.api.command.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.command.v1.StartTimerCommandAttributes) }, ) @@ -138,7 +144,7 @@ (_message.Message,), { "DESCRIPTOR": _COMPLETEWORKFLOWEXECUTIONCOMMANDATTRIBUTES, - "__module__": "temporal.api.command.v1.message_pb2", + "__module__": "temporalio.api.command.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.command.v1.CompleteWorkflowExecutionCommandAttributes) }, ) @@ -149,7 +155,7 @@ (_message.Message,), { "DESCRIPTOR": _FAILWORKFLOWEXECUTIONCOMMANDATTRIBUTES, - "__module__": "temporal.api.command.v1.message_pb2", + "__module__": "temporalio.api.command.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.command.v1.FailWorkflowExecutionCommandAttributes) }, ) @@ -160,7 +166,7 @@ (_message.Message,), { "DESCRIPTOR": _CANCELTIMERCOMMANDATTRIBUTES, - "__module__": "temporal.api.command.v1.message_pb2", + "__module__": "temporalio.api.command.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.command.v1.CancelTimerCommandAttributes) }, ) @@ -171,7 +177,7 @@ (_message.Message,), { "DESCRIPTOR": _CANCELWORKFLOWEXECUTIONCOMMANDATTRIBUTES, - "__module__": "temporal.api.command.v1.message_pb2", + "__module__": "temporalio.api.command.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.command.v1.CancelWorkflowExecutionCommandAttributes) }, ) @@ -183,7 +189,7 @@ (_message.Message,), { "DESCRIPTOR": _REQUESTCANCELEXTERNALWORKFLOWEXECUTIONCOMMANDATTRIBUTES, - "__module__": "temporal.api.command.v1.message_pb2", + "__module__": "temporalio.api.command.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.command.v1.RequestCancelExternalWorkflowExecutionCommandAttributes) }, ) @@ -196,7 +202,7 @@ (_message.Message,), { "DESCRIPTOR": _SIGNALEXTERNALWORKFLOWEXECUTIONCOMMANDATTRIBUTES, - "__module__": "temporal.api.command.v1.message_pb2", + "__module__": "temporalio.api.command.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.command.v1.SignalExternalWorkflowExecutionCommandAttributes) }, ) @@ -209,7 +215,7 @@ (_message.Message,), { "DESCRIPTOR": _UPSERTWORKFLOWSEARCHATTRIBUTESCOMMANDATTRIBUTES, - "__module__": "temporal.api.command.v1.message_pb2", + "__module__": "temporalio.api.command.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.command.v1.UpsertWorkflowSearchAttributesCommandAttributes) }, ) @@ -221,7 +227,7 @@ (_message.Message,), { "DESCRIPTOR": _MODIFYWORKFLOWPROPERTIESCOMMANDATTRIBUTES, - "__module__": "temporal.api.command.v1.message_pb2", + "__module__": "temporalio.api.command.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.command.v1.ModifyWorkflowPropertiesCommandAttributes) }, ) @@ -236,12 +242,12 @@ (_message.Message,), { "DESCRIPTOR": _RECORDMARKERCOMMANDATTRIBUTES_DETAILSENTRY, - "__module__": "temporal.api.command.v1.message_pb2", + "__module__": "temporalio.api.command.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.command.v1.RecordMarkerCommandAttributes.DetailsEntry) }, ), "DESCRIPTOR": _RECORDMARKERCOMMANDATTRIBUTES, - "__module__": "temporal.api.command.v1.message_pb2", + "__module__": "temporalio.api.command.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.command.v1.RecordMarkerCommandAttributes) }, ) @@ -254,7 +260,7 @@ (_message.Message,), { "DESCRIPTOR": _CONTINUEASNEWWORKFLOWEXECUTIONCOMMANDATTRIBUTES, - "__module__": "temporal.api.command.v1.message_pb2", + "__module__": "temporalio.api.command.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.command.v1.ContinueAsNewWorkflowExecutionCommandAttributes) }, ) @@ -266,7 +272,7 @@ (_message.Message,), { "DESCRIPTOR": _STARTCHILDWORKFLOWEXECUTIONCOMMANDATTRIBUTES, - "__module__": "temporal.api.command.v1.message_pb2", + "__module__": "temporalio.api.command.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.command.v1.StartChildWorkflowExecutionCommandAttributes) }, ) @@ -277,7 +283,7 @@ (_message.Message,), { "DESCRIPTOR": _PROTOCOLMESSAGECOMMANDATTRIBUTES, - "__module__": "temporal.api.command.v1.message_pb2", + "__module__": "temporalio.api.command.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.command.v1.ProtocolMessageCommandAttributes) }, ) @@ -292,12 +298,12 @@ (_message.Message,), { "DESCRIPTOR": _SCHEDULENEXUSOPERATIONCOMMANDATTRIBUTES_NEXUSHEADERENTRY, - "__module__": "temporal.api.command.v1.message_pb2", + "__module__": "temporalio.api.command.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.command.v1.ScheduleNexusOperationCommandAttributes.NexusHeaderEntry) }, ), "DESCRIPTOR": _SCHEDULENEXUSOPERATIONCOMMANDATTRIBUTES, - "__module__": "temporal.api.command.v1.message_pb2", + "__module__": "temporalio.api.command.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.command.v1.ScheduleNexusOperationCommandAttributes) }, ) @@ -309,7 +315,7 @@ (_message.Message,), { "DESCRIPTOR": _REQUESTCANCELNEXUSOPERATIONCOMMANDATTRIBUTES, - "__module__": "temporal.api.command.v1.message_pb2", + "__module__": "temporalio.api.command.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.command.v1.RequestCancelNexusOperationCommandAttributes) }, ) @@ -320,7 +326,7 @@ (_message.Message,), { "DESCRIPTOR": _COMMAND, - "__module__": "temporal.api.command.v1.message_pb2", + "__module__": "temporalio.api.command.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.command.v1.Command) }, ) @@ -329,12 +335,24 @@ if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b"\n\032io.temporal.api.command.v1B\014MessageProtoP\001Z%go.temporal.io/api/command/v1;command\252\002\031Temporalio.Api.Command.V1\352\002\034Temporalio::Api::Command::V1" + _REQUESTCANCELEXTERNALWORKFLOWEXECUTIONCOMMANDATTRIBUTES.fields_by_name[ + "namespace" + ]._options = None + _REQUESTCANCELEXTERNALWORKFLOWEXECUTIONCOMMANDATTRIBUTES.fields_by_name[ + "namespace" + ]._serialized_options = b"\030\001" _REQUESTCANCELEXTERNALWORKFLOWEXECUTIONCOMMANDATTRIBUTES.fields_by_name[ "control" ]._options = None _REQUESTCANCELEXTERNALWORKFLOWEXECUTIONCOMMANDATTRIBUTES.fields_by_name[ "control" ]._serialized_options = b"\030\001" + _SIGNALEXTERNALWORKFLOWEXECUTIONCOMMANDATTRIBUTES.fields_by_name[ + "namespace" + ]._options = None + _SIGNALEXTERNALWORKFLOWEXECUTIONCOMMANDATTRIBUTES.fields_by_name[ + "namespace" + ]._serialized_options = b"\030\001" _SIGNALEXTERNALWORKFLOWEXECUTIONCOMMANDATTRIBUTES.fields_by_name[ "control" ]._options = None @@ -349,6 +367,12 @@ _CONTINUEASNEWWORKFLOWEXECUTIONCOMMANDATTRIBUTES.fields_by_name[ "inherit_build_id" ]._serialized_options = b"\030\001" + _STARTCHILDWORKFLOWEXECUTIONCOMMANDATTRIBUTES.fields_by_name[ + "namespace" + ]._options = None + _STARTCHILDWORKFLOWEXECUTIONCOMMANDATTRIBUTES.fields_by_name[ + "namespace" + ]._serialized_options = b"\030\001" _STARTCHILDWORKFLOWEXECUTIONCOMMANDATTRIBUTES.fields_by_name[ "inherit_build_id" ]._options = None @@ -359,44 +383,44 @@ _SCHEDULENEXUSOPERATIONCOMMANDATTRIBUTES_NEXUSHEADERENTRY._serialized_options = ( b"8\001" ) - _SCHEDULEACTIVITYTASKCOMMANDATTRIBUTES._serialized_start = 338 - _SCHEDULEACTIVITYTASKCOMMANDATTRIBUTES._serialized_end = 1032 - _REQUESTCANCELACTIVITYTASKCOMMANDATTRIBUTES._serialized_start = 1034 - _REQUESTCANCELACTIVITYTASKCOMMANDATTRIBUTES._serialized_end = 1106 - _STARTTIMERCOMMANDATTRIBUTES._serialized_start = 1108 - _STARTTIMERCOMMANDATTRIBUTES._serialized_end = 1213 - _COMPLETEWORKFLOWEXECUTIONCOMMANDATTRIBUTES._serialized_start = 1215 - _COMPLETEWORKFLOWEXECUTIONCOMMANDATTRIBUTES._serialized_end = 1309 - _FAILWORKFLOWEXECUTIONCOMMANDATTRIBUTES._serialized_start = 1311 - _FAILWORKFLOWEXECUTIONCOMMANDATTRIBUTES._serialized_end = 1402 - _CANCELTIMERCOMMANDATTRIBUTES._serialized_start = 1404 - _CANCELTIMERCOMMANDATTRIBUTES._serialized_end = 1452 - _CANCELWORKFLOWEXECUTIONCOMMANDATTRIBUTES._serialized_start = 1454 - _CANCELWORKFLOWEXECUTIONCOMMANDATTRIBUTES._serialized_end = 1547 - _REQUESTCANCELEXTERNALWORKFLOWEXECUTIONCOMMANDATTRIBUTES._serialized_start = 1550 - _REQUESTCANCELEXTERNALWORKFLOWEXECUTIONCOMMANDATTRIBUTES._serialized_end = 1729 - _SIGNALEXTERNALWORKFLOWEXECUTIONCOMMANDATTRIBUTES._serialized_start = 1732 - _SIGNALEXTERNALWORKFLOWEXECUTIONCOMMANDATTRIBUTES._serialized_end = 2031 - _UPSERTWORKFLOWSEARCHATTRIBUTESCOMMANDATTRIBUTES._serialized_start = 2033 - _UPSERTWORKFLOWSEARCHATTRIBUTESCOMMANDATTRIBUTES._serialized_end = 2151 - _MODIFYWORKFLOWPROPERTIESCOMMANDATTRIBUTES._serialized_start = 2153 - _MODIFYWORKFLOWPROPERTIESCOMMANDATTRIBUTES._serialized_end = 2249 - _RECORDMARKERCOMMANDATTRIBUTES._serialized_start = 2252 - _RECORDMARKERCOMMANDATTRIBUTES._serialized_end = 2571 - _RECORDMARKERCOMMANDATTRIBUTES_DETAILSENTRY._serialized_start = 2491 - _RECORDMARKERCOMMANDATTRIBUTES_DETAILSENTRY._serialized_end = 2571 - _CONTINUEASNEWWORKFLOWEXECUTIONCOMMANDATTRIBUTES._serialized_start = 2574 - _CONTINUEASNEWWORKFLOWEXECUTIONCOMMANDATTRIBUTES._serialized_end = 3421 - _STARTCHILDWORKFLOWEXECUTIONCOMMANDATTRIBUTES._serialized_start = 3424 - _STARTCHILDWORKFLOWEXECUTIONCOMMANDATTRIBUTES._serialized_end = 4349 - _PROTOCOLMESSAGECOMMANDATTRIBUTES._serialized_start = 4351 - _PROTOCOLMESSAGECOMMANDATTRIBUTES._serialized_end = 4405 - _SCHEDULENEXUSOPERATIONCOMMANDATTRIBUTES._serialized_start = 4408 - _SCHEDULENEXUSOPERATIONCOMMANDATTRIBUTES._serialized_end = 4770 - _SCHEDULENEXUSOPERATIONCOMMANDATTRIBUTES_NEXUSHEADERENTRY._serialized_start = 4720 - _SCHEDULENEXUSOPERATIONCOMMANDATTRIBUTES_NEXUSHEADERENTRY._serialized_end = 4770 - _REQUESTCANCELNEXUSOPERATIONCOMMANDATTRIBUTES._serialized_start = 4772 - _REQUESTCANCELNEXUSOPERATIONCOMMANDATTRIBUTES._serialized_end = 4846 - _COMMAND._serialized_start = 4849 - _COMMAND._serialized_end = 7091 + _SCHEDULEACTIVITYTASKCOMMANDATTRIBUTES._serialized_start = 424 + _SCHEDULEACTIVITYTASKCOMMANDATTRIBUTES._serialized_end = 1118 + _REQUESTCANCELACTIVITYTASKCOMMANDATTRIBUTES._serialized_start = 1120 + _REQUESTCANCELACTIVITYTASKCOMMANDATTRIBUTES._serialized_end = 1192 + _STARTTIMERCOMMANDATTRIBUTES._serialized_start = 1194 + _STARTTIMERCOMMANDATTRIBUTES._serialized_end = 1299 + _COMPLETEWORKFLOWEXECUTIONCOMMANDATTRIBUTES._serialized_start = 1301 + _COMPLETEWORKFLOWEXECUTIONCOMMANDATTRIBUTES._serialized_end = 1395 + _FAILWORKFLOWEXECUTIONCOMMANDATTRIBUTES._serialized_start = 1397 + _FAILWORKFLOWEXECUTIONCOMMANDATTRIBUTES._serialized_end = 1488 + _CANCELTIMERCOMMANDATTRIBUTES._serialized_start = 1490 + _CANCELTIMERCOMMANDATTRIBUTES._serialized_end = 1538 + _CANCELWORKFLOWEXECUTIONCOMMANDATTRIBUTES._serialized_start = 1540 + _CANCELWORKFLOWEXECUTIONCOMMANDATTRIBUTES._serialized_end = 1633 + _REQUESTCANCELEXTERNALWORKFLOWEXECUTIONCOMMANDATTRIBUTES._serialized_start = 1636 + _REQUESTCANCELEXTERNALWORKFLOWEXECUTIONCOMMANDATTRIBUTES._serialized_end = 1819 + _SIGNALEXTERNALWORKFLOWEXECUTIONCOMMANDATTRIBUTES._serialized_start = 1822 + _SIGNALEXTERNALWORKFLOWEXECUTIONCOMMANDATTRIBUTES._serialized_end = 2125 + _UPSERTWORKFLOWSEARCHATTRIBUTESCOMMANDATTRIBUTES._serialized_start = 2127 + _UPSERTWORKFLOWSEARCHATTRIBUTESCOMMANDATTRIBUTES._serialized_end = 2245 + _MODIFYWORKFLOWPROPERTIESCOMMANDATTRIBUTES._serialized_start = 2247 + _MODIFYWORKFLOWPROPERTIESCOMMANDATTRIBUTES._serialized_end = 2343 + _RECORDMARKERCOMMANDATTRIBUTES._serialized_start = 2346 + _RECORDMARKERCOMMANDATTRIBUTES._serialized_end = 2665 + _RECORDMARKERCOMMANDATTRIBUTES_DETAILSENTRY._serialized_start = 2585 + _RECORDMARKERCOMMANDATTRIBUTES_DETAILSENTRY._serialized_end = 2665 + _CONTINUEASNEWWORKFLOWEXECUTIONCOMMANDATTRIBUTES._serialized_start = 2668 + _CONTINUEASNEWWORKFLOWEXECUTIONCOMMANDATTRIBUTES._serialized_end = 3608 + _STARTCHILDWORKFLOWEXECUTIONCOMMANDATTRIBUTES._serialized_start = 3611 + _STARTCHILDWORKFLOWEXECUTIONCOMMANDATTRIBUTES._serialized_end = 4615 + _PROTOCOLMESSAGECOMMANDATTRIBUTES._serialized_start = 4617 + _PROTOCOLMESSAGECOMMANDATTRIBUTES._serialized_end = 4671 + _SCHEDULENEXUSOPERATIONCOMMANDATTRIBUTES._serialized_start = 4674 + _SCHEDULENEXUSOPERATIONCOMMANDATTRIBUTES._serialized_end = 5157 + _SCHEDULENEXUSOPERATIONCOMMANDATTRIBUTES_NEXUSHEADERENTRY._serialized_start = 5107 + _SCHEDULENEXUSOPERATIONCOMMANDATTRIBUTES_NEXUSHEADERENTRY._serialized_end = 5157 + _REQUESTCANCELNEXUSOPERATIONCOMMANDATTRIBUTES._serialized_start = 5159 + _REQUESTCANCELNEXUSOPERATIONCOMMANDATTRIBUTES._serialized_end = 5233 + _COMMAND._serialized_start = 5236 + _COMMAND._serialized_end = 7547 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/command/v1/message_pb2.pyi b/temporalio/api/command/v1/message_pb2.pyi index 5a60e3aa1..18d0abc8b 100644 --- a/temporalio/api/command/v1/message_pb2.pyi +++ b/temporalio/api/command/v1/message_pb2.pyi @@ -16,8 +16,10 @@ import temporalio.api.common.v1.message_pb2 import temporalio.api.enums.v1.command_type_pb2 import temporalio.api.enums.v1.workflow_pb2 import temporalio.api.failure.v1.message_pb2 +import temporalio.api.sdk.v1.event_group_marker_pb2 import temporalio.api.sdk.v1.user_metadata_pb2 import temporalio.api.taskqueue.v1.message_pb2 +import temporalio.api.workflow.v1.message_pb2 if sys.version_info >= (3, 8): import typing as typing_extensions @@ -332,6 +334,7 @@ class RequestCancelExternalWorkflowExecutionCommandAttributes( CHILD_WORKFLOW_ONLY_FIELD_NUMBER: builtins.int REASON_FIELD_NUMBER: builtins.int namespace: builtins.str + """Deprecated. Cross-namespace operations are disabled by default as of server 1.30.1.""" workflow_id: builtins.str run_id: builtins.str control: builtins.str @@ -386,6 +389,7 @@ class SignalExternalWorkflowExecutionCommandAttributes(google.protobuf.message.M CHILD_WORKFLOW_ONLY_FIELD_NUMBER: builtins.int HEADER_FIELD_NUMBER: builtins.int namespace: builtins.str + """Deprecated. Cross-namespace operations are disabled by default as of server 1.30.1.""" @property def execution(self) -> temporalio.api.common.v1.message_pb2.WorkflowExecution: ... signal_name: builtins.str @@ -594,6 +598,7 @@ class ContinueAsNewWorkflowExecutionCommandAttributes(google.protobuf.message.Me MEMO_FIELD_NUMBER: builtins.int SEARCH_ATTRIBUTES_FIELD_NUMBER: builtins.int INHERIT_BUILD_ID_FIELD_NUMBER: builtins.int + INITIAL_VERSIONING_BEHAVIOR_FIELD_NUMBER: builtins.int @property def workflow_type(self) -> temporalio.api.common.v1.message_pb2.WorkflowType: ... @property @@ -634,6 +639,13 @@ class ContinueAsNewWorkflowExecutionCommandAttributes(google.protobuf.message.Me the assignment rules will be used to independently assign a Build ID to the new execution. Deprecated. Only considered for versioning v0.2. """ + initial_versioning_behavior: ( + temporalio.api.enums.v1.workflow_pb2.ContinueAsNewVersioningBehavior.ValueType + ) + """Experimental. Optionally decide the versioning behavior that the first task of the new run should use. + For example, choose to AutoUpgrade on continue-as-new instead of inheriting the pinned version + of the previous run. + """ def __init__( self, *, @@ -654,6 +666,7 @@ class ContinueAsNewWorkflowExecutionCommandAttributes(google.protobuf.message.Me search_attributes: temporalio.api.common.v1.message_pb2.SearchAttributes | None = ..., inherit_build_id: builtins.bool = ..., + initial_versioning_behavior: temporalio.api.enums.v1.workflow_pb2.ContinueAsNewVersioningBehavior.ValueType = ..., ) -> None: ... def HasField( self, @@ -697,6 +710,8 @@ class ContinueAsNewWorkflowExecutionCommandAttributes(google.protobuf.message.Me b"header", "inherit_build_id", b"inherit_build_id", + "initial_versioning_behavior", + b"initial_versioning_behavior", "initiator", b"initiator", "input", @@ -745,7 +760,9 @@ class StartChildWorkflowExecutionCommandAttributes(google.protobuf.message.Messa SEARCH_ATTRIBUTES_FIELD_NUMBER: builtins.int INHERIT_BUILD_ID_FIELD_NUMBER: builtins.int PRIORITY_FIELD_NUMBER: builtins.int + VERSIONING_OVERRIDE_FIELD_NUMBER: builtins.int namespace: builtins.str + """Deprecated. Cross-namespace operations are disabled by default as of server 1.30.1.""" workflow_id: builtins.str @property def workflow_type(self) -> temporalio.api.common.v1.message_pb2.WorkflowType: ... @@ -793,6 +810,13 @@ class StartChildWorkflowExecutionCommandAttributes(google.protobuf.message.Messa """Priority metadata. If this message is not present, or any fields are not present, they inherit the values from the workflow. """ + @property + def versioning_override( + self, + ) -> temporalio.api.workflow.v1.message_pb2.VersioningOverride: + """Versioning override for the child workflow. If present, this explicit override takes + precedence over versioning behavior inherited from the parent workflow. + """ def __init__( self, *, @@ -815,6 +839,8 @@ class StartChildWorkflowExecutionCommandAttributes(google.protobuf.message.Messa | None = ..., inherit_build_id: builtins.bool = ..., priority: temporalio.api.common.v1.message_pb2.Priority | None = ..., + versioning_override: temporalio.api.workflow.v1.message_pb2.VersioningOverride + | None = ..., ) -> None: ... def HasField( self, @@ -833,6 +859,8 @@ class StartChildWorkflowExecutionCommandAttributes(google.protobuf.message.Messa b"search_attributes", "task_queue", b"task_queue", + "versioning_override", + b"versioning_override", "workflow_execution_timeout", b"workflow_execution_timeout", "workflow_run_timeout", @@ -870,6 +898,8 @@ class StartChildWorkflowExecutionCommandAttributes(google.protobuf.message.Messa b"search_attributes", "task_queue", b"task_queue", + "versioning_override", + b"versioning_override", "workflow_execution_timeout", b"workflow_execution_timeout", "workflow_id", @@ -933,6 +963,8 @@ class ScheduleNexusOperationCommandAttributes(google.protobuf.message.Message): INPUT_FIELD_NUMBER: builtins.int SCHEDULE_TO_CLOSE_TIMEOUT_FIELD_NUMBER: builtins.int NEXUS_HEADER_FIELD_NUMBER: builtins.int + SCHEDULE_TO_START_TIMEOUT_FIELD_NUMBER: builtins.int + START_TO_CLOSE_TIMEOUT_FIELD_NUMBER: builtins.int endpoint: builtins.str """Endpoint name, must exist in the endpoint registry or this command will fail.""" service: builtins.str @@ -964,6 +996,29 @@ class ScheduleNexusOperationCommandAttributes(google.protobuf.message.Message): Note these headers are not the same as Temporal headers on internal activities and child workflows, these are transmitted to Nexus operations that may be external and are not traditional payloads. """ + @property + def schedule_to_start_timeout(self) -> google.protobuf.duration_pb2.Duration: + """Schedule-to-start timeout for this operation. + Indicates how long the caller is willing to wait for the operation to be started (or completed if synchronous) + by the handler. If the operation is not started within this timeout, it will fail with + TIMEOUT_TYPE_SCHEDULE_TO_START. + If not set or zero, no schedule-to-start timeout is enforced. + (-- api-linter: core::0140::prepositions=disabled + aip.dev/not-precedent: "to" is used to indicate interval. --) + Requires server version 1.31.0 or later. + """ + @property + def start_to_close_timeout(self) -> google.protobuf.duration_pb2.Duration: + """Start-to-close timeout for this operation. + Indicates how long the caller is willing to wait for an asynchronous operation to complete after it has been + started. If the operation does not complete within this timeout after starting, it will fail with + TIMEOUT_TYPE_START_TO_CLOSE. + Only applies to asynchronous operations. Synchronous operations ignore this timeout. + If not set or zero, no start-to-close timeout is enforced. + (-- api-linter: core::0140::prepositions=disabled + aip.dev/not-precedent: "to" is used to indicate interval. --) + Requires server version 1.31.0 or later. + """ def __init__( self, *, @@ -973,11 +1028,20 @@ class ScheduleNexusOperationCommandAttributes(google.protobuf.message.Message): input: temporalio.api.common.v1.message_pb2.Payload | None = ..., schedule_to_close_timeout: google.protobuf.duration_pb2.Duration | None = ..., nexus_header: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + schedule_to_start_timeout: google.protobuf.duration_pb2.Duration | None = ..., + start_to_close_timeout: google.protobuf.duration_pb2.Duration | None = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal[ - "input", b"input", "schedule_to_close_timeout", b"schedule_to_close_timeout" + "input", + b"input", + "schedule_to_close_timeout", + b"schedule_to_close_timeout", + "schedule_to_start_timeout", + b"schedule_to_start_timeout", + "start_to_close_timeout", + b"start_to_close_timeout", ], ) -> builtins.bool: ... def ClearField( @@ -993,8 +1057,12 @@ class ScheduleNexusOperationCommandAttributes(google.protobuf.message.Message): b"operation", "schedule_to_close_timeout", b"schedule_to_close_timeout", + "schedule_to_start_timeout", + b"schedule_to_start_timeout", "service", b"service", + "start_to_close_timeout", + b"start_to_close_timeout", ], ) -> None: ... @@ -1031,6 +1099,7 @@ class Command(google.protobuf.message.Message): COMMAND_TYPE_FIELD_NUMBER: builtins.int USER_METADATA_FIELD_NUMBER: builtins.int + EVENT_GROUP_MARKERS_FIELD_NUMBER: builtins.int SCHEDULE_ACTIVITY_TASK_COMMAND_ATTRIBUTES_FIELD_NUMBER: builtins.int START_TIMER_COMMAND_ATTRIBUTES_FIELD_NUMBER: builtins.int COMPLETE_WORKFLOW_EXECUTION_COMMAND_ATTRIBUTES_FIELD_NUMBER: builtins.int @@ -1065,6 +1134,13 @@ class Command(google.protobuf.message.Message): started where the summary is used to identify the timer. """ @property + def event_group_markers( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.sdk.v1.event_group_marker_pb2.EventGroupMarker + ]: + """Event Group Markers attached to the command by the workflow author.""" + @property def schedule_activity_task_command_attributes( self, ) -> global___ScheduleActivityTaskCommandAttributes: ... @@ -1139,6 +1215,10 @@ class Command(google.protobuf.message.Message): command_type: temporalio.api.enums.v1.command_type_pb2.CommandType.ValueType = ..., user_metadata: temporalio.api.sdk.v1.user_metadata_pb2.UserMetadata | None = ..., + event_group_markers: collections.abc.Iterable[ + temporalio.api.sdk.v1.event_group_marker_pb2.EventGroupMarker + ] + | None = ..., schedule_activity_task_command_attributes: global___ScheduleActivityTaskCommandAttributes | None = ..., start_timer_command_attributes: global___StartTimerCommandAttributes @@ -1232,6 +1312,8 @@ class Command(google.protobuf.message.Message): b"complete_workflow_execution_command_attributes", "continue_as_new_workflow_execution_command_attributes", b"continue_as_new_workflow_execution_command_attributes", + "event_group_markers", + b"event_group_markers", "fail_workflow_execution_command_attributes", b"fail_workflow_execution_command_attributes", "modify_workflow_properties_command_attributes", diff --git a/temporalio/api/common/v1/__init__.py b/temporalio/api/common/v1/__init__.py index b3d074f41..e613d71f8 100644 --- a/temporalio/api/common/v1/__init__.py +++ b/temporalio/api/common/v1/__init__.py @@ -3,16 +3,24 @@ ActivityType, Callback, DataBlob, + Execution, + FastForwardConfig, Header, Link, Memo, MeteringMetadata, + OnConflictOptions, Payload, Payloads, + Principal, Priority, ResetOptions, RetryPolicy, SearchAttributes, + TimeSkippingConfig, + TimeSkippingFastForwardInfo, + TimeSkippingInfo, + TimeSkippingStatePropagation, WorkerSelector, WorkerVersionCapabilities, WorkerVersionStamp, @@ -24,17 +32,25 @@ "ActivityType", "Callback", "DataBlob", + "Execution", + "FastForwardConfig", "GrpcStatus", "Header", "Link", "Memo", "MeteringMetadata", + "OnConflictOptions", "Payload", "Payloads", + "Principal", "Priority", "ResetOptions", "RetryPolicy", "SearchAttributes", + "TimeSkippingConfig", + "TimeSkippingFastForwardInfo", + "TimeSkippingInfo", + "TimeSkippingStatePropagation", "WorkerSelector", "WorkerVersionCapabilities", "WorkerVersionStamp", diff --git a/temporalio/api/common/v1/grpc_status_pb2.py b/temporalio/api/common/v1/grpc_status_pb2.py index fd75d79be..17642e92e 100644 --- a/temporalio/api/common/v1/grpc_status_pb2.py +++ b/temporalio/api/common/v1/grpc_status_pb2.py @@ -27,7 +27,7 @@ (_message.Message,), { "DESCRIPTOR": _GRPCSTATUS, - "__module__": "temporal.api.common.v1.grpc_status_pb2", + "__module__": "temporalio.api.common.v1.grpc_status_pb2", # @@protoc_insertion_point(class_scope:temporal.api.common.v1.GrpcStatus) }, ) diff --git a/temporalio/api/common/v1/message_pb2.py b/temporalio/api/common/v1/message_pb2.py index a30edcac2..538be6a23 100644 --- a/temporalio/api/common/v1/message_pb2.py +++ b/temporalio/api/common/v1/message_pb2.py @@ -16,6 +16,7 @@ from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2 from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 from temporalio.api.enums.v1 import ( common_pb2 as temporal_dot_api_dot_enums_dot_v1_dot_common__pb2, @@ -28,7 +29,7 @@ ) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n$temporal/api/common/v1/message.proto\x12\x16temporal.api.common.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a"temporal/api/enums/v1/common.proto\x1a&temporal/api/enums/v1/event_type.proto\x1a!temporal/api/enums/v1/reset.proto"T\n\x08\x44\x61taBlob\x12:\n\rencoding_type\x18\x01 \x01(\x0e\x32#.temporal.api.enums.v1.EncodingType\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c"=\n\x08Payloads\x12\x31\n\x08payloads\x18\x01 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload"\x89\x01\n\x07Payload\x12?\n\x08metadata\x18\x01 \x03(\x0b\x32-.temporal.api.common.v1.Payload.MetadataEntry\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x0c:\x02\x38\x01"\xbe\x01\n\x10SearchAttributes\x12S\n\x0eindexed_fields\x18\x01 \x03(\x0b\x32;.temporal.api.common.v1.SearchAttributes.IndexedFieldsEntry\x1aU\n\x12IndexedFieldsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\x90\x01\n\x04Memo\x12\x38\n\x06\x66ields\x18\x01 \x03(\x0b\x32(.temporal.api.common.v1.Memo.FieldsEntry\x1aN\n\x0b\x46ieldsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\x94\x01\n\x06Header\x12:\n\x06\x66ields\x18\x01 \x03(\x0b\x32*.temporal.api.common.v1.Header.FieldsEntry\x1aN\n\x0b\x46ieldsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"8\n\x11WorkflowExecution\x12\x13\n\x0bworkflow_id\x18\x01 \x01(\t\x12\x0e\n\x06run_id\x18\x02 \x01(\t"\x1c\n\x0cWorkflowType\x12\x0c\n\x04name\x18\x01 \x01(\t"\x1c\n\x0c\x41\x63tivityType\x12\x0c\n\x04name\x18\x01 \x01(\t"\xd1\x01\n\x0bRetryPolicy\x12\x33\n\x10initial_interval\x18\x01 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x1b\n\x13\x62\x61\x63koff_coefficient\x18\x02 \x01(\x01\x12\x33\n\x10maximum_interval\x18\x03 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x18\n\x10maximum_attempts\x18\x04 \x01(\x05\x12!\n\x19non_retryable_error_types\x18\x05 \x03(\t"F\n\x10MeteringMetadata\x12\x32\n*nonfirst_local_activity_execution_attempts\x18\r \x01(\r">\n\x12WorkerVersionStamp\x12\x10\n\x08\x62uild_id\x18\x01 \x01(\t\x12\x16\n\x0euse_versioning\x18\x03 \x01(\x08"e\n\x19WorkerVersionCapabilities\x12\x10\n\x08\x62uild_id\x18\x01 \x01(\t\x12\x16\n\x0euse_versioning\x18\x02 \x01(\x08\x12\x1e\n\x16\x64\x65ployment_series_name\x18\x04 \x01(\t"\xed\x02\n\x0cResetOptions\x12\x35\n\x13\x66irst_workflow_task\x18\x01 \x01(\x0b\x32\x16.google.protobuf.EmptyH\x00\x12\x34\n\x12last_workflow_task\x18\x02 \x01(\x0b\x32\x16.google.protobuf.EmptyH\x00\x12\x1a\n\x10workflow_task_id\x18\x03 \x01(\x03H\x00\x12\x12\n\x08\x62uild_id\x18\x04 \x01(\tH\x00\x12G\n\x12reset_reapply_type\x18\n \x01(\x0e\x32\'.temporal.api.enums.v1.ResetReapplyTypeB\x02\x18\x01\x12\x18\n\x10\x63urrent_run_only\x18\x0b \x01(\x08\x12S\n\x1breset_reapply_exclude_types\x18\x0c \x03(\x0e\x32..temporal.api.enums.v1.ResetReapplyExcludeTypeB\x08\n\x06target"\xe4\x02\n\x08\x43\x61llback\x12\x37\n\x05nexus\x18\x02 \x01(\x0b\x32&.temporal.api.common.v1.Callback.NexusH\x00\x12=\n\x08internal\x18\x03 \x01(\x0b\x32).temporal.api.common.v1.Callback.InternalH\x00\x12+\n\x05links\x18\x64 \x03(\x0b\x32\x1c.temporal.api.common.v1.Link\x1a\x87\x01\n\x05Nexus\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\x42\n\x06header\x18\x02 \x03(\x0b\x32\x32.temporal.api.common.v1.Callback.Nexus.HeaderEntry\x1a-\n\x0bHeaderEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a\x18\n\x08Internal\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x42\t\n\x07variantJ\x04\x08\x01\x10\x02"\xe9\x04\n\x04Link\x12\x44\n\x0eworkflow_event\x18\x01 \x01(\x0b\x32*.temporal.api.common.v1.Link.WorkflowEventH\x00\x12:\n\tbatch_job\x18\x02 \x01(\x0b\x32%.temporal.api.common.v1.Link.BatchJobH\x00\x1a\xb7\x03\n\rWorkflowEvent\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\t\x12N\n\tevent_ref\x18\x64 \x01(\x0b\x32\x39.temporal.api.common.v1.Link.WorkflowEvent.EventReferenceH\x00\x12W\n\x0erequest_id_ref\x18\x65 \x01(\x0b\x32=.temporal.api.common.v1.Link.WorkflowEvent.RequestIdReferenceH\x00\x1aX\n\x0e\x45ventReference\x12\x10\n\x08\x65vent_id\x18\x01 \x01(\x03\x12\x34\n\nevent_type\x18\x02 \x01(\x0e\x32 .temporal.api.enums.v1.EventType\x1a^\n\x12RequestIdReference\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12\x34\n\nevent_type\x18\x02 \x01(\x0e\x32 .temporal.api.enums.v1.EventTypeB\x0b\n\treference\x1a\x1a\n\x08\x42\x61tchJob\x12\x0e\n\x06job_id\x18\x01 \x01(\tB\t\n\x07variant"O\n\x08Priority\x12\x14\n\x0cpriority_key\x18\x01 \x01(\x05\x12\x14\n\x0c\x66\x61irness_key\x18\x02 \x01(\t\x12\x17\n\x0f\x66\x61irness_weight\x18\x03 \x01(\x02";\n\x0eWorkerSelector\x12\x1d\n\x13worker_instance_key\x18\x01 \x01(\tH\x00\x42\n\n\x08selectorB\x89\x01\n\x19io.temporal.api.common.v1B\x0cMessageProtoP\x01Z#go.temporal.io/api/common/v1;common\xaa\x02\x18Temporalio.Api.Common.V1\xea\x02\x1bTemporalio::Api::Common::V1b\x06proto3' + b'\n$temporal/api/common/v1/message.proto\x12\x16temporal.api.common.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a"temporal/api/enums/v1/common.proto\x1a&temporal/api/enums/v1/event_type.proto\x1a!temporal/api/enums/v1/reset.proto"T\n\x08\x44\x61taBlob\x12:\n\rencoding_type\x18\x01 \x01(\x0e\x32#.temporal.api.enums.v1.EncodingType\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c"=\n\x08Payloads\x12\x31\n\x08payloads\x18\x01 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload"\x8a\x02\n\x07Payload\x12?\n\x08metadata\x18\x01 \x03(\x0b\x32-.temporal.api.common.v1.Payload.MetadataEntry\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12Q\n\x11\x65xternal_payloads\x18\x03 \x03(\x0b\x32\x36.temporal.api.common.v1.Payload.ExternalPayloadDetails\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x0c:\x02\x38\x01\x1a,\n\x16\x45xternalPayloadDetails\x12\x12\n\nsize_bytes\x18\x01 \x01(\x03"\xbe\x01\n\x10SearchAttributes\x12S\n\x0eindexed_fields\x18\x01 \x03(\x0b\x32;.temporal.api.common.v1.SearchAttributes.IndexedFieldsEntry\x1aU\n\x12IndexedFieldsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\x90\x01\n\x04Memo\x12\x38\n\x06\x66ields\x18\x01 \x03(\x0b\x32(.temporal.api.common.v1.Memo.FieldsEntry\x1aN\n\x0b\x46ieldsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\x94\x01\n\x06Header\x12:\n\x06\x66ields\x18\x01 \x03(\x0b\x32*.temporal.api.common.v1.Header.FieldsEntry\x1aN\n\x0b\x46ieldsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"8\n\x11WorkflowExecution\x12\x13\n\x0bworkflow_id\x18\x01 \x01(\t\x12\x0e\n\x06run_id\x18\x02 \x01(\t"d\n\tExecution\x12\x32\n\x04type\x18\x01 \x01(\x0e\x32$.temporal.api.enums.v1.ExecutionType\x12\x13\n\x0b\x62usiness_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\t"\x1c\n\x0cWorkflowType\x12\x0c\n\x04name\x18\x01 \x01(\t"\x1c\n\x0c\x41\x63tivityType\x12\x0c\n\x04name\x18\x01 \x01(\t"\xd1\x01\n\x0bRetryPolicy\x12\x33\n\x10initial_interval\x18\x01 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x1b\n\x13\x62\x61\x63koff_coefficient\x18\x02 \x01(\x01\x12\x33\n\x10maximum_interval\x18\x03 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x18\n\x10maximum_attempts\x18\x04 \x01(\x05\x12!\n\x19non_retryable_error_types\x18\x05 \x03(\t"F\n\x10MeteringMetadata\x12\x32\n*nonfirst_local_activity_execution_attempts\x18\r \x01(\r">\n\x12WorkerVersionStamp\x12\x10\n\x08\x62uild_id\x18\x01 \x01(\t\x12\x16\n\x0euse_versioning\x18\x03 \x01(\x08"e\n\x19WorkerVersionCapabilities\x12\x10\n\x08\x62uild_id\x18\x01 \x01(\t\x12\x16\n\x0euse_versioning\x18\x02 \x01(\x08\x12\x1e\n\x16\x64\x65ployment_series_name\x18\x04 \x01(\t"\xed\x02\n\x0cResetOptions\x12\x35\n\x13\x66irst_workflow_task\x18\x01 \x01(\x0b\x32\x16.google.protobuf.EmptyH\x00\x12\x34\n\x12last_workflow_task\x18\x02 \x01(\x0b\x32\x16.google.protobuf.EmptyH\x00\x12\x1a\n\x10workflow_task_id\x18\x03 \x01(\x03H\x00\x12\x12\n\x08\x62uild_id\x18\x04 \x01(\tH\x00\x12G\n\x12reset_reapply_type\x18\n \x01(\x0e\x32\'.temporal.api.enums.v1.ResetReapplyTypeB\x02\x18\x01\x12\x18\n\x10\x63urrent_run_only\x18\x0b \x01(\x08\x12S\n\x1breset_reapply_exclude_types\x18\x0c \x03(\x0e\x32..temporal.api.enums.v1.ResetReapplyExcludeTypeB\x08\n\x06target"\xe4\x02\n\x08\x43\x61llback\x12\x37\n\x05nexus\x18\x02 \x01(\x0b\x32&.temporal.api.common.v1.Callback.NexusH\x00\x12=\n\x08internal\x18\x03 \x01(\x0b\x32).temporal.api.common.v1.Callback.InternalH\x00\x12+\n\x05links\x18\x64 \x03(\x0b\x32\x1c.temporal.api.common.v1.Link\x1a\x87\x01\n\x05Nexus\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\x42\n\x06header\x18\x02 \x03(\x0b\x32\x32.temporal.api.common.v1.Callback.Nexus.HeaderEntry\x1a-\n\x0bHeaderEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a\x18\n\x08Internal\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x42\t\n\x07variantJ\x04\x08\x01\x10\x02"\x8a\x08\n\x04Link\x12\x44\n\x0eworkflow_event\x18\x01 \x01(\x0b\x32*.temporal.api.common.v1.Link.WorkflowEventH\x00\x12:\n\tbatch_job\x18\x02 \x01(\x0b\x32%.temporal.api.common.v1.Link.BatchJobH\x00\x12\x39\n\x08\x61\x63tivity\x18\x03 \x01(\x0b\x32%.temporal.api.common.v1.Link.ActivityH\x00\x12\x46\n\x0fnexus_operation\x18\x04 \x01(\x0b\x32+.temporal.api.common.v1.Link.NexusOperationH\x00\x12\x39\n\x08workflow\x18\x05 \x01(\x0b\x32%.temporal.api.common.v1.Link.WorkflowH\x00\x1a\xb7\x03\n\rWorkflowEvent\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\t\x12N\n\tevent_ref\x18\x64 \x01(\x0b\x32\x39.temporal.api.common.v1.Link.WorkflowEvent.EventReferenceH\x00\x12W\n\x0erequest_id_ref\x18\x65 \x01(\x0b\x32=.temporal.api.common.v1.Link.WorkflowEvent.RequestIdReferenceH\x00\x1aX\n\x0e\x45ventReference\x12\x10\n\x08\x65vent_id\x18\x01 \x01(\x03\x12\x34\n\nevent_type\x18\x02 \x01(\x0e\x32 .temporal.api.enums.v1.EventType\x1a^\n\x12RequestIdReference\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12\x34\n\nevent_type\x18\x02 \x01(\x0e\x32 .temporal.api.enums.v1.EventTypeB\x0b\n\treference\x1a\x1a\n\x08\x42\x61tchJob\x12\x0e\n\x06job_id\x18\x01 \x01(\t\x1a\x42\n\x08\x41\x63tivity\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0b\x61\x63tivity_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\t\x1aI\n\x0eNexusOperation\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x14\n\x0coperation_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\t\x1aR\n\x08Workflow\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\t\x12\x0e\n\x06reason\x18\x04 \x01(\tB\t\n\x07variant"\'\n\tPrincipal\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t"O\n\x08Priority\x12\x14\n\x0cpriority_key\x18\x01 \x01(\x05\x12\x14\n\x0c\x66\x61irness_key\x18\x02 \x01(\t\x12\x17\n\x0f\x66\x61irness_weight\x18\x03 \x01(\x02";\n\x0eWorkerSelector\x12\x1d\n\x13worker_instance_key\x18\x01 \x01(\tH\x00\x42\n\n\x08selector"i\n\x11OnConflictOptions\x12\x19\n\x11\x61ttach_request_id\x18\x01 \x01(\x08\x12#\n\x1b\x61ttach_completion_callbacks\x18\x02 \x01(\x08\x12\x14\n\x0c\x61ttach_links\x18\x03 \x01(\x08"\xaa\x01\n\x12TimeSkippingConfig\x12\x0f\n\x07\x65nabled\x18\x01 \x01(\x08\x12\x46\n\x13\x66\x61st_forward_config\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.FastForwardConfig\x12\x1b\n\x13\x64isable_propagation\x18\x03 \x01(\x08\x12\x1e\n\x16max_session_skip_count\x18\x04 \x01(\x05"L\n\x11\x46\x61stForwardConfig\x12\n\n\x02id\x18\x01 \x01(\t\x12+\n\x08\x64uration\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration"\xb5\x01\n\x1cTimeSkippingStatePropagation\x12;\n\x18initial_skipped_duration\x18\x01 \x01(\x0b\x32\x19.google.protobuf.Duration\x12<\n\x18\x66\x61st_forward_target_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x1a\n\x12initial_skip_count\x18\x03 \x01(\x05"\xfe\x01\n\x10TimeSkippingInfo\x12\x30\n\x0c\x63urrent_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x44\n\x10\x65\x66\x66\x65\x63tive_config\x18\x02 \x01(\x0b\x32*.temporal.api.common.v1.TimeSkippingConfig\x12N\n\x11\x66\x61st_forward_info\x18\x04 \x01(\x0b\x32\x33.temporal.api.common.v1.TimeSkippingFastForwardInfo\x12"\n\x1a\x63urrent_session_skip_count\x18\x06 \x01(\x05"\xb8\x01\n\x1bTimeSkippingFastForwardInfo\x12\x38\n\x15\x66\x61st_forward_duration\x18\x01 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x17\n\x0f\x66\x61st_forward_id\x18\x02 \x01(\t\x12/\n\x0btarget_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x15\n\rhas_completed\x18\x04 \x01(\x08\x42\x89\x01\n\x19io.temporal.api.common.v1B\x0cMessageProtoP\x01Z#go.temporal.io/api/common/v1;common\xaa\x02\x18Temporalio.Api.Common.V1\xea\x02\x1bTemporalio::Api::Common::V1b\x06proto3' ) @@ -36,6 +37,9 @@ _PAYLOADS = DESCRIPTOR.message_types_by_name["Payloads"] _PAYLOAD = DESCRIPTOR.message_types_by_name["Payload"] _PAYLOAD_METADATAENTRY = _PAYLOAD.nested_types_by_name["MetadataEntry"] +_PAYLOAD_EXTERNALPAYLOADDETAILS = _PAYLOAD.nested_types_by_name[ + "ExternalPayloadDetails" +] _SEARCHATTRIBUTES = DESCRIPTOR.message_types_by_name["SearchAttributes"] _SEARCHATTRIBUTES_INDEXEDFIELDSENTRY = _SEARCHATTRIBUTES.nested_types_by_name[ "IndexedFieldsEntry" @@ -45,6 +49,7 @@ _HEADER = DESCRIPTOR.message_types_by_name["Header"] _HEADER_FIELDSENTRY = _HEADER.nested_types_by_name["FieldsEntry"] _WORKFLOWEXECUTION = DESCRIPTOR.message_types_by_name["WorkflowExecution"] +_EXECUTION = DESCRIPTOR.message_types_by_name["Execution"] _WORKFLOWTYPE = DESCRIPTOR.message_types_by_name["WorkflowType"] _ACTIVITYTYPE = DESCRIPTOR.message_types_by_name["ActivityType"] _RETRYPOLICY = DESCRIPTOR.message_types_by_name["RetryPolicy"] @@ -67,14 +72,28 @@ "RequestIdReference" ] _LINK_BATCHJOB = _LINK.nested_types_by_name["BatchJob"] +_LINK_ACTIVITY = _LINK.nested_types_by_name["Activity"] +_LINK_NEXUSOPERATION = _LINK.nested_types_by_name["NexusOperation"] +_LINK_WORKFLOW = _LINK.nested_types_by_name["Workflow"] +_PRINCIPAL = DESCRIPTOR.message_types_by_name["Principal"] _PRIORITY = DESCRIPTOR.message_types_by_name["Priority"] _WORKERSELECTOR = DESCRIPTOR.message_types_by_name["WorkerSelector"] +_ONCONFLICTOPTIONS = DESCRIPTOR.message_types_by_name["OnConflictOptions"] +_TIMESKIPPINGCONFIG = DESCRIPTOR.message_types_by_name["TimeSkippingConfig"] +_FASTFORWARDCONFIG = DESCRIPTOR.message_types_by_name["FastForwardConfig"] +_TIMESKIPPINGSTATEPROPAGATION = DESCRIPTOR.message_types_by_name[ + "TimeSkippingStatePropagation" +] +_TIMESKIPPINGINFO = DESCRIPTOR.message_types_by_name["TimeSkippingInfo"] +_TIMESKIPPINGFASTFORWARDINFO = DESCRIPTOR.message_types_by_name[ + "TimeSkippingFastForwardInfo" +] DataBlob = _reflection.GeneratedProtocolMessageType( "DataBlob", (_message.Message,), { "DESCRIPTOR": _DATABLOB, - "__module__": "temporal.api.common.v1.message_pb2", + "__module__": "temporalio.api.common.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.common.v1.DataBlob) }, ) @@ -85,7 +104,7 @@ (_message.Message,), { "DESCRIPTOR": _PAYLOADS, - "__module__": "temporal.api.common.v1.message_pb2", + "__module__": "temporalio.api.common.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.common.v1.Payloads) }, ) @@ -100,17 +119,27 @@ (_message.Message,), { "DESCRIPTOR": _PAYLOAD_METADATAENTRY, - "__module__": "temporal.api.common.v1.message_pb2", + "__module__": "temporalio.api.common.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.common.v1.Payload.MetadataEntry) }, ), + "ExternalPayloadDetails": _reflection.GeneratedProtocolMessageType( + "ExternalPayloadDetails", + (_message.Message,), + { + "DESCRIPTOR": _PAYLOAD_EXTERNALPAYLOADDETAILS, + "__module__": "temporalio.api.common.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.common.v1.Payload.ExternalPayloadDetails) + }, + ), "DESCRIPTOR": _PAYLOAD, - "__module__": "temporal.api.common.v1.message_pb2", + "__module__": "temporalio.api.common.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.common.v1.Payload) }, ) _sym_db.RegisterMessage(Payload) _sym_db.RegisterMessage(Payload.MetadataEntry) +_sym_db.RegisterMessage(Payload.ExternalPayloadDetails) SearchAttributes = _reflection.GeneratedProtocolMessageType( "SearchAttributes", @@ -121,12 +150,12 @@ (_message.Message,), { "DESCRIPTOR": _SEARCHATTRIBUTES_INDEXEDFIELDSENTRY, - "__module__": "temporal.api.common.v1.message_pb2", + "__module__": "temporalio.api.common.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.common.v1.SearchAttributes.IndexedFieldsEntry) }, ), "DESCRIPTOR": _SEARCHATTRIBUTES, - "__module__": "temporal.api.common.v1.message_pb2", + "__module__": "temporalio.api.common.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.common.v1.SearchAttributes) }, ) @@ -142,12 +171,12 @@ (_message.Message,), { "DESCRIPTOR": _MEMO_FIELDSENTRY, - "__module__": "temporal.api.common.v1.message_pb2", + "__module__": "temporalio.api.common.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.common.v1.Memo.FieldsEntry) }, ), "DESCRIPTOR": _MEMO, - "__module__": "temporal.api.common.v1.message_pb2", + "__module__": "temporalio.api.common.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.common.v1.Memo) }, ) @@ -163,12 +192,12 @@ (_message.Message,), { "DESCRIPTOR": _HEADER_FIELDSENTRY, - "__module__": "temporal.api.common.v1.message_pb2", + "__module__": "temporalio.api.common.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.common.v1.Header.FieldsEntry) }, ), "DESCRIPTOR": _HEADER, - "__module__": "temporal.api.common.v1.message_pb2", + "__module__": "temporalio.api.common.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.common.v1.Header) }, ) @@ -180,18 +209,29 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWEXECUTION, - "__module__": "temporal.api.common.v1.message_pb2", + "__module__": "temporalio.api.common.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.common.v1.WorkflowExecution) }, ) _sym_db.RegisterMessage(WorkflowExecution) +Execution = _reflection.GeneratedProtocolMessageType( + "Execution", + (_message.Message,), + { + "DESCRIPTOR": _EXECUTION, + "__module__": "temporalio.api.common.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.common.v1.Execution) + }, +) +_sym_db.RegisterMessage(Execution) + WorkflowType = _reflection.GeneratedProtocolMessageType( "WorkflowType", (_message.Message,), { "DESCRIPTOR": _WORKFLOWTYPE, - "__module__": "temporal.api.common.v1.message_pb2", + "__module__": "temporalio.api.common.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.common.v1.WorkflowType) }, ) @@ -202,7 +242,7 @@ (_message.Message,), { "DESCRIPTOR": _ACTIVITYTYPE, - "__module__": "temporal.api.common.v1.message_pb2", + "__module__": "temporalio.api.common.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.common.v1.ActivityType) }, ) @@ -213,7 +253,7 @@ (_message.Message,), { "DESCRIPTOR": _RETRYPOLICY, - "__module__": "temporal.api.common.v1.message_pb2", + "__module__": "temporalio.api.common.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.common.v1.RetryPolicy) }, ) @@ -224,7 +264,7 @@ (_message.Message,), { "DESCRIPTOR": _METERINGMETADATA, - "__module__": "temporal.api.common.v1.message_pb2", + "__module__": "temporalio.api.common.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.common.v1.MeteringMetadata) }, ) @@ -235,7 +275,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKERVERSIONSTAMP, - "__module__": "temporal.api.common.v1.message_pb2", + "__module__": "temporalio.api.common.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.common.v1.WorkerVersionStamp) }, ) @@ -246,7 +286,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKERVERSIONCAPABILITIES, - "__module__": "temporal.api.common.v1.message_pb2", + "__module__": "temporalio.api.common.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.common.v1.WorkerVersionCapabilities) }, ) @@ -257,7 +297,7 @@ (_message.Message,), { "DESCRIPTOR": _RESETOPTIONS, - "__module__": "temporal.api.common.v1.message_pb2", + "__module__": "temporalio.api.common.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.common.v1.ResetOptions) }, ) @@ -276,12 +316,12 @@ (_message.Message,), { "DESCRIPTOR": _CALLBACK_NEXUS_HEADERENTRY, - "__module__": "temporal.api.common.v1.message_pb2", + "__module__": "temporalio.api.common.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.common.v1.Callback.Nexus.HeaderEntry) }, ), "DESCRIPTOR": _CALLBACK_NEXUS, - "__module__": "temporal.api.common.v1.message_pb2", + "__module__": "temporalio.api.common.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.common.v1.Callback.Nexus) }, ), @@ -290,12 +330,12 @@ (_message.Message,), { "DESCRIPTOR": _CALLBACK_INTERNAL, - "__module__": "temporal.api.common.v1.message_pb2", + "__module__": "temporalio.api.common.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.common.v1.Callback.Internal) }, ), "DESCRIPTOR": _CALLBACK, - "__module__": "temporal.api.common.v1.message_pb2", + "__module__": "temporalio.api.common.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.common.v1.Callback) }, ) @@ -317,7 +357,7 @@ (_message.Message,), { "DESCRIPTOR": _LINK_WORKFLOWEVENT_EVENTREFERENCE, - "__module__": "temporal.api.common.v1.message_pb2", + "__module__": "temporalio.api.common.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.common.v1.Link.WorkflowEvent.EventReference) }, ), @@ -326,12 +366,12 @@ (_message.Message,), { "DESCRIPTOR": _LINK_WORKFLOWEVENT_REQUESTIDREFERENCE, - "__module__": "temporal.api.common.v1.message_pb2", + "__module__": "temporalio.api.common.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.common.v1.Link.WorkflowEvent.RequestIdReference) }, ), "DESCRIPTOR": _LINK_WORKFLOWEVENT, - "__module__": "temporal.api.common.v1.message_pb2", + "__module__": "temporalio.api.common.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.common.v1.Link.WorkflowEvent) }, ), @@ -340,12 +380,39 @@ (_message.Message,), { "DESCRIPTOR": _LINK_BATCHJOB, - "__module__": "temporal.api.common.v1.message_pb2", + "__module__": "temporalio.api.common.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.common.v1.Link.BatchJob) }, ), + "Activity": _reflection.GeneratedProtocolMessageType( + "Activity", + (_message.Message,), + { + "DESCRIPTOR": _LINK_ACTIVITY, + "__module__": "temporalio.api.common.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.common.v1.Link.Activity) + }, + ), + "NexusOperation": _reflection.GeneratedProtocolMessageType( + "NexusOperation", + (_message.Message,), + { + "DESCRIPTOR": _LINK_NEXUSOPERATION, + "__module__": "temporalio.api.common.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.common.v1.Link.NexusOperation) + }, + ), + "Workflow": _reflection.GeneratedProtocolMessageType( + "Workflow", + (_message.Message,), + { + "DESCRIPTOR": _LINK_WORKFLOW, + "__module__": "temporalio.api.common.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.common.v1.Link.Workflow) + }, + ), "DESCRIPTOR": _LINK, - "__module__": "temporal.api.common.v1.message_pb2", + "__module__": "temporalio.api.common.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.common.v1.Link) }, ) @@ -354,13 +421,27 @@ _sym_db.RegisterMessage(Link.WorkflowEvent.EventReference) _sym_db.RegisterMessage(Link.WorkflowEvent.RequestIdReference) _sym_db.RegisterMessage(Link.BatchJob) +_sym_db.RegisterMessage(Link.Activity) +_sym_db.RegisterMessage(Link.NexusOperation) +_sym_db.RegisterMessage(Link.Workflow) + +Principal = _reflection.GeneratedProtocolMessageType( + "Principal", + (_message.Message,), + { + "DESCRIPTOR": _PRINCIPAL, + "__module__": "temporalio.api.common.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.common.v1.Principal) + }, +) +_sym_db.RegisterMessage(Principal) Priority = _reflection.GeneratedProtocolMessageType( "Priority", (_message.Message,), { "DESCRIPTOR": _PRIORITY, - "__module__": "temporal.api.common.v1.message_pb2", + "__module__": "temporalio.api.common.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.common.v1.Priority) }, ) @@ -371,12 +452,78 @@ (_message.Message,), { "DESCRIPTOR": _WORKERSELECTOR, - "__module__": "temporal.api.common.v1.message_pb2", + "__module__": "temporalio.api.common.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.common.v1.WorkerSelector) }, ) _sym_db.RegisterMessage(WorkerSelector) +OnConflictOptions = _reflection.GeneratedProtocolMessageType( + "OnConflictOptions", + (_message.Message,), + { + "DESCRIPTOR": _ONCONFLICTOPTIONS, + "__module__": "temporalio.api.common.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.common.v1.OnConflictOptions) + }, +) +_sym_db.RegisterMessage(OnConflictOptions) + +TimeSkippingConfig = _reflection.GeneratedProtocolMessageType( + "TimeSkippingConfig", + (_message.Message,), + { + "DESCRIPTOR": _TIMESKIPPINGCONFIG, + "__module__": "temporalio.api.common.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.common.v1.TimeSkippingConfig) + }, +) +_sym_db.RegisterMessage(TimeSkippingConfig) + +FastForwardConfig = _reflection.GeneratedProtocolMessageType( + "FastForwardConfig", + (_message.Message,), + { + "DESCRIPTOR": _FASTFORWARDCONFIG, + "__module__": "temporalio.api.common.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.common.v1.FastForwardConfig) + }, +) +_sym_db.RegisterMessage(FastForwardConfig) + +TimeSkippingStatePropagation = _reflection.GeneratedProtocolMessageType( + "TimeSkippingStatePropagation", + (_message.Message,), + { + "DESCRIPTOR": _TIMESKIPPINGSTATEPROPAGATION, + "__module__": "temporalio.api.common.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.common.v1.TimeSkippingStatePropagation) + }, +) +_sym_db.RegisterMessage(TimeSkippingStatePropagation) + +TimeSkippingInfo = _reflection.GeneratedProtocolMessageType( + "TimeSkippingInfo", + (_message.Message,), + { + "DESCRIPTOR": _TIMESKIPPINGINFO, + "__module__": "temporalio.api.common.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.common.v1.TimeSkippingInfo) + }, +) +_sym_db.RegisterMessage(TimeSkippingInfo) + +TimeSkippingFastForwardInfo = _reflection.GeneratedProtocolMessageType( + "TimeSkippingFastForwardInfo", + (_message.Message,), + { + "DESCRIPTOR": _TIMESKIPPINGFASTFORWARDINFO, + "__module__": "temporalio.api.common.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.common.v1.TimeSkippingFastForwardInfo) + }, +) +_sym_db.RegisterMessage(TimeSkippingFastForwardInfo) + if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b"\n\031io.temporal.api.common.v1B\014MessageProtoP\001Z#go.temporal.io/api/common/v1;common\252\002\030Temporalio.Api.Common.V1\352\002\033Temporalio::Api::Common::V1" @@ -392,62 +539,86 @@ _RESETOPTIONS.fields_by_name["reset_reapply_type"]._serialized_options = b"\030\001" _CALLBACK_NEXUS_HEADERENTRY._options = None _CALLBACK_NEXUS_HEADERENTRY._serialized_options = b"8\001" - _DATABLOB._serialized_start = 236 - _DATABLOB._serialized_end = 320 - _PAYLOADS._serialized_start = 322 - _PAYLOADS._serialized_end = 383 - _PAYLOAD._serialized_start = 386 - _PAYLOAD._serialized_end = 523 - _PAYLOAD_METADATAENTRY._serialized_start = 476 - _PAYLOAD_METADATAENTRY._serialized_end = 523 - _SEARCHATTRIBUTES._serialized_start = 526 - _SEARCHATTRIBUTES._serialized_end = 716 - _SEARCHATTRIBUTES_INDEXEDFIELDSENTRY._serialized_start = 631 - _SEARCHATTRIBUTES_INDEXEDFIELDSENTRY._serialized_end = 716 - _MEMO._serialized_start = 719 - _MEMO._serialized_end = 863 - _MEMO_FIELDSENTRY._serialized_start = 785 - _MEMO_FIELDSENTRY._serialized_end = 863 - _HEADER._serialized_start = 866 - _HEADER._serialized_end = 1014 - _HEADER_FIELDSENTRY._serialized_start = 785 - _HEADER_FIELDSENTRY._serialized_end = 863 - _WORKFLOWEXECUTION._serialized_start = 1016 - _WORKFLOWEXECUTION._serialized_end = 1072 - _WORKFLOWTYPE._serialized_start = 1074 - _WORKFLOWTYPE._serialized_end = 1102 - _ACTIVITYTYPE._serialized_start = 1104 - _ACTIVITYTYPE._serialized_end = 1132 - _RETRYPOLICY._serialized_start = 1135 - _RETRYPOLICY._serialized_end = 1344 - _METERINGMETADATA._serialized_start = 1346 - _METERINGMETADATA._serialized_end = 1416 - _WORKERVERSIONSTAMP._serialized_start = 1418 - _WORKERVERSIONSTAMP._serialized_end = 1480 - _WORKERVERSIONCAPABILITIES._serialized_start = 1482 - _WORKERVERSIONCAPABILITIES._serialized_end = 1583 - _RESETOPTIONS._serialized_start = 1586 - _RESETOPTIONS._serialized_end = 1951 - _CALLBACK._serialized_start = 1954 - _CALLBACK._serialized_end = 2310 - _CALLBACK_NEXUS._serialized_start = 2132 - _CALLBACK_NEXUS._serialized_end = 2267 - _CALLBACK_NEXUS_HEADERENTRY._serialized_start = 2222 - _CALLBACK_NEXUS_HEADERENTRY._serialized_end = 2267 - _CALLBACK_INTERNAL._serialized_start = 2269 - _CALLBACK_INTERNAL._serialized_end = 2293 - _LINK._serialized_start = 2313 - _LINK._serialized_end = 2930 - _LINK_WORKFLOWEVENT._serialized_start = 2452 - _LINK_WORKFLOWEVENT._serialized_end = 2891 - _LINK_WORKFLOWEVENT_EVENTREFERENCE._serialized_start = 2694 - _LINK_WORKFLOWEVENT_EVENTREFERENCE._serialized_end = 2782 - _LINK_WORKFLOWEVENT_REQUESTIDREFERENCE._serialized_start = 2784 - _LINK_WORKFLOWEVENT_REQUESTIDREFERENCE._serialized_end = 2878 - _LINK_BATCHJOB._serialized_start = 2893 - _LINK_BATCHJOB._serialized_end = 2919 - _PRIORITY._serialized_start = 2932 - _PRIORITY._serialized_end = 3011 - _WORKERSELECTOR._serialized_start = 3013 - _WORKERSELECTOR._serialized_end = 3072 + _DATABLOB._serialized_start = 269 + _DATABLOB._serialized_end = 353 + _PAYLOADS._serialized_start = 355 + _PAYLOADS._serialized_end = 416 + _PAYLOAD._serialized_start = 419 + _PAYLOAD._serialized_end = 685 + _PAYLOAD_METADATAENTRY._serialized_start = 592 + _PAYLOAD_METADATAENTRY._serialized_end = 639 + _PAYLOAD_EXTERNALPAYLOADDETAILS._serialized_start = 641 + _PAYLOAD_EXTERNALPAYLOADDETAILS._serialized_end = 685 + _SEARCHATTRIBUTES._serialized_start = 688 + _SEARCHATTRIBUTES._serialized_end = 878 + _SEARCHATTRIBUTES_INDEXEDFIELDSENTRY._serialized_start = 793 + _SEARCHATTRIBUTES_INDEXEDFIELDSENTRY._serialized_end = 878 + _MEMO._serialized_start = 881 + _MEMO._serialized_end = 1025 + _MEMO_FIELDSENTRY._serialized_start = 947 + _MEMO_FIELDSENTRY._serialized_end = 1025 + _HEADER._serialized_start = 1028 + _HEADER._serialized_end = 1176 + _HEADER_FIELDSENTRY._serialized_start = 947 + _HEADER_FIELDSENTRY._serialized_end = 1025 + _WORKFLOWEXECUTION._serialized_start = 1178 + _WORKFLOWEXECUTION._serialized_end = 1234 + _EXECUTION._serialized_start = 1236 + _EXECUTION._serialized_end = 1336 + _WORKFLOWTYPE._serialized_start = 1338 + _WORKFLOWTYPE._serialized_end = 1366 + _ACTIVITYTYPE._serialized_start = 1368 + _ACTIVITYTYPE._serialized_end = 1396 + _RETRYPOLICY._serialized_start = 1399 + _RETRYPOLICY._serialized_end = 1608 + _METERINGMETADATA._serialized_start = 1610 + _METERINGMETADATA._serialized_end = 1680 + _WORKERVERSIONSTAMP._serialized_start = 1682 + _WORKERVERSIONSTAMP._serialized_end = 1744 + _WORKERVERSIONCAPABILITIES._serialized_start = 1746 + _WORKERVERSIONCAPABILITIES._serialized_end = 1847 + _RESETOPTIONS._serialized_start = 1850 + _RESETOPTIONS._serialized_end = 2215 + _CALLBACK._serialized_start = 2218 + _CALLBACK._serialized_end = 2574 + _CALLBACK_NEXUS._serialized_start = 2396 + _CALLBACK_NEXUS._serialized_end = 2531 + _CALLBACK_NEXUS_HEADERENTRY._serialized_start = 2486 + _CALLBACK_NEXUS_HEADERENTRY._serialized_end = 2531 + _CALLBACK_INTERNAL._serialized_start = 2533 + _CALLBACK_INTERNAL._serialized_end = 2557 + _LINK._serialized_start = 2577 + _LINK._serialized_end = 3611 + _LINK_WORKFLOWEVENT._serialized_start = 2906 + _LINK_WORKFLOWEVENT._serialized_end = 3345 + _LINK_WORKFLOWEVENT_EVENTREFERENCE._serialized_start = 3148 + _LINK_WORKFLOWEVENT_EVENTREFERENCE._serialized_end = 3236 + _LINK_WORKFLOWEVENT_REQUESTIDREFERENCE._serialized_start = 3238 + _LINK_WORKFLOWEVENT_REQUESTIDREFERENCE._serialized_end = 3332 + _LINK_BATCHJOB._serialized_start = 3347 + _LINK_BATCHJOB._serialized_end = 3373 + _LINK_ACTIVITY._serialized_start = 3375 + _LINK_ACTIVITY._serialized_end = 3441 + _LINK_NEXUSOPERATION._serialized_start = 3443 + _LINK_NEXUSOPERATION._serialized_end = 3516 + _LINK_WORKFLOW._serialized_start = 3518 + _LINK_WORKFLOW._serialized_end = 3600 + _PRINCIPAL._serialized_start = 3613 + _PRINCIPAL._serialized_end = 3652 + _PRIORITY._serialized_start = 3654 + _PRIORITY._serialized_end = 3733 + _WORKERSELECTOR._serialized_start = 3735 + _WORKERSELECTOR._serialized_end = 3794 + _ONCONFLICTOPTIONS._serialized_start = 3796 + _ONCONFLICTOPTIONS._serialized_end = 3901 + _TIMESKIPPINGCONFIG._serialized_start = 3904 + _TIMESKIPPINGCONFIG._serialized_end = 4074 + _FASTFORWARDCONFIG._serialized_start = 4076 + _FASTFORWARDCONFIG._serialized_end = 4152 + _TIMESKIPPINGSTATEPROPAGATION._serialized_start = 4155 + _TIMESKIPPINGSTATEPROPAGATION._serialized_end = 4336 + _TIMESKIPPINGINFO._serialized_start = 4339 + _TIMESKIPPINGINFO._serialized_end = 4593 + _TIMESKIPPINGFASTFORWARDINFO._serialized_start = 4596 + _TIMESKIPPINGFASTFORWARDINFO._serialized_end = 4780 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/common/v1/message_pb2.pyi b/temporalio/api/common/v1/message_pb2.pyi index f94baa802..f7ed8d0fd 100644 --- a/temporalio/api/common/v1/message_pb2.pyi +++ b/temporalio/api/common/v1/message_pb2.pyi @@ -12,6 +12,7 @@ import google.protobuf.duration_pb2 import google.protobuf.empty_pb2 import google.protobuf.internal.containers import google.protobuf.message +import google.protobuf.timestamp_pb2 import temporalio.api.enums.v1.common_pb2 import temporalio.api.enums.v1.event_type_pb2 @@ -95,8 +96,26 @@ class Payload(google.protobuf.message.Message): field_name: typing_extensions.Literal["key", b"key", "value", b"value"], ) -> None: ... + class ExternalPayloadDetails(google.protobuf.message.Message): + """Describes an externally stored object referenced by this payload.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SIZE_BYTES_FIELD_NUMBER: builtins.int + size_bytes: builtins.int + """Size in bytes of the externally stored payload""" + def __init__( + self, + *, + size_bytes: builtins.int = ..., + ) -> None: ... + def ClearField( + self, field_name: typing_extensions.Literal["size_bytes", b"size_bytes"] + ) -> None: ... + METADATA_FIELD_NUMBER: builtins.int DATA_FIELD_NUMBER: builtins.int + EXTERNAL_PAYLOADS_FIELD_NUMBER: builtins.int @property def metadata( self, @@ -104,15 +123,33 @@ class Payload(google.protobuf.message.Message): builtins.str, builtins.bytes ]: ... data: builtins.bytes + @property + def external_payloads( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + global___Payload.ExternalPayloadDetails + ]: + """Details about externally stored payloads associated with this payload.""" def __init__( self, *, metadata: collections.abc.Mapping[builtins.str, builtins.bytes] | None = ..., data: builtins.bytes = ..., + external_payloads: collections.abc.Iterable[ + global___Payload.ExternalPayloadDetails + ] + | None = ..., ) -> None: ... def ClearField( self, - field_name: typing_extensions.Literal["data", b"data", "metadata", b"metadata"], + field_name: typing_extensions.Literal[ + "data", + b"data", + "external_payloads", + b"external_payloads", + "metadata", + b"metadata", + ], ) -> None: ... global___Payload = Payload @@ -284,6 +321,35 @@ class WorkflowExecution(google.protobuf.message.Message): global___WorkflowExecution = WorkflowExecution +class Execution(google.protobuf.message.Message): + """Identifies a specific execution within a namespace. This is used for standalone activities + executions in batch jobs currently. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TYPE_FIELD_NUMBER: builtins.int + BUSINESS_ID_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + type: temporalio.api.enums.v1.common_pb2.ExecutionType.ValueType + business_id: builtins.str + run_id: builtins.str + def __init__( + self, + *, + type: temporalio.api.enums.v1.common_pb2.ExecutionType.ValueType = ..., + business_id: builtins.str = ..., + run_id: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "business_id", b"business_id", "run_id", b"run_id", "type", b"type" + ], + ) -> None: ... + +global___Execution = Execution + class WorkflowType(google.protobuf.message.Message): """Represents the identifier used by a workflow author to define the workflow. Typically, the name of a function. This is sometimes referred to as the workflow's "name" @@ -847,25 +913,141 @@ class Link(google.protobuf.message.Message): self, field_name: typing_extensions.Literal["job_id", b"job_id"] ) -> None: ... + class Activity(google.protobuf.message.Message): + """A link to an activity.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + ACTIVITY_ID_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + namespace: builtins.str + activity_id: builtins.str + run_id: builtins.str + def __init__( + self, + *, + namespace: builtins.str = ..., + activity_id: builtins.str = ..., + run_id: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "activity_id", + b"activity_id", + "namespace", + b"namespace", + "run_id", + b"run_id", + ], + ) -> None: ... + + class NexusOperation(google.protobuf.message.Message): + """A link to a standalone Nexus operation.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + OPERATION_ID_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + namespace: builtins.str + operation_id: builtins.str + run_id: builtins.str + def __init__( + self, + *, + namespace: builtins.str = ..., + operation_id: builtins.str = ..., + run_id: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "namespace", + b"namespace", + "operation_id", + b"operation_id", + "run_id", + b"run_id", + ], + ) -> None: ... + + class Workflow(google.protobuf.message.Message): + """A link to a workflow execution. This is a more general version of WorkflowEvent that doesn't specify a + particular event within the workflow, useful when you want to link to a workflow but there is no particular event to link to, + such as a Query or a Rejected Update. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + WORKFLOW_ID_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + REASON_FIELD_NUMBER: builtins.int + namespace: builtins.str + workflow_id: builtins.str + run_id: builtins.str + reason: builtins.str + def __init__( + self, + *, + namespace: builtins.str = ..., + workflow_id: builtins.str = ..., + run_id: builtins.str = ..., + reason: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "namespace", + b"namespace", + "reason", + b"reason", + "run_id", + b"run_id", + "workflow_id", + b"workflow_id", + ], + ) -> None: ... + WORKFLOW_EVENT_FIELD_NUMBER: builtins.int BATCH_JOB_FIELD_NUMBER: builtins.int + ACTIVITY_FIELD_NUMBER: builtins.int + NEXUS_OPERATION_FIELD_NUMBER: builtins.int + WORKFLOW_FIELD_NUMBER: builtins.int @property def workflow_event(self) -> global___Link.WorkflowEvent: ... @property def batch_job(self) -> global___Link.BatchJob: ... + @property + def activity(self) -> global___Link.Activity: ... + @property + def nexus_operation(self) -> global___Link.NexusOperation: ... + @property + def workflow(self) -> global___Link.Workflow: ... def __init__( self, *, workflow_event: global___Link.WorkflowEvent | None = ..., batch_job: global___Link.BatchJob | None = ..., + activity: global___Link.Activity | None = ..., + nexus_operation: global___Link.NexusOperation | None = ..., + workflow: global___Link.Workflow | None = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal[ + "activity", + b"activity", "batch_job", b"batch_job", + "nexus_operation", + b"nexus_operation", "variant", b"variant", + "workflow", + b"workflow", "workflow_event", b"workflow_event", ], @@ -873,20 +1055,56 @@ class Link(google.protobuf.message.Message): def ClearField( self, field_name: typing_extensions.Literal[ + "activity", + b"activity", "batch_job", b"batch_job", + "nexus_operation", + b"nexus_operation", "variant", b"variant", + "workflow", + b"workflow", "workflow_event", b"workflow_event", ], ) -> None: ... def WhichOneof( self, oneof_group: typing_extensions.Literal["variant", b"variant"] - ) -> typing_extensions.Literal["workflow_event", "batch_job"] | None: ... + ) -> ( + typing_extensions.Literal[ + "workflow_event", "batch_job", "activity", "nexus_operation", "workflow" + ] + | None + ): ... global___Link = Link +class Principal(google.protobuf.message.Message): + """Principal is an authenticated caller identity computed by the server from trusted + authentication context. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TYPE_FIELD_NUMBER: builtins.int + NAME_FIELD_NUMBER: builtins.int + type: builtins.str + """Low-cardinality category of the principal (e.g., "jwt", "users").""" + name: builtins.str + """Identifier within that category (e.g., sub JWT claim, email address).""" + def __init__( + self, + *, + type: builtins.str = ..., + name: builtins.str = ..., + ) -> None: ... + def ClearField( + self, field_name: typing_extensions.Literal["name", b"name", "type", b"type"] + ) -> None: ... + +global___Principal = Principal + class Priority(google.protobuf.message.Message): """Priority contains metadata that controls relative ordering of task processing when tasks are backed up in a queue. Initially, Priority will be used in @@ -937,7 +1155,7 @@ class Priority(google.protobuf.message.Message): configuration, and defaults to 5. If priority is not present (or zero), then the effective priority will be - the default priority, which is is calculated by (min+max)/2. With the + the default priority, which is calculated by (min+max)/2. With the default max of 5, and min of 1, that comes out to 3. """ fairness_key: builtins.str @@ -1030,3 +1248,320 @@ class WorkerSelector(google.protobuf.message.Message): ) -> typing_extensions.Literal["worker_instance_key"] | None: ... global___WorkerSelector = WorkerSelector + +class OnConflictOptions(google.protobuf.message.Message): + """When starting an execution with a conflict policy that uses an existing execution and there is already an existing + running execution, OnConflictOptions defines actions to be taken on the existing running execution. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ATTACH_REQUEST_ID_FIELD_NUMBER: builtins.int + ATTACH_COMPLETION_CALLBACKS_FIELD_NUMBER: builtins.int + ATTACH_LINKS_FIELD_NUMBER: builtins.int + attach_request_id: builtins.bool + """Attaches the request ID to the running execution.""" + attach_completion_callbacks: builtins.bool + """Attaches the completion callbacks to the running execution.""" + attach_links: builtins.bool + """Attaches the links to the running execution.""" + def __init__( + self, + *, + attach_request_id: builtins.bool = ..., + attach_completion_callbacks: builtins.bool = ..., + attach_links: builtins.bool = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "attach_completion_callbacks", + b"attach_completion_callbacks", + "attach_links", + b"attach_links", + "attach_request_id", + b"attach_request_id", + ], + ) -> None: ... + +global___OnConflictOptions = OnConflictOptions + +class TimeSkippingConfig(google.protobuf.message.Message): + """The configuration for time skipping of an execution. + When time skipping is enabled, virtual time advances automatically whenever there is no in-flight work. + Options like fast_forward, disable_propagation, and max_session_skip_count are provided for granular + control of the execution's time skipping behavior. See each field's comment for a detailed explanation. + + An example of workflows with time skipping: + For workflows, an execution is a chain of runs including retries, cron, and continue-as-new. + In-flight work includes activities, child workflows, Nexus operations, signal/cancel external workflow operations, etc. + User timers are not classified as in-flight work and will be skipped over; the virtual clock may also skip to the + time point of the registered fast-forward when there is no in-flight work. + Whenever time is skipped, the skip count is incremented by one; max_session_skip_count bounds the number of skips allowed within a single time-skipping session. + For child workflows, by default, if the parent execution is skipping time, the child execution will also skip time, + but a parent's fast_forward won't affect its child's execution. A flag is provided to disable propagation of the + "enabled" flag to child workflows; regardless of that flag, a child workflow inherits the virtual time from the + parent execution as its start time. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ENABLED_FIELD_NUMBER: builtins.int + FAST_FORWARD_CONFIG_FIELD_NUMBER: builtins.int + DISABLE_PROPAGATION_FIELD_NUMBER: builtins.int + MAX_SESSION_SKIP_COUNT_FIELD_NUMBER: builtins.int + enabled: builtins.bool + """Enables or disables time skipping for this workflow execution.""" + @property + def fast_forward_config(self) -> global___FastForwardConfig: + """An optional opt-in to control time-skipping behavior through fast-forward; see its definition for details.""" + disable_propagation: builtins.bool + """By default, executions started by another execution (e.g. a child workflow of a parent workflow or + a schedule with the time-skipping policy enabled) inherit the "enabled" flag and skip time when possible. + This flag disables that inheritance. + """ + max_session_skip_count: builtins.int + """The maximum number of skips allowed every time this field is updated. It protects the execution from + situations like unlimited retries when backoff is skipped. + + Every time the execution skips time, the skip count is incremented by one, and when it reaches + max_session_skip_count, time skipping stops. Whenever this config field is updated, the accumulated + skip count is cleared, marking the start of a new session. + For an execution with a chain of runs (retry, cron, continue-as-new), the count is accumulated + across all runs within the same session. + + If this field is not set, the server applies a large default value (e.g. 100). The default can + be changed through dynamic config, and is overridden by this field when set. + """ + def __init__( + self, + *, + enabled: builtins.bool = ..., + fast_forward_config: global___FastForwardConfig | None = ..., + disable_propagation: builtins.bool = ..., + max_session_skip_count: builtins.int = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "fast_forward_config", b"fast_forward_config" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "disable_propagation", + b"disable_propagation", + "enabled", + b"enabled", + "fast_forward_config", + b"fast_forward_config", + "max_session_skip_count", + b"max_session_skip_count", + ], + ) -> None: ... + +global___TimeSkippingConfig = TimeSkippingConfig + +class FastForwardConfig(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ID_FIELD_NUMBER: builtins.int + DURATION_FIELD_NUMBER: builtins.int + id: builtins.str + """A client-supplied ID, required field, set alongside `duration`. It is used to poll for + fast-forward completion via PollWorkflowExecutionTimeSkipping. + The server performs no idempotency check on this ID; the client is responsible for managing it. + """ + @property + def duration(self) -> google.protobuf.duration_pb2.Duration: + """Fast-forward the current execution by this duration ahead of the current execution time; required field. + The duration yields a target time (current execution time + duration), surfaced as `target_time` in + TimeSkippingFastForwardInfo. Once virtual time reaches that target, the fast-forward completes, time + skipping is disabled, and no further time is skipped. Time skipping can be resumed either + by updating the TimeSkippingConfig with a new FastForwardConfig, or by clearing the FastForwardConfig + to skip through to the end of the execution. + + If this duration exceeds the remaining execution timeout, time will not pass beyond the end + of the execution, and the fast-forward won't have a chance to complete. + """ + def __init__( + self, + *, + id: builtins.str = ..., + duration: google.protobuf.duration_pb2.Duration | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["duration", b"duration"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["duration", b"duration", "id", b"id"], + ) -> None: ... + +global___FastForwardConfig = FastForwardConfig + +class TimeSkippingStatePropagation(google.protobuf.message.Message): + """The time-skipping state that needs to be propagated from one execution to another, or through a chain of runs + within the same execution. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + INITIAL_SKIPPED_DURATION_FIELD_NUMBER: builtins.int + FAST_FORWARD_TARGET_TIME_FIELD_NUMBER: builtins.int + INITIAL_SKIP_COUNT_FIELD_NUMBER: builtins.int + @property + def initial_skipped_duration(self) -> google.protobuf.duration_pb2.Duration: + """The time skipped by the previous run. It is propagated both to executions started by the + current execution and through a chain of runs (CaN, cron, retry). + """ + @property + def fast_forward_target_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """The fast-forward target time. It only propagates across a chain of runs within the same execution.""" + initial_skip_count: builtins.int + """The initial skip count. It only propagates across a chain of runs within the same execution.""" + def __init__( + self, + *, + initial_skipped_duration: google.protobuf.duration_pb2.Duration | None = ..., + fast_forward_target_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + initial_skip_count: builtins.int = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "fast_forward_target_time", + b"fast_forward_target_time", + "initial_skipped_duration", + b"initial_skipped_duration", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "fast_forward_target_time", + b"fast_forward_target_time", + "initial_skip_count", + b"initial_skip_count", + "initial_skipped_duration", + b"initial_skipped_duration", + ], + ) -> None: ... + +global___TimeSkippingStatePropagation = TimeSkippingStatePropagation + +class TimeSkippingInfo(google.protobuf.message.Message): + """Describes the current time-skipping state of a workflow execution.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CURRENT_TIME_FIELD_NUMBER: builtins.int + EFFECTIVE_CONFIG_FIELD_NUMBER: builtins.int + FAST_FORWARD_INFO_FIELD_NUMBER: builtins.int + CURRENT_SESSION_SKIP_COUNT_FIELD_NUMBER: builtins.int + @property + def current_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Current virtual time of the execution. If the execution hasn't skipped + any time yet, it will be the same as wall clock time. + """ + @property + def effective_config(self) -> global___TimeSkippingConfig: + """The current effective time-skipping config, which can differ from the config the user last set: + internally-defaulted fields are populated, and `enabled` reflects whether the execution is still + skipping time — e.g. it is set to false once `max_session_skip_count` is reached, the fast-forward + completes, or a client call disables time skipping. + """ + @property + def fast_forward_info(self) -> global___TimeSkippingFastForwardInfo: + """The execution's current fast-forward, if any. Unset if time skipping is enabled without a fast-forward.""" + current_session_skip_count: builtins.int + """The number of skips accumulated in the current session, bounded by `max_session_skip_count`. + A new session begins — and this resets to 0 — each time `max_session_skip_count` is updated. + """ + def __init__( + self, + *, + current_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + effective_config: global___TimeSkippingConfig | None = ..., + fast_forward_info: global___TimeSkippingFastForwardInfo | None = ..., + current_session_skip_count: builtins.int = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "current_time", + b"current_time", + "effective_config", + b"effective_config", + "fast_forward_info", + b"fast_forward_info", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "current_session_skip_count", + b"current_session_skip_count", + "current_time", + b"current_time", + "effective_config", + b"effective_config", + "fast_forward_info", + b"fast_forward_info", + ], + ) -> None: ... + +global___TimeSkippingInfo = TimeSkippingInfo + +class TimeSkippingFastForwardInfo(google.protobuf.message.Message): + """TimeSkippingFastForwardInfo describes the current time-skipping fast-forward on an execution.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FAST_FORWARD_DURATION_FIELD_NUMBER: builtins.int + FAST_FORWARD_ID_FIELD_NUMBER: builtins.int + TARGET_TIME_FIELD_NUMBER: builtins.int + HAS_COMPLETED_FIELD_NUMBER: builtins.int + @property + def fast_forward_duration(self) -> google.protobuf.duration_pb2.Duration: + """The client-supplied `fast_forward` duration.""" + fast_forward_id: builtins.str + """The client-supplied ID set alongside `fast_forward` duration.""" + @property + def target_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """The target virtual time at which the fast-forward completes.""" + has_completed: builtins.bool + """True once `target_time` has been reached.""" + def __init__( + self, + *, + fast_forward_duration: google.protobuf.duration_pb2.Duration | None = ..., + fast_forward_id: builtins.str = ..., + target_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + has_completed: builtins.bool = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "fast_forward_duration", + b"fast_forward_duration", + "target_time", + b"target_time", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "fast_forward_duration", + b"fast_forward_duration", + "fast_forward_id", + b"fast_forward_id", + "has_completed", + b"has_completed", + "target_time", + b"target_time", + ], + ) -> None: ... + +global___TimeSkippingFastForwardInfo = TimeSkippingFastForwardInfo diff --git a/temporalio/api/compute/__init__.py b/temporalio/api/compute/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/temporalio/api/compute/v1/__init__.py b/temporalio/api/compute/v1/__init__.py new file mode 100644 index 000000000..816d11299 --- /dev/null +++ b/temporalio/api/compute/v1/__init__.py @@ -0,0 +1,19 @@ +from .config_pb2 import ( + ComputeConfig, + ComputeConfigScalingGroup, + ComputeConfigScalingGroupSummary, + ComputeConfigScalingGroupUpdate, + ComputeConfigSummary, +) +from .provider_pb2 import ComputeProvider +from .scaler_pb2 import ComputeScaler + +__all__ = [ + "ComputeConfig", + "ComputeConfigScalingGroup", + "ComputeConfigScalingGroupSummary", + "ComputeConfigScalingGroupUpdate", + "ComputeConfigSummary", + "ComputeProvider", + "ComputeScaler", +] diff --git a/temporalio/api/compute/v1/config_pb2.py b/temporalio/api/compute/v1/config_pb2.py new file mode 100644 index 000000000..ef3965397 --- /dev/null +++ b/temporalio/api/compute/v1/config_pb2.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: temporal/api/compute/v1/config.proto +"""Generated protocol buffer code.""" + +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import field_mask_pb2 as google_dot_protobuf_dot_field__mask__pb2 + +from temporalio.api.compute.v1 import ( + provider_pb2 as temporal_dot_api_dot_compute_dot_v1_dot_provider__pb2, +) +from temporalio.api.compute.v1 import ( + scaler_pb2 as temporal_dot_api_dot_compute_dot_v1_dot_scaler__pb2, +) +from temporalio.api.enums.v1 import ( + task_queue_pb2 as temporal_dot_api_dot_enums_dot_v1_dot_task__queue__pb2, +) + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n$temporal/api/compute/v1/config.proto\x12\x17temporal.api.compute.v1\x1a&temporal/api/compute/v1/provider.proto\x1a$temporal/api/compute/v1/scaler.proto\x1a&temporal/api/enums/v1/task_queue.proto\x1a google/protobuf/field_mask.proto"\xcf\x01\n\x19\x43omputeConfigScalingGroup\x12>\n\x10task_queue_types\x18\x01 \x03(\x0e\x32$.temporal.api.enums.v1.TaskQueueType\x12:\n\x08provider\x18\x03 \x01(\x0b\x32(.temporal.api.compute.v1.ComputeProvider\x12\x36\n\x06scaler\x18\x04 \x01(\x0b\x32&.temporal.api.compute.v1.ComputeScaler"\xcc\x01\n\rComputeConfig\x12Q\n\x0escaling_groups\x18\x01 \x03(\x0b\x32\x39.temporal.api.compute.v1.ComputeConfig.ScalingGroupsEntry\x1ah\n\x12ScalingGroupsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x41\n\x05value\x18\x02 \x01(\x0b\x32\x32.temporal.api.compute.v1.ComputeConfigScalingGroup:\x02\x38\x01"\x9d\x01\n\x1f\x43omputeConfigScalingGroupUpdate\x12I\n\rscaling_group\x18\x01 \x01(\x0b\x32\x32.temporal.api.compute.v1.ComputeConfigScalingGroup\x12/\n\x0bupdate_mask\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.FieldMask"\xe1\x01\n\x14\x43omputeConfigSummary\x12X\n\x0escaling_groups\x18\x01 \x03(\x0b\x32@.temporal.api.compute.v1.ComputeConfigSummary.ScalingGroupsEntry\x1ao\n\x12ScalingGroupsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12H\n\x05value\x18\x02 \x01(\x0b\x32\x39.temporal.api.compute.v1.ComputeConfigScalingGroupSummary:\x02\x38\x01"y\n ComputeConfigScalingGroupSummary\x12>\n\x10task_queue_types\x18\x01 \x03(\x0e\x32$.temporal.api.enums.v1.TaskQueueType\x12\x15\n\rprovider_type\x18\x02 \x01(\tB\x8d\x01\n\x1aio.temporal.api.compute.v1B\x0b\x43onfigProtoP\x01Z%go.temporal.io/api/compute/v1;compute\xaa\x02\x19Temporalio.Api.Compute.V1\xea\x02\x1cTemporalio::Api::Compute::V1b\x06proto3' +) + + +_COMPUTECONFIGSCALINGGROUP = DESCRIPTOR.message_types_by_name[ + "ComputeConfigScalingGroup" +] +_COMPUTECONFIG = DESCRIPTOR.message_types_by_name["ComputeConfig"] +_COMPUTECONFIG_SCALINGGROUPSENTRY = _COMPUTECONFIG.nested_types_by_name[ + "ScalingGroupsEntry" +] +_COMPUTECONFIGSCALINGGROUPUPDATE = DESCRIPTOR.message_types_by_name[ + "ComputeConfigScalingGroupUpdate" +] +_COMPUTECONFIGSUMMARY = DESCRIPTOR.message_types_by_name["ComputeConfigSummary"] +_COMPUTECONFIGSUMMARY_SCALINGGROUPSENTRY = _COMPUTECONFIGSUMMARY.nested_types_by_name[ + "ScalingGroupsEntry" +] +_COMPUTECONFIGSCALINGGROUPSUMMARY = DESCRIPTOR.message_types_by_name[ + "ComputeConfigScalingGroupSummary" +] +ComputeConfigScalingGroup = _reflection.GeneratedProtocolMessageType( + "ComputeConfigScalingGroup", + (_message.Message,), + { + "DESCRIPTOR": _COMPUTECONFIGSCALINGGROUP, + "__module__": "temporalio.api.compute.v1.config_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.compute.v1.ComputeConfigScalingGroup) + }, +) +_sym_db.RegisterMessage(ComputeConfigScalingGroup) + +ComputeConfig = _reflection.GeneratedProtocolMessageType( + "ComputeConfig", + (_message.Message,), + { + "ScalingGroupsEntry": _reflection.GeneratedProtocolMessageType( + "ScalingGroupsEntry", + (_message.Message,), + { + "DESCRIPTOR": _COMPUTECONFIG_SCALINGGROUPSENTRY, + "__module__": "temporalio.api.compute.v1.config_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.compute.v1.ComputeConfig.ScalingGroupsEntry) + }, + ), + "DESCRIPTOR": _COMPUTECONFIG, + "__module__": "temporalio.api.compute.v1.config_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.compute.v1.ComputeConfig) + }, +) +_sym_db.RegisterMessage(ComputeConfig) +_sym_db.RegisterMessage(ComputeConfig.ScalingGroupsEntry) + +ComputeConfigScalingGroupUpdate = _reflection.GeneratedProtocolMessageType( + "ComputeConfigScalingGroupUpdate", + (_message.Message,), + { + "DESCRIPTOR": _COMPUTECONFIGSCALINGGROUPUPDATE, + "__module__": "temporalio.api.compute.v1.config_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.compute.v1.ComputeConfigScalingGroupUpdate) + }, +) +_sym_db.RegisterMessage(ComputeConfigScalingGroupUpdate) + +ComputeConfigSummary = _reflection.GeneratedProtocolMessageType( + "ComputeConfigSummary", + (_message.Message,), + { + "ScalingGroupsEntry": _reflection.GeneratedProtocolMessageType( + "ScalingGroupsEntry", + (_message.Message,), + { + "DESCRIPTOR": _COMPUTECONFIGSUMMARY_SCALINGGROUPSENTRY, + "__module__": "temporalio.api.compute.v1.config_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.compute.v1.ComputeConfigSummary.ScalingGroupsEntry) + }, + ), + "DESCRIPTOR": _COMPUTECONFIGSUMMARY, + "__module__": "temporalio.api.compute.v1.config_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.compute.v1.ComputeConfigSummary) + }, +) +_sym_db.RegisterMessage(ComputeConfigSummary) +_sym_db.RegisterMessage(ComputeConfigSummary.ScalingGroupsEntry) + +ComputeConfigScalingGroupSummary = _reflection.GeneratedProtocolMessageType( + "ComputeConfigScalingGroupSummary", + (_message.Message,), + { + "DESCRIPTOR": _COMPUTECONFIGSCALINGGROUPSUMMARY, + "__module__": "temporalio.api.compute.v1.config_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.compute.v1.ComputeConfigScalingGroupSummary) + }, +) +_sym_db.RegisterMessage(ComputeConfigScalingGroupSummary) + +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b"\n\032io.temporal.api.compute.v1B\013ConfigProtoP\001Z%go.temporal.io/api/compute/v1;compute\252\002\031Temporalio.Api.Compute.V1\352\002\034Temporalio::Api::Compute::V1" + _COMPUTECONFIG_SCALINGGROUPSENTRY._options = None + _COMPUTECONFIG_SCALINGGROUPSENTRY._serialized_options = b"8\001" + _COMPUTECONFIGSUMMARY_SCALINGGROUPSENTRY._options = None + _COMPUTECONFIGSUMMARY_SCALINGGROUPSENTRY._serialized_options = b"8\001" + _COMPUTECONFIGSCALINGGROUP._serialized_start = 218 + _COMPUTECONFIGSCALINGGROUP._serialized_end = 425 + _COMPUTECONFIG._serialized_start = 428 + _COMPUTECONFIG._serialized_end = 632 + _COMPUTECONFIG_SCALINGGROUPSENTRY._serialized_start = 528 + _COMPUTECONFIG_SCALINGGROUPSENTRY._serialized_end = 632 + _COMPUTECONFIGSCALINGGROUPUPDATE._serialized_start = 635 + _COMPUTECONFIGSCALINGGROUPUPDATE._serialized_end = 792 + _COMPUTECONFIGSUMMARY._serialized_start = 795 + _COMPUTECONFIGSUMMARY._serialized_end = 1020 + _COMPUTECONFIGSUMMARY_SCALINGGROUPSENTRY._serialized_start = 909 + _COMPUTECONFIGSUMMARY_SCALINGGROUPSENTRY._serialized_end = 1020 + _COMPUTECONFIGSCALINGGROUPSUMMARY._serialized_start = 1022 + _COMPUTECONFIGSCALINGGROUPSUMMARY._serialized_end = 1143 +# @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/compute/v1/config_pb2.pyi b/temporalio/api/compute/v1/config_pb2.pyi new file mode 100644 index 000000000..7c0221f4c --- /dev/null +++ b/temporalio/api/compute/v1/config_pb2.pyi @@ -0,0 +1,255 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" + +import builtins +import collections.abc +import sys + +import google.protobuf.descriptor +import google.protobuf.field_mask_pb2 +import google.protobuf.internal.containers +import google.protobuf.message + +import temporalio.api.compute.v1.provider_pb2 +import temporalio.api.compute.v1.scaler_pb2 +import temporalio.api.enums.v1.task_queue_pb2 + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class ComputeConfigScalingGroup(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TASK_QUEUE_TYPES_FIELD_NUMBER: builtins.int + PROVIDER_FIELD_NUMBER: builtins.int + SCALER_FIELD_NUMBER: builtins.int + @property + def task_queue_types( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[ + temporalio.api.enums.v1.task_queue_pb2.TaskQueueType.ValueType + ]: + """Optional. The set of task queue types this scaling group serves. + If not provided, this scaling group serves all not otherwise defined + task types. + """ + @property + def provider(self) -> temporalio.api.compute.v1.provider_pb2.ComputeProvider: + """Stores instructions for a worker control plane controller how to respond + to worker lifeycle events. + """ + @property + def scaler(self) -> temporalio.api.compute.v1.scaler_pb2.ComputeScaler: + """Informs a worker lifecycle controller *when* and *how often* to perform + certain worker lifecycle actions like starting a serverless worker. + """ + def __init__( + self, + *, + task_queue_types: collections.abc.Iterable[ + temporalio.api.enums.v1.task_queue_pb2.TaskQueueType.ValueType + ] + | None = ..., + provider: temporalio.api.compute.v1.provider_pb2.ComputeProvider | None = ..., + scaler: temporalio.api.compute.v1.scaler_pb2.ComputeScaler | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "provider", b"provider", "scaler", b"scaler" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "provider", + b"provider", + "scaler", + b"scaler", + "task_queue_types", + b"task_queue_types", + ], + ) -> None: ... + +global___ComputeConfigScalingGroup = ComputeConfigScalingGroup + +class ComputeConfig(google.protobuf.message.Message): + """ComputeConfig stores configuration that helps a worker control plane + controller understand *when* and *how* to respond to worker lifecycle + events. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class ScalingGroupsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> global___ComputeConfigScalingGroup: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: global___ComputeConfigScalingGroup | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["value", b"value"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["key", b"key", "value", b"value"], + ) -> None: ... + + SCALING_GROUPS_FIELD_NUMBER: builtins.int + @property + def scaling_groups( + self, + ) -> google.protobuf.internal.containers.MessageMap[ + builtins.str, global___ComputeConfigScalingGroup + ]: + """Each scaling group describes a compute config for a specific subset of the worker + deployment version: covering a specific set of task types and/or regions. + Having different configurations for different task types, allows independent + tuning of activity and workflow task processing (for example). + + The key of the map is the ID of the scaling group used to reference it in subsequent + update calls. + """ + def __init__( + self, + *, + scaling_groups: collections.abc.Mapping[ + builtins.str, global___ComputeConfigScalingGroup + ] + | None = ..., + ) -> None: ... + def ClearField( + self, field_name: typing_extensions.Literal["scaling_groups", b"scaling_groups"] + ) -> None: ... + +global___ComputeConfig = ComputeConfig + +class ComputeConfigScalingGroupUpdate(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SCALING_GROUP_FIELD_NUMBER: builtins.int + UPDATE_MASK_FIELD_NUMBER: builtins.int + @property + def scaling_group(self) -> global___ComputeConfigScalingGroup: ... + @property + def update_mask(self) -> google.protobuf.field_mask_pb2.FieldMask: + """Controls which fields from `scaling_group` will be applied. Semantics: + - Mask is ignored for new scaling groups (only applicable when scaling group already exists). + - Empty mask for an existing scaling group is no-op: no change. + - Non-empty mask for an existing scaling group will update/unset only to the fields + mentioned in the mask. + - Accepted paths: "task_queue_types", "provider", "provider.type", "provider.details", + "provider.nexus_endpoint", "scaler", "scaler.type", "scaler.details" + """ + def __init__( + self, + *, + scaling_group: global___ComputeConfigScalingGroup | None = ..., + update_mask: google.protobuf.field_mask_pb2.FieldMask | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "scaling_group", b"scaling_group", "update_mask", b"update_mask" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "scaling_group", b"scaling_group", "update_mask", b"update_mask" + ], + ) -> None: ... + +global___ComputeConfigScalingGroupUpdate = ComputeConfigScalingGroupUpdate + +class ComputeConfigSummary(google.protobuf.message.Message): + """A subset of information in ComputeConfig optimized for list views.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class ScalingGroupsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> global___ComputeConfigScalingGroupSummary: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: global___ComputeConfigScalingGroupSummary | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["value", b"value"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["key", b"key", "value", b"value"], + ) -> None: ... + + SCALING_GROUPS_FIELD_NUMBER: builtins.int + @property + def scaling_groups( + self, + ) -> google.protobuf.internal.containers.MessageMap[ + builtins.str, global___ComputeConfigScalingGroupSummary + ]: ... + def __init__( + self, + *, + scaling_groups: collections.abc.Mapping[ + builtins.str, global___ComputeConfigScalingGroupSummary + ] + | None = ..., + ) -> None: ... + def ClearField( + self, field_name: typing_extensions.Literal["scaling_groups", b"scaling_groups"] + ) -> None: ... + +global___ComputeConfigSummary = ComputeConfigSummary + +class ComputeConfigScalingGroupSummary(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TASK_QUEUE_TYPES_FIELD_NUMBER: builtins.int + PROVIDER_TYPE_FIELD_NUMBER: builtins.int + @property + def task_queue_types( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[ + temporalio.api.enums.v1.task_queue_pb2.TaskQueueType.ValueType + ]: ... + provider_type: builtins.str + def __init__( + self, + *, + task_queue_types: collections.abc.Iterable[ + temporalio.api.enums.v1.task_queue_pb2.TaskQueueType.ValueType + ] + | None = ..., + provider_type: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "provider_type", b"provider_type", "task_queue_types", b"task_queue_types" + ], + ) -> None: ... + +global___ComputeConfigScalingGroupSummary = ComputeConfigScalingGroupSummary diff --git a/temporalio/api/compute/v1/provider_pb2.py b/temporalio/api/compute/v1/provider_pb2.py new file mode 100644 index 000000000..26d921e2b --- /dev/null +++ b/temporalio/api/compute/v1/provider_pb2.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: temporal/api/compute/v1/provider.proto +"""Generated protocol buffer code.""" + +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from temporalio.api.common.v1 import ( + message_pb2 as temporal_dot_api_dot_common_dot_v1_dot_message__pb2, +) + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n&temporal/api/compute/v1/provider.proto\x12\x17temporal.api.compute.v1\x1a$temporal/api/common/v1/message.proto"i\n\x0f\x43omputeProvider\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x30\n\x07\x64\x65tails\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x16\n\x0enexus_endpoint\x18\n \x01(\tB\x8f\x01\n\x1aio.temporal.api.compute.v1B\rProviderProtoP\x01Z%go.temporal.io/api/compute/v1;compute\xaa\x02\x19Temporalio.Api.Compute.V1\xea\x02\x1cTemporalio::Api::Compute::V1b\x06proto3' +) + + +_COMPUTEPROVIDER = DESCRIPTOR.message_types_by_name["ComputeProvider"] +ComputeProvider = _reflection.GeneratedProtocolMessageType( + "ComputeProvider", + (_message.Message,), + { + "DESCRIPTOR": _COMPUTEPROVIDER, + "__module__": "temporalio.api.compute.v1.provider_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.compute.v1.ComputeProvider) + }, +) +_sym_db.RegisterMessage(ComputeProvider) + +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b"\n\032io.temporal.api.compute.v1B\rProviderProtoP\001Z%go.temporal.io/api/compute/v1;compute\252\002\031Temporalio.Api.Compute.V1\352\002\034Temporalio::Api::Compute::V1" + _COMPUTEPROVIDER._serialized_start = 105 + _COMPUTEPROVIDER._serialized_end = 210 +# @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/compute/v1/provider_pb2.pyi b/temporalio/api/compute/v1/provider_pb2.pyi new file mode 100644 index 000000000..90f566aa4 --- /dev/null +++ b/temporalio/api/compute/v1/provider_pb2.pyi @@ -0,0 +1,68 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" + +import builtins +import sys + +import google.protobuf.descriptor +import google.protobuf.message + +import temporalio.api.common.v1.message_pb2 + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class ComputeProvider(google.protobuf.message.Message): + """ComputeProvider stores information used by a worker control plane controller + to respond to worker lifecycle events. For example, when a Task is received + on a TaskQueue that has no active pollers, a serverless worker lifecycle + controller might need to invoke an AWS Lambda Function that itself ends up + calling the SDK's worker.New() function. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TYPE_FIELD_NUMBER: builtins.int + DETAILS_FIELD_NUMBER: builtins.int + NEXUS_ENDPOINT_FIELD_NUMBER: builtins.int + type: builtins.str + """Type of the compute provider. This string is implementation-specific and + can be used by implementations to understand how to interpret the + contents of the provider_details field. + """ + @property + def details(self) -> temporalio.api.common.v1.message_pb2.Payload: + """Contains provider-specific instructions and configuration. + For server-implemented providers, use the SDK's default content + converter to ensure the server can understand it. + For remote-implemented providers, you might use your own content + converters according to what the remote endpoints understand. + """ + nexus_endpoint: builtins.str + """Optional. If the compute provider is a Nexus service, this should point + there. + """ + def __init__( + self, + *, + type: builtins.str = ..., + details: temporalio.api.common.v1.message_pb2.Payload | None = ..., + nexus_endpoint: builtins.str = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["details", b"details"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "details", b"details", "nexus_endpoint", b"nexus_endpoint", "type", b"type" + ], + ) -> None: ... + +global___ComputeProvider = ComputeProvider diff --git a/temporalio/api/compute/v1/scaler_pb2.py b/temporalio/api/compute/v1/scaler_pb2.py new file mode 100644 index 000000000..84bfe7b24 --- /dev/null +++ b/temporalio/api/compute/v1/scaler_pb2.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: temporal/api/compute/v1/scaler.proto +"""Generated protocol buffer code.""" + +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from temporalio.api.common.v1 import ( + message_pb2 as temporal_dot_api_dot_common_dot_v1_dot_message__pb2, +) + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n$temporal/api/compute/v1/scaler.proto\x12\x17temporal.api.compute.v1\x1a$temporal/api/common/v1/message.proto"O\n\rComputeScaler\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x30\n\x07\x64\x65tails\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.PayloadB\x8d\x01\n\x1aio.temporal.api.compute.v1B\x0bScalerProtoP\x01Z%go.temporal.io/api/compute/v1;compute\xaa\x02\x19Temporalio.Api.Compute.V1\xea\x02\x1cTemporalio::Api::Compute::V1b\x06proto3' +) + + +_COMPUTESCALER = DESCRIPTOR.message_types_by_name["ComputeScaler"] +ComputeScaler = _reflection.GeneratedProtocolMessageType( + "ComputeScaler", + (_message.Message,), + { + "DESCRIPTOR": _COMPUTESCALER, + "__module__": "temporalio.api.compute.v1.scaler_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.compute.v1.ComputeScaler) + }, +) +_sym_db.RegisterMessage(ComputeScaler) + +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b"\n\032io.temporal.api.compute.v1B\013ScalerProtoP\001Z%go.temporal.io/api/compute/v1;compute\252\002\031Temporalio.Api.Compute.V1\352\002\034Temporalio::Api::Compute::V1" + _COMPUTESCALER._serialized_start = 103 + _COMPUTESCALER._serialized_end = 182 +# @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/compute/v1/scaler_pb2.pyi b/temporalio/api/compute/v1/scaler_pb2.pyi new file mode 100644 index 000000000..3ff933055 --- /dev/null +++ b/temporalio/api/compute/v1/scaler_pb2.pyi @@ -0,0 +1,57 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" + +import builtins +import sys + +import google.protobuf.descriptor +import google.protobuf.message + +import temporalio.api.common.v1.message_pb2 + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class ComputeScaler(google.protobuf.message.Message): + """ComputeScaler instructs the Temporal Service when to scale up or down the number of + Workers that comprise a WorkerDeployment. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TYPE_FIELD_NUMBER: builtins.int + DETAILS_FIELD_NUMBER: builtins.int + type: builtins.str + """Type of the compute scaler. this string is implementation-specific and + can be used by implementations to understand how to interpret the + contents of the scaler_details field. + """ + @property + def details(self) -> temporalio.api.common.v1.message_pb2.Payload: + """Contains scaler-specific instructions and configuration. + For server-implemented scalers, use the SDK's default data + converter to ensure the server can understand it. + For remote-implemented scalers, you might use your own data + converters according to what the remote endpoints understand. + """ + def __init__( + self, + *, + type: builtins.str = ..., + details: temporalio.api.common.v1.message_pb2.Payload | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["details", b"details"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["details", b"details", "type", b"type"], + ) -> None: ... + +global___ComputeScaler = ComputeScaler diff --git a/temporalio/api/dependencies/__init__.py b/temporalio/api/dependencies/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/temporalio/api/dependencies/nexusannotations/__init__.py b/temporalio/api/dependencies/nexusannotations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/temporalio/api/dependencies/nexusannotations/v1/__init__.py b/temporalio/api/dependencies/nexusannotations/v1/__init__.py new file mode 100644 index 000000000..5cb684914 --- /dev/null +++ b/temporalio/api/dependencies/nexusannotations/v1/__init__.py @@ -0,0 +1,6 @@ +from .options_pb2 import OperationOptions, ServiceOptions + +__all__ = [ + "OperationOptions", + "ServiceOptions", +] diff --git a/temporalio/api/dependencies/nexusannotations/v1/options_pb2.py b/temporalio/api/dependencies/nexusannotations/v1/options_pb2.py new file mode 100644 index 000000000..deb7869f3 --- /dev/null +++ b/temporalio/api/dependencies/nexusannotations/v1/options_pb2.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: nexusannotations/v1/options.proto +"""Generated protocol buffer code.""" + +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2 + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n!nexusannotations/v1/options.proto\x12\x13nexusannotations.v1\x1a google/protobuf/descriptor.proto".\n\x10OperationOptions\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04tags\x18\x02 \x03(\t",\n\x0eServiceOptions\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04tags\x18\x02 \x03(\t:Y\n\x07service\x12\x1f.google.protobuf.ServiceOptions\x18\xa9@ \x01(\x0b\x32#.nexusannotations.v1.ServiceOptions\x88\x01\x01:\\\n\toperation\x12\x1e.google.protobuf.MethodOptions\x18\xaa@ \x01(\x0b\x32%.nexusannotations.v1.OperationOptions\x88\x01\x01\x42\x45ZCgithub.com/nexus-rpc/nexus-proto-annotations/go/nexusannotations/v1b\x06proto3' +) + + +SERVICE_FIELD_NUMBER = 8233 +service = DESCRIPTOR.extensions_by_name["service"] +OPERATION_FIELD_NUMBER = 8234 +operation = DESCRIPTOR.extensions_by_name["operation"] + +_OPERATIONOPTIONS = DESCRIPTOR.message_types_by_name["OperationOptions"] +_SERVICEOPTIONS = DESCRIPTOR.message_types_by_name["ServiceOptions"] +OperationOptions = _reflection.GeneratedProtocolMessageType( + "OperationOptions", + (_message.Message,), + { + "DESCRIPTOR": _OPERATIONOPTIONS, + "__module__": "temporalio.api.dependencies.nexusannotations.v1.options_pb2", + # @@protoc_insertion_point(class_scope:nexusannotations.v1.OperationOptions) + }, +) +_sym_db.RegisterMessage(OperationOptions) + +ServiceOptions = _reflection.GeneratedProtocolMessageType( + "ServiceOptions", + (_message.Message,), + { + "DESCRIPTOR": _SERVICEOPTIONS, + "__module__": "temporalio.api.dependencies.nexusannotations.v1.options_pb2", + # @@protoc_insertion_point(class_scope:nexusannotations.v1.ServiceOptions) + }, +) +_sym_db.RegisterMessage(ServiceOptions) + +if _descriptor._USE_C_DESCRIPTORS == False: + google_dot_protobuf_dot_descriptor__pb2.ServiceOptions.RegisterExtension(service) + google_dot_protobuf_dot_descriptor__pb2.MethodOptions.RegisterExtension(operation) + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = ( + b"ZCgithub.com/nexus-rpc/nexus-proto-annotations/go/nexusannotations/v1" + ) + _OPERATIONOPTIONS._serialized_start = 92 + _OPERATIONOPTIONS._serialized_end = 138 + _SERVICEOPTIONS._serialized_start = 140 + _SERVICEOPTIONS._serialized_end = 184 +# @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/dependencies/nexusannotations/v1/options_pb2.pyi b/temporalio/api/dependencies/nexusannotations/v1/options_pb2.pyi new file mode 100644 index 000000000..3c440fb75 --- /dev/null +++ b/temporalio/api/dependencies/nexusannotations/v1/options_pb2.pyi @@ -0,0 +1,78 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" + +import builtins +import collections.abc +import sys + +import google.protobuf.descriptor +import google.protobuf.descriptor_pb2 +import google.protobuf.internal.containers +import google.protobuf.internal.extension_dict +import google.protobuf.message + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class OperationOptions(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + TAGS_FIELD_NUMBER: builtins.int + name: builtins.str + """Nexus operation name (defaults to proto method name).""" + @property + def tags( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """Tags to attach to the operation. Used by code generators to include and exclude operations.""" + def __init__( + self, + *, + name: builtins.str = ..., + tags: collections.abc.Iterable[builtins.str] | None = ..., + ) -> None: ... + def ClearField( + self, field_name: typing_extensions.Literal["name", b"name", "tags", b"tags"] + ) -> None: ... + +global___OperationOptions = OperationOptions + +class ServiceOptions(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + TAGS_FIELD_NUMBER: builtins.int + name: builtins.str + """Nexus service name (defaults to proto service full name).""" + @property + def tags( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """Tags to attach to the service. Used by code generators to include and exclude services.""" + def __init__( + self, + *, + name: builtins.str = ..., + tags: collections.abc.Iterable[builtins.str] | None = ..., + ) -> None: ... + def ClearField( + self, field_name: typing_extensions.Literal["name", b"name", "tags", b"tags"] + ) -> None: ... + +global___ServiceOptions = ServiceOptions + +SERVICE_FIELD_NUMBER: builtins.int +OPERATION_FIELD_NUMBER: builtins.int +service: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[ + google.protobuf.descriptor_pb2.ServiceOptions, global___ServiceOptions +] +operation: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[ + google.protobuf.descriptor_pb2.MethodOptions, global___OperationOptions +] diff --git a/temporalio/api/dependencies/protoc_gen_openapiv2/__init__.py b/temporalio/api/dependencies/protoc_gen_openapiv2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/temporalio/api/dependencies/protoc_gen_openapiv2/options/__init__.py b/temporalio/api/dependencies/protoc_gen_openapiv2/options/__init__.py new file mode 100644 index 000000000..f828cbf23 --- /dev/null +++ b/temporalio/api/dependencies/protoc_gen_openapiv2/options/__init__.py @@ -0,0 +1,43 @@ +from .openapiv2_pb2 import ( + Contact, + EnumSchema, + ExternalDocumentation, + Header, + HeaderParameter, + Info, + JSONSchema, + License, + Operation, + Parameters, + Response, + Schema, + Scheme, + Scopes, + SecurityDefinitions, + SecurityRequirement, + SecurityScheme, + Swagger, + Tag, +) + +__all__ = [ + "Contact", + "EnumSchema", + "ExternalDocumentation", + "Header", + "HeaderParameter", + "Info", + "JSONSchema", + "License", + "Operation", + "Parameters", + "Response", + "Schema", + "Scheme", + "Scopes", + "SecurityDefinitions", + "SecurityRequirement", + "SecurityScheme", + "Swagger", + "Tag", +] diff --git a/temporalio/api/dependencies/protoc_gen_openapiv2/options/annotations_pb2.py b/temporalio/api/dependencies/protoc_gen_openapiv2/options/annotations_pb2.py new file mode 100644 index 000000000..3d0ab9f2e --- /dev/null +++ b/temporalio/api/dependencies/protoc_gen_openapiv2/options/annotations_pb2.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: protoc-gen-openapiv2/options/annotations.proto +"""Generated protocol buffer code.""" + +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2 + +from temporalio.api.dependencies.protoc_gen_openapiv2.options import ( + openapiv2_pb2 as protoc__gen__openapiv2_dot_options_dot_openapiv2__pb2, +) + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b"\n.protoc-gen-openapiv2/options/annotations.proto\x12)grpc.gateway.protoc_gen_openapiv2.options\x1a google/protobuf/descriptor.proto\x1a,protoc-gen-openapiv2/options/openapiv2.proto:l\n\x11openapiv2_swagger\x12\x1c.google.protobuf.FileOptions\x18\x92\x08 \x01(\x0b\x32\x32.grpc.gateway.protoc_gen_openapiv2.options.Swagger:r\n\x13openapiv2_operation\x12\x1e.google.protobuf.MethodOptions\x18\x92\x08 \x01(\x0b\x32\x34.grpc.gateway.protoc_gen_openapiv2.options.Operation:m\n\x10openapiv2_schema\x12\x1f.google.protobuf.MessageOptions\x18\x92\x08 \x01(\x0b\x32\x31.grpc.gateway.protoc_gen_openapiv2.options.Schema:l\n\x0eopenapiv2_enum\x12\x1c.google.protobuf.EnumOptions\x18\x92\x08 \x01(\x0b\x32\x35.grpc.gateway.protoc_gen_openapiv2.options.EnumSchema:g\n\ropenapiv2_tag\x12\x1f.google.protobuf.ServiceOptions\x18\x92\x08 \x01(\x0b\x32..grpc.gateway.protoc_gen_openapiv2.options.Tag:n\n\x0fopenapiv2_field\x12\x1d.google.protobuf.FieldOptions\x18\x92\x08 \x01(\x0b\x32\x35.grpc.gateway.protoc_gen_openapiv2.options.JSONSchemaBHZFgithub.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/optionsb\x06proto3" +) + + +OPENAPIV2_SWAGGER_FIELD_NUMBER = 1042 +openapiv2_swagger = DESCRIPTOR.extensions_by_name["openapiv2_swagger"] +OPENAPIV2_OPERATION_FIELD_NUMBER = 1042 +openapiv2_operation = DESCRIPTOR.extensions_by_name["openapiv2_operation"] +OPENAPIV2_SCHEMA_FIELD_NUMBER = 1042 +openapiv2_schema = DESCRIPTOR.extensions_by_name["openapiv2_schema"] +OPENAPIV2_ENUM_FIELD_NUMBER = 1042 +openapiv2_enum = DESCRIPTOR.extensions_by_name["openapiv2_enum"] +OPENAPIV2_TAG_FIELD_NUMBER = 1042 +openapiv2_tag = DESCRIPTOR.extensions_by_name["openapiv2_tag"] +OPENAPIV2_FIELD_FIELD_NUMBER = 1042 +openapiv2_field = DESCRIPTOR.extensions_by_name["openapiv2_field"] + +if _descriptor._USE_C_DESCRIPTORS == False: + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + openapiv2_swagger + ) + google_dot_protobuf_dot_descriptor__pb2.MethodOptions.RegisterExtension( + openapiv2_operation + ) + google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension( + openapiv2_schema + ) + google_dot_protobuf_dot_descriptor__pb2.EnumOptions.RegisterExtension( + openapiv2_enum + ) + google_dot_protobuf_dot_descriptor__pb2.ServiceOptions.RegisterExtension( + openapiv2_tag + ) + google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension( + openapiv2_field + ) + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = ( + b"ZFgithub.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/options" + ) +# @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/dependencies/protoc_gen_openapiv2/options/annotations_pb2.pyi b/temporalio/api/dependencies/protoc_gen_openapiv2/options/annotations_pb2.pyi new file mode 100644 index 000000000..1d2f3de31 --- /dev/null +++ b/temporalio/api/dependencies/protoc_gen_openapiv2/options/annotations_pb2.pyi @@ -0,0 +1,75 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" + +import builtins + +import google.protobuf.descriptor +import google.protobuf.descriptor_pb2 +import google.protobuf.internal.extension_dict + +import temporalio.api.dependencies.protoc_gen_openapiv2.options.openapiv2_pb2 + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +OPENAPIV2_SWAGGER_FIELD_NUMBER: builtins.int +OPENAPIV2_OPERATION_FIELD_NUMBER: builtins.int +OPENAPIV2_SCHEMA_FIELD_NUMBER: builtins.int +OPENAPIV2_ENUM_FIELD_NUMBER: builtins.int +OPENAPIV2_TAG_FIELD_NUMBER: builtins.int +OPENAPIV2_FIELD_FIELD_NUMBER: builtins.int +openapiv2_swagger: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[ + google.protobuf.descriptor_pb2.FileOptions, + temporalio.api.dependencies.protoc_gen_openapiv2.options.openapiv2_pb2.Swagger, +] +"""ID assigned by protobuf-global-extension-registry@google.com for gRPC-Gateway project. + +All IDs are the same, as assigned. It is okay that they are the same, as they extend +different descriptor messages. +""" +openapiv2_operation: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[ + google.protobuf.descriptor_pb2.MethodOptions, + temporalio.api.dependencies.protoc_gen_openapiv2.options.openapiv2_pb2.Operation, +] +"""ID assigned by protobuf-global-extension-registry@google.com for gRPC-Gateway project. + +All IDs are the same, as assigned. It is okay that they are the same, as they extend +different descriptor messages. +""" +openapiv2_schema: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[ + google.protobuf.descriptor_pb2.MessageOptions, + temporalio.api.dependencies.protoc_gen_openapiv2.options.openapiv2_pb2.Schema, +] +"""ID assigned by protobuf-global-extension-registry@google.com for gRPC-Gateway project. + +All IDs are the same, as assigned. It is okay that they are the same, as they extend +different descriptor messages. +""" +openapiv2_enum: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[ + google.protobuf.descriptor_pb2.EnumOptions, + temporalio.api.dependencies.protoc_gen_openapiv2.options.openapiv2_pb2.EnumSchema, +] +"""ID assigned by protobuf-global-extension-registry@google.com for gRPC-Gateway project. + +All IDs are the same, as assigned. It is okay that they are the same, as they extend +different descriptor messages. +""" +openapiv2_tag: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[ + google.protobuf.descriptor_pb2.ServiceOptions, + temporalio.api.dependencies.protoc_gen_openapiv2.options.openapiv2_pb2.Tag, +] +"""ID assigned by protobuf-global-extension-registry@google.com for gRPC-Gateway project. + +All IDs are the same, as assigned. It is okay that they are the same, as they extend +different descriptor messages. +""" +openapiv2_field: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[ + google.protobuf.descriptor_pb2.FieldOptions, + temporalio.api.dependencies.protoc_gen_openapiv2.options.openapiv2_pb2.JSONSchema, +] +"""ID assigned by protobuf-global-extension-registry@google.com for gRPC-Gateway project. + +All IDs are the same, as assigned. It is okay that they are the same, as they extend +different descriptor messages. +""" diff --git a/temporalio/api/dependencies/protoc_gen_openapiv2/options/openapiv2_pb2.py b/temporalio/api/dependencies/protoc_gen_openapiv2/options/openapiv2_pb2.py new file mode 100644 index 000000000..ea0b8eec5 --- /dev/null +++ b/temporalio/api/dependencies/protoc_gen_openapiv2/options/openapiv2_pb2.py @@ -0,0 +1,568 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: protoc-gen-openapiv2/options/openapiv2.proto +"""Generated protocol buffer code.""" + +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import enum_type_wrapper + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n,protoc-gen-openapiv2/options/openapiv2.proto\x12)grpc.gateway.protoc_gen_openapiv2.options\x1a\x1cgoogle/protobuf/struct.proto"\x95\x07\n\x07Swagger\x12\x0f\n\x07swagger\x18\x01 \x01(\t\x12=\n\x04info\x18\x02 \x01(\x0b\x32/.grpc.gateway.protoc_gen_openapiv2.options.Info\x12\x0c\n\x04host\x18\x03 \x01(\t\x12\x11\n\tbase_path\x18\x04 \x01(\t\x12\x42\n\x07schemes\x18\x05 \x03(\x0e\x32\x31.grpc.gateway.protoc_gen_openapiv2.options.Scheme\x12\x10\n\x08\x63onsumes\x18\x06 \x03(\t\x12\x10\n\x08produces\x18\x07 \x03(\t\x12T\n\tresponses\x18\n \x03(\x0b\x32\x41.grpc.gateway.protoc_gen_openapiv2.options.Swagger.ResponsesEntry\x12\\\n\x14security_definitions\x18\x0b \x01(\x0b\x32>.grpc.gateway.protoc_gen_openapiv2.options.SecurityDefinitions\x12P\n\x08security\x18\x0c \x03(\x0b\x32>.grpc.gateway.protoc_gen_openapiv2.options.SecurityRequirement\x12<\n\x04tags\x18\r \x03(\x0b\x32..grpc.gateway.protoc_gen_openapiv2.options.Tag\x12W\n\rexternal_docs\x18\x0e \x01(\x0b\x32@.grpc.gateway.protoc_gen_openapiv2.options.ExternalDocumentation\x12V\n\nextensions\x18\x0f \x03(\x0b\x32\x42.grpc.gateway.protoc_gen_openapiv2.options.Swagger.ExtensionsEntry\x1a\x65\n\x0eResponsesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x42\n\x05value\x18\x02 \x01(\x0b\x32\x33.grpc.gateway.protoc_gen_openapiv2.options.Response:\x02\x38\x01\x1aI\n\x0f\x45xtensionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value:\x02\x38\x01J\x04\x08\x08\x10\tJ\x04\x08\t\x10\n"\xb1\x06\n\tOperation\x12\x0c\n\x04tags\x18\x01 \x03(\t\x12\x0f\n\x07summary\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\x12W\n\rexternal_docs\x18\x04 \x01(\x0b\x32@.grpc.gateway.protoc_gen_openapiv2.options.ExternalDocumentation\x12\x14\n\x0coperation_id\x18\x05 \x01(\t\x12\x10\n\x08\x63onsumes\x18\x06 \x03(\t\x12\x10\n\x08produces\x18\x07 \x03(\t\x12V\n\tresponses\x18\t \x03(\x0b\x32\x43.grpc.gateway.protoc_gen_openapiv2.options.Operation.ResponsesEntry\x12\x42\n\x07schemes\x18\n \x03(\x0e\x32\x31.grpc.gateway.protoc_gen_openapiv2.options.Scheme\x12\x12\n\ndeprecated\x18\x0b \x01(\x08\x12P\n\x08security\x18\x0c \x03(\x0b\x32>.grpc.gateway.protoc_gen_openapiv2.options.SecurityRequirement\x12X\n\nextensions\x18\r \x03(\x0b\x32\x44.grpc.gateway.protoc_gen_openapiv2.options.Operation.ExtensionsEntry\x12I\n\nparameters\x18\x0e \x01(\x0b\x32\x35.grpc.gateway.protoc_gen_openapiv2.options.Parameters\x1a\x65\n\x0eResponsesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x42\n\x05value\x18\x02 \x01(\x0b\x32\x33.grpc.gateway.protoc_gen_openapiv2.options.Response:\x02\x38\x01\x1aI\n\x0f\x45xtensionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value:\x02\x38\x01J\x04\x08\x08\x10\t"Y\n\nParameters\x12K\n\x07headers\x18\x01 \x03(\x0b\x32:.grpc.gateway.protoc_gen_openapiv2.options.HeaderParameter"\xf8\x01\n\x0fHeaderParameter\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12M\n\x04type\x18\x03 \x01(\x0e\x32?.grpc.gateway.protoc_gen_openapiv2.options.HeaderParameter.Type\x12\x0e\n\x06\x66ormat\x18\x04 \x01(\t\x12\x10\n\x08required\x18\x05 \x01(\x08"E\n\x04Type\x12\x0b\n\x07UNKNOWN\x10\x00\x12\n\n\x06STRING\x10\x01\x12\n\n\x06NUMBER\x10\x02\x12\x0b\n\x07INTEGER\x10\x03\x12\x0b\n\x07\x42OOLEAN\x10\x04J\x04\x08\x06\x10\x07J\x04\x08\x07\x10\x08"\xab\x01\n\x06Header\x12\x13\n\x0b\x64\x65scription\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x0e\n\x06\x66ormat\x18\x03 \x01(\t\x12\x0f\n\x07\x64\x65\x66\x61ult\x18\x06 \x01(\t\x12\x0f\n\x07pattern\x18\r \x01(\tJ\x04\x08\x04\x10\x05J\x04\x08\x05\x10\x06J\x04\x08\x07\x10\x08J\x04\x08\x08\x10\tJ\x04\x08\t\x10\nJ\x04\x08\n\x10\x0bJ\x04\x08\x0b\x10\x0cJ\x04\x08\x0c\x10\rJ\x04\x08\x0e\x10\x0fJ\x04\x08\x0f\x10\x10J\x04\x08\x10\x10\x11J\x04\x08\x11\x10\x12J\x04\x08\x12\x10\x13"\xc2\x04\n\x08Response\x12\x13\n\x0b\x64\x65scription\x18\x01 \x01(\t\x12\x41\n\x06schema\x18\x02 \x01(\x0b\x32\x31.grpc.gateway.protoc_gen_openapiv2.options.Schema\x12Q\n\x07headers\x18\x03 \x03(\x0b\x32@.grpc.gateway.protoc_gen_openapiv2.options.Response.HeadersEntry\x12S\n\x08\x65xamples\x18\x04 \x03(\x0b\x32\x41.grpc.gateway.protoc_gen_openapiv2.options.Response.ExamplesEntry\x12W\n\nextensions\x18\x05 \x03(\x0b\x32\x43.grpc.gateway.protoc_gen_openapiv2.options.Response.ExtensionsEntry\x1a\x61\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12@\n\x05value\x18\x02 \x01(\x0b\x32\x31.grpc.gateway.protoc_gen_openapiv2.options.Header:\x02\x38\x01\x1a/\n\rExamplesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1aI\n\x0f\x45xtensionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value:\x02\x38\x01"\xff\x02\n\x04Info\x12\r\n\x05title\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x18\n\x10terms_of_service\x18\x03 \x01(\t\x12\x43\n\x07\x63ontact\x18\x04 \x01(\x0b\x32\x32.grpc.gateway.protoc_gen_openapiv2.options.Contact\x12\x43\n\x07license\x18\x05 \x01(\x0b\x32\x32.grpc.gateway.protoc_gen_openapiv2.options.License\x12\x0f\n\x07version\x18\x06 \x01(\t\x12S\n\nextensions\x18\x07 \x03(\x0b\x32?.grpc.gateway.protoc_gen_openapiv2.options.Info.ExtensionsEntry\x1aI\n\x0f\x45xtensionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value:\x02\x38\x01"3\n\x07\x43ontact\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\x12\r\n\x05\x65mail\x18\x03 \x01(\t"$\n\x07License\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t"9\n\x15\x45xternalDocumentation\x12\x13\n\x0b\x64\x65scription\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t"\xee\x01\n\x06Schema\x12J\n\x0bjson_schema\x18\x01 \x01(\x0b\x32\x35.grpc.gateway.protoc_gen_openapiv2.options.JSONSchema\x12\x15\n\rdiscriminator\x18\x02 \x01(\t\x12\x11\n\tread_only\x18\x03 \x01(\x08\x12W\n\rexternal_docs\x18\x05 \x01(\x0b\x32@.grpc.gateway.protoc_gen_openapiv2.options.ExternalDocumentation\x12\x0f\n\x07\x65xample\x18\x06 \x01(\tJ\x04\x08\x04\x10\x05"\x83\x03\n\nEnumSchema\x12\x13\n\x0b\x64\x65scription\x18\x01 \x01(\t\x12\x0f\n\x07\x64\x65\x66\x61ult\x18\x02 \x01(\t\x12\r\n\x05title\x18\x03 \x01(\t\x12\x10\n\x08required\x18\x04 \x01(\x08\x12\x11\n\tread_only\x18\x05 \x01(\x08\x12W\n\rexternal_docs\x18\x06 \x01(\x0b\x32@.grpc.gateway.protoc_gen_openapiv2.options.ExternalDocumentation\x12\x0f\n\x07\x65xample\x18\x07 \x01(\t\x12\x0b\n\x03ref\x18\x08 \x01(\t\x12Y\n\nextensions\x18\t \x03(\x0b\x32\x45.grpc.gateway.protoc_gen_openapiv2.options.EnumSchema.ExtensionsEntry\x1aI\n\x0f\x45xtensionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value:\x02\x38\x01"\xa2\x08\n\nJSONSchema\x12\x0b\n\x03ref\x18\x03 \x01(\t\x12\r\n\x05title\x18\x05 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x06 \x01(\t\x12\x0f\n\x07\x64\x65\x66\x61ult\x18\x07 \x01(\t\x12\x11\n\tread_only\x18\x08 \x01(\x08\x12\x0f\n\x07\x65xample\x18\t \x01(\t\x12\x13\n\x0bmultiple_of\x18\n \x01(\x01\x12\x0f\n\x07maximum\x18\x0b \x01(\x01\x12\x19\n\x11\x65xclusive_maximum\x18\x0c \x01(\x08\x12\x0f\n\x07minimum\x18\r \x01(\x01\x12\x19\n\x11\x65xclusive_minimum\x18\x0e \x01(\x08\x12\x12\n\nmax_length\x18\x0f \x01(\x04\x12\x12\n\nmin_length\x18\x10 \x01(\x04\x12\x0f\n\x07pattern\x18\x11 \x01(\t\x12\x11\n\tmax_items\x18\x14 \x01(\x04\x12\x11\n\tmin_items\x18\x15 \x01(\x04\x12\x14\n\x0cunique_items\x18\x16 \x01(\x08\x12\x16\n\x0emax_properties\x18\x18 \x01(\x04\x12\x16\n\x0emin_properties\x18\x19 \x01(\x04\x12\x10\n\x08required\x18\x1a \x03(\t\x12\r\n\x05\x61rray\x18" \x03(\t\x12Y\n\x04type\x18# \x03(\x0e\x32K.grpc.gateway.protoc_gen_openapiv2.options.JSONSchema.JSONSchemaSimpleTypes\x12\x0e\n\x06\x66ormat\x18$ \x01(\t\x12\x0c\n\x04\x65num\x18. \x03(\t\x12\x66\n\x13\x66ield_configuration\x18\xe9\x07 \x01(\x0b\x32H.grpc.gateway.protoc_gen_openapiv2.options.JSONSchema.FieldConfiguration\x12Y\n\nextensions\x18\x30 \x03(\x0b\x32\x45.grpc.gateway.protoc_gen_openapiv2.options.JSONSchema.ExtensionsEntry\x1a-\n\x12\x46ieldConfiguration\x12\x17\n\x0fpath_param_name\x18/ \x01(\t\x1aI\n\x0f\x45xtensionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value:\x02\x38\x01"w\n\x15JSONSchemaSimpleTypes\x12\x0b\n\x07UNKNOWN\x10\x00\x12\t\n\x05\x41RRAY\x10\x01\x12\x0b\n\x07\x42OOLEAN\x10\x02\x12\x0b\n\x07INTEGER\x10\x03\x12\x08\n\x04NULL\x10\x04\x12\n\n\x06NUMBER\x10\x05\x12\n\n\x06OBJECT\x10\x06\x12\n\n\x06STRING\x10\x07J\x04\x08\x01\x10\x02J\x04\x08\x02\x10\x03J\x04\x08\x04\x10\x05J\x04\x08\x12\x10\x13J\x04\x08\x13\x10\x14J\x04\x08\x17\x10\x18J\x04\x08\x1b\x10\x1cJ\x04\x08\x1c\x10\x1dJ\x04\x08\x1d\x10\x1eJ\x04\x08\x1e\x10"J\x04\x08%\x10*J\x04\x08*\x10+J\x04\x08+\x10."\xa0\x02\n\x03Tag\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12W\n\rexternal_docs\x18\x03 \x01(\x0b\x32@.grpc.gateway.protoc_gen_openapiv2.options.ExternalDocumentation\x12R\n\nextensions\x18\x04 \x03(\x0b\x32>.grpc.gateway.protoc_gen_openapiv2.options.Tag.ExtensionsEntry\x1aI\n\x0f\x45xtensionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value:\x02\x38\x01"\xe1\x01\n\x13SecurityDefinitions\x12^\n\x08security\x18\x01 \x03(\x0b\x32L.grpc.gateway.protoc_gen_openapiv2.options.SecurityDefinitions.SecurityEntry\x1aj\n\rSecurityEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12H\n\x05value\x18\x02 \x01(\x0b\x32\x39.grpc.gateway.protoc_gen_openapiv2.options.SecurityScheme:\x02\x38\x01"\xa0\x06\n\x0eSecurityScheme\x12L\n\x04type\x18\x01 \x01(\x0e\x32>.grpc.gateway.protoc_gen_openapiv2.options.SecurityScheme.Type\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12H\n\x02in\x18\x04 \x01(\x0e\x32<.grpc.gateway.protoc_gen_openapiv2.options.SecurityScheme.In\x12L\n\x04\x66low\x18\x05 \x01(\x0e\x32>.grpc.gateway.protoc_gen_openapiv2.options.SecurityScheme.Flow\x12\x19\n\x11\x61uthorization_url\x18\x06 \x01(\t\x12\x11\n\ttoken_url\x18\x07 \x01(\t\x12\x41\n\x06scopes\x18\x08 \x01(\x0b\x32\x31.grpc.gateway.protoc_gen_openapiv2.options.Scopes\x12]\n\nextensions\x18\t \x03(\x0b\x32I.grpc.gateway.protoc_gen_openapiv2.options.SecurityScheme.ExtensionsEntry\x1aI\n\x0f\x45xtensionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value:\x02\x38\x01"K\n\x04Type\x12\x10\n\x0cTYPE_INVALID\x10\x00\x12\x0e\n\nTYPE_BASIC\x10\x01\x12\x10\n\x0cTYPE_API_KEY\x10\x02\x12\x0f\n\x0bTYPE_OAUTH2\x10\x03"1\n\x02In\x12\x0e\n\nIN_INVALID\x10\x00\x12\x0c\n\x08IN_QUERY\x10\x01\x12\r\n\tIN_HEADER\x10\x02"j\n\x04\x46low\x12\x10\n\x0c\x46LOW_INVALID\x10\x00\x12\x11\n\rFLOW_IMPLICIT\x10\x01\x12\x11\n\rFLOW_PASSWORD\x10\x02\x12\x14\n\x10\x46LOW_APPLICATION\x10\x03\x12\x14\n\x10\x46LOW_ACCESS_CODE\x10\x04"\xcd\x02\n\x13SecurityRequirement\x12u\n\x14security_requirement\x18\x01 \x03(\x0b\x32W.grpc.gateway.protoc_gen_openapiv2.options.SecurityRequirement.SecurityRequirementEntry\x1a)\n\x18SecurityRequirementValue\x12\r\n\x05scope\x18\x01 \x03(\t\x1a\x93\x01\n\x18SecurityRequirementEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x66\n\x05value\x18\x02 \x01(\x0b\x32W.grpc.gateway.protoc_gen_openapiv2.options.SecurityRequirement.SecurityRequirementValue:\x02\x38\x01"\x83\x01\n\x06Scopes\x12K\n\x05scope\x18\x01 \x03(\x0b\x32<.grpc.gateway.protoc_gen_openapiv2.options.Scopes.ScopeEntry\x1a,\n\nScopeEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01*;\n\x06Scheme\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x08\n\x04HTTP\x10\x01\x12\t\n\x05HTTPS\x10\x02\x12\x06\n\x02WS\x10\x03\x12\x07\n\x03WSS\x10\x04\x42HZFgithub.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/optionsb\x06proto3' +) + +_SCHEME = DESCRIPTOR.enum_types_by_name["Scheme"] +Scheme = enum_type_wrapper.EnumTypeWrapper(_SCHEME) +UNKNOWN = 0 +HTTP = 1 +HTTPS = 2 +WS = 3 +WSS = 4 + + +_SWAGGER = DESCRIPTOR.message_types_by_name["Swagger"] +_SWAGGER_RESPONSESENTRY = _SWAGGER.nested_types_by_name["ResponsesEntry"] +_SWAGGER_EXTENSIONSENTRY = _SWAGGER.nested_types_by_name["ExtensionsEntry"] +_OPERATION = DESCRIPTOR.message_types_by_name["Operation"] +_OPERATION_RESPONSESENTRY = _OPERATION.nested_types_by_name["ResponsesEntry"] +_OPERATION_EXTENSIONSENTRY = _OPERATION.nested_types_by_name["ExtensionsEntry"] +_PARAMETERS = DESCRIPTOR.message_types_by_name["Parameters"] +_HEADERPARAMETER = DESCRIPTOR.message_types_by_name["HeaderParameter"] +_HEADER = DESCRIPTOR.message_types_by_name["Header"] +_RESPONSE = DESCRIPTOR.message_types_by_name["Response"] +_RESPONSE_HEADERSENTRY = _RESPONSE.nested_types_by_name["HeadersEntry"] +_RESPONSE_EXAMPLESENTRY = _RESPONSE.nested_types_by_name["ExamplesEntry"] +_RESPONSE_EXTENSIONSENTRY = _RESPONSE.nested_types_by_name["ExtensionsEntry"] +_INFO = DESCRIPTOR.message_types_by_name["Info"] +_INFO_EXTENSIONSENTRY = _INFO.nested_types_by_name["ExtensionsEntry"] +_CONTACT = DESCRIPTOR.message_types_by_name["Contact"] +_LICENSE = DESCRIPTOR.message_types_by_name["License"] +_EXTERNALDOCUMENTATION = DESCRIPTOR.message_types_by_name["ExternalDocumentation"] +_SCHEMA = DESCRIPTOR.message_types_by_name["Schema"] +_ENUMSCHEMA = DESCRIPTOR.message_types_by_name["EnumSchema"] +_ENUMSCHEMA_EXTENSIONSENTRY = _ENUMSCHEMA.nested_types_by_name["ExtensionsEntry"] +_JSONSCHEMA = DESCRIPTOR.message_types_by_name["JSONSchema"] +_JSONSCHEMA_FIELDCONFIGURATION = _JSONSCHEMA.nested_types_by_name["FieldConfiguration"] +_JSONSCHEMA_EXTENSIONSENTRY = _JSONSCHEMA.nested_types_by_name["ExtensionsEntry"] +_TAG = DESCRIPTOR.message_types_by_name["Tag"] +_TAG_EXTENSIONSENTRY = _TAG.nested_types_by_name["ExtensionsEntry"] +_SECURITYDEFINITIONS = DESCRIPTOR.message_types_by_name["SecurityDefinitions"] +_SECURITYDEFINITIONS_SECURITYENTRY = _SECURITYDEFINITIONS.nested_types_by_name[ + "SecurityEntry" +] +_SECURITYSCHEME = DESCRIPTOR.message_types_by_name["SecurityScheme"] +_SECURITYSCHEME_EXTENSIONSENTRY = _SECURITYSCHEME.nested_types_by_name[ + "ExtensionsEntry" +] +_SECURITYREQUIREMENT = DESCRIPTOR.message_types_by_name["SecurityRequirement"] +_SECURITYREQUIREMENT_SECURITYREQUIREMENTVALUE = ( + _SECURITYREQUIREMENT.nested_types_by_name["SecurityRequirementValue"] +) +_SECURITYREQUIREMENT_SECURITYREQUIREMENTENTRY = ( + _SECURITYREQUIREMENT.nested_types_by_name["SecurityRequirementEntry"] +) +_SCOPES = DESCRIPTOR.message_types_by_name["Scopes"] +_SCOPES_SCOPEENTRY = _SCOPES.nested_types_by_name["ScopeEntry"] +_HEADERPARAMETER_TYPE = _HEADERPARAMETER.enum_types_by_name["Type"] +_JSONSCHEMA_JSONSCHEMASIMPLETYPES = _JSONSCHEMA.enum_types_by_name[ + "JSONSchemaSimpleTypes" +] +_SECURITYSCHEME_TYPE = _SECURITYSCHEME.enum_types_by_name["Type"] +_SECURITYSCHEME_IN = _SECURITYSCHEME.enum_types_by_name["In"] +_SECURITYSCHEME_FLOW = _SECURITYSCHEME.enum_types_by_name["Flow"] +Swagger = _reflection.GeneratedProtocolMessageType( + "Swagger", + (_message.Message,), + { + "ResponsesEntry": _reflection.GeneratedProtocolMessageType( + "ResponsesEntry", + (_message.Message,), + { + "DESCRIPTOR": _SWAGGER_RESPONSESENTRY, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.Swagger.ResponsesEntry) + }, + ), + "ExtensionsEntry": _reflection.GeneratedProtocolMessageType( + "ExtensionsEntry", + (_message.Message,), + { + "DESCRIPTOR": _SWAGGER_EXTENSIONSENTRY, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.Swagger.ExtensionsEntry) + }, + ), + "DESCRIPTOR": _SWAGGER, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.Swagger) + }, +) +_sym_db.RegisterMessage(Swagger) +_sym_db.RegisterMessage(Swagger.ResponsesEntry) +_sym_db.RegisterMessage(Swagger.ExtensionsEntry) + +Operation = _reflection.GeneratedProtocolMessageType( + "Operation", + (_message.Message,), + { + "ResponsesEntry": _reflection.GeneratedProtocolMessageType( + "ResponsesEntry", + (_message.Message,), + { + "DESCRIPTOR": _OPERATION_RESPONSESENTRY, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.Operation.ResponsesEntry) + }, + ), + "ExtensionsEntry": _reflection.GeneratedProtocolMessageType( + "ExtensionsEntry", + (_message.Message,), + { + "DESCRIPTOR": _OPERATION_EXTENSIONSENTRY, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.Operation.ExtensionsEntry) + }, + ), + "DESCRIPTOR": _OPERATION, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.Operation) + }, +) +_sym_db.RegisterMessage(Operation) +_sym_db.RegisterMessage(Operation.ResponsesEntry) +_sym_db.RegisterMessage(Operation.ExtensionsEntry) + +Parameters = _reflection.GeneratedProtocolMessageType( + "Parameters", + (_message.Message,), + { + "DESCRIPTOR": _PARAMETERS, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.Parameters) + }, +) +_sym_db.RegisterMessage(Parameters) + +HeaderParameter = _reflection.GeneratedProtocolMessageType( + "HeaderParameter", + (_message.Message,), + { + "DESCRIPTOR": _HEADERPARAMETER, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.HeaderParameter) + }, +) +_sym_db.RegisterMessage(HeaderParameter) + +Header = _reflection.GeneratedProtocolMessageType( + "Header", + (_message.Message,), + { + "DESCRIPTOR": _HEADER, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.Header) + }, +) +_sym_db.RegisterMessage(Header) + +Response = _reflection.GeneratedProtocolMessageType( + "Response", + (_message.Message,), + { + "HeadersEntry": _reflection.GeneratedProtocolMessageType( + "HeadersEntry", + (_message.Message,), + { + "DESCRIPTOR": _RESPONSE_HEADERSENTRY, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.Response.HeadersEntry) + }, + ), + "ExamplesEntry": _reflection.GeneratedProtocolMessageType( + "ExamplesEntry", + (_message.Message,), + { + "DESCRIPTOR": _RESPONSE_EXAMPLESENTRY, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.Response.ExamplesEntry) + }, + ), + "ExtensionsEntry": _reflection.GeneratedProtocolMessageType( + "ExtensionsEntry", + (_message.Message,), + { + "DESCRIPTOR": _RESPONSE_EXTENSIONSENTRY, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.Response.ExtensionsEntry) + }, + ), + "DESCRIPTOR": _RESPONSE, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.Response) + }, +) +_sym_db.RegisterMessage(Response) +_sym_db.RegisterMessage(Response.HeadersEntry) +_sym_db.RegisterMessage(Response.ExamplesEntry) +_sym_db.RegisterMessage(Response.ExtensionsEntry) + +Info = _reflection.GeneratedProtocolMessageType( + "Info", + (_message.Message,), + { + "ExtensionsEntry": _reflection.GeneratedProtocolMessageType( + "ExtensionsEntry", + (_message.Message,), + { + "DESCRIPTOR": _INFO_EXTENSIONSENTRY, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.Info.ExtensionsEntry) + }, + ), + "DESCRIPTOR": _INFO, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.Info) + }, +) +_sym_db.RegisterMessage(Info) +_sym_db.RegisterMessage(Info.ExtensionsEntry) + +Contact = _reflection.GeneratedProtocolMessageType( + "Contact", + (_message.Message,), + { + "DESCRIPTOR": _CONTACT, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.Contact) + }, +) +_sym_db.RegisterMessage(Contact) + +License = _reflection.GeneratedProtocolMessageType( + "License", + (_message.Message,), + { + "DESCRIPTOR": _LICENSE, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.License) + }, +) +_sym_db.RegisterMessage(License) + +ExternalDocumentation = _reflection.GeneratedProtocolMessageType( + "ExternalDocumentation", + (_message.Message,), + { + "DESCRIPTOR": _EXTERNALDOCUMENTATION, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.ExternalDocumentation) + }, +) +_sym_db.RegisterMessage(ExternalDocumentation) + +Schema = _reflection.GeneratedProtocolMessageType( + "Schema", + (_message.Message,), + { + "DESCRIPTOR": _SCHEMA, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.Schema) + }, +) +_sym_db.RegisterMessage(Schema) + +EnumSchema = _reflection.GeneratedProtocolMessageType( + "EnumSchema", + (_message.Message,), + { + "ExtensionsEntry": _reflection.GeneratedProtocolMessageType( + "ExtensionsEntry", + (_message.Message,), + { + "DESCRIPTOR": _ENUMSCHEMA_EXTENSIONSENTRY, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.EnumSchema.ExtensionsEntry) + }, + ), + "DESCRIPTOR": _ENUMSCHEMA, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.EnumSchema) + }, +) +_sym_db.RegisterMessage(EnumSchema) +_sym_db.RegisterMessage(EnumSchema.ExtensionsEntry) + +JSONSchema = _reflection.GeneratedProtocolMessageType( + "JSONSchema", + (_message.Message,), + { + "FieldConfiguration": _reflection.GeneratedProtocolMessageType( + "FieldConfiguration", + (_message.Message,), + { + "DESCRIPTOR": _JSONSCHEMA_FIELDCONFIGURATION, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.JSONSchema.FieldConfiguration) + }, + ), + "ExtensionsEntry": _reflection.GeneratedProtocolMessageType( + "ExtensionsEntry", + (_message.Message,), + { + "DESCRIPTOR": _JSONSCHEMA_EXTENSIONSENTRY, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.JSONSchema.ExtensionsEntry) + }, + ), + "DESCRIPTOR": _JSONSCHEMA, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.JSONSchema) + }, +) +_sym_db.RegisterMessage(JSONSchema) +_sym_db.RegisterMessage(JSONSchema.FieldConfiguration) +_sym_db.RegisterMessage(JSONSchema.ExtensionsEntry) + +Tag = _reflection.GeneratedProtocolMessageType( + "Tag", + (_message.Message,), + { + "ExtensionsEntry": _reflection.GeneratedProtocolMessageType( + "ExtensionsEntry", + (_message.Message,), + { + "DESCRIPTOR": _TAG_EXTENSIONSENTRY, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.Tag.ExtensionsEntry) + }, + ), + "DESCRIPTOR": _TAG, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.Tag) + }, +) +_sym_db.RegisterMessage(Tag) +_sym_db.RegisterMessage(Tag.ExtensionsEntry) + +SecurityDefinitions = _reflection.GeneratedProtocolMessageType( + "SecurityDefinitions", + (_message.Message,), + { + "SecurityEntry": _reflection.GeneratedProtocolMessageType( + "SecurityEntry", + (_message.Message,), + { + "DESCRIPTOR": _SECURITYDEFINITIONS_SECURITYENTRY, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.SecurityDefinitions.SecurityEntry) + }, + ), + "DESCRIPTOR": _SECURITYDEFINITIONS, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.SecurityDefinitions) + }, +) +_sym_db.RegisterMessage(SecurityDefinitions) +_sym_db.RegisterMessage(SecurityDefinitions.SecurityEntry) + +SecurityScheme = _reflection.GeneratedProtocolMessageType( + "SecurityScheme", + (_message.Message,), + { + "ExtensionsEntry": _reflection.GeneratedProtocolMessageType( + "ExtensionsEntry", + (_message.Message,), + { + "DESCRIPTOR": _SECURITYSCHEME_EXTENSIONSENTRY, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.SecurityScheme.ExtensionsEntry) + }, + ), + "DESCRIPTOR": _SECURITYSCHEME, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.SecurityScheme) + }, +) +_sym_db.RegisterMessage(SecurityScheme) +_sym_db.RegisterMessage(SecurityScheme.ExtensionsEntry) + +SecurityRequirement = _reflection.GeneratedProtocolMessageType( + "SecurityRequirement", + (_message.Message,), + { + "SecurityRequirementValue": _reflection.GeneratedProtocolMessageType( + "SecurityRequirementValue", + (_message.Message,), + { + "DESCRIPTOR": _SECURITYREQUIREMENT_SECURITYREQUIREMENTVALUE, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.SecurityRequirement.SecurityRequirementValue) + }, + ), + "SecurityRequirementEntry": _reflection.GeneratedProtocolMessageType( + "SecurityRequirementEntry", + (_message.Message,), + { + "DESCRIPTOR": _SECURITYREQUIREMENT_SECURITYREQUIREMENTENTRY, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.SecurityRequirement.SecurityRequirementEntry) + }, + ), + "DESCRIPTOR": _SECURITYREQUIREMENT, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.SecurityRequirement) + }, +) +_sym_db.RegisterMessage(SecurityRequirement) +_sym_db.RegisterMessage(SecurityRequirement.SecurityRequirementValue) +_sym_db.RegisterMessage(SecurityRequirement.SecurityRequirementEntry) + +Scopes = _reflection.GeneratedProtocolMessageType( + "Scopes", + (_message.Message,), + { + "ScopeEntry": _reflection.GeneratedProtocolMessageType( + "ScopeEntry", + (_message.Message,), + { + "DESCRIPTOR": _SCOPES_SCOPEENTRY, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.Scopes.ScopeEntry) + }, + ), + "DESCRIPTOR": _SCOPES, + "__module__": "protoc_gen_openapiv2.options.openapiv2_pb2", + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_openapiv2.options.Scopes) + }, +) +_sym_db.RegisterMessage(Scopes) +_sym_db.RegisterMessage(Scopes.ScopeEntry) + +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = ( + b"ZFgithub.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/options" + ) + _SWAGGER_RESPONSESENTRY._options = None + _SWAGGER_RESPONSESENTRY._serialized_options = b"8\001" + _SWAGGER_EXTENSIONSENTRY._options = None + _SWAGGER_EXTENSIONSENTRY._serialized_options = b"8\001" + _OPERATION_RESPONSESENTRY._options = None + _OPERATION_RESPONSESENTRY._serialized_options = b"8\001" + _OPERATION_EXTENSIONSENTRY._options = None + _OPERATION_EXTENSIONSENTRY._serialized_options = b"8\001" + _RESPONSE_HEADERSENTRY._options = None + _RESPONSE_HEADERSENTRY._serialized_options = b"8\001" + _RESPONSE_EXAMPLESENTRY._options = None + _RESPONSE_EXAMPLESENTRY._serialized_options = b"8\001" + _RESPONSE_EXTENSIONSENTRY._options = None + _RESPONSE_EXTENSIONSENTRY._serialized_options = b"8\001" + _INFO_EXTENSIONSENTRY._options = None + _INFO_EXTENSIONSENTRY._serialized_options = b"8\001" + _ENUMSCHEMA_EXTENSIONSENTRY._options = None + _ENUMSCHEMA_EXTENSIONSENTRY._serialized_options = b"8\001" + _JSONSCHEMA_EXTENSIONSENTRY._options = None + _JSONSCHEMA_EXTENSIONSENTRY._serialized_options = b"8\001" + _TAG_EXTENSIONSENTRY._options = None + _TAG_EXTENSIONSENTRY._serialized_options = b"8\001" + _SECURITYDEFINITIONS_SECURITYENTRY._options = None + _SECURITYDEFINITIONS_SECURITYENTRY._serialized_options = b"8\001" + _SECURITYSCHEME_EXTENSIONSENTRY._options = None + _SECURITYSCHEME_EXTENSIONSENTRY._serialized_options = b"8\001" + _SECURITYREQUIREMENT_SECURITYREQUIREMENTENTRY._options = None + _SECURITYREQUIREMENT_SECURITYREQUIREMENTENTRY._serialized_options = b"8\001" + _SCOPES_SCOPEENTRY._options = None + _SCOPES_SCOPEENTRY._serialized_options = b"8\001" + _SCHEME._serialized_start = 6978 + _SCHEME._serialized_end = 7037 + _SWAGGER._serialized_start = 122 + _SWAGGER._serialized_end = 1039 + _SWAGGER_RESPONSESENTRY._serialized_start = 851 + _SWAGGER_RESPONSESENTRY._serialized_end = 952 + _SWAGGER_EXTENSIONSENTRY._serialized_start = 954 + _SWAGGER_EXTENSIONSENTRY._serialized_end = 1027 + _OPERATION._serialized_start = 1042 + _OPERATION._serialized_end = 1859 + _OPERATION_RESPONSESENTRY._serialized_start = 851 + _OPERATION_RESPONSESENTRY._serialized_end = 952 + _OPERATION_EXTENSIONSENTRY._serialized_start = 954 + _OPERATION_EXTENSIONSENTRY._serialized_end = 1027 + _PARAMETERS._serialized_start = 1861 + _PARAMETERS._serialized_end = 1950 + _HEADERPARAMETER._serialized_start = 1953 + _HEADERPARAMETER._serialized_end = 2201 + _HEADERPARAMETER_TYPE._serialized_start = 2120 + _HEADERPARAMETER_TYPE._serialized_end = 2189 + _HEADER._serialized_start = 2204 + _HEADER._serialized_end = 2375 + _RESPONSE._serialized_start = 2378 + _RESPONSE._serialized_end = 2956 + _RESPONSE_HEADERSENTRY._serialized_start = 2735 + _RESPONSE_HEADERSENTRY._serialized_end = 2832 + _RESPONSE_EXAMPLESENTRY._serialized_start = 2834 + _RESPONSE_EXAMPLESENTRY._serialized_end = 2881 + _RESPONSE_EXTENSIONSENTRY._serialized_start = 954 + _RESPONSE_EXTENSIONSENTRY._serialized_end = 1027 + _INFO._serialized_start = 2959 + _INFO._serialized_end = 3342 + _INFO_EXTENSIONSENTRY._serialized_start = 954 + _INFO_EXTENSIONSENTRY._serialized_end = 1027 + _CONTACT._serialized_start = 3344 + _CONTACT._serialized_end = 3395 + _LICENSE._serialized_start = 3397 + _LICENSE._serialized_end = 3433 + _EXTERNALDOCUMENTATION._serialized_start = 3435 + _EXTERNALDOCUMENTATION._serialized_end = 3492 + _SCHEMA._serialized_start = 3495 + _SCHEMA._serialized_end = 3733 + _ENUMSCHEMA._serialized_start = 3736 + _ENUMSCHEMA._serialized_end = 4123 + _ENUMSCHEMA_EXTENSIONSENTRY._serialized_start = 954 + _ENUMSCHEMA_EXTENSIONSENTRY._serialized_end = 1027 + _JSONSCHEMA._serialized_start = 4126 + _JSONSCHEMA._serialized_end = 5184 + _JSONSCHEMA_FIELDCONFIGURATION._serialized_start = 4865 + _JSONSCHEMA_FIELDCONFIGURATION._serialized_end = 4910 + _JSONSCHEMA_EXTENSIONSENTRY._serialized_start = 954 + _JSONSCHEMA_EXTENSIONSENTRY._serialized_end = 1027 + _JSONSCHEMA_JSONSCHEMASIMPLETYPES._serialized_start = 4987 + _JSONSCHEMA_JSONSCHEMASIMPLETYPES._serialized_end = 5106 + _TAG._serialized_start = 5187 + _TAG._serialized_end = 5475 + _TAG_EXTENSIONSENTRY._serialized_start = 954 + _TAG_EXTENSIONSENTRY._serialized_end = 1027 + _SECURITYDEFINITIONS._serialized_start = 5478 + _SECURITYDEFINITIONS._serialized_end = 5703 + _SECURITYDEFINITIONS_SECURITYENTRY._serialized_start = 5597 + _SECURITYDEFINITIONS_SECURITYENTRY._serialized_end = 5703 + _SECURITYSCHEME._serialized_start = 5706 + _SECURITYSCHEME._serialized_end = 6506 + _SECURITYSCHEME_EXTENSIONSENTRY._serialized_start = 954 + _SECURITYSCHEME_EXTENSIONSENTRY._serialized_end = 1027 + _SECURITYSCHEME_TYPE._serialized_start = 6272 + _SECURITYSCHEME_TYPE._serialized_end = 6347 + _SECURITYSCHEME_IN._serialized_start = 6349 + _SECURITYSCHEME_IN._serialized_end = 6398 + _SECURITYSCHEME_FLOW._serialized_start = 6400 + _SECURITYSCHEME_FLOW._serialized_end = 6506 + _SECURITYREQUIREMENT._serialized_start = 6509 + _SECURITYREQUIREMENT._serialized_end = 6842 + _SECURITYREQUIREMENT_SECURITYREQUIREMENTVALUE._serialized_start = 6651 + _SECURITYREQUIREMENT_SECURITYREQUIREMENTVALUE._serialized_end = 6692 + _SECURITYREQUIREMENT_SECURITYREQUIREMENTENTRY._serialized_start = 6695 + _SECURITYREQUIREMENT_SECURITYREQUIREMENTENTRY._serialized_end = 6842 + _SCOPES._serialized_start = 6845 + _SCOPES._serialized_end = 6976 + _SCOPES_SCOPEENTRY._serialized_start = 6932 + _SCOPES_SCOPEENTRY._serialized_end = 6976 +# @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/dependencies/protoc_gen_openapiv2/options/openapiv2_pb2.pyi b/temporalio/api/dependencies/protoc_gen_openapiv2/options/openapiv2_pb2.pyi new file mode 100644 index 000000000..458d881f9 --- /dev/null +++ b/temporalio/api/dependencies/protoc_gen_openapiv2/options/openapiv2_pb2.pyi @@ -0,0 +1,2102 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" + +import builtins +import collections.abc +import sys +import typing + +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +import google.protobuf.struct_pb2 + +if sys.version_info >= (3, 10): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class _Scheme: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _SchemeEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_Scheme.ValueType], + builtins.type, +): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + UNKNOWN: _Scheme.ValueType # 0 + HTTP: _Scheme.ValueType # 1 + HTTPS: _Scheme.ValueType # 2 + WS: _Scheme.ValueType # 3 + WSS: _Scheme.ValueType # 4 + +class Scheme(_Scheme, metaclass=_SchemeEnumTypeWrapper): + """Scheme describes the schemes supported by the OpenAPI Swagger + and Operation objects. + """ + +UNKNOWN: Scheme.ValueType # 0 +HTTP: Scheme.ValueType # 1 +HTTPS: Scheme.ValueType # 2 +WS: Scheme.ValueType # 3 +WSS: Scheme.ValueType # 4 +global___Scheme = Scheme + +class Swagger(google.protobuf.message.Message): + """`Swagger` is a representation of OpenAPI v2 specification's Swagger object. + + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#swaggerObject + + Example: + + option (grpc.gateway.temporalio.api.dependencies.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "Echo API"; + version: "1.0"; + description: ""; + contact: { + name: "gRPC-Gateway project"; + url: "https://github.com/grpc-ecosystem/grpc-gateway"; + email: "none@example.com"; + }; + license: { + name: "BSD 3-Clause License"; + url: "https://github.com/grpc-ecosystem/grpc-gateway/blob/main/LICENSE"; + }; + }; + schemes: HTTPS; + consumes: "application/json"; + produces: "application/json"; + }; + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class ResponsesEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> global___Response: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: global___Response | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["value", b"value"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["key", b"key", "value", b"value"], + ) -> None: ... + + class ExtensionsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> google.protobuf.struct_pb2.Value: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: google.protobuf.struct_pb2.Value | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["value", b"value"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["key", b"key", "value", b"value"], + ) -> None: ... + + SWAGGER_FIELD_NUMBER: builtins.int + INFO_FIELD_NUMBER: builtins.int + HOST_FIELD_NUMBER: builtins.int + BASE_PATH_FIELD_NUMBER: builtins.int + SCHEMES_FIELD_NUMBER: builtins.int + CONSUMES_FIELD_NUMBER: builtins.int + PRODUCES_FIELD_NUMBER: builtins.int + RESPONSES_FIELD_NUMBER: builtins.int + SECURITY_DEFINITIONS_FIELD_NUMBER: builtins.int + SECURITY_FIELD_NUMBER: builtins.int + TAGS_FIELD_NUMBER: builtins.int + EXTERNAL_DOCS_FIELD_NUMBER: builtins.int + EXTENSIONS_FIELD_NUMBER: builtins.int + swagger: builtins.str + """Specifies the OpenAPI Specification version being used. It can be + used by the OpenAPI UI and other clients to interpret the API listing. The + value MUST be "2.0". + """ + @property + def info(self) -> global___Info: + """Provides metadata about the API. The metadata can be used by the + clients if needed. + """ + host: builtins.str + """The host (name or ip) serving the API. This MUST be the host only and does + not include the scheme nor sub-paths. It MAY include a port. If the host is + not included, the host serving the documentation is to be used (including + the port). The host does not support path templating. + """ + base_path: builtins.str + """The base path on which the API is served, which is relative to the host. If + it is not included, the API is served directly under the host. The value + MUST start with a leading slash (/). The basePath does not support path + templating. + Note that using `base_path` does not change the endpoint paths that are + generated in the resulting OpenAPI file. If you wish to use `base_path` + with relatively generated OpenAPI paths, the `base_path` prefix must be + manually removed from your `google.api.http` paths and your code changed to + serve the API from the `base_path`. + """ + @property + def schemes( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[ + global___Scheme.ValueType + ]: + """The transfer protocol of the API. Values MUST be from the list: "http", + "https", "ws", "wss". If the schemes is not included, the default scheme to + be used is the one used to access the OpenAPI definition itself. + """ + @property + def consumes( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """A list of MIME types the APIs can consume. This is global to all APIs but + can be overridden on specific API calls. Value MUST be as described under + Mime Types. + """ + @property + def produces( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """A list of MIME types the APIs can produce. This is global to all APIs but + can be overridden on specific API calls. Value MUST be as described under + Mime Types. + """ + @property + def responses( + self, + ) -> google.protobuf.internal.containers.MessageMap[ + builtins.str, global___Response + ]: + """An object to hold responses that can be used across operations. This + property does not define global responses for all operations. + """ + @property + def security_definitions(self) -> global___SecurityDefinitions: + """Security scheme definitions that can be used across the specification.""" + @property + def security( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + global___SecurityRequirement + ]: + """A declaration of which security schemes are applied for the API as a whole. + The list of values describes alternative security schemes that can be used + (that is, there is a logical OR between the security requirements). + Individual operations can override this definition. + """ + @property + def tags( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + global___Tag + ]: + """A list of tags for API documentation control. Tags can be used for logical + grouping of operations by resources or any other qualifier. + """ + @property + def external_docs(self) -> global___ExternalDocumentation: + """Additional external documentation.""" + @property + def extensions( + self, + ) -> google.protobuf.internal.containers.MessageMap[ + builtins.str, google.protobuf.struct_pb2.Value + ]: + """Custom properties that start with "x-" such as "x-foo" used to describe + extra functionality that is not covered by the standard OpenAPI Specification. + See: https://swagger.io/docs/specification/2-0/swagger-extensions/ + """ + def __init__( + self, + *, + swagger: builtins.str = ..., + info: global___Info | None = ..., + host: builtins.str = ..., + base_path: builtins.str = ..., + schemes: collections.abc.Iterable[global___Scheme.ValueType] | None = ..., + consumes: collections.abc.Iterable[builtins.str] | None = ..., + produces: collections.abc.Iterable[builtins.str] | None = ..., + responses: collections.abc.Mapping[builtins.str, global___Response] + | None = ..., + security_definitions: global___SecurityDefinitions | None = ..., + security: collections.abc.Iterable[global___SecurityRequirement] | None = ..., + tags: collections.abc.Iterable[global___Tag] | None = ..., + external_docs: global___ExternalDocumentation | None = ..., + extensions: collections.abc.Mapping[ + builtins.str, google.protobuf.struct_pb2.Value + ] + | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "external_docs", + b"external_docs", + "info", + b"info", + "security_definitions", + b"security_definitions", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "base_path", + b"base_path", + "consumes", + b"consumes", + "extensions", + b"extensions", + "external_docs", + b"external_docs", + "host", + b"host", + "info", + b"info", + "produces", + b"produces", + "responses", + b"responses", + "schemes", + b"schemes", + "security", + b"security", + "security_definitions", + b"security_definitions", + "swagger", + b"swagger", + "tags", + b"tags", + ], + ) -> None: ... + +global___Swagger = Swagger + +class Operation(google.protobuf.message.Message): + """`Operation` is a representation of OpenAPI v2 specification's Operation object. + + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#operationObject + + Example: + + service EchoService { + rpc Echo(SimpleMessage) returns (SimpleMessage) { + option (google.api.http) = { + get: "/v1/example/echo/{id}" + }; + + option (grpc.gateway.temporalio.api.dependencies.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Get a message."; + operation_id: "getMessage"; + tags: "echo"; + responses: { + key: "200" + value: { + description: "OK"; + } + } + }; + } + } + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class ResponsesEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> global___Response: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: global___Response | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["value", b"value"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["key", b"key", "value", b"value"], + ) -> None: ... + + class ExtensionsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> google.protobuf.struct_pb2.Value: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: google.protobuf.struct_pb2.Value | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["value", b"value"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["key", b"key", "value", b"value"], + ) -> None: ... + + TAGS_FIELD_NUMBER: builtins.int + SUMMARY_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + EXTERNAL_DOCS_FIELD_NUMBER: builtins.int + OPERATION_ID_FIELD_NUMBER: builtins.int + CONSUMES_FIELD_NUMBER: builtins.int + PRODUCES_FIELD_NUMBER: builtins.int + RESPONSES_FIELD_NUMBER: builtins.int + SCHEMES_FIELD_NUMBER: builtins.int + DEPRECATED_FIELD_NUMBER: builtins.int + SECURITY_FIELD_NUMBER: builtins.int + EXTENSIONS_FIELD_NUMBER: builtins.int + PARAMETERS_FIELD_NUMBER: builtins.int + @property + def tags( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """A list of tags for API documentation control. Tags can be used for logical + grouping of operations by resources or any other qualifier. + """ + summary: builtins.str + """A short summary of what the operation does. For maximum readability in the + swagger-ui, this field SHOULD be less than 120 characters. + """ + description: builtins.str + """A verbose explanation of the operation behavior. GFM syntax can be used for + rich text representation. + """ + @property + def external_docs(self) -> global___ExternalDocumentation: + """Additional external documentation for this operation.""" + operation_id: builtins.str + """Unique string used to identify the operation. The id MUST be unique among + all operations described in the API. Tools and libraries MAY use the + operationId to uniquely identify an operation, therefore, it is recommended + to follow common programming naming conventions. + """ + @property + def consumes( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """A list of MIME types the operation can consume. This overrides the consumes + definition at the OpenAPI Object. An empty value MAY be used to clear the + global definition. Value MUST be as described under Mime Types. + """ + @property + def produces( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """A list of MIME types the operation can produce. This overrides the produces + definition at the OpenAPI Object. An empty value MAY be used to clear the + global definition. Value MUST be as described under Mime Types. + """ + @property + def responses( + self, + ) -> google.protobuf.internal.containers.MessageMap[ + builtins.str, global___Response + ]: + """The list of possible responses as they are returned from executing this + operation. + """ + @property + def schemes( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[ + global___Scheme.ValueType + ]: + """The transfer protocol for the operation. Values MUST be from the list: + "http", "https", "ws", "wss". The value overrides the OpenAPI Object + schemes definition. + """ + deprecated: builtins.bool + """Declares this operation to be deprecated. Usage of the declared operation + should be refrained. Default value is false. + """ + @property + def security( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + global___SecurityRequirement + ]: + """A declaration of which security schemes are applied for this operation. The + list of values describes alternative security schemes that can be used + (that is, there is a logical OR between the security requirements). This + definition overrides any declared top-level security. To remove a top-level + security declaration, an empty array can be used. + """ + @property + def extensions( + self, + ) -> google.protobuf.internal.containers.MessageMap[ + builtins.str, google.protobuf.struct_pb2.Value + ]: + """Custom properties that start with "x-" such as "x-foo" used to describe + extra functionality that is not covered by the standard OpenAPI Specification. + See: https://swagger.io/docs/specification/2-0/swagger-extensions/ + """ + @property + def parameters(self) -> global___Parameters: + """Custom parameters such as HTTP request headers. + See: https://swagger.io/docs/specification/2-0/describing-parameters/ + and https://swagger.io/specification/v2/#parameter-object. + """ + def __init__( + self, + *, + tags: collections.abc.Iterable[builtins.str] | None = ..., + summary: builtins.str = ..., + description: builtins.str = ..., + external_docs: global___ExternalDocumentation | None = ..., + operation_id: builtins.str = ..., + consumes: collections.abc.Iterable[builtins.str] | None = ..., + produces: collections.abc.Iterable[builtins.str] | None = ..., + responses: collections.abc.Mapping[builtins.str, global___Response] + | None = ..., + schemes: collections.abc.Iterable[global___Scheme.ValueType] | None = ..., + deprecated: builtins.bool = ..., + security: collections.abc.Iterable[global___SecurityRequirement] | None = ..., + extensions: collections.abc.Mapping[ + builtins.str, google.protobuf.struct_pb2.Value + ] + | None = ..., + parameters: global___Parameters | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "external_docs", b"external_docs", "parameters", b"parameters" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "consumes", + b"consumes", + "deprecated", + b"deprecated", + "description", + b"description", + "extensions", + b"extensions", + "external_docs", + b"external_docs", + "operation_id", + b"operation_id", + "parameters", + b"parameters", + "produces", + b"produces", + "responses", + b"responses", + "schemes", + b"schemes", + "security", + b"security", + "summary", + b"summary", + "tags", + b"tags", + ], + ) -> None: ... + +global___Operation = Operation + +class Parameters(google.protobuf.message.Message): + """`Parameters` is a representation of OpenAPI v2 specification's parameters object. + Note: This technically breaks compatibility with the OpenAPI 2 definition structure as we only + allow header parameters to be set here since we do not want users specifying custom non-header + parameters beyond those inferred from the Protobuf schema. + See: https://swagger.io/specification/v2/#parameter-object + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + HEADERS_FIELD_NUMBER: builtins.int + @property + def headers( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + global___HeaderParameter + ]: + """`Headers` is one or more HTTP header parameter. + See: https://swagger.io/docs/specification/2-0/describing-parameters/#header-parameters + """ + def __init__( + self, + *, + headers: collections.abc.Iterable[global___HeaderParameter] | None = ..., + ) -> None: ... + def ClearField( + self, field_name: typing_extensions.Literal["headers", b"headers"] + ) -> None: ... + +global___Parameters = Parameters + +class HeaderParameter(google.protobuf.message.Message): + """`HeaderParameter` a HTTP header parameter. + See: https://swagger.io/specification/v2/#parameter-object + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class _Type: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _TypeEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ + HeaderParameter._Type.ValueType + ], + builtins.type, + ): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + UNKNOWN: HeaderParameter._Type.ValueType # 0 + STRING: HeaderParameter._Type.ValueType # 1 + NUMBER: HeaderParameter._Type.ValueType # 2 + INTEGER: HeaderParameter._Type.ValueType # 3 + BOOLEAN: HeaderParameter._Type.ValueType # 4 + + class Type(_Type, metaclass=_TypeEnumTypeWrapper): + """`Type` is a supported HTTP header type. + See https://swagger.io/specification/v2/#parameterType. + """ + + UNKNOWN: HeaderParameter.Type.ValueType # 0 + STRING: HeaderParameter.Type.ValueType # 1 + NUMBER: HeaderParameter.Type.ValueType # 2 + INTEGER: HeaderParameter.Type.ValueType # 3 + BOOLEAN: HeaderParameter.Type.ValueType # 4 + + NAME_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + TYPE_FIELD_NUMBER: builtins.int + FORMAT_FIELD_NUMBER: builtins.int + REQUIRED_FIELD_NUMBER: builtins.int + name: builtins.str + """`Name` is the header name.""" + description: builtins.str + """`Description` is a short description of the header.""" + type: global___HeaderParameter.Type.ValueType + """`Type` is the type of the object. The value MUST be one of "string", "number", "integer", or "boolean". The "array" type is not supported. + See: https://swagger.io/specification/v2/#parameterType. + """ + format: builtins.str + """`Format` The extending format for the previously mentioned type.""" + required: builtins.bool + """`Required` indicates if the header is optional""" + def __init__( + self, + *, + name: builtins.str = ..., + description: builtins.str = ..., + type: global___HeaderParameter.Type.ValueType = ..., + format: builtins.str = ..., + required: builtins.bool = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "description", + b"description", + "format", + b"format", + "name", + b"name", + "required", + b"required", + "type", + b"type", + ], + ) -> None: ... + +global___HeaderParameter = HeaderParameter + +class Header(google.protobuf.message.Message): + """`Header` is a representation of OpenAPI v2 specification's Header object. + + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#headerObject + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + DESCRIPTION_FIELD_NUMBER: builtins.int + TYPE_FIELD_NUMBER: builtins.int + FORMAT_FIELD_NUMBER: builtins.int + DEFAULT_FIELD_NUMBER: builtins.int + PATTERN_FIELD_NUMBER: builtins.int + description: builtins.str + """`Description` is a short description of the header.""" + type: builtins.str + """The type of the object. The value MUST be one of "string", "number", "integer", or "boolean". The "array" type is not supported.""" + format: builtins.str + """`Format` The extending format for the previously mentioned type.""" + default: builtins.str + """`Default` Declares the value of the header that the server will use if none is provided. + See: https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-6.2. + Unlike JSON Schema this value MUST conform to the defined type for the header. + """ + pattern: builtins.str + """'Pattern' See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.2.3.""" + def __init__( + self, + *, + description: builtins.str = ..., + type: builtins.str = ..., + format: builtins.str = ..., + default: builtins.str = ..., + pattern: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "default", + b"default", + "description", + b"description", + "format", + b"format", + "pattern", + b"pattern", + "type", + b"type", + ], + ) -> None: ... + +global___Header = Header + +class Response(google.protobuf.message.Message): + """`Response` is a representation of OpenAPI v2 specification's Response object. + + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#responseObject + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class HeadersEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> global___Header: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: global___Header | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["value", b"value"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["key", b"key", "value", b"value"], + ) -> None: ... + + class ExamplesEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal["key", b"key", "value", b"value"], + ) -> None: ... + + class ExtensionsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> google.protobuf.struct_pb2.Value: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: google.protobuf.struct_pb2.Value | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["value", b"value"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["key", b"key", "value", b"value"], + ) -> None: ... + + DESCRIPTION_FIELD_NUMBER: builtins.int + SCHEMA_FIELD_NUMBER: builtins.int + HEADERS_FIELD_NUMBER: builtins.int + EXAMPLES_FIELD_NUMBER: builtins.int + EXTENSIONS_FIELD_NUMBER: builtins.int + description: builtins.str + """`Description` is a short description of the response. + GFM syntax can be used for rich text representation. + """ + @property + def schema(self) -> global___Schema: + """`Schema` optionally defines the structure of the response. + If `Schema` is not provided, it means there is no content to the response. + """ + @property + def headers( + self, + ) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___Header]: + """`Headers` A list of headers that are sent with the response. + `Header` name is expected to be a string in the canonical format of the MIME header key + See: https://golang.org/pkg/net/textproto/#CanonicalMIMEHeaderKey + """ + @property + def examples( + self, + ) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: + """`Examples` gives per-mimetype response examples. + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#example-object + """ + @property + def extensions( + self, + ) -> google.protobuf.internal.containers.MessageMap[ + builtins.str, google.protobuf.struct_pb2.Value + ]: + """Custom properties that start with "x-" such as "x-foo" used to describe + extra functionality that is not covered by the standard OpenAPI Specification. + See: https://swagger.io/docs/specification/2-0/swagger-extensions/ + """ + def __init__( + self, + *, + description: builtins.str = ..., + schema: global___Schema | None = ..., + headers: collections.abc.Mapping[builtins.str, global___Header] | None = ..., + examples: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + extensions: collections.abc.Mapping[ + builtins.str, google.protobuf.struct_pb2.Value + ] + | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["schema", b"schema"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "description", + b"description", + "examples", + b"examples", + "extensions", + b"extensions", + "headers", + b"headers", + "schema", + b"schema", + ], + ) -> None: ... + +global___Response = Response + +class Info(google.protobuf.message.Message): + """`Info` is a representation of OpenAPI v2 specification's Info object. + + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#infoObject + + Example: + + option (grpc.gateway.temporalio.api.dependencies.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "Echo API"; + version: "1.0"; + description: ""; + contact: { + name: "gRPC-Gateway project"; + url: "https://github.com/grpc-ecosystem/grpc-gateway"; + email: "none@example.com"; + }; + license: { + name: "BSD 3-Clause License"; + url: "https://github.com/grpc-ecosystem/grpc-gateway/blob/main/LICENSE"; + }; + }; + ... + }; + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class ExtensionsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> google.protobuf.struct_pb2.Value: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: google.protobuf.struct_pb2.Value | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["value", b"value"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["key", b"key", "value", b"value"], + ) -> None: ... + + TITLE_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + TERMS_OF_SERVICE_FIELD_NUMBER: builtins.int + CONTACT_FIELD_NUMBER: builtins.int + LICENSE_FIELD_NUMBER: builtins.int + VERSION_FIELD_NUMBER: builtins.int + EXTENSIONS_FIELD_NUMBER: builtins.int + title: builtins.str + """The title of the application.""" + description: builtins.str + """A short description of the application. GFM syntax can be used for rich + text representation. + """ + terms_of_service: builtins.str + """The Terms of Service for the API.""" + @property + def contact(self) -> global___Contact: + """The contact information for the exposed API.""" + @property + def license(self) -> global___License: + """The license information for the exposed API.""" + version: builtins.str + """Provides the version of the application API (not to be confused + with the specification version). + """ + @property + def extensions( + self, + ) -> google.protobuf.internal.containers.MessageMap[ + builtins.str, google.protobuf.struct_pb2.Value + ]: + """Custom properties that start with "x-" such as "x-foo" used to describe + extra functionality that is not covered by the standard OpenAPI Specification. + See: https://swagger.io/docs/specification/2-0/swagger-extensions/ + """ + def __init__( + self, + *, + title: builtins.str = ..., + description: builtins.str = ..., + terms_of_service: builtins.str = ..., + contact: global___Contact | None = ..., + license: global___License | None = ..., + version: builtins.str = ..., + extensions: collections.abc.Mapping[ + builtins.str, google.protobuf.struct_pb2.Value + ] + | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "contact", b"contact", "license", b"license" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "contact", + b"contact", + "description", + b"description", + "extensions", + b"extensions", + "license", + b"license", + "terms_of_service", + b"terms_of_service", + "title", + b"title", + "version", + b"version", + ], + ) -> None: ... + +global___Info = Info + +class Contact(google.protobuf.message.Message): + """`Contact` is a representation of OpenAPI v2 specification's Contact object. + + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#contactObject + + Example: + + option (grpc.gateway.temporalio.api.dependencies.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + ... + contact: { + name: "gRPC-Gateway project"; + url: "https://github.com/grpc-ecosystem/grpc-gateway"; + email: "none@example.com"; + }; + ... + }; + ... + }; + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + URL_FIELD_NUMBER: builtins.int + EMAIL_FIELD_NUMBER: builtins.int + name: builtins.str + """The identifying name of the contact person/organization.""" + url: builtins.str + """The URL pointing to the contact information. MUST be in the format of a + URL. + """ + email: builtins.str + """The email address of the contact person/organization. MUST be in the format + of an email address. + """ + def __init__( + self, + *, + name: builtins.str = ..., + url: builtins.str = ..., + email: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "email", b"email", "name", b"name", "url", b"url" + ], + ) -> None: ... + +global___Contact = Contact + +class License(google.protobuf.message.Message): + """`License` is a representation of OpenAPI v2 specification's License object. + + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#licenseObject + + Example: + + option (grpc.gateway.temporalio.api.dependencies.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + ... + license: { + name: "BSD 3-Clause License"; + url: "https://github.com/grpc-ecosystem/grpc-gateway/blob/main/LICENSE"; + }; + ... + }; + ... + }; + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + URL_FIELD_NUMBER: builtins.int + name: builtins.str + """The license name used for the API.""" + url: builtins.str + """A URL to the license used for the API. MUST be in the format of a URL.""" + def __init__( + self, + *, + name: builtins.str = ..., + url: builtins.str = ..., + ) -> None: ... + def ClearField( + self, field_name: typing_extensions.Literal["name", b"name", "url", b"url"] + ) -> None: ... + +global___License = License + +class ExternalDocumentation(google.protobuf.message.Message): + """`ExternalDocumentation` is a representation of OpenAPI v2 specification's + ExternalDocumentation object. + + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#externalDocumentationObject + + Example: + + option (grpc.gateway.temporalio.api.dependencies.protoc_gen_openapiv2.options.openapiv2_swagger) = { + ... + external_docs: { + description: "More about gRPC-Gateway"; + url: "https://github.com/grpc-ecosystem/grpc-gateway"; + } + ... + }; + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + DESCRIPTION_FIELD_NUMBER: builtins.int + URL_FIELD_NUMBER: builtins.int + description: builtins.str + """A short description of the target documentation. GFM syntax can be used for + rich text representation. + """ + url: builtins.str + """The URL for the target documentation. Value MUST be in the format + of a URL. + """ + def __init__( + self, + *, + description: builtins.str = ..., + url: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "description", b"description", "url", b"url" + ], + ) -> None: ... + +global___ExternalDocumentation = ExternalDocumentation + +class Schema(google.protobuf.message.Message): + """`Schema` is a representation of OpenAPI v2 specification's Schema object. + + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#schemaObject + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + JSON_SCHEMA_FIELD_NUMBER: builtins.int + DISCRIMINATOR_FIELD_NUMBER: builtins.int + READ_ONLY_FIELD_NUMBER: builtins.int + EXTERNAL_DOCS_FIELD_NUMBER: builtins.int + EXAMPLE_FIELD_NUMBER: builtins.int + @property + def json_schema(self) -> global___JSONSchema: ... + discriminator: builtins.str + """Adds support for polymorphism. The discriminator is the schema property + name that is used to differentiate between other schema that inherit this + schema. The property name used MUST be defined at this schema and it MUST + be in the required property list. When used, the value MUST be the name of + this schema or any schema that inherits it. + """ + read_only: builtins.bool + """Relevant only for Schema "properties" definitions. Declares the property as + "read only". This means that it MAY be sent as part of a response but MUST + NOT be sent as part of the request. Properties marked as readOnly being + true SHOULD NOT be in the required list of the defined schema. Default + value is false. + """ + @property + def external_docs(self) -> global___ExternalDocumentation: + """Additional external documentation for this schema.""" + example: builtins.str + """A free-form property to include an example of an instance for this schema in JSON. + This is copied verbatim to the output. + """ + def __init__( + self, + *, + json_schema: global___JSONSchema | None = ..., + discriminator: builtins.str = ..., + read_only: builtins.bool = ..., + external_docs: global___ExternalDocumentation | None = ..., + example: builtins.str = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "external_docs", b"external_docs", "json_schema", b"json_schema" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "discriminator", + b"discriminator", + "example", + b"example", + "external_docs", + b"external_docs", + "json_schema", + b"json_schema", + "read_only", + b"read_only", + ], + ) -> None: ... + +global___Schema = Schema + +class EnumSchema(google.protobuf.message.Message): + """`EnumSchema` is subset of fields from the OpenAPI v2 specification's Schema object. + Only fields that are applicable to Enums are included + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#schemaObject + + Example: + + option (grpc.gateway.temporalio.api.dependencies.protoc_gen_openapiv2.options.openapiv2_enum) = { + ... + title: "MyEnum"; + description:"This is my nice enum"; + example: "ZERO"; + required: true; + ... + }; + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class ExtensionsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> google.protobuf.struct_pb2.Value: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: google.protobuf.struct_pb2.Value | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["value", b"value"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["key", b"key", "value", b"value"], + ) -> None: ... + + DESCRIPTION_FIELD_NUMBER: builtins.int + DEFAULT_FIELD_NUMBER: builtins.int + TITLE_FIELD_NUMBER: builtins.int + REQUIRED_FIELD_NUMBER: builtins.int + READ_ONLY_FIELD_NUMBER: builtins.int + EXTERNAL_DOCS_FIELD_NUMBER: builtins.int + EXAMPLE_FIELD_NUMBER: builtins.int + REF_FIELD_NUMBER: builtins.int + EXTENSIONS_FIELD_NUMBER: builtins.int + description: builtins.str + """A short description of the schema.""" + default: builtins.str + title: builtins.str + """The title of the schema.""" + required: builtins.bool + read_only: builtins.bool + @property + def external_docs(self) -> global___ExternalDocumentation: + """Additional external documentation for this schema.""" + example: builtins.str + ref: builtins.str + """Ref is used to define an external reference to include in the message. + This could be a fully qualified proto message reference, and that type must + be imported into the protofile. If no message is identified, the Ref will + be used verbatim in the output. + For example: + `ref: ".google.protobuf.Timestamp"`. + """ + @property + def extensions( + self, + ) -> google.protobuf.internal.containers.MessageMap[ + builtins.str, google.protobuf.struct_pb2.Value + ]: + """Custom properties that start with "x-" such as "x-foo" used to describe + extra functionality that is not covered by the standard OpenAPI Specification. + See: https://swagger.io/docs/specification/2-0/swagger-extensions/ + """ + def __init__( + self, + *, + description: builtins.str = ..., + default: builtins.str = ..., + title: builtins.str = ..., + required: builtins.bool = ..., + read_only: builtins.bool = ..., + external_docs: global___ExternalDocumentation | None = ..., + example: builtins.str = ..., + ref: builtins.str = ..., + extensions: collections.abc.Mapping[ + builtins.str, google.protobuf.struct_pb2.Value + ] + | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["external_docs", b"external_docs"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "default", + b"default", + "description", + b"description", + "example", + b"example", + "extensions", + b"extensions", + "external_docs", + b"external_docs", + "read_only", + b"read_only", + "ref", + b"ref", + "required", + b"required", + "title", + b"title", + ], + ) -> None: ... + +global___EnumSchema = EnumSchema + +class JSONSchema(google.protobuf.message.Message): + """`JSONSchema` represents properties from JSON Schema taken, and as used, in + the OpenAPI v2 spec. + + This includes changes made by OpenAPI v2. + + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#schemaObject + + See also: https://cswr.github.io/JsonSchema/spec/basic_types/, + https://github.com/json-schema-org/json-schema-spec/blob/master/schema.json + + Example: + + message SimpleMessage { + option (grpc.gateway.temporalio.api.dependencies.protoc_gen_openapiv2.options.openapiv2_schema) = { + json_schema: { + title: "SimpleMessage" + description: "A simple message." + required: ["id"] + } + }; + + // Id represents the message identifier. + string id = 1; [ + (grpc.gateway.temporalio.api.dependencies.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "The unique identifier of the simple message." + }]; + } + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class _JSONSchemaSimpleTypes: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _JSONSchemaSimpleTypesEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ + JSONSchema._JSONSchemaSimpleTypes.ValueType + ], + builtins.type, + ): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + UNKNOWN: JSONSchema._JSONSchemaSimpleTypes.ValueType # 0 + ARRAY: JSONSchema._JSONSchemaSimpleTypes.ValueType # 1 + BOOLEAN: JSONSchema._JSONSchemaSimpleTypes.ValueType # 2 + INTEGER: JSONSchema._JSONSchemaSimpleTypes.ValueType # 3 + NULL: JSONSchema._JSONSchemaSimpleTypes.ValueType # 4 + NUMBER: JSONSchema._JSONSchemaSimpleTypes.ValueType # 5 + OBJECT: JSONSchema._JSONSchemaSimpleTypes.ValueType # 6 + STRING: JSONSchema._JSONSchemaSimpleTypes.ValueType # 7 + + class JSONSchemaSimpleTypes( + _JSONSchemaSimpleTypes, metaclass=_JSONSchemaSimpleTypesEnumTypeWrapper + ): ... + UNKNOWN: JSONSchema.JSONSchemaSimpleTypes.ValueType # 0 + ARRAY: JSONSchema.JSONSchemaSimpleTypes.ValueType # 1 + BOOLEAN: JSONSchema.JSONSchemaSimpleTypes.ValueType # 2 + INTEGER: JSONSchema.JSONSchemaSimpleTypes.ValueType # 3 + NULL: JSONSchema.JSONSchemaSimpleTypes.ValueType # 4 + NUMBER: JSONSchema.JSONSchemaSimpleTypes.ValueType # 5 + OBJECT: JSONSchema.JSONSchemaSimpleTypes.ValueType # 6 + STRING: JSONSchema.JSONSchemaSimpleTypes.ValueType # 7 + + class FieldConfiguration(google.protobuf.message.Message): + """'FieldConfiguration' provides additional field level properties used when generating the OpenAPI v2 file. + These properties are not defined by OpenAPIv2, but they are used to control the generation. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + PATH_PARAM_NAME_FIELD_NUMBER: builtins.int + path_param_name: builtins.str + """Alternative parameter name when used as path parameter. If set, this will + be used as the complete parameter name when this field is used as a path + parameter. Use this to avoid having auto generated path parameter names + for overlapping paths. + """ + def __init__( + self, + *, + path_param_name: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "path_param_name", b"path_param_name" + ], + ) -> None: ... + + class ExtensionsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> google.protobuf.struct_pb2.Value: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: google.protobuf.struct_pb2.Value | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["value", b"value"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["key", b"key", "value", b"value"], + ) -> None: ... + + REF_FIELD_NUMBER: builtins.int + TITLE_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + DEFAULT_FIELD_NUMBER: builtins.int + READ_ONLY_FIELD_NUMBER: builtins.int + EXAMPLE_FIELD_NUMBER: builtins.int + MULTIPLE_OF_FIELD_NUMBER: builtins.int + MAXIMUM_FIELD_NUMBER: builtins.int + EXCLUSIVE_MAXIMUM_FIELD_NUMBER: builtins.int + MINIMUM_FIELD_NUMBER: builtins.int + EXCLUSIVE_MINIMUM_FIELD_NUMBER: builtins.int + MAX_LENGTH_FIELD_NUMBER: builtins.int + MIN_LENGTH_FIELD_NUMBER: builtins.int + PATTERN_FIELD_NUMBER: builtins.int + MAX_ITEMS_FIELD_NUMBER: builtins.int + MIN_ITEMS_FIELD_NUMBER: builtins.int + UNIQUE_ITEMS_FIELD_NUMBER: builtins.int + MAX_PROPERTIES_FIELD_NUMBER: builtins.int + MIN_PROPERTIES_FIELD_NUMBER: builtins.int + REQUIRED_FIELD_NUMBER: builtins.int + ARRAY_FIELD_NUMBER: builtins.int + TYPE_FIELD_NUMBER: builtins.int + FORMAT_FIELD_NUMBER: builtins.int + ENUM_FIELD_NUMBER: builtins.int + FIELD_CONFIGURATION_FIELD_NUMBER: builtins.int + EXTENSIONS_FIELD_NUMBER: builtins.int + ref: builtins.str + """Ref is used to define an external reference to include in the message. + This could be a fully qualified proto message reference, and that type must + be imported into the protofile. If no message is identified, the Ref will + be used verbatim in the output. + For example: + `ref: ".google.protobuf.Timestamp"`. + """ + title: builtins.str + """The title of the schema.""" + description: builtins.str + """A short description of the schema.""" + default: builtins.str + read_only: builtins.bool + example: builtins.str + """A free-form property to include a JSON example of this field. This is copied + verbatim to the output swagger.json. Quotes must be escaped. + This property is the same for 2.0 and 3.0.0 https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/3.0.0.md#schemaObject https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#schemaObject + """ + multiple_of: builtins.float + maximum: builtins.float + """Maximum represents an inclusive upper limit for a numeric instance. The + value of MUST be a number, + """ + exclusive_maximum: builtins.bool + minimum: builtins.float + """minimum represents an inclusive lower limit for a numeric instance. The + value of MUST be a number, + """ + exclusive_minimum: builtins.bool + max_length: builtins.int + min_length: builtins.int + pattern: builtins.str + max_items: builtins.int + min_items: builtins.int + unique_items: builtins.bool + max_properties: builtins.int + min_properties: builtins.int + @property + def required( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[ + builtins.str + ]: ... + @property + def array( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """Items in 'array' must be unique.""" + @property + def type( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[ + global___JSONSchema.JSONSchemaSimpleTypes.ValueType + ]: ... + format: builtins.str + """`Format`""" + @property + def enum( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """Items in `enum` must be unique https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.5.1""" + @property + def field_configuration(self) -> global___JSONSchema.FieldConfiguration: + """Additional field level properties used when generating the OpenAPI v2 file.""" + @property + def extensions( + self, + ) -> google.protobuf.internal.containers.MessageMap[ + builtins.str, google.protobuf.struct_pb2.Value + ]: + """Custom properties that start with "x-" such as "x-foo" used to describe + extra functionality that is not covered by the standard OpenAPI Specification. + See: https://swagger.io/docs/specification/2-0/swagger-extensions/ + """ + def __init__( + self, + *, + ref: builtins.str = ..., + title: builtins.str = ..., + description: builtins.str = ..., + default: builtins.str = ..., + read_only: builtins.bool = ..., + example: builtins.str = ..., + multiple_of: builtins.float = ..., + maximum: builtins.float = ..., + exclusive_maximum: builtins.bool = ..., + minimum: builtins.float = ..., + exclusive_minimum: builtins.bool = ..., + max_length: builtins.int = ..., + min_length: builtins.int = ..., + pattern: builtins.str = ..., + max_items: builtins.int = ..., + min_items: builtins.int = ..., + unique_items: builtins.bool = ..., + max_properties: builtins.int = ..., + min_properties: builtins.int = ..., + required: collections.abc.Iterable[builtins.str] | None = ..., + array: collections.abc.Iterable[builtins.str] | None = ..., + type: collections.abc.Iterable[ + global___JSONSchema.JSONSchemaSimpleTypes.ValueType + ] + | None = ..., + format: builtins.str = ..., + enum: collections.abc.Iterable[builtins.str] | None = ..., + field_configuration: global___JSONSchema.FieldConfiguration | None = ..., + extensions: collections.abc.Mapping[ + builtins.str, google.protobuf.struct_pb2.Value + ] + | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "field_configuration", b"field_configuration" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "array", + b"array", + "default", + b"default", + "description", + b"description", + "enum", + b"enum", + "example", + b"example", + "exclusive_maximum", + b"exclusive_maximum", + "exclusive_minimum", + b"exclusive_minimum", + "extensions", + b"extensions", + "field_configuration", + b"field_configuration", + "format", + b"format", + "max_items", + b"max_items", + "max_length", + b"max_length", + "max_properties", + b"max_properties", + "maximum", + b"maximum", + "min_items", + b"min_items", + "min_length", + b"min_length", + "min_properties", + b"min_properties", + "minimum", + b"minimum", + "multiple_of", + b"multiple_of", + "pattern", + b"pattern", + "read_only", + b"read_only", + "ref", + b"ref", + "required", + b"required", + "title", + b"title", + "type", + b"type", + "unique_items", + b"unique_items", + ], + ) -> None: ... + +global___JSONSchema = JSONSchema + +class Tag(google.protobuf.message.Message): + """`Tag` is a representation of OpenAPI v2 specification's Tag object. + + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#tagObject + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class ExtensionsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> google.protobuf.struct_pb2.Value: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: google.protobuf.struct_pb2.Value | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["value", b"value"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["key", b"key", "value", b"value"], + ) -> None: ... + + NAME_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + EXTERNAL_DOCS_FIELD_NUMBER: builtins.int + EXTENSIONS_FIELD_NUMBER: builtins.int + name: builtins.str + """The name of the tag. Use it to allow override of the name of a + global Tag object, then use that name to reference the tag throughout the + OpenAPI file. + """ + description: builtins.str + """A short description for the tag. GFM syntax can be used for rich text + representation. + """ + @property + def external_docs(self) -> global___ExternalDocumentation: + """Additional external documentation for this tag.""" + @property + def extensions( + self, + ) -> google.protobuf.internal.containers.MessageMap[ + builtins.str, google.protobuf.struct_pb2.Value + ]: + """Custom properties that start with "x-" such as "x-foo" used to describe + extra functionality that is not covered by the standard OpenAPI Specification. + See: https://swagger.io/docs/specification/2-0/swagger-extensions/ + """ + def __init__( + self, + *, + name: builtins.str = ..., + description: builtins.str = ..., + external_docs: global___ExternalDocumentation | None = ..., + extensions: collections.abc.Mapping[ + builtins.str, google.protobuf.struct_pb2.Value + ] + | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["external_docs", b"external_docs"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "description", + b"description", + "extensions", + b"extensions", + "external_docs", + b"external_docs", + "name", + b"name", + ], + ) -> None: ... + +global___Tag = Tag + +class SecurityDefinitions(google.protobuf.message.Message): + """`SecurityDefinitions` is a representation of OpenAPI v2 specification's + Security Definitions object. + + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#securityDefinitionsObject + + A declaration of the security schemes available to be used in the + specification. This does not enforce the security schemes on the operations + and only serves to provide the relevant details for each scheme. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class SecurityEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> global___SecurityScheme: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: global___SecurityScheme | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["value", b"value"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["key", b"key", "value", b"value"], + ) -> None: ... + + SECURITY_FIELD_NUMBER: builtins.int + @property + def security( + self, + ) -> google.protobuf.internal.containers.MessageMap[ + builtins.str, global___SecurityScheme + ]: + """A single security scheme definition, mapping a "name" to the scheme it + defines. + """ + def __init__( + self, + *, + security: collections.abc.Mapping[builtins.str, global___SecurityScheme] + | None = ..., + ) -> None: ... + def ClearField( + self, field_name: typing_extensions.Literal["security", b"security"] + ) -> None: ... + +global___SecurityDefinitions = SecurityDefinitions + +class SecurityScheme(google.protobuf.message.Message): + """`SecurityScheme` is a representation of OpenAPI v2 specification's + Security Scheme object. + + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#securitySchemeObject + + Allows the definition of a security scheme that can be used by the + operations. Supported schemes are basic authentication, an API key (either as + a header or as a query parameter) and OAuth2's common flows (implicit, + password, application and access code). + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class _Type: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _TypeEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ + SecurityScheme._Type.ValueType + ], + builtins.type, + ): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + TYPE_INVALID: SecurityScheme._Type.ValueType # 0 + TYPE_BASIC: SecurityScheme._Type.ValueType # 1 + TYPE_API_KEY: SecurityScheme._Type.ValueType # 2 + TYPE_OAUTH2: SecurityScheme._Type.ValueType # 3 + + class Type(_Type, metaclass=_TypeEnumTypeWrapper): + """The type of the security scheme. Valid values are "basic", + "apiKey" or "oauth2". + """ + + TYPE_INVALID: SecurityScheme.Type.ValueType # 0 + TYPE_BASIC: SecurityScheme.Type.ValueType # 1 + TYPE_API_KEY: SecurityScheme.Type.ValueType # 2 + TYPE_OAUTH2: SecurityScheme.Type.ValueType # 3 + + class _In: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _InEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ + SecurityScheme._In.ValueType + ], + builtins.type, + ): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + IN_INVALID: SecurityScheme._In.ValueType # 0 + IN_QUERY: SecurityScheme._In.ValueType # 1 + IN_HEADER: SecurityScheme._In.ValueType # 2 + + class In(_In, metaclass=_InEnumTypeWrapper): + """The location of the API key. Valid values are "query" or "header".""" + + IN_INVALID: SecurityScheme.In.ValueType # 0 + IN_QUERY: SecurityScheme.In.ValueType # 1 + IN_HEADER: SecurityScheme.In.ValueType # 2 + + class _Flow: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _FlowEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ + SecurityScheme._Flow.ValueType + ], + builtins.type, + ): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + FLOW_INVALID: SecurityScheme._Flow.ValueType # 0 + FLOW_IMPLICIT: SecurityScheme._Flow.ValueType # 1 + FLOW_PASSWORD: SecurityScheme._Flow.ValueType # 2 + FLOW_APPLICATION: SecurityScheme._Flow.ValueType # 3 + FLOW_ACCESS_CODE: SecurityScheme._Flow.ValueType # 4 + + class Flow(_Flow, metaclass=_FlowEnumTypeWrapper): + """The flow used by the OAuth2 security scheme. Valid values are + "implicit", "password", "application" or "accessCode". + """ + + FLOW_INVALID: SecurityScheme.Flow.ValueType # 0 + FLOW_IMPLICIT: SecurityScheme.Flow.ValueType # 1 + FLOW_PASSWORD: SecurityScheme.Flow.ValueType # 2 + FLOW_APPLICATION: SecurityScheme.Flow.ValueType # 3 + FLOW_ACCESS_CODE: SecurityScheme.Flow.ValueType # 4 + + class ExtensionsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> google.protobuf.struct_pb2.Value: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: google.protobuf.struct_pb2.Value | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["value", b"value"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["key", b"key", "value", b"value"], + ) -> None: ... + + TYPE_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + NAME_FIELD_NUMBER: builtins.int + IN_FIELD_NUMBER: builtins.int + FLOW_FIELD_NUMBER: builtins.int + AUTHORIZATION_URL_FIELD_NUMBER: builtins.int + TOKEN_URL_FIELD_NUMBER: builtins.int + SCOPES_FIELD_NUMBER: builtins.int + EXTENSIONS_FIELD_NUMBER: builtins.int + type: global___SecurityScheme.Type.ValueType + """The type of the security scheme. Valid values are "basic", + "apiKey" or "oauth2". + """ + description: builtins.str + """A short description for security scheme.""" + name: builtins.str + """The name of the header or query parameter to be used. + Valid for apiKey. + """ + flow: global___SecurityScheme.Flow.ValueType + """The flow used by the OAuth2 security scheme. Valid values are + "implicit", "password", "application" or "accessCode". + Valid for oauth2. + """ + authorization_url: builtins.str + """The authorization URL to be used for this flow. This SHOULD be in + the form of a URL. + Valid for oauth2/implicit and oauth2/accessCode. + """ + token_url: builtins.str + """The token URL to be used for this flow. This SHOULD be in the + form of a URL. + Valid for oauth2/password, oauth2/application and oauth2/accessCode. + """ + @property + def scopes(self) -> global___Scopes: + """The available scopes for the OAuth2 security scheme. + Valid for oauth2. + """ + @property + def extensions( + self, + ) -> google.protobuf.internal.containers.MessageMap[ + builtins.str, google.protobuf.struct_pb2.Value + ]: + """Custom properties that start with "x-" such as "x-foo" used to describe + extra functionality that is not covered by the standard OpenAPI Specification. + See: https://swagger.io/docs/specification/2-0/swagger-extensions/ + """ + def __init__( + self, + *, + type: global___SecurityScheme.Type.ValueType = ..., + description: builtins.str = ..., + name: builtins.str = ..., + flow: global___SecurityScheme.Flow.ValueType = ..., + authorization_url: builtins.str = ..., + token_url: builtins.str = ..., + scopes: global___Scopes | None = ..., + extensions: collections.abc.Mapping[ + builtins.str, google.protobuf.struct_pb2.Value + ] + | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["scopes", b"scopes"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "authorization_url", + b"authorization_url", + "description", + b"description", + "extensions", + b"extensions", + "flow", + b"flow", + "in", + b"in", + "name", + b"name", + "scopes", + b"scopes", + "token_url", + b"token_url", + "type", + b"type", + ], + ) -> None: ... + +global___SecurityScheme = SecurityScheme + +class SecurityRequirement(google.protobuf.message.Message): + """`SecurityRequirement` is a representation of OpenAPI v2 specification's + Security Requirement object. + + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#securityRequirementObject + + Lists the required security schemes to execute this operation. The object can + have multiple security schemes declared in it which are all required (that + is, there is a logical AND between the schemes). + + The name used for each property MUST correspond to a security scheme + declared in the Security Definitions. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class SecurityRequirementValue(google.protobuf.message.Message): + """If the security scheme is of type "oauth2", then the value is a list of + scope names required for the execution. For other security scheme types, + the array MUST be empty. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SCOPE_FIELD_NUMBER: builtins.int + @property + def scope( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[ + builtins.str + ]: ... + def __init__( + self, + *, + scope: collections.abc.Iterable[builtins.str] | None = ..., + ) -> None: ... + def ClearField( + self, field_name: typing_extensions.Literal["scope", b"scope"] + ) -> None: ... + + class SecurityRequirementEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> global___SecurityRequirement.SecurityRequirementValue: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: global___SecurityRequirement.SecurityRequirementValue | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["value", b"value"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["key", b"key", "value", b"value"], + ) -> None: ... + + SECURITY_REQUIREMENT_FIELD_NUMBER: builtins.int + @property + def security_requirement( + self, + ) -> google.protobuf.internal.containers.MessageMap[ + builtins.str, global___SecurityRequirement.SecurityRequirementValue + ]: + """Each name must correspond to a security scheme which is declared in + the Security Definitions. If the security scheme is of type "oauth2", + then the value is a list of scope names required for the execution. + For other security scheme types, the array MUST be empty. + """ + def __init__( + self, + *, + security_requirement: collections.abc.Mapping[ + builtins.str, global___SecurityRequirement.SecurityRequirementValue + ] + | None = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "security_requirement", b"security_requirement" + ], + ) -> None: ... + +global___SecurityRequirement = SecurityRequirement + +class Scopes(google.protobuf.message.Message): + """`Scopes` is a representation of OpenAPI v2 specification's Scopes object. + + See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#scopesObject + + Lists the available scopes for an OAuth2 security scheme. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class ScopeEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal["key", b"key", "value", b"value"], + ) -> None: ... + + SCOPE_FIELD_NUMBER: builtins.int + @property + def scope( + self, + ) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: + """Maps between a name of a scope to a short description of it (as the value + of the property). + """ + def __init__( + self, + *, + scope: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + ) -> None: ... + def ClearField( + self, field_name: typing_extensions.Literal["scope", b"scope"] + ) -> None: ... + +global___Scopes = Scopes diff --git a/temporalio/api/deployment/v1/__init__.py b/temporalio/api/deployment/v1/__init__.py index 63d01878f..2abc00d31 100644 --- a/temporalio/api/deployment/v1/__init__.py +++ b/temporalio/api/deployment/v1/__init__.py @@ -1,7 +1,9 @@ from .message_pb2 import ( + ComputeStatus, Deployment, DeploymentInfo, DeploymentListInfo, + InheritedAutoUpgradeInfo, RoutingConfig, UpdateDeploymentMetadata, VersionDrainageInfo, @@ -13,9 +15,11 @@ ) __all__ = [ + "ComputeStatus", "Deployment", "DeploymentInfo", "DeploymentListInfo", + "InheritedAutoUpgradeInfo", "RoutingConfig", "UpdateDeploymentMetadata", "VersionDrainageInfo", diff --git a/temporalio/api/deployment/v1/message_pb2.py b/temporalio/api/deployment/v1/message_pb2.py index 3a9b0127b..cc3b1ea75 100644 --- a/temporalio/api/deployment/v1/message_pb2.py +++ b/temporalio/api/deployment/v1/message_pb2.py @@ -19,15 +19,21 @@ from temporalio.api.common.v1 import ( message_pb2 as temporal_dot_api_dot_common_dot_v1_dot_message__pb2, ) +from temporalio.api.compute.v1 import ( + config_pb2 as temporal_dot_api_dot_compute_dot_v1_dot_config__pb2, +) from temporalio.api.enums.v1 import ( deployment_pb2 as temporal_dot_api_dot_enums_dot_v1_dot_deployment__pb2, ) from temporalio.api.enums.v1 import ( task_queue_pb2 as temporal_dot_api_dot_enums_dot_v1_dot_task__queue__pb2, ) +from temporalio.api.enums.v1 import ( + workflow_pb2 as temporal_dot_api_dot_enums_dot_v1_dot_workflow__pb2, +) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n(temporal/api/deployment/v1/message.proto\x12\x1atemporal.api.deployment.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a&temporal/api/enums/v1/deployment.proto\x1a&temporal/api/enums/v1/task_queue.proto\x1a$temporal/api/common/v1/message.proto"\x91\x01\n\x17WorkerDeploymentOptions\x12\x17\n\x0f\x64\x65ployment_name\x18\x01 \x01(\t\x12\x10\n\x08\x62uild_id\x18\x02 \x01(\t\x12K\n\x16worker_versioning_mode\x18\x03 \x01(\x0e\x32+.temporal.api.enums.v1.WorkerVersioningMode"3\n\nDeployment\x12\x13\n\x0bseries_name\x18\x01 \x01(\t\x12\x10\n\x08\x62uild_id\x18\x02 \x01(\t"\x8e\x04\n\x0e\x44\x65ploymentInfo\x12:\n\ndeployment\x18\x01 \x01(\x0b\x32&.temporal.api.deployment.v1.Deployment\x12/\n\x0b\x63reate_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12R\n\x10task_queue_infos\x18\x03 \x03(\x0b\x32\x38.temporal.api.deployment.v1.DeploymentInfo.TaskQueueInfo\x12J\n\x08metadata\x18\x04 \x03(\x0b\x32\x38.temporal.api.deployment.v1.DeploymentInfo.MetadataEntry\x12\x12\n\nis_current\x18\x05 \x01(\x08\x1aP\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01\x1a\x88\x01\n\rTaskQueueInfo\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x32\n\x04type\x18\x02 \x01(\x0e\x32$.temporal.api.enums.v1.TaskQueueType\x12\x35\n\x11\x66irst_poller_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"\xea\x01\n\x18UpdateDeploymentMetadata\x12_\n\x0eupsert_entries\x18\x01 \x03(\x0b\x32G.temporal.api.deployment.v1.UpdateDeploymentMetadata.UpsertEntriesEntry\x12\x16\n\x0eremove_entries\x18\x02 \x03(\t\x1aU\n\x12UpsertEntriesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\x95\x01\n\x12\x44\x65ploymentListInfo\x12:\n\ndeployment\x18\x01 \x01(\x0b\x32&.temporal.api.deployment.v1.Deployment\x12/\n\x0b\x63reate_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x12\n\nis_current\x18\x03 \x01(\x08"\x96\x07\n\x1bWorkerDeploymentVersionInfo\x12\x13\n\x07version\x18\x01 \x01(\tB\x02\x18\x01\x12\x44\n\x06status\x18\x0e \x01(\x0e\x32\x34.temporal.api.enums.v1.WorkerDeploymentVersionStatus\x12O\n\x12\x64\x65ployment_version\x18\x0b \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12\x17\n\x0f\x64\x65ployment_name\x18\x02 \x01(\t\x12/\n\x0b\x63reate_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x38\n\x14routing_changed_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x36\n\x12\x63urrent_since_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x36\n\x12ramping_since_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x39\n\x15\x66irst_activation_time\x18\x0c \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_deactivation_time\x18\r \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x17\n\x0framp_percentage\x18\x07 \x01(\x02\x12\x66\n\x10task_queue_infos\x18\x08 \x03(\x0b\x32L.temporal.api.deployment.v1.WorkerDeploymentVersionInfo.VersionTaskQueueInfo\x12\x46\n\rdrainage_info\x18\t \x01(\x0b\x32/.temporal.api.deployment.v1.VersionDrainageInfo\x12=\n\x08metadata\x18\n \x01(\x0b\x32+.temporal.api.deployment.v1.VersionMetadata\x1aX\n\x14VersionTaskQueueInfo\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x32\n\x04type\x18\x02 \x01(\x0e\x32$.temporal.api.enums.v1.TaskQueueType"\xc1\x01\n\x13VersionDrainageInfo\x12<\n\x06status\x18\x01 \x01(\x0e\x32,.temporal.api.enums.v1.VersionDrainageStatus\x12\x35\n\x11last_changed_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x35\n\x11last_checked_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"\xd3\x07\n\x14WorkerDeploymentInfo\x12\x0c\n\x04name\x18\x01 \x01(\t\x12j\n\x11version_summaries\x18\x02 \x03(\x0b\x32O.temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary\x12/\n\x0b\x63reate_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x41\n\x0erouting_config\x18\x04 \x01(\x0b\x32).temporal.api.deployment.v1.RoutingConfig\x12\x1e\n\x16last_modifier_identity\x18\x05 \x01(\t\x1a\xac\x05\n\x1eWorkerDeploymentVersionSummary\x12\x13\n\x07version\x18\x01 \x01(\tB\x02\x18\x01\x12\x44\n\x06status\x18\x0b \x01(\x0e\x32\x34.temporal.api.enums.v1.WorkerDeploymentVersionStatus\x12O\n\x12\x64\x65ployment_version\x18\x04 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12/\n\x0b\x63reate_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x45\n\x0f\x64rainage_status\x18\x03 \x01(\x0e\x32,.temporal.api.enums.v1.VersionDrainageStatus\x12\x46\n\rdrainage_info\x18\x05 \x01(\x0b\x32/.temporal.api.deployment.v1.VersionDrainageInfo\x12\x36\n\x12\x63urrent_since_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x36\n\x12ramping_since_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x37\n\x13routing_update_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x39\n\x15\x66irst_activation_time\x18\t \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_deactivation_time\x18\n \x01(\x0b\x32\x1a.google.protobuf.Timestamp"D\n\x17WorkerDeploymentVersion\x12\x10\n\x08\x62uild_id\x18\x01 \x01(\t\x12\x17\n\x0f\x64\x65ployment_name\x18\x02 \x01(\t"\xad\x01\n\x0fVersionMetadata\x12I\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x38.temporal.api.deployment.v1.VersionMetadata.EntriesEntry\x1aO\n\x0c\x45ntriesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\xf0\x03\n\rRoutingConfig\x12W\n\x1a\x63urrent_deployment_version\x18\x07 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12\x1b\n\x0f\x63urrent_version\x18\x01 \x01(\tB\x02\x18\x01\x12W\n\x1aramping_deployment_version\x18\t \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12\x1b\n\x0framping_version\x18\x02 \x01(\tB\x02\x18\x01\x12"\n\x1aramping_version_percentage\x18\x03 \x01(\x02\x12@\n\x1c\x63urrent_version_changed_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12@\n\x1cramping_version_changed_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12K\n\'ramping_version_percentage_changed_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x9d\x01\n\x1dio.temporal.api.deployment.v1B\x0cMessageProtoP\x01Z+go.temporal.io/api/deployment/v1;deployment\xaa\x02\x1cTemporalio.Api.Deployment.V1\xea\x02\x1fTemporalio::Api::Deployment::V1b\x06proto3' + b'\n(temporal/api/deployment/v1/message.proto\x12\x1atemporal.api.deployment.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a&temporal/api/enums/v1/deployment.proto\x1a&temporal/api/enums/v1/task_queue.proto\x1a$temporal/api/common/v1/message.proto\x1a$temporal/api/compute/v1/config.proto"\x91\x01\n\x17WorkerDeploymentOptions\x12\x17\n\x0f\x64\x65ployment_name\x18\x01 \x01(\t\x12\x10\n\x08\x62uild_id\x18\x02 \x01(\t\x12K\n\x16worker_versioning_mode\x18\x03 \x01(\x0e\x32+.temporal.api.enums.v1.WorkerVersioningMode"3\n\nDeployment\x12\x13\n\x0bseries_name\x18\x01 \x01(\t\x12\x10\n\x08\x62uild_id\x18\x02 \x01(\t"\x8e\x04\n\x0e\x44\x65ploymentInfo\x12:\n\ndeployment\x18\x01 \x01(\x0b\x32&.temporal.api.deployment.v1.Deployment\x12/\n\x0b\x63reate_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12R\n\x10task_queue_infos\x18\x03 \x03(\x0b\x32\x38.temporal.api.deployment.v1.DeploymentInfo.TaskQueueInfo\x12J\n\x08metadata\x18\x04 \x03(\x0b\x32\x38.temporal.api.deployment.v1.DeploymentInfo.MetadataEntry\x12\x12\n\nis_current\x18\x05 \x01(\x08\x1aP\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01\x1a\x88\x01\n\rTaskQueueInfo\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x32\n\x04type\x18\x02 \x01(\x0e\x32$.temporal.api.enums.v1.TaskQueueType\x12\x35\n\x11\x66irst_poller_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"\xea\x01\n\x18UpdateDeploymentMetadata\x12_\n\x0eupsert_entries\x18\x01 \x03(\x0b\x32G.temporal.api.deployment.v1.UpdateDeploymentMetadata.UpsertEntriesEntry\x12\x16\n\x0eremove_entries\x18\x02 \x03(\t\x1aU\n\x12UpsertEntriesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\x95\x01\n\x12\x44\x65ploymentListInfo\x12:\n\ndeployment\x18\x01 \x01(\x0b\x32&.temporal.api.deployment.v1.Deployment\x12/\n\x0b\x63reate_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x12\n\nis_current\x18\x03 \x01(\x08"\xad\x08\n\x1bWorkerDeploymentVersionInfo\x12\x13\n\x07version\x18\x01 \x01(\tB\x02\x18\x01\x12\x44\n\x06status\x18\x0e \x01(\x0e\x32\x34.temporal.api.enums.v1.WorkerDeploymentVersionStatus\x12O\n\x12\x64\x65ployment_version\x18\x0b \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12\x17\n\x0f\x64\x65ployment_name\x18\x02 \x01(\t\x12/\n\x0b\x63reate_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x38\n\x14routing_changed_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x36\n\x12\x63urrent_since_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x36\n\x12ramping_since_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x39\n\x15\x66irst_activation_time\x18\x0c \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x35\n\x11last_current_time\x18\x0f \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_deactivation_time\x18\r \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x17\n\x0framp_percentage\x18\x07 \x01(\x02\x12\x66\n\x10task_queue_infos\x18\x08 \x03(\x0b\x32L.temporal.api.deployment.v1.WorkerDeploymentVersionInfo.VersionTaskQueueInfo\x12\x46\n\rdrainage_info\x18\t \x01(\x0b\x32/.temporal.api.deployment.v1.VersionDrainageInfo\x12=\n\x08metadata\x18\n \x01(\x0b\x32+.temporal.api.deployment.v1.VersionMetadata\x12>\n\x0e\x63ompute_config\x18\x10 \x01(\x0b\x32&.temporal.api.compute.v1.ComputeConfig\x12\x1e\n\x16last_modifier_identity\x18\x11 \x01(\t\x1aX\n\x14VersionTaskQueueInfo\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x32\n\x04type\x18\x02 \x01(\x0e\x32$.temporal.api.enums.v1.TaskQueueType"\xc1\x01\n\x13VersionDrainageInfo\x12<\n\x06status\x18\x01 \x01(\x0e\x32,.temporal.api.enums.v1.VersionDrainageStatus\x12\x35\n\x11last_changed_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x35\n\x11last_checked_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"\xd8\x01\n\rComputeStatus\x12_\n\x13provider_validation\x18\x01 \x01(\x0b\x32\x42.temporal.api.deployment.v1.ComputeStatus.ProviderValidationStatus\x1a\x66\n\x18ProviderValidationStatus\x12\x15\n\rerror_message\x18\x01 \x01(\t\x12\x33\n\x0flast_check_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"\x84\n\n\x14WorkerDeploymentInfo\x12\x0c\n\x04name\x18\x01 \x01(\t\x12j\n\x11version_summaries\x18\x02 \x03(\x0b\x32O.temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary\x12/\n\x0b\x63reate_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x41\n\x0erouting_config\x18\x04 \x01(\x0b\x32).temporal.api.deployment.v1.RoutingConfig\x12\x1e\n\x16last_modifier_identity\x18\x05 \x01(\t\x12\x18\n\x10manager_identity\x18\x06 \x01(\t\x12T\n\x1brouting_config_update_state\x18\x07 \x01(\x0e\x32/.temporal.api.enums.v1.RoutingConfigUpdateState\x1a\xed\x06\n\x1eWorkerDeploymentVersionSummary\x12\x13\n\x07version\x18\x01 \x01(\tB\x02\x18\x01\x12\x44\n\x06status\x18\x0b \x01(\x0e\x32\x34.temporal.api.enums.v1.WorkerDeploymentVersionStatus\x12O\n\x12\x64\x65ployment_version\x18\x04 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12/\n\x0b\x63reate_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x45\n\x0f\x64rainage_status\x18\x03 \x01(\x0e\x32,.temporal.api.enums.v1.VersionDrainageStatus\x12\x46\n\rdrainage_info\x18\x05 \x01(\x0b\x32/.temporal.api.deployment.v1.VersionDrainageInfo\x12\x36\n\x12\x63urrent_since_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x36\n\x12ramping_since_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x37\n\x13routing_update_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x39\n\x15\x66irst_activation_time\x18\t \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x35\n\x11last_current_time\x18\x0c \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_deactivation_time\x18\n \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x45\n\x0e\x63ompute_config\x18\r \x01(\x0b\x32-.temporal.api.compute.v1.ComputeConfigSummary\x12\x41\n\x0e\x63ompute_status\x18\x0e \x01(\x0b\x32).temporal.api.deployment.v1.ComputeStatus"D\n\x17WorkerDeploymentVersion\x12\x10\n\x08\x62uild_id\x18\x01 \x01(\t\x12\x17\n\x0f\x64\x65ployment_name\x18\x02 \x01(\t"\xad\x01\n\x0fVersionMetadata\x12I\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x38.temporal.api.deployment.v1.VersionMetadata.EntriesEntry\x1aO\n\x0c\x45ntriesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\x89\x04\n\rRoutingConfig\x12W\n\x1a\x63urrent_deployment_version\x18\x07 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12\x1b\n\x0f\x63urrent_version\x18\x01 \x01(\tB\x02\x18\x01\x12W\n\x1aramping_deployment_version\x18\t \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12\x1b\n\x0framping_version\x18\x02 \x01(\tB\x02\x18\x01\x12"\n\x1aramping_version_percentage\x18\x03 \x01(\x02\x12@\n\x1c\x63urrent_version_changed_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12@\n\x1cramping_version_changed_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12K\n\'ramping_version_percentage_changed_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x17\n\x0frevision_number\x18\n \x01(\x03"\x8a\x02\n\x18InheritedAutoUpgradeInfo\x12V\n\x19source_deployment_version\x18\x01 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12)\n!source_deployment_revision_number\x18\x02 \x01(\x03\x12k\n+continue_as_new_initial_versioning_behavior\x18\x03 \x01(\x0e\x32\x36.temporal.api.enums.v1.ContinueAsNewVersioningBehaviorB\x9d\x01\n\x1dio.temporal.api.deployment.v1B\x0cMessageProtoP\x01Z+go.temporal.io/api/deployment/v1;deployment\xaa\x02\x1cTemporalio.Api.Deployment.V1\xea\x02\x1fTemporalio::Api::Deployment::V1b\x06proto3' ) @@ -48,6 +54,10 @@ _WORKERDEPLOYMENTVERSIONINFO.nested_types_by_name["VersionTaskQueueInfo"] ) _VERSIONDRAINAGEINFO = DESCRIPTOR.message_types_by_name["VersionDrainageInfo"] +_COMPUTESTATUS = DESCRIPTOR.message_types_by_name["ComputeStatus"] +_COMPUTESTATUS_PROVIDERVALIDATIONSTATUS = _COMPUTESTATUS.nested_types_by_name[ + "ProviderValidationStatus" +] _WORKERDEPLOYMENTINFO = DESCRIPTOR.message_types_by_name["WorkerDeploymentInfo"] _WORKERDEPLOYMENTINFO_WORKERDEPLOYMENTVERSIONSUMMARY = ( _WORKERDEPLOYMENTINFO.nested_types_by_name["WorkerDeploymentVersionSummary"] @@ -56,12 +66,13 @@ _VERSIONMETADATA = DESCRIPTOR.message_types_by_name["VersionMetadata"] _VERSIONMETADATA_ENTRIESENTRY = _VERSIONMETADATA.nested_types_by_name["EntriesEntry"] _ROUTINGCONFIG = DESCRIPTOR.message_types_by_name["RoutingConfig"] +_INHERITEDAUTOUPGRADEINFO = DESCRIPTOR.message_types_by_name["InheritedAutoUpgradeInfo"] WorkerDeploymentOptions = _reflection.GeneratedProtocolMessageType( "WorkerDeploymentOptions", (_message.Message,), { "DESCRIPTOR": _WORKERDEPLOYMENTOPTIONS, - "__module__": "temporal.api.deployment.v1.message_pb2", + "__module__": "temporalio.api.deployment.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.deployment.v1.WorkerDeploymentOptions) }, ) @@ -72,7 +83,7 @@ (_message.Message,), { "DESCRIPTOR": _DEPLOYMENT, - "__module__": "temporal.api.deployment.v1.message_pb2", + "__module__": "temporalio.api.deployment.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.deployment.v1.Deployment) }, ) @@ -87,7 +98,7 @@ (_message.Message,), { "DESCRIPTOR": _DEPLOYMENTINFO_METADATAENTRY, - "__module__": "temporal.api.deployment.v1.message_pb2", + "__module__": "temporalio.api.deployment.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.deployment.v1.DeploymentInfo.MetadataEntry) }, ), @@ -96,12 +107,12 @@ (_message.Message,), { "DESCRIPTOR": _DEPLOYMENTINFO_TASKQUEUEINFO, - "__module__": "temporal.api.deployment.v1.message_pb2", + "__module__": "temporalio.api.deployment.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.deployment.v1.DeploymentInfo.TaskQueueInfo) }, ), "DESCRIPTOR": _DEPLOYMENTINFO, - "__module__": "temporal.api.deployment.v1.message_pb2", + "__module__": "temporalio.api.deployment.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.deployment.v1.DeploymentInfo) }, ) @@ -118,12 +129,12 @@ (_message.Message,), { "DESCRIPTOR": _UPDATEDEPLOYMENTMETADATA_UPSERTENTRIESENTRY, - "__module__": "temporal.api.deployment.v1.message_pb2", + "__module__": "temporalio.api.deployment.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.deployment.v1.UpdateDeploymentMetadata.UpsertEntriesEntry) }, ), "DESCRIPTOR": _UPDATEDEPLOYMENTMETADATA, - "__module__": "temporal.api.deployment.v1.message_pb2", + "__module__": "temporalio.api.deployment.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.deployment.v1.UpdateDeploymentMetadata) }, ) @@ -135,7 +146,7 @@ (_message.Message,), { "DESCRIPTOR": _DEPLOYMENTLISTINFO, - "__module__": "temporal.api.deployment.v1.message_pb2", + "__module__": "temporalio.api.deployment.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.deployment.v1.DeploymentListInfo) }, ) @@ -150,12 +161,12 @@ (_message.Message,), { "DESCRIPTOR": _WORKERDEPLOYMENTVERSIONINFO_VERSIONTASKQUEUEINFO, - "__module__": "temporal.api.deployment.v1.message_pb2", + "__module__": "temporalio.api.deployment.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.deployment.v1.WorkerDeploymentVersionInfo.VersionTaskQueueInfo) }, ), "DESCRIPTOR": _WORKERDEPLOYMENTVERSIONINFO, - "__module__": "temporal.api.deployment.v1.message_pb2", + "__module__": "temporalio.api.deployment.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.deployment.v1.WorkerDeploymentVersionInfo) }, ) @@ -167,12 +178,33 @@ (_message.Message,), { "DESCRIPTOR": _VERSIONDRAINAGEINFO, - "__module__": "temporal.api.deployment.v1.message_pb2", + "__module__": "temporalio.api.deployment.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.deployment.v1.VersionDrainageInfo) }, ) _sym_db.RegisterMessage(VersionDrainageInfo) +ComputeStatus = _reflection.GeneratedProtocolMessageType( + "ComputeStatus", + (_message.Message,), + { + "ProviderValidationStatus": _reflection.GeneratedProtocolMessageType( + "ProviderValidationStatus", + (_message.Message,), + { + "DESCRIPTOR": _COMPUTESTATUS_PROVIDERVALIDATIONSTATUS, + "__module__": "temporalio.api.deployment.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.deployment.v1.ComputeStatus.ProviderValidationStatus) + }, + ), + "DESCRIPTOR": _COMPUTESTATUS, + "__module__": "temporalio.api.deployment.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.deployment.v1.ComputeStatus) + }, +) +_sym_db.RegisterMessage(ComputeStatus) +_sym_db.RegisterMessage(ComputeStatus.ProviderValidationStatus) + WorkerDeploymentInfo = _reflection.GeneratedProtocolMessageType( "WorkerDeploymentInfo", (_message.Message,), @@ -182,12 +214,12 @@ (_message.Message,), { "DESCRIPTOR": _WORKERDEPLOYMENTINFO_WORKERDEPLOYMENTVERSIONSUMMARY, - "__module__": "temporal.api.deployment.v1.message_pb2", + "__module__": "temporalio.api.deployment.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary) }, ), "DESCRIPTOR": _WORKERDEPLOYMENTINFO, - "__module__": "temporal.api.deployment.v1.message_pb2", + "__module__": "temporalio.api.deployment.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.deployment.v1.WorkerDeploymentInfo) }, ) @@ -199,7 +231,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKERDEPLOYMENTVERSION, - "__module__": "temporal.api.deployment.v1.message_pb2", + "__module__": "temporalio.api.deployment.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.deployment.v1.WorkerDeploymentVersion) }, ) @@ -214,12 +246,12 @@ (_message.Message,), { "DESCRIPTOR": _VERSIONMETADATA_ENTRIESENTRY, - "__module__": "temporal.api.deployment.v1.message_pb2", + "__module__": "temporalio.api.deployment.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.deployment.v1.VersionMetadata.EntriesEntry) }, ), "DESCRIPTOR": _VERSIONMETADATA, - "__module__": "temporal.api.deployment.v1.message_pb2", + "__module__": "temporalio.api.deployment.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.deployment.v1.VersionMetadata) }, ) @@ -231,12 +263,23 @@ (_message.Message,), { "DESCRIPTOR": _ROUTINGCONFIG, - "__module__": "temporal.api.deployment.v1.message_pb2", + "__module__": "temporalio.api.deployment.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.deployment.v1.RoutingConfig) }, ) _sym_db.RegisterMessage(RoutingConfig) +InheritedAutoUpgradeInfo = _reflection.GeneratedProtocolMessageType( + "InheritedAutoUpgradeInfo", + (_message.Message,), + { + "DESCRIPTOR": _INHERITEDAUTOUPGRADEINFO, + "__module__": "temporalio.api.deployment.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.deployment.v1.InheritedAutoUpgradeInfo) + }, +) +_sym_db.RegisterMessage(InheritedAutoUpgradeInfo) + if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b"\n\035io.temporal.api.deployment.v1B\014MessageProtoP\001Z+go.temporal.io/api/deployment/v1;deployment\252\002\034Temporalio.Api.Deployment.V1\352\002\037Temporalio::Api::Deployment::V1" @@ -260,38 +303,44 @@ _ROUTINGCONFIG.fields_by_name["current_version"]._serialized_options = b"\030\001" _ROUTINGCONFIG.fields_by_name["ramping_version"]._options = None _ROUTINGCONFIG.fields_by_name["ramping_version"]._serialized_options = b"\030\001" - _WORKERDEPLOYMENTOPTIONS._serialized_start = 224 - _WORKERDEPLOYMENTOPTIONS._serialized_end = 369 - _DEPLOYMENT._serialized_start = 371 - _DEPLOYMENT._serialized_end = 422 - _DEPLOYMENTINFO._serialized_start = 425 - _DEPLOYMENTINFO._serialized_end = 951 - _DEPLOYMENTINFO_METADATAENTRY._serialized_start = 732 - _DEPLOYMENTINFO_METADATAENTRY._serialized_end = 812 - _DEPLOYMENTINFO_TASKQUEUEINFO._serialized_start = 815 - _DEPLOYMENTINFO_TASKQUEUEINFO._serialized_end = 951 - _UPDATEDEPLOYMENTMETADATA._serialized_start = 954 - _UPDATEDEPLOYMENTMETADATA._serialized_end = 1188 - _UPDATEDEPLOYMENTMETADATA_UPSERTENTRIESENTRY._serialized_start = 1103 - _UPDATEDEPLOYMENTMETADATA_UPSERTENTRIESENTRY._serialized_end = 1188 - _DEPLOYMENTLISTINFO._serialized_start = 1191 - _DEPLOYMENTLISTINFO._serialized_end = 1340 - _WORKERDEPLOYMENTVERSIONINFO._serialized_start = 1343 - _WORKERDEPLOYMENTVERSIONINFO._serialized_end = 2261 - _WORKERDEPLOYMENTVERSIONINFO_VERSIONTASKQUEUEINFO._serialized_start = 2173 - _WORKERDEPLOYMENTVERSIONINFO_VERSIONTASKQUEUEINFO._serialized_end = 2261 - _VERSIONDRAINAGEINFO._serialized_start = 2264 - _VERSIONDRAINAGEINFO._serialized_end = 2457 - _WORKERDEPLOYMENTINFO._serialized_start = 2460 - _WORKERDEPLOYMENTINFO._serialized_end = 3439 - _WORKERDEPLOYMENTINFO_WORKERDEPLOYMENTVERSIONSUMMARY._serialized_start = 2755 - _WORKERDEPLOYMENTINFO_WORKERDEPLOYMENTVERSIONSUMMARY._serialized_end = 3439 - _WORKERDEPLOYMENTVERSION._serialized_start = 3441 - _WORKERDEPLOYMENTVERSION._serialized_end = 3509 - _VERSIONMETADATA._serialized_start = 3512 - _VERSIONMETADATA._serialized_end = 3685 - _VERSIONMETADATA_ENTRIESENTRY._serialized_start = 3606 - _VERSIONMETADATA_ENTRIESENTRY._serialized_end = 3685 - _ROUTINGCONFIG._serialized_start = 3688 - _ROUTINGCONFIG._serialized_end = 4184 + _WORKERDEPLOYMENTOPTIONS._serialized_start = 300 + _WORKERDEPLOYMENTOPTIONS._serialized_end = 445 + _DEPLOYMENT._serialized_start = 447 + _DEPLOYMENT._serialized_end = 498 + _DEPLOYMENTINFO._serialized_start = 501 + _DEPLOYMENTINFO._serialized_end = 1027 + _DEPLOYMENTINFO_METADATAENTRY._serialized_start = 808 + _DEPLOYMENTINFO_METADATAENTRY._serialized_end = 888 + _DEPLOYMENTINFO_TASKQUEUEINFO._serialized_start = 891 + _DEPLOYMENTINFO_TASKQUEUEINFO._serialized_end = 1027 + _UPDATEDEPLOYMENTMETADATA._serialized_start = 1030 + _UPDATEDEPLOYMENTMETADATA._serialized_end = 1264 + _UPDATEDEPLOYMENTMETADATA_UPSERTENTRIESENTRY._serialized_start = 1179 + _UPDATEDEPLOYMENTMETADATA_UPSERTENTRIESENTRY._serialized_end = 1264 + _DEPLOYMENTLISTINFO._serialized_start = 1267 + _DEPLOYMENTLISTINFO._serialized_end = 1416 + _WORKERDEPLOYMENTVERSIONINFO._serialized_start = 1419 + _WORKERDEPLOYMENTVERSIONINFO._serialized_end = 2488 + _WORKERDEPLOYMENTVERSIONINFO_VERSIONTASKQUEUEINFO._serialized_start = 2400 + _WORKERDEPLOYMENTVERSIONINFO_VERSIONTASKQUEUEINFO._serialized_end = 2488 + _VERSIONDRAINAGEINFO._serialized_start = 2491 + _VERSIONDRAINAGEINFO._serialized_end = 2684 + _COMPUTESTATUS._serialized_start = 2687 + _COMPUTESTATUS._serialized_end = 2903 + _COMPUTESTATUS_PROVIDERVALIDATIONSTATUS._serialized_start = 2801 + _COMPUTESTATUS_PROVIDERVALIDATIONSTATUS._serialized_end = 2903 + _WORKERDEPLOYMENTINFO._serialized_start = 2906 + _WORKERDEPLOYMENTINFO._serialized_end = 4190 + _WORKERDEPLOYMENTINFO_WORKERDEPLOYMENTVERSIONSUMMARY._serialized_start = 3313 + _WORKERDEPLOYMENTINFO_WORKERDEPLOYMENTVERSIONSUMMARY._serialized_end = 4190 + _WORKERDEPLOYMENTVERSION._serialized_start = 4192 + _WORKERDEPLOYMENTVERSION._serialized_end = 4260 + _VERSIONMETADATA._serialized_start = 4263 + _VERSIONMETADATA._serialized_end = 4436 + _VERSIONMETADATA_ENTRIESENTRY._serialized_start = 4357 + _VERSIONMETADATA_ENTRIESENTRY._serialized_end = 4436 + _ROUTINGCONFIG._serialized_start = 4439 + _ROUTINGCONFIG._serialized_end = 4960 + _INHERITEDAUTOUPGRADEINFO._serialized_start = 4963 + _INHERITEDAUTOUPGRADEINFO._serialized_end = 5229 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/deployment/v1/message_pb2.pyi b/temporalio/api/deployment/v1/message_pb2.pyi index 79936effc..96f31edd1 100644 --- a/temporalio/api/deployment/v1/message_pb2.pyi +++ b/temporalio/api/deployment/v1/message_pb2.pyi @@ -13,8 +13,10 @@ import google.protobuf.message import google.protobuf.timestamp_pb2 import temporalio.api.common.v1.message_pb2 +import temporalio.api.compute.v1.config_pb2 import temporalio.api.enums.v1.deployment_pb2 import temporalio.api.enums.v1.task_queue_pb2 +import temporalio.api.enums.v1.workflow_pb2 if sys.version_info >= (3, 8): import typing as typing_extensions @@ -24,9 +26,7 @@ else: DESCRIPTOR: google.protobuf.descriptor.FileDescriptor class WorkerDeploymentOptions(google.protobuf.message.Message): - """Worker Deployment options set in SDK that need to be sent to server in every poll. - Experimental. Worker Deployments are experimental and might significantly change in the future. - """ + """Worker Deployment options set in SDK that need to be sent to server in every poll.""" DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -34,7 +34,7 @@ class WorkerDeploymentOptions(google.protobuf.message.Message): BUILD_ID_FIELD_NUMBER: builtins.int WORKER_VERSIONING_MODE_FIELD_NUMBER: builtins.int deployment_name: builtins.str - """Required. Worker Deployment name.""" + """Required when `worker_versioning_mode==VERSIONED`.""" build_id: builtins.str """The Build ID of the worker. Required when `worker_versioning_mode==VERSIONED`, in which case, the worker will be part of a Deployment Version. @@ -347,7 +347,6 @@ class WorkerDeploymentVersionInfo(google.protobuf.message.Message): non-determinism issues. Worker Deployment Versions are created in Temporal server automatically when their first poller arrives to the server. - Experimental. Worker Deployments are experimental and might significantly change in the future. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -379,11 +378,14 @@ class WorkerDeploymentVersionInfo(google.protobuf.message.Message): CURRENT_SINCE_TIME_FIELD_NUMBER: builtins.int RAMPING_SINCE_TIME_FIELD_NUMBER: builtins.int FIRST_ACTIVATION_TIME_FIELD_NUMBER: builtins.int + LAST_CURRENT_TIME_FIELD_NUMBER: builtins.int LAST_DEACTIVATION_TIME_FIELD_NUMBER: builtins.int RAMP_PERCENTAGE_FIELD_NUMBER: builtins.int TASK_QUEUE_INFOS_FIELD_NUMBER: builtins.int DRAINAGE_INFO_FIELD_NUMBER: builtins.int METADATA_FIELD_NUMBER: builtins.int + COMPUTE_CONFIG_FIELD_NUMBER: builtins.int + LAST_MODIFIER_IDENTITY_FIELD_NUMBER: builtins.int version: builtins.str """Deprecated. Use `deployment_version`.""" status: ( @@ -394,6 +396,7 @@ class WorkerDeploymentVersionInfo(google.protobuf.message.Message): def deployment_version(self) -> global___WorkerDeploymentVersion: """Required.""" deployment_name: builtins.str + """Deprecated. User deployment_version.deployment_name.""" @property def create_time(self) -> google.protobuf.timestamp_pb2.Timestamp: ... @property @@ -415,8 +418,15 @@ class WorkerDeploymentVersionInfo(google.protobuf.message.Message): def first_activation_time(self) -> google.protobuf.timestamp_pb2.Timestamp: """Timestamp when this version first became current or ramping.""" @property + def last_current_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Timestamp when this version last became current. + Can be used to determine whether a version has ever been Current. + """ + @property def last_deactivation_time(self) -> google.protobuf.timestamp_pb2.Timestamp: - """Timestamp when this version last stopped being current or ramping.""" + """Timestamp when this version last stopped being current or ramping. + Cleared if the version becomes current or ramping again. + """ ramp_percentage: builtins.float """Range: [0, 100]. Must be zero if the version is not ramping (i.e. `ramping_since_time` is nil). Can be in the range [0, 100] if the version is ramping. @@ -449,6 +459,18 @@ class WorkerDeploymentVersionInfo(google.protobuf.message.Message): @property def metadata(self) -> global___VersionMetadata: """Arbitrary user-provided metadata attached to this version.""" + @property + def compute_config(self) -> temporalio.api.compute.v1.config_pb2.ComputeConfig: + """Optional. Contains the new worker compute configuration for the Worker + Deployment. Used for worker scale management. + """ + last_modifier_identity: builtins.str + """Identity of the last client who modified the configuration of this Version. + As of now, this field only covers changes through the following APIs: + - `CreateWorkerDeploymentVersion` + - `UpdateWorkerDeploymentVersionComputeConfig` + - `UpdateWorkerDeploymentVersionMetadata` + """ def __init__( self, *, @@ -461,6 +483,7 @@ class WorkerDeploymentVersionInfo(google.protobuf.message.Message): current_since_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., ramping_since_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., first_activation_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + last_current_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., last_deactivation_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., ramp_percentage: builtins.float = ..., task_queue_infos: collections.abc.Iterable[ @@ -469,10 +492,14 @@ class WorkerDeploymentVersionInfo(google.protobuf.message.Message): | None = ..., drainage_info: global___VersionDrainageInfo | None = ..., metadata: global___VersionMetadata | None = ..., + compute_config: temporalio.api.compute.v1.config_pb2.ComputeConfig | None = ..., + last_modifier_identity: builtins.str = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal[ + "compute_config", + b"compute_config", "create_time", b"create_time", "current_since_time", @@ -483,6 +510,8 @@ class WorkerDeploymentVersionInfo(google.protobuf.message.Message): b"drainage_info", "first_activation_time", b"first_activation_time", + "last_current_time", + b"last_current_time", "last_deactivation_time", b"last_deactivation_time", "metadata", @@ -496,6 +525,8 @@ class WorkerDeploymentVersionInfo(google.protobuf.message.Message): def ClearField( self, field_name: typing_extensions.Literal[ + "compute_config", + b"compute_config", "create_time", b"create_time", "current_since_time", @@ -508,8 +539,12 @@ class WorkerDeploymentVersionInfo(google.protobuf.message.Message): b"drainage_info", "first_activation_time", b"first_activation_time", + "last_current_time", + b"last_current_time", "last_deactivation_time", b"last_deactivation_time", + "last_modifier_identity", + b"last_modifier_identity", "metadata", b"metadata", "ramp_percentage", @@ -532,7 +567,6 @@ global___WorkerDeploymentVersionInfo = WorkerDeploymentVersionInfo class VersionDrainageInfo(google.protobuf.message.Message): """Information about workflow drainage to help the user determine when it is safe to decommission a Version. Not present while version is current or ramping. - Experimental. Worker Deployments are experimental and might significantly change in the future. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -580,6 +614,71 @@ class VersionDrainageInfo(google.protobuf.message.Message): global___VersionDrainageInfo = VersionDrainageInfo +class ComputeStatus(google.protobuf.message.Message): + """ComputeStatus represents compute-related configuration and health for a Worker Deployment Version.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class ProviderValidationStatus(google.protobuf.message.Message): + """ProviderValidationStatus represents the result of the most recent + connectivity check between Temporal and a customer's compute provider. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ERROR_MESSAGE_FIELD_NUMBER: builtins.int + LAST_CHECK_TIME_FIELD_NUMBER: builtins.int + error_message: builtins.str + """Human-readable error message if connectivity validation failed. + An empty string means validation passed. + """ + @property + def last_check_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Timestamp of the last validation check.""" + def __init__( + self, + *, + error_message: builtins.str = ..., + last_check_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "last_check_time", b"last_check_time" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "error_message", b"error_message", "last_check_time", b"last_check_time" + ], + ) -> None: ... + + PROVIDER_VALIDATION_FIELD_NUMBER: builtins.int + @property + def provider_validation(self) -> global___ComputeStatus.ProviderValidationStatus: + """provider_validation encapsulates the health signal for validating the compute provider.""" + def __init__( + self, + *, + provider_validation: global___ComputeStatus.ProviderValidationStatus + | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "provider_validation", b"provider_validation" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "provider_validation", b"provider_validation" + ], + ) -> None: ... + +global___ComputeStatus = ComputeStatus + class WorkerDeploymentInfo(google.protobuf.message.Message): """A Worker Deployment (Deployment, for short) represents all workers serving a shared set of Task Queues. Typically, a Deployment represents one service or @@ -588,7 +687,6 @@ class WorkerDeploymentInfo(google.protobuf.message.Message): version of workers. (see documentation of WorkerDeploymentVersionInfo) Deployment records are created in Temporal server automatically when their first poller arrives to the server. - Experimental. Worker Deployments are experimental and might significantly change in the future. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -606,7 +704,10 @@ class WorkerDeploymentInfo(google.protobuf.message.Message): RAMPING_SINCE_TIME_FIELD_NUMBER: builtins.int ROUTING_UPDATE_TIME_FIELD_NUMBER: builtins.int FIRST_ACTIVATION_TIME_FIELD_NUMBER: builtins.int + LAST_CURRENT_TIME_FIELD_NUMBER: builtins.int LAST_DEACTIVATION_TIME_FIELD_NUMBER: builtins.int + COMPUTE_CONFIG_FIELD_NUMBER: builtins.int + COMPUTE_STATUS_FIELD_NUMBER: builtins.int version: builtins.str """Deprecated. Use `deployment_version`.""" status: temporalio.api.enums.v1.deployment_pb2.WorkerDeploymentVersionStatus.ValueType @@ -644,8 +745,22 @@ class WorkerDeploymentInfo(google.protobuf.message.Message): def first_activation_time(self) -> google.protobuf.timestamp_pb2.Timestamp: """Timestamp when this version first became current or ramping.""" @property + def last_current_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Timestamp when this version last became current. + Can be used to determine whether a version has ever been Current. + """ + @property def last_deactivation_time(self) -> google.protobuf.timestamp_pb2.Timestamp: - """Timestamp when this version last stopped being current or ramping.""" + """Timestamp when this version last stopped being current or ramping. + Cleared if the version becomes current or ramping again. + """ + @property + def compute_config( + self, + ) -> temporalio.api.compute.v1.config_pb2.ComputeConfigSummary: ... + @property + def compute_status(self) -> global___ComputeStatus: + """ComputeStatus represents compute-related configuration and healthchecks.""" def __init__( self, *, @@ -659,12 +774,20 @@ class WorkerDeploymentInfo(google.protobuf.message.Message): ramping_since_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., routing_update_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., first_activation_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + last_current_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., last_deactivation_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + compute_config: temporalio.api.compute.v1.config_pb2.ComputeConfigSummary + | None = ..., + compute_status: global___ComputeStatus | None = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal[ + "compute_config", + b"compute_config", + "compute_status", + b"compute_status", "create_time", b"create_time", "current_since_time", @@ -675,6 +798,8 @@ class WorkerDeploymentInfo(google.protobuf.message.Message): b"drainage_info", "first_activation_time", b"first_activation_time", + "last_current_time", + b"last_current_time", "last_deactivation_time", b"last_deactivation_time", "ramping_since_time", @@ -686,6 +811,10 @@ class WorkerDeploymentInfo(google.protobuf.message.Message): def ClearField( self, field_name: typing_extensions.Literal[ + "compute_config", + b"compute_config", + "compute_status", + b"compute_status", "create_time", b"create_time", "current_since_time", @@ -698,6 +827,8 @@ class WorkerDeploymentInfo(google.protobuf.message.Message): b"drainage_status", "first_activation_time", b"first_activation_time", + "last_current_time", + b"last_current_time", "last_deactivation_time", b"last_deactivation_time", "ramping_since_time", @@ -716,6 +847,8 @@ class WorkerDeploymentInfo(google.protobuf.message.Message): CREATE_TIME_FIELD_NUMBER: builtins.int ROUTING_CONFIG_FIELD_NUMBER: builtins.int LAST_MODIFIER_IDENTITY_FIELD_NUMBER: builtins.int + MANAGER_IDENTITY_FIELD_NUMBER: builtins.int + ROUTING_CONFIG_UPDATE_STATE_FIELD_NUMBER: builtins.int name: builtins.str """Identifies a Worker Deployment. Must be unique within the namespace.""" @property @@ -739,6 +872,18 @@ class WorkerDeploymentInfo(google.protobuf.message.Message): `identity` value sent by APIs such as `SetWorkerDeploymentCurrentVersion` and `SetWorkerDeploymentRampingVersion`. """ + manager_identity: builtins.str + """Identity of the client that has the exclusive right to make changes to this Worker Deployment. + Empty by default. + If this is set, clients whose identity does not match `manager_identity` will not be able to make changes + to this Worker Deployment. They can either set their own identity as the manager or unset the field to proceed. + """ + routing_config_update_state: ( + temporalio.api.enums.v1.task_queue_pb2.RoutingConfigUpdateState.ValueType + ) + """Indicates whether the routing_config has been fully propagated to all + relevant task queues and their partitions. + """ def __init__( self, *, @@ -750,6 +895,8 @@ class WorkerDeploymentInfo(google.protobuf.message.Message): create_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., routing_config: global___RoutingConfig | None = ..., last_modifier_identity: builtins.str = ..., + manager_identity: builtins.str = ..., + routing_config_update_state: temporalio.api.enums.v1.task_queue_pb2.RoutingConfigUpdateState.ValueType = ..., ) -> None: ... def HasField( self, @@ -764,10 +911,14 @@ class WorkerDeploymentInfo(google.protobuf.message.Message): b"create_time", "last_modifier_identity", b"last_modifier_identity", + "manager_identity", + b"manager_identity", "name", b"name", "routing_config", b"routing_config", + "routing_config_update_state", + b"routing_config_update_state", "version_summaries", b"version_summaries", ], @@ -868,6 +1019,7 @@ class RoutingConfig(google.protobuf.message.Message): CURRENT_VERSION_CHANGED_TIME_FIELD_NUMBER: builtins.int RAMPING_VERSION_CHANGED_TIME_FIELD_NUMBER: builtins.int RAMPING_VERSION_PERCENTAGE_CHANGED_TIME_FIELD_NUMBER: builtins.int + REVISION_NUMBER_FIELD_NUMBER: builtins.int @property def current_deployment_version(self) -> global___WorkerDeploymentVersion: """Specifies which Deployment Version should receive new workflow executions and tasks of @@ -907,6 +1059,10 @@ class RoutingConfig(google.protobuf.message.Message): """Last time ramping version percentage was changed. If ramping version is changed, this is also updated, even if the percentage stays the same. """ + revision_number: builtins.int + """Monotonically increasing value which is incremented on every mutation + to any field of this message to achieve eventual consistency between task queues and their partitions. + """ def __init__( self, *, @@ -921,6 +1077,7 @@ class RoutingConfig(google.protobuf.message.Message): | None = ..., ramping_version_percentage_changed_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + revision_number: builtins.int = ..., ) -> None: ... def HasField( self, @@ -956,7 +1113,66 @@ class RoutingConfig(google.protobuf.message.Message): b"ramping_version_percentage", "ramping_version_percentage_changed_time", b"ramping_version_percentage_changed_time", + "revision_number", + b"revision_number", ], ) -> None: ... global___RoutingConfig = RoutingConfig + +class InheritedAutoUpgradeInfo(google.protobuf.message.Message): + """Used as part of WorkflowExecutionStartedEventAttributes to pass down the AutoUpgrade behavior and source deployment version + to a workflow execution whose parent/previous workflow has an AutoUpgrade behavior. + Also used for Upgrade-on-CaN behaviors AutoUpgrade and UseRampingVersion. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SOURCE_DEPLOYMENT_VERSION_FIELD_NUMBER: builtins.int + SOURCE_DEPLOYMENT_REVISION_NUMBER_FIELD_NUMBER: builtins.int + CONTINUE_AS_NEW_INITIAL_VERSIONING_BEHAVIOR_FIELD_NUMBER: builtins.int + @property + def source_deployment_version(self) -> global___WorkerDeploymentVersion: + """The source deployment version of the parent/previous workflow.""" + source_deployment_revision_number: builtins.int + """The revision number of the source deployment version of the parent/previous workflow.""" + continue_as_new_initial_versioning_behavior: ( + temporalio.api.enums.v1.workflow_pb2.ContinueAsNewVersioningBehavior.ValueType + ) + """Experimental. + If this workflow is the result of a continue-as-new, this field is set to the initial_versioning_behavior + specified in that command. + Only used for the initial task of this run and the initial task of any retries of this run. + Not passed to children or to future continue-as-new. + + Note: In the first release of Upgrade-on-CaN, when the only ContinueAsNewVersioningBehavior was AutoUpgrade, + a non-empty InheritedAutoUpgradeInfo meant that the workflow should start as AutoUpgrade. So for compatibility + with history events generated during that time, know that an UNSPECIFIED value here is equivalent to AutoUpgrade + value if the InheritedAutoUpgradeInfo is non-empty. + """ + def __init__( + self, + *, + source_deployment_version: global___WorkerDeploymentVersion | None = ..., + source_deployment_revision_number: builtins.int = ..., + continue_as_new_initial_versioning_behavior: temporalio.api.enums.v1.workflow_pb2.ContinueAsNewVersioningBehavior.ValueType = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "source_deployment_version", b"source_deployment_version" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "continue_as_new_initial_versioning_behavior", + b"continue_as_new_initial_versioning_behavior", + "source_deployment_revision_number", + b"source_deployment_revision_number", + "source_deployment_version", + b"source_deployment_version", + ], + ) -> None: ... + +global___InheritedAutoUpgradeInfo = InheritedAutoUpgradeInfo diff --git a/temporalio/api/enums/v1/__init__.py b/temporalio/api/enums/v1/__init__.py index 156d02e86..3dc3a179b 100644 --- a/temporalio/api/enums/v1/__init__.py +++ b/temporalio/api/enums/v1/__init__.py @@ -1,9 +1,15 @@ +from .activity_pb2 import ( + ActivityExecutionStatus, + ActivityIdConflictPolicy, + ActivityIdReusePolicy, +) from .batch_operation_pb2 import BatchOperationState, BatchOperationType from .command_type_pb2 import CommandType from .common_pb2 import ( ApplicationErrorCategory, CallbackState, EncodingType, + ExecutionType, IndexedValueType, NexusOperationCancellationState, PendingNexusOperationState, @@ -27,7 +33,13 @@ WorkflowTaskFailedCause, ) from .namespace_pb2 import ArchivalState, NamespaceState, ReplicationState -from .nexus_pb2 import NexusHandlerErrorRetryBehavior +from .nexus_pb2 import ( + NexusHandlerErrorRetryBehavior, + NexusOperationExecutionStatus, + NexusOperationIdConflictPolicy, + NexusOperationIdReusePolicy, + NexusOperationWaitStage, +) from .query_pb2 import QueryRejectCondition, QueryResultType from .reset_pb2 import ResetReapplyExcludeType, ResetReapplyType, ResetType from .schedule_pb2 import ScheduleOverlapPolicy @@ -35,18 +47,22 @@ BuildIdTaskReachability, DescribeTaskQueueMode, RateLimitSource, + RoutingConfigUpdateState, TaskQueueKind, TaskQueueType, TaskReachability, ) +from .time_skipping_pb2 import FastForwardPollingResult from .update_pb2 import UpdateAdmittedEventOrigin, UpdateWorkflowExecutionLifecycleStage from .workflow_pb2 import ( ContinueAsNewInitiator, + ContinueAsNewVersioningBehavior, HistoryEventFilterType, ParentClosePolicy, PendingActivityState, PendingWorkflowTaskState, RetryState, + SuggestContinueAsNewReason, TimeoutType, VersioningBehavior, WorkflowExecutionStatus, @@ -55,6 +71,9 @@ ) __all__ = [ + "ActivityExecutionStatus", + "ActivityIdConflictPolicy", + "ActivityIdReusePolicy", "ApplicationErrorCategory", "ArchivalState", "BatchOperationState", @@ -64,15 +83,22 @@ "CancelExternalWorkflowExecutionFailedCause", "CommandType", "ContinueAsNewInitiator", + "ContinueAsNewVersioningBehavior", "DeploymentReachability", "DescribeTaskQueueMode", "EncodingType", "EventType", + "ExecutionType", + "FastForwardPollingResult", "HistoryEventFilterType", "IndexedValueType", "NamespaceState", "NexusHandlerErrorRetryBehavior", "NexusOperationCancellationState", + "NexusOperationExecutionStatus", + "NexusOperationIdConflictPolicy", + "NexusOperationIdReusePolicy", + "NexusOperationWaitStage", "ParentClosePolicy", "PendingActivityState", "PendingNexusOperationState", @@ -87,10 +113,12 @@ "ResourceExhaustedCause", "ResourceExhaustedScope", "RetryState", + "RoutingConfigUpdateState", "ScheduleOverlapPolicy", "Severity", "SignalExternalWorkflowExecutionFailedCause", "StartChildWorkflowExecutionFailedCause", + "SuggestContinueAsNewReason", "TaskQueueKind", "TaskQueueType", "TaskReachability", diff --git a/temporalio/api/enums/v1/activity_pb2.py b/temporalio/api/enums/v1/activity_pb2.py new file mode 100644 index 000000000..fd709beb6 --- /dev/null +++ b/temporalio/api/enums/v1/activity_pb2.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: temporal/api/enums/v1/activity.proto +"""Generated protocol buffer code.""" + +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import enum_type_wrapper + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b"\n$temporal/api/enums/v1/activity.proto\x12\x15temporal.api.enums.v1*\xdb\x02\n\x17\x41\x63tivityExecutionStatus\x12)\n%ACTIVITY_EXECUTION_STATUS_UNSPECIFIED\x10\x00\x12%\n!ACTIVITY_EXECUTION_STATUS_RUNNING\x10\x01\x12'\n#ACTIVITY_EXECUTION_STATUS_COMPLETED\x10\x02\x12$\n ACTIVITY_EXECUTION_STATUS_FAILED\x10\x03\x12&\n\"ACTIVITY_EXECUTION_STATUS_CANCELED\x10\x04\x12(\n$ACTIVITY_EXECUTION_STATUS_TERMINATED\x10\x05\x12'\n#ACTIVITY_EXECUTION_STATUS_TIMED_OUT\x10\x06\x12$\n ACTIVITY_EXECUTION_STATUS_PAUSED\x10\x07*\xd8\x01\n\x15\x41\x63tivityIdReusePolicy\x12(\n$ACTIVITY_ID_REUSE_POLICY_UNSPECIFIED\x10\x00\x12,\n(ACTIVITY_ID_REUSE_POLICY_ALLOW_DUPLICATE\x10\x01\x12\x38\n4ACTIVITY_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY\x10\x02\x12-\n)ACTIVITY_ID_REUSE_POLICY_REJECT_DUPLICATE\x10\x03*\x9b\x01\n\x18\x41\x63tivityIdConflictPolicy\x12+\n'ACTIVITY_ID_CONFLICT_POLICY_UNSPECIFIED\x10\x00\x12$\n ACTIVITY_ID_CONFLICT_POLICY_FAIL\x10\x01\x12,\n(ACTIVITY_ID_CONFLICT_POLICY_USE_EXISTING\x10\x02\x42\x85\x01\n\x18io.temporal.api.enums.v1B\rActivityProtoP\x01Z!go.temporal.io/api/enums/v1;enums\xaa\x02\x17Temporalio.Api.Enums.V1\xea\x02\x1aTemporalio::Api::Enums::V1b\x06proto3" +) + +_ACTIVITYEXECUTIONSTATUS = DESCRIPTOR.enum_types_by_name["ActivityExecutionStatus"] +ActivityExecutionStatus = enum_type_wrapper.EnumTypeWrapper(_ACTIVITYEXECUTIONSTATUS) +_ACTIVITYIDREUSEPOLICY = DESCRIPTOR.enum_types_by_name["ActivityIdReusePolicy"] +ActivityIdReusePolicy = enum_type_wrapper.EnumTypeWrapper(_ACTIVITYIDREUSEPOLICY) +_ACTIVITYIDCONFLICTPOLICY = DESCRIPTOR.enum_types_by_name["ActivityIdConflictPolicy"] +ActivityIdConflictPolicy = enum_type_wrapper.EnumTypeWrapper(_ACTIVITYIDCONFLICTPOLICY) +ACTIVITY_EXECUTION_STATUS_UNSPECIFIED = 0 +ACTIVITY_EXECUTION_STATUS_RUNNING = 1 +ACTIVITY_EXECUTION_STATUS_COMPLETED = 2 +ACTIVITY_EXECUTION_STATUS_FAILED = 3 +ACTIVITY_EXECUTION_STATUS_CANCELED = 4 +ACTIVITY_EXECUTION_STATUS_TERMINATED = 5 +ACTIVITY_EXECUTION_STATUS_TIMED_OUT = 6 +ACTIVITY_EXECUTION_STATUS_PAUSED = 7 +ACTIVITY_ID_REUSE_POLICY_UNSPECIFIED = 0 +ACTIVITY_ID_REUSE_POLICY_ALLOW_DUPLICATE = 1 +ACTIVITY_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY = 2 +ACTIVITY_ID_REUSE_POLICY_REJECT_DUPLICATE = 3 +ACTIVITY_ID_CONFLICT_POLICY_UNSPECIFIED = 0 +ACTIVITY_ID_CONFLICT_POLICY_FAIL = 1 +ACTIVITY_ID_CONFLICT_POLICY_USE_EXISTING = 2 + + +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b"\n\030io.temporal.api.enums.v1B\rActivityProtoP\001Z!go.temporal.io/api/enums/v1;enums\252\002\027Temporalio.Api.Enums.V1\352\002\032Temporalio::Api::Enums::V1" + _ACTIVITYEXECUTIONSTATUS._serialized_start = 64 + _ACTIVITYEXECUTIONSTATUS._serialized_end = 411 + _ACTIVITYIDREUSEPOLICY._serialized_start = 414 + _ACTIVITYIDREUSEPOLICY._serialized_end = 630 + _ACTIVITYIDCONFLICTPOLICY._serialized_start = 633 + _ACTIVITYIDCONFLICTPOLICY._serialized_end = 788 +# @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/enums/v1/activity_pb2.pyi b/temporalio/api/enums/v1/activity_pb2.pyi new file mode 100644 index 000000000..cadaedb37 --- /dev/null +++ b/temporalio/api/enums/v1/activity_pb2.pyi @@ -0,0 +1,200 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" + +import builtins +import sys +import typing + +import google.protobuf.descriptor +import google.protobuf.internal.enum_type_wrapper + +if sys.version_info >= (3, 10): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class _ActivityExecutionStatus: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _ActivityExecutionStatusEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ + _ActivityExecutionStatus.ValueType + ], + builtins.type, +): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + ACTIVITY_EXECUTION_STATUS_UNSPECIFIED: _ActivityExecutionStatus.ValueType # 0 + ACTIVITY_EXECUTION_STATUS_RUNNING: _ActivityExecutionStatus.ValueType # 1 + """The activity has not reached a terminal status. See PendingActivityState for the run state + (SCHEDULED, STARTED, or CANCEL_REQUESTED). + """ + ACTIVITY_EXECUTION_STATUS_COMPLETED: _ActivityExecutionStatus.ValueType # 2 + """The activity completed successfully. An activity can complete even after cancellation is + requested if the worker calls RespondActivityTaskCompleted before acknowledging cancellation. + """ + ACTIVITY_EXECUTION_STATUS_FAILED: _ActivityExecutionStatus.ValueType # 3 + """The activity failed. Causes: + - Worker returned a non-retryable failure + - RetryPolicy.maximum_attempts exhausted + - Attempt failed after cancellation was requested (retries blocked) + """ + ACTIVITY_EXECUTION_STATUS_CANCELED: _ActivityExecutionStatus.ValueType # 4 + """The activity was canceled. Reached when: + - Cancellation requested while SCHEDULED (immediate), or + - Cancellation requested while STARTED and worker called RespondActivityTaskCanceled. + + Workers discover cancellation requests via heartbeat responses (cancel_requested=true). + Activities that do not heartbeat will not learn of cancellation and may complete, fail, or + time out normally. CANCELED requires explicit worker acknowledgment or immediate cancellation + of a SCHEDULED activity. + """ + ACTIVITY_EXECUTION_STATUS_TERMINATED: _ActivityExecutionStatus.ValueType # 5 + """The activity was terminated. Immediate; does not wait for worker acknowledgment.""" + ACTIVITY_EXECUTION_STATUS_TIMED_OUT: _ActivityExecutionStatus.ValueType # 6 + """The activity timed out. See TimeoutType for the specific timeout. + - SCHEDULE_TO_START and SCHEDULE_TO_CLOSE timeouts always result in TIMED_OUT. + - START_TO_CLOSE and HEARTBEAT may retry if RetryPolicy permits; TIMED_OUT is + reached when retry is blocked (RetryPolicy.maximum_attempts exhausted, + SCHEDULE_TO_CLOSE would be exceeded, or cancellation has been requested). + """ + ACTIVITY_EXECUTION_STATUS_PAUSED: _ActivityExecutionStatus.ValueType # 7 + """The activity is paused. Paused state is only reachable after calling + PauseActivityExecution on a standalone activity. + """ + +class ActivityExecutionStatus( + _ActivityExecutionStatus, metaclass=_ActivityExecutionStatusEnumTypeWrapper +): + """Status of a standalone activity. + The status is updated when the activity is originally scheduled, paused, unpaused, and when the + activity reaches a terminal state. + (-- api-linter: core::0216::synonyms=disabled + aip.dev/not-precedent: Named consistently with WorkflowExecutionStatus. --) + """ + +ACTIVITY_EXECUTION_STATUS_UNSPECIFIED: ActivityExecutionStatus.ValueType # 0 +ACTIVITY_EXECUTION_STATUS_RUNNING: ActivityExecutionStatus.ValueType # 1 +"""The activity has not reached a terminal status. See PendingActivityState for the run state +(SCHEDULED, STARTED, or CANCEL_REQUESTED). +""" +ACTIVITY_EXECUTION_STATUS_COMPLETED: ActivityExecutionStatus.ValueType # 2 +"""The activity completed successfully. An activity can complete even after cancellation is +requested if the worker calls RespondActivityTaskCompleted before acknowledging cancellation. +""" +ACTIVITY_EXECUTION_STATUS_FAILED: ActivityExecutionStatus.ValueType # 3 +"""The activity failed. Causes: +- Worker returned a non-retryable failure +- RetryPolicy.maximum_attempts exhausted +- Attempt failed after cancellation was requested (retries blocked) +""" +ACTIVITY_EXECUTION_STATUS_CANCELED: ActivityExecutionStatus.ValueType # 4 +"""The activity was canceled. Reached when: +- Cancellation requested while SCHEDULED (immediate), or +- Cancellation requested while STARTED and worker called RespondActivityTaskCanceled. + +Workers discover cancellation requests via heartbeat responses (cancel_requested=true). +Activities that do not heartbeat will not learn of cancellation and may complete, fail, or +time out normally. CANCELED requires explicit worker acknowledgment or immediate cancellation +of a SCHEDULED activity. +""" +ACTIVITY_EXECUTION_STATUS_TERMINATED: ActivityExecutionStatus.ValueType # 5 +"""The activity was terminated. Immediate; does not wait for worker acknowledgment.""" +ACTIVITY_EXECUTION_STATUS_TIMED_OUT: ActivityExecutionStatus.ValueType # 6 +"""The activity timed out. See TimeoutType for the specific timeout. +- SCHEDULE_TO_START and SCHEDULE_TO_CLOSE timeouts always result in TIMED_OUT. +- START_TO_CLOSE and HEARTBEAT may retry if RetryPolicy permits; TIMED_OUT is + reached when retry is blocked (RetryPolicy.maximum_attempts exhausted, + SCHEDULE_TO_CLOSE would be exceeded, or cancellation has been requested). +""" +ACTIVITY_EXECUTION_STATUS_PAUSED: ActivityExecutionStatus.ValueType # 7 +"""The activity is paused. Paused state is only reachable after calling +PauseActivityExecution on a standalone activity. +""" +global___ActivityExecutionStatus = ActivityExecutionStatus + +class _ActivityIdReusePolicy: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _ActivityIdReusePolicyEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ + _ActivityIdReusePolicy.ValueType + ], + builtins.type, +): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + ACTIVITY_ID_REUSE_POLICY_UNSPECIFIED: _ActivityIdReusePolicy.ValueType # 0 + ACTIVITY_ID_REUSE_POLICY_ALLOW_DUPLICATE: _ActivityIdReusePolicy.ValueType # 1 + """Always allow starting an activity using the same activity ID.""" + ACTIVITY_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY: ( + _ActivityIdReusePolicy.ValueType + ) # 2 + """Allow starting an activity using the same ID only when the last activity's final state is one + of {failed, canceled, terminated, timed out}. + """ + ACTIVITY_ID_REUSE_POLICY_REJECT_DUPLICATE: _ActivityIdReusePolicy.ValueType # 3 + """Do not permit re-use of the ID for this activity. Future start requests could potentially change the policy, + allowing re-use of the ID. + """ + +class ActivityIdReusePolicy( + _ActivityIdReusePolicy, metaclass=_ActivityIdReusePolicyEnumTypeWrapper +): + """Defines whether to allow re-using an activity ID from a previously *closed* activity. + If the request is denied, the server returns an `ActivityExecutionAlreadyStarted` error. + + See `ActivityIdConflictPolicy` for handling ID duplication with a *running* activity. + """ + +ACTIVITY_ID_REUSE_POLICY_UNSPECIFIED: ActivityIdReusePolicy.ValueType # 0 +ACTIVITY_ID_REUSE_POLICY_ALLOW_DUPLICATE: ActivityIdReusePolicy.ValueType # 1 +"""Always allow starting an activity using the same activity ID.""" +ACTIVITY_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY: ( + ActivityIdReusePolicy.ValueType +) # 2 +"""Allow starting an activity using the same ID only when the last activity's final state is one +of {failed, canceled, terminated, timed out}. +""" +ACTIVITY_ID_REUSE_POLICY_REJECT_DUPLICATE: ActivityIdReusePolicy.ValueType # 3 +"""Do not permit re-use of the ID for this activity. Future start requests could potentially change the policy, +allowing re-use of the ID. +""" +global___ActivityIdReusePolicy = ActivityIdReusePolicy + +class _ActivityIdConflictPolicy: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _ActivityIdConflictPolicyEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ + _ActivityIdConflictPolicy.ValueType + ], + builtins.type, +): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + ACTIVITY_ID_CONFLICT_POLICY_UNSPECIFIED: _ActivityIdConflictPolicy.ValueType # 0 + ACTIVITY_ID_CONFLICT_POLICY_FAIL: _ActivityIdConflictPolicy.ValueType # 1 + """Don't start a new activity; instead return `ActivityExecutionAlreadyStarted` error.""" + ACTIVITY_ID_CONFLICT_POLICY_USE_EXISTING: _ActivityIdConflictPolicy.ValueType # 2 + """Don't start a new activity; instead return a handle for the running activity.""" + +class ActivityIdConflictPolicy( + _ActivityIdConflictPolicy, metaclass=_ActivityIdConflictPolicyEnumTypeWrapper +): + """Defines what to do when trying to start an activity with the same ID as a *running* activity. + Note that it is *never* valid to have two running instances of the same activity ID. + + See `ActivityIdReusePolicy` for handling activity ID duplication with a *closed* activity. + """ + +ACTIVITY_ID_CONFLICT_POLICY_UNSPECIFIED: ActivityIdConflictPolicy.ValueType # 0 +ACTIVITY_ID_CONFLICT_POLICY_FAIL: ActivityIdConflictPolicy.ValueType # 1 +"""Don't start a new activity; instead return `ActivityExecutionAlreadyStarted` error.""" +ACTIVITY_ID_CONFLICT_POLICY_USE_EXISTING: ActivityIdConflictPolicy.ValueType # 2 +"""Don't start a new activity; instead return a handle for the running activity.""" +global___ActivityIdConflictPolicy = ActivityIdConflictPolicy diff --git a/temporalio/api/enums/v1/batch_operation_pb2.py b/temporalio/api/enums/v1/batch_operation_pb2.py index 60c9d7f2f..73d4e011b 100644 --- a/temporalio/api/enums/v1/batch_operation_pb2.py +++ b/temporalio/api/enums/v1/batch_operation_pb2.py @@ -16,7 +16,7 @@ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b"\n+temporal/api/enums/v1/batch_operation.proto\x12\x15temporal.api.enums.v1*\x9a\x03\n\x12\x42\x61tchOperationType\x12$\n BATCH_OPERATION_TYPE_UNSPECIFIED\x10\x00\x12\"\n\x1e\x42\x41TCH_OPERATION_TYPE_TERMINATE\x10\x01\x12\x1f\n\x1b\x42\x41TCH_OPERATION_TYPE_CANCEL\x10\x02\x12\x1f\n\x1b\x42\x41TCH_OPERATION_TYPE_SIGNAL\x10\x03\x12\x1f\n\x1b\x42\x41TCH_OPERATION_TYPE_DELETE\x10\x04\x12\x1e\n\x1a\x42\x41TCH_OPERATION_TYPE_RESET\x10\x05\x12\x31\n-BATCH_OPERATION_TYPE_UPDATE_EXECUTION_OPTIONS\x10\x06\x12)\n%BATCH_OPERATION_TYPE_UNPAUSE_ACTIVITY\x10\x07\x12\x30\n,BATCH_OPERATION_TYPE_UPDATE_ACTIVITY_OPTIONS\x10\x08\x12'\n#BATCH_OPERATION_TYPE_RESET_ACTIVITY\x10\t*\xa6\x01\n\x13\x42\x61tchOperationState\x12%\n!BATCH_OPERATION_STATE_UNSPECIFIED\x10\x00\x12!\n\x1d\x42\x41TCH_OPERATION_STATE_RUNNING\x10\x01\x12#\n\x1f\x42\x41TCH_OPERATION_STATE_COMPLETED\x10\x02\x12 \n\x1c\x42\x41TCH_OPERATION_STATE_FAILED\x10\x03\x42\x8b\x01\n\x18io.temporal.api.enums.v1B\x13\x42\x61tchOperationProtoP\x01Z!go.temporal.io/api/enums/v1;enums\xaa\x02\x17Temporalio.Api.Enums.V1\xea\x02\x1aTemporalio::Api::Enums::V1b\x06proto3" + b"\n+temporal/api/enums/v1/batch_operation.proto\x12\x15temporal.api.enums.v1*\xc3\x06\n\x12\x42\x61tchOperationType\x12$\n BATCH_OPERATION_TYPE_UNSPECIFIED\x10\x00\x12&\n\x1e\x42\x41TCH_OPERATION_TYPE_TERMINATE\x10\x01\x1a\x02\x08\x01\x12+\n'BATCH_OPERATION_TYPE_TERMINATE_WORKFLOW\x10\r\x12#\n\x1b\x42\x41TCH_OPERATION_TYPE_CANCEL\x10\x02\x1a\x02\x08\x01\x12(\n$BATCH_OPERATION_TYPE_CANCEL_WORKFLOW\x10\x0e\x12#\n\x1b\x42\x41TCH_OPERATION_TYPE_SIGNAL\x10\x03\x1a\x02\x08\x01\x12(\n$BATCH_OPERATION_TYPE_SIGNAL_WORKFLOW\x10\x0f\x12#\n\x1b\x42\x41TCH_OPERATION_TYPE_DELETE\x10\x04\x1a\x02\x08\x01\x12(\n$BATCH_OPERATION_TYPE_DELETE_WORKFLOW\x10\x10\x12\"\n\x1a\x42\x41TCH_OPERATION_TYPE_RESET\x10\x05\x1a\x02\x08\x01\x12'\n#BATCH_OPERATION_TYPE_RESET_WORKFLOW\x10\x11\x12\x35\n-BATCH_OPERATION_TYPE_UPDATE_EXECUTION_OPTIONS\x10\x06\x1a\x02\x08\x01\x12:\n6BATCH_OPERATION_TYPE_UPDATE_WORKFLOW_EXECUTION_OPTIONS\x10\x12\x12)\n%BATCH_OPERATION_TYPE_UNPAUSE_ACTIVITY\x10\x07\x12\x30\n,BATCH_OPERATION_TYPE_UPDATE_ACTIVITY_OPTIONS\x10\x08\x12'\n#BATCH_OPERATION_TYPE_RESET_ACTIVITY\x10\t\x12+\n'BATCH_OPERATION_TYPE_TERMINATE_ACTIVITY\x10\n\x12(\n$BATCH_OPERATION_TYPE_CANCEL_ACTIVITY\x10\x0b\x12(\n$BATCH_OPERATION_TYPE_DELETE_ACTIVITY\x10\x0c*\xa6\x01\n\x13\x42\x61tchOperationState\x12%\n!BATCH_OPERATION_STATE_UNSPECIFIED\x10\x00\x12!\n\x1d\x42\x41TCH_OPERATION_STATE_RUNNING\x10\x01\x12#\n\x1f\x42\x41TCH_OPERATION_STATE_COMPLETED\x10\x02\x12 \n\x1c\x42\x41TCH_OPERATION_STATE_FAILED\x10\x03\x42\x8b\x01\n\x18io.temporal.api.enums.v1B\x13\x42\x61tchOperationProtoP\x01Z!go.temporal.io/api/enums/v1;enums\xaa\x02\x17Temporalio.Api.Enums.V1\xea\x02\x1aTemporalio::Api::Enums::V1b\x06proto3" ) _BATCHOPERATIONTYPE = DESCRIPTOR.enum_types_by_name["BatchOperationType"] @@ -25,14 +25,23 @@ BatchOperationState = enum_type_wrapper.EnumTypeWrapper(_BATCHOPERATIONSTATE) BATCH_OPERATION_TYPE_UNSPECIFIED = 0 BATCH_OPERATION_TYPE_TERMINATE = 1 +BATCH_OPERATION_TYPE_TERMINATE_WORKFLOW = 13 BATCH_OPERATION_TYPE_CANCEL = 2 +BATCH_OPERATION_TYPE_CANCEL_WORKFLOW = 14 BATCH_OPERATION_TYPE_SIGNAL = 3 +BATCH_OPERATION_TYPE_SIGNAL_WORKFLOW = 15 BATCH_OPERATION_TYPE_DELETE = 4 +BATCH_OPERATION_TYPE_DELETE_WORKFLOW = 16 BATCH_OPERATION_TYPE_RESET = 5 +BATCH_OPERATION_TYPE_RESET_WORKFLOW = 17 BATCH_OPERATION_TYPE_UPDATE_EXECUTION_OPTIONS = 6 +BATCH_OPERATION_TYPE_UPDATE_WORKFLOW_EXECUTION_OPTIONS = 18 BATCH_OPERATION_TYPE_UNPAUSE_ACTIVITY = 7 BATCH_OPERATION_TYPE_UPDATE_ACTIVITY_OPTIONS = 8 BATCH_OPERATION_TYPE_RESET_ACTIVITY = 9 +BATCH_OPERATION_TYPE_TERMINATE_ACTIVITY = 10 +BATCH_OPERATION_TYPE_CANCEL_ACTIVITY = 11 +BATCH_OPERATION_TYPE_DELETE_ACTIVITY = 12 BATCH_OPERATION_STATE_UNSPECIFIED = 0 BATCH_OPERATION_STATE_RUNNING = 1 BATCH_OPERATION_STATE_COMPLETED = 2 @@ -42,8 +51,34 @@ if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b"\n\030io.temporal.api.enums.v1B\023BatchOperationProtoP\001Z!go.temporal.io/api/enums/v1;enums\252\002\027Temporalio.Api.Enums.V1\352\002\032Temporalio::Api::Enums::V1" + _BATCHOPERATIONTYPE.values_by_name["BATCH_OPERATION_TYPE_TERMINATE"]._options = None + _BATCHOPERATIONTYPE.values_by_name[ + "BATCH_OPERATION_TYPE_TERMINATE" + ]._serialized_options = b"\010\001" + _BATCHOPERATIONTYPE.values_by_name["BATCH_OPERATION_TYPE_CANCEL"]._options = None + _BATCHOPERATIONTYPE.values_by_name[ + "BATCH_OPERATION_TYPE_CANCEL" + ]._serialized_options = b"\010\001" + _BATCHOPERATIONTYPE.values_by_name["BATCH_OPERATION_TYPE_SIGNAL"]._options = None + _BATCHOPERATIONTYPE.values_by_name[ + "BATCH_OPERATION_TYPE_SIGNAL" + ]._serialized_options = b"\010\001" + _BATCHOPERATIONTYPE.values_by_name["BATCH_OPERATION_TYPE_DELETE"]._options = None + _BATCHOPERATIONTYPE.values_by_name[ + "BATCH_OPERATION_TYPE_DELETE" + ]._serialized_options = b"\010\001" + _BATCHOPERATIONTYPE.values_by_name["BATCH_OPERATION_TYPE_RESET"]._options = None + _BATCHOPERATIONTYPE.values_by_name[ + "BATCH_OPERATION_TYPE_RESET" + ]._serialized_options = b"\010\001" + _BATCHOPERATIONTYPE.values_by_name[ + "BATCH_OPERATION_TYPE_UPDATE_EXECUTION_OPTIONS" + ]._options = None + _BATCHOPERATIONTYPE.values_by_name[ + "BATCH_OPERATION_TYPE_UPDATE_EXECUTION_OPTIONS" + ]._serialized_options = b"\010\001" _BATCHOPERATIONTYPE._serialized_start = 71 - _BATCHOPERATIONTYPE._serialized_end = 481 - _BATCHOPERATIONSTATE._serialized_start = 484 - _BATCHOPERATIONSTATE._serialized_end = 650 + _BATCHOPERATIONTYPE._serialized_end = 906 + _BATCHOPERATIONSTATE._serialized_start = 909 + _BATCHOPERATIONSTATE._serialized_end = 1075 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/enums/v1/batch_operation_pb2.pyi b/temporalio/api/enums/v1/batch_operation_pb2.pyi index 49f426ebb..156ba51ed 100644 --- a/temporalio/api/enums/v1/batch_operation_pb2.pyi +++ b/temporalio/api/enums/v1/batch_operation_pb2.pyi @@ -30,14 +30,31 @@ class _BatchOperationTypeEnumTypeWrapper( DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor BATCH_OPERATION_TYPE_UNSPECIFIED: _BatchOperationType.ValueType # 0 BATCH_OPERATION_TYPE_TERMINATE: _BatchOperationType.ValueType # 1 + """DEPRECATED: Use BATCH_OPERATION_TYPE_TERMINATE_WORKFLOW instead.""" + BATCH_OPERATION_TYPE_TERMINATE_WORKFLOW: _BatchOperationType.ValueType # 13 BATCH_OPERATION_TYPE_CANCEL: _BatchOperationType.ValueType # 2 + """DEPRECATED: Use BATCH_OPERATION_TYPE_CANCEL_WORKFLOW instead.""" + BATCH_OPERATION_TYPE_CANCEL_WORKFLOW: _BatchOperationType.ValueType # 14 BATCH_OPERATION_TYPE_SIGNAL: _BatchOperationType.ValueType # 3 + """DEPRECATED: Use BATCH_OPERATION_TYPE_SIGNAL_WORKFLOW instead.""" + BATCH_OPERATION_TYPE_SIGNAL_WORKFLOW: _BatchOperationType.ValueType # 15 BATCH_OPERATION_TYPE_DELETE: _BatchOperationType.ValueType # 4 + """DEPRECATED: Use BATCH_OPERATION_TYPE_DELETE_WORKFLOW instead.""" + BATCH_OPERATION_TYPE_DELETE_WORKFLOW: _BatchOperationType.ValueType # 16 BATCH_OPERATION_TYPE_RESET: _BatchOperationType.ValueType # 5 + """DEPRECATED: Use BATCH_OPERATION_TYPE_RESET_WORKFLOW instead.""" + BATCH_OPERATION_TYPE_RESET_WORKFLOW: _BatchOperationType.ValueType # 17 BATCH_OPERATION_TYPE_UPDATE_EXECUTION_OPTIONS: _BatchOperationType.ValueType # 6 + """DEPRECATED: Use BATCH_OPERATION_TYPE_UPDATE_WORKFLOW_EXECUTION_OPTIONS instead.""" + BATCH_OPERATION_TYPE_UPDATE_WORKFLOW_EXECUTION_OPTIONS: ( + _BatchOperationType.ValueType + ) # 18 BATCH_OPERATION_TYPE_UNPAUSE_ACTIVITY: _BatchOperationType.ValueType # 7 BATCH_OPERATION_TYPE_UPDATE_ACTIVITY_OPTIONS: _BatchOperationType.ValueType # 8 BATCH_OPERATION_TYPE_RESET_ACTIVITY: _BatchOperationType.ValueType # 9 + BATCH_OPERATION_TYPE_TERMINATE_ACTIVITY: _BatchOperationType.ValueType # 10 + BATCH_OPERATION_TYPE_CANCEL_ACTIVITY: _BatchOperationType.ValueType # 11 + BATCH_OPERATION_TYPE_DELETE_ACTIVITY: _BatchOperationType.ValueType # 12 class BatchOperationType( _BatchOperationType, metaclass=_BatchOperationTypeEnumTypeWrapper @@ -45,14 +62,31 @@ class BatchOperationType( BATCH_OPERATION_TYPE_UNSPECIFIED: BatchOperationType.ValueType # 0 BATCH_OPERATION_TYPE_TERMINATE: BatchOperationType.ValueType # 1 +"""DEPRECATED: Use BATCH_OPERATION_TYPE_TERMINATE_WORKFLOW instead.""" +BATCH_OPERATION_TYPE_TERMINATE_WORKFLOW: BatchOperationType.ValueType # 13 BATCH_OPERATION_TYPE_CANCEL: BatchOperationType.ValueType # 2 +"""DEPRECATED: Use BATCH_OPERATION_TYPE_CANCEL_WORKFLOW instead.""" +BATCH_OPERATION_TYPE_CANCEL_WORKFLOW: BatchOperationType.ValueType # 14 BATCH_OPERATION_TYPE_SIGNAL: BatchOperationType.ValueType # 3 +"""DEPRECATED: Use BATCH_OPERATION_TYPE_SIGNAL_WORKFLOW instead.""" +BATCH_OPERATION_TYPE_SIGNAL_WORKFLOW: BatchOperationType.ValueType # 15 BATCH_OPERATION_TYPE_DELETE: BatchOperationType.ValueType # 4 +"""DEPRECATED: Use BATCH_OPERATION_TYPE_DELETE_WORKFLOW instead.""" +BATCH_OPERATION_TYPE_DELETE_WORKFLOW: BatchOperationType.ValueType # 16 BATCH_OPERATION_TYPE_RESET: BatchOperationType.ValueType # 5 +"""DEPRECATED: Use BATCH_OPERATION_TYPE_RESET_WORKFLOW instead.""" +BATCH_OPERATION_TYPE_RESET_WORKFLOW: BatchOperationType.ValueType # 17 BATCH_OPERATION_TYPE_UPDATE_EXECUTION_OPTIONS: BatchOperationType.ValueType # 6 +"""DEPRECATED: Use BATCH_OPERATION_TYPE_UPDATE_WORKFLOW_EXECUTION_OPTIONS instead.""" +BATCH_OPERATION_TYPE_UPDATE_WORKFLOW_EXECUTION_OPTIONS: ( + BatchOperationType.ValueType +) # 18 BATCH_OPERATION_TYPE_UNPAUSE_ACTIVITY: BatchOperationType.ValueType # 7 BATCH_OPERATION_TYPE_UPDATE_ACTIVITY_OPTIONS: BatchOperationType.ValueType # 8 BATCH_OPERATION_TYPE_RESET_ACTIVITY: BatchOperationType.ValueType # 9 +BATCH_OPERATION_TYPE_TERMINATE_ACTIVITY: BatchOperationType.ValueType # 10 +BATCH_OPERATION_TYPE_CANCEL_ACTIVITY: BatchOperationType.ValueType # 11 +BATCH_OPERATION_TYPE_DELETE_ACTIVITY: BatchOperationType.ValueType # 12 global___BatchOperationType = BatchOperationType class _BatchOperationState: diff --git a/temporalio/api/enums/v1/common_pb2.py b/temporalio/api/enums/v1/common_pb2.py index 557ce7631..a646df4ad 100644 --- a/temporalio/api/enums/v1/common_pb2.py +++ b/temporalio/api/enums/v1/common_pb2.py @@ -16,7 +16,7 @@ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b"\n\"temporal/api/enums/v1/common.proto\x12\x15temporal.api.enums.v1*_\n\x0c\x45ncodingType\x12\x1d\n\x19\x45NCODING_TYPE_UNSPECIFIED\x10\x00\x12\x18\n\x14\x45NCODING_TYPE_PROTO3\x10\x01\x12\x16\n\x12\x45NCODING_TYPE_JSON\x10\x02*\x91\x02\n\x10IndexedValueType\x12\"\n\x1eINDEXED_VALUE_TYPE_UNSPECIFIED\x10\x00\x12\x1b\n\x17INDEXED_VALUE_TYPE_TEXT\x10\x01\x12\x1e\n\x1aINDEXED_VALUE_TYPE_KEYWORD\x10\x02\x12\x1a\n\x16INDEXED_VALUE_TYPE_INT\x10\x03\x12\x1d\n\x19INDEXED_VALUE_TYPE_DOUBLE\x10\x04\x12\x1b\n\x17INDEXED_VALUE_TYPE_BOOL\x10\x05\x12\x1f\n\x1bINDEXED_VALUE_TYPE_DATETIME\x10\x06\x12#\n\x1fINDEXED_VALUE_TYPE_KEYWORD_LIST\x10\x07*^\n\x08Severity\x12\x18\n\x14SEVERITY_UNSPECIFIED\x10\x00\x12\x11\n\rSEVERITY_HIGH\x10\x01\x12\x13\n\x0fSEVERITY_MEDIUM\x10\x02\x12\x10\n\x0cSEVERITY_LOW\x10\x03*\xde\x01\n\rCallbackState\x12\x1e\n\x1a\x43\x41LLBACK_STATE_UNSPECIFIED\x10\x00\x12\x1a\n\x16\x43\x41LLBACK_STATE_STANDBY\x10\x01\x12\x1c\n\x18\x43\x41LLBACK_STATE_SCHEDULED\x10\x02\x12\x1e\n\x1a\x43\x41LLBACK_STATE_BACKING_OFF\x10\x03\x12\x19\n\x15\x43\x41LLBACK_STATE_FAILED\x10\x04\x12\x1c\n\x18\x43\x41LLBACK_STATE_SUCCEEDED\x10\x05\x12\x1a\n\x16\x43\x41LLBACK_STATE_BLOCKED\x10\x06*\xfd\x01\n\x1aPendingNexusOperationState\x12-\n)PENDING_NEXUS_OPERATION_STATE_UNSPECIFIED\x10\x00\x12+\n'PENDING_NEXUS_OPERATION_STATE_SCHEDULED\x10\x01\x12-\n)PENDING_NEXUS_OPERATION_STATE_BACKING_OFF\x10\x02\x12)\n%PENDING_NEXUS_OPERATION_STATE_STARTED\x10\x03\x12)\n%PENDING_NEXUS_OPERATION_STATE_BLOCKED\x10\x04*\xfe\x02\n\x1fNexusOperationCancellationState\x12\x32\n.NEXUS_OPERATION_CANCELLATION_STATE_UNSPECIFIED\x10\x00\x12\x30\n,NEXUS_OPERATION_CANCELLATION_STATE_SCHEDULED\x10\x01\x12\x32\n.NEXUS_OPERATION_CANCELLATION_STATE_BACKING_OFF\x10\x02\x12\x30\n,NEXUS_OPERATION_CANCELLATION_STATE_SUCCEEDED\x10\x03\x12-\n)NEXUS_OPERATION_CANCELLATION_STATE_FAILED\x10\x04\x12\x30\n,NEXUS_OPERATION_CANCELLATION_STATE_TIMED_OUT\x10\x05\x12.\n*NEXUS_OPERATION_CANCELLATION_STATE_BLOCKED\x10\x06*\x97\x01\n\x17WorkflowRuleActionScope\x12*\n&WORKFLOW_RULE_ACTION_SCOPE_UNSPECIFIED\x10\x00\x12'\n#WORKFLOW_RULE_ACTION_SCOPE_WORKFLOW\x10\x01\x12'\n#WORKFLOW_RULE_ACTION_SCOPE_ACTIVITY\x10\x02*m\n\x18\x41pplicationErrorCategory\x12*\n&APPLICATION_ERROR_CATEGORY_UNSPECIFIED\x10\x00\x12%\n!APPLICATION_ERROR_CATEGORY_BENIGN\x10\x01*\x85\x01\n\x0cWorkerStatus\x12\x1d\n\x19WORKER_STATUS_UNSPECIFIED\x10\x00\x12\x19\n\x15WORKER_STATUS_RUNNING\x10\x01\x12\x1f\n\x1bWORKER_STATUS_SHUTTING_DOWN\x10\x02\x12\x1a\n\x16WORKER_STATUS_SHUTDOWN\x10\x03\x42\x83\x01\n\x18io.temporal.api.enums.v1B\x0b\x43ommonProtoP\x01Z!go.temporal.io/api/enums/v1;enums\xaa\x02\x17Temporalio.Api.Enums.V1\xea\x02\x1aTemporalio::Api::Enums::V1b\x06proto3" + b"\n\"temporal/api/enums/v1/common.proto\x12\x15temporal.api.enums.v1*_\n\x0c\x45ncodingType\x12\x1d\n\x19\x45NCODING_TYPE_UNSPECIFIED\x10\x00\x12\x18\n\x14\x45NCODING_TYPE_PROTO3\x10\x01\x12\x16\n\x12\x45NCODING_TYPE_JSON\x10\x02*\x91\x02\n\x10IndexedValueType\x12\"\n\x1eINDEXED_VALUE_TYPE_UNSPECIFIED\x10\x00\x12\x1b\n\x17INDEXED_VALUE_TYPE_TEXT\x10\x01\x12\x1e\n\x1aINDEXED_VALUE_TYPE_KEYWORD\x10\x02\x12\x1a\n\x16INDEXED_VALUE_TYPE_INT\x10\x03\x12\x1d\n\x19INDEXED_VALUE_TYPE_DOUBLE\x10\x04\x12\x1b\n\x17INDEXED_VALUE_TYPE_BOOL\x10\x05\x12\x1f\n\x1bINDEXED_VALUE_TYPE_DATETIME\x10\x06\x12#\n\x1fINDEXED_VALUE_TYPE_KEYWORD_LIST\x10\x07*^\n\x08Severity\x12\x18\n\x14SEVERITY_UNSPECIFIED\x10\x00\x12\x11\n\rSEVERITY_HIGH\x10\x01\x12\x13\n\x0fSEVERITY_MEDIUM\x10\x02\x12\x10\n\x0cSEVERITY_LOW\x10\x03*\xde\x01\n\rCallbackState\x12\x1e\n\x1a\x43\x41LLBACK_STATE_UNSPECIFIED\x10\x00\x12\x1a\n\x16\x43\x41LLBACK_STATE_STANDBY\x10\x01\x12\x1c\n\x18\x43\x41LLBACK_STATE_SCHEDULED\x10\x02\x12\x1e\n\x1a\x43\x41LLBACK_STATE_BACKING_OFF\x10\x03\x12\x19\n\x15\x43\x41LLBACK_STATE_FAILED\x10\x04\x12\x1c\n\x18\x43\x41LLBACK_STATE_SUCCEEDED\x10\x05\x12\x1a\n\x16\x43\x41LLBACK_STATE_BLOCKED\x10\x06*\xfd\x01\n\x1aPendingNexusOperationState\x12-\n)PENDING_NEXUS_OPERATION_STATE_UNSPECIFIED\x10\x00\x12+\n'PENDING_NEXUS_OPERATION_STATE_SCHEDULED\x10\x01\x12-\n)PENDING_NEXUS_OPERATION_STATE_BACKING_OFF\x10\x02\x12)\n%PENDING_NEXUS_OPERATION_STATE_STARTED\x10\x03\x12)\n%PENDING_NEXUS_OPERATION_STATE_BLOCKED\x10\x04*\xfe\x02\n\x1fNexusOperationCancellationState\x12\x32\n.NEXUS_OPERATION_CANCELLATION_STATE_UNSPECIFIED\x10\x00\x12\x30\n,NEXUS_OPERATION_CANCELLATION_STATE_SCHEDULED\x10\x01\x12\x32\n.NEXUS_OPERATION_CANCELLATION_STATE_BACKING_OFF\x10\x02\x12\x30\n,NEXUS_OPERATION_CANCELLATION_STATE_SUCCEEDED\x10\x03\x12-\n)NEXUS_OPERATION_CANCELLATION_STATE_FAILED\x10\x04\x12\x30\n,NEXUS_OPERATION_CANCELLATION_STATE_TIMED_OUT\x10\x05\x12.\n*NEXUS_OPERATION_CANCELLATION_STATE_BLOCKED\x10\x06*\x97\x01\n\x17WorkflowRuleActionScope\x12*\n&WORKFLOW_RULE_ACTION_SCOPE_UNSPECIFIED\x10\x00\x12'\n#WORKFLOW_RULE_ACTION_SCOPE_WORKFLOW\x10\x01\x12'\n#WORKFLOW_RULE_ACTION_SCOPE_ACTIVITY\x10\x02*m\n\x18\x41pplicationErrorCategory\x12*\n&APPLICATION_ERROR_CATEGORY_UNSPECIFIED\x10\x00\x12%\n!APPLICATION_ERROR_CATEGORY_BENIGN\x10\x01*\x85\x01\n\x0cWorkerStatus\x12\x1d\n\x19WORKER_STATUS_UNSPECIFIED\x10\x00\x12\x19\n\x15WORKER_STATUS_RUNNING\x10\x01\x12\x1f\n\x1bWORKER_STATUS_SHUTTING_DOWN\x10\x02\x12\x1a\n\x16WORKER_STATUS_SHUTDOWN\x10\x03*i\n\rExecutionType\x12\x1e\n\x1a\x45XECUTION_TYPE_UNSPECIFIED\x10\x00\x12\x1b\n\x17\x45XECUTION_TYPE_WORKFLOW\x10\x01\x12\x1b\n\x17\x45XECUTION_TYPE_ACTIVITY\x10\x02\x42\x83\x01\n\x18io.temporal.api.enums.v1B\x0b\x43ommonProtoP\x01Z!go.temporal.io/api/enums/v1;enums\xaa\x02\x17Temporalio.Api.Enums.V1\xea\x02\x1aTemporalio::Api::Enums::V1b\x06proto3" ) _ENCODINGTYPE = DESCRIPTOR.enum_types_by_name["EncodingType"] @@ -45,6 +45,8 @@ ApplicationErrorCategory = enum_type_wrapper.EnumTypeWrapper(_APPLICATIONERRORCATEGORY) _WORKERSTATUS = DESCRIPTOR.enum_types_by_name["WorkerStatus"] WorkerStatus = enum_type_wrapper.EnumTypeWrapper(_WORKERSTATUS) +_EXECUTIONTYPE = DESCRIPTOR.enum_types_by_name["ExecutionType"] +ExecutionType = enum_type_wrapper.EnumTypeWrapper(_EXECUTIONTYPE) ENCODING_TYPE_UNSPECIFIED = 0 ENCODING_TYPE_PROTO3 = 1 ENCODING_TYPE_JSON = 2 @@ -88,6 +90,9 @@ WORKER_STATUS_RUNNING = 1 WORKER_STATUS_SHUTTING_DOWN = 2 WORKER_STATUS_SHUTDOWN = 3 +EXECUTION_TYPE_UNSPECIFIED = 0 +EXECUTION_TYPE_WORKFLOW = 1 +EXECUTION_TYPE_ACTIVITY = 2 if _descriptor._USE_C_DESCRIPTORS == False: @@ -111,4 +116,6 @@ _APPLICATIONERRORCATEGORY._serialized_end = 1659 _WORKERSTATUS._serialized_start = 1662 _WORKERSTATUS._serialized_end = 1795 + _EXECUTIONTYPE._serialized_start = 1797 + _EXECUTIONTYPE._serialized_end = 1902 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/enums/v1/common_pb2.pyi b/temporalio/api/enums/v1/common_pb2.pyi index 47e470295..47b6d030b 100644 --- a/temporalio/api/enums/v1/common_pb2.pyi +++ b/temporalio/api/enums/v1/common_pb2.pyi @@ -339,3 +339,29 @@ WORKER_STATUS_RUNNING: WorkerStatus.ValueType # 1 WORKER_STATUS_SHUTTING_DOWN: WorkerStatus.ValueType # 2 WORKER_STATUS_SHUTDOWN: WorkerStatus.ValueType # 3 global___WorkerStatus = WorkerStatus + +class _ExecutionType: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _ExecutionTypeEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ + _ExecutionType.ValueType + ], + builtins.type, +): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + EXECUTION_TYPE_UNSPECIFIED: _ExecutionType.ValueType # 0 + EXECUTION_TYPE_WORKFLOW: _ExecutionType.ValueType # 1 + """A workflow execution archetype.""" + EXECUTION_TYPE_ACTIVITY: _ExecutionType.ValueType # 2 + """An activity execution archetype. This is reserved for standalone activities.""" + +class ExecutionType(_ExecutionType, metaclass=_ExecutionTypeEnumTypeWrapper): ... + +EXECUTION_TYPE_UNSPECIFIED: ExecutionType.ValueType # 0 +EXECUTION_TYPE_WORKFLOW: ExecutionType.ValueType # 1 +"""A workflow execution archetype.""" +EXECUTION_TYPE_ACTIVITY: ExecutionType.ValueType # 2 +"""An activity execution archetype. This is reserved for standalone activities.""" +global___ExecutionType = ExecutionType diff --git a/temporalio/api/enums/v1/deployment_pb2.py b/temporalio/api/enums/v1/deployment_pb2.py index a4c5e00aa..aa05071ed 100644 --- a/temporalio/api/enums/v1/deployment_pb2.py +++ b/temporalio/api/enums/v1/deployment_pb2.py @@ -16,7 +16,7 @@ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b"\n&temporal/api/enums/v1/deployment.proto\x12\x15temporal.api.enums.v1*\xc4\x01\n\x16\x44\x65ploymentReachability\x12'\n#DEPLOYMENT_REACHABILITY_UNSPECIFIED\x10\x00\x12%\n!DEPLOYMENT_REACHABILITY_REACHABLE\x10\x01\x12\x31\n-DEPLOYMENT_REACHABILITY_CLOSED_WORKFLOWS_ONLY\x10\x02\x12'\n#DEPLOYMENT_REACHABILITY_UNREACHABLE\x10\x03*\x8b\x01\n\x15VersionDrainageStatus\x12'\n#VERSION_DRAINAGE_STATUS_UNSPECIFIED\x10\x00\x12$\n VERSION_DRAINAGE_STATUS_DRAINING\x10\x01\x12#\n\x1fVERSION_DRAINAGE_STATUS_DRAINED\x10\x02*\x8c\x01\n\x14WorkerVersioningMode\x12&\n\"WORKER_VERSIONING_MODE_UNSPECIFIED\x10\x00\x12&\n\"WORKER_VERSIONING_MODE_UNVERSIONED\x10\x01\x12$\n WORKER_VERSIONING_MODE_VERSIONED\x10\x02*\xb9\x02\n\x1dWorkerDeploymentVersionStatus\x12\x30\n,WORKER_DEPLOYMENT_VERSION_STATUS_UNSPECIFIED\x10\x00\x12-\n)WORKER_DEPLOYMENT_VERSION_STATUS_INACTIVE\x10\x01\x12,\n(WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT\x10\x02\x12,\n(WORKER_DEPLOYMENT_VERSION_STATUS_RAMPING\x10\x03\x12-\n)WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING\x10\x04\x12,\n(WORKER_DEPLOYMENT_VERSION_STATUS_DRAINED\x10\x05\x42\x87\x01\n\x18io.temporal.api.enums.v1B\x0f\x44\x65ploymentProtoP\x01Z!go.temporal.io/api/enums/v1;enums\xaa\x02\x17Temporalio.Api.Enums.V1\xea\x02\x1aTemporalio::Api::Enums::V1b\x06proto3" + b"\n&temporal/api/enums/v1/deployment.proto\x12\x15temporal.api.enums.v1*\xc4\x01\n\x16\x44\x65ploymentReachability\x12'\n#DEPLOYMENT_REACHABILITY_UNSPECIFIED\x10\x00\x12%\n!DEPLOYMENT_REACHABILITY_REACHABLE\x10\x01\x12\x31\n-DEPLOYMENT_REACHABILITY_CLOSED_WORKFLOWS_ONLY\x10\x02\x12'\n#DEPLOYMENT_REACHABILITY_UNREACHABLE\x10\x03*\x8b\x01\n\x15VersionDrainageStatus\x12'\n#VERSION_DRAINAGE_STATUS_UNSPECIFIED\x10\x00\x12$\n VERSION_DRAINAGE_STATUS_DRAINING\x10\x01\x12#\n\x1fVERSION_DRAINAGE_STATUS_DRAINED\x10\x02*\x8c\x01\n\x14WorkerVersioningMode\x12&\n\"WORKER_VERSIONING_MODE_UNSPECIFIED\x10\x00\x12&\n\"WORKER_VERSIONING_MODE_UNVERSIONED\x10\x01\x12$\n WORKER_VERSIONING_MODE_VERSIONED\x10\x02*\xe7\x02\n\x1dWorkerDeploymentVersionStatus\x12\x30\n,WORKER_DEPLOYMENT_VERSION_STATUS_UNSPECIFIED\x10\x00\x12-\n)WORKER_DEPLOYMENT_VERSION_STATUS_INACTIVE\x10\x01\x12,\n(WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT\x10\x02\x12,\n(WORKER_DEPLOYMENT_VERSION_STATUS_RAMPING\x10\x03\x12-\n)WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING\x10\x04\x12,\n(WORKER_DEPLOYMENT_VERSION_STATUS_DRAINED\x10\x05\x12,\n(WORKER_DEPLOYMENT_VERSION_STATUS_CREATED\x10\x06\x42\x87\x01\n\x18io.temporal.api.enums.v1B\x0f\x44\x65ploymentProtoP\x01Z!go.temporal.io/api/enums/v1;enums\xaa\x02\x17Temporalio.Api.Enums.V1\xea\x02\x1aTemporalio::Api::Enums::V1b\x06proto3" ) _DEPLOYMENTREACHABILITY = DESCRIPTOR.enum_types_by_name["DeploymentReachability"] @@ -47,6 +47,7 @@ WORKER_DEPLOYMENT_VERSION_STATUS_RAMPING = 3 WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING = 4 WORKER_DEPLOYMENT_VERSION_STATUS_DRAINED = 5 +WORKER_DEPLOYMENT_VERSION_STATUS_CREATED = 6 if _descriptor._USE_C_DESCRIPTORS == False: @@ -59,5 +60,5 @@ _WORKERVERSIONINGMODE._serialized_start = 407 _WORKERVERSIONINGMODE._serialized_end = 547 _WORKERDEPLOYMENTVERSIONSTATUS._serialized_start = 550 - _WORKERDEPLOYMENTVERSIONSTATUS._serialized_end = 863 + _WORKERDEPLOYMENTVERSIONSTATUS._serialized_end = 909 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/enums/v1/deployment_pb2.pyi b/temporalio/api/enums/v1/deployment_pb2.pyi index 92074757c..de3378e69 100644 --- a/temporalio/api/enums/v1/deployment_pb2.pyi +++ b/temporalio/api/enums/v1/deployment_pb2.pyi @@ -101,7 +101,6 @@ class VersionDrainageStatus( aip.dev/not-precedent: Call this status because it is . --) Specify the drainage status for a Worker Deployment Version so users can decide whether they can safely decommission the version. - Experimental. Worker Deployments are experimental and might significantly change in the future. """ VERSION_DRAINAGE_STATUS_UNSPECIFIED: VersionDrainageStatus.ValueType # 0 @@ -160,7 +159,6 @@ class WorkerVersioningMode( - Whether or not Temporal Server considers this worker's version (Build ID) when dispatching tasks to it. - Whether or not the workflows processed by this worker are versioned using the worker's version. - Experimental. Worker Deployments are experimental and might significantly change in the future. """ WORKER_VERSIONING_MODE_UNSPECIFIED: WorkerVersioningMode.ValueType # 0 @@ -234,6 +232,12 @@ class _WorkerDeploymentVersionStatusEnumTypeWrapper( not query closed workflows. If the user does query closed workflows for some time x after workflows are closed, they should decommission the version after it has been drained for that duration. """ + WORKER_DEPLOYMENT_VERSION_STATUS_CREATED: ( + _WorkerDeploymentVersionStatus.ValueType + ) # 6 + """The Worker Deployment Version is created by user (via `CreateWorkerDeploymentVersion` API) + but server has not seen any poller for it yet. + """ class WorkerDeploymentVersionStatus( _WorkerDeploymentVersionStatus, @@ -242,7 +246,6 @@ class WorkerDeploymentVersionStatus( """(-- api-linter: core::0216::synonyms=disabled aip.dev/not-precedent: Call this status because it is . --) Specify the status of a Worker Deployment Version. - Experimental. Worker Deployments are experimental and might significantly change in the future. """ WORKER_DEPLOYMENT_VERSION_STATUS_UNSPECIFIED: ( @@ -271,4 +274,8 @@ Queries sent to closed workflows. The version can be decommissioned safely if us not query closed workflows. If the user does query closed workflows for some time x after workflows are closed, they should decommission the version after it has been drained for that duration. """ +WORKER_DEPLOYMENT_VERSION_STATUS_CREATED: WorkerDeploymentVersionStatus.ValueType # 6 +"""The Worker Deployment Version is created by user (via `CreateWorkerDeploymentVersion` API) +but server has not seen any poller for it yet. +""" global___WorkerDeploymentVersionStatus = WorkerDeploymentVersionStatus diff --git a/temporalio/api/enums/v1/event_type_pb2.py b/temporalio/api/enums/v1/event_type_pb2.py index f51809725..14ff7984d 100644 --- a/temporalio/api/enums/v1/event_type_pb2.py +++ b/temporalio/api/enums/v1/event_type_pb2.py @@ -16,7 +16,7 @@ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n&temporal/api/enums/v1/event_type.proto\x12\x15temporal.api.enums.v1*\x8a\x15\n\tEventType\x12\x1a\n\x16\x45VENT_TYPE_UNSPECIFIED\x10\x00\x12)\n%EVENT_TYPE_WORKFLOW_EXECUTION_STARTED\x10\x01\x12+\n\'EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED\x10\x02\x12(\n$EVENT_TYPE_WORKFLOW_EXECUTION_FAILED\x10\x03\x12+\n\'EVENT_TYPE_WORKFLOW_EXECUTION_TIMED_OUT\x10\x04\x12&\n"EVENT_TYPE_WORKFLOW_TASK_SCHEDULED\x10\x05\x12$\n EVENT_TYPE_WORKFLOW_TASK_STARTED\x10\x06\x12&\n"EVENT_TYPE_WORKFLOW_TASK_COMPLETED\x10\x07\x12&\n"EVENT_TYPE_WORKFLOW_TASK_TIMED_OUT\x10\x08\x12#\n\x1f\x45VENT_TYPE_WORKFLOW_TASK_FAILED\x10\t\x12&\n"EVENT_TYPE_ACTIVITY_TASK_SCHEDULED\x10\n\x12$\n EVENT_TYPE_ACTIVITY_TASK_STARTED\x10\x0b\x12&\n"EVENT_TYPE_ACTIVITY_TASK_COMPLETED\x10\x0c\x12#\n\x1f\x45VENT_TYPE_ACTIVITY_TASK_FAILED\x10\r\x12&\n"EVENT_TYPE_ACTIVITY_TASK_TIMED_OUT\x10\x0e\x12-\n)EVENT_TYPE_ACTIVITY_TASK_CANCEL_REQUESTED\x10\x0f\x12%\n!EVENT_TYPE_ACTIVITY_TASK_CANCELED\x10\x10\x12\x1c\n\x18\x45VENT_TYPE_TIMER_STARTED\x10\x11\x12\x1a\n\x16\x45VENT_TYPE_TIMER_FIRED\x10\x12\x12\x1d\n\x19\x45VENT_TYPE_TIMER_CANCELED\x10\x13\x12\x32\n.EVENT_TYPE_WORKFLOW_EXECUTION_CANCEL_REQUESTED\x10\x14\x12*\n&EVENT_TYPE_WORKFLOW_EXECUTION_CANCELED\x10\x15\x12\x43\n?EVENT_TYPE_REQUEST_CANCEL_EXTERNAL_WORKFLOW_EXECUTION_INITIATED\x10\x16\x12@\n= (3, 10): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class _FastForwardPollingResult: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _FastForwardPollingResultEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ + _FastForwardPollingResult.ValueType + ], + builtins.type, +): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + FAST_FORWARD_POLLING_RESULT_UNSPECIFIED: _FastForwardPollingResult.ValueType # 0 + """Never returned; guards against an unset result.""" + FAST_FORWARD_POLLING_RESULT_POLL_TIMEOUT: _FastForwardPollingResult.ValueType # 1 + """The poll timed out server-side before the fast-forward completed. The caller may poll again.""" + FAST_FORWARD_POLLING_RESULT_FAST_FORWARD_COMPLETED: ( + _FastForwardPollingResult.ValueType + ) # 2 + """The fast-forward identified by the request's `fast_forward_id` reached its target time and completed.""" + FAST_FORWARD_POLLING_RESULT_FAST_FORWARD_FAILED: ( + _FastForwardPollingResult.ValueType + ) # 3 + """The fast-forward can no longer complete, which usually indicates improper usage of + fast-forward on the client side. Possible reasons: the `fast_forward_id` does not match + the execution's current fast-forward, the execution ended before the fast-forward + completed, the fast-forward config was updated while the poll was in flight, etc. + See `failed_reason` in the response for the specific cause. + """ + +class FastForwardPollingResult( + _FastForwardPollingResult, metaclass=_FastForwardPollingResultEnumTypeWrapper +): + """FastForwardPollingResult is the result of polling and waiting for a fast-forward to complete + on a time-skipping execution. + FAST_FORWARD_POLLING_RESULT_POLL_TIMEOUT and FAST_FORWARD_POLLING_RESULT_FAST_FORWARD_COMPLETED + are the normal poll outcomes; FAST_FORWARD_POLLING_RESULT_FAST_FORWARD_FAILED means the + fast-forward can no longer complete. + """ + +FAST_FORWARD_POLLING_RESULT_UNSPECIFIED: FastForwardPollingResult.ValueType # 0 +"""Never returned; guards against an unset result.""" +FAST_FORWARD_POLLING_RESULT_POLL_TIMEOUT: FastForwardPollingResult.ValueType # 1 +"""The poll timed out server-side before the fast-forward completed. The caller may poll again.""" +FAST_FORWARD_POLLING_RESULT_FAST_FORWARD_COMPLETED: ( + FastForwardPollingResult.ValueType +) # 2 +"""The fast-forward identified by the request's `fast_forward_id` reached its target time and completed.""" +FAST_FORWARD_POLLING_RESULT_FAST_FORWARD_FAILED: FastForwardPollingResult.ValueType # 3 +"""The fast-forward can no longer complete, which usually indicates improper usage of +fast-forward on the client side. Possible reasons: the `fast_forward_id` does not match +the execution's current fast-forward, the execution ended before the fast-forward +completed, the fast-forward config was updated while the poll was in flight, etc. +See `failed_reason` in the response for the specific cause. +""" +global___FastForwardPollingResult = FastForwardPollingResult diff --git a/temporalio/api/enums/v1/workflow_pb2.py b/temporalio/api/enums/v1/workflow_pb2.py index 03600d345..84fcf8d83 100644 --- a/temporalio/api/enums/v1/workflow_pb2.py +++ b/temporalio/api/enums/v1/workflow_pb2.py @@ -16,7 +16,7 @@ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b"\n$temporal/api/enums/v1/workflow.proto\x12\x15temporal.api.enums.v1*\x8b\x02\n\x15WorkflowIdReusePolicy\x12(\n$WORKFLOW_ID_REUSE_POLICY_UNSPECIFIED\x10\x00\x12,\n(WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE\x10\x01\x12\x38\n4WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY\x10\x02\x12-\n)WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE\x10\x03\x12\x31\n-WORKFLOW_ID_REUSE_POLICY_TERMINATE_IF_RUNNING\x10\x04*\xcf\x01\n\x18WorkflowIdConflictPolicy\x12+\n'WORKFLOW_ID_CONFLICT_POLICY_UNSPECIFIED\x10\x00\x12$\n WORKFLOW_ID_CONFLICT_POLICY_FAIL\x10\x01\x12,\n(WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING\x10\x02\x12\x32\n.WORKFLOW_ID_CONFLICT_POLICY_TERMINATE_EXISTING\x10\x03*\xa4\x01\n\x11ParentClosePolicy\x12#\n\x1fPARENT_CLOSE_POLICY_UNSPECIFIED\x10\x00\x12!\n\x1dPARENT_CLOSE_POLICY_TERMINATE\x10\x01\x12\x1f\n\x1bPARENT_CLOSE_POLICY_ABANDON\x10\x02\x12&\n\"PARENT_CLOSE_POLICY_REQUEST_CANCEL\x10\x03*\xbd\x01\n\x16\x43ontinueAsNewInitiator\x12)\n%CONTINUE_AS_NEW_INITIATOR_UNSPECIFIED\x10\x00\x12&\n\"CONTINUE_AS_NEW_INITIATOR_WORKFLOW\x10\x01\x12#\n\x1f\x43ONTINUE_AS_NEW_INITIATOR_RETRY\x10\x02\x12+\n'CONTINUE_AS_NEW_INITIATOR_CRON_SCHEDULE\x10\x03*\xe5\x02\n\x17WorkflowExecutionStatus\x12)\n%WORKFLOW_EXECUTION_STATUS_UNSPECIFIED\x10\x00\x12%\n!WORKFLOW_EXECUTION_STATUS_RUNNING\x10\x01\x12'\n#WORKFLOW_EXECUTION_STATUS_COMPLETED\x10\x02\x12$\n WORKFLOW_EXECUTION_STATUS_FAILED\x10\x03\x12&\n\"WORKFLOW_EXECUTION_STATUS_CANCELED\x10\x04\x12(\n$WORKFLOW_EXECUTION_STATUS_TERMINATED\x10\x05\x12.\n*WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW\x10\x06\x12'\n#WORKFLOW_EXECUTION_STATUS_TIMED_OUT\x10\x07*\x84\x02\n\x14PendingActivityState\x12&\n\"PENDING_ACTIVITY_STATE_UNSPECIFIED\x10\x00\x12$\n PENDING_ACTIVITY_STATE_SCHEDULED\x10\x01\x12\"\n\x1ePENDING_ACTIVITY_STATE_STARTED\x10\x02\x12+\n'PENDING_ACTIVITY_STATE_CANCEL_REQUESTED\x10\x03\x12!\n\x1dPENDING_ACTIVITY_STATE_PAUSED\x10\x04\x12*\n&PENDING_ACTIVITY_STATE_PAUSE_REQUESTED\x10\x05*\x9b\x01\n\x18PendingWorkflowTaskState\x12+\n'PENDING_WORKFLOW_TASK_STATE_UNSPECIFIED\x10\x00\x12)\n%PENDING_WORKFLOW_TASK_STATE_SCHEDULED\x10\x01\x12'\n#PENDING_WORKFLOW_TASK_STATE_STARTED\x10\x02*\x97\x01\n\x16HistoryEventFilterType\x12)\n%HISTORY_EVENT_FILTER_TYPE_UNSPECIFIED\x10\x00\x12'\n#HISTORY_EVENT_FILTER_TYPE_ALL_EVENT\x10\x01\x12)\n%HISTORY_EVENT_FILTER_TYPE_CLOSE_EVENT\x10\x02*\x9f\x02\n\nRetryState\x12\x1b\n\x17RETRY_STATE_UNSPECIFIED\x10\x00\x12\x1b\n\x17RETRY_STATE_IN_PROGRESS\x10\x01\x12%\n!RETRY_STATE_NON_RETRYABLE_FAILURE\x10\x02\x12\x17\n\x13RETRY_STATE_TIMEOUT\x10\x03\x12(\n$RETRY_STATE_MAXIMUM_ATTEMPTS_REACHED\x10\x04\x12$\n RETRY_STATE_RETRY_POLICY_NOT_SET\x10\x05\x12%\n!RETRY_STATE_INTERNAL_SERVER_ERROR\x10\x06\x12 \n\x1cRETRY_STATE_CANCEL_REQUESTED\x10\x07*\xb0\x01\n\x0bTimeoutType\x12\x1c\n\x18TIMEOUT_TYPE_UNSPECIFIED\x10\x00\x12\x1f\n\x1bTIMEOUT_TYPE_START_TO_CLOSE\x10\x01\x12\"\n\x1eTIMEOUT_TYPE_SCHEDULE_TO_START\x10\x02\x12\"\n\x1eTIMEOUT_TYPE_SCHEDULE_TO_CLOSE\x10\x03\x12\x1a\n\x16TIMEOUT_TYPE_HEARTBEAT\x10\x04*\x7f\n\x12VersioningBehavior\x12#\n\x1fVERSIONING_BEHAVIOR_UNSPECIFIED\x10\x00\x12\x1e\n\x1aVERSIONING_BEHAVIOR_PINNED\x10\x01\x12$\n VERSIONING_BEHAVIOR_AUTO_UPGRADE\x10\x02\x42\x85\x01\n\x18io.temporal.api.enums.v1B\rWorkflowProtoP\x01Z!go.temporal.io/api/enums/v1;enums\xaa\x02\x17Temporalio.Api.Enums.V1\xea\x02\x1aTemporalio::Api::Enums::V1b\x06proto3" + b"\n$temporal/api/enums/v1/workflow.proto\x12\x15temporal.api.enums.v1*\x8f\x02\n\x15WorkflowIdReusePolicy\x12(\n$WORKFLOW_ID_REUSE_POLICY_UNSPECIFIED\x10\x00\x12,\n(WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE\x10\x01\x12\x38\n4WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY\x10\x02\x12-\n)WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE\x10\x03\x12\x35\n-WORKFLOW_ID_REUSE_POLICY_TERMINATE_IF_RUNNING\x10\x04\x1a\x02\x08\x01*\xcf\x01\n\x18WorkflowIdConflictPolicy\x12+\n'WORKFLOW_ID_CONFLICT_POLICY_UNSPECIFIED\x10\x00\x12$\n WORKFLOW_ID_CONFLICT_POLICY_FAIL\x10\x01\x12,\n(WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING\x10\x02\x12\x32\n.WORKFLOW_ID_CONFLICT_POLICY_TERMINATE_EXISTING\x10\x03*\xa4\x01\n\x11ParentClosePolicy\x12#\n\x1fPARENT_CLOSE_POLICY_UNSPECIFIED\x10\x00\x12!\n\x1dPARENT_CLOSE_POLICY_TERMINATE\x10\x01\x12\x1f\n\x1bPARENT_CLOSE_POLICY_ABANDON\x10\x02\x12&\n\"PARENT_CLOSE_POLICY_REQUEST_CANCEL\x10\x03*\xbd\x01\n\x16\x43ontinueAsNewInitiator\x12)\n%CONTINUE_AS_NEW_INITIATOR_UNSPECIFIED\x10\x00\x12&\n\"CONTINUE_AS_NEW_INITIATOR_WORKFLOW\x10\x01\x12#\n\x1f\x43ONTINUE_AS_NEW_INITIATOR_RETRY\x10\x02\x12+\n'CONTINUE_AS_NEW_INITIATOR_CRON_SCHEDULE\x10\x03*\x8b\x03\n\x17WorkflowExecutionStatus\x12)\n%WORKFLOW_EXECUTION_STATUS_UNSPECIFIED\x10\x00\x12%\n!WORKFLOW_EXECUTION_STATUS_RUNNING\x10\x01\x12'\n#WORKFLOW_EXECUTION_STATUS_COMPLETED\x10\x02\x12$\n WORKFLOW_EXECUTION_STATUS_FAILED\x10\x03\x12&\n\"WORKFLOW_EXECUTION_STATUS_CANCELED\x10\x04\x12(\n$WORKFLOW_EXECUTION_STATUS_TERMINATED\x10\x05\x12.\n*WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW\x10\x06\x12'\n#WORKFLOW_EXECUTION_STATUS_TIMED_OUT\x10\x07\x12$\n WORKFLOW_EXECUTION_STATUS_PAUSED\x10\x08*\x84\x02\n\x14PendingActivityState\x12&\n\"PENDING_ACTIVITY_STATE_UNSPECIFIED\x10\x00\x12$\n PENDING_ACTIVITY_STATE_SCHEDULED\x10\x01\x12\"\n\x1ePENDING_ACTIVITY_STATE_STARTED\x10\x02\x12+\n'PENDING_ACTIVITY_STATE_CANCEL_REQUESTED\x10\x03\x12!\n\x1dPENDING_ACTIVITY_STATE_PAUSED\x10\x04\x12*\n&PENDING_ACTIVITY_STATE_PAUSE_REQUESTED\x10\x05*\x9b\x01\n\x18PendingWorkflowTaskState\x12+\n'PENDING_WORKFLOW_TASK_STATE_UNSPECIFIED\x10\x00\x12)\n%PENDING_WORKFLOW_TASK_STATE_SCHEDULED\x10\x01\x12'\n#PENDING_WORKFLOW_TASK_STATE_STARTED\x10\x02*\x97\x01\n\x16HistoryEventFilterType\x12)\n%HISTORY_EVENT_FILTER_TYPE_UNSPECIFIED\x10\x00\x12'\n#HISTORY_EVENT_FILTER_TYPE_ALL_EVENT\x10\x01\x12)\n%HISTORY_EVENT_FILTER_TYPE_CLOSE_EVENT\x10\x02*\x9f\x02\n\nRetryState\x12\x1b\n\x17RETRY_STATE_UNSPECIFIED\x10\x00\x12\x1b\n\x17RETRY_STATE_IN_PROGRESS\x10\x01\x12%\n!RETRY_STATE_NON_RETRYABLE_FAILURE\x10\x02\x12\x17\n\x13RETRY_STATE_TIMEOUT\x10\x03\x12(\n$RETRY_STATE_MAXIMUM_ATTEMPTS_REACHED\x10\x04\x12$\n RETRY_STATE_RETRY_POLICY_NOT_SET\x10\x05\x12%\n!RETRY_STATE_INTERNAL_SERVER_ERROR\x10\x06\x12 \n\x1cRETRY_STATE_CANCEL_REQUESTED\x10\x07*\xb0\x01\n\x0bTimeoutType\x12\x1c\n\x18TIMEOUT_TYPE_UNSPECIFIED\x10\x00\x12\x1f\n\x1bTIMEOUT_TYPE_START_TO_CLOSE\x10\x01\x12\"\n\x1eTIMEOUT_TYPE_SCHEDULE_TO_START\x10\x02\x12\"\n\x1eTIMEOUT_TYPE_SCHEDULE_TO_CLOSE\x10\x03\x12\x1a\n\x16TIMEOUT_TYPE_HEARTBEAT\x10\x04*\x7f\n\x12VersioningBehavior\x12#\n\x1fVERSIONING_BEHAVIOR_UNSPECIFIED\x10\x00\x12\x1e\n\x1aVERSIONING_BEHAVIOR_PINNED\x10\x01\x12$\n VERSIONING_BEHAVIOR_AUTO_UPGRADE\x10\x02*\xc9\x01\n\x1f\x43ontinueAsNewVersioningBehavior\x12\x33\n/CONTINUE_AS_NEW_VERSIONING_BEHAVIOR_UNSPECIFIED\x10\x00\x12\x34\n0CONTINUE_AS_NEW_VERSIONING_BEHAVIOR_AUTO_UPGRADE\x10\x01\x12;\n7CONTINUE_AS_NEW_VERSIONING_BEHAVIOR_USE_RAMPING_VERSION\x10\x02*\xc7\x02\n\x1aSuggestContinueAsNewReason\x12.\n*SUGGEST_CONTINUE_AS_NEW_REASON_UNSPECIFIED\x10\x00\x12\x39\n5SUGGEST_CONTINUE_AS_NEW_REASON_HISTORY_SIZE_TOO_LARGE\x10\x01\x12:\n6SUGGEST_CONTINUE_AS_NEW_REASON_TOO_MANY_HISTORY_EVENTS\x10\x02\x12\x33\n/SUGGEST_CONTINUE_AS_NEW_REASON_TOO_MANY_UPDATES\x10\x03\"\x04\x08\x04\x10\x04*GSUGGEST_CONTINUE_AS_NEW_REASON_TARGET_WORKER_DEPLOYMENT_VERSION_CHANGEDB\x85\x01\n\x18io.temporal.api.enums.v1B\rWorkflowProtoP\x01Z!go.temporal.io/api/enums/v1;enums\xaa\x02\x17Temporalio.Api.Enums.V1\xea\x02\x1aTemporalio::Api::Enums::V1b\x06proto3" ) _WORKFLOWIDREUSEPOLICY = DESCRIPTOR.enum_types_by_name["WorkflowIdReusePolicy"] @@ -41,6 +41,18 @@ TimeoutType = enum_type_wrapper.EnumTypeWrapper(_TIMEOUTTYPE) _VERSIONINGBEHAVIOR = DESCRIPTOR.enum_types_by_name["VersioningBehavior"] VersioningBehavior = enum_type_wrapper.EnumTypeWrapper(_VERSIONINGBEHAVIOR) +_CONTINUEASNEWVERSIONINGBEHAVIOR = DESCRIPTOR.enum_types_by_name[ + "ContinueAsNewVersioningBehavior" +] +ContinueAsNewVersioningBehavior = enum_type_wrapper.EnumTypeWrapper( + _CONTINUEASNEWVERSIONINGBEHAVIOR +) +_SUGGESTCONTINUEASNEWREASON = DESCRIPTOR.enum_types_by_name[ + "SuggestContinueAsNewReason" +] +SuggestContinueAsNewReason = enum_type_wrapper.EnumTypeWrapper( + _SUGGESTCONTINUEASNEWREASON +) WORKFLOW_ID_REUSE_POLICY_UNSPECIFIED = 0 WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE = 1 WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY = 2 @@ -66,6 +78,7 @@ WORKFLOW_EXECUTION_STATUS_TERMINATED = 5 WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW = 6 WORKFLOW_EXECUTION_STATUS_TIMED_OUT = 7 +WORKFLOW_EXECUTION_STATUS_PAUSED = 8 PENDING_ACTIVITY_STATE_UNSPECIFIED = 0 PENDING_ACTIVITY_STATE_SCHEDULED = 1 PENDING_ACTIVITY_STATE_STARTED = 2 @@ -94,31 +107,48 @@ VERSIONING_BEHAVIOR_UNSPECIFIED = 0 VERSIONING_BEHAVIOR_PINNED = 1 VERSIONING_BEHAVIOR_AUTO_UPGRADE = 2 +CONTINUE_AS_NEW_VERSIONING_BEHAVIOR_UNSPECIFIED = 0 +CONTINUE_AS_NEW_VERSIONING_BEHAVIOR_AUTO_UPGRADE = 1 +CONTINUE_AS_NEW_VERSIONING_BEHAVIOR_USE_RAMPING_VERSION = 2 +SUGGEST_CONTINUE_AS_NEW_REASON_UNSPECIFIED = 0 +SUGGEST_CONTINUE_AS_NEW_REASON_HISTORY_SIZE_TOO_LARGE = 1 +SUGGEST_CONTINUE_AS_NEW_REASON_TOO_MANY_HISTORY_EVENTS = 2 +SUGGEST_CONTINUE_AS_NEW_REASON_TOO_MANY_UPDATES = 3 if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b"\n\030io.temporal.api.enums.v1B\rWorkflowProtoP\001Z!go.temporal.io/api/enums/v1;enums\252\002\027Temporalio.Api.Enums.V1\352\002\032Temporalio::Api::Enums::V1" + _WORKFLOWIDREUSEPOLICY.values_by_name[ + "WORKFLOW_ID_REUSE_POLICY_TERMINATE_IF_RUNNING" + ]._options = None + _WORKFLOWIDREUSEPOLICY.values_by_name[ + "WORKFLOW_ID_REUSE_POLICY_TERMINATE_IF_RUNNING" + ]._serialized_options = b"\010\001" _WORKFLOWIDREUSEPOLICY._serialized_start = 64 - _WORKFLOWIDREUSEPOLICY._serialized_end = 331 - _WORKFLOWIDCONFLICTPOLICY._serialized_start = 334 - _WORKFLOWIDCONFLICTPOLICY._serialized_end = 541 - _PARENTCLOSEPOLICY._serialized_start = 544 - _PARENTCLOSEPOLICY._serialized_end = 708 - _CONTINUEASNEWINITIATOR._serialized_start = 711 - _CONTINUEASNEWINITIATOR._serialized_end = 900 - _WORKFLOWEXECUTIONSTATUS._serialized_start = 903 - _WORKFLOWEXECUTIONSTATUS._serialized_end = 1260 - _PENDINGACTIVITYSTATE._serialized_start = 1263 - _PENDINGACTIVITYSTATE._serialized_end = 1523 - _PENDINGWORKFLOWTASKSTATE._serialized_start = 1526 - _PENDINGWORKFLOWTASKSTATE._serialized_end = 1681 - _HISTORYEVENTFILTERTYPE._serialized_start = 1684 - _HISTORYEVENTFILTERTYPE._serialized_end = 1835 - _RETRYSTATE._serialized_start = 1838 - _RETRYSTATE._serialized_end = 2125 - _TIMEOUTTYPE._serialized_start = 2128 - _TIMEOUTTYPE._serialized_end = 2304 - _VERSIONINGBEHAVIOR._serialized_start = 2306 - _VERSIONINGBEHAVIOR._serialized_end = 2433 + _WORKFLOWIDREUSEPOLICY._serialized_end = 335 + _WORKFLOWIDCONFLICTPOLICY._serialized_start = 338 + _WORKFLOWIDCONFLICTPOLICY._serialized_end = 545 + _PARENTCLOSEPOLICY._serialized_start = 548 + _PARENTCLOSEPOLICY._serialized_end = 712 + _CONTINUEASNEWINITIATOR._serialized_start = 715 + _CONTINUEASNEWINITIATOR._serialized_end = 904 + _WORKFLOWEXECUTIONSTATUS._serialized_start = 907 + _WORKFLOWEXECUTIONSTATUS._serialized_end = 1302 + _PENDINGACTIVITYSTATE._serialized_start = 1305 + _PENDINGACTIVITYSTATE._serialized_end = 1565 + _PENDINGWORKFLOWTASKSTATE._serialized_start = 1568 + _PENDINGWORKFLOWTASKSTATE._serialized_end = 1723 + _HISTORYEVENTFILTERTYPE._serialized_start = 1726 + _HISTORYEVENTFILTERTYPE._serialized_end = 1877 + _RETRYSTATE._serialized_start = 1880 + _RETRYSTATE._serialized_end = 2167 + _TIMEOUTTYPE._serialized_start = 2170 + _TIMEOUTTYPE._serialized_end = 2346 + _VERSIONINGBEHAVIOR._serialized_start = 2348 + _VERSIONINGBEHAVIOR._serialized_end = 2475 + _CONTINUEASNEWVERSIONINGBEHAVIOR._serialized_start = 2478 + _CONTINUEASNEWVERSIONINGBEHAVIOR._serialized_end = 2679 + _SUGGESTCONTINUEASNEWREASON._serialized_start = 2682 + _SUGGESTCONTINUEASNEWREASON._serialized_end = 3009 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/enums/v1/workflow_pb2.pyi b/temporalio/api/enums/v1/workflow_pb2.pyi index 516bdce56..0b32394d3 100644 --- a/temporalio/api/enums/v1/workflow_pb2.pyi +++ b/temporalio/api/enums/v1/workflow_pb2.pyi @@ -42,10 +42,12 @@ class _WorkflowIdReusePolicyEnumTypeWrapper( could potentially change the policy, allowing re-use of the workflow id. """ WORKFLOW_ID_REUSE_POLICY_TERMINATE_IF_RUNNING: _WorkflowIdReusePolicy.ValueType # 4 - """This option belongs in WorkflowIdConflictPolicy but is here for backwards compatibility. - If specified, it acts like ALLOW_DUPLICATE, but also the WorkflowId*Conflict*Policy on - the request is treated as WORKFLOW_ID_CONFLICT_POLICY_TERMINATE_EXISTING. - If no running workflow, then the behavior is the same as ALLOW_DUPLICATE. + """Terminate the current Workflow if one is already running; otherwise allow reusing the + Workflow ID. When using this option, `WorkflowIdConflictPolicy` must be left unspecified. + + Deprecated. Instead, set `WorkflowIdReusePolicy` to `ALLOW_DUPLICATE` and + `WorkflowIdConflictPolicy` to `TERMINATE_EXISTING`. Note that `WorkflowIdConflictPolicy` + requires Temporal Server v1.24.0 or later. """ class WorkflowIdReusePolicy( @@ -71,10 +73,12 @@ WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE: WorkflowIdReusePolicy.ValueType # 3 could potentially change the policy, allowing re-use of the workflow id. """ WORKFLOW_ID_REUSE_POLICY_TERMINATE_IF_RUNNING: WorkflowIdReusePolicy.ValueType # 4 -"""This option belongs in WorkflowIdConflictPolicy but is here for backwards compatibility. -If specified, it acts like ALLOW_DUPLICATE, but also the WorkflowId*Conflict*Policy on -the request is treated as WORKFLOW_ID_CONFLICT_POLICY_TERMINATE_EXISTING. -If no running workflow, then the behavior is the same as ALLOW_DUPLICATE. +"""Terminate the current Workflow if one is already running; otherwise allow reusing the +Workflow ID. When using this option, `WorkflowIdConflictPolicy` must be left unspecified. + +Deprecated. Instead, set `WorkflowIdReusePolicy` to `ALLOW_DUPLICATE` and +`WorkflowIdConflictPolicy` to `TERMINATE_EXISTING`. Note that `WorkflowIdConflictPolicy` +requires Temporal Server v1.24.0 or later. """ global___WorkflowIdReusePolicy = WorkflowIdReusePolicy @@ -202,6 +206,7 @@ class _WorkflowExecutionStatusEnumTypeWrapper( WORKFLOW_EXECUTION_STATUS_TERMINATED: _WorkflowExecutionStatus.ValueType # 5 WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW: _WorkflowExecutionStatus.ValueType # 6 WORKFLOW_EXECUTION_STATUS_TIMED_OUT: _WorkflowExecutionStatus.ValueType # 7 + WORKFLOW_EXECUTION_STATUS_PAUSED: _WorkflowExecutionStatus.ValueType # 8 class WorkflowExecutionStatus( _WorkflowExecutionStatus, metaclass=_WorkflowExecutionStatusEnumTypeWrapper @@ -219,6 +224,7 @@ WORKFLOW_EXECUTION_STATUS_CANCELED: WorkflowExecutionStatus.ValueType # 4 WORKFLOW_EXECUTION_STATUS_TERMINATED: WorkflowExecutionStatus.ValueType # 5 WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW: WorkflowExecutionStatus.ValueType # 6 WORKFLOW_EXECUTION_STATUS_TIMED_OUT: WorkflowExecutionStatus.ValueType # 7 +WORKFLOW_EXECUTION_STATUS_PAUSED: WorkflowExecutionStatus.ValueType # 8 global___WorkflowExecutionStatus = WorkflowExecutionStatus class _PendingActivityState: @@ -376,9 +382,15 @@ class _VersioningBehaviorEnumTypeWrapper( with Unversioned workflows. """ VERSIONING_BEHAVIOR_PINNED: _VersioningBehavior.ValueType # 1 - """Workflow will start on the Current Deployment Version of its Task Queue, and then - will be pinned to that same Deployment Version until completion (the Version that - this Workflow is pinned to is specified in `versioning_info.version`). + """Workflow will start on its Target Version and then will be pinned to that same Deployment + Version until completion (the Version that this Workflow is pinned to is specified in + `versioning_info.version` and is the Pinned Version of the Workflow). + + The workflow's Target Version is the Current Version of its Task Queue, or, if the + Task Queue has a Ramping Version with non-zero Ramp Percentage `P`, the workflow's Target + Version has a P% chance of being the Ramping Version. Whether a workflow falls into the + Ramping group depends on its Workflow ID and and the Ramp Percentage. + This behavior eliminates most of compatibility concerns users face when changing their code. Patching is not needed when pinned workflows code change. Can be overridden explicitly via `UpdateWorkflowExecutionOptions` API to move the @@ -389,8 +401,13 @@ class _VersioningBehaviorEnumTypeWrapper( task queue. """ VERSIONING_BEHAVIOR_AUTO_UPGRADE: _VersioningBehavior.ValueType # 2 - """Workflow will automatically move to the Current Deployment Version of its Task Queue when the - next workflow task is dispatched. + """Workflow will automatically move to its Target Version when the next workflow task is dispatched. + + The workflow's Target Version is the Current Version of its Task Queue, or, if the + Task Queue has a Ramping Version with non-zero Ramp Percentage `P`, the workflow's Target + Version has a P% chance of being the Ramping Version. Whether a workflow falls into the + Ramping group depends on its Workflow ID and and the Ramp Percentage. + AutoUpgrade behavior is suitable for long-running workflows as it allows them to move to the latest Deployment Version, but the user still needs to use Patching to keep the new code compatible with prior versions for changed workflow types. @@ -398,11 +415,10 @@ class _VersioningBehaviorEnumTypeWrapper( execution (as specified in versioning_info.version based on the last completed workflow task). Exception to this would be when the activity Task Queue workers are not present in the workflow's Deployment Version, in which case, the activity will be sent to a - different Deployment Version according to the Current Deployment Version of its own task - queue. - Workflows stuck on a backlogged activity will still auto-upgrade if the Current Deployment - Version of their Task Queue changes, without having to wait for the backlogged activity to - complete on the old Version. + different Deployment Version according to the Current or Ramping Deployment Version of its own + Task Queue. + Workflows stuck on a backlogged activity will still auto-upgrade if their Target Version + changes, without having to wait for the backlogged activity to complete on the old Version. """ class VersioningBehavior( @@ -412,7 +428,6 @@ class VersioningBehavior( Versions. The Versioning Behavior of a workflow execution is typically specified by the worker who completes the first task of the execution, but is also overridable manually for new and existing workflows (see VersioningOverride). - Experimental. Worker Deployments are experimental and might significantly change in the future. """ VERSIONING_BEHAVIOR_UNSPECIFIED: VersioningBehavior.ValueType # 0 @@ -423,9 +438,15 @@ User needs to use Patching to keep the new code compatible with prior versions w with Unversioned workflows. """ VERSIONING_BEHAVIOR_PINNED: VersioningBehavior.ValueType # 1 -"""Workflow will start on the Current Deployment Version of its Task Queue, and then -will be pinned to that same Deployment Version until completion (the Version that -this Workflow is pinned to is specified in `versioning_info.version`). +"""Workflow will start on its Target Version and then will be pinned to that same Deployment +Version until completion (the Version that this Workflow is pinned to is specified in +`versioning_info.version` and is the Pinned Version of the Workflow). + +The workflow's Target Version is the Current Version of its Task Queue, or, if the +Task Queue has a Ramping Version with non-zero Ramp Percentage `P`, the workflow's Target +Version has a P% chance of being the Ramping Version. Whether a workflow falls into the +Ramping group depends on its Workflow ID and and the Ramp Percentage. + This behavior eliminates most of compatibility concerns users face when changing their code. Patching is not needed when pinned workflows code change. Can be overridden explicitly via `UpdateWorkflowExecutionOptions` API to move the @@ -436,8 +457,13 @@ Version, in which case the activity will be sent to the Current Deployment Versi task queue. """ VERSIONING_BEHAVIOR_AUTO_UPGRADE: VersioningBehavior.ValueType # 2 -"""Workflow will automatically move to the Current Deployment Version of its Task Queue when the -next workflow task is dispatched. +"""Workflow will automatically move to its Target Version when the next workflow task is dispatched. + +The workflow's Target Version is the Current Version of its Task Queue, or, if the +Task Queue has a Ramping Version with non-zero Ramp Percentage `P`, the workflow's Target +Version has a P% chance of being the Ramping Version. Whether a workflow falls into the +Ramping group depends on its Workflow ID and and the Ramp Percentage. + AutoUpgrade behavior is suitable for long-running workflows as it allows them to move to the latest Deployment Version, but the user still needs to use Patching to keep the new code compatible with prior versions for changed workflow types. @@ -445,10 +471,142 @@ Activities of `AUTO_UPGRADE` workflows are sent to the Deployment Version of the execution (as specified in versioning_info.version based on the last completed workflow task). Exception to this would be when the activity Task Queue workers are not present in the workflow's Deployment Version, in which case, the activity will be sent to a -different Deployment Version according to the Current Deployment Version of its own task -queue. -Workflows stuck on a backlogged activity will still auto-upgrade if the Current Deployment -Version of their Task Queue changes, without having to wait for the backlogged activity to -complete on the old Version. +different Deployment Version according to the Current or Ramping Deployment Version of its own +Task Queue. +Workflows stuck on a backlogged activity will still auto-upgrade if their Target Version +changes, without having to wait for the backlogged activity to complete on the old Version. """ global___VersioningBehavior = VersioningBehavior + +class _ContinueAsNewVersioningBehavior: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _ContinueAsNewVersioningBehaviorEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ + _ContinueAsNewVersioningBehavior.ValueType + ], + builtins.type, +): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + CONTINUE_AS_NEW_VERSIONING_BEHAVIOR_UNSPECIFIED: ( + _ContinueAsNewVersioningBehavior.ValueType + ) # 0 + CONTINUE_AS_NEW_VERSIONING_BEHAVIOR_AUTO_UPGRADE: ( + _ContinueAsNewVersioningBehavior.ValueType + ) # 1 + """Experimental. + Start the new run with AutoUpgrade behavior. Use the Target Version of the workflow's task queue at + start-time, as AutoUpgrade workflows do. After the first workflow task completes, use whatever + Versioning Behavior the workflow is annotated with in the workflow code. + + Note that if the workflow being continued has a Pinned override, that override will be inherited by the + new workflow run regardless of the ContinueAsNewVersioningBehavior specified in the continue-as-new + command. Versioning Override always takes precedence until it's removed manually via UpdateWorkflowExecutionOptions. + """ + CONTINUE_AS_NEW_VERSIONING_BEHAVIOR_USE_RAMPING_VERSION: ( + _ContinueAsNewVersioningBehavior.ValueType + ) # 2 + """Experimental. + Use the Ramping Version of the workflow's task queue at start time, regardless of the workflow's + Target Version (according to f(workflow_id, ramp_percentage)). After the first workflow task completes, + the workflow will use whatever Versioning Behavior it is annotated with. If there is no Ramping + Version by the time that the first workflow task is dispatched, it will be sent to the Current Version. + + It is highly discouraged to use this if the workflow is annotated with AutoUpgrade behavior, because + this setting ONLY applies to the first task of the workflow. If, after the first task, the workflow + is AutoUpgrade, it will behave like a normal AutoUpgrade workflow and go to the Target Version, which + may be the Current Version instead of the Ramping Version. + + Note that if the workflow being continued has a Pinned override, that override will be inherited by the + new workflow run regardless of the ContinueAsNewVersioningBehavior specified in the continue-as-new + command. Versioning Override always takes precedence until it's removed manually via UpdateWorkflowExecutionOptions. + """ + +class ContinueAsNewVersioningBehavior( + _ContinueAsNewVersioningBehavior, + metaclass=_ContinueAsNewVersioningBehaviorEnumTypeWrapper, +): + """Experimental. Defines the versioning behavior to be used by the first task of a new workflow run in a continue-as-new chain.""" + +CONTINUE_AS_NEW_VERSIONING_BEHAVIOR_UNSPECIFIED: ( + ContinueAsNewVersioningBehavior.ValueType +) # 0 +CONTINUE_AS_NEW_VERSIONING_BEHAVIOR_AUTO_UPGRADE: ( + ContinueAsNewVersioningBehavior.ValueType +) # 1 +"""Experimental. +Start the new run with AutoUpgrade behavior. Use the Target Version of the workflow's task queue at +start-time, as AutoUpgrade workflows do. After the first workflow task completes, use whatever +Versioning Behavior the workflow is annotated with in the workflow code. + +Note that if the workflow being continued has a Pinned override, that override will be inherited by the +new workflow run regardless of the ContinueAsNewVersioningBehavior specified in the continue-as-new +command. Versioning Override always takes precedence until it's removed manually via UpdateWorkflowExecutionOptions. +""" +CONTINUE_AS_NEW_VERSIONING_BEHAVIOR_USE_RAMPING_VERSION: ( + ContinueAsNewVersioningBehavior.ValueType +) # 2 +"""Experimental. +Use the Ramping Version of the workflow's task queue at start time, regardless of the workflow's +Target Version (according to f(workflow_id, ramp_percentage)). After the first workflow task completes, +the workflow will use whatever Versioning Behavior it is annotated with. If there is no Ramping +Version by the time that the first workflow task is dispatched, it will be sent to the Current Version. + +It is highly discouraged to use this if the workflow is annotated with AutoUpgrade behavior, because +this setting ONLY applies to the first task of the workflow. If, after the first task, the workflow +is AutoUpgrade, it will behave like a normal AutoUpgrade workflow and go to the Target Version, which +may be the Current Version instead of the Ramping Version. + +Note that if the workflow being continued has a Pinned override, that override will be inherited by the +new workflow run regardless of the ContinueAsNewVersioningBehavior specified in the continue-as-new +command. Versioning Override always takes precedence until it's removed manually via UpdateWorkflowExecutionOptions. +""" +global___ContinueAsNewVersioningBehavior = ContinueAsNewVersioningBehavior + +class _SuggestContinueAsNewReason: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _SuggestContinueAsNewReasonEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ + _SuggestContinueAsNewReason.ValueType + ], + builtins.type, +): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + SUGGEST_CONTINUE_AS_NEW_REASON_UNSPECIFIED: ( + _SuggestContinueAsNewReason.ValueType + ) # 0 + SUGGEST_CONTINUE_AS_NEW_REASON_HISTORY_SIZE_TOO_LARGE: ( + _SuggestContinueAsNewReason.ValueType + ) # 1 + """Workflow History size is getting too large.""" + SUGGEST_CONTINUE_AS_NEW_REASON_TOO_MANY_HISTORY_EVENTS: ( + _SuggestContinueAsNewReason.ValueType + ) # 2 + """Workflow History event count is getting too large.""" + SUGGEST_CONTINUE_AS_NEW_REASON_TOO_MANY_UPDATES: ( + _SuggestContinueAsNewReason.ValueType + ) # 3 + """Workflow's count of completed plus in-flight updates is too large.""" + +class SuggestContinueAsNewReason( + _SuggestContinueAsNewReason, metaclass=_SuggestContinueAsNewReasonEnumTypeWrapper +): + """SuggestContinueAsNewReason specifies why SuggestContinueAsNew is true.""" + +SUGGEST_CONTINUE_AS_NEW_REASON_UNSPECIFIED: SuggestContinueAsNewReason.ValueType # 0 +SUGGEST_CONTINUE_AS_NEW_REASON_HISTORY_SIZE_TOO_LARGE: ( + SuggestContinueAsNewReason.ValueType +) # 1 +"""Workflow History size is getting too large.""" +SUGGEST_CONTINUE_AS_NEW_REASON_TOO_MANY_HISTORY_EVENTS: ( + SuggestContinueAsNewReason.ValueType +) # 2 +"""Workflow History event count is getting too large.""" +SUGGEST_CONTINUE_AS_NEW_REASON_TOO_MANY_UPDATES: ( + SuggestContinueAsNewReason.ValueType +) # 3 +"""Workflow's count of completed plus in-flight updates is too large.""" +global___SuggestContinueAsNewReason = SuggestContinueAsNewReason diff --git a/temporalio/api/errordetails/v1/__init__.py b/temporalio/api/errordetails/v1/__init__.py index 2afb6ec95..068503e9e 100644 --- a/temporalio/api/errordetails/v1/__init__.py +++ b/temporalio/api/errordetails/v1/__init__.py @@ -1,4 +1,5 @@ from .message_pb2 import ( + ActivityExecutionAlreadyStartedFailure, CancellationAlreadyRequestedFailure, ClientVersionNotSupportedFailure, MultiOperationExecutionFailure, @@ -8,6 +9,7 @@ NamespaceNotFoundFailure, NamespaceUnavailableFailure, NewerBuildExistsFailure, + NexusOperationExecutionAlreadyStartedFailure, NotFoundFailure, PermissionDeniedFailure, QueryFailedFailure, @@ -16,9 +18,11 @@ SystemWorkflowFailure, WorkflowExecutionAlreadyStartedFailure, WorkflowNotReadyFailure, + WorkflowTaskCompletionBufferLostFailure, ) __all__ = [ + "ActivityExecutionAlreadyStartedFailure", "CancellationAlreadyRequestedFailure", "ClientVersionNotSupportedFailure", "MultiOperationExecutionFailure", @@ -28,6 +32,7 @@ "NamespaceNotFoundFailure", "NamespaceUnavailableFailure", "NewerBuildExistsFailure", + "NexusOperationExecutionAlreadyStartedFailure", "NotFoundFailure", "PermissionDeniedFailure", "QueryFailedFailure", @@ -36,4 +41,5 @@ "SystemWorkflowFailure", "WorkflowExecutionAlreadyStartedFailure", "WorkflowNotReadyFailure", + "WorkflowTaskCompletionBufferLostFailure", ] diff --git a/temporalio/api/errordetails/v1/message_pb2.py b/temporalio/api/errordetails/v1/message_pb2.py index 8f8b2c853..22c58484c 100644 --- a/temporalio/api/errordetails/v1/message_pb2.py +++ b/temporalio/api/errordetails/v1/message_pb2.py @@ -30,7 +30,7 @@ ) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n*temporal/api/errordetails/v1/message.proto\x12\x1ctemporal.api.errordetails.v1\x1a\x19google/protobuf/any.proto\x1a$temporal/api/common/v1/message.proto\x1a(temporal/api/enums/v1/failed_cause.proto\x1a%temporal/api/enums/v1/namespace.proto\x1a%temporal/api/failure/v1/message.proto"B\n\x0fNotFoundFailure\x12\x17\n\x0f\x63urrent_cluster\x18\x01 \x01(\t\x12\x16\n\x0e\x61\x63tive_cluster\x18\x02 \x01(\t"R\n&WorkflowExecutionAlreadyStartedFailure\x12\x18\n\x10start_request_id\x18\x01 \x01(\t\x12\x0e\n\x06run_id\x18\x02 \x01(\t"_\n\x19NamespaceNotActiveFailure\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x17\n\x0f\x63urrent_cluster\x18\x02 \x01(\t\x12\x16\n\x0e\x61\x63tive_cluster\x18\x03 \x01(\t"0\n\x1bNamespaceUnavailableFailure\x12\x11\n\tnamespace\x18\x01 \x01(\t"\xa6\x01\n\x1cNamespaceInvalidStateFailure\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x34\n\x05state\x18\x02 \x01(\x0e\x32%.temporal.api.enums.v1.NamespaceState\x12=\n\x0e\x61llowed_states\x18\x03 \x03(\x0e\x32%.temporal.api.enums.v1.NamespaceState"-\n\x18NamespaceNotFoundFailure\x12\x11\n\tnamespace\x18\x01 \x01(\t"\x1f\n\x1dNamespaceAlreadyExistsFailure"k\n ClientVersionNotSupportedFailure\x12\x16\n\x0e\x63lient_version\x18\x01 \x01(\t\x12\x13\n\x0b\x63lient_name\x18\x02 \x01(\t\x12\x1a\n\x12supported_versions\x18\x03 \x01(\t"d\n ServerVersionNotSupportedFailure\x12\x16\n\x0eserver_version\x18\x01 \x01(\t\x12(\n client_supported_server_versions\x18\x02 \x01(\t"%\n#CancellationAlreadyRequestedFailure"G\n\x12QueryFailedFailure\x12\x31\n\x07\x66\x61ilure\x18\x01 \x01(\x0b\x32 .temporal.api.failure.v1.Failure")\n\x17PermissionDeniedFailure\x12\x0e\n\x06reason\x18\x01 \x01(\t"\x96\x01\n\x18ResourceExhaustedFailure\x12<\n\x05\x63\x61use\x18\x01 \x01(\x0e\x32-.temporal.api.enums.v1.ResourceExhaustedCause\x12<\n\x05scope\x18\x02 \x01(\x0e\x32-.temporal.api.enums.v1.ResourceExhaustedScope"v\n\x15SystemWorkflowFailure\x12\x45\n\x12workflow_execution\x18\x01 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x16\n\x0eworkflow_error\x18\x02 \x01(\t"\x19\n\x17WorkflowNotReadyFailure"3\n\x17NewerBuildExistsFailure\x12\x18\n\x10\x64\x65\x66\x61ult_build_id\x18\x01 \x01(\t"\xd9\x01\n\x1eMultiOperationExecutionFailure\x12^\n\x08statuses\x18\x01 \x03(\x0b\x32L.temporal.api.errordetails.v1.MultiOperationExecutionFailure.OperationStatus\x1aW\n\x0fOperationStatus\x12\x0c\n\x04\x63ode\x18\x01 \x01(\x05\x12\x0f\n\x07message\x18\x02 \x01(\t\x12%\n\x07\x64\x65tails\x18\x03 \x03(\x0b\x32\x14.google.protobuf.AnyB\xa7\x01\n\x1fio.temporal.api.errordetails.v1B\x0cMessageProtoP\x01Z/go.temporal.io/api/errordetails/v1;errordetails\xaa\x02\x1eTemporalio.Api.ErrorDetails.V1\xea\x02!Temporalio::Api::ErrorDetails::V1b\x06proto3' + b'\n*temporal/api/errordetails/v1/message.proto\x12\x1ctemporal.api.errordetails.v1\x1a\x19google/protobuf/any.proto\x1a$temporal/api/common/v1/message.proto\x1a(temporal/api/enums/v1/failed_cause.proto\x1a%temporal/api/enums/v1/namespace.proto\x1a%temporal/api/failure/v1/message.proto"B\n\x0fNotFoundFailure\x12\x17\n\x0f\x63urrent_cluster\x18\x01 \x01(\t\x12\x16\n\x0e\x61\x63tive_cluster\x18\x02 \x01(\t"r\n&WorkflowExecutionAlreadyStartedFailure\x12\x18\n\x10start_request_id\x18\x01 \x01(\t\x12\x0e\n\x06run_id\x18\x02 \x01(\t\x12\x1e\n\x16\x66irst_execution_run_id\x18\x03 \x01(\t"_\n\x19NamespaceNotActiveFailure\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x17\n\x0f\x63urrent_cluster\x18\x02 \x01(\t\x12\x16\n\x0e\x61\x63tive_cluster\x18\x03 \x01(\t"0\n\x1bNamespaceUnavailableFailure\x12\x11\n\tnamespace\x18\x01 \x01(\t"\xa6\x01\n\x1cNamespaceInvalidStateFailure\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x34\n\x05state\x18\x02 \x01(\x0e\x32%.temporal.api.enums.v1.NamespaceState\x12=\n\x0e\x61llowed_states\x18\x03 \x03(\x0e\x32%.temporal.api.enums.v1.NamespaceState"-\n\x18NamespaceNotFoundFailure\x12\x11\n\tnamespace\x18\x01 \x01(\t"\x1f\n\x1dNamespaceAlreadyExistsFailure"k\n ClientVersionNotSupportedFailure\x12\x16\n\x0e\x63lient_version\x18\x01 \x01(\t\x12\x13\n\x0b\x63lient_name\x18\x02 \x01(\t\x12\x1a\n\x12supported_versions\x18\x03 \x01(\t"d\n ServerVersionNotSupportedFailure\x12\x16\n\x0eserver_version\x18\x01 \x01(\t\x12(\n client_supported_server_versions\x18\x02 \x01(\t"%\n#CancellationAlreadyRequestedFailure"G\n\x12QueryFailedFailure\x12\x31\n\x07\x66\x61ilure\x18\x01 \x01(\x0b\x32 .temporal.api.failure.v1.Failure")\n\x17PermissionDeniedFailure\x12\x0e\n\x06reason\x18\x01 \x01(\t"\x96\x01\n\x18ResourceExhaustedFailure\x12<\n\x05\x63\x61use\x18\x01 \x01(\x0e\x32-.temporal.api.enums.v1.ResourceExhaustedCause\x12<\n\x05scope\x18\x02 \x01(\x0e\x32-.temporal.api.enums.v1.ResourceExhaustedScope"v\n\x15SystemWorkflowFailure\x12\x45\n\x12workflow_execution\x18\x01 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x16\n\x0eworkflow_error\x18\x02 \x01(\t"\x19\n\x17WorkflowNotReadyFailure"3\n\x17NewerBuildExistsFailure\x12\x18\n\x10\x64\x65\x66\x61ult_build_id\x18\x01 \x01(\t"\xd9\x01\n\x1eMultiOperationExecutionFailure\x12^\n\x08statuses\x18\x01 \x03(\x0b\x32L.temporal.api.errordetails.v1.MultiOperationExecutionFailure.OperationStatus\x1aW\n\x0fOperationStatus\x12\x0c\n\x04\x63ode\x18\x01 \x01(\x05\x12\x0f\n\x07message\x18\x02 \x01(\t\x12%\n\x07\x64\x65tails\x18\x03 \x03(\x0b\x32\x14.google.protobuf.Any"R\n&ActivityExecutionAlreadyStartedFailure\x12\x18\n\x10start_request_id\x18\x01 \x01(\t\x12\x0e\n\x06run_id\x18\x02 \x01(\t"X\n,NexusOperationExecutionAlreadyStartedFailure\x12\x18\n\x10start_request_id\x18\x01 \x01(\t\x12\x0e\n\x06run_id\x18\x02 \x01(\t")\n\'WorkflowTaskCompletionBufferLostFailureB\xa7\x01\n\x1fio.temporal.api.errordetails.v1B\x0cMessageProtoP\x01Z/go.temporal.io/api/errordetails/v1;errordetails\xaa\x02\x1eTemporalio.Api.ErrorDetails.V1\xea\x02!Temporalio::Api::ErrorDetails::V1b\x06proto3' ) @@ -72,12 +72,21 @@ _MULTIOPERATIONEXECUTIONFAILURE_OPERATIONSTATUS = ( _MULTIOPERATIONEXECUTIONFAILURE.nested_types_by_name["OperationStatus"] ) +_ACTIVITYEXECUTIONALREADYSTARTEDFAILURE = DESCRIPTOR.message_types_by_name[ + "ActivityExecutionAlreadyStartedFailure" +] +_NEXUSOPERATIONEXECUTIONALREADYSTARTEDFAILURE = DESCRIPTOR.message_types_by_name[ + "NexusOperationExecutionAlreadyStartedFailure" +] +_WORKFLOWTASKCOMPLETIONBUFFERLOSTFAILURE = DESCRIPTOR.message_types_by_name[ + "WorkflowTaskCompletionBufferLostFailure" +] NotFoundFailure = _reflection.GeneratedProtocolMessageType( "NotFoundFailure", (_message.Message,), { "DESCRIPTOR": _NOTFOUNDFAILURE, - "__module__": "temporal.api.errordetails.v1.message_pb2", + "__module__": "temporalio.api.errordetails.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.errordetails.v1.NotFoundFailure) }, ) @@ -88,7 +97,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWEXECUTIONALREADYSTARTEDFAILURE, - "__module__": "temporal.api.errordetails.v1.message_pb2", + "__module__": "temporalio.api.errordetails.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.errordetails.v1.WorkflowExecutionAlreadyStartedFailure) }, ) @@ -99,7 +108,7 @@ (_message.Message,), { "DESCRIPTOR": _NAMESPACENOTACTIVEFAILURE, - "__module__": "temporal.api.errordetails.v1.message_pb2", + "__module__": "temporalio.api.errordetails.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.errordetails.v1.NamespaceNotActiveFailure) }, ) @@ -110,7 +119,7 @@ (_message.Message,), { "DESCRIPTOR": _NAMESPACEUNAVAILABLEFAILURE, - "__module__": "temporal.api.errordetails.v1.message_pb2", + "__module__": "temporalio.api.errordetails.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.errordetails.v1.NamespaceUnavailableFailure) }, ) @@ -121,7 +130,7 @@ (_message.Message,), { "DESCRIPTOR": _NAMESPACEINVALIDSTATEFAILURE, - "__module__": "temporal.api.errordetails.v1.message_pb2", + "__module__": "temporalio.api.errordetails.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.errordetails.v1.NamespaceInvalidStateFailure) }, ) @@ -132,7 +141,7 @@ (_message.Message,), { "DESCRIPTOR": _NAMESPACENOTFOUNDFAILURE, - "__module__": "temporal.api.errordetails.v1.message_pb2", + "__module__": "temporalio.api.errordetails.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.errordetails.v1.NamespaceNotFoundFailure) }, ) @@ -143,7 +152,7 @@ (_message.Message,), { "DESCRIPTOR": _NAMESPACEALREADYEXISTSFAILURE, - "__module__": "temporal.api.errordetails.v1.message_pb2", + "__module__": "temporalio.api.errordetails.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.errordetails.v1.NamespaceAlreadyExistsFailure) }, ) @@ -154,7 +163,7 @@ (_message.Message,), { "DESCRIPTOR": _CLIENTVERSIONNOTSUPPORTEDFAILURE, - "__module__": "temporal.api.errordetails.v1.message_pb2", + "__module__": "temporalio.api.errordetails.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.errordetails.v1.ClientVersionNotSupportedFailure) }, ) @@ -165,7 +174,7 @@ (_message.Message,), { "DESCRIPTOR": _SERVERVERSIONNOTSUPPORTEDFAILURE, - "__module__": "temporal.api.errordetails.v1.message_pb2", + "__module__": "temporalio.api.errordetails.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.errordetails.v1.ServerVersionNotSupportedFailure) }, ) @@ -176,7 +185,7 @@ (_message.Message,), { "DESCRIPTOR": _CANCELLATIONALREADYREQUESTEDFAILURE, - "__module__": "temporal.api.errordetails.v1.message_pb2", + "__module__": "temporalio.api.errordetails.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.errordetails.v1.CancellationAlreadyRequestedFailure) }, ) @@ -187,7 +196,7 @@ (_message.Message,), { "DESCRIPTOR": _QUERYFAILEDFAILURE, - "__module__": "temporal.api.errordetails.v1.message_pb2", + "__module__": "temporalio.api.errordetails.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.errordetails.v1.QueryFailedFailure) }, ) @@ -198,7 +207,7 @@ (_message.Message,), { "DESCRIPTOR": _PERMISSIONDENIEDFAILURE, - "__module__": "temporal.api.errordetails.v1.message_pb2", + "__module__": "temporalio.api.errordetails.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.errordetails.v1.PermissionDeniedFailure) }, ) @@ -209,7 +218,7 @@ (_message.Message,), { "DESCRIPTOR": _RESOURCEEXHAUSTEDFAILURE, - "__module__": "temporal.api.errordetails.v1.message_pb2", + "__module__": "temporalio.api.errordetails.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.errordetails.v1.ResourceExhaustedFailure) }, ) @@ -220,7 +229,7 @@ (_message.Message,), { "DESCRIPTOR": _SYSTEMWORKFLOWFAILURE, - "__module__": "temporal.api.errordetails.v1.message_pb2", + "__module__": "temporalio.api.errordetails.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.errordetails.v1.SystemWorkflowFailure) }, ) @@ -231,7 +240,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWNOTREADYFAILURE, - "__module__": "temporal.api.errordetails.v1.message_pb2", + "__module__": "temporalio.api.errordetails.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.errordetails.v1.WorkflowNotReadyFailure) }, ) @@ -242,7 +251,7 @@ (_message.Message,), { "DESCRIPTOR": _NEWERBUILDEXISTSFAILURE, - "__module__": "temporal.api.errordetails.v1.message_pb2", + "__module__": "temporalio.api.errordetails.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.errordetails.v1.NewerBuildExistsFailure) }, ) @@ -257,55 +266,94 @@ (_message.Message,), { "DESCRIPTOR": _MULTIOPERATIONEXECUTIONFAILURE_OPERATIONSTATUS, - "__module__": "temporal.api.errordetails.v1.message_pb2", + "__module__": "temporalio.api.errordetails.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.errordetails.v1.MultiOperationExecutionFailure.OperationStatus) }, ), "DESCRIPTOR": _MULTIOPERATIONEXECUTIONFAILURE, - "__module__": "temporal.api.errordetails.v1.message_pb2", + "__module__": "temporalio.api.errordetails.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.errordetails.v1.MultiOperationExecutionFailure) }, ) _sym_db.RegisterMessage(MultiOperationExecutionFailure) _sym_db.RegisterMessage(MultiOperationExecutionFailure.OperationStatus) +ActivityExecutionAlreadyStartedFailure = _reflection.GeneratedProtocolMessageType( + "ActivityExecutionAlreadyStartedFailure", + (_message.Message,), + { + "DESCRIPTOR": _ACTIVITYEXECUTIONALREADYSTARTEDFAILURE, + "__module__": "temporalio.api.errordetails.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.errordetails.v1.ActivityExecutionAlreadyStartedFailure) + }, +) +_sym_db.RegisterMessage(ActivityExecutionAlreadyStartedFailure) + +NexusOperationExecutionAlreadyStartedFailure = _reflection.GeneratedProtocolMessageType( + "NexusOperationExecutionAlreadyStartedFailure", + (_message.Message,), + { + "DESCRIPTOR": _NEXUSOPERATIONEXECUTIONALREADYSTARTEDFAILURE, + "__module__": "temporalio.api.errordetails.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.errordetails.v1.NexusOperationExecutionAlreadyStartedFailure) + }, +) +_sym_db.RegisterMessage(NexusOperationExecutionAlreadyStartedFailure) + +WorkflowTaskCompletionBufferLostFailure = _reflection.GeneratedProtocolMessageType( + "WorkflowTaskCompletionBufferLostFailure", + (_message.Message,), + { + "DESCRIPTOR": _WORKFLOWTASKCOMPLETIONBUFFERLOSTFAILURE, + "__module__": "temporalio.api.errordetails.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.errordetails.v1.WorkflowTaskCompletionBufferLostFailure) + }, +) +_sym_db.RegisterMessage(WorkflowTaskCompletionBufferLostFailure) + if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b"\n\037io.temporal.api.errordetails.v1B\014MessageProtoP\001Z/go.temporal.io/api/errordetails/v1;errordetails\252\002\036Temporalio.Api.ErrorDetails.V1\352\002!Temporalio::Api::ErrorDetails::V1" _NOTFOUNDFAILURE._serialized_start = 261 _NOTFOUNDFAILURE._serialized_end = 327 _WORKFLOWEXECUTIONALREADYSTARTEDFAILURE._serialized_start = 329 - _WORKFLOWEXECUTIONALREADYSTARTEDFAILURE._serialized_end = 411 - _NAMESPACENOTACTIVEFAILURE._serialized_start = 413 - _NAMESPACENOTACTIVEFAILURE._serialized_end = 508 - _NAMESPACEUNAVAILABLEFAILURE._serialized_start = 510 - _NAMESPACEUNAVAILABLEFAILURE._serialized_end = 558 - _NAMESPACEINVALIDSTATEFAILURE._serialized_start = 561 - _NAMESPACEINVALIDSTATEFAILURE._serialized_end = 727 - _NAMESPACENOTFOUNDFAILURE._serialized_start = 729 - _NAMESPACENOTFOUNDFAILURE._serialized_end = 774 - _NAMESPACEALREADYEXISTSFAILURE._serialized_start = 776 - _NAMESPACEALREADYEXISTSFAILURE._serialized_end = 807 - _CLIENTVERSIONNOTSUPPORTEDFAILURE._serialized_start = 809 - _CLIENTVERSIONNOTSUPPORTEDFAILURE._serialized_end = 916 - _SERVERVERSIONNOTSUPPORTEDFAILURE._serialized_start = 918 - _SERVERVERSIONNOTSUPPORTEDFAILURE._serialized_end = 1018 - _CANCELLATIONALREADYREQUESTEDFAILURE._serialized_start = 1020 - _CANCELLATIONALREADYREQUESTEDFAILURE._serialized_end = 1057 - _QUERYFAILEDFAILURE._serialized_start = 1059 - _QUERYFAILEDFAILURE._serialized_end = 1130 - _PERMISSIONDENIEDFAILURE._serialized_start = 1132 - _PERMISSIONDENIEDFAILURE._serialized_end = 1173 - _RESOURCEEXHAUSTEDFAILURE._serialized_start = 1176 - _RESOURCEEXHAUSTEDFAILURE._serialized_end = 1326 - _SYSTEMWORKFLOWFAILURE._serialized_start = 1328 - _SYSTEMWORKFLOWFAILURE._serialized_end = 1446 - _WORKFLOWNOTREADYFAILURE._serialized_start = 1448 - _WORKFLOWNOTREADYFAILURE._serialized_end = 1473 - _NEWERBUILDEXISTSFAILURE._serialized_start = 1475 - _NEWERBUILDEXISTSFAILURE._serialized_end = 1526 - _MULTIOPERATIONEXECUTIONFAILURE._serialized_start = 1529 - _MULTIOPERATIONEXECUTIONFAILURE._serialized_end = 1746 - _MULTIOPERATIONEXECUTIONFAILURE_OPERATIONSTATUS._serialized_start = 1659 - _MULTIOPERATIONEXECUTIONFAILURE_OPERATIONSTATUS._serialized_end = 1746 + _WORKFLOWEXECUTIONALREADYSTARTEDFAILURE._serialized_end = 443 + _NAMESPACENOTACTIVEFAILURE._serialized_start = 445 + _NAMESPACENOTACTIVEFAILURE._serialized_end = 540 + _NAMESPACEUNAVAILABLEFAILURE._serialized_start = 542 + _NAMESPACEUNAVAILABLEFAILURE._serialized_end = 590 + _NAMESPACEINVALIDSTATEFAILURE._serialized_start = 593 + _NAMESPACEINVALIDSTATEFAILURE._serialized_end = 759 + _NAMESPACENOTFOUNDFAILURE._serialized_start = 761 + _NAMESPACENOTFOUNDFAILURE._serialized_end = 806 + _NAMESPACEALREADYEXISTSFAILURE._serialized_start = 808 + _NAMESPACEALREADYEXISTSFAILURE._serialized_end = 839 + _CLIENTVERSIONNOTSUPPORTEDFAILURE._serialized_start = 841 + _CLIENTVERSIONNOTSUPPORTEDFAILURE._serialized_end = 948 + _SERVERVERSIONNOTSUPPORTEDFAILURE._serialized_start = 950 + _SERVERVERSIONNOTSUPPORTEDFAILURE._serialized_end = 1050 + _CANCELLATIONALREADYREQUESTEDFAILURE._serialized_start = 1052 + _CANCELLATIONALREADYREQUESTEDFAILURE._serialized_end = 1089 + _QUERYFAILEDFAILURE._serialized_start = 1091 + _QUERYFAILEDFAILURE._serialized_end = 1162 + _PERMISSIONDENIEDFAILURE._serialized_start = 1164 + _PERMISSIONDENIEDFAILURE._serialized_end = 1205 + _RESOURCEEXHAUSTEDFAILURE._serialized_start = 1208 + _RESOURCEEXHAUSTEDFAILURE._serialized_end = 1358 + _SYSTEMWORKFLOWFAILURE._serialized_start = 1360 + _SYSTEMWORKFLOWFAILURE._serialized_end = 1478 + _WORKFLOWNOTREADYFAILURE._serialized_start = 1480 + _WORKFLOWNOTREADYFAILURE._serialized_end = 1505 + _NEWERBUILDEXISTSFAILURE._serialized_start = 1507 + _NEWERBUILDEXISTSFAILURE._serialized_end = 1558 + _MULTIOPERATIONEXECUTIONFAILURE._serialized_start = 1561 + _MULTIOPERATIONEXECUTIONFAILURE._serialized_end = 1778 + _MULTIOPERATIONEXECUTIONFAILURE_OPERATIONSTATUS._serialized_start = 1691 + _MULTIOPERATIONEXECUTIONFAILURE_OPERATIONSTATUS._serialized_end = 1778 + _ACTIVITYEXECUTIONALREADYSTARTEDFAILURE._serialized_start = 1780 + _ACTIVITYEXECUTIONALREADYSTARTEDFAILURE._serialized_end = 1862 + _NEXUSOPERATIONEXECUTIONALREADYSTARTEDFAILURE._serialized_start = 1864 + _NEXUSOPERATIONEXECUTIONALREADYSTARTEDFAILURE._serialized_end = 1952 + _WORKFLOWTASKCOMPLETIONBUFFERLOSTFAILURE._serialized_start = 1954 + _WORKFLOWTASKCOMPLETIONBUFFERLOSTFAILURE._serialized_end = 1995 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/errordetails/v1/message_pb2.pyi b/temporalio/api/errordetails/v1/message_pb2.pyi index 4857dbf31..5ffe230b3 100644 --- a/temporalio/api/errordetails/v1/message_pb2.pyi +++ b/temporalio/api/errordetails/v1/message_pb2.pyi @@ -53,18 +53,26 @@ class WorkflowExecutionAlreadyStartedFailure(google.protobuf.message.Message): START_REQUEST_ID_FIELD_NUMBER: builtins.int RUN_ID_FIELD_NUMBER: builtins.int + FIRST_EXECUTION_RUN_ID_FIELD_NUMBER: builtins.int start_request_id: builtins.str run_id: builtins.str + first_execution_run_id: builtins.str def __init__( self, *, start_request_id: builtins.str = ..., run_id: builtins.str = ..., + first_execution_run_id: builtins.str = ..., ) -> None: ... def ClearField( self, field_name: typing_extensions.Literal[ - "run_id", b"run_id", "start_request_id", b"start_request_id" + "first_execution_run_id", + b"first_execution_run_id", + "run_id", + b"run_id", + "start_request_id", + b"start_request_id", ], ) -> None: ... @@ -440,3 +448,75 @@ class MultiOperationExecutionFailure(google.protobuf.message.Message): ) -> None: ... global___MultiOperationExecutionFailure = MultiOperationExecutionFailure + +class ActivityExecutionAlreadyStartedFailure(google.protobuf.message.Message): + """An error indicating that an activity execution failed to start. Returned when there is an existing activity with the + given activity ID, and the given ID reuse and conflict policies do not permit starting a new one or attaching to an + existing one. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + START_REQUEST_ID_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + start_request_id: builtins.str + run_id: builtins.str + def __init__( + self, + *, + start_request_id: builtins.str = ..., + run_id: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "run_id", b"run_id", "start_request_id", b"start_request_id" + ], + ) -> None: ... + +global___ActivityExecutionAlreadyStartedFailure = ActivityExecutionAlreadyStartedFailure + +class NexusOperationExecutionAlreadyStartedFailure(google.protobuf.message.Message): + """An error indicating that a Nexus operation failed to start. Returned when there is an existing operation with the + given operation ID, and the given ID reuse and conflict policies do not permit starting a new one or attaching to an + existing one. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + START_REQUEST_ID_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + start_request_id: builtins.str + run_id: builtins.str + def __init__( + self, + *, + start_request_id: builtins.str = ..., + run_id: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "run_id", b"run_id", "start_request_id", b"start_request_id" + ], + ) -> None: ... + +global___NexusOperationExecutionAlreadyStartedFailure = ( + NexusOperationExecutionAlreadyStartedFailure +) + +class WorkflowTaskCompletionBufferLostFailure(google.protobuf.message.Message): + """An error indicating that the server lost the buffered pages of a paginated workflow task + completion. This is a transient error: the workflow task is still valid, and the client + should resend all pages from page 0 using the same task token. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___WorkflowTaskCompletionBufferLostFailure = ( + WorkflowTaskCompletionBufferLostFailure +) diff --git a/temporalio/api/export/v1/message_pb2.py b/temporalio/api/export/v1/message_pb2.py index 05a46aeff..35d672eca 100644 --- a/temporalio/api/export/v1/message_pb2.py +++ b/temporalio/api/export/v1/message_pb2.py @@ -30,7 +30,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWEXECUTION, - "__module__": "temporal.api.export.v1.message_pb2", + "__module__": "temporalio.api.export.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.export.v1.WorkflowExecution) }, ) @@ -41,7 +41,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWEXECUTIONS, - "__module__": "temporal.api.export.v1.message_pb2", + "__module__": "temporalio.api.export.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.export.v1.WorkflowExecutions) }, ) diff --git a/temporalio/api/failure/v1/message_pb2.py b/temporalio/api/failure/v1/message_pb2.py index b4867507d..db46b0717 100644 --- a/temporalio/api/failure/v1/message_pb2.py +++ b/temporalio/api/failure/v1/message_pb2.py @@ -30,7 +30,7 @@ ) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n%temporal/api/failure/v1/message.proto\x12\x17temporal.api.failure.v1\x1a$temporal/api/common/v1/message.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a!temporal/api/enums/v1/nexus.proto\x1a"temporal/api/enums/v1/common.proto\x1a\x1egoogle/protobuf/duration.proto"\xe8\x01\n\x16\x41pplicationFailureInfo\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x15\n\rnon_retryable\x18\x02 \x01(\x08\x12\x31\n\x07\x64\x65tails\x18\x03 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x33\n\x10next_retry_delay\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x41\n\x08\x63\x61tegory\x18\x05 \x01(\x0e\x32/.temporal.api.enums.v1.ApplicationErrorCategory"\x90\x01\n\x12TimeoutFailureInfo\x12\x38\n\x0ctimeout_type\x18\x01 \x01(\x0e\x32".temporal.api.enums.v1.TimeoutType\x12@\n\x16last_heartbeat_details\x18\x02 \x01(\x0b\x32 .temporal.api.common.v1.Payloads"H\n\x13\x43\x61nceledFailureInfo\x12\x31\n\x07\x64\x65tails\x18\x01 \x01(\x0b\x32 .temporal.api.common.v1.Payloads"\x17\n\x15TerminatedFailureInfo"*\n\x11ServerFailureInfo\x12\x15\n\rnon_retryable\x18\x01 \x01(\x08"\\\n\x18ResetWorkflowFailureInfo\x12@\n\x16last_heartbeat_details\x18\x01 \x01(\x0b\x32 .temporal.api.common.v1.Payloads"\xe7\x01\n\x13\x41\x63tivityFailureInfo\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03\x12\x18\n\x10started_event_id\x18\x02 \x01(\x03\x12\x10\n\x08identity\x18\x03 \x01(\t\x12;\n\ractivity_type\x18\x04 \x01(\x0b\x32$.temporal.api.common.v1.ActivityType\x12\x13\n\x0b\x61\x63tivity_id\x18\x05 \x01(\t\x12\x36\n\x0bretry_state\x18\x06 \x01(\x0e\x32!.temporal.api.enums.v1.RetryState"\xa8\x02\n!ChildWorkflowExecutionFailureInfo\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x45\n\x12workflow_execution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12;\n\rworkflow_type\x18\x03 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12\x1a\n\x12initiated_event_id\x18\x04 \x01(\x03\x12\x18\n\x10started_event_id\x18\x05 \x01(\x03\x12\x36\n\x0bretry_state\x18\x06 \x01(\x0e\x32!.temporal.api.enums.v1.RetryState"\xa0\x01\n\x19NexusOperationFailureInfo\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03\x12\x10\n\x08\x65ndpoint\x18\x02 \x01(\t\x12\x0f\n\x07service\x18\x03 \x01(\t\x12\x11\n\toperation\x18\x04 \x01(\t\x12\x18\n\x0coperation_id\x18\x05 \x01(\tB\x02\x18\x01\x12\x17\n\x0foperation_token\x18\x06 \x01(\t"v\n\x17NexusHandlerFailureInfo\x12\x0c\n\x04type\x18\x01 \x01(\t\x12M\n\x0eretry_behavior\x18\x02 \x01(\x0e\x32\x35.temporal.api.enums.v1.NexusHandlerErrorRetryBehavior"\xa0\x08\n\x07\x46\x61ilure\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x0e\n\x06source\x18\x02 \x01(\t\x12\x13\n\x0bstack_trace\x18\x03 \x01(\t\x12;\n\x12\x65ncoded_attributes\x18\x14 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12/\n\x05\x63\x61use\x18\x04 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12S\n\x18\x61pplication_failure_info\x18\x05 \x01(\x0b\x32/.temporal.api.failure.v1.ApplicationFailureInfoH\x00\x12K\n\x14timeout_failure_info\x18\x06 \x01(\x0b\x32+.temporal.api.failure.v1.TimeoutFailureInfoH\x00\x12M\n\x15\x63\x61nceled_failure_info\x18\x07 \x01(\x0b\x32,.temporal.api.failure.v1.CanceledFailureInfoH\x00\x12Q\n\x17terminated_failure_info\x18\x08 \x01(\x0b\x32..temporal.api.failure.v1.TerminatedFailureInfoH\x00\x12I\n\x13server_failure_info\x18\t \x01(\x0b\x32*.temporal.api.failure.v1.ServerFailureInfoH\x00\x12X\n\x1breset_workflow_failure_info\x18\n \x01(\x0b\x32\x31.temporal.api.failure.v1.ResetWorkflowFailureInfoH\x00\x12M\n\x15\x61\x63tivity_failure_info\x18\x0b \x01(\x0b\x32,.temporal.api.failure.v1.ActivityFailureInfoH\x00\x12k\n%child_workflow_execution_failure_info\x18\x0c \x01(\x0b\x32:.temporal.api.failure.v1.ChildWorkflowExecutionFailureInfoH\x00\x12\x64\n&nexus_operation_execution_failure_info\x18\r \x01(\x0b\x32\x32.temporal.api.failure.v1.NexusOperationFailureInfoH\x00\x12V\n\x1anexus_handler_failure_info\x18\x0e \x01(\x0b\x32\x30.temporal.api.failure.v1.NexusHandlerFailureInfoH\x00\x42\x0e\n\x0c\x66\x61ilure_info" \n\x1eMultiOperationExecutionAbortedB\x8e\x01\n\x1aio.temporal.api.failure.v1B\x0cMessageProtoP\x01Z%go.temporal.io/api/failure/v1;failure\xaa\x02\x19Temporalio.Api.Failure.V1\xea\x02\x1cTemporalio::Api::Failure::V1b\x06proto3' + b'\n%temporal/api/failure/v1/message.proto\x12\x17temporal.api.failure.v1\x1a$temporal/api/common/v1/message.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a!temporal/api/enums/v1/nexus.proto\x1a"temporal/api/enums/v1/common.proto\x1a\x1egoogle/protobuf/duration.proto"\xe8\x01\n\x16\x41pplicationFailureInfo\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x15\n\rnon_retryable\x18\x02 \x01(\x08\x12\x31\n\x07\x64\x65tails\x18\x03 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x33\n\x10next_retry_delay\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x41\n\x08\x63\x61tegory\x18\x05 \x01(\x0e\x32/.temporal.api.enums.v1.ApplicationErrorCategory"\x90\x01\n\x12TimeoutFailureInfo\x12\x38\n\x0ctimeout_type\x18\x01 \x01(\x0e\x32".temporal.api.enums.v1.TimeoutType\x12@\n\x16last_heartbeat_details\x18\x02 \x01(\x0b\x32 .temporal.api.common.v1.Payloads"Z\n\x13\x43\x61nceledFailureInfo\x12\x31\n\x07\x64\x65tails\x18\x01 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x10\n\x08identity\x18\x02 \x01(\t")\n\x15TerminatedFailureInfo\x12\x10\n\x08identity\x18\x01 \x01(\t"*\n\x11ServerFailureInfo\x12\x15\n\rnon_retryable\x18\x01 \x01(\x08"\\\n\x18ResetWorkflowFailureInfo\x12@\n\x16last_heartbeat_details\x18\x01 \x01(\x0b\x32 .temporal.api.common.v1.Payloads"\xe7\x01\n\x13\x41\x63tivityFailureInfo\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03\x12\x18\n\x10started_event_id\x18\x02 \x01(\x03\x12\x10\n\x08identity\x18\x03 \x01(\t\x12;\n\ractivity_type\x18\x04 \x01(\x0b\x32$.temporal.api.common.v1.ActivityType\x12\x13\n\x0b\x61\x63tivity_id\x18\x05 \x01(\t\x12\x36\n\x0bretry_state\x18\x06 \x01(\x0e\x32!.temporal.api.enums.v1.RetryState"\xa8\x02\n!ChildWorkflowExecutionFailureInfo\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x45\n\x12workflow_execution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12;\n\rworkflow_type\x18\x03 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12\x1a\n\x12initiated_event_id\x18\x04 \x01(\x03\x12\x18\n\x10started_event_id\x18\x05 \x01(\x03\x12\x36\n\x0bretry_state\x18\x06 \x01(\x0e\x32!.temporal.api.enums.v1.RetryState"\xa0\x01\n\x19NexusOperationFailureInfo\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03\x12\x10\n\x08\x65ndpoint\x18\x02 \x01(\t\x12\x0f\n\x07service\x18\x03 \x01(\t\x12\x11\n\toperation\x18\x04 \x01(\t\x12\x18\n\x0coperation_id\x18\x05 \x01(\tB\x02\x18\x01\x12\x17\n\x0foperation_token\x18\x06 \x01(\t"v\n\x17NexusHandlerFailureInfo\x12\x0c\n\x04type\x18\x01 \x01(\t\x12M\n\x0eretry_behavior\x18\x02 \x01(\x0e\x32\x35.temporal.api.enums.v1.NexusHandlerErrorRetryBehavior"\xa0\x08\n\x07\x46\x61ilure\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x0e\n\x06source\x18\x02 \x01(\t\x12\x13\n\x0bstack_trace\x18\x03 \x01(\t\x12;\n\x12\x65ncoded_attributes\x18\x14 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12/\n\x05\x63\x61use\x18\x04 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12S\n\x18\x61pplication_failure_info\x18\x05 \x01(\x0b\x32/.temporal.api.failure.v1.ApplicationFailureInfoH\x00\x12K\n\x14timeout_failure_info\x18\x06 \x01(\x0b\x32+.temporal.api.failure.v1.TimeoutFailureInfoH\x00\x12M\n\x15\x63\x61nceled_failure_info\x18\x07 \x01(\x0b\x32,.temporal.api.failure.v1.CanceledFailureInfoH\x00\x12Q\n\x17terminated_failure_info\x18\x08 \x01(\x0b\x32..temporal.api.failure.v1.TerminatedFailureInfoH\x00\x12I\n\x13server_failure_info\x18\t \x01(\x0b\x32*.temporal.api.failure.v1.ServerFailureInfoH\x00\x12X\n\x1breset_workflow_failure_info\x18\n \x01(\x0b\x32\x31.temporal.api.failure.v1.ResetWorkflowFailureInfoH\x00\x12M\n\x15\x61\x63tivity_failure_info\x18\x0b \x01(\x0b\x32,.temporal.api.failure.v1.ActivityFailureInfoH\x00\x12k\n%child_workflow_execution_failure_info\x18\x0c \x01(\x0b\x32:.temporal.api.failure.v1.ChildWorkflowExecutionFailureInfoH\x00\x12\x64\n&nexus_operation_execution_failure_info\x18\r \x01(\x0b\x32\x32.temporal.api.failure.v1.NexusOperationFailureInfoH\x00\x12V\n\x1anexus_handler_failure_info\x18\x0e \x01(\x0b\x32\x30.temporal.api.failure.v1.NexusHandlerFailureInfoH\x00\x42\x0e\n\x0c\x66\x61ilure_info" \n\x1eMultiOperationExecutionAbortedB\x8e\x01\n\x1aio.temporal.api.failure.v1B\x0cMessageProtoP\x01Z%go.temporal.io/api/failure/v1;failure\xaa\x02\x19Temporalio.Api.Failure.V1\xea\x02\x1cTemporalio::Api::Failure::V1b\x06proto3' ) @@ -57,7 +57,7 @@ (_message.Message,), { "DESCRIPTOR": _APPLICATIONFAILUREINFO, - "__module__": "temporal.api.failure.v1.message_pb2", + "__module__": "temporalio.api.failure.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.failure.v1.ApplicationFailureInfo) }, ) @@ -68,7 +68,7 @@ (_message.Message,), { "DESCRIPTOR": _TIMEOUTFAILUREINFO, - "__module__": "temporal.api.failure.v1.message_pb2", + "__module__": "temporalio.api.failure.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.failure.v1.TimeoutFailureInfo) }, ) @@ -79,7 +79,7 @@ (_message.Message,), { "DESCRIPTOR": _CANCELEDFAILUREINFO, - "__module__": "temporal.api.failure.v1.message_pb2", + "__module__": "temporalio.api.failure.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.failure.v1.CanceledFailureInfo) }, ) @@ -90,7 +90,7 @@ (_message.Message,), { "DESCRIPTOR": _TERMINATEDFAILUREINFO, - "__module__": "temporal.api.failure.v1.message_pb2", + "__module__": "temporalio.api.failure.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.failure.v1.TerminatedFailureInfo) }, ) @@ -101,7 +101,7 @@ (_message.Message,), { "DESCRIPTOR": _SERVERFAILUREINFO, - "__module__": "temporal.api.failure.v1.message_pb2", + "__module__": "temporalio.api.failure.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.failure.v1.ServerFailureInfo) }, ) @@ -112,7 +112,7 @@ (_message.Message,), { "DESCRIPTOR": _RESETWORKFLOWFAILUREINFO, - "__module__": "temporal.api.failure.v1.message_pb2", + "__module__": "temporalio.api.failure.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.failure.v1.ResetWorkflowFailureInfo) }, ) @@ -123,7 +123,7 @@ (_message.Message,), { "DESCRIPTOR": _ACTIVITYFAILUREINFO, - "__module__": "temporal.api.failure.v1.message_pb2", + "__module__": "temporalio.api.failure.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.failure.v1.ActivityFailureInfo) }, ) @@ -134,7 +134,7 @@ (_message.Message,), { "DESCRIPTOR": _CHILDWORKFLOWEXECUTIONFAILUREINFO, - "__module__": "temporal.api.failure.v1.message_pb2", + "__module__": "temporalio.api.failure.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.failure.v1.ChildWorkflowExecutionFailureInfo) }, ) @@ -145,7 +145,7 @@ (_message.Message,), { "DESCRIPTOR": _NEXUSOPERATIONFAILUREINFO, - "__module__": "temporal.api.failure.v1.message_pb2", + "__module__": "temporalio.api.failure.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.failure.v1.NexusOperationFailureInfo) }, ) @@ -156,7 +156,7 @@ (_message.Message,), { "DESCRIPTOR": _NEXUSHANDLERFAILUREINFO, - "__module__": "temporal.api.failure.v1.message_pb2", + "__module__": "temporalio.api.failure.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.failure.v1.NexusHandlerFailureInfo) }, ) @@ -167,7 +167,7 @@ (_message.Message,), { "DESCRIPTOR": _FAILURE, - "__module__": "temporal.api.failure.v1.message_pb2", + "__module__": "temporalio.api.failure.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.failure.v1.Failure) }, ) @@ -178,7 +178,7 @@ (_message.Message,), { "DESCRIPTOR": _MULTIOPERATIONEXECUTIONABORTED, - "__module__": "temporal.api.failure.v1.message_pb2", + "__module__": "temporalio.api.failure.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.failure.v1.MultiOperationExecutionAborted) }, ) @@ -196,23 +196,23 @@ _TIMEOUTFAILUREINFO._serialized_start = 481 _TIMEOUTFAILUREINFO._serialized_end = 625 _CANCELEDFAILUREINFO._serialized_start = 627 - _CANCELEDFAILUREINFO._serialized_end = 699 - _TERMINATEDFAILUREINFO._serialized_start = 701 - _TERMINATEDFAILUREINFO._serialized_end = 724 - _SERVERFAILUREINFO._serialized_start = 726 - _SERVERFAILUREINFO._serialized_end = 768 - _RESETWORKFLOWFAILUREINFO._serialized_start = 770 - _RESETWORKFLOWFAILUREINFO._serialized_end = 862 - _ACTIVITYFAILUREINFO._serialized_start = 865 - _ACTIVITYFAILUREINFO._serialized_end = 1096 - _CHILDWORKFLOWEXECUTIONFAILUREINFO._serialized_start = 1099 - _CHILDWORKFLOWEXECUTIONFAILUREINFO._serialized_end = 1395 - _NEXUSOPERATIONFAILUREINFO._serialized_start = 1398 - _NEXUSOPERATIONFAILUREINFO._serialized_end = 1558 - _NEXUSHANDLERFAILUREINFO._serialized_start = 1560 - _NEXUSHANDLERFAILUREINFO._serialized_end = 1678 - _FAILURE._serialized_start = 1681 - _FAILURE._serialized_end = 2737 - _MULTIOPERATIONEXECUTIONABORTED._serialized_start = 2739 - _MULTIOPERATIONEXECUTIONABORTED._serialized_end = 2771 + _CANCELEDFAILUREINFO._serialized_end = 717 + _TERMINATEDFAILUREINFO._serialized_start = 719 + _TERMINATEDFAILUREINFO._serialized_end = 760 + _SERVERFAILUREINFO._serialized_start = 762 + _SERVERFAILUREINFO._serialized_end = 804 + _RESETWORKFLOWFAILUREINFO._serialized_start = 806 + _RESETWORKFLOWFAILUREINFO._serialized_end = 898 + _ACTIVITYFAILUREINFO._serialized_start = 901 + _ACTIVITYFAILUREINFO._serialized_end = 1132 + _CHILDWORKFLOWEXECUTIONFAILUREINFO._serialized_start = 1135 + _CHILDWORKFLOWEXECUTIONFAILUREINFO._serialized_end = 1431 + _NEXUSOPERATIONFAILUREINFO._serialized_start = 1434 + _NEXUSOPERATIONFAILUREINFO._serialized_end = 1594 + _NEXUSHANDLERFAILUREINFO._serialized_start = 1596 + _NEXUSHANDLERFAILUREINFO._serialized_end = 1714 + _FAILURE._serialized_start = 1717 + _FAILURE._serialized_end = 2773 + _MULTIOPERATIONEXECUTIONABORTED._serialized_start = 2775 + _MULTIOPERATIONEXECUTIONABORTED._serialized_end = 2807 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/failure/v1/message_pb2.pyi b/temporalio/api/failure/v1/message_pb2.pyi index 76efeb112..e0eecf7e7 100644 --- a/temporalio/api/failure/v1/message_pb2.pyi +++ b/temporalio/api/failure/v1/message_pb2.pyi @@ -114,18 +114,25 @@ class CanceledFailureInfo(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor DETAILS_FIELD_NUMBER: builtins.int + IDENTITY_FIELD_NUMBER: builtins.int @property def details(self) -> temporalio.api.common.v1.message_pb2.Payloads: ... + identity: builtins.str + """The identity of the worker or client that requested the cancellation.""" def __init__( self, *, details: temporalio.api.common.v1.message_pb2.Payloads | None = ..., + identity: builtins.str = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal["details", b"details"] ) -> builtins.bool: ... def ClearField( - self, field_name: typing_extensions.Literal["details", b"details"] + self, + field_name: typing_extensions.Literal[ + "details", b"details", "identity", b"identity" + ], ) -> None: ... global___CanceledFailureInfo = CanceledFailureInfo @@ -133,8 +140,16 @@ global___CanceledFailureInfo = CanceledFailureInfo class TerminatedFailureInfo(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor + IDENTITY_FIELD_NUMBER: builtins.int + identity: builtins.str + """The identity of the worker or client that requested the termination.""" def __init__( self, + *, + identity: builtins.str = ..., + ) -> None: ... + def ClearField( + self, field_name: typing_extensions.Literal["identity", b"identity"] ) -> None: ... global___TerminatedFailureInfo = TerminatedFailureInfo @@ -293,6 +308,8 @@ class ChildWorkflowExecutionFailureInfo(google.protobuf.message.Message): global___ChildWorkflowExecutionFailureInfo = ChildWorkflowExecutionFailureInfo class NexusOperationFailureInfo(google.protobuf.message.Message): + """Representation of the Temporal SDK NexusOperationError object that is returned to workflow callers.""" + DESCRIPTOR: google.protobuf.descriptor.Descriptor SCHEDULED_EVENT_ID_FIELD_NUMBER: builtins.int diff --git a/temporalio/api/filter/v1/message_pb2.py b/temporalio/api/filter/v1/message_pb2.py index d939344c2..3df20337d 100644 --- a/temporalio/api/filter/v1/message_pb2.py +++ b/temporalio/api/filter/v1/message_pb2.py @@ -34,7 +34,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWEXECUTIONFILTER, - "__module__": "temporal.api.filter.v1.message_pb2", + "__module__": "temporalio.api.filter.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.filter.v1.WorkflowExecutionFilter) }, ) @@ -45,7 +45,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWTYPEFILTER, - "__module__": "temporal.api.filter.v1.message_pb2", + "__module__": "temporalio.api.filter.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.filter.v1.WorkflowTypeFilter) }, ) @@ -56,7 +56,7 @@ (_message.Message,), { "DESCRIPTOR": _STARTTIMEFILTER, - "__module__": "temporal.api.filter.v1.message_pb2", + "__module__": "temporalio.api.filter.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.filter.v1.StartTimeFilter) }, ) @@ -67,7 +67,7 @@ (_message.Message,), { "DESCRIPTOR": _STATUSFILTER, - "__module__": "temporal.api.filter.v1.message_pb2", + "__module__": "temporalio.api.filter.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.filter.v1.StatusFilter) }, ) diff --git a/temporalio/api/history/v1/__init__.py b/temporalio/api/history/v1/__init__.py index 3ed424c12..ec7f65962 100644 --- a/temporalio/api/history/v1/__init__.py +++ b/temporalio/api/history/v1/__init__.py @@ -13,6 +13,7 @@ ChildWorkflowExecutionStartedEventAttributes, ChildWorkflowExecutionTerminatedEventAttributes, ChildWorkflowExecutionTimedOutEventAttributes, + DeclinedTargetVersionUpgrade, ExternalWorkflowExecutionCancelRequestedEventAttributes, ExternalWorkflowExecutionSignaledEventAttributes, History, @@ -43,10 +44,13 @@ WorkflowExecutionContinuedAsNewEventAttributes, WorkflowExecutionFailedEventAttributes, WorkflowExecutionOptionsUpdatedEventAttributes, + WorkflowExecutionPausedEventAttributes, WorkflowExecutionSignaledEventAttributes, WorkflowExecutionStartedEventAttributes, WorkflowExecutionTerminatedEventAttributes, WorkflowExecutionTimedOutEventAttributes, + WorkflowExecutionTimeSkippingTransitionedEventAttributes, + WorkflowExecutionUnpausedEventAttributes, WorkflowExecutionUpdateAcceptedEventAttributes, WorkflowExecutionUpdateAdmittedEventAttributes, WorkflowExecutionUpdateCompletedEventAttributes, @@ -75,6 +79,7 @@ "ChildWorkflowExecutionStartedEventAttributes", "ChildWorkflowExecutionTerminatedEventAttributes", "ChildWorkflowExecutionTimedOutEventAttributes", + "DeclinedTargetVersionUpgrade", "ExternalWorkflowExecutionCancelRequestedEventAttributes", "ExternalWorkflowExecutionSignaledEventAttributes", "History", @@ -105,10 +110,13 @@ "WorkflowExecutionContinuedAsNewEventAttributes", "WorkflowExecutionFailedEventAttributes", "WorkflowExecutionOptionsUpdatedEventAttributes", + "WorkflowExecutionPausedEventAttributes", "WorkflowExecutionSignaledEventAttributes", "WorkflowExecutionStartedEventAttributes", "WorkflowExecutionTerminatedEventAttributes", + "WorkflowExecutionTimeSkippingTransitionedEventAttributes", "WorkflowExecutionTimedOutEventAttributes", + "WorkflowExecutionUnpausedEventAttributes", "WorkflowExecutionUpdateAcceptedEventAttributes", "WorkflowExecutionUpdateAdmittedEventAttributes", "WorkflowExecutionUpdateCompletedEventAttributes", diff --git a/temporalio/api/history/v1/message_pb2.py b/temporalio/api/history/v1/message_pb2.py index d3f40a8c9..ac337b708 100644 --- a/temporalio/api/history/v1/message_pb2.py +++ b/temporalio/api/history/v1/message_pb2.py @@ -38,6 +38,9 @@ from temporalio.api.failure.v1 import ( message_pb2 as temporal_dot_api_dot_failure_dot_v1_dot_message__pb2, ) +from temporalio.api.sdk.v1 import ( + event_group_marker_pb2 as temporal_dot_api_dot_sdk_dot_v1_dot_event__group__marker__pb2, +) from temporalio.api.sdk.v1 import ( task_complete_metadata_pb2 as temporal_dot_api_dot_sdk_dot_v1_dot_task__complete__metadata__pb2, ) @@ -55,13 +58,16 @@ ) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n%temporal/api/history/v1/message.proto\x12\x17temporal.api.history.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a&temporal/api/enums/v1/event_type.proto\x1a(temporal/api/enums/v1/failed_cause.proto\x1a"temporal/api/enums/v1/update.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a$temporal/api/common/v1/message.proto\x1a(temporal/api/deployment/v1/message.proto\x1a%temporal/api/failure/v1/message.proto\x1a\'temporal/api/taskqueue/v1/message.proto\x1a$temporal/api/update/v1/message.proto\x1a&temporal/api/workflow/v1/message.proto\x1a\x30temporal/api/sdk/v1/task_complete_metadata.proto\x1a\'temporal/api/sdk/v1/user_metadata.proto"\xd6\x0f\n\'WorkflowExecutionStartedEventAttributes\x12;\n\rworkflow_type\x18\x01 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12!\n\x19parent_workflow_namespace\x18\x02 \x01(\t\x12$\n\x1cparent_workflow_namespace_id\x18\x1b \x01(\t\x12L\n\x19parent_workflow_execution\x18\x03 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12!\n\x19parent_initiated_event_id\x18\x04 \x01(\x03\x12\x38\n\ntask_queue\x18\x05 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12/\n\x05input\x18\x06 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12=\n\x1aworkflow_execution_timeout\x18\x07 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x37\n\x14workflow_run_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12"\n\x1a\x63ontinued_execution_run_id\x18\n \x01(\t\x12@\n\tinitiator\x18\x0b \x01(\x0e\x32-.temporal.api.enums.v1.ContinueAsNewInitiator\x12;\n\x11\x63ontinued_failure\x18\x0c \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12@\n\x16last_completion_result\x18\r \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12!\n\x19original_execution_run_id\x18\x0e \x01(\t\x12\x10\n\x08identity\x18\x0f \x01(\t\x12\x1e\n\x16\x66irst_execution_run_id\x18\x10 \x01(\t\x12\x39\n\x0cretry_policy\x18\x11 \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x0f\n\x07\x61ttempt\x18\x12 \x01(\x05\x12\x46\n"workflow_execution_expiration_time\x18\x13 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x15\n\rcron_schedule\x18\x14 \x01(\t\x12>\n\x1b\x66irst_workflow_task_backoff\x18\x15 \x01(\x0b\x32\x19.google.protobuf.Duration\x12*\n\x04memo\x18\x16 \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo\x12\x43\n\x11search_attributes\x18\x17 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12\x45\n\x16prev_auto_reset_points\x18\x18 \x01(\x0b\x32%.temporal.api.workflow.v1.ResetPoints\x12.\n\x06header\x18\x19 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12&\n\x1eparent_initiated_event_version\x18\x1a \x01(\x03\x12\x13\n\x0bworkflow_id\x18\x1c \x01(\t\x12L\n\x14source_version_stamp\x18\x1d \x01(\x0b\x32*.temporal.api.common.v1.WorkerVersionStampB\x02\x18\x01\x12>\n\x14\x63ompletion_callbacks\x18\x1e \x03(\x0b\x32 .temporal.api.common.v1.Callback\x12J\n\x17root_workflow_execution\x18\x1f \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x1e\n\x12inherited_build_id\x18 \x01(\tB\x02\x18\x01\x12I\n\x13versioning_override\x18! \x01(\x0b\x32,.temporal.api.workflow.v1.VersioningOverride\x12\x33\n\'parent_pinned_worker_deployment_version\x18" \x01(\tB\x02\x18\x01\x12\x32\n\x08priority\x18# \x01(\x0b\x32 .temporal.api.common.v1.Priority\x12U\n\x18inherited_pinned_version\x18% \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12 \n\x18\x65\x61ger_execution_accepted\x18& \x01(\x08J\x04\x08$\x10%R parent_pinned_deployment_version"\xa5\x01\n)WorkflowExecutionCompletedEventAttributes\x12\x30\n\x06result\x18\x01 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12(\n workflow_task_completed_event_id\x18\x02 \x01(\x03\x12\x1c\n\x14new_execution_run_id\x18\x03 \x01(\t"\xdb\x01\n&WorkflowExecutionFailedEventAttributes\x12\x31\n\x07\x66\x61ilure\x18\x01 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x36\n\x0bretry_state\x18\x02 \x01(\x0e\x32!.temporal.api.enums.v1.RetryState\x12(\n workflow_task_completed_event_id\x18\x03 \x01(\x03\x12\x1c\n\x14new_execution_run_id\x18\x04 \x01(\t"\x80\x01\n(WorkflowExecutionTimedOutEventAttributes\x12\x36\n\x0bretry_state\x18\x01 \x01(\x0e\x32!.temporal.api.enums.v1.RetryState\x12\x1c\n\x14new_execution_run_id\x18\x02 \x01(\t"\xc8\x06\n.WorkflowExecutionContinuedAsNewEventAttributes\x12\x1c\n\x14new_execution_run_id\x18\x01 \x01(\t\x12;\n\rworkflow_type\x18\x02 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12\x38\n\ntask_queue\x18\x03 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12/\n\x05input\x18\x04 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x37\n\x14workflow_run_timeout\x18\x05 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12(\n workflow_task_completed_event_id\x18\x07 \x01(\x03\x12\x39\n\x16\x62\x61\x63koff_start_interval\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12@\n\tinitiator\x18\t \x01(\x0e\x32-.temporal.api.enums.v1.ContinueAsNewInitiator\x12\x35\n\x07\x66\x61ilure\x18\n \x01(\x0b\x32 .temporal.api.failure.v1.FailureB\x02\x18\x01\x12@\n\x16last_completion_result\x18\x0b \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12.\n\x06header\x18\x0c \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12*\n\x04memo\x18\r \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo\x12\x43\n\x11search_attributes\x18\x0e \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12\x1c\n\x10inherit_build_id\x18\x0f \x01(\x08\x42\x02\x18\x01"\xac\x01\n$WorkflowTaskScheduledEventAttributes\x12\x38\n\ntask_queue\x18\x01 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12\x39\n\x16start_to_close_timeout\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x0f\n\x07\x61ttempt\x18\x03 \x01(\x05"\x92\x02\n"WorkflowTaskStartedEventAttributes\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03\x12\x10\n\x08identity\x18\x02 \x01(\t\x12\x12\n\nrequest_id\x18\x03 \x01(\t\x12\x1f\n\x17suggest_continue_as_new\x18\x04 \x01(\x08\x12\x1a\n\x12history_size_bytes\x18\x05 \x01(\x03\x12\x46\n\x0eworker_version\x18\x06 \x01(\x0b\x32*.temporal.api.common.v1.WorkerVersionStampB\x02\x18\x01\x12%\n\x19\x62uild_id_redirect_counter\x18\x07 \x01(\x03\x42\x02\x18\x01"\x82\x05\n$WorkflowTaskCompletedEventAttributes\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03\x12\x18\n\x10started_event_id\x18\x02 \x01(\x03\x12\x10\n\x08identity\x18\x03 \x01(\t\x12\x1b\n\x0f\x62inary_checksum\x18\x04 \x01(\tB\x02\x18\x01\x12\x46\n\x0eworker_version\x18\x05 \x01(\x0b\x32*.temporal.api.common.v1.WorkerVersionStampB\x02\x18\x01\x12H\n\x0csdk_metadata\x18\x06 \x01(\x0b\x32\x32.temporal.api.sdk.v1.WorkflowTaskCompletedMetadata\x12\x43\n\x11metering_metadata\x18\r \x01(\x0b\x32(.temporal.api.common.v1.MeteringMetadata\x12>\n\ndeployment\x18\x07 \x01(\x0b\x32&.temporal.api.deployment.v1.DeploymentB\x02\x18\x01\x12\x46\n\x13versioning_behavior\x18\x08 \x01(\x0e\x32).temporal.api.enums.v1.VersioningBehavior\x12%\n\x19worker_deployment_version\x18\t \x01(\tB\x02\x18\x01\x12\x1e\n\x16worker_deployment_name\x18\n \x01(\t\x12O\n\x12\x64\x65ployment_version\x18\x0b \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion"\x95\x01\n#WorkflowTaskTimedOutEventAttributes\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03\x12\x18\n\x10started_event_id\x18\x02 \x01(\x03\x12\x38\n\x0ctimeout_type\x18\x03 \x01(\x0e\x32".temporal.api.enums.v1.TimeoutType"\x87\x03\n!WorkflowTaskFailedEventAttributes\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03\x12\x18\n\x10started_event_id\x18\x02 \x01(\x03\x12=\n\x05\x63\x61use\x18\x03 \x01(\x0e\x32..temporal.api.enums.v1.WorkflowTaskFailedCause\x12\x31\n\x07\x66\x61ilure\x18\x04 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x10\n\x08identity\x18\x05 \x01(\t\x12\x13\n\x0b\x62\x61se_run_id\x18\x06 \x01(\t\x12\x12\n\nnew_run_id\x18\x07 \x01(\t\x12\x1a\n\x12\x66ork_event_version\x18\x08 \x01(\x03\x12\x1b\n\x0f\x62inary_checksum\x18\t \x01(\tB\x02\x18\x01\x12\x46\n\x0eworker_version\x18\n \x01(\x0b\x32*.temporal.api.common.v1.WorkerVersionStampB\x02\x18\x01"\xc2\x05\n$ActivityTaskScheduledEventAttributes\x12\x13\n\x0b\x61\x63tivity_id\x18\x01 \x01(\t\x12;\n\ractivity_type\x18\x02 \x01(\x0b\x32$.temporal.api.common.v1.ActivityType\x12\x38\n\ntask_queue\x18\x04 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12.\n\x06header\x18\x05 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12/\n\x05input\x18\x06 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12<\n\x19schedule_to_close_timeout\x18\x07 \x01(\x0b\x32\x19.google.protobuf.Duration\x12<\n\x19schedule_to_start_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x34\n\x11heartbeat_timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.Duration\x12(\n workflow_task_completed_event_id\x18\x0b \x01(\x03\x12\x39\n\x0cretry_policy\x18\x0c \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12!\n\x15use_workflow_build_id\x18\r \x01(\x08\x42\x02\x18\x01\x12\x32\n\x08priority\x18\x0e \x01(\x0b\x32 .temporal.api.common.v1.PriorityJ\x04\x08\x03\x10\x04"\x9e\x02\n"ActivityTaskStartedEventAttributes\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03\x12\x10\n\x08identity\x18\x02 \x01(\t\x12\x12\n\nrequest_id\x18\x03 \x01(\t\x12\x0f\n\x07\x61ttempt\x18\x04 \x01(\x05\x12\x36\n\x0clast_failure\x18\x05 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x46\n\x0eworker_version\x18\x06 \x01(\x0b\x32*.temporal.api.common.v1.WorkerVersionStampB\x02\x18\x01\x12%\n\x19\x62uild_id_redirect_counter\x18\x07 \x01(\x03\x42\x02\x18\x01"\xe8\x01\n$ActivityTaskCompletedEventAttributes\x12\x30\n\x06result\x18\x01 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x1a\n\x12scheduled_event_id\x18\x02 \x01(\x03\x12\x18\n\x10started_event_id\x18\x03 \x01(\x03\x12\x10\n\x08identity\x18\x04 \x01(\t\x12\x46\n\x0eworker_version\x18\x05 \x01(\x0b\x32*.temporal.api.common.v1.WorkerVersionStampB\x02\x18\x01"\x9e\x02\n!ActivityTaskFailedEventAttributes\x12\x31\n\x07\x66\x61ilure\x18\x01 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x1a\n\x12scheduled_event_id\x18\x02 \x01(\x03\x12\x18\n\x10started_event_id\x18\x03 \x01(\x03\x12\x10\n\x08identity\x18\x04 \x01(\t\x12\x36\n\x0bretry_state\x18\x05 \x01(\x0e\x32!.temporal.api.enums.v1.RetryState\x12\x46\n\x0eworker_version\x18\x06 \x01(\x0b\x32*.temporal.api.common.v1.WorkerVersionStampB\x02\x18\x01"\xc6\x01\n#ActivityTaskTimedOutEventAttributes\x12\x31\n\x07\x66\x61ilure\x18\x01 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x1a\n\x12scheduled_event_id\x18\x02 \x01(\x03\x12\x18\n\x10started_event_id\x18\x03 \x01(\x03\x12\x36\n\x0bretry_state\x18\x04 \x01(\x0e\x32!.temporal.api.enums.v1.RetryState"r\n*ActivityTaskCancelRequestedEventAttributes\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03\x12(\n workflow_task_completed_event_id\x18\x02 \x01(\x03"\x92\x02\n#ActivityTaskCanceledEventAttributes\x12\x31\n\x07\x64\x65tails\x18\x01 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12(\n latest_cancel_requested_event_id\x18\x02 \x01(\x03\x12\x1a\n\x12scheduled_event_id\x18\x03 \x01(\x03\x12\x18\n\x10started_event_id\x18\x04 \x01(\x03\x12\x10\n\x08identity\x18\x05 \x01(\t\x12\x46\n\x0eworker_version\x18\x06 \x01(\x0b\x32*.temporal.api.common.v1.WorkerVersionStampB\x02\x18\x01"\x93\x01\n\x1bTimerStartedEventAttributes\x12\x10\n\x08timer_id\x18\x01 \x01(\t\x12\x38\n\x15start_to_fire_timeout\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration\x12(\n workflow_task_completed_event_id\x18\x03 \x01(\x03"G\n\x19TimerFiredEventAttributes\x12\x10\n\x08timer_id\x18\x01 \x01(\t\x12\x18\n\x10started_event_id\x18\x02 \x01(\x03"\x86\x01\n\x1cTimerCanceledEventAttributes\x12\x10\n\x08timer_id\x18\x01 \x01(\t\x12\x18\n\x10started_event_id\x18\x02 \x01(\x03\x12(\n workflow_task_completed_event_id\x18\x03 \x01(\x03\x12\x10\n\x08identity\x18\x04 \x01(\t"\xc7\x01\n/WorkflowExecutionCancelRequestedEventAttributes\x12\r\n\x05\x63\x61use\x18\x01 \x01(\t\x12#\n\x1b\x65xternal_initiated_event_id\x18\x02 \x01(\x03\x12N\n\x1b\x65xternal_workflow_execution\x18\x03 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x10\n\x08identity\x18\x04 \x01(\t"\x87\x01\n(WorkflowExecutionCanceledEventAttributes\x12(\n workflow_task_completed_event_id\x18\x01 \x01(\x03\x12\x31\n\x07\x64\x65tails\x18\x02 \x01(\x0b\x32 .temporal.api.common.v1.Payloads"\xe9\x02\n\x1dMarkerRecordedEventAttributes\x12\x13\n\x0bmarker_name\x18\x01 \x01(\t\x12T\n\x07\x64\x65tails\x18\x02 \x03(\x0b\x32\x43.temporal.api.history.v1.MarkerRecordedEventAttributes.DetailsEntry\x12(\n workflow_task_completed_event_id\x18\x03 \x01(\x03\x12.\n\x06header\x18\x04 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12\x31\n\x07\x66\x61ilure\x18\x05 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x1aP\n\x0c\x44\x65tailsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12/\n\x05value\x18\x02 \x01(\x0b\x32 .temporal.api.common.v1.Payloads:\x02\x38\x01"\xab\x02\n(WorkflowExecutionSignaledEventAttributes\x12\x13\n\x0bsignal_name\x18\x01 \x01(\t\x12/\n\x05input\x18\x02 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x10\n\x08identity\x18\x03 \x01(\t\x12.\n\x06header\x18\x04 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12\'\n\x1bskip_generate_workflow_task\x18\x05 \x01(\x08\x42\x02\x18\x01\x12N\n\x1b\x65xternal_workflow_execution\x18\x06 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution"\x81\x01\n*WorkflowExecutionTerminatedEventAttributes\x12\x0e\n\x06reason\x18\x01 \x01(\t\x12\x31\n\x07\x64\x65tails\x18\x02 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x10\n\x08identity\x18\x03 \x01(\t"\x9c\x02\n>RequestCancelExternalWorkflowExecutionInitiatedEventAttributes\x12(\n workflow_task_completed_event_id\x18\x01 \x01(\x03\x12\x11\n\tnamespace\x18\x02 \x01(\t\x12\x14\n\x0cnamespace_id\x18\x07 \x01(\t\x12\x45\n\x12workflow_execution\x18\x03 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x13\n\x07\x63ontrol\x18\x04 \x01(\tB\x02\x18\x01\x12\x1b\n\x13\x63hild_workflow_only\x18\x05 \x01(\x08\x12\x0e\n\x06reason\x18\x06 \x01(\t"\xda\x02\n;RequestCancelExternalWorkflowExecutionFailedEventAttributes\x12P\n\x05\x63\x61use\x18\x01 \x01(\x0e\x32\x41.temporal.api.enums.v1.CancelExternalWorkflowExecutionFailedCause\x12(\n workflow_task_completed_event_id\x18\x02 \x01(\x03\x12\x11\n\tnamespace\x18\x03 \x01(\t\x12\x14\n\x0cnamespace_id\x18\x07 \x01(\t\x12\x45\n\x12workflow_execution\x18\x04 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x1a\n\x12initiated_event_id\x18\x05 \x01(\x03\x12\x13\n\x07\x63ontrol\x18\x06 \x01(\tB\x02\x18\x01"\xc5\x01\n7ExternalWorkflowExecutionCancelRequestedEventAttributes\x12\x1a\n\x12initiated_event_id\x18\x01 \x01(\x03\x12\x11\n\tnamespace\x18\x02 \x01(\t\x12\x14\n\x0cnamespace_id\x18\x04 \x01(\t\x12\x45\n\x12workflow_execution\x18\x03 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution"\xfb\x02\n7SignalExternalWorkflowExecutionInitiatedEventAttributes\x12(\n workflow_task_completed_event_id\x18\x01 \x01(\x03\x12\x11\n\tnamespace\x18\x02 \x01(\t\x12\x14\n\x0cnamespace_id\x18\t \x01(\t\x12\x45\n\x12workflow_execution\x18\x03 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x13\n\x0bsignal_name\x18\x04 \x01(\t\x12/\n\x05input\x18\x05 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x13\n\x07\x63ontrol\x18\x06 \x01(\tB\x02\x18\x01\x12\x1b\n\x13\x63hild_workflow_only\x18\x07 \x01(\x08\x12.\n\x06header\x18\x08 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header"\xd3\x02\n4SignalExternalWorkflowExecutionFailedEventAttributes\x12P\n\x05\x63\x61use\x18\x01 \x01(\x0e\x32\x41.temporal.api.enums.v1.SignalExternalWorkflowExecutionFailedCause\x12(\n workflow_task_completed_event_id\x18\x02 \x01(\x03\x12\x11\n\tnamespace\x18\x03 \x01(\t\x12\x14\n\x0cnamespace_id\x18\x07 \x01(\t\x12\x45\n\x12workflow_execution\x18\x04 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x1a\n\x12initiated_event_id\x18\x05 \x01(\x03\x12\x13\n\x07\x63ontrol\x18\x06 \x01(\tB\x02\x18\x01"\xd3\x01\n0ExternalWorkflowExecutionSignaledEventAttributes\x12\x1a\n\x12initiated_event_id\x18\x01 \x01(\x03\x12\x11\n\tnamespace\x18\x02 \x01(\t\x12\x14\n\x0cnamespace_id\x18\x05 \x01(\t\x12\x45\n\x12workflow_execution\x18\x03 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x13\n\x07\x63ontrol\x18\x04 \x01(\tB\x02\x18\x01"\x9e\x01\n-UpsertWorkflowSearchAttributesEventAttributes\x12(\n workflow_task_completed_event_id\x18\x01 \x01(\x03\x12\x43\n\x11search_attributes\x18\x02 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes"\x8a\x01\n)WorkflowPropertiesModifiedEventAttributes\x12(\n workflow_task_completed_event_id\x18\x01 \x01(\x03\x12\x33\n\rupserted_memo\x18\x02 \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo"\xe8\x07\n3StartChildWorkflowExecutionInitiatedEventAttributes\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x14\n\x0cnamespace_id\x18\x12 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12;\n\rworkflow_type\x18\x03 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12\x38\n\ntask_queue\x18\x04 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12/\n\x05input\x18\x05 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12=\n\x1aworkflow_execution_timeout\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x37\n\x14workflow_run_timeout\x18\x07 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x45\n\x13parent_close_policy\x18\t \x01(\x0e\x32(.temporal.api.enums.v1.ParentClosePolicy\x12\x13\n\x07\x63ontrol\x18\n \x01(\tB\x02\x18\x01\x12(\n workflow_task_completed_event_id\x18\x0b \x01(\x03\x12N\n\x18workflow_id_reuse_policy\x18\x0c \x01(\x0e\x32,.temporal.api.enums.v1.WorkflowIdReusePolicy\x12\x39\n\x0cretry_policy\x18\r \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x15\n\rcron_schedule\x18\x0e \x01(\t\x12.\n\x06header\x18\x0f \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12*\n\x04memo\x18\x10 \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo\x12\x43\n\x11search_attributes\x18\x11 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12\x1c\n\x10inherit_build_id\x18\x13 \x01(\x08\x42\x02\x18\x01\x12\x32\n\x08priority\x18\x14 \x01(\x0b\x32 .temporal.api.common.v1.Priority"\xd6\x02\n0StartChildWorkflowExecutionFailedEventAttributes\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x14\n\x0cnamespace_id\x18\x08 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12;\n\rworkflow_type\x18\x03 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12L\n\x05\x63\x61use\x18\x04 \x01(\x0e\x32=.temporal.api.enums.v1.StartChildWorkflowExecutionFailedCause\x12\x13\n\x07\x63ontrol\x18\x05 \x01(\tB\x02\x18\x01\x12\x1a\n\x12initiated_event_id\x18\x06 \x01(\x03\x12(\n workflow_task_completed_event_id\x18\x07 \x01(\x03"\xa7\x02\n,ChildWorkflowExecutionStartedEventAttributes\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x14\n\x0cnamespace_id\x18\x06 \x01(\t\x12\x1a\n\x12initiated_event_id\x18\x02 \x01(\x03\x12\x45\n\x12workflow_execution\x18\x03 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12;\n\rworkflow_type\x18\x04 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12.\n\x06header\x18\x05 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header"\xc5\x02\n.ChildWorkflowExecutionCompletedEventAttributes\x12\x30\n\x06result\x18\x01 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x11\n\tnamespace\x18\x02 \x01(\t\x12\x14\n\x0cnamespace_id\x18\x07 \x01(\t\x12\x45\n\x12workflow_execution\x18\x03 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12;\n\rworkflow_type\x18\x04 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12\x1a\n\x12initiated_event_id\x18\x05 \x01(\x03\x12\x18\n\x10started_event_id\x18\x06 \x01(\x03"\xfb\x02\n+ChildWorkflowExecutionFailedEventAttributes\x12\x31\n\x07\x66\x61ilure\x18\x01 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x11\n\tnamespace\x18\x02 \x01(\t\x12\x14\n\x0cnamespace_id\x18\x08 \x01(\t\x12\x45\n\x12workflow_execution\x18\x03 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12;\n\rworkflow_type\x18\x04 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12\x1a\n\x12initiated_event_id\x18\x05 \x01(\x03\x12\x18\n\x10started_event_id\x18\x06 \x01(\x03\x12\x36\n\x0bretry_state\x18\x07 \x01(\x0e\x32!.temporal.api.enums.v1.RetryState"\xc5\x02\n-ChildWorkflowExecutionCanceledEventAttributes\x12\x31\n\x07\x64\x65tails\x18\x01 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x11\n\tnamespace\x18\x02 \x01(\t\x12\x14\n\x0cnamespace_id\x18\x07 \x01(\t\x12\x45\n\x12workflow_execution\x18\x03 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12;\n\rworkflow_type\x18\x04 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12\x1a\n\x12initiated_event_id\x18\x05 \x01(\x03\x12\x18\n\x10started_event_id\x18\x06 \x01(\x03"\xca\x02\n-ChildWorkflowExecutionTimedOutEventAttributes\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x14\n\x0cnamespace_id\x18\x07 \x01(\t\x12\x45\n\x12workflow_execution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12;\n\rworkflow_type\x18\x03 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12\x1a\n\x12initiated_event_id\x18\x04 \x01(\x03\x12\x18\n\x10started_event_id\x18\x05 \x01(\x03\x12\x36\n\x0bretry_state\x18\x06 \x01(\x0e\x32!.temporal.api.enums.v1.RetryState"\x94\x02\n/ChildWorkflowExecutionTerminatedEventAttributes\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x14\n\x0cnamespace_id\x18\x06 \x01(\t\x12\x45\n\x12workflow_execution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12;\n\rworkflow_type\x18\x03 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12\x1a\n\x12initiated_event_id\x18\x04 \x01(\x03\x12\x18\n\x10started_event_id\x18\x05 \x01(\x03"\x84\x02\n.WorkflowExecutionOptionsUpdatedEventAttributes\x12I\n\x13versioning_override\x18\x01 \x01(\x0b\x32,.temporal.api.workflow.v1.VersioningOverride\x12!\n\x19unset_versioning_override\x18\x02 \x01(\x08\x12\x1b\n\x13\x61ttached_request_id\x18\x03 \x01(\t\x12G\n\x1d\x61ttached_completion_callbacks\x18\x04 \x03(\x0b\x32 .temporal.api.common.v1.Callback"\xc0\x02\n3WorkflowPropertiesModifiedExternallyEventAttributes\x12\x16\n\x0enew_task_queue\x18\x01 \x01(\t\x12<\n\x19new_workflow_task_timeout\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration\x12;\n\x18new_workflow_run_timeout\x18\x03 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x41\n\x1enew_workflow_execution_timeout\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x33\n\rupserted_memo\x18\x05 \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo"\x90\x01\n3ActivityPropertiesModifiedExternallyEventAttributes\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03\x12=\n\x10new_retry_policy\x18\x02 \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy"\xdc\x01\n.WorkflowExecutionUpdateAcceptedEventAttributes\x12\x1c\n\x14protocol_instance_id\x18\x01 \x01(\t\x12#\n\x1b\x61\x63\x63\x65pted_request_message_id\x18\x02 \x01(\t\x12,\n$accepted_request_sequencing_event_id\x18\x03 \x01(\x03\x12\x39\n\x10\x61\x63\x63\x65pted_request\x18\x04 \x01(\x0b\x32\x1f.temporal.api.update.v1.Request"\xaa\x01\n/WorkflowExecutionUpdateCompletedEventAttributes\x12*\n\x04meta\x18\x01 \x01(\x0b\x32\x1c.temporal.api.update.v1.Meta\x12\x19\n\x11\x61\x63\x63\x65pted_event_id\x18\x03 \x01(\x03\x12\x30\n\x07outcome\x18\x02 \x01(\x0b\x32\x1f.temporal.api.update.v1.Outcome"\x8f\x02\n.WorkflowExecutionUpdateRejectedEventAttributes\x12\x1c\n\x14protocol_instance_id\x18\x01 \x01(\t\x12#\n\x1brejected_request_message_id\x18\x02 \x01(\t\x12,\n$rejected_request_sequencing_event_id\x18\x03 \x01(\x03\x12\x39\n\x10rejected_request\x18\x04 \x01(\x0b\x32\x1f.temporal.api.update.v1.Request\x12\x31\n\x07\x66\x61ilure\x18\x05 \x01(\x0b\x32 .temporal.api.failure.v1.Failure"\xa4\x01\n.WorkflowExecutionUpdateAdmittedEventAttributes\x12\x30\n\x07request\x18\x01 \x01(\x0b\x32\x1f.temporal.api.update.v1.Request\x12@\n\x06origin\x18\x02 \x01(\x0e\x32\x30.temporal.api.enums.v1.UpdateAdmittedEventOrigin"\xbb\x03\n&NexusOperationScheduledEventAttributes\x12\x10\n\x08\x65ndpoint\x18\x01 \x01(\t\x12\x0f\n\x07service\x18\x02 \x01(\t\x12\x11\n\toperation\x18\x03 \x01(\t\x12.\n\x05input\x18\x04 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12<\n\x19schedule_to_close_timeout\x18\x05 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x66\n\x0cnexus_header\x18\x06 \x03(\x0b\x32P.temporal.api.history.v1.NexusOperationScheduledEventAttributes.NexusHeaderEntry\x12(\n workflow_task_completed_event_id\x18\x07 \x01(\x03\x12\x12\n\nrequest_id\x18\x08 \x01(\t\x12\x13\n\x0b\x65ndpoint_id\x18\t \x01(\t\x1a\x32\n\x10NexusHeaderEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"\x89\x01\n$NexusOperationStartedEventAttributes\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03\x12\x18\n\x0coperation_id\x18\x03 \x01(\tB\x02\x18\x01\x12\x12\n\nrequest_id\x18\x04 \x01(\t\x12\x17\n\x0foperation_token\x18\x05 \x01(\t"\x89\x01\n&NexusOperationCompletedEventAttributes\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03\x12/\n\x06result\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x12\n\nrequest_id\x18\x03 \x01(\t"\x88\x01\n#NexusOperationFailedEventAttributes\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03\x12\x31\n\x07\x66\x61ilure\x18\x02 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x12\n\nrequest_id\x18\x03 \x01(\t"\x8a\x01\n%NexusOperationTimedOutEventAttributes\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03\x12\x31\n\x07\x66\x61ilure\x18\x02 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x12\n\nrequest_id\x18\x03 \x01(\t"\x8a\x01\n%NexusOperationCanceledEventAttributes\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03\x12\x31\n\x07\x66\x61ilure\x18\x02 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x12\n\nrequest_id\x18\x03 \x01(\t"t\n,NexusOperationCancelRequestedEventAttributes\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03\x12(\n workflow_task_completed_event_id\x18\x02 \x01(\x03"\x97\x01\n3NexusOperationCancelRequestCompletedEventAttributes\x12\x1a\n\x12requested_event_id\x18\x01 \x01(\x03\x12(\n workflow_task_completed_event_id\x18\x02 \x01(\x03\x12\x1a\n\x12scheduled_event_id\x18\x03 \x01(\x03"\xc7\x01\n0NexusOperationCancelRequestFailedEventAttributes\x12\x1a\n\x12requested_event_id\x18\x01 \x01(\x03\x12(\n workflow_task_completed_event_id\x18\x02 \x01(\x03\x12\x31\n\x07\x66\x61ilure\x18\x03 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x1a\n\x12scheduled_event_id\x18\x04 \x01(\x03"\xbe;\n\x0cHistoryEvent\x12\x10\n\x08\x65vent_id\x18\x01 \x01(\x03\x12.\n\nevent_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x34\n\nevent_type\x18\x03 \x01(\x0e\x32 .temporal.api.enums.v1.EventType\x12\x0f\n\x07version\x18\x04 \x01(\x03\x12\x0f\n\x07task_id\x18\x05 \x01(\x03\x12\x1a\n\x11worker_may_ignore\x18\xac\x02 \x01(\x08\x12\x39\n\ruser_metadata\x18\xad\x02 \x01(\x0b\x32!.temporal.api.sdk.v1.UserMetadata\x12,\n\x05links\x18\xae\x02 \x03(\x0b\x32\x1c.temporal.api.common.v1.Link\x12w\n+workflow_execution_started_event_attributes\x18\x06 \x01(\x0b\x32@.temporal.api.history.v1.WorkflowExecutionStartedEventAttributesH\x00\x12{\n-workflow_execution_completed_event_attributes\x18\x07 \x01(\x0b\x32\x42.temporal.api.history.v1.WorkflowExecutionCompletedEventAttributesH\x00\x12u\n*workflow_execution_failed_event_attributes\x18\x08 \x01(\x0b\x32?.temporal.api.history.v1.WorkflowExecutionFailedEventAttributesH\x00\x12z\n-workflow_execution_timed_out_event_attributes\x18\t \x01(\x0b\x32\x41.temporal.api.history.v1.WorkflowExecutionTimedOutEventAttributesH\x00\x12q\n(workflow_task_scheduled_event_attributes\x18\n \x01(\x0b\x32=.temporal.api.history.v1.WorkflowTaskScheduledEventAttributesH\x00\x12m\n&workflow_task_started_event_attributes\x18\x0b \x01(\x0b\x32;.temporal.api.history.v1.WorkflowTaskStartedEventAttributesH\x00\x12q\n(workflow_task_completed_event_attributes\x18\x0c \x01(\x0b\x32=.temporal.api.history.v1.WorkflowTaskCompletedEventAttributesH\x00\x12p\n(workflow_task_timed_out_event_attributes\x18\r \x01(\x0b\x32<.temporal.api.history.v1.WorkflowTaskTimedOutEventAttributesH\x00\x12k\n%workflow_task_failed_event_attributes\x18\x0e \x01(\x0b\x32:.temporal.api.history.v1.WorkflowTaskFailedEventAttributesH\x00\x12q\n(activity_task_scheduled_event_attributes\x18\x0f \x01(\x0b\x32=.temporal.api.history.v1.ActivityTaskScheduledEventAttributesH\x00\x12m\n&activity_task_started_event_attributes\x18\x10 \x01(\x0b\x32;.temporal.api.history.v1.ActivityTaskStartedEventAttributesH\x00\x12q\n(activity_task_completed_event_attributes\x18\x11 \x01(\x0b\x32=.temporal.api.history.v1.ActivityTaskCompletedEventAttributesH\x00\x12k\n%activity_task_failed_event_attributes\x18\x12 \x01(\x0b\x32:.temporal.api.history.v1.ActivityTaskFailedEventAttributesH\x00\x12p\n(activity_task_timed_out_event_attributes\x18\x13 \x01(\x0b\x32<.temporal.api.history.v1.ActivityTaskTimedOutEventAttributesH\x00\x12^\n\x1etimer_started_event_attributes\x18\x14 \x01(\x0b\x32\x34.temporal.api.history.v1.TimerStartedEventAttributesH\x00\x12Z\n\x1ctimer_fired_event_attributes\x18\x15 \x01(\x0b\x32\x32.temporal.api.history.v1.TimerFiredEventAttributesH\x00\x12~\n/activity_task_cancel_requested_event_attributes\x18\x16 \x01(\x0b\x32\x43.temporal.api.history.v1.ActivityTaskCancelRequestedEventAttributesH\x00\x12o\n\'activity_task_canceled_event_attributes\x18\x17 \x01(\x0b\x32<.temporal.api.history.v1.ActivityTaskCanceledEventAttributesH\x00\x12`\n\x1ftimer_canceled_event_attributes\x18\x18 \x01(\x0b\x32\x35.temporal.api.history.v1.TimerCanceledEventAttributesH\x00\x12\x62\n marker_recorded_event_attributes\x18\x19 \x01(\x0b\x32\x36.temporal.api.history.v1.MarkerRecordedEventAttributesH\x00\x12y\n,workflow_execution_signaled_event_attributes\x18\x1a \x01(\x0b\x32\x41.temporal.api.history.v1.WorkflowExecutionSignaledEventAttributesH\x00\x12}\n.workflow_execution_terminated_event_attributes\x18\x1b \x01(\x0b\x32\x43.temporal.api.history.v1.WorkflowExecutionTerminatedEventAttributesH\x00\x12\x88\x01\n4workflow_execution_cancel_requested_event_attributes\x18\x1c \x01(\x0b\x32H.temporal.api.history.v1.WorkflowExecutionCancelRequestedEventAttributesH\x00\x12y\n,workflow_execution_canceled_event_attributes\x18\x1d \x01(\x0b\x32\x41.temporal.api.history.v1.WorkflowExecutionCanceledEventAttributesH\x00\x12\xa8\x01\nErequest_cancel_external_workflow_execution_initiated_event_attributes\x18\x1e \x01(\x0b\x32W.temporal.api.history.v1.RequestCancelExternalWorkflowExecutionInitiatedEventAttributesH\x00\x12\xa2\x01\nBrequest_cancel_external_workflow_execution_failed_event_attributes\x18\x1f \x01(\x0b\x32T.temporal.api.history.v1.RequestCancelExternalWorkflowExecutionFailedEventAttributesH\x00\x12\x99\x01\n=external_workflow_execution_cancel_requested_event_attributes\x18 \x01(\x0b\x32P.temporal.api.history.v1.ExternalWorkflowExecutionCancelRequestedEventAttributesH\x00\x12\x87\x01\n4workflow_execution_continued_as_new_event_attributes\x18! \x01(\x0b\x32G.temporal.api.history.v1.WorkflowExecutionContinuedAsNewEventAttributesH\x00\x12\x91\x01\n9start_child_workflow_execution_initiated_event_attributes\x18" \x01(\x0b\x32L.temporal.api.history.v1.StartChildWorkflowExecutionInitiatedEventAttributesH\x00\x12\x8b\x01\n6start_child_workflow_execution_failed_event_attributes\x18# \x01(\x0b\x32I.temporal.api.history.v1.StartChildWorkflowExecutionFailedEventAttributesH\x00\x12\x82\x01\n1child_workflow_execution_started_event_attributes\x18$ \x01(\x0b\x32\x45.temporal.api.history.v1.ChildWorkflowExecutionStartedEventAttributesH\x00\x12\x86\x01\n3child_workflow_execution_completed_event_attributes\x18% \x01(\x0b\x32G.temporal.api.history.v1.ChildWorkflowExecutionCompletedEventAttributesH\x00\x12\x80\x01\n0child_workflow_execution_failed_event_attributes\x18& \x01(\x0b\x32\x44.temporal.api.history.v1.ChildWorkflowExecutionFailedEventAttributesH\x00\x12\x84\x01\n2child_workflow_execution_canceled_event_attributes\x18\' \x01(\x0b\x32\x46.temporal.api.history.v1.ChildWorkflowExecutionCanceledEventAttributesH\x00\x12\x85\x01\n3child_workflow_execution_timed_out_event_attributes\x18( \x01(\x0b\x32\x46.temporal.api.history.v1.ChildWorkflowExecutionTimedOutEventAttributesH\x00\x12\x88\x01\n4child_workflow_execution_terminated_event_attributes\x18) \x01(\x0b\x32H.temporal.api.history.v1.ChildWorkflowExecutionTerminatedEventAttributesH\x00\x12\x99\x01\n=signal_external_workflow_execution_initiated_event_attributes\x18* \x01(\x0b\x32P.temporal.api.history.v1.SignalExternalWorkflowExecutionInitiatedEventAttributesH\x00\x12\x93\x01\n:signal_external_workflow_execution_failed_event_attributes\x18+ \x01(\x0b\x32M.temporal.api.history.v1.SignalExternalWorkflowExecutionFailedEventAttributesH\x00\x12\x8a\x01\n5external_workflow_execution_signaled_event_attributes\x18, \x01(\x0b\x32I.temporal.api.history.v1.ExternalWorkflowExecutionSignaledEventAttributesH\x00\x12\x84\x01\n2upsert_workflow_search_attributes_event_attributes\x18- \x01(\x0b\x32\x46.temporal.api.history.v1.UpsertWorkflowSearchAttributesEventAttributesH\x00\x12\x86\x01\n3workflow_execution_update_accepted_event_attributes\x18. \x01(\x0b\x32G.temporal.api.history.v1.WorkflowExecutionUpdateAcceptedEventAttributesH\x00\x12\x86\x01\n3workflow_execution_update_rejected_event_attributes\x18/ \x01(\x0b\x32G.temporal.api.history.v1.WorkflowExecutionUpdateRejectedEventAttributesH\x00\x12\x88\x01\n4workflow_execution_update_completed_event_attributes\x18\x30 \x01(\x0b\x32H.temporal.api.history.v1.WorkflowExecutionUpdateCompletedEventAttributesH\x00\x12\x90\x01\n8workflow_properties_modified_externally_event_attributes\x18\x31 \x01(\x0b\x32L.temporal.api.history.v1.WorkflowPropertiesModifiedExternallyEventAttributesH\x00\x12\x90\x01\n8activity_properties_modified_externally_event_attributes\x18\x32 \x01(\x0b\x32L.temporal.api.history.v1.ActivityPropertiesModifiedExternallyEventAttributesH\x00\x12{\n-workflow_properties_modified_event_attributes\x18\x33 \x01(\x0b\x32\x42.temporal.api.history.v1.WorkflowPropertiesModifiedEventAttributesH\x00\x12\x86\x01\n3workflow_execution_update_admitted_event_attributes\x18\x34 \x01(\x0b\x32G.temporal.api.history.v1.WorkflowExecutionUpdateAdmittedEventAttributesH\x00\x12u\n*nexus_operation_scheduled_event_attributes\x18\x35 \x01(\x0b\x32?.temporal.api.history.v1.NexusOperationScheduledEventAttributesH\x00\x12q\n(nexus_operation_started_event_attributes\x18\x36 \x01(\x0b\x32=.temporal.api.history.v1.NexusOperationStartedEventAttributesH\x00\x12u\n*nexus_operation_completed_event_attributes\x18\x37 \x01(\x0b\x32?.temporal.api.history.v1.NexusOperationCompletedEventAttributesH\x00\x12o\n\'nexus_operation_failed_event_attributes\x18\x38 \x01(\x0b\x32<.temporal.api.history.v1.NexusOperationFailedEventAttributesH\x00\x12s\n)nexus_operation_canceled_event_attributes\x18\x39 \x01(\x0b\x32>.temporal.api.history.v1.NexusOperationCanceledEventAttributesH\x00\x12t\n*nexus_operation_timed_out_event_attributes\x18: \x01(\x0b\x32>.temporal.api.history.v1.NexusOperationTimedOutEventAttributesH\x00\x12\x82\x01\n1nexus_operation_cancel_requested_event_attributes\x18; \x01(\x0b\x32\x45.temporal.api.history.v1.NexusOperationCancelRequestedEventAttributesH\x00\x12\x86\x01\n3workflow_execution_options_updated_event_attributes\x18< \x01(\x0b\x32G.temporal.api.history.v1.WorkflowExecutionOptionsUpdatedEventAttributesH\x00\x12\x91\x01\n9nexus_operation_cancel_request_completed_event_attributes\x18= \x01(\x0b\x32L.temporal.api.history.v1.NexusOperationCancelRequestCompletedEventAttributesH\x00\x12\x8b\x01\n6nexus_operation_cancel_request_failed_event_attributes\x18> \x01(\x0b\x32I.temporal.api.history.v1.NexusOperationCancelRequestFailedEventAttributesH\x00\x42\x0c\n\nattributes"@\n\x07History\x12\x35\n\x06\x65vents\x18\x01 \x03(\x0b\x32%.temporal.api.history.v1.HistoryEventB\x8e\x01\n\x1aio.temporal.api.history.v1B\x0cMessageProtoP\x01Z%go.temporal.io/api/history/v1;history\xaa\x02\x19Temporalio.Api.History.V1\xea\x02\x1cTemporalio::Api::History::V1b\x06proto3' + b'\n%temporal/api/history/v1/message.proto\x12\x17temporal.api.history.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a&temporal/api/enums/v1/event_type.proto\x1a(temporal/api/enums/v1/failed_cause.proto\x1a"temporal/api/enums/v1/update.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a$temporal/api/common/v1/message.proto\x1a(temporal/api/deployment/v1/message.proto\x1a%temporal/api/failure/v1/message.proto\x1a\'temporal/api/taskqueue/v1/message.proto\x1a$temporal/api/update/v1/message.proto\x1a&temporal/api/workflow/v1/message.proto\x1a\x30temporal/api/sdk/v1/task_complete_metadata.proto\x1a\'temporal/api/sdk/v1/user_metadata.proto\x1a,temporal/api/sdk/v1/event_group_marker.proto"\xda\x12\n\'WorkflowExecutionStartedEventAttributes\x12;\n\rworkflow_type\x18\x01 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12!\n\x19parent_workflow_namespace\x18\x02 \x01(\t\x12$\n\x1cparent_workflow_namespace_id\x18\x1b \x01(\t\x12L\n\x19parent_workflow_execution\x18\x03 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12!\n\x19parent_initiated_event_id\x18\x04 \x01(\x03\x12\x38\n\ntask_queue\x18\x05 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12/\n\x05input\x18\x06 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12=\n\x1aworkflow_execution_timeout\x18\x07 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x37\n\x14workflow_run_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12"\n\x1a\x63ontinued_execution_run_id\x18\n \x01(\t\x12@\n\tinitiator\x18\x0b \x01(\x0e\x32-.temporal.api.enums.v1.ContinueAsNewInitiator\x12;\n\x11\x63ontinued_failure\x18\x0c \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12@\n\x16last_completion_result\x18\r \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12!\n\x19original_execution_run_id\x18\x0e \x01(\t\x12\x10\n\x08identity\x18\x0f \x01(\t\x12\x1e\n\x16\x66irst_execution_run_id\x18\x10 \x01(\t\x12\x39\n\x0cretry_policy\x18\x11 \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x0f\n\x07\x61ttempt\x18\x12 \x01(\x05\x12\x46\n"workflow_execution_expiration_time\x18\x13 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x15\n\rcron_schedule\x18\x14 \x01(\t\x12>\n\x1b\x66irst_workflow_task_backoff\x18\x15 \x01(\x0b\x32\x19.google.protobuf.Duration\x12*\n\x04memo\x18\x16 \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo\x12\x43\n\x11search_attributes\x18\x17 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12\x45\n\x16prev_auto_reset_points\x18\x18 \x01(\x0b\x32%.temporal.api.workflow.v1.ResetPoints\x12.\n\x06header\x18\x19 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12&\n\x1eparent_initiated_event_version\x18\x1a \x01(\x03\x12\x13\n\x0bworkflow_id\x18\x1c \x01(\t\x12L\n\x14source_version_stamp\x18\x1d \x01(\x0b\x32*.temporal.api.common.v1.WorkerVersionStampB\x02\x18\x01\x12>\n\x14\x63ompletion_callbacks\x18\x1e \x03(\x0b\x32 .temporal.api.common.v1.Callback\x12J\n\x17root_workflow_execution\x18\x1f \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x1e\n\x12inherited_build_id\x18 \x01(\tB\x02\x18\x01\x12I\n\x13versioning_override\x18! \x01(\x0b\x32,.temporal.api.workflow.v1.VersioningOverride\x12\x33\n\'parent_pinned_worker_deployment_version\x18" \x01(\tB\x02\x18\x01\x12\x32\n\x08priority\x18# \x01(\x0b\x32 .temporal.api.common.v1.Priority\x12U\n\x18inherited_pinned_version\x18% \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12Y\n\x1binherited_auto_upgrade_info\x18\' \x01(\x0b\x32\x34.temporal.api.deployment.v1.InheritedAutoUpgradeInfo\x12 \n\x18\x65\x61ger_execution_accepted\x18& \x01(\x08\x12^\n\x1f\x64\x65\x63lined_target_version_upgrade\x18( \x01(\x0b\x32\x35.temporal.api.history.v1.DeclinedTargetVersionUpgrade\x12H\n\x14time_skipping_config\x18) \x01(\x0b\x32*.temporal.api.common.v1.TimeSkippingConfig\x12]\n\x1ftime_skipping_state_propagation\x18+ \x01(\x0b\x32\x34.temporal.api.common.v1.TimeSkippingStatePropagationJ\x04\x08$\x10%J\x04\x08*\x10+R parent_pinned_deployment_versionR\x18initial_skipped_duration"\x88\x01\n\x1c\x44\x65\x63linedTargetVersionUpgrade\x12O\n\x12\x64\x65ployment_version\x18\x01 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12\x17\n\x0frevision_number\x18\x02 \x01(\x03"\xa5\x01\n)WorkflowExecutionCompletedEventAttributes\x12\x30\n\x06result\x18\x01 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12(\n workflow_task_completed_event_id\x18\x02 \x01(\x03\x12\x1c\n\x14new_execution_run_id\x18\x03 \x01(\t"\xdb\x01\n&WorkflowExecutionFailedEventAttributes\x12\x31\n\x07\x66\x61ilure\x18\x01 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x36\n\x0bretry_state\x18\x02 \x01(\x0e\x32!.temporal.api.enums.v1.RetryState\x12(\n workflow_task_completed_event_id\x18\x03 \x01(\x03\x12\x1c\n\x14new_execution_run_id\x18\x04 \x01(\t"\x80\x01\n(WorkflowExecutionTimedOutEventAttributes\x12\x36\n\x0bretry_state\x18\x01 \x01(\x0e\x32!.temporal.api.enums.v1.RetryState\x12\x1c\n\x14new_execution_run_id\x18\x02 \x01(\t"\xa5\x07\n.WorkflowExecutionContinuedAsNewEventAttributes\x12\x1c\n\x14new_execution_run_id\x18\x01 \x01(\t\x12;\n\rworkflow_type\x18\x02 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12\x38\n\ntask_queue\x18\x03 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12/\n\x05input\x18\x04 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x37\n\x14workflow_run_timeout\x18\x05 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12(\n workflow_task_completed_event_id\x18\x07 \x01(\x03\x12\x39\n\x16\x62\x61\x63koff_start_interval\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12@\n\tinitiator\x18\t \x01(\x0e\x32-.temporal.api.enums.v1.ContinueAsNewInitiator\x12\x35\n\x07\x66\x61ilure\x18\n \x01(\x0b\x32 .temporal.api.failure.v1.FailureB\x02\x18\x01\x12@\n\x16last_completion_result\x18\x0b \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12.\n\x06header\x18\x0c \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12*\n\x04memo\x18\r \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo\x12\x43\n\x11search_attributes\x18\x0e \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12\x1c\n\x10inherit_build_id\x18\x0f \x01(\x08\x42\x02\x18\x01\x12[\n\x1binitial_versioning_behavior\x18\x10 \x01(\x0e\x32\x36.temporal.api.enums.v1.ContinueAsNewVersioningBehavior"\xac\x01\n$WorkflowTaskScheduledEventAttributes\x12\x38\n\ntask_queue\x18\x01 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12\x39\n\x16start_to_close_timeout\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x0f\n\x07\x61ttempt\x18\x03 \x01(\x05"\xa0\x03\n"WorkflowTaskStartedEventAttributes\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03\x12\x10\n\x08identity\x18\x02 \x01(\t\x12\x12\n\nrequest_id\x18\x03 \x01(\t\x12\x1f\n\x17suggest_continue_as_new\x18\x04 \x01(\x08\x12Z\n\x1fsuggest_continue_as_new_reasons\x18\x08 \x03(\x0e\x32\x31.temporal.api.enums.v1.SuggestContinueAsNewReason\x12\x30\n(target_worker_deployment_version_changed\x18\t \x01(\x08\x12\x1a\n\x12history_size_bytes\x18\x05 \x01(\x03\x12\x46\n\x0eworker_version\x18\x06 \x01(\x0b\x32*.temporal.api.common.v1.WorkerVersionStampB\x02\x18\x01\x12%\n\x19\x62uild_id_redirect_counter\x18\x07 \x01(\x03\x42\x02\x18\x01"\x82\x05\n$WorkflowTaskCompletedEventAttributes\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03\x12\x18\n\x10started_event_id\x18\x02 \x01(\x03\x12\x10\n\x08identity\x18\x03 \x01(\t\x12\x1b\n\x0f\x62inary_checksum\x18\x04 \x01(\tB\x02\x18\x01\x12\x46\n\x0eworker_version\x18\x05 \x01(\x0b\x32*.temporal.api.common.v1.WorkerVersionStampB\x02\x18\x01\x12H\n\x0csdk_metadata\x18\x06 \x01(\x0b\x32\x32.temporal.api.sdk.v1.WorkflowTaskCompletedMetadata\x12\x43\n\x11metering_metadata\x18\r \x01(\x0b\x32(.temporal.api.common.v1.MeteringMetadata\x12>\n\ndeployment\x18\x07 \x01(\x0b\x32&.temporal.api.deployment.v1.DeploymentB\x02\x18\x01\x12\x46\n\x13versioning_behavior\x18\x08 \x01(\x0e\x32).temporal.api.enums.v1.VersioningBehavior\x12%\n\x19worker_deployment_version\x18\t \x01(\tB\x02\x18\x01\x12\x1e\n\x16worker_deployment_name\x18\n \x01(\t\x12O\n\x12\x64\x65ployment_version\x18\x0b \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion"\x95\x01\n#WorkflowTaskTimedOutEventAttributes\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03\x12\x18\n\x10started_event_id\x18\x02 \x01(\x03\x12\x38\n\x0ctimeout_type\x18\x03 \x01(\x0e\x32".temporal.api.enums.v1.TimeoutType"\x87\x03\n!WorkflowTaskFailedEventAttributes\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03\x12\x18\n\x10started_event_id\x18\x02 \x01(\x03\x12=\n\x05\x63\x61use\x18\x03 \x01(\x0e\x32..temporal.api.enums.v1.WorkflowTaskFailedCause\x12\x31\n\x07\x66\x61ilure\x18\x04 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x10\n\x08identity\x18\x05 \x01(\t\x12\x13\n\x0b\x62\x61se_run_id\x18\x06 \x01(\t\x12\x12\n\nnew_run_id\x18\x07 \x01(\t\x12\x1a\n\x12\x66ork_event_version\x18\x08 \x01(\x03\x12\x1b\n\x0f\x62inary_checksum\x18\t \x01(\tB\x02\x18\x01\x12\x46\n\x0eworker_version\x18\n \x01(\x0b\x32*.temporal.api.common.v1.WorkerVersionStampB\x02\x18\x01"\xc2\x05\n$ActivityTaskScheduledEventAttributes\x12\x13\n\x0b\x61\x63tivity_id\x18\x01 \x01(\t\x12;\n\ractivity_type\x18\x02 \x01(\x0b\x32$.temporal.api.common.v1.ActivityType\x12\x38\n\ntask_queue\x18\x04 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12.\n\x06header\x18\x05 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12/\n\x05input\x18\x06 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12<\n\x19schedule_to_close_timeout\x18\x07 \x01(\x0b\x32\x19.google.protobuf.Duration\x12<\n\x19schedule_to_start_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x34\n\x11heartbeat_timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.Duration\x12(\n workflow_task_completed_event_id\x18\x0b \x01(\x03\x12\x39\n\x0cretry_policy\x18\x0c \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12!\n\x15use_workflow_build_id\x18\r \x01(\x08\x42\x02\x18\x01\x12\x32\n\x08priority\x18\x0e \x01(\x0b\x32 .temporal.api.common.v1.PriorityJ\x04\x08\x03\x10\x04"\x9e\x02\n"ActivityTaskStartedEventAttributes\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03\x12\x10\n\x08identity\x18\x02 \x01(\t\x12\x12\n\nrequest_id\x18\x03 \x01(\t\x12\x0f\n\x07\x61ttempt\x18\x04 \x01(\x05\x12\x36\n\x0clast_failure\x18\x05 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x46\n\x0eworker_version\x18\x06 \x01(\x0b\x32*.temporal.api.common.v1.WorkerVersionStampB\x02\x18\x01\x12%\n\x19\x62uild_id_redirect_counter\x18\x07 \x01(\x03\x42\x02\x18\x01"\xe8\x01\n$ActivityTaskCompletedEventAttributes\x12\x30\n\x06result\x18\x01 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x1a\n\x12scheduled_event_id\x18\x02 \x01(\x03\x12\x18\n\x10started_event_id\x18\x03 \x01(\x03\x12\x10\n\x08identity\x18\x04 \x01(\t\x12\x46\n\x0eworker_version\x18\x05 \x01(\x0b\x32*.temporal.api.common.v1.WorkerVersionStampB\x02\x18\x01"\x9e\x02\n!ActivityTaskFailedEventAttributes\x12\x31\n\x07\x66\x61ilure\x18\x01 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x1a\n\x12scheduled_event_id\x18\x02 \x01(\x03\x12\x18\n\x10started_event_id\x18\x03 \x01(\x03\x12\x10\n\x08identity\x18\x04 \x01(\t\x12\x36\n\x0bretry_state\x18\x05 \x01(\x0e\x32!.temporal.api.enums.v1.RetryState\x12\x46\n\x0eworker_version\x18\x06 \x01(\x0b\x32*.temporal.api.common.v1.WorkerVersionStampB\x02\x18\x01"\xc6\x01\n#ActivityTaskTimedOutEventAttributes\x12\x31\n\x07\x66\x61ilure\x18\x01 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x1a\n\x12scheduled_event_id\x18\x02 \x01(\x03\x12\x18\n\x10started_event_id\x18\x03 \x01(\x03\x12\x36\n\x0bretry_state\x18\x04 \x01(\x0e\x32!.temporal.api.enums.v1.RetryState"r\n*ActivityTaskCancelRequestedEventAttributes\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03\x12(\n workflow_task_completed_event_id\x18\x02 \x01(\x03"\x92\x02\n#ActivityTaskCanceledEventAttributes\x12\x31\n\x07\x64\x65tails\x18\x01 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12(\n latest_cancel_requested_event_id\x18\x02 \x01(\x03\x12\x1a\n\x12scheduled_event_id\x18\x03 \x01(\x03\x12\x18\n\x10started_event_id\x18\x04 \x01(\x03\x12\x10\n\x08identity\x18\x05 \x01(\t\x12\x46\n\x0eworker_version\x18\x06 \x01(\x0b\x32*.temporal.api.common.v1.WorkerVersionStampB\x02\x18\x01"\x93\x01\n\x1bTimerStartedEventAttributes\x12\x10\n\x08timer_id\x18\x01 \x01(\t\x12\x38\n\x15start_to_fire_timeout\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration\x12(\n workflow_task_completed_event_id\x18\x03 \x01(\x03"G\n\x19TimerFiredEventAttributes\x12\x10\n\x08timer_id\x18\x01 \x01(\t\x12\x18\n\x10started_event_id\x18\x02 \x01(\x03"\x86\x01\n\x1cTimerCanceledEventAttributes\x12\x10\n\x08timer_id\x18\x01 \x01(\t\x12\x18\n\x10started_event_id\x18\x02 \x01(\x03\x12(\n workflow_task_completed_event_id\x18\x03 \x01(\x03\x12\x10\n\x08identity\x18\x04 \x01(\t"\xc7\x01\n/WorkflowExecutionCancelRequestedEventAttributes\x12\r\n\x05\x63\x61use\x18\x01 \x01(\t\x12#\n\x1b\x65xternal_initiated_event_id\x18\x02 \x01(\x03\x12N\n\x1b\x65xternal_workflow_execution\x18\x03 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x10\n\x08identity\x18\x04 \x01(\t"\x87\x01\n(WorkflowExecutionCanceledEventAttributes\x12(\n workflow_task_completed_event_id\x18\x01 \x01(\x03\x12\x31\n\x07\x64\x65tails\x18\x02 \x01(\x0b\x32 .temporal.api.common.v1.Payloads"\xe9\x02\n\x1dMarkerRecordedEventAttributes\x12\x13\n\x0bmarker_name\x18\x01 \x01(\t\x12T\n\x07\x64\x65tails\x18\x02 \x03(\x0b\x32\x43.temporal.api.history.v1.MarkerRecordedEventAttributes.DetailsEntry\x12(\n workflow_task_completed_event_id\x18\x03 \x01(\x03\x12.\n\x06header\x18\x04 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12\x31\n\x07\x66\x61ilure\x18\x05 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x1aP\n\x0c\x44\x65tailsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12/\n\x05value\x18\x02 \x01(\x0b\x32 .temporal.api.common.v1.Payloads:\x02\x38\x01"\xbf\x02\n(WorkflowExecutionSignaledEventAttributes\x12\x13\n\x0bsignal_name\x18\x01 \x01(\t\x12/\n\x05input\x18\x02 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x10\n\x08identity\x18\x03 \x01(\t\x12.\n\x06header\x18\x04 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12\'\n\x1bskip_generate_workflow_task\x18\x05 \x01(\x08\x42\x02\x18\x01\x12N\n\x1b\x65xternal_workflow_execution\x18\x06 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x12\n\nrequest_id\x18\x07 \x01(\t"\x81\x01\n*WorkflowExecutionTerminatedEventAttributes\x12\x0e\n\x06reason\x18\x01 \x01(\t\x12\x31\n\x07\x64\x65tails\x18\x02 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x10\n\x08identity\x18\x03 \x01(\t"\x9c\x02\n>RequestCancelExternalWorkflowExecutionInitiatedEventAttributes\x12(\n workflow_task_completed_event_id\x18\x01 \x01(\x03\x12\x11\n\tnamespace\x18\x02 \x01(\t\x12\x14\n\x0cnamespace_id\x18\x07 \x01(\t\x12\x45\n\x12workflow_execution\x18\x03 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x13\n\x07\x63ontrol\x18\x04 \x01(\tB\x02\x18\x01\x12\x1b\n\x13\x63hild_workflow_only\x18\x05 \x01(\x08\x12\x0e\n\x06reason\x18\x06 \x01(\t"\xda\x02\n;RequestCancelExternalWorkflowExecutionFailedEventAttributes\x12P\n\x05\x63\x61use\x18\x01 \x01(\x0e\x32\x41.temporal.api.enums.v1.CancelExternalWorkflowExecutionFailedCause\x12(\n workflow_task_completed_event_id\x18\x02 \x01(\x03\x12\x11\n\tnamespace\x18\x03 \x01(\t\x12\x14\n\x0cnamespace_id\x18\x07 \x01(\t\x12\x45\n\x12workflow_execution\x18\x04 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x1a\n\x12initiated_event_id\x18\x05 \x01(\x03\x12\x13\n\x07\x63ontrol\x18\x06 \x01(\tB\x02\x18\x01"\xc5\x01\n7ExternalWorkflowExecutionCancelRequestedEventAttributes\x12\x1a\n\x12initiated_event_id\x18\x01 \x01(\x03\x12\x11\n\tnamespace\x18\x02 \x01(\t\x12\x14\n\x0cnamespace_id\x18\x04 \x01(\t\x12\x45\n\x12workflow_execution\x18\x03 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution"\xfb\x02\n7SignalExternalWorkflowExecutionInitiatedEventAttributes\x12(\n workflow_task_completed_event_id\x18\x01 \x01(\x03\x12\x11\n\tnamespace\x18\x02 \x01(\t\x12\x14\n\x0cnamespace_id\x18\t \x01(\t\x12\x45\n\x12workflow_execution\x18\x03 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x13\n\x0bsignal_name\x18\x04 \x01(\t\x12/\n\x05input\x18\x05 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x13\n\x07\x63ontrol\x18\x06 \x01(\tB\x02\x18\x01\x12\x1b\n\x13\x63hild_workflow_only\x18\x07 \x01(\x08\x12.\n\x06header\x18\x08 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header"\xd3\x02\n4SignalExternalWorkflowExecutionFailedEventAttributes\x12P\n\x05\x63\x61use\x18\x01 \x01(\x0e\x32\x41.temporal.api.enums.v1.SignalExternalWorkflowExecutionFailedCause\x12(\n workflow_task_completed_event_id\x18\x02 \x01(\x03\x12\x11\n\tnamespace\x18\x03 \x01(\t\x12\x14\n\x0cnamespace_id\x18\x07 \x01(\t\x12\x45\n\x12workflow_execution\x18\x04 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x1a\n\x12initiated_event_id\x18\x05 \x01(\x03\x12\x13\n\x07\x63ontrol\x18\x06 \x01(\tB\x02\x18\x01"\xd3\x01\n0ExternalWorkflowExecutionSignaledEventAttributes\x12\x1a\n\x12initiated_event_id\x18\x01 \x01(\x03\x12\x11\n\tnamespace\x18\x02 \x01(\t\x12\x14\n\x0cnamespace_id\x18\x05 \x01(\t\x12\x45\n\x12workflow_execution\x18\x03 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x13\n\x07\x63ontrol\x18\x04 \x01(\tB\x02\x18\x01"\x9e\x01\n-UpsertWorkflowSearchAttributesEventAttributes\x12(\n workflow_task_completed_event_id\x18\x01 \x01(\x03\x12\x43\n\x11search_attributes\x18\x02 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes"\x8a\x01\n)WorkflowPropertiesModifiedEventAttributes\x12(\n workflow_task_completed_event_id\x18\x01 \x01(\x03\x12\x33\n\rupserted_memo\x18\x02 \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo"\xfc\t\n3StartChildWorkflowExecutionInitiatedEventAttributes\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x14\n\x0cnamespace_id\x18\x12 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12;\n\rworkflow_type\x18\x03 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12\x38\n\ntask_queue\x18\x04 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12/\n\x05input\x18\x05 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12=\n\x1aworkflow_execution_timeout\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x37\n\x14workflow_run_timeout\x18\x07 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x45\n\x13parent_close_policy\x18\t \x01(\x0e\x32(.temporal.api.enums.v1.ParentClosePolicy\x12\x13\n\x07\x63ontrol\x18\n \x01(\tB\x02\x18\x01\x12(\n workflow_task_completed_event_id\x18\x0b \x01(\x03\x12N\n\x18workflow_id_reuse_policy\x18\x0c \x01(\x0e\x32,.temporal.api.enums.v1.WorkflowIdReusePolicy\x12\x39\n\x0cretry_policy\x18\r \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x15\n\rcron_schedule\x18\x0e \x01(\t\x12.\n\x06header\x18\x0f \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12*\n\x04memo\x18\x10 \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo\x12\x43\n\x11search_attributes\x18\x11 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12\x1c\n\x10inherit_build_id\x18\x13 \x01(\x08\x42\x02\x18\x01\x12\x32\n\x08priority\x18\x14 \x01(\x0b\x32 .temporal.api.common.v1.Priority\x12H\n\x14time_skipping_config\x18\x15 \x01(\x0b\x32*.temporal.api.common.v1.TimeSkippingConfig\x12]\n\x1ftime_skipping_state_propagation\x18\x17 \x01(\x0b\x32\x34.temporal.api.common.v1.TimeSkippingStatePropagation\x12I\n\x13versioning_override\x18\x18 \x01(\x0b\x32,.temporal.api.workflow.v1.VersioningOverrideJ\x04\x08\x16\x10\x17R\x18initial_skipped_duration"\xd6\x02\n0StartChildWorkflowExecutionFailedEventAttributes\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x14\n\x0cnamespace_id\x18\x08 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12;\n\rworkflow_type\x18\x03 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12L\n\x05\x63\x61use\x18\x04 \x01(\x0e\x32=.temporal.api.enums.v1.StartChildWorkflowExecutionFailedCause\x12\x13\n\x07\x63ontrol\x18\x05 \x01(\tB\x02\x18\x01\x12\x1a\n\x12initiated_event_id\x18\x06 \x01(\x03\x12(\n workflow_task_completed_event_id\x18\x07 \x01(\x03"\xa7\x02\n,ChildWorkflowExecutionStartedEventAttributes\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x14\n\x0cnamespace_id\x18\x06 \x01(\t\x12\x1a\n\x12initiated_event_id\x18\x02 \x01(\x03\x12\x45\n\x12workflow_execution\x18\x03 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12;\n\rworkflow_type\x18\x04 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12.\n\x06header\x18\x05 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header"\xc5\x02\n.ChildWorkflowExecutionCompletedEventAttributes\x12\x30\n\x06result\x18\x01 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x11\n\tnamespace\x18\x02 \x01(\t\x12\x14\n\x0cnamespace_id\x18\x07 \x01(\t\x12\x45\n\x12workflow_execution\x18\x03 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12;\n\rworkflow_type\x18\x04 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12\x1a\n\x12initiated_event_id\x18\x05 \x01(\x03\x12\x18\n\x10started_event_id\x18\x06 \x01(\x03"\xfb\x02\n+ChildWorkflowExecutionFailedEventAttributes\x12\x31\n\x07\x66\x61ilure\x18\x01 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x11\n\tnamespace\x18\x02 \x01(\t\x12\x14\n\x0cnamespace_id\x18\x08 \x01(\t\x12\x45\n\x12workflow_execution\x18\x03 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12;\n\rworkflow_type\x18\x04 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12\x1a\n\x12initiated_event_id\x18\x05 \x01(\x03\x12\x18\n\x10started_event_id\x18\x06 \x01(\x03\x12\x36\n\x0bretry_state\x18\x07 \x01(\x0e\x32!.temporal.api.enums.v1.RetryState"\xc5\x02\n-ChildWorkflowExecutionCanceledEventAttributes\x12\x31\n\x07\x64\x65tails\x18\x01 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x11\n\tnamespace\x18\x02 \x01(\t\x12\x14\n\x0cnamespace_id\x18\x07 \x01(\t\x12\x45\n\x12workflow_execution\x18\x03 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12;\n\rworkflow_type\x18\x04 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12\x1a\n\x12initiated_event_id\x18\x05 \x01(\x03\x12\x18\n\x10started_event_id\x18\x06 \x01(\x03"\xca\x02\n-ChildWorkflowExecutionTimedOutEventAttributes\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x14\n\x0cnamespace_id\x18\x07 \x01(\t\x12\x45\n\x12workflow_execution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12;\n\rworkflow_type\x18\x03 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12\x1a\n\x12initiated_event_id\x18\x04 \x01(\x03\x12\x18\n\x10started_event_id\x18\x05 \x01(\x03\x12\x36\n\x0bretry_state\x18\x06 \x01(\x0e\x32!.temporal.api.enums.v1.RetryState"\x94\x02\n/ChildWorkflowExecutionTerminatedEventAttributes\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x14\n\x0cnamespace_id\x18\x06 \x01(\t\x12\x45\n\x12workflow_execution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12;\n\rworkflow_type\x18\x03 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12\x1a\n\x12initiated_event_id\x18\x04 \x01(\x03\x12\x18\n\x10started_event_id\x18\x05 \x01(\x03"\xda\x05\n.WorkflowExecutionOptionsUpdatedEventAttributes\x12I\n\x13versioning_override\x18\x01 \x01(\x0b\x32,.temporal.api.workflow.v1.VersioningOverride\x12!\n\x19unset_versioning_override\x18\x02 \x01(\x08\x12\x1b\n\x13\x61ttached_request_id\x18\x03 \x01(\t\x12G\n\x1d\x61ttached_completion_callbacks\x18\x04 \x03(\x0b\x32 .temporal.api.common.v1.Callback\x12\x10\n\x08identity\x18\x05 \x01(\t\x12\x32\n\x08priority\x18\x06 \x01(\x0b\x32 .temporal.api.common.v1.Priority\x12H\n\x14time_skipping_config\x18\x07 \x01(\x0b\x32*.temporal.api.common.v1.TimeSkippingConfig\x12$\n\x1ctime_skipping_config_updated\x18\t \x01(\x08\x12\x84\x01\n\x17workflow_update_options\x18\x08 \x03(\x0b\x32\x63.temporal.api.history.v1.WorkflowExecutionOptionsUpdatedEventAttributes.WorkflowUpdateOptionsUpdate\x1a\x96\x01\n\x1bWorkflowUpdateOptionsUpdate\x12\x11\n\tupdate_id\x18\x01 \x01(\t\x12\x1b\n\x13\x61ttached_request_id\x18\x02 \x01(\t\x12G\n\x1d\x61ttached_completion_callbacks\x18\x03 \x03(\x0b\x32 .temporal.api.common.v1.Callback"\xc0\x02\n3WorkflowPropertiesModifiedExternallyEventAttributes\x12\x16\n\x0enew_task_queue\x18\x01 \x01(\t\x12<\n\x19new_workflow_task_timeout\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration\x12;\n\x18new_workflow_run_timeout\x18\x03 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x41\n\x1enew_workflow_execution_timeout\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x33\n\rupserted_memo\x18\x05 \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo"\x90\x01\n3ActivityPropertiesModifiedExternallyEventAttributes\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03\x12=\n\x10new_retry_policy\x18\x02 \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy"\xdc\x01\n.WorkflowExecutionUpdateAcceptedEventAttributes\x12\x1c\n\x14protocol_instance_id\x18\x01 \x01(\t\x12#\n\x1b\x61\x63\x63\x65pted_request_message_id\x18\x02 \x01(\t\x12,\n$accepted_request_sequencing_event_id\x18\x03 \x01(\x03\x12\x39\n\x10\x61\x63\x63\x65pted_request\x18\x04 \x01(\x0b\x32\x1f.temporal.api.update.v1.Request"\xaa\x01\n/WorkflowExecutionUpdateCompletedEventAttributes\x12*\n\x04meta\x18\x01 \x01(\x0b\x32\x1c.temporal.api.update.v1.Meta\x12\x19\n\x11\x61\x63\x63\x65pted_event_id\x18\x03 \x01(\x03\x12\x30\n\x07outcome\x18\x02 \x01(\x0b\x32\x1f.temporal.api.update.v1.Outcome"\x8f\x02\n.WorkflowExecutionUpdateRejectedEventAttributes\x12\x1c\n\x14protocol_instance_id\x18\x01 \x01(\t\x12#\n\x1brejected_request_message_id\x18\x02 \x01(\t\x12,\n$rejected_request_sequencing_event_id\x18\x03 \x01(\x03\x12\x39\n\x10rejected_request\x18\x04 \x01(\x0b\x32\x1f.temporal.api.update.v1.Request\x12\x31\n\x07\x66\x61ilure\x18\x05 \x01(\x0b\x32 .temporal.api.failure.v1.Failure"\xa4\x01\n.WorkflowExecutionUpdateAdmittedEventAttributes\x12\x30\n\x07request\x18\x01 \x01(\x0b\x32\x1f.temporal.api.update.v1.Request\x12@\n\x06origin\x18\x02 \x01(\x0e\x32\x30.temporal.api.enums.v1.UpdateAdmittedEventOrigin"^\n&WorkflowExecutionPausedEventAttributes\x12\x10\n\x08identity\x18\x01 \x01(\t\x12\x0e\n\x06reason\x18\x02 \x01(\t\x12\x12\n\nrequest_id\x18\x03 \x01(\t"`\n(WorkflowExecutionUnpausedEventAttributes\x12\x10\n\x08identity\x18\x01 \x01(\t\x12\x0e\n\x06reason\x18\x02 \x01(\t\x12\x12\n\nrequest_id\x18\x03 \x01(\t"\xc5\x01\n8WorkflowExecutionTimeSkippingTransitionedEventAttributes\x12/\n\x0btarget_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12#\n\x1b\x64isabled_after_fast_forward\x18\x02 \x01(\x08\x12\x33\n\x0fwall_clock_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"\xb4\x04\n&NexusOperationScheduledEventAttributes\x12\x10\n\x08\x65ndpoint\x18\x01 \x01(\t\x12\x0f\n\x07service\x18\x02 \x01(\t\x12\x11\n\toperation\x18\x03 \x01(\t\x12.\n\x05input\x18\x04 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12<\n\x19schedule_to_close_timeout\x18\x05 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x66\n\x0cnexus_header\x18\x06 \x03(\x0b\x32P.temporal.api.history.v1.NexusOperationScheduledEventAttributes.NexusHeaderEntry\x12(\n workflow_task_completed_event_id\x18\x07 \x01(\x03\x12\x12\n\nrequest_id\x18\x08 \x01(\t\x12\x13\n\x0b\x65ndpoint_id\x18\t \x01(\t\x12<\n\x19schedule_to_start_timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\x0b \x01(\x0b\x32\x19.google.protobuf.Duration\x1a\x32\n\x10NexusHeaderEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"\x89\x01\n$NexusOperationStartedEventAttributes\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03\x12\x18\n\x0coperation_id\x18\x03 \x01(\tB\x02\x18\x01\x12\x12\n\nrequest_id\x18\x04 \x01(\t\x12\x17\n\x0foperation_token\x18\x05 \x01(\t"\x89\x01\n&NexusOperationCompletedEventAttributes\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03\x12/\n\x06result\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x12\n\nrequest_id\x18\x03 \x01(\t"\x88\x01\n#NexusOperationFailedEventAttributes\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03\x12\x31\n\x07\x66\x61ilure\x18\x02 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x12\n\nrequest_id\x18\x03 \x01(\t"\x8a\x01\n%NexusOperationTimedOutEventAttributes\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03\x12\x31\n\x07\x66\x61ilure\x18\x02 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x12\n\nrequest_id\x18\x03 \x01(\t"\x8a\x01\n%NexusOperationCanceledEventAttributes\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03\x12\x31\n\x07\x66\x61ilure\x18\x02 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x12\n\nrequest_id\x18\x03 \x01(\t"t\n,NexusOperationCancelRequestedEventAttributes\x12\x1a\n\x12scheduled_event_id\x18\x01 \x01(\x03\x12(\n workflow_task_completed_event_id\x18\x02 \x01(\x03"\x97\x01\n3NexusOperationCancelRequestCompletedEventAttributes\x12\x1a\n\x12requested_event_id\x18\x01 \x01(\x03\x12(\n workflow_task_completed_event_id\x18\x02 \x01(\x03\x12\x1a\n\x12scheduled_event_id\x18\x03 \x01(\x03"\xc7\x01\n0NexusOperationCancelRequestFailedEventAttributes\x12\x1a\n\x12requested_event_id\x18\x01 \x01(\x03\x12(\n workflow_task_completed_event_id\x18\x02 \x01(\x03\x12\x31\n\x07\x66\x61ilure\x18\x03 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x1a\n\x12scheduled_event_id\x18\x04 \x01(\x03"\xca?\n\x0cHistoryEvent\x12\x10\n\x08\x65vent_id\x18\x01 \x01(\x03\x12.\n\nevent_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x34\n\nevent_type\x18\x03 \x01(\x0e\x32 .temporal.api.enums.v1.EventType\x12\x0f\n\x07version\x18\x04 \x01(\x03\x12\x0f\n\x07task_id\x18\x05 \x01(\x03\x12\x1a\n\x11worker_may_ignore\x18\xac\x02 \x01(\x08\x12\x39\n\ruser_metadata\x18\xad\x02 \x01(\x0b\x32!.temporal.api.sdk.v1.UserMetadata\x12,\n\x05links\x18\xae\x02 \x03(\x0b\x32\x1c.temporal.api.common.v1.Link\x12\x35\n\tprincipal\x18\xaf\x02 \x01(\x0b\x32!.temporal.api.common.v1.Principal\x12\x43\n\x13\x65vent_group_markers\x18\xb0\x02 \x03(\x0b\x32%.temporal.api.sdk.v1.EventGroupMarker\x12w\n+workflow_execution_started_event_attributes\x18\x06 \x01(\x0b\x32@.temporal.api.history.v1.WorkflowExecutionStartedEventAttributesH\x00\x12{\n-workflow_execution_completed_event_attributes\x18\x07 \x01(\x0b\x32\x42.temporal.api.history.v1.WorkflowExecutionCompletedEventAttributesH\x00\x12u\n*workflow_execution_failed_event_attributes\x18\x08 \x01(\x0b\x32?.temporal.api.history.v1.WorkflowExecutionFailedEventAttributesH\x00\x12z\n-workflow_execution_timed_out_event_attributes\x18\t \x01(\x0b\x32\x41.temporal.api.history.v1.WorkflowExecutionTimedOutEventAttributesH\x00\x12q\n(workflow_task_scheduled_event_attributes\x18\n \x01(\x0b\x32=.temporal.api.history.v1.WorkflowTaskScheduledEventAttributesH\x00\x12m\n&workflow_task_started_event_attributes\x18\x0b \x01(\x0b\x32;.temporal.api.history.v1.WorkflowTaskStartedEventAttributesH\x00\x12q\n(workflow_task_completed_event_attributes\x18\x0c \x01(\x0b\x32=.temporal.api.history.v1.WorkflowTaskCompletedEventAttributesH\x00\x12p\n(workflow_task_timed_out_event_attributes\x18\r \x01(\x0b\x32<.temporal.api.history.v1.WorkflowTaskTimedOutEventAttributesH\x00\x12k\n%workflow_task_failed_event_attributes\x18\x0e \x01(\x0b\x32:.temporal.api.history.v1.WorkflowTaskFailedEventAttributesH\x00\x12q\n(activity_task_scheduled_event_attributes\x18\x0f \x01(\x0b\x32=.temporal.api.history.v1.ActivityTaskScheduledEventAttributesH\x00\x12m\n&activity_task_started_event_attributes\x18\x10 \x01(\x0b\x32;.temporal.api.history.v1.ActivityTaskStartedEventAttributesH\x00\x12q\n(activity_task_completed_event_attributes\x18\x11 \x01(\x0b\x32=.temporal.api.history.v1.ActivityTaskCompletedEventAttributesH\x00\x12k\n%activity_task_failed_event_attributes\x18\x12 \x01(\x0b\x32:.temporal.api.history.v1.ActivityTaskFailedEventAttributesH\x00\x12p\n(activity_task_timed_out_event_attributes\x18\x13 \x01(\x0b\x32<.temporal.api.history.v1.ActivityTaskTimedOutEventAttributesH\x00\x12^\n\x1etimer_started_event_attributes\x18\x14 \x01(\x0b\x32\x34.temporal.api.history.v1.TimerStartedEventAttributesH\x00\x12Z\n\x1ctimer_fired_event_attributes\x18\x15 \x01(\x0b\x32\x32.temporal.api.history.v1.TimerFiredEventAttributesH\x00\x12~\n/activity_task_cancel_requested_event_attributes\x18\x16 \x01(\x0b\x32\x43.temporal.api.history.v1.ActivityTaskCancelRequestedEventAttributesH\x00\x12o\n\'activity_task_canceled_event_attributes\x18\x17 \x01(\x0b\x32<.temporal.api.history.v1.ActivityTaskCanceledEventAttributesH\x00\x12`\n\x1ftimer_canceled_event_attributes\x18\x18 \x01(\x0b\x32\x35.temporal.api.history.v1.TimerCanceledEventAttributesH\x00\x12\x62\n marker_recorded_event_attributes\x18\x19 \x01(\x0b\x32\x36.temporal.api.history.v1.MarkerRecordedEventAttributesH\x00\x12y\n,workflow_execution_signaled_event_attributes\x18\x1a \x01(\x0b\x32\x41.temporal.api.history.v1.WorkflowExecutionSignaledEventAttributesH\x00\x12}\n.workflow_execution_terminated_event_attributes\x18\x1b \x01(\x0b\x32\x43.temporal.api.history.v1.WorkflowExecutionTerminatedEventAttributesH\x00\x12\x88\x01\n4workflow_execution_cancel_requested_event_attributes\x18\x1c \x01(\x0b\x32H.temporal.api.history.v1.WorkflowExecutionCancelRequestedEventAttributesH\x00\x12y\n,workflow_execution_canceled_event_attributes\x18\x1d \x01(\x0b\x32\x41.temporal.api.history.v1.WorkflowExecutionCanceledEventAttributesH\x00\x12\xa8\x01\nErequest_cancel_external_workflow_execution_initiated_event_attributes\x18\x1e \x01(\x0b\x32W.temporal.api.history.v1.RequestCancelExternalWorkflowExecutionInitiatedEventAttributesH\x00\x12\xa2\x01\nBrequest_cancel_external_workflow_execution_failed_event_attributes\x18\x1f \x01(\x0b\x32T.temporal.api.history.v1.RequestCancelExternalWorkflowExecutionFailedEventAttributesH\x00\x12\x99\x01\n=external_workflow_execution_cancel_requested_event_attributes\x18 \x01(\x0b\x32P.temporal.api.history.v1.ExternalWorkflowExecutionCancelRequestedEventAttributesH\x00\x12\x87\x01\n4workflow_execution_continued_as_new_event_attributes\x18! \x01(\x0b\x32G.temporal.api.history.v1.WorkflowExecutionContinuedAsNewEventAttributesH\x00\x12\x91\x01\n9start_child_workflow_execution_initiated_event_attributes\x18" \x01(\x0b\x32L.temporal.api.history.v1.StartChildWorkflowExecutionInitiatedEventAttributesH\x00\x12\x8b\x01\n6start_child_workflow_execution_failed_event_attributes\x18# \x01(\x0b\x32I.temporal.api.history.v1.StartChildWorkflowExecutionFailedEventAttributesH\x00\x12\x82\x01\n1child_workflow_execution_started_event_attributes\x18$ \x01(\x0b\x32\x45.temporal.api.history.v1.ChildWorkflowExecutionStartedEventAttributesH\x00\x12\x86\x01\n3child_workflow_execution_completed_event_attributes\x18% \x01(\x0b\x32G.temporal.api.history.v1.ChildWorkflowExecutionCompletedEventAttributesH\x00\x12\x80\x01\n0child_workflow_execution_failed_event_attributes\x18& \x01(\x0b\x32\x44.temporal.api.history.v1.ChildWorkflowExecutionFailedEventAttributesH\x00\x12\x84\x01\n2child_workflow_execution_canceled_event_attributes\x18\' \x01(\x0b\x32\x46.temporal.api.history.v1.ChildWorkflowExecutionCanceledEventAttributesH\x00\x12\x85\x01\n3child_workflow_execution_timed_out_event_attributes\x18( \x01(\x0b\x32\x46.temporal.api.history.v1.ChildWorkflowExecutionTimedOutEventAttributesH\x00\x12\x88\x01\n4child_workflow_execution_terminated_event_attributes\x18) \x01(\x0b\x32H.temporal.api.history.v1.ChildWorkflowExecutionTerminatedEventAttributesH\x00\x12\x99\x01\n=signal_external_workflow_execution_initiated_event_attributes\x18* \x01(\x0b\x32P.temporal.api.history.v1.SignalExternalWorkflowExecutionInitiatedEventAttributesH\x00\x12\x93\x01\n:signal_external_workflow_execution_failed_event_attributes\x18+ \x01(\x0b\x32M.temporal.api.history.v1.SignalExternalWorkflowExecutionFailedEventAttributesH\x00\x12\x8a\x01\n5external_workflow_execution_signaled_event_attributes\x18, \x01(\x0b\x32I.temporal.api.history.v1.ExternalWorkflowExecutionSignaledEventAttributesH\x00\x12\x84\x01\n2upsert_workflow_search_attributes_event_attributes\x18- \x01(\x0b\x32\x46.temporal.api.history.v1.UpsertWorkflowSearchAttributesEventAttributesH\x00\x12\x86\x01\n3workflow_execution_update_accepted_event_attributes\x18. \x01(\x0b\x32G.temporal.api.history.v1.WorkflowExecutionUpdateAcceptedEventAttributesH\x00\x12\x86\x01\n3workflow_execution_update_rejected_event_attributes\x18/ \x01(\x0b\x32G.temporal.api.history.v1.WorkflowExecutionUpdateRejectedEventAttributesH\x00\x12\x88\x01\n4workflow_execution_update_completed_event_attributes\x18\x30 \x01(\x0b\x32H.temporal.api.history.v1.WorkflowExecutionUpdateCompletedEventAttributesH\x00\x12\x90\x01\n8workflow_properties_modified_externally_event_attributes\x18\x31 \x01(\x0b\x32L.temporal.api.history.v1.WorkflowPropertiesModifiedExternallyEventAttributesH\x00\x12\x90\x01\n8activity_properties_modified_externally_event_attributes\x18\x32 \x01(\x0b\x32L.temporal.api.history.v1.ActivityPropertiesModifiedExternallyEventAttributesH\x00\x12{\n-workflow_properties_modified_event_attributes\x18\x33 \x01(\x0b\x32\x42.temporal.api.history.v1.WorkflowPropertiesModifiedEventAttributesH\x00\x12\x86\x01\n3workflow_execution_update_admitted_event_attributes\x18\x34 \x01(\x0b\x32G.temporal.api.history.v1.WorkflowExecutionUpdateAdmittedEventAttributesH\x00\x12u\n*nexus_operation_scheduled_event_attributes\x18\x35 \x01(\x0b\x32?.temporal.api.history.v1.NexusOperationScheduledEventAttributesH\x00\x12q\n(nexus_operation_started_event_attributes\x18\x36 \x01(\x0b\x32=.temporal.api.history.v1.NexusOperationStartedEventAttributesH\x00\x12u\n*nexus_operation_completed_event_attributes\x18\x37 \x01(\x0b\x32?.temporal.api.history.v1.NexusOperationCompletedEventAttributesH\x00\x12o\n\'nexus_operation_failed_event_attributes\x18\x38 \x01(\x0b\x32<.temporal.api.history.v1.NexusOperationFailedEventAttributesH\x00\x12s\n)nexus_operation_canceled_event_attributes\x18\x39 \x01(\x0b\x32>.temporal.api.history.v1.NexusOperationCanceledEventAttributesH\x00\x12t\n*nexus_operation_timed_out_event_attributes\x18: \x01(\x0b\x32>.temporal.api.history.v1.NexusOperationTimedOutEventAttributesH\x00\x12\x82\x01\n1nexus_operation_cancel_requested_event_attributes\x18; \x01(\x0b\x32\x45.temporal.api.history.v1.NexusOperationCancelRequestedEventAttributesH\x00\x12\x86\x01\n3workflow_execution_options_updated_event_attributes\x18< \x01(\x0b\x32G.temporal.api.history.v1.WorkflowExecutionOptionsUpdatedEventAttributesH\x00\x12\x91\x01\n9nexus_operation_cancel_request_completed_event_attributes\x18= \x01(\x0b\x32L.temporal.api.history.v1.NexusOperationCancelRequestCompletedEventAttributesH\x00\x12\x8b\x01\n6nexus_operation_cancel_request_failed_event_attributes\x18> \x01(\x0b\x32I.temporal.api.history.v1.NexusOperationCancelRequestFailedEventAttributesH\x00\x12u\n*workflow_execution_paused_event_attributes\x18? \x01(\x0b\x32?.temporal.api.history.v1.WorkflowExecutionPausedEventAttributesH\x00\x12y\n,workflow_execution_unpaused_event_attributes\x18@ \x01(\x0b\x32\x41.temporal.api.history.v1.WorkflowExecutionUnpausedEventAttributesH\x00\x12\x9b\x01\n>workflow_execution_time_skipping_transitioned_event_attributes\x18\x41 \x01(\x0b\x32Q.temporal.api.history.v1.WorkflowExecutionTimeSkippingTransitionedEventAttributesH\x00\x42\x0c\n\nattributes"@\n\x07History\x12\x35\n\x06\x65vents\x18\x01 \x03(\x0b\x32%.temporal.api.history.v1.HistoryEventB\x8e\x01\n\x1aio.temporal.api.history.v1B\x0cMessageProtoP\x01Z%go.temporal.io/api/history/v1;history\xaa\x02\x19Temporalio.Api.History.V1\xea\x02\x1cTemporalio::Api::History::V1b\x06proto3' ) _WORKFLOWEXECUTIONSTARTEDEVENTATTRIBUTES = DESCRIPTOR.message_types_by_name[ "WorkflowExecutionStartedEventAttributes" ] +_DECLINEDTARGETVERSIONUPGRADE = DESCRIPTOR.message_types_by_name[ + "DeclinedTargetVersionUpgrade" +] _WORKFLOWEXECUTIONCOMPLETEDEVENTATTRIBUTES = DESCRIPTOR.message_types_by_name[ "WorkflowExecutionCompletedEventAttributes" ] @@ -198,6 +204,11 @@ _WORKFLOWEXECUTIONOPTIONSUPDATEDEVENTATTRIBUTES = DESCRIPTOR.message_types_by_name[ "WorkflowExecutionOptionsUpdatedEventAttributes" ] +_WORKFLOWEXECUTIONOPTIONSUPDATEDEVENTATTRIBUTES_WORKFLOWUPDATEOPTIONSUPDATE = ( + _WORKFLOWEXECUTIONOPTIONSUPDATEDEVENTATTRIBUTES.nested_types_by_name[ + "WorkflowUpdateOptionsUpdate" + ] +) _WORKFLOWPROPERTIESMODIFIEDEXTERNALLYEVENTATTRIBUTES = DESCRIPTOR.message_types_by_name[ "WorkflowPropertiesModifiedExternallyEventAttributes" ] @@ -216,6 +227,17 @@ _WORKFLOWEXECUTIONUPDATEADMITTEDEVENTATTRIBUTES = DESCRIPTOR.message_types_by_name[ "WorkflowExecutionUpdateAdmittedEventAttributes" ] +_WORKFLOWEXECUTIONPAUSEDEVENTATTRIBUTES = DESCRIPTOR.message_types_by_name[ + "WorkflowExecutionPausedEventAttributes" +] +_WORKFLOWEXECUTIONUNPAUSEDEVENTATTRIBUTES = DESCRIPTOR.message_types_by_name[ + "WorkflowExecutionUnpausedEventAttributes" +] +_WORKFLOWEXECUTIONTIMESKIPPINGTRANSITIONEDEVENTATTRIBUTES = ( + DESCRIPTOR.message_types_by_name[ + "WorkflowExecutionTimeSkippingTransitionedEventAttributes" + ] +) _NEXUSOPERATIONSCHEDULEDEVENTATTRIBUTES = DESCRIPTOR.message_types_by_name[ "NexusOperationScheduledEventAttributes" ] @@ -253,18 +275,29 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWEXECUTIONSTARTEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.WorkflowExecutionStartedEventAttributes) }, ) _sym_db.RegisterMessage(WorkflowExecutionStartedEventAttributes) +DeclinedTargetVersionUpgrade = _reflection.GeneratedProtocolMessageType( + "DeclinedTargetVersionUpgrade", + (_message.Message,), + { + "DESCRIPTOR": _DECLINEDTARGETVERSIONUPGRADE, + "__module__": "temporalio.api.history.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.history.v1.DeclinedTargetVersionUpgrade) + }, +) +_sym_db.RegisterMessage(DeclinedTargetVersionUpgrade) + WorkflowExecutionCompletedEventAttributes = _reflection.GeneratedProtocolMessageType( "WorkflowExecutionCompletedEventAttributes", (_message.Message,), { "DESCRIPTOR": _WORKFLOWEXECUTIONCOMPLETEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.WorkflowExecutionCompletedEventAttributes) }, ) @@ -275,7 +308,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWEXECUTIONFAILEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.WorkflowExecutionFailedEventAttributes) }, ) @@ -286,7 +319,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWEXECUTIONTIMEDOUTEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.WorkflowExecutionTimedOutEventAttributes) }, ) @@ -298,7 +331,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWEXECUTIONCONTINUEDASNEWEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.WorkflowExecutionContinuedAsNewEventAttributes) }, ) @@ -310,7 +343,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWTASKSCHEDULEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.WorkflowTaskScheduledEventAttributes) }, ) @@ -321,7 +354,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWTASKSTARTEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.WorkflowTaskStartedEventAttributes) }, ) @@ -332,7 +365,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWTASKCOMPLETEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.WorkflowTaskCompletedEventAttributes) }, ) @@ -343,7 +376,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWTASKTIMEDOUTEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.WorkflowTaskTimedOutEventAttributes) }, ) @@ -354,7 +387,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWTASKFAILEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.WorkflowTaskFailedEventAttributes) }, ) @@ -365,7 +398,7 @@ (_message.Message,), { "DESCRIPTOR": _ACTIVITYTASKSCHEDULEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.ActivityTaskScheduledEventAttributes) }, ) @@ -376,7 +409,7 @@ (_message.Message,), { "DESCRIPTOR": _ACTIVITYTASKSTARTEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.ActivityTaskStartedEventAttributes) }, ) @@ -387,7 +420,7 @@ (_message.Message,), { "DESCRIPTOR": _ACTIVITYTASKCOMPLETEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.ActivityTaskCompletedEventAttributes) }, ) @@ -398,7 +431,7 @@ (_message.Message,), { "DESCRIPTOR": _ACTIVITYTASKFAILEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.ActivityTaskFailedEventAttributes) }, ) @@ -409,7 +442,7 @@ (_message.Message,), { "DESCRIPTOR": _ACTIVITYTASKTIMEDOUTEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.ActivityTaskTimedOutEventAttributes) }, ) @@ -420,7 +453,7 @@ (_message.Message,), { "DESCRIPTOR": _ACTIVITYTASKCANCELREQUESTEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.ActivityTaskCancelRequestedEventAttributes) }, ) @@ -431,7 +464,7 @@ (_message.Message,), { "DESCRIPTOR": _ACTIVITYTASKCANCELEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.ActivityTaskCanceledEventAttributes) }, ) @@ -442,7 +475,7 @@ (_message.Message,), { "DESCRIPTOR": _TIMERSTARTEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.TimerStartedEventAttributes) }, ) @@ -453,7 +486,7 @@ (_message.Message,), { "DESCRIPTOR": _TIMERFIREDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.TimerFiredEventAttributes) }, ) @@ -464,7 +497,7 @@ (_message.Message,), { "DESCRIPTOR": _TIMERCANCELEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.TimerCanceledEventAttributes) }, ) @@ -476,7 +509,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWEXECUTIONCANCELREQUESTEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.WorkflowExecutionCancelRequestedEventAttributes) }, ) @@ -488,7 +521,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWEXECUTIONCANCELEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.WorkflowExecutionCanceledEventAttributes) }, ) @@ -503,12 +536,12 @@ (_message.Message,), { "DESCRIPTOR": _MARKERRECORDEDEVENTATTRIBUTES_DETAILSENTRY, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.MarkerRecordedEventAttributes.DetailsEntry) }, ), "DESCRIPTOR": _MARKERRECORDEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.MarkerRecordedEventAttributes) }, ) @@ -520,7 +553,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWEXECUTIONSIGNALEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.WorkflowExecutionSignaledEventAttributes) }, ) @@ -531,7 +564,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWEXECUTIONTERMINATEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.WorkflowExecutionTerminatedEventAttributes) }, ) @@ -542,7 +575,7 @@ (_message.Message,), { "DESCRIPTOR": _REQUESTCANCELEXTERNALWORKFLOWEXECUTIONINITIATEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.RequestCancelExternalWorkflowExecutionInitiatedEventAttributes) }, ) @@ -554,7 +587,7 @@ (_message.Message,), { "DESCRIPTOR": _REQUESTCANCELEXTERNALWORKFLOWEXECUTIONFAILEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.RequestCancelExternalWorkflowExecutionFailedEventAttributes) }, ) @@ -567,7 +600,7 @@ (_message.Message,), { "DESCRIPTOR": _EXTERNALWORKFLOWEXECUTIONCANCELREQUESTEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.ExternalWorkflowExecutionCancelRequestedEventAttributes) }, ) @@ -580,7 +613,7 @@ (_message.Message,), { "DESCRIPTOR": _SIGNALEXTERNALWORKFLOWEXECUTIONINITIATEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.SignalExternalWorkflowExecutionInitiatedEventAttributes) }, ) @@ -593,7 +626,7 @@ (_message.Message,), { "DESCRIPTOR": _SIGNALEXTERNALWORKFLOWEXECUTIONFAILEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.SignalExternalWorkflowExecutionFailedEventAttributes) }, ) @@ -606,7 +639,7 @@ (_message.Message,), { "DESCRIPTOR": _EXTERNALWORKFLOWEXECUTIONSIGNALEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.ExternalWorkflowExecutionSignaledEventAttributes) }, ) @@ -619,7 +652,7 @@ (_message.Message,), { "DESCRIPTOR": _UPSERTWORKFLOWSEARCHATTRIBUTESEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.UpsertWorkflowSearchAttributesEventAttributes) }, ) @@ -631,7 +664,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWPROPERTIESMODIFIEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.WorkflowPropertiesModifiedEventAttributes) }, ) @@ -643,7 +676,7 @@ (_message.Message,), { "DESCRIPTOR": _STARTCHILDWORKFLOWEXECUTIONINITIATEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.StartChildWorkflowExecutionInitiatedEventAttributes) }, ) @@ -656,7 +689,7 @@ (_message.Message,), { "DESCRIPTOR": _STARTCHILDWORKFLOWEXECUTIONFAILEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.StartChildWorkflowExecutionFailedEventAttributes) }, ) @@ -668,7 +701,7 @@ (_message.Message,), { "DESCRIPTOR": _CHILDWORKFLOWEXECUTIONSTARTEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.ChildWorkflowExecutionStartedEventAttributes) }, ) @@ -680,7 +713,7 @@ (_message.Message,), { "DESCRIPTOR": _CHILDWORKFLOWEXECUTIONCOMPLETEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.ChildWorkflowExecutionCompletedEventAttributes) }, ) @@ -692,7 +725,7 @@ (_message.Message,), { "DESCRIPTOR": _CHILDWORKFLOWEXECUTIONFAILEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.ChildWorkflowExecutionFailedEventAttributes) }, ) @@ -704,7 +737,7 @@ (_message.Message,), { "DESCRIPTOR": _CHILDWORKFLOWEXECUTIONCANCELEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.ChildWorkflowExecutionCanceledEventAttributes) }, ) @@ -717,7 +750,7 @@ (_message.Message,), { "DESCRIPTOR": _CHILDWORKFLOWEXECUTIONTIMEDOUTEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.ChildWorkflowExecutionTimedOutEventAttributes) }, ) @@ -730,25 +763,35 @@ (_message.Message,), { "DESCRIPTOR": _CHILDWORKFLOWEXECUTIONTERMINATEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.ChildWorkflowExecutionTerminatedEventAttributes) }, ) ) _sym_db.RegisterMessage(ChildWorkflowExecutionTerminatedEventAttributes) -WorkflowExecutionOptionsUpdatedEventAttributes = ( - _reflection.GeneratedProtocolMessageType( - "WorkflowExecutionOptionsUpdatedEventAttributes", - (_message.Message,), - { - "DESCRIPTOR": _WORKFLOWEXECUTIONOPTIONSUPDATEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", - # @@protoc_insertion_point(class_scope:temporal.api.history.v1.WorkflowExecutionOptionsUpdatedEventAttributes) - }, - ) +WorkflowExecutionOptionsUpdatedEventAttributes = _reflection.GeneratedProtocolMessageType( + "WorkflowExecutionOptionsUpdatedEventAttributes", + (_message.Message,), + { + "WorkflowUpdateOptionsUpdate": _reflection.GeneratedProtocolMessageType( + "WorkflowUpdateOptionsUpdate", + (_message.Message,), + { + "DESCRIPTOR": _WORKFLOWEXECUTIONOPTIONSUPDATEDEVENTATTRIBUTES_WORKFLOWUPDATEOPTIONSUPDATE, + "__module__": "temporalio.api.history.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.history.v1.WorkflowExecutionOptionsUpdatedEventAttributes.WorkflowUpdateOptionsUpdate) + }, + ), + "DESCRIPTOR": _WORKFLOWEXECUTIONOPTIONSUPDATEDEVENTATTRIBUTES, + "__module__": "temporalio.api.history.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.history.v1.WorkflowExecutionOptionsUpdatedEventAttributes) + }, ) _sym_db.RegisterMessage(WorkflowExecutionOptionsUpdatedEventAttributes) +_sym_db.RegisterMessage( + WorkflowExecutionOptionsUpdatedEventAttributes.WorkflowUpdateOptionsUpdate +) WorkflowPropertiesModifiedExternallyEventAttributes = ( _reflection.GeneratedProtocolMessageType( @@ -756,7 +799,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWPROPERTIESMODIFIEDEXTERNALLYEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.WorkflowPropertiesModifiedExternallyEventAttributes) }, ) @@ -769,7 +812,7 @@ (_message.Message,), { "DESCRIPTOR": _ACTIVITYPROPERTIESMODIFIEDEXTERNALLYEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.ActivityPropertiesModifiedExternallyEventAttributes) }, ) @@ -782,7 +825,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWEXECUTIONUPDATEACCEPTEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.WorkflowExecutionUpdateAcceptedEventAttributes) }, ) @@ -795,7 +838,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWEXECUTIONUPDATECOMPLETEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.WorkflowExecutionUpdateCompletedEventAttributes) }, ) @@ -808,7 +851,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWEXECUTIONUPDATEREJECTEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.WorkflowExecutionUpdateRejectedEventAttributes) }, ) @@ -821,13 +864,48 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWEXECUTIONUPDATEADMITTEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.WorkflowExecutionUpdateAdmittedEventAttributes) }, ) ) _sym_db.RegisterMessage(WorkflowExecutionUpdateAdmittedEventAttributes) +WorkflowExecutionPausedEventAttributes = _reflection.GeneratedProtocolMessageType( + "WorkflowExecutionPausedEventAttributes", + (_message.Message,), + { + "DESCRIPTOR": _WORKFLOWEXECUTIONPAUSEDEVENTATTRIBUTES, + "__module__": "temporalio.api.history.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.history.v1.WorkflowExecutionPausedEventAttributes) + }, +) +_sym_db.RegisterMessage(WorkflowExecutionPausedEventAttributes) + +WorkflowExecutionUnpausedEventAttributes = _reflection.GeneratedProtocolMessageType( + "WorkflowExecutionUnpausedEventAttributes", + (_message.Message,), + { + "DESCRIPTOR": _WORKFLOWEXECUTIONUNPAUSEDEVENTATTRIBUTES, + "__module__": "temporalio.api.history.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.history.v1.WorkflowExecutionUnpausedEventAttributes) + }, +) +_sym_db.RegisterMessage(WorkflowExecutionUnpausedEventAttributes) + +WorkflowExecutionTimeSkippingTransitionedEventAttributes = ( + _reflection.GeneratedProtocolMessageType( + "WorkflowExecutionTimeSkippingTransitionedEventAttributes", + (_message.Message,), + { + "DESCRIPTOR": _WORKFLOWEXECUTIONTIMESKIPPINGTRANSITIONEDEVENTATTRIBUTES, + "__module__": "temporalio.api.history.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.history.v1.WorkflowExecutionTimeSkippingTransitionedEventAttributes) + }, + ) +) +_sym_db.RegisterMessage(WorkflowExecutionTimeSkippingTransitionedEventAttributes) + NexusOperationScheduledEventAttributes = _reflection.GeneratedProtocolMessageType( "NexusOperationScheduledEventAttributes", (_message.Message,), @@ -837,12 +915,12 @@ (_message.Message,), { "DESCRIPTOR": _NEXUSOPERATIONSCHEDULEDEVENTATTRIBUTES_NEXUSHEADERENTRY, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.NexusOperationScheduledEventAttributes.NexusHeaderEntry) }, ), "DESCRIPTOR": _NEXUSOPERATIONSCHEDULEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.NexusOperationScheduledEventAttributes) }, ) @@ -854,7 +932,7 @@ (_message.Message,), { "DESCRIPTOR": _NEXUSOPERATIONSTARTEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.NexusOperationStartedEventAttributes) }, ) @@ -865,7 +943,7 @@ (_message.Message,), { "DESCRIPTOR": _NEXUSOPERATIONCOMPLETEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.NexusOperationCompletedEventAttributes) }, ) @@ -876,7 +954,7 @@ (_message.Message,), { "DESCRIPTOR": _NEXUSOPERATIONFAILEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.NexusOperationFailedEventAttributes) }, ) @@ -887,7 +965,7 @@ (_message.Message,), { "DESCRIPTOR": _NEXUSOPERATIONTIMEDOUTEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.NexusOperationTimedOutEventAttributes) }, ) @@ -898,7 +976,7 @@ (_message.Message,), { "DESCRIPTOR": _NEXUSOPERATIONCANCELEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.NexusOperationCanceledEventAttributes) }, ) @@ -909,7 +987,7 @@ (_message.Message,), { "DESCRIPTOR": _NEXUSOPERATIONCANCELREQUESTEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.NexusOperationCancelRequestedEventAttributes) }, ) @@ -921,7 +999,7 @@ (_message.Message,), { "DESCRIPTOR": _NEXUSOPERATIONCANCELREQUESTCOMPLETEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.NexusOperationCancelRequestCompletedEventAttributes) }, ) @@ -934,7 +1012,7 @@ (_message.Message,), { "DESCRIPTOR": _NEXUSOPERATIONCANCELREQUESTFAILEDEVENTATTRIBUTES, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.NexusOperationCancelRequestFailedEventAttributes) }, ) @@ -946,7 +1024,7 @@ (_message.Message,), { "DESCRIPTOR": _HISTORYEVENT, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.HistoryEvent) }, ) @@ -957,7 +1035,7 @@ (_message.Message,), { "DESCRIPTOR": _HISTORY, - "__module__": "temporal.api.history.v1.message_pb2", + "__module__": "temporalio.api.history.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.history.v1.History) }, ) @@ -1132,130 +1210,140 @@ _NEXUSOPERATIONSTARTEDEVENTATTRIBUTES.fields_by_name[ "operation_id" ]._serialized_options = b"\030\001" - _WORKFLOWEXECUTIONSTARTEDEVENTATTRIBUTES._serialized_start = 617 - _WORKFLOWEXECUTIONSTARTEDEVENTATTRIBUTES._serialized_end = 2623 - _WORKFLOWEXECUTIONCOMPLETEDEVENTATTRIBUTES._serialized_start = 2626 - _WORKFLOWEXECUTIONCOMPLETEDEVENTATTRIBUTES._serialized_end = 2791 - _WORKFLOWEXECUTIONFAILEDEVENTATTRIBUTES._serialized_start = 2794 - _WORKFLOWEXECUTIONFAILEDEVENTATTRIBUTES._serialized_end = 3013 - _WORKFLOWEXECUTIONTIMEDOUTEVENTATTRIBUTES._serialized_start = 3016 - _WORKFLOWEXECUTIONTIMEDOUTEVENTATTRIBUTES._serialized_end = 3144 - _WORKFLOWEXECUTIONCONTINUEDASNEWEVENTATTRIBUTES._serialized_start = 3147 - _WORKFLOWEXECUTIONCONTINUEDASNEWEVENTATTRIBUTES._serialized_end = 3987 - _WORKFLOWTASKSCHEDULEDEVENTATTRIBUTES._serialized_start = 3990 - _WORKFLOWTASKSCHEDULEDEVENTATTRIBUTES._serialized_end = 4162 - _WORKFLOWTASKSTARTEDEVENTATTRIBUTES._serialized_start = 4165 - _WORKFLOWTASKSTARTEDEVENTATTRIBUTES._serialized_end = 4439 - _WORKFLOWTASKCOMPLETEDEVENTATTRIBUTES._serialized_start = 4442 - _WORKFLOWTASKCOMPLETEDEVENTATTRIBUTES._serialized_end = 5084 - _WORKFLOWTASKTIMEDOUTEVENTATTRIBUTES._serialized_start = 5087 - _WORKFLOWTASKTIMEDOUTEVENTATTRIBUTES._serialized_end = 5236 - _WORKFLOWTASKFAILEDEVENTATTRIBUTES._serialized_start = 5239 - _WORKFLOWTASKFAILEDEVENTATTRIBUTES._serialized_end = 5630 - _ACTIVITYTASKSCHEDULEDEVENTATTRIBUTES._serialized_start = 5633 - _ACTIVITYTASKSCHEDULEDEVENTATTRIBUTES._serialized_end = 6339 - _ACTIVITYTASKSTARTEDEVENTATTRIBUTES._serialized_start = 6342 - _ACTIVITYTASKSTARTEDEVENTATTRIBUTES._serialized_end = 6628 - _ACTIVITYTASKCOMPLETEDEVENTATTRIBUTES._serialized_start = 6631 - _ACTIVITYTASKCOMPLETEDEVENTATTRIBUTES._serialized_end = 6863 - _ACTIVITYTASKFAILEDEVENTATTRIBUTES._serialized_start = 6866 - _ACTIVITYTASKFAILEDEVENTATTRIBUTES._serialized_end = 7152 - _ACTIVITYTASKTIMEDOUTEVENTATTRIBUTES._serialized_start = 7155 - _ACTIVITYTASKTIMEDOUTEVENTATTRIBUTES._serialized_end = 7353 - _ACTIVITYTASKCANCELREQUESTEDEVENTATTRIBUTES._serialized_start = 7355 - _ACTIVITYTASKCANCELREQUESTEDEVENTATTRIBUTES._serialized_end = 7469 - _ACTIVITYTASKCANCELEDEVENTATTRIBUTES._serialized_start = 7472 - _ACTIVITYTASKCANCELEDEVENTATTRIBUTES._serialized_end = 7746 - _TIMERSTARTEDEVENTATTRIBUTES._serialized_start = 7749 - _TIMERSTARTEDEVENTATTRIBUTES._serialized_end = 7896 - _TIMERFIREDEVENTATTRIBUTES._serialized_start = 7898 - _TIMERFIREDEVENTATTRIBUTES._serialized_end = 7969 - _TIMERCANCELEDEVENTATTRIBUTES._serialized_start = 7972 - _TIMERCANCELEDEVENTATTRIBUTES._serialized_end = 8106 - _WORKFLOWEXECUTIONCANCELREQUESTEDEVENTATTRIBUTES._serialized_start = 8109 - _WORKFLOWEXECUTIONCANCELREQUESTEDEVENTATTRIBUTES._serialized_end = 8308 - _WORKFLOWEXECUTIONCANCELEDEVENTATTRIBUTES._serialized_start = 8311 - _WORKFLOWEXECUTIONCANCELEDEVENTATTRIBUTES._serialized_end = 8446 - _MARKERRECORDEDEVENTATTRIBUTES._serialized_start = 8449 - _MARKERRECORDEDEVENTATTRIBUTES._serialized_end = 8810 - _MARKERRECORDEDEVENTATTRIBUTES_DETAILSENTRY._serialized_start = 8730 - _MARKERRECORDEDEVENTATTRIBUTES_DETAILSENTRY._serialized_end = 8810 - _WORKFLOWEXECUTIONSIGNALEDEVENTATTRIBUTES._serialized_start = 8813 - _WORKFLOWEXECUTIONSIGNALEDEVENTATTRIBUTES._serialized_end = 9112 - _WORKFLOWEXECUTIONTERMINATEDEVENTATTRIBUTES._serialized_start = 9115 - _WORKFLOWEXECUTIONTERMINATEDEVENTATTRIBUTES._serialized_end = 9244 - _REQUESTCANCELEXTERNALWORKFLOWEXECUTIONINITIATEDEVENTATTRIBUTES._serialized_start = 9247 + _WORKFLOWEXECUTIONSTARTEDEVENTATTRIBUTES._serialized_start = 663 + _WORKFLOWEXECUTIONSTARTEDEVENTATTRIBUTES._serialized_end = 3057 + _DECLINEDTARGETVERSIONUPGRADE._serialized_start = 3060 + _DECLINEDTARGETVERSIONUPGRADE._serialized_end = 3196 + _WORKFLOWEXECUTIONCOMPLETEDEVENTATTRIBUTES._serialized_start = 3199 + _WORKFLOWEXECUTIONCOMPLETEDEVENTATTRIBUTES._serialized_end = 3364 + _WORKFLOWEXECUTIONFAILEDEVENTATTRIBUTES._serialized_start = 3367 + _WORKFLOWEXECUTIONFAILEDEVENTATTRIBUTES._serialized_end = 3586 + _WORKFLOWEXECUTIONTIMEDOUTEVENTATTRIBUTES._serialized_start = 3589 + _WORKFLOWEXECUTIONTIMEDOUTEVENTATTRIBUTES._serialized_end = 3717 + _WORKFLOWEXECUTIONCONTINUEDASNEWEVENTATTRIBUTES._serialized_start = 3720 + _WORKFLOWEXECUTIONCONTINUEDASNEWEVENTATTRIBUTES._serialized_end = 4653 + _WORKFLOWTASKSCHEDULEDEVENTATTRIBUTES._serialized_start = 4656 + _WORKFLOWTASKSCHEDULEDEVENTATTRIBUTES._serialized_end = 4828 + _WORKFLOWTASKSTARTEDEVENTATTRIBUTES._serialized_start = 4831 + _WORKFLOWTASKSTARTEDEVENTATTRIBUTES._serialized_end = 5247 + _WORKFLOWTASKCOMPLETEDEVENTATTRIBUTES._serialized_start = 5250 + _WORKFLOWTASKCOMPLETEDEVENTATTRIBUTES._serialized_end = 5892 + _WORKFLOWTASKTIMEDOUTEVENTATTRIBUTES._serialized_start = 5895 + _WORKFLOWTASKTIMEDOUTEVENTATTRIBUTES._serialized_end = 6044 + _WORKFLOWTASKFAILEDEVENTATTRIBUTES._serialized_start = 6047 + _WORKFLOWTASKFAILEDEVENTATTRIBUTES._serialized_end = 6438 + _ACTIVITYTASKSCHEDULEDEVENTATTRIBUTES._serialized_start = 6441 + _ACTIVITYTASKSCHEDULEDEVENTATTRIBUTES._serialized_end = 7147 + _ACTIVITYTASKSTARTEDEVENTATTRIBUTES._serialized_start = 7150 + _ACTIVITYTASKSTARTEDEVENTATTRIBUTES._serialized_end = 7436 + _ACTIVITYTASKCOMPLETEDEVENTATTRIBUTES._serialized_start = 7439 + _ACTIVITYTASKCOMPLETEDEVENTATTRIBUTES._serialized_end = 7671 + _ACTIVITYTASKFAILEDEVENTATTRIBUTES._serialized_start = 7674 + _ACTIVITYTASKFAILEDEVENTATTRIBUTES._serialized_end = 7960 + _ACTIVITYTASKTIMEDOUTEVENTATTRIBUTES._serialized_start = 7963 + _ACTIVITYTASKTIMEDOUTEVENTATTRIBUTES._serialized_end = 8161 + _ACTIVITYTASKCANCELREQUESTEDEVENTATTRIBUTES._serialized_start = 8163 + _ACTIVITYTASKCANCELREQUESTEDEVENTATTRIBUTES._serialized_end = 8277 + _ACTIVITYTASKCANCELEDEVENTATTRIBUTES._serialized_start = 8280 + _ACTIVITYTASKCANCELEDEVENTATTRIBUTES._serialized_end = 8554 + _TIMERSTARTEDEVENTATTRIBUTES._serialized_start = 8557 + _TIMERSTARTEDEVENTATTRIBUTES._serialized_end = 8704 + _TIMERFIREDEVENTATTRIBUTES._serialized_start = 8706 + _TIMERFIREDEVENTATTRIBUTES._serialized_end = 8777 + _TIMERCANCELEDEVENTATTRIBUTES._serialized_start = 8780 + _TIMERCANCELEDEVENTATTRIBUTES._serialized_end = 8914 + _WORKFLOWEXECUTIONCANCELREQUESTEDEVENTATTRIBUTES._serialized_start = 8917 + _WORKFLOWEXECUTIONCANCELREQUESTEDEVENTATTRIBUTES._serialized_end = 9116 + _WORKFLOWEXECUTIONCANCELEDEVENTATTRIBUTES._serialized_start = 9119 + _WORKFLOWEXECUTIONCANCELEDEVENTATTRIBUTES._serialized_end = 9254 + _MARKERRECORDEDEVENTATTRIBUTES._serialized_start = 9257 + _MARKERRECORDEDEVENTATTRIBUTES._serialized_end = 9618 + _MARKERRECORDEDEVENTATTRIBUTES_DETAILSENTRY._serialized_start = 9538 + _MARKERRECORDEDEVENTATTRIBUTES_DETAILSENTRY._serialized_end = 9618 + _WORKFLOWEXECUTIONSIGNALEDEVENTATTRIBUTES._serialized_start = 9621 + _WORKFLOWEXECUTIONSIGNALEDEVENTATTRIBUTES._serialized_end = 9940 + _WORKFLOWEXECUTIONTERMINATEDEVENTATTRIBUTES._serialized_start = 9943 + _WORKFLOWEXECUTIONTERMINATEDEVENTATTRIBUTES._serialized_end = 10072 + _REQUESTCANCELEXTERNALWORKFLOWEXECUTIONINITIATEDEVENTATTRIBUTES._serialized_start = 10075 _REQUESTCANCELEXTERNALWORKFLOWEXECUTIONINITIATEDEVENTATTRIBUTES._serialized_end = ( - 9531 + 10359 ) _REQUESTCANCELEXTERNALWORKFLOWEXECUTIONFAILEDEVENTATTRIBUTES._serialized_start = ( - 9534 + 10362 ) - _REQUESTCANCELEXTERNALWORKFLOWEXECUTIONFAILEDEVENTATTRIBUTES._serialized_end = 9880 - _EXTERNALWORKFLOWEXECUTIONCANCELREQUESTEDEVENTATTRIBUTES._serialized_start = 9883 - _EXTERNALWORKFLOWEXECUTIONCANCELREQUESTEDEVENTATTRIBUTES._serialized_end = 10080 - _SIGNALEXTERNALWORKFLOWEXECUTIONINITIATEDEVENTATTRIBUTES._serialized_start = 10083 - _SIGNALEXTERNALWORKFLOWEXECUTIONINITIATEDEVENTATTRIBUTES._serialized_end = 10462 - _SIGNALEXTERNALWORKFLOWEXECUTIONFAILEDEVENTATTRIBUTES._serialized_start = 10465 - _SIGNALEXTERNALWORKFLOWEXECUTIONFAILEDEVENTATTRIBUTES._serialized_end = 10804 - _EXTERNALWORKFLOWEXECUTIONSIGNALEDEVENTATTRIBUTES._serialized_start = 10807 - _EXTERNALWORKFLOWEXECUTIONSIGNALEDEVENTATTRIBUTES._serialized_end = 11018 - _UPSERTWORKFLOWSEARCHATTRIBUTESEVENTATTRIBUTES._serialized_start = 11021 - _UPSERTWORKFLOWSEARCHATTRIBUTESEVENTATTRIBUTES._serialized_end = 11179 - _WORKFLOWPROPERTIESMODIFIEDEVENTATTRIBUTES._serialized_start = 11182 - _WORKFLOWPROPERTIESMODIFIEDEVENTATTRIBUTES._serialized_end = 11320 - _STARTCHILDWORKFLOWEXECUTIONINITIATEDEVENTATTRIBUTES._serialized_start = 11323 - _STARTCHILDWORKFLOWEXECUTIONINITIATEDEVENTATTRIBUTES._serialized_end = 12323 - _STARTCHILDWORKFLOWEXECUTIONFAILEDEVENTATTRIBUTES._serialized_start = 12326 - _STARTCHILDWORKFLOWEXECUTIONFAILEDEVENTATTRIBUTES._serialized_end = 12668 - _CHILDWORKFLOWEXECUTIONSTARTEDEVENTATTRIBUTES._serialized_start = 12671 - _CHILDWORKFLOWEXECUTIONSTARTEDEVENTATTRIBUTES._serialized_end = 12966 - _CHILDWORKFLOWEXECUTIONCOMPLETEDEVENTATTRIBUTES._serialized_start = 12969 - _CHILDWORKFLOWEXECUTIONCOMPLETEDEVENTATTRIBUTES._serialized_end = 13294 - _CHILDWORKFLOWEXECUTIONFAILEDEVENTATTRIBUTES._serialized_start = 13297 - _CHILDWORKFLOWEXECUTIONFAILEDEVENTATTRIBUTES._serialized_end = 13676 - _CHILDWORKFLOWEXECUTIONCANCELEDEVENTATTRIBUTES._serialized_start = 13679 - _CHILDWORKFLOWEXECUTIONCANCELEDEVENTATTRIBUTES._serialized_end = 14004 - _CHILDWORKFLOWEXECUTIONTIMEDOUTEVENTATTRIBUTES._serialized_start = 14007 - _CHILDWORKFLOWEXECUTIONTIMEDOUTEVENTATTRIBUTES._serialized_end = 14337 - _CHILDWORKFLOWEXECUTIONTERMINATEDEVENTATTRIBUTES._serialized_start = 14340 - _CHILDWORKFLOWEXECUTIONTERMINATEDEVENTATTRIBUTES._serialized_end = 14616 - _WORKFLOWEXECUTIONOPTIONSUPDATEDEVENTATTRIBUTES._serialized_start = 14619 - _WORKFLOWEXECUTIONOPTIONSUPDATEDEVENTATTRIBUTES._serialized_end = 14879 - _WORKFLOWPROPERTIESMODIFIEDEXTERNALLYEVENTATTRIBUTES._serialized_start = 14882 - _WORKFLOWPROPERTIESMODIFIEDEXTERNALLYEVENTATTRIBUTES._serialized_end = 15202 - _ACTIVITYPROPERTIESMODIFIEDEXTERNALLYEVENTATTRIBUTES._serialized_start = 15205 - _ACTIVITYPROPERTIESMODIFIEDEXTERNALLYEVENTATTRIBUTES._serialized_end = 15349 - _WORKFLOWEXECUTIONUPDATEACCEPTEDEVENTATTRIBUTES._serialized_start = 15352 - _WORKFLOWEXECUTIONUPDATEACCEPTEDEVENTATTRIBUTES._serialized_end = 15572 - _WORKFLOWEXECUTIONUPDATECOMPLETEDEVENTATTRIBUTES._serialized_start = 15575 - _WORKFLOWEXECUTIONUPDATECOMPLETEDEVENTATTRIBUTES._serialized_end = 15745 - _WORKFLOWEXECUTIONUPDATEREJECTEDEVENTATTRIBUTES._serialized_start = 15748 - _WORKFLOWEXECUTIONUPDATEREJECTEDEVENTATTRIBUTES._serialized_end = 16019 - _WORKFLOWEXECUTIONUPDATEADMITTEDEVENTATTRIBUTES._serialized_start = 16022 - _WORKFLOWEXECUTIONUPDATEADMITTEDEVENTATTRIBUTES._serialized_end = 16186 - _NEXUSOPERATIONSCHEDULEDEVENTATTRIBUTES._serialized_start = 16189 - _NEXUSOPERATIONSCHEDULEDEVENTATTRIBUTES._serialized_end = 16632 - _NEXUSOPERATIONSCHEDULEDEVENTATTRIBUTES_NEXUSHEADERENTRY._serialized_start = 16582 - _NEXUSOPERATIONSCHEDULEDEVENTATTRIBUTES_NEXUSHEADERENTRY._serialized_end = 16632 - _NEXUSOPERATIONSTARTEDEVENTATTRIBUTES._serialized_start = 16635 - _NEXUSOPERATIONSTARTEDEVENTATTRIBUTES._serialized_end = 16772 - _NEXUSOPERATIONCOMPLETEDEVENTATTRIBUTES._serialized_start = 16775 - _NEXUSOPERATIONCOMPLETEDEVENTATTRIBUTES._serialized_end = 16912 - _NEXUSOPERATIONFAILEDEVENTATTRIBUTES._serialized_start = 16915 - _NEXUSOPERATIONFAILEDEVENTATTRIBUTES._serialized_end = 17051 - _NEXUSOPERATIONTIMEDOUTEVENTATTRIBUTES._serialized_start = 17054 - _NEXUSOPERATIONTIMEDOUTEVENTATTRIBUTES._serialized_end = 17192 - _NEXUSOPERATIONCANCELEDEVENTATTRIBUTES._serialized_start = 17195 - _NEXUSOPERATIONCANCELEDEVENTATTRIBUTES._serialized_end = 17333 - _NEXUSOPERATIONCANCELREQUESTEDEVENTATTRIBUTES._serialized_start = 17335 - _NEXUSOPERATIONCANCELREQUESTEDEVENTATTRIBUTES._serialized_end = 17451 - _NEXUSOPERATIONCANCELREQUESTCOMPLETEDEVENTATTRIBUTES._serialized_start = 17454 - _NEXUSOPERATIONCANCELREQUESTCOMPLETEDEVENTATTRIBUTES._serialized_end = 17605 - _NEXUSOPERATIONCANCELREQUESTFAILEDEVENTATTRIBUTES._serialized_start = 17608 - _NEXUSOPERATIONCANCELREQUESTFAILEDEVENTATTRIBUTES._serialized_end = 17807 - _HISTORYEVENT._serialized_start = 17810 - _HISTORYEVENT._serialized_end = 25424 - _HISTORY._serialized_start = 25426 - _HISTORY._serialized_end = 25490 + _REQUESTCANCELEXTERNALWORKFLOWEXECUTIONFAILEDEVENTATTRIBUTES._serialized_end = 10708 + _EXTERNALWORKFLOWEXECUTIONCANCELREQUESTEDEVENTATTRIBUTES._serialized_start = 10711 + _EXTERNALWORKFLOWEXECUTIONCANCELREQUESTEDEVENTATTRIBUTES._serialized_end = 10908 + _SIGNALEXTERNALWORKFLOWEXECUTIONINITIATEDEVENTATTRIBUTES._serialized_start = 10911 + _SIGNALEXTERNALWORKFLOWEXECUTIONINITIATEDEVENTATTRIBUTES._serialized_end = 11290 + _SIGNALEXTERNALWORKFLOWEXECUTIONFAILEDEVENTATTRIBUTES._serialized_start = 11293 + _SIGNALEXTERNALWORKFLOWEXECUTIONFAILEDEVENTATTRIBUTES._serialized_end = 11632 + _EXTERNALWORKFLOWEXECUTIONSIGNALEDEVENTATTRIBUTES._serialized_start = 11635 + _EXTERNALWORKFLOWEXECUTIONSIGNALEDEVENTATTRIBUTES._serialized_end = 11846 + _UPSERTWORKFLOWSEARCHATTRIBUTESEVENTATTRIBUTES._serialized_start = 11849 + _UPSERTWORKFLOWSEARCHATTRIBUTESEVENTATTRIBUTES._serialized_end = 12007 + _WORKFLOWPROPERTIESMODIFIEDEVENTATTRIBUTES._serialized_start = 12010 + _WORKFLOWPROPERTIESMODIFIEDEVENTATTRIBUTES._serialized_end = 12148 + _STARTCHILDWORKFLOWEXECUTIONINITIATEDEVENTATTRIBUTES._serialized_start = 12151 + _STARTCHILDWORKFLOWEXECUTIONINITIATEDEVENTATTRIBUTES._serialized_end = 13427 + _STARTCHILDWORKFLOWEXECUTIONFAILEDEVENTATTRIBUTES._serialized_start = 13430 + _STARTCHILDWORKFLOWEXECUTIONFAILEDEVENTATTRIBUTES._serialized_end = 13772 + _CHILDWORKFLOWEXECUTIONSTARTEDEVENTATTRIBUTES._serialized_start = 13775 + _CHILDWORKFLOWEXECUTIONSTARTEDEVENTATTRIBUTES._serialized_end = 14070 + _CHILDWORKFLOWEXECUTIONCOMPLETEDEVENTATTRIBUTES._serialized_start = 14073 + _CHILDWORKFLOWEXECUTIONCOMPLETEDEVENTATTRIBUTES._serialized_end = 14398 + _CHILDWORKFLOWEXECUTIONFAILEDEVENTATTRIBUTES._serialized_start = 14401 + _CHILDWORKFLOWEXECUTIONFAILEDEVENTATTRIBUTES._serialized_end = 14780 + _CHILDWORKFLOWEXECUTIONCANCELEDEVENTATTRIBUTES._serialized_start = 14783 + _CHILDWORKFLOWEXECUTIONCANCELEDEVENTATTRIBUTES._serialized_end = 15108 + _CHILDWORKFLOWEXECUTIONTIMEDOUTEVENTATTRIBUTES._serialized_start = 15111 + _CHILDWORKFLOWEXECUTIONTIMEDOUTEVENTATTRIBUTES._serialized_end = 15441 + _CHILDWORKFLOWEXECUTIONTERMINATEDEVENTATTRIBUTES._serialized_start = 15444 + _CHILDWORKFLOWEXECUTIONTERMINATEDEVENTATTRIBUTES._serialized_end = 15720 + _WORKFLOWEXECUTIONOPTIONSUPDATEDEVENTATTRIBUTES._serialized_start = 15723 + _WORKFLOWEXECUTIONOPTIONSUPDATEDEVENTATTRIBUTES._serialized_end = 16453 + _WORKFLOWEXECUTIONOPTIONSUPDATEDEVENTATTRIBUTES_WORKFLOWUPDATEOPTIONSUPDATE._serialized_start = 16303 + _WORKFLOWEXECUTIONOPTIONSUPDATEDEVENTATTRIBUTES_WORKFLOWUPDATEOPTIONSUPDATE._serialized_end = 16453 + _WORKFLOWPROPERTIESMODIFIEDEXTERNALLYEVENTATTRIBUTES._serialized_start = 16456 + _WORKFLOWPROPERTIESMODIFIEDEXTERNALLYEVENTATTRIBUTES._serialized_end = 16776 + _ACTIVITYPROPERTIESMODIFIEDEXTERNALLYEVENTATTRIBUTES._serialized_start = 16779 + _ACTIVITYPROPERTIESMODIFIEDEXTERNALLYEVENTATTRIBUTES._serialized_end = 16923 + _WORKFLOWEXECUTIONUPDATEACCEPTEDEVENTATTRIBUTES._serialized_start = 16926 + _WORKFLOWEXECUTIONUPDATEACCEPTEDEVENTATTRIBUTES._serialized_end = 17146 + _WORKFLOWEXECUTIONUPDATECOMPLETEDEVENTATTRIBUTES._serialized_start = 17149 + _WORKFLOWEXECUTIONUPDATECOMPLETEDEVENTATTRIBUTES._serialized_end = 17319 + _WORKFLOWEXECUTIONUPDATEREJECTEDEVENTATTRIBUTES._serialized_start = 17322 + _WORKFLOWEXECUTIONUPDATEREJECTEDEVENTATTRIBUTES._serialized_end = 17593 + _WORKFLOWEXECUTIONUPDATEADMITTEDEVENTATTRIBUTES._serialized_start = 17596 + _WORKFLOWEXECUTIONUPDATEADMITTEDEVENTATTRIBUTES._serialized_end = 17760 + _WORKFLOWEXECUTIONPAUSEDEVENTATTRIBUTES._serialized_start = 17762 + _WORKFLOWEXECUTIONPAUSEDEVENTATTRIBUTES._serialized_end = 17856 + _WORKFLOWEXECUTIONUNPAUSEDEVENTATTRIBUTES._serialized_start = 17858 + _WORKFLOWEXECUTIONUNPAUSEDEVENTATTRIBUTES._serialized_end = 17954 + _WORKFLOWEXECUTIONTIMESKIPPINGTRANSITIONEDEVENTATTRIBUTES._serialized_start = 17957 + _WORKFLOWEXECUTIONTIMESKIPPINGTRANSITIONEDEVENTATTRIBUTES._serialized_end = 18154 + _NEXUSOPERATIONSCHEDULEDEVENTATTRIBUTES._serialized_start = 18157 + _NEXUSOPERATIONSCHEDULEDEVENTATTRIBUTES._serialized_end = 18721 + _NEXUSOPERATIONSCHEDULEDEVENTATTRIBUTES_NEXUSHEADERENTRY._serialized_start = 18671 + _NEXUSOPERATIONSCHEDULEDEVENTATTRIBUTES_NEXUSHEADERENTRY._serialized_end = 18721 + _NEXUSOPERATIONSTARTEDEVENTATTRIBUTES._serialized_start = 18724 + _NEXUSOPERATIONSTARTEDEVENTATTRIBUTES._serialized_end = 18861 + _NEXUSOPERATIONCOMPLETEDEVENTATTRIBUTES._serialized_start = 18864 + _NEXUSOPERATIONCOMPLETEDEVENTATTRIBUTES._serialized_end = 19001 + _NEXUSOPERATIONFAILEDEVENTATTRIBUTES._serialized_start = 19004 + _NEXUSOPERATIONFAILEDEVENTATTRIBUTES._serialized_end = 19140 + _NEXUSOPERATIONTIMEDOUTEVENTATTRIBUTES._serialized_start = 19143 + _NEXUSOPERATIONTIMEDOUTEVENTATTRIBUTES._serialized_end = 19281 + _NEXUSOPERATIONCANCELEDEVENTATTRIBUTES._serialized_start = 19284 + _NEXUSOPERATIONCANCELEDEVENTATTRIBUTES._serialized_end = 19422 + _NEXUSOPERATIONCANCELREQUESTEDEVENTATTRIBUTES._serialized_start = 19424 + _NEXUSOPERATIONCANCELREQUESTEDEVENTATTRIBUTES._serialized_end = 19540 + _NEXUSOPERATIONCANCELREQUESTCOMPLETEDEVENTATTRIBUTES._serialized_start = 19543 + _NEXUSOPERATIONCANCELREQUESTCOMPLETEDEVENTATTRIBUTES._serialized_end = 19694 + _NEXUSOPERATIONCANCELREQUESTFAILEDEVENTATTRIBUTES._serialized_start = 19697 + _NEXUSOPERATIONCANCELREQUESTFAILEDEVENTATTRIBUTES._serialized_end = 19896 + _HISTORYEVENT._serialized_start = 19899 + _HISTORYEVENT._serialized_end = 28037 + _HISTORY._serialized_start = 28039 + _HISTORY._serialized_end = 28103 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/history/v1/message_pb2.pyi b/temporalio/api/history/v1/message_pb2.pyi index 50fc04f0e..783e9d3b6 100644 --- a/temporalio/api/history/v1/message_pb2.pyi +++ b/temporalio/api/history/v1/message_pb2.pyi @@ -20,6 +20,7 @@ import temporalio.api.enums.v1.failed_cause_pb2 import temporalio.api.enums.v1.update_pb2 import temporalio.api.enums.v1.workflow_pb2 import temporalio.api.failure.v1.message_pb2 +import temporalio.api.sdk.v1.event_group_marker_pb2 import temporalio.api.sdk.v1.task_complete_metadata_pb2 import temporalio.api.sdk.v1.user_metadata_pb2 import temporalio.api.taskqueue.v1.message_pb2 @@ -74,7 +75,11 @@ class WorkflowExecutionStartedEventAttributes(google.protobuf.message.Message): PARENT_PINNED_WORKER_DEPLOYMENT_VERSION_FIELD_NUMBER: builtins.int PRIORITY_FIELD_NUMBER: builtins.int INHERITED_PINNED_VERSION_FIELD_NUMBER: builtins.int + INHERITED_AUTO_UPGRADE_INFO_FIELD_NUMBER: builtins.int EAGER_EXECUTION_ACCEPTED_FIELD_NUMBER: builtins.int + DECLINED_TARGET_VERSION_UPGRADE_FIELD_NUMBER: builtins.int + TIME_SKIPPING_CONFIG_FIELD_NUMBER: builtins.int + TIME_SKIPPING_STATE_PROPAGATION_FIELD_NUMBER: builtins.int @property def workflow_type(self) -> temporalio.api.common.v1.message_pb2.WorkflowType: ... parent_workflow_namespace: builtins.str @@ -239,23 +244,83 @@ class WorkflowExecutionStartedEventAttributes(google.protobuf.message.Message): """If present, the new workflow should start on this version with pinned base behavior. Child of pinned parent will inherit the parent's version if the Child's Task Queue belongs to that version. - New run initiated by workflow ContinueAsNew of pinned run, will inherit the previous run's version if the + A new run initiated by workflow ContinueAsNew of pinned run, will inherit the previous run's version if the new run's Task Queue belongs to that version. - New run initiated by workflow Cron will never inherit. + A new run initiated by workflow Cron will never inherit. - New run initiated by workflow Retry will only inherit if the retried run is effectively pinned at the time + A new run initiated by workflow Retry will only inherit if the retried run is effectively pinned at the time of retry, and the retried run inherited a pinned version when it started (ie. it is a child of a pinned parent, or a CaN of a pinned run, and is running on a Task Queue in the inherited version). Pinned override is inherited if Task Queue of new run is compatible with the override version. Override is inherited separately and takes precedence over inherited base version. + + Note: This field is mutually exclusive with inherited_auto_upgrade_info. + Additionaly, versioning_override, if present, overrides this field during routing decisions. + """ + @property + def inherited_auto_upgrade_info( + self, + ) -> temporalio.api.deployment.v1.message_pb2.InheritedAutoUpgradeInfo: + """If present, the new workflow begins with AutoUpgrade behavior. Before dispatching the + first workflow task, this field is set to the deployment version on which the parent/ + previous run was operating. This inheritance only happens when the task queues belong to + the same deployment version. The first workflow task will then be dispatched to either + this inherited deployment version, or the current deployment version of the task queue's + Deployment. After the first workflow task, the effective behavior depends on worker-sent + values in subsequent workflow tasks. + + Inheritance rules: + - ContinueAsNew and child workflows: inherit AutoUpgrade behavior and deployment version + - Cron: never inherits + - Retry: inherits only if the retried run is effectively AutoUpgrade at the time of + retry, and inherited AutoUpgrade behavior when it started (i.e. it is a child of an + AutoUpgrade parent or ContinueAsNew of an AutoUpgrade run, running on the same + deployment as the parent/previous run) + + Additional notes: + - This field is mutually exclusive with `inherited_pinned_version`. + - `versioning_override`, if present, overrides this field during routing decisions. + - SDK implementations do not interact with this field and is only used internally by + the server to ensure task routing correctness. """ eager_execution_accepted: builtins.bool """A boolean indicating whether the SDK has asked to eagerly execute the first workflow task for this workflow and eager execution was accepted by the server. Only populated by server with version >= 1.29.0. """ + @property + def declined_target_version_upgrade(self) -> global___DeclinedTargetVersionUpgrade: + """During a previous run of this workflow, the server may have notified the SDK + that the Target Worker Deployment Version changed, but the SDK declined to + upgrade (e.g., by continuing-as-new with PINNED behavior). This field records + the target version that was declined. + + This is a wrapper message to distinguish "never declined" (nil wrapper) from + "declined an unversioned target" (non-nil wrapper with nil deployment_version). + + Used internally by the server during continue-as-new and retry. + Should not be read or interpreted by SDKs. + """ + @property + def time_skipping_config( + self, + ) -> temporalio.api.common.v1.message_pb2.TimeSkippingConfig: + """Initial time-skipping configuration for this workflow execution, recorded at start time. + This may have been set explicitly via the start workflow request, or propagated from a + parent/previous execution. + + The configuration may be updated after start via UpdateWorkflowExecutionOptions, which + will be reflected in the WorkflowExecutionOptionsUpdatedEvent. + """ + @property + def time_skipping_state_propagation( + self, + ) -> temporalio.api.common.v1.message_pb2.TimeSkippingStatePropagation: + """The time-skipping state propagated from a previous run of this workflow. This can be nil + if no time skipping has occurred or there is no previous run. + """ def __init__( self, *, @@ -307,17 +372,29 @@ class WorkflowExecutionStartedEventAttributes(google.protobuf.message.Message): priority: temporalio.api.common.v1.message_pb2.Priority | None = ..., inherited_pinned_version: temporalio.api.deployment.v1.message_pb2.WorkerDeploymentVersion | None = ..., + inherited_auto_upgrade_info: temporalio.api.deployment.v1.message_pb2.InheritedAutoUpgradeInfo + | None = ..., eager_execution_accepted: builtins.bool = ..., + declined_target_version_upgrade: global___DeclinedTargetVersionUpgrade + | None = ..., + time_skipping_config: temporalio.api.common.v1.message_pb2.TimeSkippingConfig + | None = ..., + time_skipping_state_propagation: temporalio.api.common.v1.message_pb2.TimeSkippingStatePropagation + | None = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal[ "continued_failure", b"continued_failure", + "declined_target_version_upgrade", + b"declined_target_version_upgrade", "first_workflow_task_backoff", b"first_workflow_task_backoff", "header", b"header", + "inherited_auto_upgrade_info", + b"inherited_auto_upgrade_info", "inherited_pinned_version", b"inherited_pinned_version", "input", @@ -342,6 +419,10 @@ class WorkflowExecutionStartedEventAttributes(google.protobuf.message.Message): b"source_version_stamp", "task_queue", b"task_queue", + "time_skipping_config", + b"time_skipping_config", + "time_skipping_state_propagation", + b"time_skipping_state_propagation", "versioning_override", b"versioning_override", "workflow_execution_expiration_time", @@ -369,6 +450,8 @@ class WorkflowExecutionStartedEventAttributes(google.protobuf.message.Message): b"continued_failure", "cron_schedule", b"cron_schedule", + "declined_target_version_upgrade", + b"declined_target_version_upgrade", "eager_execution_accepted", b"eager_execution_accepted", "first_execution_run_id", @@ -379,6 +462,8 @@ class WorkflowExecutionStartedEventAttributes(google.protobuf.message.Message): b"header", "identity", b"identity", + "inherited_auto_upgrade_info", + b"inherited_auto_upgrade_info", "inherited_build_id", b"inherited_build_id", "inherited_pinned_version", @@ -419,6 +504,10 @@ class WorkflowExecutionStartedEventAttributes(google.protobuf.message.Message): b"source_version_stamp", "task_queue", b"task_queue", + "time_skipping_config", + b"time_skipping_config", + "time_skipping_state_propagation", + b"time_skipping_state_propagation", "versioning_override", b"versioning_override", "workflow_execution_expiration_time", @@ -440,6 +529,50 @@ global___WorkflowExecutionStartedEventAttributes = ( WorkflowExecutionStartedEventAttributes ) +class DeclinedTargetVersionUpgrade(google.protobuf.message.Message): + """Wrapper for a target deployment version that the SDK declined to upgrade to. + See declined_target_version_upgrade on WorkflowExecutionStartedEventAttributes. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + DEPLOYMENT_VERSION_FIELD_NUMBER: builtins.int + REVISION_NUMBER_FIELD_NUMBER: builtins.int + @property + def deployment_version( + self, + ) -> temporalio.api.deployment.v1.message_pb2.WorkerDeploymentVersion: ... + revision_number: builtins.int + """Revision number of the task queue routing config at the time the target + was declined. If an incoming target's revision is <= this value, it is + not newer and is not used for deciding whether or not to suppress the + upgrade signal. + """ + def __init__( + self, + *, + deployment_version: temporalio.api.deployment.v1.message_pb2.WorkerDeploymentVersion + | None = ..., + revision_number: builtins.int = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "deployment_version", b"deployment_version" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "deployment_version", + b"deployment_version", + "revision_number", + b"revision_number", + ], + ) -> None: ... + +global___DeclinedTargetVersionUpgrade = DeclinedTargetVersionUpgrade + class WorkflowExecutionCompletedEventAttributes(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -567,6 +700,7 @@ class WorkflowExecutionContinuedAsNewEventAttributes(google.protobuf.message.Mes MEMO_FIELD_NUMBER: builtins.int SEARCH_ATTRIBUTES_FIELD_NUMBER: builtins.int INHERIT_BUILD_ID_FIELD_NUMBER: builtins.int + INITIAL_VERSIONING_BEHAVIOR_FIELD_NUMBER: builtins.int new_execution_run_id: builtins.str """The run ID of the new workflow started by this continue-as-new""" @property @@ -585,18 +719,22 @@ class WorkflowExecutionContinuedAsNewEventAttributes(google.protobuf.message.Mes """The `WORKFLOW_TASK_COMPLETED` event which this command was reported with""" @property def backoff_start_interval(self) -> google.protobuf.duration_pb2.Duration: - """TODO: How and is this used?""" + """How long the server will wait before scheduling the first workflow task for the new run. + Used for cron, retry, and other continue-as-new cases that server may enforce some minimal + delay between new runs for system protection purpose. + """ initiator: temporalio.api.enums.v1.workflow_pb2.ContinueAsNewInitiator.ValueType @property def failure(self) -> temporalio.api.failure.v1.message_pb2.Failure: - """TODO: David are these right? - Deprecated. If a workflow's retry policy would cause a new run to start when the current one + """Deprecated. If a workflow's retry policy would cause a new run to start when the current one has failed, this field would be populated with that failure. Now (when supported by server and sdk) the final event will be `WORKFLOW_EXECUTION_FAILED` with `new_execution_run_id` set. """ @property def last_completion_result(self) -> temporalio.api.common.v1.message_pb2.Payloads: - """TODO: Is this the result of *this* workflow as it continued-as-new?""" + """The result from the most recent completed run of this workflow. The SDK surfaces this to the + new run via APIs such as `GetLastCompletionResult`. + """ @property def header(self) -> temporalio.api.common.v1.message_pb2.Header: ... @property @@ -610,6 +748,13 @@ class WorkflowExecutionContinuedAsNewEventAttributes(google.protobuf.message.Mes the assignment rules will be used to independently assign a Build ID to the new execution. Deprecated. Only considered for versioning v0.2. """ + initial_versioning_behavior: ( + temporalio.api.enums.v1.workflow_pb2.ContinueAsNewVersioningBehavior.ValueType + ) + """Experimental. Optionally decide the versioning behavior that the first task of the new run should use. + For example, choose to AutoUpgrade on continue-as-new instead of inheriting the pinned version + of the previous run. + """ def __init__( self, *, @@ -630,6 +775,7 @@ class WorkflowExecutionContinuedAsNewEventAttributes(google.protobuf.message.Mes search_attributes: temporalio.api.common.v1.message_pb2.SearchAttributes | None = ..., inherit_build_id: builtins.bool = ..., + initial_versioning_behavior: temporalio.api.enums.v1.workflow_pb2.ContinueAsNewVersioningBehavior.ValueType = ..., ) -> None: ... def HasField( self, @@ -669,6 +815,8 @@ class WorkflowExecutionContinuedAsNewEventAttributes(google.protobuf.message.Mes b"header", "inherit_build_id", b"inherit_build_id", + "initial_versioning_behavior", + b"initial_versioning_behavior", "initiator", b"initiator", "input", @@ -753,6 +901,8 @@ class WorkflowTaskStartedEventAttributes(google.protobuf.message.Message): IDENTITY_FIELD_NUMBER: builtins.int REQUEST_ID_FIELD_NUMBER: builtins.int SUGGEST_CONTINUE_AS_NEW_FIELD_NUMBER: builtins.int + SUGGEST_CONTINUE_AS_NEW_REASONS_FIELD_NUMBER: builtins.int + TARGET_WORKER_DEPLOYMENT_VERSION_CHANGED_FIELD_NUMBER: builtins.int HISTORY_SIZE_BYTES_FIELD_NUMBER: builtins.int WORKER_VERSION_FIELD_NUMBER: builtins.int BUILD_ID_REDIRECT_COUNTER_FIELD_NUMBER: builtins.int @@ -761,10 +911,28 @@ class WorkflowTaskStartedEventAttributes(google.protobuf.message.Message): identity: builtins.str """Identity of the worker who picked up this task""" request_id: builtins.str - """TODO: ? Appears unused?""" + """This field is populated from the RecordWorkflowTaskStartedRequest. Matching service would + set the request_id on the RecordWorkflowTaskStartedRequest to a new UUID. This is useful + in case a RecordWorkflowTaskStarted call succeed but matching doesn't get that response, + so matching could retry and history service would return success if the request_id matches. + In that case, matching will continue to deliver the task to worker. Without this field, history + service would return AlreadyStarted error, and matching would drop the task. + """ suggest_continue_as_new: builtins.bool - """True if this workflow should continue-as-new soon because its history size (in - either event count or bytes) is getting large. + """True if this workflow should continue-as-new soon. See `suggest_continue_as_new_reasons` for why.""" + @property + def suggest_continue_as_new_reasons( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[ + temporalio.api.enums.v1.workflow_pb2.SuggestContinueAsNewReason.ValueType + ]: + """The reason(s) that suggest_continue_as_new is true, if it is. + Unset if suggest_continue_as_new is false. + """ + target_worker_deployment_version_changed: builtins.bool + """True if Workflow's Target Worker Deployment Version is different from its Pinned Version and + the workflow is Pinned. + Experimental. """ history_size_bytes: builtins.int """Total history size in bytes, which the workflow might use to decide when to @@ -788,6 +956,11 @@ class WorkflowTaskStartedEventAttributes(google.protobuf.message.Message): identity: builtins.str = ..., request_id: builtins.str = ..., suggest_continue_as_new: builtins.bool = ..., + suggest_continue_as_new_reasons: collections.abc.Iterable[ + temporalio.api.enums.v1.workflow_pb2.SuggestContinueAsNewReason.ValueType + ] + | None = ..., + target_worker_deployment_version_changed: builtins.bool = ..., history_size_bytes: builtins.int = ..., worker_version: temporalio.api.common.v1.message_pb2.WorkerVersionStamp | None = ..., @@ -811,6 +984,10 @@ class WorkflowTaskStartedEventAttributes(google.protobuf.message.Message): b"scheduled_event_id", "suggest_continue_as_new", b"suggest_continue_as_new", + "suggest_continue_as_new_reasons", + b"suggest_continue_as_new_reasons", + "target_worker_deployment_version_changed", + b"target_worker_deployment_version_changed", "worker_version", b"worker_version", ], @@ -879,13 +1056,11 @@ class WorkflowTaskCompletedEventAttributes(google.protobuf.message.Message): worker_deployment_version: builtins.str """The Worker Deployment Version that completed this task. Must be set if `versioning_behavior` is set. This value updates workflow execution's `versioning_info.version`. - Experimental. Worker Deployments are experimental and might significantly change in the future. Deprecated. Replaced with `deployment_version`. """ worker_deployment_name: builtins.str """The name of Worker Deployment that completed this task. Must be set if `versioning_behavior` is set. This value updates workflow execution's `worker_deployment_name`. - Experimental. Worker Deployments are experimental and might significantly change in the future. """ @property def deployment_version( @@ -893,7 +1068,6 @@ class WorkflowTaskCompletedEventAttributes(google.protobuf.message.Message): ) -> temporalio.api.deployment.v1.message_pb2.WorkerDeploymentVersion: """The Worker Deployment Version that completed this task. Must be set if `versioning_behavior` is set. This value updates workflow execution's `versioning_info.deployment_version`. - Experimental. Worker Deployments are experimental and might significantly change in the future. """ def __init__( self, @@ -1016,13 +1190,17 @@ class WorkflowTaskFailedEventAttributes(google.protobuf.message.Message): def failure(self) -> temporalio.api.failure.v1.message_pb2.Failure: """The failure details""" identity: builtins.str - """If a worker explicitly failed this task, it's identity. TODO: What is this set to if server fails the task?""" + """If a worker explicitly failed this task, this field contains the worker's identity. + When the server generates the failure internally this field is set as 'history-service'. + """ base_run_id: builtins.str """The original run id of the workflow. For reset workflow.""" new_run_id: builtins.str """If the workflow is being reset, the new run id.""" fork_event_version: builtins.int - """TODO: ?""" + """Version of the event where the history branch was forked. Used by multi-cluster replication + during resets to identify the correct history branch. + """ binary_checksum: builtins.str """Deprecated. This field should be cleaned up when versioning-2 API is removed. [cleanup-experimental-wv] If a worker explicitly failed this task, its binary id @@ -1248,7 +1426,13 @@ class ActivityTaskStartedEventAttributes(google.protobuf.message.Message): identity: builtins.str """id of the worker that picked up this task""" request_id: builtins.str - """TODO ??""" + """This field is populated from the RecordActivityTaskStartedRequest. Matching service would + set the request_id on the RecordActivityTaskStartedRequest to a new UUID. This is useful + in case a RecordActivityTaskStarted call succeed but matching doesn't get that response, + so matching could retry and history service would return success if the request_id matches. + In that case, matching will continue to deliver the task to worker. Without this field, history + service would return AlreadyStarted error, and matching would drop the task. + """ attempt: builtins.int """Starting at 1, the number of times this task has been attempted""" @property @@ -1675,11 +1859,11 @@ class WorkflowExecutionCancelRequestedEventAttributes(google.protobuf.message.Me EXTERNAL_WORKFLOW_EXECUTION_FIELD_NUMBER: builtins.int IDENTITY_FIELD_NUMBER: builtins.int cause: builtins.str - """User provided reason for requesting cancellation - TODO: shall we create a new field with name "reason" and deprecate this one? - """ + """User provided reason for requesting cancellation""" external_initiated_event_id: builtins.int - """TODO: Is this the ID of the event in the workflow which initiated this cancel, if there was one?""" + """The ID of the `REQUEST_CANCEL_EXTERNAL_WORKFLOW_EXECUTION_INITIATED` event in the external + workflow history when the cancellation was requested by another workflow. + """ @property def external_workflow_execution( self, @@ -1842,6 +2026,7 @@ class WorkflowExecutionSignaledEventAttributes(google.protobuf.message.Message): HEADER_FIELD_NUMBER: builtins.int SKIP_GENERATE_WORKFLOW_TASK_FIELD_NUMBER: builtins.int EXTERNAL_WORKFLOW_EXECUTION_FIELD_NUMBER: builtins.int + REQUEST_ID_FIELD_NUMBER: builtins.int signal_name: builtins.str """The name/type of the signal to fire""" @property @@ -1861,6 +2046,10 @@ class WorkflowExecutionSignaledEventAttributes(google.protobuf.message.Message): self, ) -> temporalio.api.common.v1.message_pb2.WorkflowExecution: """When signal origin is a workflow execution, this field is set.""" + request_id: builtins.str + """The request ID of the Signal request, used by the server to attach this to + the correct Event ID when generating link. + """ def __init__( self, *, @@ -1871,6 +2060,7 @@ class WorkflowExecutionSignaledEventAttributes(google.protobuf.message.Message): skip_generate_workflow_task: builtins.bool = ..., external_workflow_execution: temporalio.api.common.v1.message_pb2.WorkflowExecution | None = ..., + request_id: builtins.str = ..., ) -> None: ... def HasField( self, @@ -1894,6 +2084,8 @@ class WorkflowExecutionSignaledEventAttributes(google.protobuf.message.Message): b"identity", "input", b"input", + "request_id", + b"request_id", "signal_name", b"signal_name", "skip_generate_workflow_task", @@ -2454,6 +2646,9 @@ class StartChildWorkflowExecutionInitiatedEventAttributes( SEARCH_ATTRIBUTES_FIELD_NUMBER: builtins.int INHERIT_BUILD_ID_FIELD_NUMBER: builtins.int PRIORITY_FIELD_NUMBER: builtins.int + TIME_SKIPPING_CONFIG_FIELD_NUMBER: builtins.int + TIME_SKIPPING_STATE_PROPAGATION_FIELD_NUMBER: builtins.int + VERSIONING_OVERRIDE_FIELD_NUMBER: builtins.int namespace: builtins.str """Namespace of the child workflow. SDKs and UI tools should use `namespace` field but server must use `namespace_id` only. @@ -2507,6 +2702,25 @@ class StartChildWorkflowExecutionInitiatedEventAttributes( @property def priority(self) -> temporalio.api.common.v1.message_pb2.Priority: """Priority metadata""" + @property + def time_skipping_config( + self, + ) -> temporalio.api.common.v1.message_pb2.TimeSkippingConfig: + """The propagated time-skipping configuration for the child workflow.""" + @property + def time_skipping_state_propagation( + self, + ) -> temporalio.api.common.v1.message_pb2.TimeSkippingStatePropagation: + """The time-skipping state propagated from the parent workflow. This can be nil if no time skipping + has occurred or there is no previous run. + """ + @property + def versioning_override( + self, + ) -> temporalio.api.workflow.v1.message_pb2.VersioningOverride: + """Versioning override requested for the child workflow. If present, this explicit override + takes precedence over versioning behavior inherited from the parent workflow. + """ def __init__( self, *, @@ -2531,6 +2745,12 @@ class StartChildWorkflowExecutionInitiatedEventAttributes( | None = ..., inherit_build_id: builtins.bool = ..., priority: temporalio.api.common.v1.message_pb2.Priority | None = ..., + time_skipping_config: temporalio.api.common.v1.message_pb2.TimeSkippingConfig + | None = ..., + time_skipping_state_propagation: temporalio.api.common.v1.message_pb2.TimeSkippingStatePropagation + | None = ..., + versioning_override: temporalio.api.workflow.v1.message_pb2.VersioningOverride + | None = ..., ) -> None: ... def HasField( self, @@ -2549,6 +2769,12 @@ class StartChildWorkflowExecutionInitiatedEventAttributes( b"search_attributes", "task_queue", b"task_queue", + "time_skipping_config", + b"time_skipping_config", + "time_skipping_state_propagation", + b"time_skipping_state_propagation", + "versioning_override", + b"versioning_override", "workflow_execution_timeout", b"workflow_execution_timeout", "workflow_run_timeout", @@ -2588,6 +2814,12 @@ class StartChildWorkflowExecutionInitiatedEventAttributes( b"search_attributes", "task_queue", b"task_queue", + "time_skipping_config", + b"time_skipping_config", + "time_skipping_state_propagation", + b"time_skipping_state_propagation", + "versioning_override", + b"versioning_override", "workflow_execution_timeout", b"workflow_execution_timeout", "workflow_id", @@ -3111,10 +3343,56 @@ global___ChildWorkflowExecutionTerminatedEventAttributes = ( class WorkflowExecutionOptionsUpdatedEventAttributes(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor + class WorkflowUpdateOptionsUpdate(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + UPDATE_ID_FIELD_NUMBER: builtins.int + ATTACHED_REQUEST_ID_FIELD_NUMBER: builtins.int + ATTACHED_COMPLETION_CALLBACKS_FIELD_NUMBER: builtins.int + update_id: builtins.str + """The ID of the workflow update this update options update corresponds to.""" + attached_request_id: builtins.str + """Request ID attached to the running workflow update so that subsequent requests with same + request ID will be deduped + """ + @property + def attached_completion_callbacks( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.common.v1.message_pb2.Callback + ]: + """Completion callbacks attached to the running workflow update.""" + def __init__( + self, + *, + update_id: builtins.str = ..., + attached_request_id: builtins.str = ..., + attached_completion_callbacks: collections.abc.Iterable[ + temporalio.api.common.v1.message_pb2.Callback + ] + | None = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "attached_completion_callbacks", + b"attached_completion_callbacks", + "attached_request_id", + b"attached_request_id", + "update_id", + b"update_id", + ], + ) -> None: ... + VERSIONING_OVERRIDE_FIELD_NUMBER: builtins.int UNSET_VERSIONING_OVERRIDE_FIELD_NUMBER: builtins.int ATTACHED_REQUEST_ID_FIELD_NUMBER: builtins.int ATTACHED_COMPLETION_CALLBACKS_FIELD_NUMBER: builtins.int + IDENTITY_FIELD_NUMBER: builtins.int + PRIORITY_FIELD_NUMBER: builtins.int + TIME_SKIPPING_CONFIG_FIELD_NUMBER: builtins.int + TIME_SKIPPING_CONFIG_UPDATED_FIELD_NUMBER: builtins.int + WORKFLOW_UPDATE_OPTIONS_FIELD_NUMBER: builtins.int @property def versioning_override( self, @@ -3125,7 +3403,7 @@ class WorkflowExecutionOptionsUpdatedEventAttributes(google.protobuf.message.Mes unset_versioning_override: builtins.bool """Versioning override removed in this event.""" attached_request_id: builtins.str - """Request ID attachedto the running workflow execution so that subsequent requests with same + """Request ID attached to the running workflow execution so that subsequent requests with same request ID will be deduped. """ @property @@ -3135,6 +3413,29 @@ class WorkflowExecutionOptionsUpdatedEventAttributes(google.protobuf.message.Mes temporalio.api.common.v1.message_pb2.Callback ]: """Completion callbacks attached to the running workflow execution.""" + identity: builtins.str + """Optional. The identity of the client who initiated the request that created this event.""" + @property + def priority(self) -> temporalio.api.common.v1.message_pb2.Priority: + """Priority override upserted in this event. Represents the full priority; not just partial fields. + Ignored if nil. + """ + @property + def time_skipping_config( + self, + ) -> temporalio.api.common.v1.message_pb2.TimeSkippingConfig: + """TimeSkippingConfig override upserted in this event. Represents the full config.""" + time_skipping_config_updated: builtins.bool + """Indicates the time skipping config was updated by the recent call to update + workflow execution options. + """ + @property + def workflow_update_options( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + global___WorkflowExecutionOptionsUpdatedEventAttributes.WorkflowUpdateOptionsUpdate + ]: + """Updates to workflow updates options.""" def __init__( self, *, @@ -3146,11 +3447,25 @@ class WorkflowExecutionOptionsUpdatedEventAttributes(google.protobuf.message.Mes temporalio.api.common.v1.message_pb2.Callback ] | None = ..., + identity: builtins.str = ..., + priority: temporalio.api.common.v1.message_pb2.Priority | None = ..., + time_skipping_config: temporalio.api.common.v1.message_pb2.TimeSkippingConfig + | None = ..., + time_skipping_config_updated: builtins.bool = ..., + workflow_update_options: collections.abc.Iterable[ + global___WorkflowExecutionOptionsUpdatedEventAttributes.WorkflowUpdateOptionsUpdate + ] + | None = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal[ - "versioning_override", b"versioning_override" + "priority", + b"priority", + "time_skipping_config", + b"time_skipping_config", + "versioning_override", + b"versioning_override", ], ) -> builtins.bool: ... def ClearField( @@ -3160,10 +3475,20 @@ class WorkflowExecutionOptionsUpdatedEventAttributes(google.protobuf.message.Mes b"attached_completion_callbacks", "attached_request_id", b"attached_request_id", + "identity", + b"identity", + "priority", + b"priority", + "time_skipping_config", + b"time_skipping_config", + "time_skipping_config_updated", + b"time_skipping_config_updated", "unset_versioning_override", b"unset_versioning_override", "versioning_override", b"versioning_override", + "workflow_update_options", + b"workflow_update_options", ], ) -> None: ... @@ -3460,6 +3785,121 @@ global___WorkflowExecutionUpdateAdmittedEventAttributes = ( WorkflowExecutionUpdateAdmittedEventAttributes ) +class WorkflowExecutionPausedEventAttributes(google.protobuf.message.Message): + """Attributes for an event marking that a workflow execution was paused.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + IDENTITY_FIELD_NUMBER: builtins.int + REASON_FIELD_NUMBER: builtins.int + REQUEST_ID_FIELD_NUMBER: builtins.int + identity: builtins.str + """The identity of the client who paused the workflow execution.""" + reason: builtins.str + """The reason for pausing the workflow execution.""" + request_id: builtins.str + """The request ID of the request that paused the workflow execution.""" + def __init__( + self, + *, + identity: builtins.str = ..., + reason: builtins.str = ..., + request_id: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "identity", b"identity", "reason", b"reason", "request_id", b"request_id" + ], + ) -> None: ... + +global___WorkflowExecutionPausedEventAttributes = WorkflowExecutionPausedEventAttributes + +class WorkflowExecutionUnpausedEventAttributes(google.protobuf.message.Message): + """Attributes for an event marking that a workflow execution was unpaused.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + IDENTITY_FIELD_NUMBER: builtins.int + REASON_FIELD_NUMBER: builtins.int + REQUEST_ID_FIELD_NUMBER: builtins.int + identity: builtins.str + """The identity of the client who unpaused the workflow execution.""" + reason: builtins.str + """The reason for unpausing the workflow execution.""" + request_id: builtins.str + """The request ID of the request that unpaused the workflow execution.""" + def __init__( + self, + *, + identity: builtins.str = ..., + reason: builtins.str = ..., + request_id: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "identity", b"identity", "reason", b"reason", "request_id", b"request_id" + ], + ) -> None: ... + +global___WorkflowExecutionUnpausedEventAttributes = ( + WorkflowExecutionUnpausedEventAttributes +) + +class WorkflowExecutionTimeSkippingTransitionedEventAttributes( + google.protobuf.message.Message +): + """Attributes for an event indicating that time skipping state changed for a workflow execution: + either time was advanced, or time skipping was stopped automatically due to the fast_forward completing. + The worker_may_ignore field in HistoryEvent should always be set true for this event. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TARGET_TIME_FIELD_NUMBER: builtins.int + DISABLED_AFTER_FAST_FORWARD_FIELD_NUMBER: builtins.int + WALL_CLOCK_TIME_FIELD_NUMBER: builtins.int + @property + def target_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """The virtual time point that time skipping advanced to.""" + disabled_after_fast_forward: builtins.bool + """When true, time skipping has been stopped automatically due to a call to fast_forward completing. + (-- api-linter: core::0140::prepositions=disabled + aip.dev/not-precedent: "after" is used to indicate temporal ordering. --) + """ + @property + def wall_clock_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """The wall-clock time when the time-skipping state changed event was generated.""" + def __init__( + self, + *, + target_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + disabled_after_fast_forward: builtins.bool = ..., + wall_clock_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "target_time", b"target_time", "wall_clock_time", b"wall_clock_time" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "disabled_after_fast_forward", + b"disabled_after_fast_forward", + "target_time", + b"target_time", + "wall_clock_time", + b"wall_clock_time", + ], + ) -> None: ... + +global___WorkflowExecutionTimeSkippingTransitionedEventAttributes = ( + WorkflowExecutionTimeSkippingTransitionedEventAttributes +) + class NexusOperationScheduledEventAttributes(google.protobuf.message.Message): """Event marking that an operation was scheduled by a workflow via the ScheduleNexusOperation command.""" @@ -3492,6 +3932,8 @@ class NexusOperationScheduledEventAttributes(google.protobuf.message.Message): WORKFLOW_TASK_COMPLETED_EVENT_ID_FIELD_NUMBER: builtins.int REQUEST_ID_FIELD_NUMBER: builtins.int ENDPOINT_ID_FIELD_NUMBER: builtins.int + SCHEDULE_TO_START_TIMEOUT_FIELD_NUMBER: builtins.int + START_TO_CLOSE_TIMEOUT_FIELD_NUMBER: builtins.int endpoint: builtins.str """Endpoint name, must exist in the endpoint registry.""" service: builtins.str @@ -3511,6 +3953,8 @@ class NexusOperationScheduledEventAttributes(google.protobuf.message.Message): Calls are retried internally by the server. (-- api-linter: core::0140::prepositions=disabled aip.dev/not-precedent: "to" is used to indicate interval. --) + (-- api-linter: core::0142::time-field-names=disabled + aip.dev/not-precedent: "timeout" is an acceptable suffix for duration fields in this API. --) """ @property def nexus_header( @@ -3531,6 +3975,20 @@ class NexusOperationScheduledEventAttributes(google.protobuf.message.Message): This is stored on the event and used internally by the server in case the endpoint is renamed from the time the event was originally scheduled. """ + @property + def schedule_to_start_timeout(self) -> google.protobuf.duration_pb2.Duration: + """Schedule-to-start timeout for this operation. + See ScheduleNexusOperationCommandAttributes.schedule_to_start_timeout for details. + (-- api-linter: core::0140::prepositions=disabled + aip.dev/not-precedent: "to" is used to indicate interval. --) + """ + @property + def start_to_close_timeout(self) -> google.protobuf.duration_pb2.Duration: + """Start-to-close timeout for this operation. + See ScheduleNexusOperationCommandAttributes.start_to_close_timeout for details. + (-- api-linter: core::0140::prepositions=disabled + aip.dev/not-precedent: "to" is used to indicate interval. --) + """ def __init__( self, *, @@ -3543,11 +4001,20 @@ class NexusOperationScheduledEventAttributes(google.protobuf.message.Message): workflow_task_completed_event_id: builtins.int = ..., request_id: builtins.str = ..., endpoint_id: builtins.str = ..., + schedule_to_start_timeout: google.protobuf.duration_pb2.Duration | None = ..., + start_to_close_timeout: google.protobuf.duration_pb2.Duration | None = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal[ - "input", b"input", "schedule_to_close_timeout", b"schedule_to_close_timeout" + "input", + b"input", + "schedule_to_close_timeout", + b"schedule_to_close_timeout", + "schedule_to_start_timeout", + b"schedule_to_start_timeout", + "start_to_close_timeout", + b"start_to_close_timeout", ], ) -> builtins.bool: ... def ClearField( @@ -3567,8 +4034,12 @@ class NexusOperationScheduledEventAttributes(google.protobuf.message.Message): b"request_id", "schedule_to_close_timeout", b"schedule_to_close_timeout", + "schedule_to_start_timeout", + b"schedule_to_start_timeout", "service", b"service", + "start_to_close_timeout", + b"start_to_close_timeout", "workflow_task_completed_event_id", b"workflow_task_completed_event_id", ], @@ -3917,6 +4388,8 @@ class HistoryEvent(google.protobuf.message.Message): WORKER_MAY_IGNORE_FIELD_NUMBER: builtins.int USER_METADATA_FIELD_NUMBER: builtins.int LINKS_FIELD_NUMBER: builtins.int + PRINCIPAL_FIELD_NUMBER: builtins.int + EVENT_GROUP_MARKERS_FIELD_NUMBER: builtins.int WORKFLOW_EXECUTION_STARTED_EVENT_ATTRIBUTES_FIELD_NUMBER: builtins.int WORKFLOW_EXECUTION_COMPLETED_EVENT_ATTRIBUTES_FIELD_NUMBER: builtins.int WORKFLOW_EXECUTION_FAILED_EVENT_ATTRIBUTES_FIELD_NUMBER: builtins.int @@ -3982,15 +4455,24 @@ class HistoryEvent(google.protobuf.message.Message): WORKFLOW_EXECUTION_OPTIONS_UPDATED_EVENT_ATTRIBUTES_FIELD_NUMBER: builtins.int NEXUS_OPERATION_CANCEL_REQUEST_COMPLETED_EVENT_ATTRIBUTES_FIELD_NUMBER: builtins.int NEXUS_OPERATION_CANCEL_REQUEST_FAILED_EVENT_ATTRIBUTES_FIELD_NUMBER: builtins.int + WORKFLOW_EXECUTION_PAUSED_EVENT_ATTRIBUTES_FIELD_NUMBER: builtins.int + WORKFLOW_EXECUTION_UNPAUSED_EVENT_ATTRIBUTES_FIELD_NUMBER: builtins.int + WORKFLOW_EXECUTION_TIME_SKIPPING_TRANSITIONED_EVENT_ATTRIBUTES_FIELD_NUMBER: ( + builtins.int + ) event_id: builtins.int """Monotonically increasing event number, starts at 1.""" @property def event_time(self) -> google.protobuf.timestamp_pb2.Timestamp: ... event_type: temporalio.api.enums.v1.event_type_pb2.EventType.ValueType version: builtins.int - """TODO: What is this? Appears unused by SDKs""" + """Failover version of the event, used by the server for multi-cluster replication and history + versioning. SDKs generally ignore this field. + """ task_id: builtins.int - """TODO: What is this? Appears unused by SDKs""" + """Identifier used by the service to order replication and transfer tasks associated with this + event. SDKs generally ignore this field. + """ worker_may_ignore: builtins.bool """Set to true when the SDK may ignore the event as it does not impact workflow state or information in any way that the SDK need be concerned with. If an SDK encounters an event @@ -4014,7 +4496,17 @@ class HistoryEvent(google.protobuf.message.Message): ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ temporalio.api.common.v1.message_pb2.Link ]: - """Links associated with the event.""" + """Links to related entities, such as the entity that started this event's workflow.""" + @property + def principal(self) -> temporalio.api.common.v1.message_pb2.Principal: + """Server-computed authenticated caller identity associated with this event.""" + @property + def event_group_markers( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.sdk.v1.event_group_marker_pb2.EventGroupMarker + ]: + """Event group markers attached to this event.""" @property def workflow_execution_started_event_attributes( self, @@ -4241,6 +4733,18 @@ class HistoryEvent(google.protobuf.message.Message): def nexus_operation_cancel_request_failed_event_attributes( self, ) -> global___NexusOperationCancelRequestFailedEventAttributes: ... + @property + def workflow_execution_paused_event_attributes( + self, + ) -> global___WorkflowExecutionPausedEventAttributes: ... + @property + def workflow_execution_unpaused_event_attributes( + self, + ) -> global___WorkflowExecutionUnpausedEventAttributes: ... + @property + def workflow_execution_time_skipping_transitioned_event_attributes( + self, + ) -> global___WorkflowExecutionTimeSkippingTransitionedEventAttributes: ... def __init__( self, *, @@ -4254,6 +4758,11 @@ class HistoryEvent(google.protobuf.message.Message): | None = ..., links: collections.abc.Iterable[temporalio.api.common.v1.message_pb2.Link] | None = ..., + principal: temporalio.api.common.v1.message_pb2.Principal | None = ..., + event_group_markers: collections.abc.Iterable[ + temporalio.api.sdk.v1.event_group_marker_pb2.EventGroupMarker + ] + | None = ..., workflow_execution_started_event_attributes: global___WorkflowExecutionStartedEventAttributes | None = ..., workflow_execution_completed_event_attributes: global___WorkflowExecutionCompletedEventAttributes @@ -4367,6 +4876,12 @@ class HistoryEvent(google.protobuf.message.Message): | None = ..., nexus_operation_cancel_request_failed_event_attributes: global___NexusOperationCancelRequestFailedEventAttributes | None = ..., + workflow_execution_paused_event_attributes: global___WorkflowExecutionPausedEventAttributes + | None = ..., + workflow_execution_unpaused_event_attributes: global___WorkflowExecutionUnpausedEventAttributes + | None = ..., + workflow_execution_time_skipping_transitioned_event_attributes: global___WorkflowExecutionTimeSkippingTransitionedEventAttributes + | None = ..., ) -> None: ... def HasField( self, @@ -4427,6 +4942,8 @@ class HistoryEvent(google.protobuf.message.Message): b"nexus_operation_started_event_attributes", "nexus_operation_timed_out_event_attributes", b"nexus_operation_timed_out_event_attributes", + "principal", + b"principal", "request_cancel_external_workflow_execution_failed_event_attributes", b"request_cancel_external_workflow_execution_failed_event_attributes", "request_cancel_external_workflow_execution_initiated_event_attributes", @@ -4461,14 +4978,20 @@ class HistoryEvent(google.protobuf.message.Message): b"workflow_execution_failed_event_attributes", "workflow_execution_options_updated_event_attributes", b"workflow_execution_options_updated_event_attributes", + "workflow_execution_paused_event_attributes", + b"workflow_execution_paused_event_attributes", "workflow_execution_signaled_event_attributes", b"workflow_execution_signaled_event_attributes", "workflow_execution_started_event_attributes", b"workflow_execution_started_event_attributes", "workflow_execution_terminated_event_attributes", b"workflow_execution_terminated_event_attributes", + "workflow_execution_time_skipping_transitioned_event_attributes", + b"workflow_execution_time_skipping_transitioned_event_attributes", "workflow_execution_timed_out_event_attributes", b"workflow_execution_timed_out_event_attributes", + "workflow_execution_unpaused_event_attributes", + b"workflow_execution_unpaused_event_attributes", "workflow_execution_update_accepted_event_attributes", b"workflow_execution_update_accepted_event_attributes", "workflow_execution_update_admitted_event_attributes", @@ -4526,6 +5049,8 @@ class HistoryEvent(google.protobuf.message.Message): b"child_workflow_execution_terminated_event_attributes", "child_workflow_execution_timed_out_event_attributes", b"child_workflow_execution_timed_out_event_attributes", + "event_group_markers", + b"event_group_markers", "event_id", b"event_id", "event_time", @@ -4558,6 +5083,8 @@ class HistoryEvent(google.protobuf.message.Message): b"nexus_operation_started_event_attributes", "nexus_operation_timed_out_event_attributes", b"nexus_operation_timed_out_event_attributes", + "principal", + b"principal", "request_cancel_external_workflow_execution_failed_event_attributes", b"request_cancel_external_workflow_execution_failed_event_attributes", "request_cancel_external_workflow_execution_initiated_event_attributes", @@ -4598,14 +5125,20 @@ class HistoryEvent(google.protobuf.message.Message): b"workflow_execution_failed_event_attributes", "workflow_execution_options_updated_event_attributes", b"workflow_execution_options_updated_event_attributes", + "workflow_execution_paused_event_attributes", + b"workflow_execution_paused_event_attributes", "workflow_execution_signaled_event_attributes", b"workflow_execution_signaled_event_attributes", "workflow_execution_started_event_attributes", b"workflow_execution_started_event_attributes", "workflow_execution_terminated_event_attributes", b"workflow_execution_terminated_event_attributes", + "workflow_execution_time_skipping_transitioned_event_attributes", + b"workflow_execution_time_skipping_transitioned_event_attributes", "workflow_execution_timed_out_event_attributes", b"workflow_execution_timed_out_event_attributes", + "workflow_execution_unpaused_event_attributes", + b"workflow_execution_unpaused_event_attributes", "workflow_execution_update_accepted_event_attributes", b"workflow_execution_update_accepted_event_attributes", "workflow_execution_update_admitted_event_attributes", @@ -4691,6 +5224,9 @@ class HistoryEvent(google.protobuf.message.Message): "workflow_execution_options_updated_event_attributes", "nexus_operation_cancel_request_completed_event_attributes", "nexus_operation_cancel_request_failed_event_attributes", + "workflow_execution_paused_event_attributes", + "workflow_execution_unpaused_event_attributes", + "workflow_execution_time_skipping_transitioned_event_attributes", ] | None ): ... diff --git a/temporalio/api/namespace/v1/message_pb2.py b/temporalio/api/namespace/v1/message_pb2.py index 06b0fd956..43a3773f2 100644 --- a/temporalio/api/namespace/v1/message_pb2.py +++ b/temporalio/api/namespace/v1/message_pb2.py @@ -22,13 +22,14 @@ ) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n\'temporal/api/namespace/v1/message.proto\x12\x19temporal.api.namespace.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a%temporal/api/enums/v1/namespace.proto"\xba\x03\n\rNamespaceInfo\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x34\n\x05state\x18\x02 \x01(\x0e\x32%.temporal.api.enums.v1.NamespaceState\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\x12\x13\n\x0bowner_email\x18\x04 \x01(\t\x12@\n\x04\x64\x61ta\x18\x05 \x03(\x0b\x32\x32.temporal.api.namespace.v1.NamespaceInfo.DataEntry\x12\n\n\x02id\x18\x06 \x01(\t\x12K\n\x0c\x63\x61pabilities\x18\x07 \x01(\x0b\x32\x35.temporal.api.namespace.v1.NamespaceInfo.Capabilities\x12\x1a\n\x12supports_schedules\x18\x64 \x01(\x08\x1a+\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1aW\n\x0c\x43\x61pabilities\x12\x1c\n\x14\x65\x61ger_workflow_start\x18\x01 \x01(\x08\x12\x13\n\x0bsync_update\x18\x02 \x01(\x08\x12\x14\n\x0c\x61sync_update\x18\x03 \x01(\x08"\x9e\x04\n\x0fNamespaceConfig\x12\x43\n workflow_execution_retention_ttl\x18\x01 \x01(\x0b\x32\x19.google.protobuf.Duration\x12<\n\x0c\x62\x61\x64_binaries\x18\x02 \x01(\x0b\x32&.temporal.api.namespace.v1.BadBinaries\x12\x44\n\x16history_archival_state\x18\x03 \x01(\x0e\x32$.temporal.api.enums.v1.ArchivalState\x12\x1c\n\x14history_archival_uri\x18\x04 \x01(\t\x12G\n\x19visibility_archival_state\x18\x05 \x01(\x0e\x32$.temporal.api.enums.v1.ArchivalState\x12\x1f\n\x17visibility_archival_uri\x18\x06 \x01(\t\x12u\n\x1f\x63ustom_search_attribute_aliases\x18\x07 \x03(\x0b\x32L.temporal.api.namespace.v1.NamespaceConfig.CustomSearchAttributeAliasesEntry\x1a\x43\n!CustomSearchAttributeAliasesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"\xb0\x01\n\x0b\x42\x61\x64\x42inaries\x12\x46\n\x08\x62inaries\x18\x01 \x03(\x0b\x32\x34.temporal.api.namespace.v1.BadBinaries.BinariesEntry\x1aY\n\rBinariesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x37\n\x05value\x18\x02 \x01(\x0b\x32(.temporal.api.namespace.v1.BadBinaryInfo:\x02\x38\x01"b\n\rBadBinaryInfo\x12\x0e\n\x06reason\x18\x01 \x01(\t\x12\x10\n\x08operator\x18\x02 \x01(\t\x12/\n\x0b\x63reate_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"\xea\x01\n\x13UpdateNamespaceInfo\x12\x13\n\x0b\x64\x65scription\x18\x01 \x01(\t\x12\x13\n\x0bowner_email\x18\x02 \x01(\t\x12\x46\n\x04\x64\x61ta\x18\x03 \x03(\x0b\x32\x38.temporal.api.namespace.v1.UpdateNamespaceInfo.DataEntry\x12\x34\n\x05state\x18\x04 \x01(\x0e\x32%.temporal.api.enums.v1.NamespaceState\x1a+\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"*\n\x0fNamespaceFilter\x12\x17\n\x0finclude_deleted\x18\x01 \x01(\x08\x42\x98\x01\n\x1cio.temporal.api.namespace.v1B\x0cMessageProtoP\x01Z)go.temporal.io/api/namespace/v1;namespace\xaa\x02\x1bTemporalio.Api.Namespace.V1\xea\x02\x1eTemporalio::Api::Namespace::V1b\x06proto3' + b'\n\'temporal/api/namespace/v1/message.proto\x12\x19temporal.api.namespace.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a%temporal/api/enums/v1/namespace.proto"\xf6\x08\n\rNamespaceInfo\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x34\n\x05state\x18\x02 \x01(\x0e\x32%.temporal.api.enums.v1.NamespaceState\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\x12\x13\n\x0bowner_email\x18\x04 \x01(\t\x12@\n\x04\x64\x61ta\x18\x05 \x03(\x0b\x32\x32.temporal.api.namespace.v1.NamespaceInfo.DataEntry\x12\n\n\x02id\x18\x06 \x01(\t\x12K\n\x0c\x63\x61pabilities\x18\x07 \x01(\x0b\x32\x35.temporal.api.namespace.v1.NamespaceInfo.Capabilities\x12?\n\x06limits\x18\x08 \x01(\x0b\x32/.temporal.api.namespace.v1.NamespaceInfo.Limits\x12\x1a\n\x12supports_schedules\x18\x64 \x01(\x08\x1a+\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a\xd6\x04\n\x0c\x43\x61pabilities\x12\x1c\n\x14\x65\x61ger_workflow_start\x18\x01 \x01(\x08\x12\x13\n\x0bsync_update\x18\x02 \x01(\x08\x12\x14\n\x0c\x61sync_update\x18\x03 \x01(\x08\x12\x19\n\x11worker_heartbeats\x18\x04 \x01(\x08\x12*\n"reported_problems_search_attribute\x18\x05 \x01(\x08\x12\x16\n\x0eworkflow_pause\x18\x06 \x01(\x08\x12\x1d\n\x15standalone_activities\x18\x07 \x01(\x08\x12(\n worker_poll_complete_on_shutdown\x18\x08 \x01(\x08\x12\x1a\n\x12poller_autoscaling\x18\t \x01(\x08\x12\x17\n\x0fworker_commands\x18\n \x01(\x08\x12"\n\x1astandalone_nexus_operation\x18\x0b \x01(\x08\x12!\n\x19workflow_update_callbacks\x18\x0c \x01(\x08\x12&\n\x1epoller_autoscaling_auto_enroll\x18\r \x01(\x08\x12+\n#workflow_task_completion_pagination\x18\x0e \x01(\x08\x12\'\n\x1fstandalone_activity_start_delay\x18\x0f \x01(\x08\x12,\n$standalone_activity_batch_operations\x18\x10 \x01(\x08\x12-\n%standalone_activity_operator_commands\x18\x11 \x01(\x08\x1ay\n\x06Limits\x12\x1d\n\x15\x62lob_size_limit_error\x18\x01 \x01(\x03\x12\x1d\n\x15memo_size_limit_error\x18\x02 \x01(\x03\x12\x31\n)workflow_task_completion_size_limit_error\x18\x03 \x01(\x03"\x9e\x04\n\x0fNamespaceConfig\x12\x43\n workflow_execution_retention_ttl\x18\x01 \x01(\x0b\x32\x19.google.protobuf.Duration\x12<\n\x0c\x62\x61\x64_binaries\x18\x02 \x01(\x0b\x32&.temporal.api.namespace.v1.BadBinaries\x12\x44\n\x16history_archival_state\x18\x03 \x01(\x0e\x32$.temporal.api.enums.v1.ArchivalState\x12\x1c\n\x14history_archival_uri\x18\x04 \x01(\t\x12G\n\x19visibility_archival_state\x18\x05 \x01(\x0e\x32$.temporal.api.enums.v1.ArchivalState\x12\x1f\n\x17visibility_archival_uri\x18\x06 \x01(\t\x12u\n\x1f\x63ustom_search_attribute_aliases\x18\x07 \x03(\x0b\x32L.temporal.api.namespace.v1.NamespaceConfig.CustomSearchAttributeAliasesEntry\x1a\x43\n!CustomSearchAttributeAliasesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"\xb0\x01\n\x0b\x42\x61\x64\x42inaries\x12\x46\n\x08\x62inaries\x18\x01 \x03(\x0b\x32\x34.temporal.api.namespace.v1.BadBinaries.BinariesEntry\x1aY\n\rBinariesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x37\n\x05value\x18\x02 \x01(\x0b\x32(.temporal.api.namespace.v1.BadBinaryInfo:\x02\x38\x01"b\n\rBadBinaryInfo\x12\x0e\n\x06reason\x18\x01 \x01(\t\x12\x10\n\x08operator\x18\x02 \x01(\t\x12/\n\x0b\x63reate_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"\xea\x01\n\x13UpdateNamespaceInfo\x12\x13\n\x0b\x64\x65scription\x18\x01 \x01(\t\x12\x13\n\x0bowner_email\x18\x02 \x01(\t\x12\x46\n\x04\x64\x61ta\x18\x03 \x03(\x0b\x32\x38.temporal.api.namespace.v1.UpdateNamespaceInfo.DataEntry\x12\x34\n\x05state\x18\x04 \x01(\x0e\x32%.temporal.api.enums.v1.NamespaceState\x1a+\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"*\n\x0fNamespaceFilter\x12\x17\n\x0finclude_deleted\x18\x01 \x01(\x08\x42\x98\x01\n\x1cio.temporal.api.namespace.v1B\x0cMessageProtoP\x01Z)go.temporal.io/api/namespace/v1;namespace\xaa\x02\x1bTemporalio.Api.Namespace.V1\xea\x02\x1eTemporalio::Api::Namespace::V1b\x06proto3' ) _NAMESPACEINFO = DESCRIPTOR.message_types_by_name["NamespaceInfo"] _NAMESPACEINFO_DATAENTRY = _NAMESPACEINFO.nested_types_by_name["DataEntry"] _NAMESPACEINFO_CAPABILITIES = _NAMESPACEINFO.nested_types_by_name["Capabilities"] +_NAMESPACEINFO_LIMITS = _NAMESPACEINFO.nested_types_by_name["Limits"] _NAMESPACECONFIG = DESCRIPTOR.message_types_by_name["NamespaceConfig"] _NAMESPACECONFIG_CUSTOMSEARCHATTRIBUTEALIASESENTRY = ( _NAMESPACECONFIG.nested_types_by_name["CustomSearchAttributeAliasesEntry"] @@ -48,7 +49,7 @@ (_message.Message,), { "DESCRIPTOR": _NAMESPACEINFO_DATAENTRY, - "__module__": "temporal.api.namespace.v1.message_pb2", + "__module__": "temporalio.api.namespace.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.namespace.v1.NamespaceInfo.DataEntry) }, ), @@ -57,18 +58,28 @@ (_message.Message,), { "DESCRIPTOR": _NAMESPACEINFO_CAPABILITIES, - "__module__": "temporal.api.namespace.v1.message_pb2", + "__module__": "temporalio.api.namespace.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.namespace.v1.NamespaceInfo.Capabilities) }, ), + "Limits": _reflection.GeneratedProtocolMessageType( + "Limits", + (_message.Message,), + { + "DESCRIPTOR": _NAMESPACEINFO_LIMITS, + "__module__": "temporalio.api.namespace.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.namespace.v1.NamespaceInfo.Limits) + }, + ), "DESCRIPTOR": _NAMESPACEINFO, - "__module__": "temporal.api.namespace.v1.message_pb2", + "__module__": "temporalio.api.namespace.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.namespace.v1.NamespaceInfo) }, ) _sym_db.RegisterMessage(NamespaceInfo) _sym_db.RegisterMessage(NamespaceInfo.DataEntry) _sym_db.RegisterMessage(NamespaceInfo.Capabilities) +_sym_db.RegisterMessage(NamespaceInfo.Limits) NamespaceConfig = _reflection.GeneratedProtocolMessageType( "NamespaceConfig", @@ -79,12 +90,12 @@ (_message.Message,), { "DESCRIPTOR": _NAMESPACECONFIG_CUSTOMSEARCHATTRIBUTEALIASESENTRY, - "__module__": "temporal.api.namespace.v1.message_pb2", + "__module__": "temporalio.api.namespace.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.namespace.v1.NamespaceConfig.CustomSearchAttributeAliasesEntry) }, ), "DESCRIPTOR": _NAMESPACECONFIG, - "__module__": "temporal.api.namespace.v1.message_pb2", + "__module__": "temporalio.api.namespace.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.namespace.v1.NamespaceConfig) }, ) @@ -100,12 +111,12 @@ (_message.Message,), { "DESCRIPTOR": _BADBINARIES_BINARIESENTRY, - "__module__": "temporal.api.namespace.v1.message_pb2", + "__module__": "temporalio.api.namespace.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.namespace.v1.BadBinaries.BinariesEntry) }, ), "DESCRIPTOR": _BADBINARIES, - "__module__": "temporal.api.namespace.v1.message_pb2", + "__module__": "temporalio.api.namespace.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.namespace.v1.BadBinaries) }, ) @@ -117,7 +128,7 @@ (_message.Message,), { "DESCRIPTOR": _BADBINARYINFO, - "__module__": "temporal.api.namespace.v1.message_pb2", + "__module__": "temporalio.api.namespace.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.namespace.v1.BadBinaryInfo) }, ) @@ -132,12 +143,12 @@ (_message.Message,), { "DESCRIPTOR": _UPDATENAMESPACEINFO_DATAENTRY, - "__module__": "temporal.api.namespace.v1.message_pb2", + "__module__": "temporalio.api.namespace.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.namespace.v1.UpdateNamespaceInfo.DataEntry) }, ), "DESCRIPTOR": _UPDATENAMESPACEINFO, - "__module__": "temporal.api.namespace.v1.message_pb2", + "__module__": "temporalio.api.namespace.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.namespace.v1.UpdateNamespaceInfo) }, ) @@ -149,7 +160,7 @@ (_message.Message,), { "DESCRIPTOR": _NAMESPACEFILTER, - "__module__": "temporal.api.namespace.v1.message_pb2", + "__module__": "temporalio.api.namespace.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.namespace.v1.NamespaceFilter) }, ) @@ -167,25 +178,27 @@ _UPDATENAMESPACEINFO_DATAENTRY._options = None _UPDATENAMESPACEINFO_DATAENTRY._serialized_options = b"8\001" _NAMESPACEINFO._serialized_start = 175 - _NAMESPACEINFO._serialized_end = 617 - _NAMESPACEINFO_DATAENTRY._serialized_start = 485 - _NAMESPACEINFO_DATAENTRY._serialized_end = 528 - _NAMESPACEINFO_CAPABILITIES._serialized_start = 530 - _NAMESPACEINFO_CAPABILITIES._serialized_end = 617 - _NAMESPACECONFIG._serialized_start = 620 - _NAMESPACECONFIG._serialized_end = 1162 - _NAMESPACECONFIG_CUSTOMSEARCHATTRIBUTEALIASESENTRY._serialized_start = 1095 - _NAMESPACECONFIG_CUSTOMSEARCHATTRIBUTEALIASESENTRY._serialized_end = 1162 - _BADBINARIES._serialized_start = 1165 - _BADBINARIES._serialized_end = 1341 - _BADBINARIES_BINARIESENTRY._serialized_start = 1252 - _BADBINARIES_BINARIESENTRY._serialized_end = 1341 - _BADBINARYINFO._serialized_start = 1343 - _BADBINARYINFO._serialized_end = 1441 - _UPDATENAMESPACEINFO._serialized_start = 1444 - _UPDATENAMESPACEINFO._serialized_end = 1678 - _UPDATENAMESPACEINFO_DATAENTRY._serialized_start = 485 - _UPDATENAMESPACEINFO_DATAENTRY._serialized_end = 528 - _NAMESPACEFILTER._serialized_start = 1680 - _NAMESPACEFILTER._serialized_end = 1722 + _NAMESPACEINFO._serialized_end = 1317 + _NAMESPACEINFO_DATAENTRY._serialized_start = 550 + _NAMESPACEINFO_DATAENTRY._serialized_end = 593 + _NAMESPACEINFO_CAPABILITIES._serialized_start = 596 + _NAMESPACEINFO_CAPABILITIES._serialized_end = 1194 + _NAMESPACEINFO_LIMITS._serialized_start = 1196 + _NAMESPACEINFO_LIMITS._serialized_end = 1317 + _NAMESPACECONFIG._serialized_start = 1320 + _NAMESPACECONFIG._serialized_end = 1862 + _NAMESPACECONFIG_CUSTOMSEARCHATTRIBUTEALIASESENTRY._serialized_start = 1795 + _NAMESPACECONFIG_CUSTOMSEARCHATTRIBUTEALIASESENTRY._serialized_end = 1862 + _BADBINARIES._serialized_start = 1865 + _BADBINARIES._serialized_end = 2041 + _BADBINARIES_BINARIESENTRY._serialized_start = 1952 + _BADBINARIES_BINARIESENTRY._serialized_end = 2041 + _BADBINARYINFO._serialized_start = 2043 + _BADBINARYINFO._serialized_end = 2141 + _UPDATENAMESPACEINFO._serialized_start = 2144 + _UPDATENAMESPACEINFO._serialized_end = 2378 + _UPDATENAMESPACEINFO_DATAENTRY._serialized_start = 550 + _UPDATENAMESPACEINFO_DATAENTRY._serialized_end = 593 + _NAMESPACEFILTER._serialized_start = 2380 + _NAMESPACEFILTER._serialized_end = 2422 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/namespace/v1/message_pb2.pyi b/temporalio/api/namespace/v1/message_pb2.pyi index 831972b79..06b1546b3 100644 --- a/temporalio/api/namespace/v1/message_pb2.pyi +++ b/temporalio/api/namespace/v1/message_pb2.pyi @@ -51,18 +51,79 @@ class NamespaceInfo(google.protobuf.message.Message): EAGER_WORKFLOW_START_FIELD_NUMBER: builtins.int SYNC_UPDATE_FIELD_NUMBER: builtins.int ASYNC_UPDATE_FIELD_NUMBER: builtins.int + WORKER_HEARTBEATS_FIELD_NUMBER: builtins.int + REPORTED_PROBLEMS_SEARCH_ATTRIBUTE_FIELD_NUMBER: builtins.int + WORKFLOW_PAUSE_FIELD_NUMBER: builtins.int + STANDALONE_ACTIVITIES_FIELD_NUMBER: builtins.int + WORKER_POLL_COMPLETE_ON_SHUTDOWN_FIELD_NUMBER: builtins.int + POLLER_AUTOSCALING_FIELD_NUMBER: builtins.int + WORKER_COMMANDS_FIELD_NUMBER: builtins.int + STANDALONE_NEXUS_OPERATION_FIELD_NUMBER: builtins.int + WORKFLOW_UPDATE_CALLBACKS_FIELD_NUMBER: builtins.int + POLLER_AUTOSCALING_AUTO_ENROLL_FIELD_NUMBER: builtins.int + WORKFLOW_TASK_COMPLETION_PAGINATION_FIELD_NUMBER: builtins.int + STANDALONE_ACTIVITY_START_DELAY_FIELD_NUMBER: builtins.int + STANDALONE_ACTIVITY_BATCH_OPERATIONS_FIELD_NUMBER: builtins.int + STANDALONE_ACTIVITY_OPERATOR_COMMANDS_FIELD_NUMBER: builtins.int eager_workflow_start: builtins.bool """True if the namespace supports eager workflow start.""" sync_update: builtins.bool """True if the namespace supports sync update""" async_update: builtins.bool """True if the namespace supports async update""" + worker_heartbeats: builtins.bool + """True if the namespace supports worker heartbeats""" + reported_problems_search_attribute: builtins.bool + """True if the namespace supports reported problems search attribute""" + workflow_pause: builtins.bool + """True if the namespace supports pausing workflows""" + standalone_activities: builtins.bool + """True if the namespace supports standalone activities""" + worker_poll_complete_on_shutdown: builtins.bool + """True if the namespace supports server-side completion of outstanding worker polls on shutdown. + When enabled, the server will complete polls for workers that send WorkerInstanceKey in their + poll requests and call ShutdownWorker with the same WorkerInstanceKey. The poll will return + an empty response. When this flag is true, workers should allow polls to return gracefully + rather than terminating any open polls on shutdown. + """ + poller_autoscaling: builtins.bool + """True if the namespace supports poller autoscaling""" + worker_commands: builtins.bool + """True if the namespace supports worker commands (server-to-worker communication via control queues).""" + standalone_nexus_operation: builtins.bool + """True if the namespace supports standalone Nexus operations.""" + workflow_update_callbacks: builtins.bool + """True if the namespace supports attaching callbacks on workflow updates""" + poller_autoscaling_auto_enroll: builtins.bool + """When true, workers should use poller autoscaling by default unless explicitly configured otherwise.""" + workflow_task_completion_pagination: builtins.bool + """True if the namespace supports pagination of `RespondWorkflowTaskCompleted` request.""" + standalone_activity_start_delay: builtins.bool + """True if the namespace supports start delay for standalone activities.""" + standalone_activity_batch_operations: builtins.bool + """True if the namespace supports batch operations for standalone activities.""" + standalone_activity_operator_commands: builtins.bool + """True if the namespace supports standalone activity operator commands.""" def __init__( self, *, eager_workflow_start: builtins.bool = ..., sync_update: builtins.bool = ..., async_update: builtins.bool = ..., + worker_heartbeats: builtins.bool = ..., + reported_problems_search_attribute: builtins.bool = ..., + workflow_pause: builtins.bool = ..., + standalone_activities: builtins.bool = ..., + worker_poll_complete_on_shutdown: builtins.bool = ..., + poller_autoscaling: builtins.bool = ..., + worker_commands: builtins.bool = ..., + standalone_nexus_operation: builtins.bool = ..., + workflow_update_callbacks: builtins.bool = ..., + poller_autoscaling_auto_enroll: builtins.bool = ..., + workflow_task_completion_pagination: builtins.bool = ..., + standalone_activity_start_delay: builtins.bool = ..., + standalone_activity_batch_operations: builtins.bool = ..., + standalone_activity_operator_commands: builtins.bool = ..., ) -> None: ... def ClearField( self, @@ -71,8 +132,73 @@ class NamespaceInfo(google.protobuf.message.Message): b"async_update", "eager_workflow_start", b"eager_workflow_start", + "poller_autoscaling", + b"poller_autoscaling", + "poller_autoscaling_auto_enroll", + b"poller_autoscaling_auto_enroll", + "reported_problems_search_attribute", + b"reported_problems_search_attribute", + "standalone_activities", + b"standalone_activities", + "standalone_activity_batch_operations", + b"standalone_activity_batch_operations", + "standalone_activity_operator_commands", + b"standalone_activity_operator_commands", + "standalone_activity_start_delay", + b"standalone_activity_start_delay", + "standalone_nexus_operation", + b"standalone_nexus_operation", "sync_update", b"sync_update", + "worker_commands", + b"worker_commands", + "worker_heartbeats", + b"worker_heartbeats", + "worker_poll_complete_on_shutdown", + b"worker_poll_complete_on_shutdown", + "workflow_pause", + b"workflow_pause", + "workflow_task_completion_pagination", + b"workflow_task_completion_pagination", + "workflow_update_callbacks", + b"workflow_update_callbacks", + ], + ) -> None: ... + + class Limits(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + BLOB_SIZE_LIMIT_ERROR_FIELD_NUMBER: builtins.int + MEMO_SIZE_LIMIT_ERROR_FIELD_NUMBER: builtins.int + WORKFLOW_TASK_COMPLETION_SIZE_LIMIT_ERROR_FIELD_NUMBER: builtins.int + blob_size_limit_error: builtins.int + """Maximum size in bytes for payload fields in workflow history events + (e.g., workflow/activity inputs and results, failure details, signal payloads). + When exceeded, the server will reject the operation with an error. + """ + memo_size_limit_error: builtins.int + """Maximum total memo size in bytes per workflow execution.""" + workflow_task_completion_size_limit_error: builtins.int + """Maximum total size in bytes of a single RespondWorkflowTaskCompleted request. + Requests exceeding this fail the workflow task with + WORKFLOW_TASK_FAILED_CAUSE_REQUEST_TOO_LARGE. 0 means no explicit limit. + """ + def __init__( + self, + *, + blob_size_limit_error: builtins.int = ..., + memo_size_limit_error: builtins.int = ..., + workflow_task_completion_size_limit_error: builtins.int = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "blob_size_limit_error", + b"blob_size_limit_error", + "memo_size_limit_error", + b"memo_size_limit_error", + "workflow_task_completion_size_limit_error", + b"workflow_task_completion_size_limit_error", ], ) -> None: ... @@ -83,6 +209,7 @@ class NamespaceInfo(google.protobuf.message.Message): DATA_FIELD_NUMBER: builtins.int ID_FIELD_NUMBER: builtins.int CAPABILITIES_FIELD_NUMBER: builtins.int + LIMITS_FIELD_NUMBER: builtins.int SUPPORTS_SCHEDULES_FIELD_NUMBER: builtins.int name: builtins.str state: temporalio.api.enums.v1.namespace_pb2.NamespaceState.ValueType @@ -97,6 +224,9 @@ class NamespaceInfo(google.protobuf.message.Message): @property def capabilities(self) -> global___NamespaceInfo.Capabilities: """All capabilities the namespace supports.""" + @property + def limits(self) -> global___NamespaceInfo.Limits: + """Namespace configured limits""" supports_schedules: builtins.bool """Whether scheduled workflows are supported on this namespace. This is only needed temporarily while the feature is experimental, so we can give it a high tag. @@ -111,10 +241,14 @@ class NamespaceInfo(google.protobuf.message.Message): data: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., id: builtins.str = ..., capabilities: global___NamespaceInfo.Capabilities | None = ..., + limits: global___NamespaceInfo.Limits | None = ..., supports_schedules: builtins.bool = ..., ) -> None: ... def HasField( - self, field_name: typing_extensions.Literal["capabilities", b"capabilities"] + self, + field_name: typing_extensions.Literal[ + "capabilities", b"capabilities", "limits", b"limits" + ], ) -> builtins.bool: ... def ClearField( self, @@ -127,6 +261,8 @@ class NamespaceInfo(google.protobuf.message.Message): b"description", "id", b"id", + "limits", + b"limits", "name", b"name", "owner_email", diff --git a/temporalio/api/nexus/v1/__init__.py b/temporalio/api/nexus/v1/__init__.py index 7ae90d590..b85ea67f4 100644 --- a/temporalio/api/nexus/v1/__init__.py +++ b/temporalio/api/nexus/v1/__init__.py @@ -7,6 +7,9 @@ Failure, HandlerError, Link, + NexusOperationExecutionCancellationInfo, + NexusOperationExecutionInfo, + NexusOperationExecutionListInfo, Request, Response, StartOperationRequest, @@ -23,6 +26,9 @@ "Failure", "HandlerError", "Link", + "NexusOperationExecutionCancellationInfo", + "NexusOperationExecutionInfo", + "NexusOperationExecutionListInfo", "Request", "Response", "StartOperationRequest", diff --git a/temporalio/api/nexus/v1/message_pb2.py b/temporalio/api/nexus/v1/message_pb2.py index 263952fd7..d607eb9c8 100644 --- a/temporalio/api/nexus/v1/message_pb2.py +++ b/temporalio/api/nexus/v1/message_pb2.py @@ -14,17 +14,27 @@ _sym_db = _symbol_database.Default() +from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2 from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 from temporalio.api.common.v1 import ( message_pb2 as temporal_dot_api_dot_common_dot_v1_dot_message__pb2, ) +from temporalio.api.enums.v1 import ( + common_pb2 as temporal_dot_api_dot_enums_dot_v1_dot_common__pb2, +) from temporalio.api.enums.v1 import ( nexus_pb2 as temporal_dot_api_dot_enums_dot_v1_dot_nexus__pb2, ) +from temporalio.api.failure.v1 import ( + message_pb2 as temporal_dot_api_dot_failure_dot_v1_dot_message__pb2, +) +from temporalio.api.sdk.v1 import ( + user_metadata_pb2 as temporal_dot_api_dot_sdk_dot_v1_dot_user__metadata__pb2, +) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n#temporal/api/nexus/v1/message.proto\x12\x15temporal.api.nexus.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\x1a!temporal/api/enums/v1/nexus.proto"\x9c\x01\n\x07\x46\x61ilure\x12\x0f\n\x07message\x18\x01 \x01(\t\x12>\n\x08metadata\x18\x02 \x03(\x0b\x32,.temporal.api.nexus.v1.Failure.MetadataEntry\x12\x0f\n\x07\x64\x65tails\x18\x03 \x01(\x0c\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"\xa2\x01\n\x0cHandlerError\x12\x12\n\nerror_type\x18\x01 \x01(\t\x12/\n\x07\x66\x61ilure\x18\x02 \x01(\x0b\x32\x1e.temporal.api.nexus.v1.Failure\x12M\n\x0eretry_behavior\x18\x03 \x01(\x0e\x32\x35.temporal.api.enums.v1.NexusHandlerErrorRetryBehavior"f\n\x1aUnsuccessfulOperationError\x12\x17\n\x0foperation_state\x18\x01 \x01(\t\x12/\n\x07\x66\x61ilure\x18\x02 \x01(\x0b\x32\x1e.temporal.api.nexus.v1.Failure"!\n\x04Link\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t"\xd1\x02\n\x15StartOperationRequest\x12\x0f\n\x07service\x18\x01 \x01(\t\x12\x11\n\toperation\x18\x02 \x01(\t\x12\x12\n\nrequest_id\x18\x03 \x01(\t\x12\x10\n\x08\x63\x61llback\x18\x04 \x01(\t\x12\x30\n\x07payload\x18\x05 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12Y\n\x0f\x63\x61llback_header\x18\x06 \x03(\x0b\x32@.temporal.api.nexus.v1.StartOperationRequest.CallbackHeaderEntry\x12*\n\x05links\x18\x07 \x03(\x0b\x32\x1b.temporal.api.nexus.v1.Link\x1a\x35\n\x13\x43\x61llbackHeaderEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"o\n\x16\x43\x61ncelOperationRequest\x12\x0f\n\x07service\x18\x01 \x01(\t\x12\x11\n\toperation\x18\x02 \x01(\t\x12\x18\n\x0coperation_id\x18\x03 \x01(\tB\x02\x18\x01\x12\x17\n\x0foperation_token\x18\x04 \x01(\t"\xc7\x02\n\x07Request\x12:\n\x06header\x18\x01 \x03(\x0b\x32*.temporal.api.nexus.v1.Request.HeaderEntry\x12\x32\n\x0escheduled_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12G\n\x0fstart_operation\x18\x03 \x01(\x0b\x32,.temporal.api.nexus.v1.StartOperationRequestH\x00\x12I\n\x10\x63\x61ncel_operation\x18\x04 \x01(\x0b\x32-.temporal.api.nexus.v1.CancelOperationRequestH\x00\x1a-\n\x0bHeaderEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\t\n\x07variant"\xd9\x03\n\x16StartOperationResponse\x12J\n\x0csync_success\x18\x01 \x01(\x0b\x32\x32.temporal.api.nexus.v1.StartOperationResponse.SyncH\x00\x12L\n\rasync_success\x18\x02 \x01(\x0b\x32\x33.temporal.api.nexus.v1.StartOperationResponse.AsyncH\x00\x12L\n\x0foperation_error\x18\x03 \x01(\x0b\x32\x31.temporal.api.nexus.v1.UnsuccessfulOperationErrorH\x00\x1a\x64\n\x04Sync\x12\x30\n\x07payload\x18\x01 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12*\n\x05links\x18\x02 \x03(\x0b\x32\x1b.temporal.api.nexus.v1.Link\x1a\x66\n\x05\x41sync\x12\x18\n\x0coperation_id\x18\x01 \x01(\tB\x02\x18\x01\x12*\n\x05links\x18\x02 \x03(\x0b\x32\x1b.temporal.api.nexus.v1.Link\x12\x17\n\x0foperation_token\x18\x03 \x01(\tB\t\n\x07variant"\x19\n\x17\x43\x61ncelOperationResponse"\xab\x01\n\x08Response\x12H\n\x0fstart_operation\x18\x01 \x01(\x0b\x32-.temporal.api.nexus.v1.StartOperationResponseH\x00\x12J\n\x10\x63\x61ncel_operation\x18\x02 \x01(\x0b\x32..temporal.api.nexus.v1.CancelOperationResponseH\x00\x42\t\n\x07variant"\xd8\x01\n\x08\x45ndpoint\x12\x0f\n\x07version\x18\x01 \x01(\x03\x12\n\n\x02id\x18\x02 \x01(\t\x12\x31\n\x04spec\x18\x03 \x01(\x0b\x32#.temporal.api.nexus.v1.EndpointSpec\x12\x30\n\x0c\x63reated_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x36\n\x12last_modified_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x12\n\nurl_prefix\x18\x06 \x01(\t"\x89\x01\n\x0c\x45ndpointSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x34\n\x0b\x64\x65scription\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x35\n\x06target\x18\x03 \x01(\x0b\x32%.temporal.api.nexus.v1.EndpointTarget"\xe9\x01\n\x0e\x45ndpointTarget\x12>\n\x06worker\x18\x01 \x01(\x0b\x32,.temporal.api.nexus.v1.EndpointTarget.WorkerH\x00\x12\x42\n\x08\x65xternal\x18\x02 \x01(\x0b\x32..temporal.api.nexus.v1.EndpointTarget.ExternalH\x00\x1a/\n\x06Worker\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x12\n\ntask_queue\x18\x02 \x01(\t\x1a\x17\n\x08\x45xternal\x12\x0b\n\x03url\x18\x01 \x01(\tB\t\n\x07variantB\x84\x01\n\x18io.temporal.api.nexus.v1B\x0cMessageProtoP\x01Z!go.temporal.io/api/nexus/v1;nexus\xaa\x02\x17Temporalio.Api.Nexus.V1\xea\x02\x1aTemporalio::Api::Nexus::V1b\x06proto3' + b'\n#temporal/api/nexus/v1/message.proto\x12\x15temporal.api.nexus.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\x1a"temporal/api/enums/v1/common.proto\x1a!temporal/api/enums/v1/nexus.proto\x1a%temporal/api/failure/v1/message.proto\x1a\'temporal/api/sdk/v1/user_metadata.proto"\xe0\x01\n\x07\x46\x61ilure\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x13\n\x0bstack_trace\x18\x04 \x01(\t\x12>\n\x08metadata\x18\x02 \x03(\x0b\x32,.temporal.api.nexus.v1.Failure.MetadataEntry\x12\x0f\n\x07\x64\x65tails\x18\x03 \x01(\x0c\x12-\n\x05\x63\x61use\x18\x05 \x01(\x0b\x32\x1e.temporal.api.nexus.v1.Failure\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"\xa2\x01\n\x0cHandlerError\x12\x12\n\nerror_type\x18\x01 \x01(\t\x12/\n\x07\x66\x61ilure\x18\x02 \x01(\x0b\x32\x1e.temporal.api.nexus.v1.Failure\x12M\n\x0eretry_behavior\x18\x03 \x01(\x0e\x32\x35.temporal.api.enums.v1.NexusHandlerErrorRetryBehavior"f\n\x1aUnsuccessfulOperationError\x12\x17\n\x0foperation_state\x18\x01 \x01(\t\x12/\n\x07\x66\x61ilure\x18\x02 \x01(\x0b\x32\x1e.temporal.api.nexus.v1.Failure"!\n\x04Link\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t"\xd1\x02\n\x15StartOperationRequest\x12\x0f\n\x07service\x18\x01 \x01(\t\x12\x11\n\toperation\x18\x02 \x01(\t\x12\x12\n\nrequest_id\x18\x03 \x01(\t\x12\x10\n\x08\x63\x61llback\x18\x04 \x01(\t\x12\x30\n\x07payload\x18\x05 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12Y\n\x0f\x63\x61llback_header\x18\x06 \x03(\x0b\x32@.temporal.api.nexus.v1.StartOperationRequest.CallbackHeaderEntry\x12*\n\x05links\x18\x07 \x03(\x0b\x32\x1b.temporal.api.nexus.v1.Link\x1a\x35\n\x13\x43\x61llbackHeaderEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"o\n\x16\x43\x61ncelOperationRequest\x12\x0f\n\x07service\x18\x01 \x01(\t\x12\x11\n\toperation\x18\x02 \x01(\t\x12\x18\n\x0coperation_id\x18\x03 \x01(\tB\x02\x18\x01\x12\x17\n\x0foperation_token\x18\x04 \x01(\t"\xd0\x03\n\x07Request\x12:\n\x06header\x18\x01 \x03(\x0b\x32*.temporal.api.nexus.v1.Request.HeaderEntry\x12\x32\n\x0escheduled_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x41\n\x0c\x63\x61pabilities\x18\x64 \x01(\x0b\x32+.temporal.api.nexus.v1.Request.Capabilities\x12G\n\x0fstart_operation\x18\x03 \x01(\x0b\x32,.temporal.api.nexus.v1.StartOperationRequestH\x00\x12I\n\x10\x63\x61ncel_operation\x18\x04 \x01(\x0b\x32-.temporal.api.nexus.v1.CancelOperationRequestH\x00\x12\x10\n\x08\x65ndpoint\x18\n \x01(\t\x1a\x32\n\x0c\x43\x61pabilities\x12"\n\x1atemporal_failure_responses\x18\x01 \x01(\x08\x1a-\n\x0bHeaderEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\t\n\x07variant"\x92\x04\n\x16StartOperationResponse\x12J\n\x0csync_success\x18\x01 \x01(\x0b\x32\x32.temporal.api.nexus.v1.StartOperationResponse.SyncH\x00\x12L\n\rasync_success\x18\x02 \x01(\x0b\x32\x33.temporal.api.nexus.v1.StartOperationResponse.AsyncH\x00\x12P\n\x0foperation_error\x18\x03 \x01(\x0b\x32\x31.temporal.api.nexus.v1.UnsuccessfulOperationErrorB\x02\x18\x01H\x00\x12\x33\n\x07\x66\x61ilure\x18\x04 \x01(\x0b\x32 .temporal.api.failure.v1.FailureH\x00\x1a\x64\n\x04Sync\x12\x30\n\x07payload\x18\x01 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12*\n\x05links\x18\x02 \x03(\x0b\x32\x1b.temporal.api.nexus.v1.Link\x1a\x66\n\x05\x41sync\x12\x18\n\x0coperation_id\x18\x01 \x01(\tB\x02\x18\x01\x12*\n\x05links\x18\x02 \x03(\x0b\x32\x1b.temporal.api.nexus.v1.Link\x12\x17\n\x0foperation_token\x18\x03 \x01(\tB\t\n\x07variant"\x19\n\x17\x43\x61ncelOperationResponse"\xab\x01\n\x08Response\x12H\n\x0fstart_operation\x18\x01 \x01(\x0b\x32-.temporal.api.nexus.v1.StartOperationResponseH\x00\x12J\n\x10\x63\x61ncel_operation\x18\x02 \x01(\x0b\x32..temporal.api.nexus.v1.CancelOperationResponseH\x00\x42\t\n\x07variant"\xd8\x01\n\x08\x45ndpoint\x12\x0f\n\x07version\x18\x01 \x01(\x03\x12\n\n\x02id\x18\x02 \x01(\t\x12\x31\n\x04spec\x18\x03 \x01(\x0b\x32#.temporal.api.nexus.v1.EndpointSpec\x12\x30\n\x0c\x63reated_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x36\n\x12last_modified_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x12\n\nurl_prefix\x18\x06 \x01(\t"\x89\x01\n\x0c\x45ndpointSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x34\n\x0b\x64\x65scription\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x35\n\x06target\x18\x03 \x01(\x0b\x32%.temporal.api.nexus.v1.EndpointTarget"\xe9\x01\n\x0e\x45ndpointTarget\x12>\n\x06worker\x18\x01 \x01(\x0b\x32,.temporal.api.nexus.v1.EndpointTarget.WorkerH\x00\x12\x42\n\x08\x65xternal\x18\x02 \x01(\x0b\x32..temporal.api.nexus.v1.EndpointTarget.ExternalH\x00\x1a/\n\x06Worker\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x12\n\ntask_queue\x18\x02 \x01(\t\x1a\x17\n\x08\x45xternal\x12\x0b\n\x03url\x18\x01 \x01(\tB\t\n\x07variant"\x9d\x03\n\'NexusOperationExecutionCancellationInfo\x12\x32\n\x0erequested_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x45\n\x05state\x18\x02 \x01(\x0e\x32\x36.temporal.api.enums.v1.NexusOperationCancellationState\x12\x0f\n\x07\x61ttempt\x18\x03 \x01(\x05\x12>\n\x1alast_attempt_complete_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12>\n\x14last_attempt_failure\x18\x05 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12>\n\x1anext_attempt_schedule_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x16\n\x0e\x62locked_reason\x18\x07 \x01(\t\x12\x0e\n\x06reason\x18\x08 \x01(\t"\xff\n\n\x1bNexusOperationExecutionInfo\x12\x14\n\x0coperation_id\x18\x01 \x01(\t\x12\x0e\n\x06run_id\x18\x02 \x01(\t\x12\x10\n\x08\x65ndpoint\x18\x03 \x01(\t\x12\x0f\n\x07service\x18\x04 \x01(\t\x12\x11\n\toperation\x18\x05 \x01(\t\x12\x44\n\x06status\x18\x06 \x01(\x0e\x32\x34.temporal.api.enums.v1.NexusOperationExecutionStatus\x12@\n\x05state\x18\x07 \x01(\x0e\x32\x31.temporal.api.enums.v1.PendingNexusOperationState\x12<\n\x19schedule_to_close_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12<\n\x19schedule_to_start_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x0f\n\x07\x61ttempt\x18\x0b \x01(\x05\x12\x31\n\rschedule_time\x18\x0c \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x33\n\x0f\x65xpiration_time\x18\r \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12.\n\nclose_time\x18\x0e \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12>\n\x1alast_attempt_complete_time\x18\x0f \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12>\n\x14last_attempt_failure\x18\x10 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12>\n\x1anext_attempt_schedule_time\x18\x11 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x35\n\x12\x65xecution_duration\x18\x12 \x01(\x0b\x32\x19.google.protobuf.Duration\x12Y\n\x11\x63\x61ncellation_info\x18\x13 \x01(\x0b\x32>.temporal.api.nexus.v1.NexusOperationExecutionCancellationInfo\x12\x16\n\x0e\x62locked_reason\x18\x14 \x01(\t\x12\x12\n\nrequest_id\x18\x15 \x01(\t\x12\x17\n\x0foperation_token\x18\x16 \x01(\t\x12\x1e\n\x16state_transition_count\x18\x17 \x01(\x03\x12\x43\n\x11search_attributes\x18\x18 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12Y\n\x0cnexus_header\x18\x19 \x03(\x0b\x32\x43.temporal.api.nexus.v1.NexusOperationExecutionInfo.NexusHeaderEntry\x12\x38\n\ruser_metadata\x18\x1a \x01(\x0b\x32!.temporal.api.sdk.v1.UserMetadata\x12+\n\x05links\x18\x1b \x03(\x0b\x32\x1c.temporal.api.common.v1.Link\x12\x10\n\x08identity\x18\x1c \x01(\t\x12\x18\n\x10state_size_bytes\x18\x1d \x01(\x03\x1a\x32\n\x10NexusHeaderEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"\xdc\x03\n\x1fNexusOperationExecutionListInfo\x12\x14\n\x0coperation_id\x18\x01 \x01(\t\x12\x0e\n\x06run_id\x18\x02 \x01(\t\x12\x10\n\x08\x65ndpoint\x18\x03 \x01(\t\x12\x0f\n\x07service\x18\x04 \x01(\t\x12\x11\n\toperation\x18\x05 \x01(\t\x12\x31\n\rschedule_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12.\n\nclose_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x44\n\x06status\x18\x08 \x01(\x0e\x32\x34.temporal.api.enums.v1.NexusOperationExecutionStatus\x12\x43\n\x11search_attributes\x18\t \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12\x1e\n\x16state_transition_count\x18\n \x01(\x03\x12\x35\n\x12\x65xecution_duration\x18\x0b \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x18\n\x10state_size_bytes\x18\x0c \x01(\x03\x42\x84\x01\n\x18io.temporal.api.nexus.v1B\x0cMessageProtoP\x01Z!go.temporal.io/api/nexus/v1;nexus\xaa\x02\x17Temporalio.Api.Nexus.V1\xea\x02\x1aTemporalio::Api::Nexus::V1b\x06proto3' ) @@ -41,6 +51,7 @@ ) _CANCELOPERATIONREQUEST = DESCRIPTOR.message_types_by_name["CancelOperationRequest"] _REQUEST = DESCRIPTOR.message_types_by_name["Request"] +_REQUEST_CAPABILITIES = _REQUEST.nested_types_by_name["Capabilities"] _REQUEST_HEADERENTRY = _REQUEST.nested_types_by_name["HeaderEntry"] _STARTOPERATIONRESPONSE = DESCRIPTOR.message_types_by_name["StartOperationResponse"] _STARTOPERATIONRESPONSE_SYNC = _STARTOPERATIONRESPONSE.nested_types_by_name["Sync"] @@ -52,6 +63,18 @@ _ENDPOINTTARGET = DESCRIPTOR.message_types_by_name["EndpointTarget"] _ENDPOINTTARGET_WORKER = _ENDPOINTTARGET.nested_types_by_name["Worker"] _ENDPOINTTARGET_EXTERNAL = _ENDPOINTTARGET.nested_types_by_name["External"] +_NEXUSOPERATIONEXECUTIONCANCELLATIONINFO = DESCRIPTOR.message_types_by_name[ + "NexusOperationExecutionCancellationInfo" +] +_NEXUSOPERATIONEXECUTIONINFO = DESCRIPTOR.message_types_by_name[ + "NexusOperationExecutionInfo" +] +_NEXUSOPERATIONEXECUTIONINFO_NEXUSHEADERENTRY = ( + _NEXUSOPERATIONEXECUTIONINFO.nested_types_by_name["NexusHeaderEntry"] +) +_NEXUSOPERATIONEXECUTIONLISTINFO = DESCRIPTOR.message_types_by_name[ + "NexusOperationExecutionListInfo" +] Failure = _reflection.GeneratedProtocolMessageType( "Failure", (_message.Message,), @@ -61,12 +84,12 @@ (_message.Message,), { "DESCRIPTOR": _FAILURE_METADATAENTRY, - "__module__": "temporal.api.nexus.v1.message_pb2", + "__module__": "temporalio.api.nexus.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.nexus.v1.Failure.MetadataEntry) }, ), "DESCRIPTOR": _FAILURE, - "__module__": "temporal.api.nexus.v1.message_pb2", + "__module__": "temporalio.api.nexus.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.nexus.v1.Failure) }, ) @@ -78,7 +101,7 @@ (_message.Message,), { "DESCRIPTOR": _HANDLERERROR, - "__module__": "temporal.api.nexus.v1.message_pb2", + "__module__": "temporalio.api.nexus.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.nexus.v1.HandlerError) }, ) @@ -89,7 +112,7 @@ (_message.Message,), { "DESCRIPTOR": _UNSUCCESSFULOPERATIONERROR, - "__module__": "temporal.api.nexus.v1.message_pb2", + "__module__": "temporalio.api.nexus.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.nexus.v1.UnsuccessfulOperationError) }, ) @@ -100,7 +123,7 @@ (_message.Message,), { "DESCRIPTOR": _LINK, - "__module__": "temporal.api.nexus.v1.message_pb2", + "__module__": "temporalio.api.nexus.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.nexus.v1.Link) }, ) @@ -115,12 +138,12 @@ (_message.Message,), { "DESCRIPTOR": _STARTOPERATIONREQUEST_CALLBACKHEADERENTRY, - "__module__": "temporal.api.nexus.v1.message_pb2", + "__module__": "temporalio.api.nexus.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.nexus.v1.StartOperationRequest.CallbackHeaderEntry) }, ), "DESCRIPTOR": _STARTOPERATIONREQUEST, - "__module__": "temporal.api.nexus.v1.message_pb2", + "__module__": "temporalio.api.nexus.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.nexus.v1.StartOperationRequest) }, ) @@ -132,7 +155,7 @@ (_message.Message,), { "DESCRIPTOR": _CANCELOPERATIONREQUEST, - "__module__": "temporal.api.nexus.v1.message_pb2", + "__module__": "temporalio.api.nexus.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.nexus.v1.CancelOperationRequest) }, ) @@ -142,21 +165,31 @@ "Request", (_message.Message,), { + "Capabilities": _reflection.GeneratedProtocolMessageType( + "Capabilities", + (_message.Message,), + { + "DESCRIPTOR": _REQUEST_CAPABILITIES, + "__module__": "temporalio.api.nexus.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.nexus.v1.Request.Capabilities) + }, + ), "HeaderEntry": _reflection.GeneratedProtocolMessageType( "HeaderEntry", (_message.Message,), { "DESCRIPTOR": _REQUEST_HEADERENTRY, - "__module__": "temporal.api.nexus.v1.message_pb2", + "__module__": "temporalio.api.nexus.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.nexus.v1.Request.HeaderEntry) }, ), "DESCRIPTOR": _REQUEST, - "__module__": "temporal.api.nexus.v1.message_pb2", + "__module__": "temporalio.api.nexus.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.nexus.v1.Request) }, ) _sym_db.RegisterMessage(Request) +_sym_db.RegisterMessage(Request.Capabilities) _sym_db.RegisterMessage(Request.HeaderEntry) StartOperationResponse = _reflection.GeneratedProtocolMessageType( @@ -168,7 +201,7 @@ (_message.Message,), { "DESCRIPTOR": _STARTOPERATIONRESPONSE_SYNC, - "__module__": "temporal.api.nexus.v1.message_pb2", + "__module__": "temporalio.api.nexus.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.nexus.v1.StartOperationResponse.Sync) }, ), @@ -177,12 +210,12 @@ (_message.Message,), { "DESCRIPTOR": _STARTOPERATIONRESPONSE_ASYNC, - "__module__": "temporal.api.nexus.v1.message_pb2", + "__module__": "temporalio.api.nexus.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.nexus.v1.StartOperationResponse.Async) }, ), "DESCRIPTOR": _STARTOPERATIONRESPONSE, - "__module__": "temporal.api.nexus.v1.message_pb2", + "__module__": "temporalio.api.nexus.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.nexus.v1.StartOperationResponse) }, ) @@ -195,7 +228,7 @@ (_message.Message,), { "DESCRIPTOR": _CANCELOPERATIONRESPONSE, - "__module__": "temporal.api.nexus.v1.message_pb2", + "__module__": "temporalio.api.nexus.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.nexus.v1.CancelOperationResponse) }, ) @@ -206,7 +239,7 @@ (_message.Message,), { "DESCRIPTOR": _RESPONSE, - "__module__": "temporal.api.nexus.v1.message_pb2", + "__module__": "temporalio.api.nexus.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.nexus.v1.Response) }, ) @@ -217,7 +250,7 @@ (_message.Message,), { "DESCRIPTOR": _ENDPOINT, - "__module__": "temporal.api.nexus.v1.message_pb2", + "__module__": "temporalio.api.nexus.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.nexus.v1.Endpoint) }, ) @@ -228,7 +261,7 @@ (_message.Message,), { "DESCRIPTOR": _ENDPOINTSPEC, - "__module__": "temporal.api.nexus.v1.message_pb2", + "__module__": "temporalio.api.nexus.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.nexus.v1.EndpointSpec) }, ) @@ -243,7 +276,7 @@ (_message.Message,), { "DESCRIPTOR": _ENDPOINTTARGET_WORKER, - "__module__": "temporal.api.nexus.v1.message_pb2", + "__module__": "temporalio.api.nexus.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.nexus.v1.EndpointTarget.Worker) }, ), @@ -252,12 +285,12 @@ (_message.Message,), { "DESCRIPTOR": _ENDPOINTTARGET_EXTERNAL, - "__module__": "temporal.api.nexus.v1.message_pb2", + "__module__": "temporalio.api.nexus.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.nexus.v1.EndpointTarget.External) }, ), "DESCRIPTOR": _ENDPOINTTARGET, - "__module__": "temporal.api.nexus.v1.message_pb2", + "__module__": "temporalio.api.nexus.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.nexus.v1.EndpointTarget) }, ) @@ -265,6 +298,49 @@ _sym_db.RegisterMessage(EndpointTarget.Worker) _sym_db.RegisterMessage(EndpointTarget.External) +NexusOperationExecutionCancellationInfo = _reflection.GeneratedProtocolMessageType( + "NexusOperationExecutionCancellationInfo", + (_message.Message,), + { + "DESCRIPTOR": _NEXUSOPERATIONEXECUTIONCANCELLATIONINFO, + "__module__": "temporalio.api.nexus.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.nexus.v1.NexusOperationExecutionCancellationInfo) + }, +) +_sym_db.RegisterMessage(NexusOperationExecutionCancellationInfo) + +NexusOperationExecutionInfo = _reflection.GeneratedProtocolMessageType( + "NexusOperationExecutionInfo", + (_message.Message,), + { + "NexusHeaderEntry": _reflection.GeneratedProtocolMessageType( + "NexusHeaderEntry", + (_message.Message,), + { + "DESCRIPTOR": _NEXUSOPERATIONEXECUTIONINFO_NEXUSHEADERENTRY, + "__module__": "temporalio.api.nexus.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.nexus.v1.NexusOperationExecutionInfo.NexusHeaderEntry) + }, + ), + "DESCRIPTOR": _NEXUSOPERATIONEXECUTIONINFO, + "__module__": "temporalio.api.nexus.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.nexus.v1.NexusOperationExecutionInfo) + }, +) +_sym_db.RegisterMessage(NexusOperationExecutionInfo) +_sym_db.RegisterMessage(NexusOperationExecutionInfo.NexusHeaderEntry) + +NexusOperationExecutionListInfo = _reflection.GeneratedProtocolMessageType( + "NexusOperationExecutionListInfo", + (_message.Message,), + { + "DESCRIPTOR": _NEXUSOPERATIONEXECUTIONLISTINFO, + "__module__": "temporalio.api.nexus.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.nexus.v1.NexusOperationExecutionListInfo) + }, +) +_sym_db.RegisterMessage(NexusOperationExecutionListInfo) + if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b"\n\030io.temporal.api.nexus.v1B\014MessageProtoP\001Z!go.temporal.io/api/nexus/v1;nexus\252\002\027Temporalio.Api.Nexus.V1\352\002\032Temporalio::Api::Nexus::V1" @@ -282,44 +358,60 @@ _STARTOPERATIONRESPONSE_ASYNC.fields_by_name[ "operation_id" ]._serialized_options = b"\030\001" - _FAILURE._serialized_start = 169 - _FAILURE._serialized_end = 325 - _FAILURE_METADATAENTRY._serialized_start = 278 - _FAILURE_METADATAENTRY._serialized_end = 325 - _HANDLERERROR._serialized_start = 328 - _HANDLERERROR._serialized_end = 490 - _UNSUCCESSFULOPERATIONERROR._serialized_start = 492 - _UNSUCCESSFULOPERATIONERROR._serialized_end = 594 - _LINK._serialized_start = 596 - _LINK._serialized_end = 629 - _STARTOPERATIONREQUEST._serialized_start = 632 - _STARTOPERATIONREQUEST._serialized_end = 969 - _STARTOPERATIONREQUEST_CALLBACKHEADERENTRY._serialized_start = 916 - _STARTOPERATIONREQUEST_CALLBACKHEADERENTRY._serialized_end = 969 - _CANCELOPERATIONREQUEST._serialized_start = 971 - _CANCELOPERATIONREQUEST._serialized_end = 1082 - _REQUEST._serialized_start = 1085 - _REQUEST._serialized_end = 1412 - _REQUEST_HEADERENTRY._serialized_start = 1356 - _REQUEST_HEADERENTRY._serialized_end = 1401 - _STARTOPERATIONRESPONSE._serialized_start = 1415 - _STARTOPERATIONRESPONSE._serialized_end = 1888 - _STARTOPERATIONRESPONSE_SYNC._serialized_start = 1673 - _STARTOPERATIONRESPONSE_SYNC._serialized_end = 1773 - _STARTOPERATIONRESPONSE_ASYNC._serialized_start = 1775 - _STARTOPERATIONRESPONSE_ASYNC._serialized_end = 1877 - _CANCELOPERATIONRESPONSE._serialized_start = 1890 - _CANCELOPERATIONRESPONSE._serialized_end = 1915 - _RESPONSE._serialized_start = 1918 - _RESPONSE._serialized_end = 2089 - _ENDPOINT._serialized_start = 2092 - _ENDPOINT._serialized_end = 2308 - _ENDPOINTSPEC._serialized_start = 2311 - _ENDPOINTSPEC._serialized_end = 2448 - _ENDPOINTTARGET._serialized_start = 2451 - _ENDPOINTTARGET._serialized_end = 2684 - _ENDPOINTTARGET_WORKER._serialized_start = 2601 - _ENDPOINTTARGET_WORKER._serialized_end = 2648 - _ENDPOINTTARGET_EXTERNAL._serialized_start = 2650 - _ENDPOINTTARGET_EXTERNAL._serialized_end = 2673 + _STARTOPERATIONRESPONSE.fields_by_name["operation_error"]._options = None + _STARTOPERATIONRESPONSE.fields_by_name[ + "operation_error" + ]._serialized_options = b"\030\001" + _NEXUSOPERATIONEXECUTIONINFO_NEXUSHEADERENTRY._options = None + _NEXUSOPERATIONEXECUTIONINFO_NEXUSHEADERENTRY._serialized_options = b"8\001" + _FAILURE._serialized_start = 317 + _FAILURE._serialized_end = 541 + _FAILURE_METADATAENTRY._serialized_start = 494 + _FAILURE_METADATAENTRY._serialized_end = 541 + _HANDLERERROR._serialized_start = 544 + _HANDLERERROR._serialized_end = 706 + _UNSUCCESSFULOPERATIONERROR._serialized_start = 708 + _UNSUCCESSFULOPERATIONERROR._serialized_end = 810 + _LINK._serialized_start = 812 + _LINK._serialized_end = 845 + _STARTOPERATIONREQUEST._serialized_start = 848 + _STARTOPERATIONREQUEST._serialized_end = 1185 + _STARTOPERATIONREQUEST_CALLBACKHEADERENTRY._serialized_start = 1132 + _STARTOPERATIONREQUEST_CALLBACKHEADERENTRY._serialized_end = 1185 + _CANCELOPERATIONREQUEST._serialized_start = 1187 + _CANCELOPERATIONREQUEST._serialized_end = 1298 + _REQUEST._serialized_start = 1301 + _REQUEST._serialized_end = 1765 + _REQUEST_CAPABILITIES._serialized_start = 1657 + _REQUEST_CAPABILITIES._serialized_end = 1707 + _REQUEST_HEADERENTRY._serialized_start = 1709 + _REQUEST_HEADERENTRY._serialized_end = 1754 + _STARTOPERATIONRESPONSE._serialized_start = 1768 + _STARTOPERATIONRESPONSE._serialized_end = 2298 + _STARTOPERATIONRESPONSE_SYNC._serialized_start = 2083 + _STARTOPERATIONRESPONSE_SYNC._serialized_end = 2183 + _STARTOPERATIONRESPONSE_ASYNC._serialized_start = 2185 + _STARTOPERATIONRESPONSE_ASYNC._serialized_end = 2287 + _CANCELOPERATIONRESPONSE._serialized_start = 2300 + _CANCELOPERATIONRESPONSE._serialized_end = 2325 + _RESPONSE._serialized_start = 2328 + _RESPONSE._serialized_end = 2499 + _ENDPOINT._serialized_start = 2502 + _ENDPOINT._serialized_end = 2718 + _ENDPOINTSPEC._serialized_start = 2721 + _ENDPOINTSPEC._serialized_end = 2858 + _ENDPOINTTARGET._serialized_start = 2861 + _ENDPOINTTARGET._serialized_end = 3094 + _ENDPOINTTARGET_WORKER._serialized_start = 3011 + _ENDPOINTTARGET_WORKER._serialized_end = 3058 + _ENDPOINTTARGET_EXTERNAL._serialized_start = 3060 + _ENDPOINTTARGET_EXTERNAL._serialized_end = 3083 + _NEXUSOPERATIONEXECUTIONCANCELLATIONINFO._serialized_start = 3097 + _NEXUSOPERATIONEXECUTIONCANCELLATIONINFO._serialized_end = 3510 + _NEXUSOPERATIONEXECUTIONINFO._serialized_start = 3513 + _NEXUSOPERATIONEXECUTIONINFO._serialized_end = 4920 + _NEXUSOPERATIONEXECUTIONINFO_NEXUSHEADERENTRY._serialized_start = 4870 + _NEXUSOPERATIONEXECUTIONINFO_NEXUSHEADERENTRY._serialized_end = 4920 + _NEXUSOPERATIONEXECUTIONLISTINFO._serialized_start = 4923 + _NEXUSOPERATIONEXECUTIONLISTINFO._serialized_end = 5399 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/nexus/v1/message_pb2.pyi b/temporalio/api/nexus/v1/message_pb2.pyi index b72127ed6..ecdd004e2 100644 --- a/temporalio/api/nexus/v1/message_pb2.pyi +++ b/temporalio/api/nexus/v1/message_pb2.pyi @@ -8,12 +8,16 @@ import collections.abc import sys import google.protobuf.descriptor +import google.protobuf.duration_pb2 import google.protobuf.internal.containers import google.protobuf.message import google.protobuf.timestamp_pb2 import temporalio.api.common.v1.message_pb2 +import temporalio.api.enums.v1.common_pb2 import temporalio.api.enums.v1.nexus_pb2 +import temporalio.api.failure.v1.message_pb2 +import temporalio.api.sdk.v1.user_metadata_pb2 if sys.version_info >= (3, 8): import typing as typing_extensions @@ -48,26 +52,45 @@ class Failure(google.protobuf.message.Message): ) -> None: ... MESSAGE_FIELD_NUMBER: builtins.int + STACK_TRACE_FIELD_NUMBER: builtins.int METADATA_FIELD_NUMBER: builtins.int DETAILS_FIELD_NUMBER: builtins.int + CAUSE_FIELD_NUMBER: builtins.int message: builtins.str + stack_trace: builtins.str @property def metadata( self, ) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: ... details: builtins.bytes """UTF-8 encoded JSON serializable details.""" + @property + def cause(self) -> global___Failure: ... def __init__( self, *, message: builtins.str = ..., + stack_trace: builtins.str = ..., metadata: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., details: builtins.bytes = ..., + cause: global___Failure | None = ..., ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["cause", b"cause"] + ) -> builtins.bool: ... def ClearField( self, field_name: typing_extensions.Literal[ - "details", b"details", "message", b"message", "metadata", b"metadata" + "cause", + b"cause", + "details", + b"details", + "message", + b"message", + "metadata", + b"metadata", + "stack_trace", + b"stack_trace", ], ) -> None: ... @@ -297,6 +320,26 @@ class Request(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor + class Capabilities(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TEMPORAL_FAILURE_RESPONSES_FIELD_NUMBER: builtins.int + temporal_failure_responses: builtins.bool + """If set, handlers may use temporalio.api.failure.v1.Failure instances to return failures to the server. + This also allows handler and operation errors to have their own messages and stack traces. + """ + def __init__( + self, + *, + temporal_failure_responses: builtins.bool = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "temporal_failure_responses", b"temporal_failure_responses" + ], + ) -> None: ... + class HeaderEntry(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -317,8 +360,10 @@ class Request(google.protobuf.message.Message): HEADER_FIELD_NUMBER: builtins.int SCHEDULED_TIME_FIELD_NUMBER: builtins.int + CAPABILITIES_FIELD_NUMBER: builtins.int START_OPERATION_FIELD_NUMBER: builtins.int CANCEL_OPERATION_FIELD_NUMBER: builtins.int + ENDPOINT_FIELD_NUMBER: builtins.int @property def header( self, @@ -333,22 +378,32 @@ class Request(google.protobuf.message.Message): aip.dev/not-precedent: Not following linter rules. --) """ @property + def capabilities(self) -> global___Request.Capabilities: ... + @property def start_operation(self) -> global___StartOperationRequest: ... @property def cancel_operation(self) -> global___CancelOperationRequest: ... + endpoint: builtins.str + """The endpoint this request was addressed to before forwarding to the worker. + Supported from server version 1.30.0. + """ def __init__( self, *, header: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., scheduled_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + capabilities: global___Request.Capabilities | None = ..., start_operation: global___StartOperationRequest | None = ..., cancel_operation: global___CancelOperationRequest | None = ..., + endpoint: builtins.str = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal[ "cancel_operation", b"cancel_operation", + "capabilities", + b"capabilities", "scheduled_time", b"scheduled_time", "start_operation", @@ -362,6 +417,10 @@ class Request(google.protobuf.message.Message): field_name: typing_extensions.Literal[ "cancel_operation", b"cancel_operation", + "capabilities", + b"capabilities", + "endpoint", + b"endpoint", "header", b"header", "scheduled_time", @@ -455,25 +514,36 @@ class StartOperationResponse(google.protobuf.message.Message): SYNC_SUCCESS_FIELD_NUMBER: builtins.int ASYNC_SUCCESS_FIELD_NUMBER: builtins.int OPERATION_ERROR_FIELD_NUMBER: builtins.int + FAILURE_FIELD_NUMBER: builtins.int @property def sync_success(self) -> global___StartOperationResponse.Sync: ... @property def async_success(self) -> global___StartOperationResponse.Async: ... @property def operation_error(self) -> global___UnsuccessfulOperationError: - """The operation completed unsuccessfully (failed or canceled).""" + """The operation completed unsuccessfully (failed or canceled). + Deprecated. Use the failure variant instead. + """ + @property + def failure(self) -> temporalio.api.failure.v1.message_pb2.Failure: + """The operation completed unsuccessfully (failed or canceled). + Failure object must contain an ApplicationFailureInfo or CanceledFailureInfo object. + """ def __init__( self, *, sync_success: global___StartOperationResponse.Sync | None = ..., async_success: global___StartOperationResponse.Async | None = ..., operation_error: global___UnsuccessfulOperationError | None = ..., + failure: temporalio.api.failure.v1.message_pb2.Failure | None = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal[ "async_success", b"async_success", + "failure", + b"failure", "operation_error", b"operation_error", "sync_success", @@ -487,6 +557,8 @@ class StartOperationResponse(google.protobuf.message.Message): field_name: typing_extensions.Literal[ "async_success", b"async_success", + "failure", + b"failure", "operation_error", b"operation_error", "sync_success", @@ -498,7 +570,9 @@ class StartOperationResponse(google.protobuf.message.Message): def WhichOneof( self, oneof_group: typing_extensions.Literal["variant", b"variant"] ) -> ( - typing_extensions.Literal["sync_success", "async_success", "operation_error"] + typing_extensions.Literal[ + "sync_success", "async_success", "operation_error", "failure" + ] | None ): ... @@ -758,3 +832,492 @@ class EndpointTarget(google.protobuf.message.Message): ) -> typing_extensions.Literal["worker", "external"] | None: ... global___EndpointTarget = EndpointTarget + +class NexusOperationExecutionCancellationInfo(google.protobuf.message.Message): + """NexusOperationExecutionCancellationInfo contains the state of a Nexus operation cancellation.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + REQUESTED_TIME_FIELD_NUMBER: builtins.int + STATE_FIELD_NUMBER: builtins.int + ATTEMPT_FIELD_NUMBER: builtins.int + LAST_ATTEMPT_COMPLETE_TIME_FIELD_NUMBER: builtins.int + LAST_ATTEMPT_FAILURE_FIELD_NUMBER: builtins.int + NEXT_ATTEMPT_SCHEDULE_TIME_FIELD_NUMBER: builtins.int + BLOCKED_REASON_FIELD_NUMBER: builtins.int + REASON_FIELD_NUMBER: builtins.int + @property + def requested_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """The time when cancellation was requested.""" + state: temporalio.api.enums.v1.common_pb2.NexusOperationCancellationState.ValueType + attempt: builtins.int + """The number of attempts made to deliver the cancel operation request. + This number represents a minimum bound since the attempt is incremented after the request completes. + """ + @property + def last_attempt_complete_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """The time when the last attempt completed.""" + @property + def last_attempt_failure(self) -> temporalio.api.failure.v1.message_pb2.Failure: + """The last attempt's failure, if any.""" + @property + def next_attempt_schedule_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """The time when the next attempt is scheduled.""" + blocked_reason: builtins.str + """If the state is BLOCKED, blocked reason provides additional information.""" + reason: builtins.str + """A reason that may be specified in the CancelNexusOperationRequest.""" + def __init__( + self, + *, + requested_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + state: temporalio.api.enums.v1.common_pb2.NexusOperationCancellationState.ValueType = ..., + attempt: builtins.int = ..., + last_attempt_complete_time: google.protobuf.timestamp_pb2.Timestamp + | None = ..., + last_attempt_failure: temporalio.api.failure.v1.message_pb2.Failure + | None = ..., + next_attempt_schedule_time: google.protobuf.timestamp_pb2.Timestamp + | None = ..., + blocked_reason: builtins.str = ..., + reason: builtins.str = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "last_attempt_complete_time", + b"last_attempt_complete_time", + "last_attempt_failure", + b"last_attempt_failure", + "next_attempt_schedule_time", + b"next_attempt_schedule_time", + "requested_time", + b"requested_time", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "attempt", + b"attempt", + "blocked_reason", + b"blocked_reason", + "last_attempt_complete_time", + b"last_attempt_complete_time", + "last_attempt_failure", + b"last_attempt_failure", + "next_attempt_schedule_time", + b"next_attempt_schedule_time", + "reason", + b"reason", + "requested_time", + b"requested_time", + "state", + b"state", + ], + ) -> None: ... + +global___NexusOperationExecutionCancellationInfo = ( + NexusOperationExecutionCancellationInfo +) + +class NexusOperationExecutionInfo(google.protobuf.message.Message): + """Full current state of a standalone Nexus operation, as of the time of the request.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class NexusHeaderEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal["key", b"key", "value", b"value"], + ) -> None: ... + + OPERATION_ID_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + ENDPOINT_FIELD_NUMBER: builtins.int + SERVICE_FIELD_NUMBER: builtins.int + OPERATION_FIELD_NUMBER: builtins.int + STATUS_FIELD_NUMBER: builtins.int + STATE_FIELD_NUMBER: builtins.int + SCHEDULE_TO_CLOSE_TIMEOUT_FIELD_NUMBER: builtins.int + SCHEDULE_TO_START_TIMEOUT_FIELD_NUMBER: builtins.int + START_TO_CLOSE_TIMEOUT_FIELD_NUMBER: builtins.int + ATTEMPT_FIELD_NUMBER: builtins.int + SCHEDULE_TIME_FIELD_NUMBER: builtins.int + EXPIRATION_TIME_FIELD_NUMBER: builtins.int + CLOSE_TIME_FIELD_NUMBER: builtins.int + LAST_ATTEMPT_COMPLETE_TIME_FIELD_NUMBER: builtins.int + LAST_ATTEMPT_FAILURE_FIELD_NUMBER: builtins.int + NEXT_ATTEMPT_SCHEDULE_TIME_FIELD_NUMBER: builtins.int + EXECUTION_DURATION_FIELD_NUMBER: builtins.int + CANCELLATION_INFO_FIELD_NUMBER: builtins.int + BLOCKED_REASON_FIELD_NUMBER: builtins.int + REQUEST_ID_FIELD_NUMBER: builtins.int + OPERATION_TOKEN_FIELD_NUMBER: builtins.int + STATE_TRANSITION_COUNT_FIELD_NUMBER: builtins.int + SEARCH_ATTRIBUTES_FIELD_NUMBER: builtins.int + NEXUS_HEADER_FIELD_NUMBER: builtins.int + USER_METADATA_FIELD_NUMBER: builtins.int + LINKS_FIELD_NUMBER: builtins.int + IDENTITY_FIELD_NUMBER: builtins.int + STATE_SIZE_BYTES_FIELD_NUMBER: builtins.int + operation_id: builtins.str + """Unique identifier of this Nexus operation within its namespace along with run ID (below).""" + run_id: builtins.str + endpoint: builtins.str + """Endpoint name, resolved to a URL via the cluster's endpoint registry.""" + service: builtins.str + """Service name.""" + operation: builtins.str + """Operation name.""" + status: temporalio.api.enums.v1.nexus_pb2.NexusOperationExecutionStatus.ValueType + """A general status for this operation, indicates whether it is currently running or in one of the terminal statuses. + Updated once when the operation is originally scheduled, and again when it reaches a terminal status. + """ + state: temporalio.api.enums.v1.common_pb2.PendingNexusOperationState.ValueType + """More detailed breakdown of NEXUS_OPERATION_EXECUTION_STATUS_RUNNING.""" + @property + def schedule_to_close_timeout(self) -> google.protobuf.duration_pb2.Duration: + """Schedule-to-close timeout for this operation. + (-- api-linter: core::0140::prepositions=disabled + aip.dev/not-precedent: "to" is used to indicate interval. --) + """ + @property + def schedule_to_start_timeout(self) -> google.protobuf.duration_pb2.Duration: + """Schedule-to-start timeout for this operation. + (-- api-linter: core::0140::prepositions=disabled + aip.dev/not-precedent: "to" is used to indicate interval. --) + """ + @property + def start_to_close_timeout(self) -> google.protobuf.duration_pb2.Duration: + """Start-to-close timeout for this operation. + (-- api-linter: core::0140::prepositions=disabled + aip.dev/not-precedent: "to" is used to indicate interval. --) + """ + attempt: builtins.int + """The number of attempts made to deliver the start operation request. + This number is approximate, it is incremented when a task is added to the history queue. + In practice, there could be more attempts if a task is executed but fails to commit, or less attempts if a task + was never executed. + """ + @property + def schedule_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Time the operation was originally scheduled via a StartNexusOperation request.""" + @property + def expiration_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Scheduled time + schedule to close timeout.""" + @property + def close_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Time when the operation transitioned to a closed state.""" + @property + def last_attempt_complete_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """The time when the last attempt completed.""" + @property + def last_attempt_failure(self) -> temporalio.api.failure.v1.message_pb2.Failure: + """The last attempt's failure, if any.""" + @property + def next_attempt_schedule_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """The time when the next attempt is scheduled.""" + @property + def execution_duration(self) -> google.protobuf.duration_pb2.Duration: + """Elapsed time from schedule_time to now for running operations or to close_time for closed + operations, including all attempts and backoff between attempts. + """ + @property + def cancellation_info(self) -> global___NexusOperationExecutionCancellationInfo: ... + blocked_reason: builtins.str + """If the state is BLOCKED, blocked reason provides additional information.""" + request_id: builtins.str + """Server-generated request ID used as an idempotency token when submitting start requests to + the handler. Distinct from the request_id in StartNexusOperationRequest, which is the + caller-side idempotency key for the StartNexusOperation RPC itself. + """ + operation_token: builtins.str + """Operation token. Only set for asynchronous operations after a successful StartOperation call.""" + state_transition_count: builtins.int + """Incremented each time the operation's state is mutated in persistence.""" + @property + def search_attributes( + self, + ) -> temporalio.api.common.v1.message_pb2.SearchAttributes: ... + @property + def nexus_header( + self, + ) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: + """Header for context propagation and tracing purposes.""" + @property + def user_metadata(self) -> temporalio.api.sdk.v1.user_metadata_pb2.UserMetadata: + """Metadata for use by user interfaces to display the fixed as-of-start summary and details of the operation.""" + @property + def links( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.common.v1.message_pb2.Link + ]: + """Links attached by the handler of this operation on start or completion.""" + identity: builtins.str + """The identity of the client who started this operation.""" + state_size_bytes: builtins.int + """Updated once on scheduled and once on terminal status.""" + def __init__( + self, + *, + operation_id: builtins.str = ..., + run_id: builtins.str = ..., + endpoint: builtins.str = ..., + service: builtins.str = ..., + operation: builtins.str = ..., + status: temporalio.api.enums.v1.nexus_pb2.NexusOperationExecutionStatus.ValueType = ..., + state: temporalio.api.enums.v1.common_pb2.PendingNexusOperationState.ValueType = ..., + schedule_to_close_timeout: google.protobuf.duration_pb2.Duration | None = ..., + schedule_to_start_timeout: google.protobuf.duration_pb2.Duration | None = ..., + start_to_close_timeout: google.protobuf.duration_pb2.Duration | None = ..., + attempt: builtins.int = ..., + schedule_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + expiration_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + close_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + last_attempt_complete_time: google.protobuf.timestamp_pb2.Timestamp + | None = ..., + last_attempt_failure: temporalio.api.failure.v1.message_pb2.Failure + | None = ..., + next_attempt_schedule_time: google.protobuf.timestamp_pb2.Timestamp + | None = ..., + execution_duration: google.protobuf.duration_pb2.Duration | None = ..., + cancellation_info: global___NexusOperationExecutionCancellationInfo + | None = ..., + blocked_reason: builtins.str = ..., + request_id: builtins.str = ..., + operation_token: builtins.str = ..., + state_transition_count: builtins.int = ..., + search_attributes: temporalio.api.common.v1.message_pb2.SearchAttributes + | None = ..., + nexus_header: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + user_metadata: temporalio.api.sdk.v1.user_metadata_pb2.UserMetadata + | None = ..., + links: collections.abc.Iterable[temporalio.api.common.v1.message_pb2.Link] + | None = ..., + identity: builtins.str = ..., + state_size_bytes: builtins.int = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "cancellation_info", + b"cancellation_info", + "close_time", + b"close_time", + "execution_duration", + b"execution_duration", + "expiration_time", + b"expiration_time", + "last_attempt_complete_time", + b"last_attempt_complete_time", + "last_attempt_failure", + b"last_attempt_failure", + "next_attempt_schedule_time", + b"next_attempt_schedule_time", + "schedule_time", + b"schedule_time", + "schedule_to_close_timeout", + b"schedule_to_close_timeout", + "schedule_to_start_timeout", + b"schedule_to_start_timeout", + "search_attributes", + b"search_attributes", + "start_to_close_timeout", + b"start_to_close_timeout", + "user_metadata", + b"user_metadata", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "attempt", + b"attempt", + "blocked_reason", + b"blocked_reason", + "cancellation_info", + b"cancellation_info", + "close_time", + b"close_time", + "endpoint", + b"endpoint", + "execution_duration", + b"execution_duration", + "expiration_time", + b"expiration_time", + "identity", + b"identity", + "last_attempt_complete_time", + b"last_attempt_complete_time", + "last_attempt_failure", + b"last_attempt_failure", + "links", + b"links", + "next_attempt_schedule_time", + b"next_attempt_schedule_time", + "nexus_header", + b"nexus_header", + "operation", + b"operation", + "operation_id", + b"operation_id", + "operation_token", + b"operation_token", + "request_id", + b"request_id", + "run_id", + b"run_id", + "schedule_time", + b"schedule_time", + "schedule_to_close_timeout", + b"schedule_to_close_timeout", + "schedule_to_start_timeout", + b"schedule_to_start_timeout", + "search_attributes", + b"search_attributes", + "service", + b"service", + "start_to_close_timeout", + b"start_to_close_timeout", + "state", + b"state", + "state_size_bytes", + b"state_size_bytes", + "state_transition_count", + b"state_transition_count", + "status", + b"status", + "user_metadata", + b"user_metadata", + ], + ) -> None: ... + +global___NexusOperationExecutionInfo = NexusOperationExecutionInfo + +class NexusOperationExecutionListInfo(google.protobuf.message.Message): + """Limited Nexus operation information returned in the list response. + When adding fields here, ensure that it is also present in NexusOperationExecutionInfo (note that it may already be present in + NexusOperationExecutionInfo but not at the top-level). + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + OPERATION_ID_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + ENDPOINT_FIELD_NUMBER: builtins.int + SERVICE_FIELD_NUMBER: builtins.int + OPERATION_FIELD_NUMBER: builtins.int + SCHEDULE_TIME_FIELD_NUMBER: builtins.int + CLOSE_TIME_FIELD_NUMBER: builtins.int + STATUS_FIELD_NUMBER: builtins.int + SEARCH_ATTRIBUTES_FIELD_NUMBER: builtins.int + STATE_TRANSITION_COUNT_FIELD_NUMBER: builtins.int + EXECUTION_DURATION_FIELD_NUMBER: builtins.int + STATE_SIZE_BYTES_FIELD_NUMBER: builtins.int + operation_id: builtins.str + """A unique identifier of this operation within its namespace along with run ID (below).""" + run_id: builtins.str + """The run ID of the standalone Nexus operation.""" + endpoint: builtins.str + """Endpoint name.""" + service: builtins.str + """Service name.""" + operation: builtins.str + """Operation name.""" + @property + def schedule_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Time the operation was originally scheduled via a StartNexusOperation request.""" + @property + def close_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """If the operation is in a terminal status, this field represents the time the operation transitioned to that status.""" + status: temporalio.api.enums.v1.nexus_pb2.NexusOperationExecutionStatus.ValueType + """The status is updated once, when the operation is originally scheduled, and again when the operation reaches a terminal status.""" + @property + def search_attributes( + self, + ) -> temporalio.api.common.v1.message_pb2.SearchAttributes: + """Search attributes from the start request.""" + state_transition_count: builtins.int + """Updated on terminal status.""" + @property + def execution_duration(self) -> google.protobuf.duration_pb2.Duration: + """The difference between close time and scheduled time. + This field is only populated if the operation is closed. + """ + state_size_bytes: builtins.int + """Updated once on scheduled and once on terminal status.""" + def __init__( + self, + *, + operation_id: builtins.str = ..., + run_id: builtins.str = ..., + endpoint: builtins.str = ..., + service: builtins.str = ..., + operation: builtins.str = ..., + schedule_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + close_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + status: temporalio.api.enums.v1.nexus_pb2.NexusOperationExecutionStatus.ValueType = ..., + search_attributes: temporalio.api.common.v1.message_pb2.SearchAttributes + | None = ..., + state_transition_count: builtins.int = ..., + execution_duration: google.protobuf.duration_pb2.Duration | None = ..., + state_size_bytes: builtins.int = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "close_time", + b"close_time", + "execution_duration", + b"execution_duration", + "schedule_time", + b"schedule_time", + "search_attributes", + b"search_attributes", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "close_time", + b"close_time", + "endpoint", + b"endpoint", + "execution_duration", + b"execution_duration", + "operation", + b"operation", + "operation_id", + b"operation_id", + "run_id", + b"run_id", + "schedule_time", + b"schedule_time", + "search_attributes", + b"search_attributes", + "service", + b"service", + "state_size_bytes", + b"state_size_bytes", + "state_transition_count", + b"state_transition_count", + "status", + b"status", + ], + ) -> None: ... + +global___NexusOperationExecutionListInfo = NexusOperationExecutionListInfo diff --git a/temporalio/api/nexusservices/__init__.py b/temporalio/api/nexusservices/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/temporalio/api/nexusservices/workerservice/__init__.py b/temporalio/api/nexusservices/workerservice/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/temporalio/api/nexusservices/workerservice/v1/__init__.py b/temporalio/api/nexusservices/workerservice/v1/__init__.py new file mode 100644 index 000000000..0b9e4a6ca --- /dev/null +++ b/temporalio/api/nexusservices/workerservice/v1/__init__.py @@ -0,0 +1,6 @@ +from .request_response_pb2 import ExecuteCommandsRequest, ExecuteCommandsResponse + +__all__ = [ + "ExecuteCommandsRequest", + "ExecuteCommandsResponse", +] diff --git a/temporalio/api/nexusservices/workerservice/v1/request_response_pb2.py b/temporalio/api/nexusservices/workerservice/v1/request_response_pb2.py new file mode 100644 index 000000000..ed045767d --- /dev/null +++ b/temporalio/api/nexusservices/workerservice/v1/request_response_pb2.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: temporal/api/nexusservices/workerservice/v1/request_response.proto +"""Generated protocol buffer code.""" + +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from temporalio.api.worker.v1 import ( + message_pb2 as temporal_dot_api_dot_worker_dot_v1_dot_message__pb2, +) + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\nBtemporal/api/nexusservices/workerservice/v1/request_response.proto\x12+temporal.api.nexusservices.workerservice.v1\x1a$temporal/api/worker/v1/message.proto"Q\n\x16\x45xecuteCommandsRequest\x12\x37\n\x08\x63ommands\x18\x01 \x03(\x0b\x32%.temporal.api.worker.v1.WorkerCommand"W\n\x17\x45xecuteCommandsResponse\x12<\n\x07results\x18\x01 \x03(\x0b\x32+.temporal.api.worker.v1.WorkerCommandResultB\xed\x01\n.io.temporal.api.nexusservices.workerservice.v1B\x14RequestResponseProtoP\x01Z?go.temporal.io/api/nexusservices/workerservice/v1;workerservice\xaa\x02-Temporalio.Api.Nexusservices.Workerservice.V1\xea\x02\x31Temporalio::Api::Nexusservices::Workerservice::V1b\x06proto3' +) + + +_EXECUTECOMMANDSREQUEST = DESCRIPTOR.message_types_by_name["ExecuteCommandsRequest"] +_EXECUTECOMMANDSRESPONSE = DESCRIPTOR.message_types_by_name["ExecuteCommandsResponse"] +ExecuteCommandsRequest = _reflection.GeneratedProtocolMessageType( + "ExecuteCommandsRequest", + (_message.Message,), + { + "DESCRIPTOR": _EXECUTECOMMANDSREQUEST, + "__module__": "temporalio.api.nexusservices.workerservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.nexusservices.workerservice.v1.ExecuteCommandsRequest) + }, +) +_sym_db.RegisterMessage(ExecuteCommandsRequest) + +ExecuteCommandsResponse = _reflection.GeneratedProtocolMessageType( + "ExecuteCommandsResponse", + (_message.Message,), + { + "DESCRIPTOR": _EXECUTECOMMANDSRESPONSE, + "__module__": "temporalio.api.nexusservices.workerservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.nexusservices.workerservice.v1.ExecuteCommandsResponse) + }, +) +_sym_db.RegisterMessage(ExecuteCommandsResponse) + +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b"\n.io.temporal.api.nexusservices.workerservice.v1B\024RequestResponseProtoP\001Z?go.temporal.io/api/nexusservices/workerservice/v1;workerservice\252\002-Temporalio.Api.Nexusservices.Workerservice.V1\352\0021Temporalio::Api::Nexusservices::Workerservice::V1" + _EXECUTECOMMANDSREQUEST._serialized_start = 153 + _EXECUTECOMMANDSREQUEST._serialized_end = 234 + _EXECUTECOMMANDSRESPONSE._serialized_start = 236 + _EXECUTECOMMANDSRESPONSE._serialized_end = 323 +# @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/nexusservices/workerservice/v1/request_response_pb2.pyi b/temporalio/api/nexusservices/workerservice/v1/request_response_pb2.pyi new file mode 100644 index 000000000..8fd328cb8 --- /dev/null +++ b/temporalio/api/nexusservices/workerservice/v1/request_response_pb2.pyi @@ -0,0 +1,80 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" + +import builtins +import collections.abc +import sys + +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message + +import temporalio.api.worker.v1.message_pb2 + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class ExecuteCommandsRequest(google.protobuf.message.Message): + """(-- + Internal Nexus service for server-to-worker communication. + --) + + Request payload for the "ExecuteCommands" Nexus operation. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + COMMANDS_FIELD_NUMBER: builtins.int + @property + def commands( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.worker.v1.message_pb2.WorkerCommand + ]: ... + def __init__( + self, + *, + commands: collections.abc.Iterable[ + temporalio.api.worker.v1.message_pb2.WorkerCommand + ] + | None = ..., + ) -> None: ... + def ClearField( + self, field_name: typing_extensions.Literal["commands", b"commands"] + ) -> None: ... + +global___ExecuteCommandsRequest = ExecuteCommandsRequest + +class ExecuteCommandsResponse(google.protobuf.message.Message): + """Response payload for the "ExecuteCommands" Nexus operation. + The results list must be 1:1 with the commands list in the request (same size and order). + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + RESULTS_FIELD_NUMBER: builtins.int + @property + def results( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.worker.v1.message_pb2.WorkerCommandResult + ]: ... + def __init__( + self, + *, + results: collections.abc.Iterable[ + temporalio.api.worker.v1.message_pb2.WorkerCommandResult + ] + | None = ..., + ) -> None: ... + def ClearField( + self, field_name: typing_extensions.Literal["results", b"results"] + ) -> None: ... + +global___ExecuteCommandsResponse = ExecuteCommandsResponse diff --git a/temporalio/api/nexusservices/workerservice/v1/request_response_pb2_grpc.py b/temporalio/api/nexusservices/workerservice/v1/request_response_pb2_grpc.py new file mode 100644 index 000000000..bf947056a --- /dev/null +++ b/temporalio/api/nexusservices/workerservice/v1/request_response_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" + +import grpc diff --git a/temporalio/api/nexusservices/workerservice/v1/request_response_pb2_grpc.pyi b/temporalio/api/nexusservices/workerservice/v1/request_response_pb2_grpc.pyi new file mode 100644 index 000000000..f3a5a087e --- /dev/null +++ b/temporalio/api/nexusservices/workerservice/v1/request_response_pb2_grpc.pyi @@ -0,0 +1,4 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" diff --git a/temporalio/api/operatorservice/v1/request_response_pb2.py b/temporalio/api/operatorservice/v1/request_response_pb2.py index 78e372d6c..8edc4274a 100644 --- a/temporalio/api/operatorservice/v1/request_response_pb2.py +++ b/temporalio/api/operatorservice/v1/request_response_pb2.py @@ -24,7 +24,7 @@ ) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n6temporal/api/operatorservice/v1/request_response.proto\x12\x1ftemporal.api.operatorservice.v1\x1a"temporal/api/enums/v1/common.proto\x1a#temporal/api/nexus/v1/message.proto\x1a\x1egoogle/protobuf/duration.proto"\xff\x01\n\x1a\x41\x64\x64SearchAttributesRequest\x12l\n\x11search_attributes\x18\x01 \x03(\x0b\x32Q.temporal.api.operatorservice.v1.AddSearchAttributesRequest.SearchAttributesEntry\x12\x11\n\tnamespace\x18\x02 \x01(\t\x1a`\n\x15SearchAttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0e\x32\'.temporal.api.enums.v1.IndexedValueType:\x02\x38\x01"\x1d\n\x1b\x41\x64\x64SearchAttributesResponse"M\n\x1dRemoveSearchAttributesRequest\x12\x19\n\x11search_attributes\x18\x01 \x03(\t\x12\x11\n\tnamespace\x18\x02 \x01(\t" \n\x1eRemoveSearchAttributesResponse"0\n\x1bListSearchAttributesRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t"\xe2\x04\n\x1cListSearchAttributesResponse\x12n\n\x11\x63ustom_attributes\x18\x01 \x03(\x0b\x32S.temporal.api.operatorservice.v1.ListSearchAttributesResponse.CustomAttributesEntry\x12n\n\x11system_attributes\x18\x02 \x03(\x0b\x32S.temporal.api.operatorservice.v1.ListSearchAttributesResponse.SystemAttributesEntry\x12h\n\x0estorage_schema\x18\x03 \x03(\x0b\x32P.temporal.api.operatorservice.v1.ListSearchAttributesResponse.StorageSchemaEntry\x1a`\n\x15\x43ustomAttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0e\x32\'.temporal.api.enums.v1.IndexedValueType:\x02\x38\x01\x1a`\n\x15SystemAttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0e\x32\'.temporal.api.enums.v1.IndexedValueType:\x02\x38\x01\x1a\x34\n\x12StorageSchemaEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"|\n\x16\x44\x65leteNamespaceRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x14\n\x0cnamespace_id\x18\x02 \x01(\t\x12\x39\n\x16namespace_delete_delay\x18\x03 \x01(\x0b\x32\x19.google.protobuf.Duration"4\n\x17\x44\x65leteNamespaceResponse\x12\x19\n\x11\x64\x65leted_namespace\x18\x01 \x01(\t"\x84\x01\n\x1f\x41\x64\x64OrUpdateRemoteClusterRequest\x12\x18\n\x10\x66rontend_address\x18\x01 \x01(\t\x12(\n enable_remote_cluster_connection\x18\x02 \x01(\x08\x12\x1d\n\x15\x66rontend_http_address\x18\x03 \x01(\t""\n AddOrUpdateRemoteClusterResponse"2\n\x1aRemoveRemoteClusterRequest\x12\x14\n\x0c\x63luster_name\x18\x01 \x01(\t"\x1d\n\x1bRemoveRemoteClusterResponse"A\n\x13ListClustersRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\x0c"s\n\x14ListClustersResponse\x12\x42\n\x08\x63lusters\x18\x01 \x03(\x0b\x32\x30.temporal.api.operatorservice.v1.ClusterMetadata\x12\x17\n\x0fnext_page_token\x18\x04 \x01(\x0c"\xc0\x01\n\x0f\x43lusterMetadata\x12\x14\n\x0c\x63luster_name\x18\x01 \x01(\t\x12\x12\n\ncluster_id\x18\x02 \x01(\t\x12\x0f\n\x07\x61\x64\x64ress\x18\x03 \x01(\t\x12\x14\n\x0chttp_address\x18\x07 \x01(\t\x12 \n\x18initial_failover_version\x18\x04 \x01(\x03\x12\x1b\n\x13history_shard_count\x18\x05 \x01(\x05\x12\x1d\n\x15is_connection_enabled\x18\x06 \x01(\x08"%\n\x17GetNexusEndpointRequest\x12\n\n\x02id\x18\x01 \x01(\t"M\n\x18GetNexusEndpointResponse\x12\x31\n\x08\x65ndpoint\x18\x01 \x01(\x0b\x32\x1f.temporal.api.nexus.v1.Endpoint"O\n\x1a\x43reateNexusEndpointRequest\x12\x31\n\x04spec\x18\x01 \x01(\x0b\x32#.temporal.api.nexus.v1.EndpointSpec"P\n\x1b\x43reateNexusEndpointResponse\x12\x31\n\x08\x65ndpoint\x18\x01 \x01(\x0b\x32\x1f.temporal.api.nexus.v1.Endpoint"l\n\x1aUpdateNexusEndpointRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\x03\x12\x31\n\x04spec\x18\x03 \x01(\x0b\x32#.temporal.api.nexus.v1.EndpointSpec"P\n\x1bUpdateNexusEndpointResponse\x12\x31\n\x08\x65ndpoint\x18\x01 \x01(\x0b\x32\x1f.temporal.api.nexus.v1.Endpoint"9\n\x1a\x44\x65leteNexusEndpointRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\x03"\x1d\n\x1b\x44\x65leteNexusEndpointResponse"U\n\x19ListNexusEndpointsRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\x0c\x12\x0c\n\x04name\x18\x03 \x01(\t"i\n\x1aListNexusEndpointsResponse\x12\x17\n\x0fnext_page_token\x18\x01 \x01(\x0c\x12\x32\n\tendpoints\x18\x02 \x03(\x0b\x32\x1f.temporal.api.nexus.v1.EndpointB\xbe\x01\n"io.temporal.api.operatorservice.v1B\x14RequestResponseProtoP\x01Z5go.temporal.io/api/operatorservice/v1;operatorservice\xaa\x02!Temporalio.Api.OperatorService.V1\xea\x02$Temporalio::Api::OperatorService::V1b\x06proto3' + b'\n6temporal/api/operatorservice/v1/request_response.proto\x12\x1ftemporal.api.operatorservice.v1\x1a"temporal/api/enums/v1/common.proto\x1a#temporal/api/nexus/v1/message.proto\x1a\x1egoogle/protobuf/duration.proto"\xff\x01\n\x1a\x41\x64\x64SearchAttributesRequest\x12l\n\x11search_attributes\x18\x01 \x03(\x0b\x32Q.temporal.api.operatorservice.v1.AddSearchAttributesRequest.SearchAttributesEntry\x12\x11\n\tnamespace\x18\x02 \x01(\t\x1a`\n\x15SearchAttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0e\x32\'.temporal.api.enums.v1.IndexedValueType:\x02\x38\x01"\x1d\n\x1b\x41\x64\x64SearchAttributesResponse"M\n\x1dRemoveSearchAttributesRequest\x12\x19\n\x11search_attributes\x18\x01 \x03(\t\x12\x11\n\tnamespace\x18\x02 \x01(\t" \n\x1eRemoveSearchAttributesResponse"0\n\x1bListSearchAttributesRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t"\xe2\x04\n\x1cListSearchAttributesResponse\x12n\n\x11\x63ustom_attributes\x18\x01 \x03(\x0b\x32S.temporal.api.operatorservice.v1.ListSearchAttributesResponse.CustomAttributesEntry\x12n\n\x11system_attributes\x18\x02 \x03(\x0b\x32S.temporal.api.operatorservice.v1.ListSearchAttributesResponse.SystemAttributesEntry\x12h\n\x0estorage_schema\x18\x03 \x03(\x0b\x32P.temporal.api.operatorservice.v1.ListSearchAttributesResponse.StorageSchemaEntry\x1a`\n\x15\x43ustomAttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0e\x32\'.temporal.api.enums.v1.IndexedValueType:\x02\x38\x01\x1a`\n\x15SystemAttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0e\x32\'.temporal.api.enums.v1.IndexedValueType:\x02\x38\x01\x1a\x34\n\x12StorageSchemaEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"|\n\x16\x44\x65leteNamespaceRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x14\n\x0cnamespace_id\x18\x02 \x01(\t\x12\x39\n\x16namespace_delete_delay\x18\x03 \x01(\x0b\x32\x19.google.protobuf.Duration"4\n\x17\x44\x65leteNamespaceResponse\x12\x19\n\x11\x64\x65leted_namespace\x18\x01 \x01(\t"\xa0\x01\n\x1f\x41\x64\x64OrUpdateRemoteClusterRequest\x12\x18\n\x10\x66rontend_address\x18\x01 \x01(\t\x12(\n enable_remote_cluster_connection\x18\x02 \x01(\x08\x12\x1d\n\x15\x66rontend_http_address\x18\x03 \x01(\t\x12\x1a\n\x12\x65nable_replication\x18\x04 \x01(\x08""\n AddOrUpdateRemoteClusterResponse"2\n\x1aRemoveRemoteClusterRequest\x12\x14\n\x0c\x63luster_name\x18\x01 \x01(\t"\x1d\n\x1bRemoveRemoteClusterResponse"A\n\x13ListClustersRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\x0c"s\n\x14ListClustersResponse\x12\x42\n\x08\x63lusters\x18\x01 \x03(\x0b\x32\x30.temporal.api.operatorservice.v1.ClusterMetadata\x12\x17\n\x0fnext_page_token\x18\x04 \x01(\x0c"\xe0\x01\n\x0f\x43lusterMetadata\x12\x14\n\x0c\x63luster_name\x18\x01 \x01(\t\x12\x12\n\ncluster_id\x18\x02 \x01(\t\x12\x0f\n\x07\x61\x64\x64ress\x18\x03 \x01(\t\x12\x14\n\x0chttp_address\x18\x07 \x01(\t\x12 \n\x18initial_failover_version\x18\x04 \x01(\x03\x12\x1b\n\x13history_shard_count\x18\x05 \x01(\x05\x12\x1d\n\x15is_connection_enabled\x18\x06 \x01(\x08\x12\x1e\n\x16is_replication_enabled\x18\x08 \x01(\x08"%\n\x17GetNexusEndpointRequest\x12\n\n\x02id\x18\x01 \x01(\t"M\n\x18GetNexusEndpointResponse\x12\x31\n\x08\x65ndpoint\x18\x01 \x01(\x0b\x32\x1f.temporal.api.nexus.v1.Endpoint"O\n\x1a\x43reateNexusEndpointRequest\x12\x31\n\x04spec\x18\x01 \x01(\x0b\x32#.temporal.api.nexus.v1.EndpointSpec"P\n\x1b\x43reateNexusEndpointResponse\x12\x31\n\x08\x65ndpoint\x18\x01 \x01(\x0b\x32\x1f.temporal.api.nexus.v1.Endpoint"l\n\x1aUpdateNexusEndpointRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\x03\x12\x31\n\x04spec\x18\x03 \x01(\x0b\x32#.temporal.api.nexus.v1.EndpointSpec"P\n\x1bUpdateNexusEndpointResponse\x12\x31\n\x08\x65ndpoint\x18\x01 \x01(\x0b\x32\x1f.temporal.api.nexus.v1.Endpoint"9\n\x1a\x44\x65leteNexusEndpointRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\x03"\x1d\n\x1b\x44\x65leteNexusEndpointResponse"U\n\x19ListNexusEndpointsRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\x0c\x12\x0c\n\x04name\x18\x03 \x01(\t"i\n\x1aListNexusEndpointsResponse\x12\x17\n\x0fnext_page_token\x18\x01 \x01(\x0c\x12\x32\n\tendpoints\x18\x02 \x03(\x0b\x32\x1f.temporal.api.nexus.v1.EndpointB\xbe\x01\n"io.temporal.api.operatorservice.v1B\x14RequestResponseProtoP\x01Z5go.temporal.io/api/operatorservice/v1;operatorservice\xaa\x02!Temporalio.Api.OperatorService.V1\xea\x02$Temporalio::Api::OperatorService::V1b\x06proto3' ) @@ -110,12 +110,12 @@ (_message.Message,), { "DESCRIPTOR": _ADDSEARCHATTRIBUTESREQUEST_SEARCHATTRIBUTESENTRY, - "__module__": "temporal.api.operatorservice.v1.request_response_pb2", + "__module__": "temporalio.api.operatorservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.operatorservice.v1.AddSearchAttributesRequest.SearchAttributesEntry) }, ), "DESCRIPTOR": _ADDSEARCHATTRIBUTESREQUEST, - "__module__": "temporal.api.operatorservice.v1.request_response_pb2", + "__module__": "temporalio.api.operatorservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.operatorservice.v1.AddSearchAttributesRequest) }, ) @@ -127,7 +127,7 @@ (_message.Message,), { "DESCRIPTOR": _ADDSEARCHATTRIBUTESRESPONSE, - "__module__": "temporal.api.operatorservice.v1.request_response_pb2", + "__module__": "temporalio.api.operatorservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.operatorservice.v1.AddSearchAttributesResponse) }, ) @@ -138,7 +138,7 @@ (_message.Message,), { "DESCRIPTOR": _REMOVESEARCHATTRIBUTESREQUEST, - "__module__": "temporal.api.operatorservice.v1.request_response_pb2", + "__module__": "temporalio.api.operatorservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.operatorservice.v1.RemoveSearchAttributesRequest) }, ) @@ -149,7 +149,7 @@ (_message.Message,), { "DESCRIPTOR": _REMOVESEARCHATTRIBUTESRESPONSE, - "__module__": "temporal.api.operatorservice.v1.request_response_pb2", + "__module__": "temporalio.api.operatorservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.operatorservice.v1.RemoveSearchAttributesResponse) }, ) @@ -160,7 +160,7 @@ (_message.Message,), { "DESCRIPTOR": _LISTSEARCHATTRIBUTESREQUEST, - "__module__": "temporal.api.operatorservice.v1.request_response_pb2", + "__module__": "temporalio.api.operatorservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.operatorservice.v1.ListSearchAttributesRequest) }, ) @@ -175,7 +175,7 @@ (_message.Message,), { "DESCRIPTOR": _LISTSEARCHATTRIBUTESRESPONSE_CUSTOMATTRIBUTESENTRY, - "__module__": "temporal.api.operatorservice.v1.request_response_pb2", + "__module__": "temporalio.api.operatorservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.operatorservice.v1.ListSearchAttributesResponse.CustomAttributesEntry) }, ), @@ -184,7 +184,7 @@ (_message.Message,), { "DESCRIPTOR": _LISTSEARCHATTRIBUTESRESPONSE_SYSTEMATTRIBUTESENTRY, - "__module__": "temporal.api.operatorservice.v1.request_response_pb2", + "__module__": "temporalio.api.operatorservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.operatorservice.v1.ListSearchAttributesResponse.SystemAttributesEntry) }, ), @@ -193,12 +193,12 @@ (_message.Message,), { "DESCRIPTOR": _LISTSEARCHATTRIBUTESRESPONSE_STORAGESCHEMAENTRY, - "__module__": "temporal.api.operatorservice.v1.request_response_pb2", + "__module__": "temporalio.api.operatorservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.operatorservice.v1.ListSearchAttributesResponse.StorageSchemaEntry) }, ), "DESCRIPTOR": _LISTSEARCHATTRIBUTESRESPONSE, - "__module__": "temporal.api.operatorservice.v1.request_response_pb2", + "__module__": "temporalio.api.operatorservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.operatorservice.v1.ListSearchAttributesResponse) }, ) @@ -212,7 +212,7 @@ (_message.Message,), { "DESCRIPTOR": _DELETENAMESPACEREQUEST, - "__module__": "temporal.api.operatorservice.v1.request_response_pb2", + "__module__": "temporalio.api.operatorservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.operatorservice.v1.DeleteNamespaceRequest) }, ) @@ -223,7 +223,7 @@ (_message.Message,), { "DESCRIPTOR": _DELETENAMESPACERESPONSE, - "__module__": "temporal.api.operatorservice.v1.request_response_pb2", + "__module__": "temporalio.api.operatorservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.operatorservice.v1.DeleteNamespaceResponse) }, ) @@ -234,7 +234,7 @@ (_message.Message,), { "DESCRIPTOR": _ADDORUPDATEREMOTECLUSTERREQUEST, - "__module__": "temporal.api.operatorservice.v1.request_response_pb2", + "__module__": "temporalio.api.operatorservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.operatorservice.v1.AddOrUpdateRemoteClusterRequest) }, ) @@ -245,7 +245,7 @@ (_message.Message,), { "DESCRIPTOR": _ADDORUPDATEREMOTECLUSTERRESPONSE, - "__module__": "temporal.api.operatorservice.v1.request_response_pb2", + "__module__": "temporalio.api.operatorservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.operatorservice.v1.AddOrUpdateRemoteClusterResponse) }, ) @@ -256,7 +256,7 @@ (_message.Message,), { "DESCRIPTOR": _REMOVEREMOTECLUSTERREQUEST, - "__module__": "temporal.api.operatorservice.v1.request_response_pb2", + "__module__": "temporalio.api.operatorservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.operatorservice.v1.RemoveRemoteClusterRequest) }, ) @@ -267,7 +267,7 @@ (_message.Message,), { "DESCRIPTOR": _REMOVEREMOTECLUSTERRESPONSE, - "__module__": "temporal.api.operatorservice.v1.request_response_pb2", + "__module__": "temporalio.api.operatorservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.operatorservice.v1.RemoveRemoteClusterResponse) }, ) @@ -278,7 +278,7 @@ (_message.Message,), { "DESCRIPTOR": _LISTCLUSTERSREQUEST, - "__module__": "temporal.api.operatorservice.v1.request_response_pb2", + "__module__": "temporalio.api.operatorservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.operatorservice.v1.ListClustersRequest) }, ) @@ -289,7 +289,7 @@ (_message.Message,), { "DESCRIPTOR": _LISTCLUSTERSRESPONSE, - "__module__": "temporal.api.operatorservice.v1.request_response_pb2", + "__module__": "temporalio.api.operatorservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.operatorservice.v1.ListClustersResponse) }, ) @@ -300,7 +300,7 @@ (_message.Message,), { "DESCRIPTOR": _CLUSTERMETADATA, - "__module__": "temporal.api.operatorservice.v1.request_response_pb2", + "__module__": "temporalio.api.operatorservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.operatorservice.v1.ClusterMetadata) }, ) @@ -311,7 +311,7 @@ (_message.Message,), { "DESCRIPTOR": _GETNEXUSENDPOINTREQUEST, - "__module__": "temporal.api.operatorservice.v1.request_response_pb2", + "__module__": "temporalio.api.operatorservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.operatorservice.v1.GetNexusEndpointRequest) }, ) @@ -322,7 +322,7 @@ (_message.Message,), { "DESCRIPTOR": _GETNEXUSENDPOINTRESPONSE, - "__module__": "temporal.api.operatorservice.v1.request_response_pb2", + "__module__": "temporalio.api.operatorservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.operatorservice.v1.GetNexusEndpointResponse) }, ) @@ -333,7 +333,7 @@ (_message.Message,), { "DESCRIPTOR": _CREATENEXUSENDPOINTREQUEST, - "__module__": "temporal.api.operatorservice.v1.request_response_pb2", + "__module__": "temporalio.api.operatorservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.operatorservice.v1.CreateNexusEndpointRequest) }, ) @@ -344,7 +344,7 @@ (_message.Message,), { "DESCRIPTOR": _CREATENEXUSENDPOINTRESPONSE, - "__module__": "temporal.api.operatorservice.v1.request_response_pb2", + "__module__": "temporalio.api.operatorservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.operatorservice.v1.CreateNexusEndpointResponse) }, ) @@ -355,7 +355,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATENEXUSENDPOINTREQUEST, - "__module__": "temporal.api.operatorservice.v1.request_response_pb2", + "__module__": "temporalio.api.operatorservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.operatorservice.v1.UpdateNexusEndpointRequest) }, ) @@ -366,7 +366,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATENEXUSENDPOINTRESPONSE, - "__module__": "temporal.api.operatorservice.v1.request_response_pb2", + "__module__": "temporalio.api.operatorservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.operatorservice.v1.UpdateNexusEndpointResponse) }, ) @@ -377,7 +377,7 @@ (_message.Message,), { "DESCRIPTOR": _DELETENEXUSENDPOINTREQUEST, - "__module__": "temporal.api.operatorservice.v1.request_response_pb2", + "__module__": "temporalio.api.operatorservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.operatorservice.v1.DeleteNexusEndpointRequest) }, ) @@ -388,7 +388,7 @@ (_message.Message,), { "DESCRIPTOR": _DELETENEXUSENDPOINTRESPONSE, - "__module__": "temporal.api.operatorservice.v1.request_response_pb2", + "__module__": "temporalio.api.operatorservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.operatorservice.v1.DeleteNexusEndpointResponse) }, ) @@ -399,7 +399,7 @@ (_message.Message,), { "DESCRIPTOR": _LISTNEXUSENDPOINTSREQUEST, - "__module__": "temporal.api.operatorservice.v1.request_response_pb2", + "__module__": "temporalio.api.operatorservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.operatorservice.v1.ListNexusEndpointsRequest) }, ) @@ -410,7 +410,7 @@ (_message.Message,), { "DESCRIPTOR": _LISTNEXUSENDPOINTSRESPONSE, - "__module__": "temporal.api.operatorservice.v1.request_response_pb2", + "__module__": "temporalio.api.operatorservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.operatorservice.v1.ListNexusEndpointsResponse) }, ) @@ -452,37 +452,37 @@ _DELETENAMESPACERESPONSE._serialized_start = 1387 _DELETENAMESPACERESPONSE._serialized_end = 1439 _ADDORUPDATEREMOTECLUSTERREQUEST._serialized_start = 1442 - _ADDORUPDATEREMOTECLUSTERREQUEST._serialized_end = 1574 - _ADDORUPDATEREMOTECLUSTERRESPONSE._serialized_start = 1576 - _ADDORUPDATEREMOTECLUSTERRESPONSE._serialized_end = 1610 - _REMOVEREMOTECLUSTERREQUEST._serialized_start = 1612 - _REMOVEREMOTECLUSTERREQUEST._serialized_end = 1662 - _REMOVEREMOTECLUSTERRESPONSE._serialized_start = 1664 - _REMOVEREMOTECLUSTERRESPONSE._serialized_end = 1693 - _LISTCLUSTERSREQUEST._serialized_start = 1695 - _LISTCLUSTERSREQUEST._serialized_end = 1760 - _LISTCLUSTERSRESPONSE._serialized_start = 1762 - _LISTCLUSTERSRESPONSE._serialized_end = 1877 - _CLUSTERMETADATA._serialized_start = 1880 - _CLUSTERMETADATA._serialized_end = 2072 - _GETNEXUSENDPOINTREQUEST._serialized_start = 2074 - _GETNEXUSENDPOINTREQUEST._serialized_end = 2111 - _GETNEXUSENDPOINTRESPONSE._serialized_start = 2113 - _GETNEXUSENDPOINTRESPONSE._serialized_end = 2190 - _CREATENEXUSENDPOINTREQUEST._serialized_start = 2192 - _CREATENEXUSENDPOINTREQUEST._serialized_end = 2271 - _CREATENEXUSENDPOINTRESPONSE._serialized_start = 2273 - _CREATENEXUSENDPOINTRESPONSE._serialized_end = 2353 - _UPDATENEXUSENDPOINTREQUEST._serialized_start = 2355 - _UPDATENEXUSENDPOINTREQUEST._serialized_end = 2463 - _UPDATENEXUSENDPOINTRESPONSE._serialized_start = 2465 - _UPDATENEXUSENDPOINTRESPONSE._serialized_end = 2545 - _DELETENEXUSENDPOINTREQUEST._serialized_start = 2547 - _DELETENEXUSENDPOINTREQUEST._serialized_end = 2604 - _DELETENEXUSENDPOINTRESPONSE._serialized_start = 2606 - _DELETENEXUSENDPOINTRESPONSE._serialized_end = 2635 - _LISTNEXUSENDPOINTSREQUEST._serialized_start = 2637 - _LISTNEXUSENDPOINTSREQUEST._serialized_end = 2722 - _LISTNEXUSENDPOINTSRESPONSE._serialized_start = 2724 - _LISTNEXUSENDPOINTSRESPONSE._serialized_end = 2829 + _ADDORUPDATEREMOTECLUSTERREQUEST._serialized_end = 1602 + _ADDORUPDATEREMOTECLUSTERRESPONSE._serialized_start = 1604 + _ADDORUPDATEREMOTECLUSTERRESPONSE._serialized_end = 1638 + _REMOVEREMOTECLUSTERREQUEST._serialized_start = 1640 + _REMOVEREMOTECLUSTERREQUEST._serialized_end = 1690 + _REMOVEREMOTECLUSTERRESPONSE._serialized_start = 1692 + _REMOVEREMOTECLUSTERRESPONSE._serialized_end = 1721 + _LISTCLUSTERSREQUEST._serialized_start = 1723 + _LISTCLUSTERSREQUEST._serialized_end = 1788 + _LISTCLUSTERSRESPONSE._serialized_start = 1790 + _LISTCLUSTERSRESPONSE._serialized_end = 1905 + _CLUSTERMETADATA._serialized_start = 1908 + _CLUSTERMETADATA._serialized_end = 2132 + _GETNEXUSENDPOINTREQUEST._serialized_start = 2134 + _GETNEXUSENDPOINTREQUEST._serialized_end = 2171 + _GETNEXUSENDPOINTRESPONSE._serialized_start = 2173 + _GETNEXUSENDPOINTRESPONSE._serialized_end = 2250 + _CREATENEXUSENDPOINTREQUEST._serialized_start = 2252 + _CREATENEXUSENDPOINTREQUEST._serialized_end = 2331 + _CREATENEXUSENDPOINTRESPONSE._serialized_start = 2333 + _CREATENEXUSENDPOINTRESPONSE._serialized_end = 2413 + _UPDATENEXUSENDPOINTREQUEST._serialized_start = 2415 + _UPDATENEXUSENDPOINTREQUEST._serialized_end = 2523 + _UPDATENEXUSENDPOINTRESPONSE._serialized_start = 2525 + _UPDATENEXUSENDPOINTRESPONSE._serialized_end = 2605 + _DELETENEXUSENDPOINTREQUEST._serialized_start = 2607 + _DELETENEXUSENDPOINTREQUEST._serialized_end = 2664 + _DELETENEXUSENDPOINTRESPONSE._serialized_start = 2666 + _DELETENEXUSENDPOINTRESPONSE._serialized_end = 2695 + _LISTNEXUSENDPOINTSREQUEST._serialized_start = 2697 + _LISTNEXUSENDPOINTSREQUEST._serialized_end = 2782 + _LISTNEXUSENDPOINTSRESPONSE._serialized_start = 2784 + _LISTNEXUSENDPOINTSRESPONSE._serialized_end = 2889 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/operatorservice/v1/request_response_pb2.pyi b/temporalio/api/operatorservice/v1/request_response_pb2.pyi index e1043b5e1..b06ab93b8 100644 --- a/temporalio/api/operatorservice/v1/request_response_pb2.pyi +++ b/temporalio/api/operatorservice/v1/request_response_pb2.pyi @@ -307,6 +307,7 @@ class AddOrUpdateRemoteClusterRequest(google.protobuf.message.Message): FRONTEND_ADDRESS_FIELD_NUMBER: builtins.int ENABLE_REMOTE_CLUSTER_CONNECTION_FIELD_NUMBER: builtins.int FRONTEND_HTTP_ADDRESS_FIELD_NUMBER: builtins.int + ENABLE_REPLICATION_FIELD_NUMBER: builtins.int frontend_address: builtins.str """Frontend Address is a cross cluster accessible address for gRPC traffic. This field is required.""" enable_remote_cluster_connection: builtins.bool @@ -315,18 +316,23 @@ class AddOrUpdateRemoteClusterRequest(google.protobuf.message.Message): """Frontend HTTP Address is a cross cluster accessible address for HTTP traffic. This field is optional. If not provided on update, the existing HTTP address will be removed. """ + enable_replication: builtins.bool + """Controls whether replication streams are active.""" def __init__( self, *, frontend_address: builtins.str = ..., enable_remote_cluster_connection: builtins.bool = ..., frontend_http_address: builtins.str = ..., + enable_replication: builtins.bool = ..., ) -> None: ... def ClearField( self, field_name: typing_extensions.Literal[ "enable_remote_cluster_connection", b"enable_remote_cluster_connection", + "enable_replication", + b"enable_replication", "frontend_address", b"frontend_address", "frontend_http_address", @@ -431,6 +437,7 @@ class ClusterMetadata(google.protobuf.message.Message): INITIAL_FAILOVER_VERSION_FIELD_NUMBER: builtins.int HISTORY_SHARD_COUNT_FIELD_NUMBER: builtins.int IS_CONNECTION_ENABLED_FIELD_NUMBER: builtins.int + IS_REPLICATION_ENABLED_FIELD_NUMBER: builtins.int cluster_name: builtins.str """Name of the cluster name.""" cluster_id: builtins.str @@ -445,6 +452,8 @@ class ClusterMetadata(google.protobuf.message.Message): """History service shard number.""" is_connection_enabled: builtins.bool """A flag to indicate if a connection is active.""" + is_replication_enabled: builtins.bool + """A flag to indicate if replication is enabled.""" def __init__( self, *, @@ -455,6 +464,7 @@ class ClusterMetadata(google.protobuf.message.Message): initial_failover_version: builtins.int = ..., history_shard_count: builtins.int = ..., is_connection_enabled: builtins.bool = ..., + is_replication_enabled: builtins.bool = ..., ) -> None: ... def ClearField( self, @@ -473,6 +483,8 @@ class ClusterMetadata(google.protobuf.message.Message): b"initial_failover_version", "is_connection_enabled", b"is_connection_enabled", + "is_replication_enabled", + b"is_replication_enabled", ], ) -> None: ... diff --git a/temporalio/api/protocol/v1/message_pb2.py b/temporalio/api/protocol/v1/message_pb2.py index ac643ab35..f91d62157 100644 --- a/temporalio/api/protocol/v1/message_pb2.py +++ b/temporalio/api/protocol/v1/message_pb2.py @@ -27,7 +27,7 @@ (_message.Message,), { "DESCRIPTOR": _MESSAGE, - "__module__": "temporal.api.protocol.v1.message_pb2", + "__module__": "temporalio.api.protocol.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.protocol.v1.Message) }, ) diff --git a/temporalio/api/protometa/__init__.py b/temporalio/api/protometa/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/temporalio/api/protometa/v1/__init__.py b/temporalio/api/protometa/v1/__init__.py new file mode 100644 index 000000000..b63c2e8df --- /dev/null +++ b/temporalio/api/protometa/v1/__init__.py @@ -0,0 +1,5 @@ +from .annotations_pb2 import RequestHeaderAnnotation + +__all__ = [ + "RequestHeaderAnnotation", +] diff --git a/temporalio/api/protometa/v1/annotations_pb2.py b/temporalio/api/protometa/v1/annotations_pb2.py new file mode 100644 index 000000000..3f790e1fd --- /dev/null +++ b/temporalio/api/protometa/v1/annotations_pb2.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: temporal/api/protometa/v1/annotations.proto +"""Generated protocol buffer code.""" + +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2 + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n+temporal/api/protometa/v1/annotations.proto\x12\x19temporal.api.protometa.v1\x1a google/protobuf/descriptor.proto"8\n\x17RequestHeaderAnnotation\x12\x0e\n\x06header\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:m\n\x0erequest_header\x12\x1e.google.protobuf.MethodOptions\x18\xd1\xc3\xb9\x03 \x03(\x0b\x32\x32.temporal.api.protometa.v1.RequestHeaderAnnotationB\x9c\x01\n\x1cio.temporal.api.protometa.v1B\x10\x41nnotationsProtoP\x01Z)go.temporal.io/api/protometa/v1;protometa\xaa\x02\x1bTemporalio.Api.Protometa.V1\xea\x02\x1eTemporalio::Api::Protometa::V1b\x06proto3' +) + + +REQUEST_HEADER_FIELD_NUMBER = 7234001 +request_header = DESCRIPTOR.extensions_by_name["request_header"] + +_REQUESTHEADERANNOTATION = DESCRIPTOR.message_types_by_name["RequestHeaderAnnotation"] +RequestHeaderAnnotation = _reflection.GeneratedProtocolMessageType( + "RequestHeaderAnnotation", + (_message.Message,), + { + "DESCRIPTOR": _REQUESTHEADERANNOTATION, + "__module__": "temporalio.api.protometa.v1.annotations_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.protometa.v1.RequestHeaderAnnotation) + }, +) +_sym_db.RegisterMessage(RequestHeaderAnnotation) + +if _descriptor._USE_C_DESCRIPTORS == False: + google_dot_protobuf_dot_descriptor__pb2.MethodOptions.RegisterExtension( + request_header + ) + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b"\n\034io.temporal.api.protometa.v1B\020AnnotationsProtoP\001Z)go.temporal.io/api/protometa/v1;protometa\252\002\033Temporalio.Api.Protometa.V1\352\002\036Temporalio::Api::Protometa::V1" + _REQUESTHEADERANNOTATION._serialized_start = 108 + _REQUESTHEADERANNOTATION._serialized_end = 164 +# @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/protometa/v1/annotations_pb2.pyi b/temporalio/api/protometa/v1/annotations_pb2.pyi new file mode 100644 index 000000000..143d7d7d9 --- /dev/null +++ b/temporalio/api/protometa/v1/annotations_pb2.pyi @@ -0,0 +1,64 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" + +import builtins +import sys + +import google.protobuf.descriptor +import google.protobuf.descriptor_pb2 +import google.protobuf.internal.containers +import google.protobuf.internal.extension_dict +import google.protobuf.message + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class RequestHeaderAnnotation(google.protobuf.message.Message): + """RequestHeaderAnnotation allows specifying that field values from a request + should be propagated as outbound headers. + + The value field supports template interpolation where field paths enclosed + in braces will be replaced with the actual field values from the request. + For example: + value: "{workflow_execution.workflow_id}" + value: "workflow-{workflow_execution.workflow_id}" + value: "{namespace}/{workflow_execution.workflow_id}" + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + HEADER_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + header: builtins.str + """The name of the header to set (e.g., "temporal-resource-id")""" + value: builtins.str + """A template string that may contain field paths in braces. + Field paths use dot notation to traverse nested messages. + Example: "{workflow_execution.workflow_id}" + """ + def __init__( + self, + *, + header: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal["header", b"header", "value", b"value"], + ) -> None: ... + +global___RequestHeaderAnnotation = RequestHeaderAnnotation + +REQUEST_HEADER_FIELD_NUMBER: builtins.int +request_header: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[ + google.protobuf.descriptor_pb2.MethodOptions, + google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + global___RequestHeaderAnnotation + ], +] diff --git a/temporalio/api/query/v1/message_pb2.py b/temporalio/api/query/v1/message_pb2.py index 805b30f7e..aaee2715a 100644 --- a/temporalio/api/query/v1/message_pb2.py +++ b/temporalio/api/query/v1/message_pb2.py @@ -40,7 +40,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWQUERY, - "__module__": "temporal.api.query.v1.message_pb2", + "__module__": "temporalio.api.query.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.query.v1.WorkflowQuery) }, ) @@ -51,7 +51,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWQUERYRESULT, - "__module__": "temporal.api.query.v1.message_pb2", + "__module__": "temporalio.api.query.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.query.v1.WorkflowQueryResult) }, ) @@ -62,7 +62,7 @@ (_message.Message,), { "DESCRIPTOR": _QUERYREJECTED, - "__module__": "temporal.api.query.v1.message_pb2", + "__module__": "temporalio.api.query.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.query.v1.QueryRejected) }, ) diff --git a/temporalio/api/replication/v1/message_pb2.py b/temporalio/api/replication/v1/message_pb2.py index 7b5a4a158..8dbe73e1d 100644 --- a/temporalio/api/replication/v1/message_pb2.py +++ b/temporalio/api/replication/v1/message_pb2.py @@ -35,7 +35,7 @@ (_message.Message,), { "DESCRIPTOR": _CLUSTERREPLICATIONCONFIG, - "__module__": "temporal.api.replication.v1.message_pb2", + "__module__": "temporalio.api.replication.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.replication.v1.ClusterReplicationConfig) }, ) @@ -46,7 +46,7 @@ (_message.Message,), { "DESCRIPTOR": _NAMESPACEREPLICATIONCONFIG, - "__module__": "temporal.api.replication.v1.message_pb2", + "__module__": "temporalio.api.replication.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.replication.v1.NamespaceReplicationConfig) }, ) @@ -57,7 +57,7 @@ (_message.Message,), { "DESCRIPTOR": _FAILOVERSTATUS, - "__module__": "temporal.api.replication.v1.message_pb2", + "__module__": "temporalio.api.replication.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.replication.v1.FailoverStatus) }, ) diff --git a/temporalio/api/rules/v1/message_pb2.py b/temporalio/api/rules/v1/message_pb2.py index ce46e1684..ad0d0014c 100644 --- a/temporalio/api/rules/v1/message_pb2.py +++ b/temporalio/api/rules/v1/message_pb2.py @@ -39,12 +39,12 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWRULEACTION_ACTIONACTIVITYPAUSE, - "__module__": "temporal.api.rules.v1.message_pb2", + "__module__": "temporalio.api.rules.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.rules.v1.WorkflowRuleAction.ActionActivityPause) }, ), "DESCRIPTOR": _WORKFLOWRULEACTION, - "__module__": "temporal.api.rules.v1.message_pb2", + "__module__": "temporalio.api.rules.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.rules.v1.WorkflowRuleAction) }, ) @@ -60,12 +60,12 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWRULESPEC_ACTIVITYSTARTINGTRIGGER, - "__module__": "temporal.api.rules.v1.message_pb2", + "__module__": "temporalio.api.rules.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.rules.v1.WorkflowRuleSpec.ActivityStartingTrigger) }, ), "DESCRIPTOR": _WORKFLOWRULESPEC, - "__module__": "temporal.api.rules.v1.message_pb2", + "__module__": "temporalio.api.rules.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.rules.v1.WorkflowRuleSpec) }, ) @@ -77,7 +77,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWRULE, - "__module__": "temporal.api.rules.v1.message_pb2", + "__module__": "temporalio.api.rules.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.rules.v1.WorkflowRule) }, ) diff --git a/temporalio/api/schedule/v1/message_pb2.py b/temporalio/api/schedule/v1/message_pb2.py index 6180b37f9..de57c94ed 100644 --- a/temporalio/api/schedule/v1/message_pb2.py +++ b/temporalio/api/schedule/v1/message_pb2.py @@ -31,7 +31,7 @@ ) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n&temporal/api/schedule/v1/message.proto\x12\x18temporal.api.schedule.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\x1a$temporal/api/enums/v1/schedule.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a&temporal/api/workflow/v1/message.proto"\x95\x01\n\x0c\x43\x61lendarSpec\x12\x0e\n\x06second\x18\x01 \x01(\t\x12\x0e\n\x06minute\x18\x02 \x01(\t\x12\x0c\n\x04hour\x18\x03 \x01(\t\x12\x14\n\x0c\x64\x61y_of_month\x18\x04 \x01(\t\x12\r\n\x05month\x18\x05 \x01(\t\x12\x0c\n\x04year\x18\x06 \x01(\t\x12\x13\n\x0b\x64\x61y_of_week\x18\x07 \x01(\t\x12\x0f\n\x07\x63omment\x18\x08 \x01(\t"1\n\x05Range\x12\r\n\x05start\x18\x01 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x05\x12\x0c\n\x04step\x18\x03 \x01(\x05"\x86\x03\n\x16StructuredCalendarSpec\x12/\n\x06second\x18\x01 \x03(\x0b\x32\x1f.temporal.api.schedule.v1.Range\x12/\n\x06minute\x18\x02 \x03(\x0b\x32\x1f.temporal.api.schedule.v1.Range\x12-\n\x04hour\x18\x03 \x03(\x0b\x32\x1f.temporal.api.schedule.v1.Range\x12\x35\n\x0c\x64\x61y_of_month\x18\x04 \x03(\x0b\x32\x1f.temporal.api.schedule.v1.Range\x12.\n\x05month\x18\x05 \x03(\x0b\x32\x1f.temporal.api.schedule.v1.Range\x12-\n\x04year\x18\x06 \x03(\x0b\x32\x1f.temporal.api.schedule.v1.Range\x12\x34\n\x0b\x64\x61y_of_week\x18\x07 \x03(\x0b\x32\x1f.temporal.api.schedule.v1.Range\x12\x0f\n\x07\x63omment\x18\x08 \x01(\t"e\n\x0cIntervalSpec\x12+\n\x08interval\x18\x01 \x01(\x0b\x32\x19.google.protobuf.Duration\x12(\n\x05phase\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration"\xba\x04\n\x0cScheduleSpec\x12M\n\x13structured_calendar\x18\x07 \x03(\x0b\x32\x30.temporal.api.schedule.v1.StructuredCalendarSpec\x12\x13\n\x0b\x63ron_string\x18\x08 \x03(\t\x12\x38\n\x08\x63\x61lendar\x18\x01 \x03(\x0b\x32&.temporal.api.schedule.v1.CalendarSpec\x12\x38\n\x08interval\x18\x02 \x03(\x0b\x32&.temporal.api.schedule.v1.IntervalSpec\x12\x44\n\x10\x65xclude_calendar\x18\x03 \x03(\x0b\x32&.temporal.api.schedule.v1.CalendarSpecB\x02\x18\x01\x12U\n\x1b\x65xclude_structured_calendar\x18\t \x03(\x0b\x32\x30.temporal.api.schedule.v1.StructuredCalendarSpec\x12.\n\nstart_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12,\n\x08\x65nd_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12)\n\x06jitter\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x15\n\rtimezone_name\x18\n \x01(\t\x12\x15\n\rtimezone_data\x18\x0b \x01(\x0c"\xc8\x01\n\x10SchedulePolicies\x12\x44\n\x0eoverlap_policy\x18\x01 \x01(\x0e\x32,.temporal.api.enums.v1.ScheduleOverlapPolicy\x12\x31\n\x0e\x63\x61tchup_window\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x18\n\x10pause_on_failure\x18\x03 \x01(\x08\x12!\n\x19keep_original_workflow_id\x18\x04 \x01(\x08"h\n\x0eScheduleAction\x12L\n\x0estart_workflow\x18\x01 \x01(\x0b\x32\x32.temporal.api.workflow.v1.NewWorkflowExecutionInfoH\x00\x42\x08\n\x06\x61\x63tion"\x93\x02\n\x14ScheduleActionResult\x12\x31\n\rschedule_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0b\x61\x63tual_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12H\n\x15start_workflow_result\x18\x0b \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12M\n\x15start_workflow_status\x18\x0c \x01(\x0e\x32..temporal.api.enums.v1.WorkflowExecutionStatus"b\n\rScheduleState\x12\r\n\x05notes\x18\x01 \x01(\t\x12\x0e\n\x06paused\x18\x02 \x01(\x08\x12\x17\n\x0flimited_actions\x18\x03 \x01(\x08\x12\x19\n\x11remaining_actions\x18\x04 \x01(\x03"\x95\x01\n\x19TriggerImmediatelyRequest\x12\x44\n\x0eoverlap_policy\x18\x01 \x01(\x0e\x32,.temporal.api.enums.v1.ScheduleOverlapPolicy\x12\x32\n\x0escheduled_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"\xb5\x01\n\x0f\x42\x61\x63kfillRequest\x12.\n\nstart_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12,\n\x08\x65nd_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x44\n\x0eoverlap_policy\x18\x03 \x01(\x0e\x32,.temporal.api.enums.v1.ScheduleOverlapPolicy"\xc6\x01\n\rSchedulePatch\x12P\n\x13trigger_immediately\x18\x01 \x01(\x0b\x32\x33.temporal.api.schedule.v1.TriggerImmediatelyRequest\x12\x43\n\x10\x62\x61\x63kfill_request\x18\x02 \x03(\x0b\x32).temporal.api.schedule.v1.BackfillRequest\x12\r\n\x05pause\x18\x03 \x01(\t\x12\x0f\n\x07unpause\x18\x04 \x01(\t"\xd6\x03\n\x0cScheduleInfo\x12\x14\n\x0c\x61\x63tion_count\x18\x01 \x01(\x03\x12\x1d\n\x15missed_catchup_window\x18\x02 \x01(\x03\x12\x17\n\x0foverlap_skipped\x18\x03 \x01(\x03\x12\x16\n\x0e\x62uffer_dropped\x18\n \x01(\x03\x12\x13\n\x0b\x62uffer_size\x18\x0b \x01(\x03\x12\x44\n\x11running_workflows\x18\t \x03(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x46\n\x0erecent_actions\x18\x04 \x03(\x0b\x32..temporal.api.schedule.v1.ScheduleActionResult\x12\x37\n\x13\x66uture_action_times\x18\x05 \x03(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0b\x63reate_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0bupdate_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12"\n\x16invalid_schedule_error\x18\x08 \x01(\tB\x02\x18\x01"\xf0\x01\n\x08Schedule\x12\x34\n\x04spec\x18\x01 \x01(\x0b\x32&.temporal.api.schedule.v1.ScheduleSpec\x12\x38\n\x06\x61\x63tion\x18\x02 \x01(\x0b\x32(.temporal.api.schedule.v1.ScheduleAction\x12<\n\x08policies\x18\x03 \x01(\x0b\x32*.temporal.api.schedule.v1.SchedulePolicies\x12\x36\n\x05state\x18\x04 \x01(\x0b\x32\'.temporal.api.schedule.v1.ScheduleState"\xa5\x02\n\x10ScheduleListInfo\x12\x34\n\x04spec\x18\x01 \x01(\x0b\x32&.temporal.api.schedule.v1.ScheduleSpec\x12;\n\rworkflow_type\x18\x02 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12\r\n\x05notes\x18\x03 \x01(\t\x12\x0e\n\x06paused\x18\x04 \x01(\x08\x12\x46\n\x0erecent_actions\x18\x05 \x03(\x0b\x32..temporal.api.schedule.v1.ScheduleActionResult\x12\x37\n\x13\x66uture_action_times\x18\x06 \x03(\x0b\x32\x1a.google.protobuf.Timestamp"\xd3\x01\n\x11ScheduleListEntry\x12\x13\n\x0bschedule_id\x18\x01 \x01(\t\x12*\n\x04memo\x18\x02 \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo\x12\x43\n\x11search_attributes\x18\x03 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12\x38\n\x04info\x18\x04 \x01(\x0b\x32*.temporal.api.schedule.v1.ScheduleListInfoB\x93\x01\n\x1bio.temporal.api.schedule.v1B\x0cMessageProtoP\x01Z\'go.temporal.io/api/schedule/v1;schedule\xaa\x02\x1aTemporalio.Api.Schedule.V1\xea\x02\x1dTemporalio::Api::Schedule::V1b\x06proto3' + b'\n&temporal/api/schedule/v1/message.proto\x12\x18temporal.api.schedule.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\x1a$temporal/api/enums/v1/schedule.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a&temporal/api/workflow/v1/message.proto"\x95\x01\n\x0c\x43\x61lendarSpec\x12\x0e\n\x06second\x18\x01 \x01(\t\x12\x0e\n\x06minute\x18\x02 \x01(\t\x12\x0c\n\x04hour\x18\x03 \x01(\t\x12\x14\n\x0c\x64\x61y_of_month\x18\x04 \x01(\t\x12\r\n\x05month\x18\x05 \x01(\t\x12\x0c\n\x04year\x18\x06 \x01(\t\x12\x13\n\x0b\x64\x61y_of_week\x18\x07 \x01(\t\x12\x0f\n\x07\x63omment\x18\x08 \x01(\t"1\n\x05Range\x12\r\n\x05start\x18\x01 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x05\x12\x0c\n\x04step\x18\x03 \x01(\x05"\x86\x03\n\x16StructuredCalendarSpec\x12/\n\x06second\x18\x01 \x03(\x0b\x32\x1f.temporal.api.schedule.v1.Range\x12/\n\x06minute\x18\x02 \x03(\x0b\x32\x1f.temporal.api.schedule.v1.Range\x12-\n\x04hour\x18\x03 \x03(\x0b\x32\x1f.temporal.api.schedule.v1.Range\x12\x35\n\x0c\x64\x61y_of_month\x18\x04 \x03(\x0b\x32\x1f.temporal.api.schedule.v1.Range\x12.\n\x05month\x18\x05 \x03(\x0b\x32\x1f.temporal.api.schedule.v1.Range\x12-\n\x04year\x18\x06 \x03(\x0b\x32\x1f.temporal.api.schedule.v1.Range\x12\x34\n\x0b\x64\x61y_of_week\x18\x07 \x03(\x0b\x32\x1f.temporal.api.schedule.v1.Range\x12\x0f\n\x07\x63omment\x18\x08 \x01(\t"e\n\x0cIntervalSpec\x12+\n\x08interval\x18\x01 \x01(\x0b\x32\x19.google.protobuf.Duration\x12(\n\x05phase\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration"\xba\x04\n\x0cScheduleSpec\x12M\n\x13structured_calendar\x18\x07 \x03(\x0b\x32\x30.temporal.api.schedule.v1.StructuredCalendarSpec\x12\x13\n\x0b\x63ron_string\x18\x08 \x03(\t\x12\x38\n\x08\x63\x61lendar\x18\x01 \x03(\x0b\x32&.temporal.api.schedule.v1.CalendarSpec\x12\x38\n\x08interval\x18\x02 \x03(\x0b\x32&.temporal.api.schedule.v1.IntervalSpec\x12\x44\n\x10\x65xclude_calendar\x18\x03 \x03(\x0b\x32&.temporal.api.schedule.v1.CalendarSpecB\x02\x18\x01\x12U\n\x1b\x65xclude_structured_calendar\x18\t \x03(\x0b\x32\x30.temporal.api.schedule.v1.StructuredCalendarSpec\x12.\n\nstart_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12,\n\x08\x65nd_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12)\n\x06jitter\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x15\n\rtimezone_name\x18\n \x01(\t\x12\x15\n\rtimezone_data\x18\x0b \x01(\x0c"\xc8\x01\n\x10SchedulePolicies\x12\x44\n\x0eoverlap_policy\x18\x01 \x01(\x0e\x32,.temporal.api.enums.v1.ScheduleOverlapPolicy\x12\x31\n\x0e\x63\x61tchup_window\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x18\n\x10pause_on_failure\x18\x03 \x01(\x08\x12!\n\x19keep_original_workflow_id\x18\x04 \x01(\x08"h\n\x0eScheduleAction\x12L\n\x0estart_workflow\x18\x01 \x01(\x0b\x32\x32.temporal.api.workflow.v1.NewWorkflowExecutionInfoH\x00\x42\x08\n\x06\x61\x63tion"\x93\x02\n\x14ScheduleActionResult\x12\x31\n\rschedule_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0b\x61\x63tual_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12H\n\x15start_workflow_result\x18\x0b \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12M\n\x15start_workflow_status\x18\x0c \x01(\x0e\x32..temporal.api.enums.v1.WorkflowExecutionStatus"b\n\rScheduleState\x12\r\n\x05notes\x18\x01 \x01(\t\x12\x0e\n\x06paused\x18\x02 \x01(\x08\x12\x17\n\x0flimited_actions\x18\x03 \x01(\x08\x12\x19\n\x11remaining_actions\x18\x04 \x01(\x03"\x95\x01\n\x19TriggerImmediatelyRequest\x12\x44\n\x0eoverlap_policy\x18\x01 \x01(\x0e\x32,.temporal.api.enums.v1.ScheduleOverlapPolicy\x12\x32\n\x0escheduled_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"\xb5\x01\n\x0f\x42\x61\x63kfillRequest\x12.\n\nstart_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12,\n\x08\x65nd_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x44\n\x0eoverlap_policy\x18\x03 \x01(\x0e\x32,.temporal.api.enums.v1.ScheduleOverlapPolicy"\xc6\x01\n\rSchedulePatch\x12P\n\x13trigger_immediately\x18\x01 \x01(\x0b\x32\x33.temporal.api.schedule.v1.TriggerImmediatelyRequest\x12\x43\n\x10\x62\x61\x63kfill_request\x18\x02 \x03(\x0b\x32).temporal.api.schedule.v1.BackfillRequest\x12\r\n\x05pause\x18\x03 \x01(\t\x12\x0f\n\x07unpause\x18\x04 \x01(\t"\xf0\x03\n\x0cScheduleInfo\x12\x14\n\x0c\x61\x63tion_count\x18\x01 \x01(\x03\x12\x1d\n\x15missed_catchup_window\x18\x02 \x01(\x03\x12\x17\n\x0foverlap_skipped\x18\x03 \x01(\x03\x12\x16\n\x0e\x62uffer_dropped\x18\n \x01(\x03\x12\x13\n\x0b\x62uffer_size\x18\x0b \x01(\x03\x12\x44\n\x11running_workflows\x18\t \x03(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x46\n\x0erecent_actions\x18\x04 \x03(\x0b\x32..temporal.api.schedule.v1.ScheduleActionResult\x12\x37\n\x13\x66uture_action_times\x18\x05 \x03(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0b\x63reate_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0bupdate_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12"\n\x16invalid_schedule_error\x18\x08 \x01(\tB\x02\x18\x01\x12\x18\n\x10state_size_bytes\x18\x0c \x01(\x03"\xf0\x01\n\x08Schedule\x12\x34\n\x04spec\x18\x01 \x01(\x0b\x32&.temporal.api.schedule.v1.ScheduleSpec\x12\x38\n\x06\x61\x63tion\x18\x02 \x01(\x0b\x32(.temporal.api.schedule.v1.ScheduleAction\x12<\n\x08policies\x18\x03 \x01(\x0b\x32*.temporal.api.schedule.v1.SchedulePolicies\x12\x36\n\x05state\x18\x04 \x01(\x0b\x32\'.temporal.api.schedule.v1.ScheduleState"\xbf\x02\n\x10ScheduleListInfo\x12\x34\n\x04spec\x18\x01 \x01(\x0b\x32&.temporal.api.schedule.v1.ScheduleSpec\x12;\n\rworkflow_type\x18\x02 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12\r\n\x05notes\x18\x03 \x01(\t\x12\x0e\n\x06paused\x18\x04 \x01(\x08\x12\x46\n\x0erecent_actions\x18\x05 \x03(\x0b\x32..temporal.api.schedule.v1.ScheduleActionResult\x12\x37\n\x13\x66uture_action_times\x18\x06 \x03(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x18\n\x10state_size_bytes\x18\x07 \x01(\x03"\xd3\x01\n\x11ScheduleListEntry\x12\x13\n\x0bschedule_id\x18\x01 \x01(\t\x12*\n\x04memo\x18\x02 \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo\x12\x43\n\x11search_attributes\x18\x03 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12\x38\n\x04info\x18\x04 \x01(\x0b\x32*.temporal.api.schedule.v1.ScheduleListInfoB\x93\x01\n\x1bio.temporal.api.schedule.v1B\x0cMessageProtoP\x01Z\'go.temporal.io/api/schedule/v1;schedule\xaa\x02\x1aTemporalio.Api.Schedule.V1\xea\x02\x1dTemporalio::Api::Schedule::V1b\x06proto3' ) @@ -58,7 +58,7 @@ (_message.Message,), { "DESCRIPTOR": _CALENDARSPEC, - "__module__": "temporal.api.schedule.v1.message_pb2", + "__module__": "temporalio.api.schedule.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.schedule.v1.CalendarSpec) }, ) @@ -69,7 +69,7 @@ (_message.Message,), { "DESCRIPTOR": _RANGE, - "__module__": "temporal.api.schedule.v1.message_pb2", + "__module__": "temporalio.api.schedule.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.schedule.v1.Range) }, ) @@ -80,7 +80,7 @@ (_message.Message,), { "DESCRIPTOR": _STRUCTUREDCALENDARSPEC, - "__module__": "temporal.api.schedule.v1.message_pb2", + "__module__": "temporalio.api.schedule.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.schedule.v1.StructuredCalendarSpec) }, ) @@ -91,7 +91,7 @@ (_message.Message,), { "DESCRIPTOR": _INTERVALSPEC, - "__module__": "temporal.api.schedule.v1.message_pb2", + "__module__": "temporalio.api.schedule.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.schedule.v1.IntervalSpec) }, ) @@ -102,7 +102,7 @@ (_message.Message,), { "DESCRIPTOR": _SCHEDULESPEC, - "__module__": "temporal.api.schedule.v1.message_pb2", + "__module__": "temporalio.api.schedule.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.schedule.v1.ScheduleSpec) }, ) @@ -113,7 +113,7 @@ (_message.Message,), { "DESCRIPTOR": _SCHEDULEPOLICIES, - "__module__": "temporal.api.schedule.v1.message_pb2", + "__module__": "temporalio.api.schedule.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.schedule.v1.SchedulePolicies) }, ) @@ -124,7 +124,7 @@ (_message.Message,), { "DESCRIPTOR": _SCHEDULEACTION, - "__module__": "temporal.api.schedule.v1.message_pb2", + "__module__": "temporalio.api.schedule.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.schedule.v1.ScheduleAction) }, ) @@ -135,7 +135,7 @@ (_message.Message,), { "DESCRIPTOR": _SCHEDULEACTIONRESULT, - "__module__": "temporal.api.schedule.v1.message_pb2", + "__module__": "temporalio.api.schedule.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.schedule.v1.ScheduleActionResult) }, ) @@ -146,7 +146,7 @@ (_message.Message,), { "DESCRIPTOR": _SCHEDULESTATE, - "__module__": "temporal.api.schedule.v1.message_pb2", + "__module__": "temporalio.api.schedule.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.schedule.v1.ScheduleState) }, ) @@ -157,7 +157,7 @@ (_message.Message,), { "DESCRIPTOR": _TRIGGERIMMEDIATELYREQUEST, - "__module__": "temporal.api.schedule.v1.message_pb2", + "__module__": "temporalio.api.schedule.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.schedule.v1.TriggerImmediatelyRequest) }, ) @@ -168,7 +168,7 @@ (_message.Message,), { "DESCRIPTOR": _BACKFILLREQUEST, - "__module__": "temporal.api.schedule.v1.message_pb2", + "__module__": "temporalio.api.schedule.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.schedule.v1.BackfillRequest) }, ) @@ -179,7 +179,7 @@ (_message.Message,), { "DESCRIPTOR": _SCHEDULEPATCH, - "__module__": "temporal.api.schedule.v1.message_pb2", + "__module__": "temporalio.api.schedule.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.schedule.v1.SchedulePatch) }, ) @@ -190,7 +190,7 @@ (_message.Message,), { "DESCRIPTOR": _SCHEDULEINFO, - "__module__": "temporal.api.schedule.v1.message_pb2", + "__module__": "temporalio.api.schedule.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.schedule.v1.ScheduleInfo) }, ) @@ -201,7 +201,7 @@ (_message.Message,), { "DESCRIPTOR": _SCHEDULE, - "__module__": "temporal.api.schedule.v1.message_pb2", + "__module__": "temporalio.api.schedule.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.schedule.v1.Schedule) }, ) @@ -212,7 +212,7 @@ (_message.Message,), { "DESCRIPTOR": _SCHEDULELISTINFO, - "__module__": "temporal.api.schedule.v1.message_pb2", + "__module__": "temporalio.api.schedule.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.schedule.v1.ScheduleListInfo) }, ) @@ -223,7 +223,7 @@ (_message.Message,), { "DESCRIPTOR": _SCHEDULELISTENTRY, - "__module__": "temporal.api.schedule.v1.message_pb2", + "__module__": "temporalio.api.schedule.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.schedule.v1.ScheduleListEntry) }, ) @@ -263,11 +263,11 @@ _SCHEDULEPATCH._serialized_start = 2583 _SCHEDULEPATCH._serialized_end = 2781 _SCHEDULEINFO._serialized_start = 2784 - _SCHEDULEINFO._serialized_end = 3254 - _SCHEDULE._serialized_start = 3257 - _SCHEDULE._serialized_end = 3497 - _SCHEDULELISTINFO._serialized_start = 3500 - _SCHEDULELISTINFO._serialized_end = 3793 - _SCHEDULELISTENTRY._serialized_start = 3796 - _SCHEDULELISTENTRY._serialized_end = 4007 + _SCHEDULEINFO._serialized_end = 3280 + _SCHEDULE._serialized_start = 3283 + _SCHEDULE._serialized_end = 3523 + _SCHEDULELISTINFO._serialized_start = 3526 + _SCHEDULELISTINFO._serialized_end = 3845 + _SCHEDULELISTENTRY._serialized_start = 3848 + _SCHEDULELISTENTRY._serialized_end = 4059 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/schedule/v1/message_pb2.pyi b/temporalio/api/schedule/v1/message_pb2.pyi index b7a65ad0f..fe7c1e900 100644 --- a/temporalio/api/schedule/v1/message_pb2.pyi +++ b/temporalio/api/schedule/v1/message_pb2.pyi @@ -157,8 +157,8 @@ class StructuredCalendarSpec(google.protobuf.message.Message): corresponding fields of the timestamp, except for year: if year is missing, that means all years match. For all fields besides year, at least one Range must be present to match anything. - TODO: add relative-to-end-of-month - TODO: add nth day-of-week in month + Relative expressions such as "last day of the month" or "third Monday" are not currently + representable; callers must enumerate the concrete days they require. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -841,6 +841,7 @@ class ScheduleInfo(google.protobuf.message.Message): CREATE_TIME_FIELD_NUMBER: builtins.int UPDATE_TIME_FIELD_NUMBER: builtins.int INVALID_SCHEDULE_ERROR_FIELD_NUMBER: builtins.int + STATE_SIZE_BYTES_FIELD_NUMBER: builtins.int action_count: builtins.int """Number of actions taken so far.""" missed_catchup_window: builtins.int @@ -887,6 +888,8 @@ class ScheduleInfo(google.protobuf.message.Message): def update_time(self) -> google.protobuf.timestamp_pb2.Timestamp: ... invalid_schedule_error: builtins.str """Deprecated.""" + state_size_bytes: builtins.int + """Size of the schedule's internal state (including payloads) in bytes.""" def __init__( self, *, @@ -908,6 +911,7 @@ class ScheduleInfo(google.protobuf.message.Message): create_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., update_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., invalid_schedule_error: builtins.str = ..., + state_size_bytes: builtins.int = ..., ) -> None: ... def HasField( self, @@ -938,6 +942,8 @@ class ScheduleInfo(google.protobuf.message.Message): b"recent_actions", "running_workflows", b"running_workflows", + "state_size_bytes", + b"state_size_bytes", "update_time", b"update_time", ], @@ -1010,6 +1016,7 @@ class ScheduleListInfo(google.protobuf.message.Message): PAUSED_FIELD_NUMBER: builtins.int RECENT_ACTIONS_FIELD_NUMBER: builtins.int FUTURE_ACTION_TIMES_FIELD_NUMBER: builtins.int + STATE_SIZE_BYTES_FIELD_NUMBER: builtins.int @property def spec(self) -> global___ScheduleSpec: """From spec: @@ -1037,6 +1044,8 @@ class ScheduleListInfo(google.protobuf.message.Message): ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ google.protobuf.timestamp_pb2.Timestamp ]: ... + state_size_bytes: builtins.int + """Size of the schedule's internal state (including payloads) in bytes.""" def __init__( self, *, @@ -1050,6 +1059,7 @@ class ScheduleListInfo(google.protobuf.message.Message): google.protobuf.timestamp_pb2.Timestamp ] | None = ..., + state_size_bytes: builtins.int = ..., ) -> None: ... def HasField( self, @@ -1070,6 +1080,8 @@ class ScheduleListInfo(google.protobuf.message.Message): b"recent_actions", "spec", b"spec", + "state_size_bytes", + b"state_size_bytes", "workflow_type", b"workflow_type", ], diff --git a/temporalio/api/sdk/v1/__init__.py b/temporalio/api/sdk/v1/__init__.py index 2657df300..4f23aac6a 100644 --- a/temporalio/api/sdk/v1/__init__.py +++ b/temporalio/api/sdk/v1/__init__.py @@ -5,6 +5,8 @@ StackTraceFileSlice, StackTraceSDKInfo, ) +from .event_group_marker_pb2 import EventGroupMarker +from .external_storage_pb2 import ExternalStorageReference from .task_complete_metadata_pb2 import WorkflowTaskCompletedMetadata from .user_metadata_pb2 import UserMetadata from .worker_config_pb2 import WorkerConfig @@ -16,6 +18,8 @@ __all__ = [ "EnhancedStackTrace", + "EventGroupMarker", + "ExternalStorageReference", "StackTrace", "StackTraceFileLocation", "StackTraceFileSlice", diff --git a/temporalio/api/sdk/v1/enhanced_stack_trace_pb2.py b/temporalio/api/sdk/v1/enhanced_stack_trace_pb2.py index eaa8ff251..0890a547a 100644 --- a/temporalio/api/sdk/v1/enhanced_stack_trace_pb2.py +++ b/temporalio/api/sdk/v1/enhanced_stack_trace_pb2.py @@ -36,12 +36,12 @@ (_message.Message,), { "DESCRIPTOR": _ENHANCEDSTACKTRACE_SOURCESENTRY, - "__module__": "temporal.api.sdk.v1.enhanced_stack_trace_pb2", + "__module__": "temporalio.api.sdk.v1.enhanced_stack_trace_pb2", # @@protoc_insertion_point(class_scope:temporal.api.sdk.v1.EnhancedStackTrace.SourcesEntry) }, ), "DESCRIPTOR": _ENHANCEDSTACKTRACE, - "__module__": "temporal.api.sdk.v1.enhanced_stack_trace_pb2", + "__module__": "temporalio.api.sdk.v1.enhanced_stack_trace_pb2", # @@protoc_insertion_point(class_scope:temporal.api.sdk.v1.EnhancedStackTrace) }, ) @@ -53,7 +53,7 @@ (_message.Message,), { "DESCRIPTOR": _STACKTRACESDKINFO, - "__module__": "temporal.api.sdk.v1.enhanced_stack_trace_pb2", + "__module__": "temporalio.api.sdk.v1.enhanced_stack_trace_pb2", # @@protoc_insertion_point(class_scope:temporal.api.sdk.v1.StackTraceSDKInfo) }, ) @@ -64,7 +64,7 @@ (_message.Message,), { "DESCRIPTOR": _STACKTRACEFILESLICE, - "__module__": "temporal.api.sdk.v1.enhanced_stack_trace_pb2", + "__module__": "temporalio.api.sdk.v1.enhanced_stack_trace_pb2", # @@protoc_insertion_point(class_scope:temporal.api.sdk.v1.StackTraceFileSlice) }, ) @@ -75,7 +75,7 @@ (_message.Message,), { "DESCRIPTOR": _STACKTRACEFILELOCATION, - "__module__": "temporal.api.sdk.v1.enhanced_stack_trace_pb2", + "__module__": "temporalio.api.sdk.v1.enhanced_stack_trace_pb2", # @@protoc_insertion_point(class_scope:temporal.api.sdk.v1.StackTraceFileLocation) }, ) @@ -86,7 +86,7 @@ (_message.Message,), { "DESCRIPTOR": _STACKTRACE, - "__module__": "temporal.api.sdk.v1.enhanced_stack_trace_pb2", + "__module__": "temporalio.api.sdk.v1.enhanced_stack_trace_pb2", # @@protoc_insertion_point(class_scope:temporal.api.sdk.v1.StackTrace) }, ) diff --git a/temporalio/api/sdk/v1/event_group_marker_pb2.py b/temporalio/api/sdk/v1/event_group_marker_pb2.py new file mode 100644 index 000000000..3475c8588 --- /dev/null +++ b/temporalio/api/sdk/v1/event_group_marker_pb2.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: temporal/api/sdk/v1/event_group_marker.proto +"""Generated protocol buffer code.""" + +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from temporalio.api.common.v1 import ( + message_pb2 as temporal_dot_api_dot_common_dot_v1_dot_message__pb2, +) + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n,temporal/api/sdk/v1/event_group_marker.proto\x12\x13temporal.api.sdk.v1\x1a$temporal/api/common/v1/message.proto"\x92\x03\n\x10\x45ventGroupMarker\x12<\n\x05label\x18\x01 \x01(\x0b\x32+.temporal.api.sdk.v1.EventGroupMarker.LabelH\x00\x12K\n\rinbound_event\x18\x02 \x01(\x0b\x32\x32.temporal.api.sdk.v1.EventGroupMarker.InboundEventH\x00\x12M\n\x0einbound_update\x18\x03 \x01(\x0b\x32\x33.temporal.api.sdk.v1.EventGroupMarker.InboundUpdateH\x00\x1a\x43\n\x05Label\x12\n\n\x02id\x18\x01 \x01(\t\x12.\n\x05label\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload\x1a(\n\x0cInboundEvent\x12\x18\n\x10inbound_event_id\x18\x01 \x01(\x03\x1a*\n\rInboundUpdate\x12\x19\n\x11inbound_update_id\x18\x01 \x01(\tB\t\n\x07variantB\x83\x01\n\x16io.temporal.api.sdk.v1B\x15\x45ventGroupMarkerProtoP\x01Z\x1dgo.temporal.io/api/sdk/v1;sdk\xaa\x02\x15Temporalio.Api.Sdk.V1\xea\x02\x18Temporalio::Api::Sdk::V1b\x06proto3' +) + + +_EVENTGROUPMARKER = DESCRIPTOR.message_types_by_name["EventGroupMarker"] +_EVENTGROUPMARKER_LABEL = _EVENTGROUPMARKER.nested_types_by_name["Label"] +_EVENTGROUPMARKER_INBOUNDEVENT = _EVENTGROUPMARKER.nested_types_by_name["InboundEvent"] +_EVENTGROUPMARKER_INBOUNDUPDATE = _EVENTGROUPMARKER.nested_types_by_name[ + "InboundUpdate" +] +EventGroupMarker = _reflection.GeneratedProtocolMessageType( + "EventGroupMarker", + (_message.Message,), + { + "Label": _reflection.GeneratedProtocolMessageType( + "Label", + (_message.Message,), + { + "DESCRIPTOR": _EVENTGROUPMARKER_LABEL, + "__module__": "temporalio.api.sdk.v1.event_group_marker_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.sdk.v1.EventGroupMarker.Label) + }, + ), + "InboundEvent": _reflection.GeneratedProtocolMessageType( + "InboundEvent", + (_message.Message,), + { + "DESCRIPTOR": _EVENTGROUPMARKER_INBOUNDEVENT, + "__module__": "temporalio.api.sdk.v1.event_group_marker_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.sdk.v1.EventGroupMarker.InboundEvent) + }, + ), + "InboundUpdate": _reflection.GeneratedProtocolMessageType( + "InboundUpdate", + (_message.Message,), + { + "DESCRIPTOR": _EVENTGROUPMARKER_INBOUNDUPDATE, + "__module__": "temporalio.api.sdk.v1.event_group_marker_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.sdk.v1.EventGroupMarker.InboundUpdate) + }, + ), + "DESCRIPTOR": _EVENTGROUPMARKER, + "__module__": "temporalio.api.sdk.v1.event_group_marker_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.sdk.v1.EventGroupMarker) + }, +) +_sym_db.RegisterMessage(EventGroupMarker) +_sym_db.RegisterMessage(EventGroupMarker.Label) +_sym_db.RegisterMessage(EventGroupMarker.InboundEvent) +_sym_db.RegisterMessage(EventGroupMarker.InboundUpdate) + +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b"\n\026io.temporal.api.sdk.v1B\025EventGroupMarkerProtoP\001Z\035go.temporal.io/api/sdk/v1;sdk\252\002\025Temporalio.Api.Sdk.V1\352\002\030Temporalio::Api::Sdk::V1" + _EVENTGROUPMARKER._serialized_start = 108 + _EVENTGROUPMARKER._serialized_end = 510 + _EVENTGROUPMARKER_LABEL._serialized_start = 346 + _EVENTGROUPMARKER_LABEL._serialized_end = 413 + _EVENTGROUPMARKER_INBOUNDEVENT._serialized_start = 415 + _EVENTGROUPMARKER_INBOUNDEVENT._serialized_end = 455 + _EVENTGROUPMARKER_INBOUNDUPDATE._serialized_start = 457 + _EVENTGROUPMARKER_INBOUNDUPDATE._serialized_end = 499 +# @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/sdk/v1/event_group_marker_pb2.pyi b/temporalio/api/sdk/v1/event_group_marker_pb2.pyi new file mode 100644 index 000000000..63a5f53eb --- /dev/null +++ b/temporalio/api/sdk/v1/event_group_marker_pb2.pyi @@ -0,0 +1,159 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" + +import builtins +import sys + +import google.protobuf.descriptor +import google.protobuf.message + +import temporalio.api.common.v1.message_pb2 + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class EventGroupMarker(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class Label(google.protobuf.message.Message): + """A user-defined short-form string value to be used as the group's label.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ID_FIELD_NUMBER: builtins.int + LABEL_FIELD_NUMBER: builtins.int + id: builtins.str + """Opaque identifier assigned by the SDK.""" + @property + def label(self) -> temporalio.api.common.v1.message_pb2.Payload: + """This payload should be a "json/plain"-encoded payload that is a single + JSON string for use in user interfaces. User interface formatting may not + apply to this text when used in "label" situations. The payload data + section is limited to 400 bytes by default. + + Payload only needs to be set on the first use of a given Marker ID; + further references to an existing Marker ID reuse existing attributes of + the referenced Marker -- i.e. further label payloads are ignored. + + Note that it is valid to have distinct Markers (i.e. distinct Marker IDs) + in a given workflow execution that carry the same label, provided that + they have the distinct ID. + """ + def __init__( + self, + *, + id: builtins.str = ..., + label: temporalio.api.common.v1.message_pb2.Payload | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["label", b"label"] + ) -> builtins.bool: ... + def ClearField( + self, field_name: typing_extensions.Literal["id", b"id", "label", b"label"] + ) -> None: ... + + class InboundEvent(google.protobuf.message.Message): + """The event ID of an event in the present workflow that triggered implicit + creation of this group Marker. + + The target event's type must be one of the following: + - `WORKFLOW_EXECUTION_STARTED` + - `WORKFLOW_EXECUTION_SIGNALED` + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + INBOUND_EVENT_ID_FIELD_NUMBER: builtins.int + inbound_event_id: builtins.int + def __init__( + self, + *, + inbound_event_id: builtins.int = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "inbound_event_id", b"inbound_event_id" + ], + ) -> None: ... + + class InboundUpdate(google.protobuf.message.Message): + """The identifier of an inbound Update (request.meta.update_id) + whose handler triggered implicit creation of this group Marker. + + Used in place of `inbound_event_id` for Updates because the event ID of the + UpdateAccepted history event is not known until the Workflow Task is + completed and recorded by the server, which may be too late. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + INBOUND_UPDATE_ID_FIELD_NUMBER: builtins.int + inbound_update_id: builtins.str + def __init__( + self, + *, + inbound_update_id: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "inbound_update_id", b"inbound_update_id" + ], + ) -> None: ... + + LABEL_FIELD_NUMBER: builtins.int + INBOUND_EVENT_FIELD_NUMBER: builtins.int + INBOUND_UPDATE_FIELD_NUMBER: builtins.int + @property + def label(self) -> global___EventGroupMarker.Label: ... + @property + def inbound_event(self) -> global___EventGroupMarker.InboundEvent: ... + @property + def inbound_update(self) -> global___EventGroupMarker.InboundUpdate: ... + def __init__( + self, + *, + label: global___EventGroupMarker.Label | None = ..., + inbound_event: global___EventGroupMarker.InboundEvent | None = ..., + inbound_update: global___EventGroupMarker.InboundUpdate | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "inbound_event", + b"inbound_event", + "inbound_update", + b"inbound_update", + "label", + b"label", + "variant", + b"variant", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "inbound_event", + b"inbound_event", + "inbound_update", + b"inbound_update", + "label", + b"label", + "variant", + b"variant", + ], + ) -> None: ... + def WhichOneof( + self, oneof_group: typing_extensions.Literal["variant", b"variant"] + ) -> ( + typing_extensions.Literal["label", "inbound_event", "inbound_update"] | None + ): ... + +global___EventGroupMarker = EventGroupMarker diff --git a/temporalio/api/sdk/v1/external_storage_pb2.py b/temporalio/api/sdk/v1/external_storage_pb2.py new file mode 100644 index 000000000..75676a90a --- /dev/null +++ b/temporalio/api/sdk/v1/external_storage_pb2.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: temporal/api/sdk/v1/external_storage.proto +"""Generated protocol buffer code.""" + +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n*temporal/api/sdk/v1/external_storage.proto\x12\x13temporal.api.sdk.v1"\xb3\x01\n\x18\x45xternalStorageReference\x12\x13\n\x0b\x64river_name\x18\x01 \x01(\t\x12P\n\nclaim_data\x18\x02 \x03(\x0b\x32<.temporal.api.sdk.v1.ExternalStorageReference.ClaimDataEntry\x1a\x30\n\x0e\x43laimDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\x82\x01\n\x16io.temporal.api.sdk.v1B\x14\x45xternalStorageProtoP\x01Z\x1dgo.temporal.io/api/sdk/v1;sdk\xaa\x02\x15Temporalio.Api.Sdk.V1\xea\x02\x18Temporalio::Api::Sdk::V1b\x06proto3' +) + + +_EXTERNALSTORAGEREFERENCE = DESCRIPTOR.message_types_by_name["ExternalStorageReference"] +_EXTERNALSTORAGEREFERENCE_CLAIMDATAENTRY = ( + _EXTERNALSTORAGEREFERENCE.nested_types_by_name["ClaimDataEntry"] +) +ExternalStorageReference = _reflection.GeneratedProtocolMessageType( + "ExternalStorageReference", + (_message.Message,), + { + "ClaimDataEntry": _reflection.GeneratedProtocolMessageType( + "ClaimDataEntry", + (_message.Message,), + { + "DESCRIPTOR": _EXTERNALSTORAGEREFERENCE_CLAIMDATAENTRY, + "__module__": "temporalio.api.sdk.v1.external_storage_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.sdk.v1.ExternalStorageReference.ClaimDataEntry) + }, + ), + "DESCRIPTOR": _EXTERNALSTORAGEREFERENCE, + "__module__": "temporalio.api.sdk.v1.external_storage_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.sdk.v1.ExternalStorageReference) + }, +) +_sym_db.RegisterMessage(ExternalStorageReference) +_sym_db.RegisterMessage(ExternalStorageReference.ClaimDataEntry) + +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b"\n\026io.temporal.api.sdk.v1B\024ExternalStorageProtoP\001Z\035go.temporal.io/api/sdk/v1;sdk\252\002\025Temporalio.Api.Sdk.V1\352\002\030Temporalio::Api::Sdk::V1" + _EXTERNALSTORAGEREFERENCE_CLAIMDATAENTRY._options = None + _EXTERNALSTORAGEREFERENCE_CLAIMDATAENTRY._serialized_options = b"8\001" + _EXTERNALSTORAGEREFERENCE._serialized_start = 68 + _EXTERNALSTORAGEREFERENCE._serialized_end = 247 + _EXTERNALSTORAGEREFERENCE_CLAIMDATAENTRY._serialized_start = 199 + _EXTERNALSTORAGEREFERENCE_CLAIMDATAENTRY._serialized_end = 247 +# @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/sdk/v1/external_storage_pb2.pyi b/temporalio/api/sdk/v1/external_storage_pb2.pyi new file mode 100644 index 000000000..9a27636a2 --- /dev/null +++ b/temporalio/api/sdk/v1/external_storage_pb2.pyi @@ -0,0 +1,69 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" + +import builtins +import collections.abc +import sys + +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class ExternalStorageReference(google.protobuf.message.Message): + """ExternalStorageReference identifies a payload stored in an external storage system. + It is used as a claim-check token, allowing the actual payload data to be retrieved + from the named driver using the provided claim data. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class ClaimDataEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal["key", b"key", "value", b"value"], + ) -> None: ... + + DRIVER_NAME_FIELD_NUMBER: builtins.int + CLAIM_DATA_FIELD_NUMBER: builtins.int + driver_name: builtins.str + """The name of the storage driver responsible for retrieving the payload.""" + @property + def claim_data( + self, + ) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: + """Driver-specific key-value pairs that identify and provide access to the stored payload.""" + def __init__( + self, + *, + driver_name: builtins.str = ..., + claim_data: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "claim_data", b"claim_data", "driver_name", b"driver_name" + ], + ) -> None: ... + +global___ExternalStorageReference = ExternalStorageReference diff --git a/temporalio/api/sdk/v1/task_complete_metadata_pb2.py b/temporalio/api/sdk/v1/task_complete_metadata_pb2.py index 4b3306d7e..0b95e1710 100644 --- a/temporalio/api/sdk/v1/task_complete_metadata_pb2.py +++ b/temporalio/api/sdk/v1/task_complete_metadata_pb2.py @@ -27,7 +27,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWTASKCOMPLETEDMETADATA, - "__module__": "temporal.api.sdk.v1.task_complete_metadata_pb2", + "__module__": "temporalio.api.sdk.v1.task_complete_metadata_pb2", # @@protoc_insertion_point(class_scope:temporal.api.sdk.v1.WorkflowTaskCompletedMetadata) }, ) diff --git a/temporalio/api/sdk/v1/user_metadata_pb2.py b/temporalio/api/sdk/v1/user_metadata_pb2.py index bbdb6882c..4fcbee2bb 100644 --- a/temporalio/api/sdk/v1/user_metadata_pb2.py +++ b/temporalio/api/sdk/v1/user_metadata_pb2.py @@ -29,7 +29,7 @@ (_message.Message,), { "DESCRIPTOR": _USERMETADATA, - "__module__": "temporal.api.sdk.v1.user_metadata_pb2", + "__module__": "temporalio.api.sdk.v1.user_metadata_pb2", # @@protoc_insertion_point(class_scope:temporal.api.sdk.v1.UserMetadata) }, ) diff --git a/temporalio/api/sdk/v1/worker_config_pb2.py b/temporalio/api/sdk/v1/worker_config_pb2.py index c6f09582c..2a1304f6b 100644 --- a/temporalio/api/sdk/v1/worker_config_pb2.py +++ b/temporalio/api/sdk/v1/worker_config_pb2.py @@ -35,7 +35,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKERCONFIG_SIMPLEPOLLERBEHAVIOR, - "__module__": "temporal.api.sdk.v1.worker_config_pb2", + "__module__": "temporalio.api.sdk.v1.worker_config_pb2", # @@protoc_insertion_point(class_scope:temporal.api.sdk.v1.WorkerConfig.SimplePollerBehavior) }, ), @@ -44,12 +44,12 @@ (_message.Message,), { "DESCRIPTOR": _WORKERCONFIG_AUTOSCALINGPOLLERBEHAVIOR, - "__module__": "temporal.api.sdk.v1.worker_config_pb2", + "__module__": "temporalio.api.sdk.v1.worker_config_pb2", # @@protoc_insertion_point(class_scope:temporal.api.sdk.v1.WorkerConfig.AutoscalingPollerBehavior) }, ), "DESCRIPTOR": _WORKERCONFIG, - "__module__": "temporal.api.sdk.v1.worker_config_pb2", + "__module__": "temporalio.api.sdk.v1.worker_config_pb2", # @@protoc_insertion_point(class_scope:temporal.api.sdk.v1.WorkerConfig) }, ) diff --git a/temporalio/api/sdk/v1/workflow_metadata_pb2.py b/temporalio/api/sdk/v1/workflow_metadata_pb2.py index fabbe9403..c872c59f2 100644 --- a/temporalio/api/sdk/v1/workflow_metadata_pb2.py +++ b/temporalio/api/sdk/v1/workflow_metadata_pb2.py @@ -29,7 +29,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWMETADATA, - "__module__": "temporal.api.sdk.v1.workflow_metadata_pb2", + "__module__": "temporalio.api.sdk.v1.workflow_metadata_pb2", # @@protoc_insertion_point(class_scope:temporal.api.sdk.v1.WorkflowMetadata) }, ) @@ -40,7 +40,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWDEFINITION, - "__module__": "temporal.api.sdk.v1.workflow_metadata_pb2", + "__module__": "temporalio.api.sdk.v1.workflow_metadata_pb2", # @@protoc_insertion_point(class_scope:temporal.api.sdk.v1.WorkflowDefinition) }, ) @@ -51,7 +51,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWINTERACTIONDEFINITION, - "__module__": "temporal.api.sdk.v1.workflow_metadata_pb2", + "__module__": "temporalio.api.sdk.v1.workflow_metadata_pb2", # @@protoc_insertion_point(class_scope:temporal.api.sdk.v1.WorkflowInteractionDefinition) }, ) diff --git a/temporalio/api/taskqueue/v1/__init__.py b/temporalio/api/taskqueue/v1/__init__.py index 888c7ff2a..98595069b 100644 --- a/temporalio/api/taskqueue/v1/__init__.py +++ b/temporalio/api/taskqueue/v1/__init__.py @@ -4,6 +4,8 @@ CompatibleBuildIdRedirectRule, CompatibleVersionSet, ConfigMetadata, + PollerGroupInfo, + PollerGroupsInfo, PollerInfo, PollerScalingDecision, RampByPercentage, @@ -32,6 +34,8 @@ "CompatibleBuildIdRedirectRule", "CompatibleVersionSet", "ConfigMetadata", + "PollerGroupInfo", + "PollerGroupsInfo", "PollerInfo", "PollerScalingDecision", "RampByPercentage", diff --git a/temporalio/api/taskqueue/v1/message_pb2.py b/temporalio/api/taskqueue/v1/message_pb2.py index 3eccbd5cc..e5caf07ac 100644 --- a/temporalio/api/taskqueue/v1/message_pb2.py +++ b/temporalio/api/taskqueue/v1/message_pb2.py @@ -29,7 +29,7 @@ ) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n\'temporal/api/taskqueue/v1/message.proto\x12\x19temporal.api.taskqueue.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/wrappers.proto\x1a&temporal/api/enums/v1/task_queue.proto\x1a$temporal/api/common/v1/message.proto\x1a(temporal/api/deployment/v1/message.proto"b\n\tTaskQueue\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x32\n\x04kind\x18\x02 \x01(\x0e\x32$.temporal.api.enums.v1.TaskQueueKind\x12\x13\n\x0bnormal_name\x18\x03 \x01(\t"O\n\x11TaskQueueMetadata\x12:\n\x14max_tasks_per_second\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.DoubleValue"\xda\x02\n\x17TaskQueueVersioningInfo\x12W\n\x1a\x63urrent_deployment_version\x18\x07 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12\x1b\n\x0f\x63urrent_version\x18\x01 \x01(\tB\x02\x18\x01\x12W\n\x1aramping_deployment_version\x18\t \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12\x1b\n\x0framping_version\x18\x02 \x01(\tB\x02\x18\x01\x12"\n\x1aramping_version_percentage\x18\x03 \x01(\x02\x12/\n\x0bupdate_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"W\n\x19TaskQueueVersionSelection\x12\x11\n\tbuild_ids\x18\x01 \x03(\t\x12\x13\n\x0bunversioned\x18\x02 \x01(\x08\x12\x12\n\nall_active\x18\x03 \x01(\x08"\x95\x02\n\x14TaskQueueVersionInfo\x12R\n\ntypes_info\x18\x01 \x03(\x0b\x32>.temporal.api.taskqueue.v1.TaskQueueVersionInfo.TypesInfoEntry\x12I\n\x11task_reachability\x18\x02 \x01(\x0e\x32..temporal.api.enums.v1.BuildIdTaskReachability\x1a^\n\x0eTypesInfoEntry\x12\x0b\n\x03key\x18\x01 \x01(\x05\x12;\n\x05value\x18\x02 \x01(\x0b\x32,.temporal.api.taskqueue.v1.TaskQueueTypeInfo:\x02\x38\x01"\x85\x01\n\x11TaskQueueTypeInfo\x12\x36\n\x07pollers\x18\x01 \x03(\x0b\x32%.temporal.api.taskqueue.v1.PollerInfo\x12\x38\n\x05stats\x18\x02 \x01(\x0b\x32).temporal.api.taskqueue.v1.TaskQueueStats"\xa4\x01\n\x0eTaskQueueStats\x12!\n\x19\x61pproximate_backlog_count\x18\x01 \x01(\x03\x12:\n\x17\x61pproximate_backlog_age\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x16\n\x0etasks_add_rate\x18\x03 \x01(\x02\x12\x1b\n\x13tasks_dispatch_rate\x18\x04 \x01(\x02"\xac\x01\n\x0fTaskQueueStatus\x12\x1a\n\x12\x62\x61\x63klog_count_hint\x18\x01 \x01(\x03\x12\x12\n\nread_level\x18\x02 \x01(\x03\x12\x11\n\tack_level\x18\x03 \x01(\x03\x12\x17\n\x0frate_per_second\x18\x04 \x01(\x01\x12=\n\rtask_id_block\x18\x05 \x01(\x0b\x32&.temporal.api.taskqueue.v1.TaskIdBlock"/\n\x0bTaskIdBlock\x12\x10\n\x08start_id\x18\x01 \x01(\x03\x12\x0e\n\x06\x65nd_id\x18\x02 \x01(\x03"B\n\x1aTaskQueuePartitionMetadata\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x17\n\x0fowner_host_name\x18\x02 \x01(\t"\x9a\x02\n\nPollerInfo\x12\x34\n\x10last_access_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x10\n\x08identity\x18\x02 \x01(\t\x12\x17\n\x0frate_per_second\x18\x03 \x01(\x01\x12Z\n\x1bworker_version_capabilities\x18\x04 \x01(\x0b\x32\x31.temporal.api.common.v1.WorkerVersionCapabilitiesB\x02\x18\x01\x12O\n\x12\x64\x65ployment_options\x18\x05 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentOptions"\x9a\x01\n\x19StickyExecutionAttributes\x12?\n\x11worker_task_queue\x18\x01 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12<\n\x19schedule_to_start_timeout\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration")\n\x14\x43ompatibleVersionSet\x12\x11\n\tbuild_ids\x18\x01 \x03(\t"j\n\x15TaskQueueReachability\x12\x12\n\ntask_queue\x18\x01 \x01(\t\x12=\n\x0creachability\x18\x02 \x03(\x0e\x32\'.temporal.api.enums.v1.TaskReachability"z\n\x13\x42uildIdReachability\x12\x10\n\x08\x62uild_id\x18\x01 \x01(\t\x12Q\n\x17task_queue_reachability\x18\x02 \x03(\x0b\x32\x30.temporal.api.taskqueue.v1.TaskQueueReachability"+\n\x10RampByPercentage\x12\x17\n\x0framp_percentage\x18\x01 \x01(\x02"\x80\x01\n\x15\x42uildIdAssignmentRule\x12\x17\n\x0ftarget_build_id\x18\x01 \x01(\t\x12\x46\n\x0fpercentage_ramp\x18\x03 \x01(\x0b\x32+.temporal.api.taskqueue.v1.RampByPercentageH\x00\x42\x06\n\x04ramp"Q\n\x1d\x43ompatibleBuildIdRedirectRule\x12\x17\n\x0fsource_build_id\x18\x01 \x01(\t\x12\x17\n\x0ftarget_build_id\x18\x02 \x01(\t"\x93\x01\n TimestampedBuildIdAssignmentRule\x12>\n\x04rule\x18\x01 \x01(\x0b\x32\x30.temporal.api.taskqueue.v1.BuildIdAssignmentRule\x12/\n\x0b\x63reate_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"\xa3\x01\n(TimestampedCompatibleBuildIdRedirectRule\x12\x46\n\x04rule\x18\x01 \x01(\x0b\x32\x38.temporal.api.taskqueue.v1.CompatibleBuildIdRedirectRule\x12/\n\x0b\x63reate_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp">\n\x15PollerScalingDecision\x12%\n\x1dpoll_request_delta_suggestion\x18\x01 \x01(\x05"(\n\tRateLimit\x12\x1b\n\x13requests_per_second\x18\x01 \x01(\x02"j\n\x0e\x43onfigMetadata\x12\x0e\n\x06reason\x18\x01 \x01(\t\x12\x17\n\x0fupdate_identity\x18\x02 \x01(\t\x12/\n\x0bupdate_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"\x88\x01\n\x0fRateLimitConfig\x12\x38\n\nrate_limit\x18\x01 \x01(\x0b\x32$.temporal.api.taskqueue.v1.RateLimit\x12;\n\x08metadata\x18\x02 \x01(\x0b\x32).temporal.api.taskqueue.v1.ConfigMetadata"\xad\x01\n\x0fTaskQueueConfig\x12\x44\n\x10queue_rate_limit\x18\x01 \x01(\x0b\x32*.temporal.api.taskqueue.v1.RateLimitConfig\x12T\n fairness_keys_rate_limit_default\x18\x02 \x01(\x0b\x32*.temporal.api.taskqueue.v1.RateLimitConfigB\x98\x01\n\x1cio.temporal.api.taskqueue.v1B\x0cMessageProtoP\x01Z)go.temporal.io/api/taskqueue/v1;taskqueue\xaa\x02\x1bTemporalio.Api.TaskQueue.V1\xea\x02\x1eTemporalio::Api::TaskQueue::V1b\x06proto3' + b'\n\'temporal/api/taskqueue/v1/message.proto\x12\x19temporal.api.taskqueue.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/wrappers.proto\x1a&temporal/api/enums/v1/task_queue.proto\x1a$temporal/api/common/v1/message.proto\x1a(temporal/api/deployment/v1/message.proto"b\n\tTaskQueue\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x32\n\x04kind\x18\x02 \x01(\x0e\x32$.temporal.api.enums.v1.TaskQueueKind\x12\x13\n\x0bnormal_name\x18\x03 \x01(\t"O\n\x11TaskQueueMetadata\x12:\n\x14max_tasks_per_second\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.DoubleValue"\xda\x02\n\x17TaskQueueVersioningInfo\x12W\n\x1a\x63urrent_deployment_version\x18\x07 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12\x1b\n\x0f\x63urrent_version\x18\x01 \x01(\tB\x02\x18\x01\x12W\n\x1aramping_deployment_version\x18\t \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12\x1b\n\x0framping_version\x18\x02 \x01(\tB\x02\x18\x01\x12"\n\x1aramping_version_percentage\x18\x03 \x01(\x02\x12/\n\x0bupdate_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"W\n\x19TaskQueueVersionSelection\x12\x11\n\tbuild_ids\x18\x01 \x03(\t\x12\x13\n\x0bunversioned\x18\x02 \x01(\x08\x12\x12\n\nall_active\x18\x03 \x01(\x08"\x95\x02\n\x14TaskQueueVersionInfo\x12R\n\ntypes_info\x18\x01 \x03(\x0b\x32>.temporal.api.taskqueue.v1.TaskQueueVersionInfo.TypesInfoEntry\x12I\n\x11task_reachability\x18\x02 \x01(\x0e\x32..temporal.api.enums.v1.BuildIdTaskReachability\x1a^\n\x0eTypesInfoEntry\x12\x0b\n\x03key\x18\x01 \x01(\x05\x12;\n\x05value\x18\x02 \x01(\x0b\x32,.temporal.api.taskqueue.v1.TaskQueueTypeInfo:\x02\x38\x01"\x85\x01\n\x11TaskQueueTypeInfo\x12\x36\n\x07pollers\x18\x01 \x03(\x0b\x32%.temporal.api.taskqueue.v1.PollerInfo\x12\x38\n\x05stats\x18\x02 \x01(\x0b\x32).temporal.api.taskqueue.v1.TaskQueueStats"\xc2\x01\n\x0eTaskQueueStats\x12!\n\x19\x61pproximate_backlog_count\x18\x01 \x01(\x03\x12:\n\x17\x61pproximate_backlog_age\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x16\n\x0etasks_add_rate\x18\x03 \x01(\x02\x12\x1b\n\x13tasks_dispatch_rate\x18\x04 \x01(\x02\x12\x1c\n\x14rate_limiting_active\x18\x05 \x01(\x08"\xac\x01\n\x0fTaskQueueStatus\x12\x1a\n\x12\x62\x61\x63klog_count_hint\x18\x01 \x01(\x03\x12\x12\n\nread_level\x18\x02 \x01(\x03\x12\x11\n\tack_level\x18\x03 \x01(\x03\x12\x17\n\x0frate_per_second\x18\x04 \x01(\x01\x12=\n\rtask_id_block\x18\x05 \x01(\x0b\x32&.temporal.api.taskqueue.v1.TaskIdBlock"/\n\x0bTaskIdBlock\x12\x10\n\x08start_id\x18\x01 \x01(\x03\x12\x0e\n\x06\x65nd_id\x18\x02 \x01(\x03"B\n\x1aTaskQueuePartitionMetadata\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x17\n\x0fowner_host_name\x18\x02 \x01(\t"\x9a\x02\n\nPollerInfo\x12\x34\n\x10last_access_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x10\n\x08identity\x18\x02 \x01(\t\x12\x17\n\x0frate_per_second\x18\x03 \x01(\x01\x12Z\n\x1bworker_version_capabilities\x18\x04 \x01(\x0b\x32\x31.temporal.api.common.v1.WorkerVersionCapabilitiesB\x02\x18\x01\x12O\n\x12\x64\x65ployment_options\x18\x05 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentOptions"\x9a\x01\n\x19StickyExecutionAttributes\x12?\n\x11worker_task_queue\x18\x01 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12<\n\x19schedule_to_start_timeout\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration")\n\x14\x43ompatibleVersionSet\x12\x11\n\tbuild_ids\x18\x01 \x03(\t"j\n\x15TaskQueueReachability\x12\x12\n\ntask_queue\x18\x01 \x01(\t\x12=\n\x0creachability\x18\x02 \x03(\x0e\x32\'.temporal.api.enums.v1.TaskReachability"z\n\x13\x42uildIdReachability\x12\x10\n\x08\x62uild_id\x18\x01 \x01(\t\x12Q\n\x17task_queue_reachability\x18\x02 \x03(\x0b\x32\x30.temporal.api.taskqueue.v1.TaskQueueReachability"+\n\x10RampByPercentage\x12\x17\n\x0framp_percentage\x18\x01 \x01(\x02"\x80\x01\n\x15\x42uildIdAssignmentRule\x12\x17\n\x0ftarget_build_id\x18\x01 \x01(\t\x12\x46\n\x0fpercentage_ramp\x18\x03 \x01(\x0b\x32+.temporal.api.taskqueue.v1.RampByPercentageH\x00\x42\x06\n\x04ramp"Q\n\x1d\x43ompatibleBuildIdRedirectRule\x12\x17\n\x0fsource_build_id\x18\x01 \x01(\t\x12\x17\n\x0ftarget_build_id\x18\x02 \x01(\t"\x93\x01\n TimestampedBuildIdAssignmentRule\x12>\n\x04rule\x18\x01 \x01(\x0b\x32\x30.temporal.api.taskqueue.v1.BuildIdAssignmentRule\x12/\n\x0b\x63reate_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"\xa3\x01\n(TimestampedCompatibleBuildIdRedirectRule\x12\x46\n\x04rule\x18\x01 \x01(\x0b\x32\x38.temporal.api.taskqueue.v1.CompatibleBuildIdRedirectRule\x12/\n\x0b\x63reate_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"-\n\x0fPollerGroupInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0e\n\x06weight\x18\x02 \x01(\x02"f\n\x10PollerGroupsInfo\x12\x0f\n\x07version\x18\x01 \x01(\x03\x12\x41\n\rpoller_groups\x18\x02 \x03(\x0b\x32*.temporal.api.taskqueue.v1.PollerGroupInfo">\n\x15PollerScalingDecision\x12%\n\x1dpoll_request_delta_suggestion\x18\x01 \x01(\x05"(\n\tRateLimit\x12\x1b\n\x13requests_per_second\x18\x01 \x01(\x02"j\n\x0e\x43onfigMetadata\x12\x0e\n\x06reason\x18\x01 \x01(\t\x12\x17\n\x0fupdate_identity\x18\x02 \x01(\t\x12/\n\x0bupdate_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"\x88\x01\n\x0fRateLimitConfig\x12\x38\n\nrate_limit\x18\x01 \x01(\x0b\x32$.temporal.api.taskqueue.v1.RateLimit\x12;\n\x08metadata\x18\x02 \x01(\x0b\x32).temporal.api.taskqueue.v1.ConfigMetadata"\xd9\x02\n\x0fTaskQueueConfig\x12\x44\n\x10queue_rate_limit\x18\x01 \x01(\x0b\x32*.temporal.api.taskqueue.v1.RateLimitConfig\x12T\n fairness_keys_rate_limit_default\x18\x02 \x01(\x0b\x32*.temporal.api.taskqueue.v1.RateLimitConfig\x12j\n\x19\x66\x61irness_weight_overrides\x18\x03 \x03(\x0b\x32G.temporal.api.taskqueue.v1.TaskQueueConfig.FairnessWeightOverridesEntry\x1a>\n\x1c\x46\x61irnessWeightOverridesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x02:\x02\x38\x01\x42\x98\x01\n\x1cio.temporal.api.taskqueue.v1B\x0cMessageProtoP\x01Z)go.temporal.io/api/taskqueue/v1;taskqueue\xaa\x02\x1bTemporalio.Api.TaskQueue.V1\xea\x02\x1eTemporalio::Api::TaskQueue::V1b\x06proto3' ) @@ -68,17 +68,22 @@ _TIMESTAMPEDCOMPATIBLEBUILDIDREDIRECTRULE = DESCRIPTOR.message_types_by_name[ "TimestampedCompatibleBuildIdRedirectRule" ] +_POLLERGROUPINFO = DESCRIPTOR.message_types_by_name["PollerGroupInfo"] +_POLLERGROUPSINFO = DESCRIPTOR.message_types_by_name["PollerGroupsInfo"] _POLLERSCALINGDECISION = DESCRIPTOR.message_types_by_name["PollerScalingDecision"] _RATELIMIT = DESCRIPTOR.message_types_by_name["RateLimit"] _CONFIGMETADATA = DESCRIPTOR.message_types_by_name["ConfigMetadata"] _RATELIMITCONFIG = DESCRIPTOR.message_types_by_name["RateLimitConfig"] _TASKQUEUECONFIG = DESCRIPTOR.message_types_by_name["TaskQueueConfig"] +_TASKQUEUECONFIG_FAIRNESSWEIGHTOVERRIDESENTRY = _TASKQUEUECONFIG.nested_types_by_name[ + "FairnessWeightOverridesEntry" +] TaskQueue = _reflection.GeneratedProtocolMessageType( "TaskQueue", (_message.Message,), { "DESCRIPTOR": _TASKQUEUE, - "__module__": "temporal.api.taskqueue.v1.message_pb2", + "__module__": "temporalio.api.taskqueue.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.taskqueue.v1.TaskQueue) }, ) @@ -89,7 +94,7 @@ (_message.Message,), { "DESCRIPTOR": _TASKQUEUEMETADATA, - "__module__": "temporal.api.taskqueue.v1.message_pb2", + "__module__": "temporalio.api.taskqueue.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.taskqueue.v1.TaskQueueMetadata) }, ) @@ -100,7 +105,7 @@ (_message.Message,), { "DESCRIPTOR": _TASKQUEUEVERSIONINGINFO, - "__module__": "temporal.api.taskqueue.v1.message_pb2", + "__module__": "temporalio.api.taskqueue.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.taskqueue.v1.TaskQueueVersioningInfo) }, ) @@ -111,7 +116,7 @@ (_message.Message,), { "DESCRIPTOR": _TASKQUEUEVERSIONSELECTION, - "__module__": "temporal.api.taskqueue.v1.message_pb2", + "__module__": "temporalio.api.taskqueue.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.taskqueue.v1.TaskQueueVersionSelection) }, ) @@ -126,12 +131,12 @@ (_message.Message,), { "DESCRIPTOR": _TASKQUEUEVERSIONINFO_TYPESINFOENTRY, - "__module__": "temporal.api.taskqueue.v1.message_pb2", + "__module__": "temporalio.api.taskqueue.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.taskqueue.v1.TaskQueueVersionInfo.TypesInfoEntry) }, ), "DESCRIPTOR": _TASKQUEUEVERSIONINFO, - "__module__": "temporal.api.taskqueue.v1.message_pb2", + "__module__": "temporalio.api.taskqueue.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.taskqueue.v1.TaskQueueVersionInfo) }, ) @@ -143,7 +148,7 @@ (_message.Message,), { "DESCRIPTOR": _TASKQUEUETYPEINFO, - "__module__": "temporal.api.taskqueue.v1.message_pb2", + "__module__": "temporalio.api.taskqueue.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.taskqueue.v1.TaskQueueTypeInfo) }, ) @@ -154,7 +159,7 @@ (_message.Message,), { "DESCRIPTOR": _TASKQUEUESTATS, - "__module__": "temporal.api.taskqueue.v1.message_pb2", + "__module__": "temporalio.api.taskqueue.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.taskqueue.v1.TaskQueueStats) }, ) @@ -165,7 +170,7 @@ (_message.Message,), { "DESCRIPTOR": _TASKQUEUESTATUS, - "__module__": "temporal.api.taskqueue.v1.message_pb2", + "__module__": "temporalio.api.taskqueue.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.taskqueue.v1.TaskQueueStatus) }, ) @@ -176,7 +181,7 @@ (_message.Message,), { "DESCRIPTOR": _TASKIDBLOCK, - "__module__": "temporal.api.taskqueue.v1.message_pb2", + "__module__": "temporalio.api.taskqueue.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.taskqueue.v1.TaskIdBlock) }, ) @@ -187,7 +192,7 @@ (_message.Message,), { "DESCRIPTOR": _TASKQUEUEPARTITIONMETADATA, - "__module__": "temporal.api.taskqueue.v1.message_pb2", + "__module__": "temporalio.api.taskqueue.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.taskqueue.v1.TaskQueuePartitionMetadata) }, ) @@ -198,7 +203,7 @@ (_message.Message,), { "DESCRIPTOR": _POLLERINFO, - "__module__": "temporal.api.taskqueue.v1.message_pb2", + "__module__": "temporalio.api.taskqueue.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.taskqueue.v1.PollerInfo) }, ) @@ -209,7 +214,7 @@ (_message.Message,), { "DESCRIPTOR": _STICKYEXECUTIONATTRIBUTES, - "__module__": "temporal.api.taskqueue.v1.message_pb2", + "__module__": "temporalio.api.taskqueue.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.taskqueue.v1.StickyExecutionAttributes) }, ) @@ -220,7 +225,7 @@ (_message.Message,), { "DESCRIPTOR": _COMPATIBLEVERSIONSET, - "__module__": "temporal.api.taskqueue.v1.message_pb2", + "__module__": "temporalio.api.taskqueue.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.taskqueue.v1.CompatibleVersionSet) }, ) @@ -231,7 +236,7 @@ (_message.Message,), { "DESCRIPTOR": _TASKQUEUEREACHABILITY, - "__module__": "temporal.api.taskqueue.v1.message_pb2", + "__module__": "temporalio.api.taskqueue.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.taskqueue.v1.TaskQueueReachability) }, ) @@ -242,7 +247,7 @@ (_message.Message,), { "DESCRIPTOR": _BUILDIDREACHABILITY, - "__module__": "temporal.api.taskqueue.v1.message_pb2", + "__module__": "temporalio.api.taskqueue.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.taskqueue.v1.BuildIdReachability) }, ) @@ -253,7 +258,7 @@ (_message.Message,), { "DESCRIPTOR": _RAMPBYPERCENTAGE, - "__module__": "temporal.api.taskqueue.v1.message_pb2", + "__module__": "temporalio.api.taskqueue.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.taskqueue.v1.RampByPercentage) }, ) @@ -264,7 +269,7 @@ (_message.Message,), { "DESCRIPTOR": _BUILDIDASSIGNMENTRULE, - "__module__": "temporal.api.taskqueue.v1.message_pb2", + "__module__": "temporalio.api.taskqueue.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.taskqueue.v1.BuildIdAssignmentRule) }, ) @@ -275,7 +280,7 @@ (_message.Message,), { "DESCRIPTOR": _COMPATIBLEBUILDIDREDIRECTRULE, - "__module__": "temporal.api.taskqueue.v1.message_pb2", + "__module__": "temporalio.api.taskqueue.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.taskqueue.v1.CompatibleBuildIdRedirectRule) }, ) @@ -286,7 +291,7 @@ (_message.Message,), { "DESCRIPTOR": _TIMESTAMPEDBUILDIDASSIGNMENTRULE, - "__module__": "temporal.api.taskqueue.v1.message_pb2", + "__module__": "temporalio.api.taskqueue.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.taskqueue.v1.TimestampedBuildIdAssignmentRule) }, ) @@ -297,18 +302,40 @@ (_message.Message,), { "DESCRIPTOR": _TIMESTAMPEDCOMPATIBLEBUILDIDREDIRECTRULE, - "__module__": "temporal.api.taskqueue.v1.message_pb2", + "__module__": "temporalio.api.taskqueue.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.taskqueue.v1.TimestampedCompatibleBuildIdRedirectRule) }, ) _sym_db.RegisterMessage(TimestampedCompatibleBuildIdRedirectRule) +PollerGroupInfo = _reflection.GeneratedProtocolMessageType( + "PollerGroupInfo", + (_message.Message,), + { + "DESCRIPTOR": _POLLERGROUPINFO, + "__module__": "temporalio.api.taskqueue.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.taskqueue.v1.PollerGroupInfo) + }, +) +_sym_db.RegisterMessage(PollerGroupInfo) + +PollerGroupsInfo = _reflection.GeneratedProtocolMessageType( + "PollerGroupsInfo", + (_message.Message,), + { + "DESCRIPTOR": _POLLERGROUPSINFO, + "__module__": "temporalio.api.taskqueue.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.taskqueue.v1.PollerGroupsInfo) + }, +) +_sym_db.RegisterMessage(PollerGroupsInfo) + PollerScalingDecision = _reflection.GeneratedProtocolMessageType( "PollerScalingDecision", (_message.Message,), { "DESCRIPTOR": _POLLERSCALINGDECISION, - "__module__": "temporal.api.taskqueue.v1.message_pb2", + "__module__": "temporalio.api.taskqueue.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.taskqueue.v1.PollerScalingDecision) }, ) @@ -319,7 +346,7 @@ (_message.Message,), { "DESCRIPTOR": _RATELIMIT, - "__module__": "temporal.api.taskqueue.v1.message_pb2", + "__module__": "temporalio.api.taskqueue.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.taskqueue.v1.RateLimit) }, ) @@ -330,7 +357,7 @@ (_message.Message,), { "DESCRIPTOR": _CONFIGMETADATA, - "__module__": "temporal.api.taskqueue.v1.message_pb2", + "__module__": "temporalio.api.taskqueue.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.taskqueue.v1.ConfigMetadata) }, ) @@ -341,7 +368,7 @@ (_message.Message,), { "DESCRIPTOR": _RATELIMITCONFIG, - "__module__": "temporal.api.taskqueue.v1.message_pb2", + "__module__": "temporalio.api.taskqueue.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.taskqueue.v1.RateLimitConfig) }, ) @@ -351,12 +378,22 @@ "TaskQueueConfig", (_message.Message,), { + "FairnessWeightOverridesEntry": _reflection.GeneratedProtocolMessageType( + "FairnessWeightOverridesEntry", + (_message.Message,), + { + "DESCRIPTOR": _TASKQUEUECONFIG_FAIRNESSWEIGHTOVERRIDESENTRY, + "__module__": "temporalio.api.taskqueue.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.taskqueue.v1.TaskQueueConfig.FairnessWeightOverridesEntry) + }, + ), "DESCRIPTOR": _TASKQUEUECONFIG, - "__module__": "temporal.api.taskqueue.v1.message_pb2", + "__module__": "temporalio.api.taskqueue.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.taskqueue.v1.TaskQueueConfig) }, ) _sym_db.RegisterMessage(TaskQueueConfig) +_sym_db.RegisterMessage(TaskQueueConfig.FairnessWeightOverridesEntry) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None @@ -375,6 +412,8 @@ _POLLERINFO.fields_by_name[ "worker_version_capabilities" ]._serialized_options = b"\030\001" + _TASKQUEUECONFIG_FAIRNESSWEIGHTOVERRIDESENTRY._options = None + _TASKQUEUECONFIG_FAIRNESSWEIGHTOVERRIDESENTRY._serialized_options = b"8\001" _TASKQUEUE._serialized_start = 287 _TASKQUEUE._serialized_end = 385 _TASKQUEUEMETADATA._serialized_start = 387 @@ -390,41 +429,47 @@ _TASKQUEUETYPEINFO._serialized_start = 1187 _TASKQUEUETYPEINFO._serialized_end = 1320 _TASKQUEUESTATS._serialized_start = 1323 - _TASKQUEUESTATS._serialized_end = 1487 - _TASKQUEUESTATUS._serialized_start = 1490 - _TASKQUEUESTATUS._serialized_end = 1662 - _TASKIDBLOCK._serialized_start = 1664 - _TASKIDBLOCK._serialized_end = 1711 - _TASKQUEUEPARTITIONMETADATA._serialized_start = 1713 - _TASKQUEUEPARTITIONMETADATA._serialized_end = 1779 - _POLLERINFO._serialized_start = 1782 - _POLLERINFO._serialized_end = 2064 - _STICKYEXECUTIONATTRIBUTES._serialized_start = 2067 - _STICKYEXECUTIONATTRIBUTES._serialized_end = 2221 - _COMPATIBLEVERSIONSET._serialized_start = 2223 - _COMPATIBLEVERSIONSET._serialized_end = 2264 - _TASKQUEUEREACHABILITY._serialized_start = 2266 - _TASKQUEUEREACHABILITY._serialized_end = 2372 - _BUILDIDREACHABILITY._serialized_start = 2374 - _BUILDIDREACHABILITY._serialized_end = 2496 - _RAMPBYPERCENTAGE._serialized_start = 2498 - _RAMPBYPERCENTAGE._serialized_end = 2541 - _BUILDIDASSIGNMENTRULE._serialized_start = 2544 - _BUILDIDASSIGNMENTRULE._serialized_end = 2672 - _COMPATIBLEBUILDIDREDIRECTRULE._serialized_start = 2674 - _COMPATIBLEBUILDIDREDIRECTRULE._serialized_end = 2755 - _TIMESTAMPEDBUILDIDASSIGNMENTRULE._serialized_start = 2758 - _TIMESTAMPEDBUILDIDASSIGNMENTRULE._serialized_end = 2905 - _TIMESTAMPEDCOMPATIBLEBUILDIDREDIRECTRULE._serialized_start = 2908 - _TIMESTAMPEDCOMPATIBLEBUILDIDREDIRECTRULE._serialized_end = 3071 - _POLLERSCALINGDECISION._serialized_start = 3073 - _POLLERSCALINGDECISION._serialized_end = 3135 - _RATELIMIT._serialized_start = 3137 - _RATELIMIT._serialized_end = 3177 - _CONFIGMETADATA._serialized_start = 3179 - _CONFIGMETADATA._serialized_end = 3285 - _RATELIMITCONFIG._serialized_start = 3288 - _RATELIMITCONFIG._serialized_end = 3424 - _TASKQUEUECONFIG._serialized_start = 3427 - _TASKQUEUECONFIG._serialized_end = 3600 + _TASKQUEUESTATS._serialized_end = 1517 + _TASKQUEUESTATUS._serialized_start = 1520 + _TASKQUEUESTATUS._serialized_end = 1692 + _TASKIDBLOCK._serialized_start = 1694 + _TASKIDBLOCK._serialized_end = 1741 + _TASKQUEUEPARTITIONMETADATA._serialized_start = 1743 + _TASKQUEUEPARTITIONMETADATA._serialized_end = 1809 + _POLLERINFO._serialized_start = 1812 + _POLLERINFO._serialized_end = 2094 + _STICKYEXECUTIONATTRIBUTES._serialized_start = 2097 + _STICKYEXECUTIONATTRIBUTES._serialized_end = 2251 + _COMPATIBLEVERSIONSET._serialized_start = 2253 + _COMPATIBLEVERSIONSET._serialized_end = 2294 + _TASKQUEUEREACHABILITY._serialized_start = 2296 + _TASKQUEUEREACHABILITY._serialized_end = 2402 + _BUILDIDREACHABILITY._serialized_start = 2404 + _BUILDIDREACHABILITY._serialized_end = 2526 + _RAMPBYPERCENTAGE._serialized_start = 2528 + _RAMPBYPERCENTAGE._serialized_end = 2571 + _BUILDIDASSIGNMENTRULE._serialized_start = 2574 + _BUILDIDASSIGNMENTRULE._serialized_end = 2702 + _COMPATIBLEBUILDIDREDIRECTRULE._serialized_start = 2704 + _COMPATIBLEBUILDIDREDIRECTRULE._serialized_end = 2785 + _TIMESTAMPEDBUILDIDASSIGNMENTRULE._serialized_start = 2788 + _TIMESTAMPEDBUILDIDASSIGNMENTRULE._serialized_end = 2935 + _TIMESTAMPEDCOMPATIBLEBUILDIDREDIRECTRULE._serialized_start = 2938 + _TIMESTAMPEDCOMPATIBLEBUILDIDREDIRECTRULE._serialized_end = 3101 + _POLLERGROUPINFO._serialized_start = 3103 + _POLLERGROUPINFO._serialized_end = 3148 + _POLLERGROUPSINFO._serialized_start = 3150 + _POLLERGROUPSINFO._serialized_end = 3252 + _POLLERSCALINGDECISION._serialized_start = 3254 + _POLLERSCALINGDECISION._serialized_end = 3316 + _RATELIMIT._serialized_start = 3318 + _RATELIMIT._serialized_end = 3358 + _CONFIGMETADATA._serialized_start = 3360 + _CONFIGMETADATA._serialized_end = 3466 + _RATELIMITCONFIG._serialized_start = 3469 + _RATELIMITCONFIG._serialized_end = 3605 + _TASKQUEUECONFIG._serialized_start = 3608 + _TASKQUEUECONFIG._serialized_end = 3953 + _TASKQUEUECONFIG_FAIRNESSWEIGHTOVERRIDESENTRY._serialized_start = 3891 + _TASKQUEUECONFIG_FAIRNESSWEIGHTOVERRIDESENTRY._serialized_end = 3953 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/taskqueue/v1/message_pb2.pyi b/temporalio/api/taskqueue/v1/message_pb2.pyi index 7ea6e9fdb..3b43a559f 100644 --- a/temporalio/api/taskqueue/v1/message_pb2.pyi +++ b/temporalio/api/taskqueue/v1/message_pb2.pyi @@ -86,8 +86,6 @@ class TaskQueueMetadata(google.protobuf.message.Message): global___TaskQueueMetadata = TaskQueueMetadata class TaskQueueVersioningInfo(google.protobuf.message.Message): - """Experimental. Worker Deployments are experimental and might significantly change in the future.""" - DESCRIPTOR: google.protobuf.descriptor.Descriptor CURRENT_DEPLOYMENT_VERSION_FIELD_NUMBER: builtins.int @@ -318,6 +316,7 @@ class TaskQueueStats(google.protobuf.message.Message): APPROXIMATE_BACKLOG_AGE_FIELD_NUMBER: builtins.int TASKS_ADD_RATE_FIELD_NUMBER: builtins.int TASKS_DISPATCH_RATE_FIELD_NUMBER: builtins.int + RATE_LIMITING_ACTIVE_FIELD_NUMBER: builtins.int approximate_backlog_count: builtins.int """The approximate number of tasks backlogged in this task queue. May count expired tasks but eventually converges to the right value. Can be relied upon for scaling decisions. @@ -367,6 +366,12 @@ class TaskQueueStats(google.protobuf.message.Message): workflow goes to a normal queue, and the rest workflow tasks go to the Sticky queue associated with a specific worker instance. """ + rate_limiting_active: builtins.bool + """Whether rate limiting blocked any dispatches within the recent observation window (approximately + 30 seconds). When true, adding more workers will not increase throughput — the bottleneck is the + rate limit, not worker count. This field is useful for auto-scaling systems to avoid unnecessary + scale-up. + """ def __init__( self, *, @@ -374,6 +379,7 @@ class TaskQueueStats(google.protobuf.message.Message): approximate_backlog_age: google.protobuf.duration_pb2.Duration | None = ..., tasks_add_rate: builtins.float = ..., tasks_dispatch_rate: builtins.float = ..., + rate_limiting_active: builtins.bool = ..., ) -> None: ... def HasField( self, @@ -388,6 +394,8 @@ class TaskQueueStats(google.protobuf.message.Message): b"approximate_backlog_age", "approximate_backlog_count", b"approximate_backlog_count", + "rate_limiting_active", + b"rate_limiting_active", "tasks_add_rate", b"tasks_add_rate", "tasks_dispatch_rate", @@ -897,6 +905,61 @@ global___TimestampedCompatibleBuildIdRedirectRule = ( TimestampedCompatibleBuildIdRedirectRule ) +class PollerGroupInfo(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ID_FIELD_NUMBER: builtins.int + WEIGHT_FIELD_NUMBER: builtins.int + id: builtins.str + weight: builtins.float + def __init__( + self, + *, + id: builtins.str = ..., + weight: builtins.float = ..., + ) -> None: ... + def ClearField( + self, field_name: typing_extensions.Literal["id", b"id", "weight", b"weight"] + ) -> None: ... + +global___PollerGroupInfo = PollerGroupInfo + +class PollerGroupsInfo(google.protobuf.message.Message): + """A versioned snapshot of the poller groups the client should use for future polls to a task + queue. The version is monotonically increasing so that a client can ignore a snapshot that is + older than the one it has already applied. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VERSION_FIELD_NUMBER: builtins.int + POLLER_GROUPS_FIELD_NUMBER: builtins.int + version: builtins.int + """Monotonically increasing version of this snapshot. A client should ignore any snapshot whose + version is not greater than the one it last applied. + """ + @property + def poller_groups( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + global___PollerGroupInfo + ]: + """The weighted list of poller groups the client should use for future polls to this task queue.""" + def __init__( + self, + *, + version: builtins.int = ..., + poller_groups: collections.abc.Iterable[global___PollerGroupInfo] | None = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "poller_groups", b"poller_groups", "version", b"version" + ], + ) -> None: ... + +global___PollerGroupsInfo = PollerGroupsInfo + class PollerScalingDecision(google.protobuf.message.Message): """Attached to task responses to give hints to the SDK about how it may adjust its number of pollers. @@ -1019,19 +1082,45 @@ global___RateLimitConfig = RateLimitConfig class TaskQueueConfig(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor + class FairnessWeightOverridesEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.float + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.float = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal["key", b"key", "value", b"value"], + ) -> None: ... + QUEUE_RATE_LIMIT_FIELD_NUMBER: builtins.int FAIRNESS_KEYS_RATE_LIMIT_DEFAULT_FIELD_NUMBER: builtins.int + FAIRNESS_WEIGHT_OVERRIDES_FIELD_NUMBER: builtins.int @property def queue_rate_limit(self) -> global___RateLimitConfig: """Unless modified, this is the system-defined rate limit.""" @property def fairness_keys_rate_limit_default(self) -> global___RateLimitConfig: """If set, each individual fairness key will be limited to this rate, scaled by the weight of the fairness key.""" + @property + def fairness_weight_overrides( + self, + ) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.float]: + """If set, overrides the fairness weights for the corresponding fairness keys.""" def __init__( self, *, queue_rate_limit: global___RateLimitConfig | None = ..., fairness_keys_rate_limit_default: global___RateLimitConfig | None = ..., + fairness_weight_overrides: collections.abc.Mapping[builtins.str, builtins.float] + | None = ..., ) -> None: ... def HasField( self, @@ -1047,6 +1136,8 @@ class TaskQueueConfig(google.protobuf.message.Message): field_name: typing_extensions.Literal[ "fairness_keys_rate_limit_default", b"fairness_keys_rate_limit_default", + "fairness_weight_overrides", + b"fairness_weight_overrides", "queue_rate_limit", b"queue_rate_limit", ], diff --git a/temporalio/api/testservice/v1/request_response_pb2.py b/temporalio/api/testservice/v1/request_response_pb2.py index 51ef70f65..65e52f72a 100644 --- a/temporalio/api/testservice/v1/request_response_pb2.py +++ b/temporalio/api/testservice/v1/request_response_pb2.py @@ -39,7 +39,7 @@ (_message.Message,), { "DESCRIPTOR": _LOCKTIMESKIPPINGREQUEST, - "__module__": "temporal.api.testservice.v1.request_response_pb2", + "__module__": "temporalio.api.testservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.testservice.v1.LockTimeSkippingRequest) }, ) @@ -50,7 +50,7 @@ (_message.Message,), { "DESCRIPTOR": _LOCKTIMESKIPPINGRESPONSE, - "__module__": "temporal.api.testservice.v1.request_response_pb2", + "__module__": "temporalio.api.testservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.testservice.v1.LockTimeSkippingResponse) }, ) @@ -61,7 +61,7 @@ (_message.Message,), { "DESCRIPTOR": _UNLOCKTIMESKIPPINGREQUEST, - "__module__": "temporal.api.testservice.v1.request_response_pb2", + "__module__": "temporalio.api.testservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.testservice.v1.UnlockTimeSkippingRequest) }, ) @@ -72,7 +72,7 @@ (_message.Message,), { "DESCRIPTOR": _UNLOCKTIMESKIPPINGRESPONSE, - "__module__": "temporal.api.testservice.v1.request_response_pb2", + "__module__": "temporalio.api.testservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.testservice.v1.UnlockTimeSkippingResponse) }, ) @@ -83,7 +83,7 @@ (_message.Message,), { "DESCRIPTOR": _SLEEPUNTILREQUEST, - "__module__": "temporal.api.testservice.v1.request_response_pb2", + "__module__": "temporalio.api.testservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.testservice.v1.SleepUntilRequest) }, ) @@ -94,7 +94,7 @@ (_message.Message,), { "DESCRIPTOR": _SLEEPREQUEST, - "__module__": "temporal.api.testservice.v1.request_response_pb2", + "__module__": "temporalio.api.testservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.testservice.v1.SleepRequest) }, ) @@ -105,7 +105,7 @@ (_message.Message,), { "DESCRIPTOR": _SLEEPRESPONSE, - "__module__": "temporal.api.testservice.v1.request_response_pb2", + "__module__": "temporalio.api.testservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.testservice.v1.SleepResponse) }, ) @@ -116,7 +116,7 @@ (_message.Message,), { "DESCRIPTOR": _GETCURRENTTIMERESPONSE, - "__module__": "temporal.api.testservice.v1.request_response_pb2", + "__module__": "temporalio.api.testservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.testservice.v1.GetCurrentTimeResponse) }, ) diff --git a/temporalio/api/update/v1/message_pb2.py b/temporalio/api/update/v1/message_pb2.py index badea1dd1..d136516bc 100644 --- a/temporalio/api/update/v1/message_pb2.py +++ b/temporalio/api/update/v1/message_pb2.py @@ -25,7 +25,7 @@ ) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n$temporal/api/update/v1/message.proto\x12\x16temporal.api.update.v1\x1a$temporal/api/common/v1/message.proto\x1a"temporal/api/enums/v1/update.proto\x1a%temporal/api/failure/v1/message.proto"c\n\nWaitPolicy\x12U\n\x0flifecycle_stage\x18\x01 \x01(\x0e\x32<.temporal.api.enums.v1.UpdateWorkflowExecutionLifecycleStage"e\n\tUpdateRef\x12\x45\n\x12workflow_execution\x18\x01 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x11\n\tupdate_id\x18\x02 \x01(\t"|\n\x07Outcome\x12\x33\n\x07success\x18\x01 \x01(\x0b\x32 .temporal.api.common.v1.PayloadsH\x00\x12\x33\n\x07\x66\x61ilure\x18\x02 \x01(\x0b\x32 .temporal.api.failure.v1.FailureH\x00\x42\x07\n\x05value"+\n\x04Meta\x12\x11\n\tupdate_id\x18\x01 \x01(\t\x12\x10\n\x08identity\x18\x02 \x01(\t"u\n\x05Input\x12.\n\x06header\x18\x01 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12\x0c\n\x04name\x18\x02 \x01(\t\x12.\n\x04\x61rgs\x18\x03 \x01(\x0b\x32 .temporal.api.common.v1.Payloads"c\n\x07Request\x12*\n\x04meta\x18\x01 \x01(\x0b\x32\x1c.temporal.api.update.v1.Meta\x12,\n\x05input\x18\x02 \x01(\x0b\x32\x1d.temporal.api.update.v1.Input"\xcc\x01\n\tRejection\x12#\n\x1brejected_request_message_id\x18\x01 \x01(\t\x12,\n$rejected_request_sequencing_event_id\x18\x02 \x01(\x03\x12\x39\n\x10rejected_request\x18\x03 \x01(\x0b\x32\x1f.temporal.api.update.v1.Request\x12\x31\n\x07\x66\x61ilure\x18\x04 \x01(\x0b\x32 .temporal.api.failure.v1.Failure"\x9a\x01\n\nAcceptance\x12#\n\x1b\x61\x63\x63\x65pted_request_message_id\x18\x01 \x01(\t\x12,\n$accepted_request_sequencing_event_id\x18\x02 \x01(\x03\x12\x39\n\x10\x61\x63\x63\x65pted_request\x18\x03 \x01(\x0b\x32\x1f.temporal.api.update.v1.Request"h\n\x08Response\x12*\n\x04meta\x18\x01 \x01(\x0b\x32\x1c.temporal.api.update.v1.Meta\x12\x30\n\x07outcome\x18\x02 \x01(\x0b\x32\x1f.temporal.api.update.v1.OutcomeB\x89\x01\n\x19io.temporal.api.update.v1B\x0cMessageProtoP\x01Z#go.temporal.io/api/update/v1;update\xaa\x02\x18Temporalio.Api.Update.V1\xea\x02\x1bTemporalio::Api::Update::V1b\x06proto3' + b'\n$temporal/api/update/v1/message.proto\x12\x16temporal.api.update.v1\x1a$temporal/api/common/v1/message.proto\x1a"temporal/api/enums/v1/update.proto\x1a%temporal/api/failure/v1/message.proto"c\n\nWaitPolicy\x12U\n\x0flifecycle_stage\x18\x01 \x01(\x0e\x32<.temporal.api.enums.v1.UpdateWorkflowExecutionLifecycleStage"e\n\tUpdateRef\x12\x45\n\x12workflow_execution\x18\x01 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x11\n\tupdate_id\x18\x02 \x01(\t"|\n\x07Outcome\x12\x33\n\x07success\x18\x01 \x01(\x0b\x32 .temporal.api.common.v1.PayloadsH\x00\x12\x33\n\x07\x66\x61ilure\x18\x02 \x01(\x0b\x32 .temporal.api.failure.v1.FailureH\x00\x42\x07\n\x05value"+\n\x04Meta\x12\x11\n\tupdate_id\x18\x01 \x01(\t\x12\x10\n\x08identity\x18\x02 \x01(\t"u\n\x05Input\x12.\n\x06header\x18\x01 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12\x0c\n\x04name\x18\x02 \x01(\t\x12.\n\x04\x61rgs\x18\x03 \x01(\x0b\x32 .temporal.api.common.v1.Payloads"\xe4\x01\n\x07Request\x12*\n\x04meta\x18\x01 \x01(\x0b\x32\x1c.temporal.api.update.v1.Meta\x12,\n\x05input\x18\x02 \x01(\x0b\x32\x1d.temporal.api.update.v1.Input\x12\x12\n\nrequest_id\x18\x03 \x01(\t\x12>\n\x14\x63ompletion_callbacks\x18\x04 \x03(\x0b\x32 .temporal.api.common.v1.Callback\x12+\n\x05links\x18\x05 \x03(\x0b\x32\x1c.temporal.api.common.v1.Link"\xcc\x01\n\tRejection\x12#\n\x1brejected_request_message_id\x18\x01 \x01(\t\x12,\n$rejected_request_sequencing_event_id\x18\x02 \x01(\x03\x12\x39\n\x10rejected_request\x18\x03 \x01(\x0b\x32\x1f.temporal.api.update.v1.Request\x12\x31\n\x07\x66\x61ilure\x18\x04 \x01(\x0b\x32 .temporal.api.failure.v1.Failure"\x9a\x01\n\nAcceptance\x12#\n\x1b\x61\x63\x63\x65pted_request_message_id\x18\x01 \x01(\t\x12,\n$accepted_request_sequencing_event_id\x18\x02 \x01(\x03\x12\x39\n\x10\x61\x63\x63\x65pted_request\x18\x03 \x01(\x0b\x32\x1f.temporal.api.update.v1.Request"h\n\x08Response\x12*\n\x04meta\x18\x01 \x01(\x0b\x32\x1c.temporal.api.update.v1.Meta\x12\x30\n\x07outcome\x18\x02 \x01(\x0b\x32\x1f.temporal.api.update.v1.OutcomeB\x89\x01\n\x19io.temporal.api.update.v1B\x0cMessageProtoP\x01Z#go.temporal.io/api/update/v1;update\xaa\x02\x18Temporalio.Api.Update.V1\xea\x02\x1bTemporalio::Api::Update::V1b\x06proto3' ) @@ -43,7 +43,7 @@ (_message.Message,), { "DESCRIPTOR": _WAITPOLICY, - "__module__": "temporal.api.update.v1.message_pb2", + "__module__": "temporalio.api.update.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.update.v1.WaitPolicy) }, ) @@ -54,7 +54,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATEREF, - "__module__": "temporal.api.update.v1.message_pb2", + "__module__": "temporalio.api.update.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.update.v1.UpdateRef) }, ) @@ -65,7 +65,7 @@ (_message.Message,), { "DESCRIPTOR": _OUTCOME, - "__module__": "temporal.api.update.v1.message_pb2", + "__module__": "temporalio.api.update.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.update.v1.Outcome) }, ) @@ -76,7 +76,7 @@ (_message.Message,), { "DESCRIPTOR": _META, - "__module__": "temporal.api.update.v1.message_pb2", + "__module__": "temporalio.api.update.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.update.v1.Meta) }, ) @@ -87,7 +87,7 @@ (_message.Message,), { "DESCRIPTOR": _INPUT, - "__module__": "temporal.api.update.v1.message_pb2", + "__module__": "temporalio.api.update.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.update.v1.Input) }, ) @@ -98,7 +98,7 @@ (_message.Message,), { "DESCRIPTOR": _REQUEST, - "__module__": "temporal.api.update.v1.message_pb2", + "__module__": "temporalio.api.update.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.update.v1.Request) }, ) @@ -109,7 +109,7 @@ (_message.Message,), { "DESCRIPTOR": _REJECTION, - "__module__": "temporal.api.update.v1.message_pb2", + "__module__": "temporalio.api.update.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.update.v1.Rejection) }, ) @@ -120,7 +120,7 @@ (_message.Message,), { "DESCRIPTOR": _ACCEPTANCE, - "__module__": "temporal.api.update.v1.message_pb2", + "__module__": "temporalio.api.update.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.update.v1.Acceptance) }, ) @@ -131,7 +131,7 @@ (_message.Message,), { "DESCRIPTOR": _RESPONSE, - "__module__": "temporal.api.update.v1.message_pb2", + "__module__": "temporalio.api.update.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.update.v1.Response) }, ) @@ -150,12 +150,12 @@ _META._serialized_end = 550 _INPUT._serialized_start = 552 _INPUT._serialized_end = 669 - _REQUEST._serialized_start = 671 - _REQUEST._serialized_end = 770 - _REJECTION._serialized_start = 773 - _REJECTION._serialized_end = 977 - _ACCEPTANCE._serialized_start = 980 - _ACCEPTANCE._serialized_end = 1134 - _RESPONSE._serialized_start = 1136 - _RESPONSE._serialized_end = 1240 + _REQUEST._serialized_start = 672 + _REQUEST._serialized_end = 900 + _REJECTION._serialized_start = 903 + _REJECTION._serialized_end = 1107 + _ACCEPTANCE._serialized_start = 1110 + _ACCEPTANCE._serialized_end = 1264 + _RESPONSE._serialized_start = 1266 + _RESPONSE._serialized_end = 1370 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/update/v1/message_pb2.pyi b/temporalio/api/update/v1/message_pb2.pyi index 072821a0c..de3c6682c 100644 --- a/temporalio/api/update/v1/message_pb2.pyi +++ b/temporalio/api/update/v1/message_pb2.pyi @@ -4,9 +4,11 @@ isort:skip_file """ import builtins +import collections.abc import sys import google.protobuf.descriptor +import google.protobuf.internal.containers import google.protobuf.message import temporalio.api.common.v1.message_pb2 @@ -183,21 +185,59 @@ class Request(google.protobuf.message.Message): META_FIELD_NUMBER: builtins.int INPUT_FIELD_NUMBER: builtins.int + REQUEST_ID_FIELD_NUMBER: builtins.int + COMPLETION_CALLBACKS_FIELD_NUMBER: builtins.int + LINKS_FIELD_NUMBER: builtins.int @property def meta(self) -> global___Meta: ... @property def input(self) -> global___Input: ... + request_id: builtins.str + """The request ID of the request.""" + @property + def completion_callbacks( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.common.v1.message_pb2.Callback + ]: + """Callbacks to be called by the server when this update reaches a terminal state.""" + @property + def links( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.common.v1.message_pb2.Link + ]: + """Links to be associated with this update.""" def __init__( self, *, meta: global___Meta | None = ..., input: global___Input | None = ..., + request_id: builtins.str = ..., + completion_callbacks: collections.abc.Iterable[ + temporalio.api.common.v1.message_pb2.Callback + ] + | None = ..., + links: collections.abc.Iterable[temporalio.api.common.v1.message_pb2.Link] + | None = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal["input", b"input", "meta", b"meta"] ) -> builtins.bool: ... def ClearField( - self, field_name: typing_extensions.Literal["input", b"input", "meta", b"meta"] + self, + field_name: typing_extensions.Literal[ + "completion_callbacks", + b"completion_callbacks", + "input", + b"input", + "links", + b"links", + "meta", + b"meta", + "request_id", + b"request_id", + ], ) -> None: ... global___Request = Request diff --git a/temporalio/api/version/v1/message_pb2.py b/temporalio/api/version/v1/message_pb2.py index de0b7a6ce..e17a3ed9f 100644 --- a/temporalio/api/version/v1/message_pb2.py +++ b/temporalio/api/version/v1/message_pb2.py @@ -33,7 +33,7 @@ (_message.Message,), { "DESCRIPTOR": _RELEASEINFO, - "__module__": "temporal.api.version.v1.message_pb2", + "__module__": "temporalio.api.version.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.version.v1.ReleaseInfo) }, ) @@ -44,7 +44,7 @@ (_message.Message,), { "DESCRIPTOR": _ALERT, - "__module__": "temporal.api.version.v1.message_pb2", + "__module__": "temporalio.api.version.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.version.v1.Alert) }, ) @@ -55,7 +55,7 @@ (_message.Message,), { "DESCRIPTOR": _VERSIONINFO, - "__module__": "temporal.api.version.v1.message_pb2", + "__module__": "temporalio.api.version.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.version.v1.VersionInfo) }, ) diff --git a/temporalio/api/worker/v1/__init__.py b/temporalio/api/worker/v1/__init__.py index ba261cbe3..8b6e343e5 100644 --- a/temporalio/api/worker/v1/__init__.py +++ b/temporalio/api/worker/v1/__init__.py @@ -1,17 +1,31 @@ from .message_pb2 import ( + CancelActivityCommand, + CancelActivityResult, + EnvironmentInfo, PluginInfo, + StorageDriverInfo, + WorkerCommand, + WorkerCommandResult, WorkerHeartbeat, WorkerHostInfo, WorkerInfo, + WorkerListInfo, WorkerPollerInfo, WorkerSlotsInfo, ) __all__ = [ + "CancelActivityCommand", + "CancelActivityResult", + "EnvironmentInfo", "PluginInfo", + "StorageDriverInfo", + "WorkerCommand", + "WorkerCommandResult", "WorkerHeartbeat", "WorkerHostInfo", "WorkerInfo", + "WorkerListInfo", "WorkerPollerInfo", "WorkerSlotsInfo", ] diff --git a/temporalio/api/worker/v1/message_pb2.py b/temporalio/api/worker/v1/message_pb2.py index 4a5820368..78b58817a 100644 --- a/temporalio/api/worker/v1/message_pb2.py +++ b/temporalio/api/worker/v1/message_pb2.py @@ -25,7 +25,7 @@ ) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n$temporal/api/worker/v1/message.proto\x12\x16temporal.api.worker.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a(temporal/api/deployment/v1/message.proto\x1a"temporal/api/enums/v1/common.proto"\x82\x01\n\x10WorkerPollerInfo\x12\x17\n\x0f\x63urrent_pollers\x18\x01 \x01(\x05\x12=\n\x19last_successful_poll_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x16\n\x0eis_autoscaling\x18\x03 \x01(\x08"\xf1\x01\n\x0fWorkerSlotsInfo\x12\x1f\n\x17\x63urrent_available_slots\x18\x01 \x01(\x05\x12\x1a\n\x12\x63urrent_used_slots\x18\x02 \x01(\x05\x12\x1a\n\x12slot_supplier_kind\x18\x03 \x01(\t\x12\x1d\n\x15total_processed_tasks\x18\x04 \x01(\x05\x12\x1a\n\x12total_failed_tasks\x18\x05 \x01(\x05\x12%\n\x1dlast_interval_processed_tasks\x18\x06 \x01(\x05\x12#\n\x1blast_interval_failure_tasks\x18\x07 \x01(\x05"\x8c\x01\n\x0eWorkerHostInfo\x12\x11\n\thost_name\x18\x01 \x01(\t\x12\x13\n\x0bprocess_key\x18\x05 \x01(\t\x12\x12\n\nprocess_id\x18\x02 \x01(\t\x12\x1e\n\x16\x63urrent_host_cpu_usage\x18\x03 \x01(\x02\x12\x1e\n\x16\x63urrent_host_mem_usage\x18\x04 \x01(\x02"\xcf\t\n\x0fWorkerHeartbeat\x12\x1b\n\x13worker_instance_key\x18\x01 \x01(\t\x12\x17\n\x0fworker_identity\x18\x02 \x01(\t\x12\x39\n\thost_info\x18\x03 \x01(\x0b\x32&.temporal.api.worker.v1.WorkerHostInfo\x12\x12\n\ntask_queue\x18\x04 \x01(\t\x12O\n\x12\x64\x65ployment_version\x18\x05 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12\x10\n\x08sdk_name\x18\x06 \x01(\t\x12\x13\n\x0bsdk_version\x18\x07 \x01(\t\x12\x33\n\x06status\x18\x08 \x01(\x0e\x32#.temporal.api.enums.v1.WorkerStatus\x12.\n\nstart_time\x18\t \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x32\n\x0eheartbeat_time\x18\n \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12?\n\x1c\x65lapsed_since_last_heartbeat\x18\x0b \x01(\x0b\x32\x19.google.protobuf.Duration\x12I\n\x18workflow_task_slots_info\x18\x0c \x01(\x0b\x32\'.temporal.api.worker.v1.WorkerSlotsInfo\x12I\n\x18\x61\x63tivity_task_slots_info\x18\r \x01(\x0b\x32\'.temporal.api.worker.v1.WorkerSlotsInfo\x12\x46\n\x15nexus_task_slots_info\x18\x0e \x01(\x0b\x32\'.temporal.api.worker.v1.WorkerSlotsInfo\x12J\n\x19local_activity_slots_info\x18\x0f \x01(\x0b\x32\'.temporal.api.worker.v1.WorkerSlotsInfo\x12\x46\n\x14workflow_poller_info\x18\x10 \x01(\x0b\x32(.temporal.api.worker.v1.WorkerPollerInfo\x12M\n\x1bworkflow_sticky_poller_info\x18\x11 \x01(\x0b\x32(.temporal.api.worker.v1.WorkerPollerInfo\x12\x46\n\x14\x61\x63tivity_poller_info\x18\x12 \x01(\x0b\x32(.temporal.api.worker.v1.WorkerPollerInfo\x12\x43\n\x11nexus_poller_info\x18\x13 \x01(\x0b\x32(.temporal.api.worker.v1.WorkerPollerInfo\x12\x1e\n\x16total_sticky_cache_hit\x18\x14 \x01(\x05\x12\x1f\n\x17total_sticky_cache_miss\x18\x15 \x01(\x05\x12!\n\x19\x63urrent_sticky_cache_size\x18\x16 \x01(\x05\x12\x33\n\x07plugins\x18\x17 \x03(\x0b\x32".temporal.api.worker.v1.PluginInfo"O\n\nWorkerInfo\x12\x41\n\x10worker_heartbeat\x18\x01 \x01(\x0b\x32\'.temporal.api.worker.v1.WorkerHeartbeat"+\n\nPluginInfo\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\tB\x89\x01\n\x19io.temporal.api.worker.v1B\x0cMessageProtoP\x01Z#go.temporal.io/api/worker/v1;worker\xaa\x02\x18Temporalio.Api.Worker.V1\xea\x02\x1bTemporalio::Api::Worker::V1b\x06proto3' + b'\n$temporal/api/worker/v1/message.proto\x12\x16temporal.api.worker.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a(temporal/api/deployment/v1/message.proto\x1a"temporal/api/enums/v1/common.proto"\x82\x01\n\x10WorkerPollerInfo\x12\x17\n\x0f\x63urrent_pollers\x18\x01 \x01(\x05\x12=\n\x19last_successful_poll_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x16\n\x0eis_autoscaling\x18\x03 \x01(\x08"\xf1\x01\n\x0fWorkerSlotsInfo\x12\x1f\n\x17\x63urrent_available_slots\x18\x01 \x01(\x05\x12\x1a\n\x12\x63urrent_used_slots\x18\x02 \x01(\x05\x12\x1a\n\x12slot_supplier_kind\x18\x03 \x01(\t\x12\x1d\n\x15total_processed_tasks\x18\x04 \x01(\x05\x12\x1a\n\x12total_failed_tasks\x18\x05 \x01(\x05\x12%\n\x1dlast_interval_processed_tasks\x18\x06 \x01(\x05\x12#\n\x1blast_interval_failure_tasks\x18\x07 \x01(\x05"\x94\x01\n\x0eWorkerHostInfo\x12\x11\n\thost_name\x18\x01 \x01(\t\x12\x1b\n\x13worker_grouping_key\x18\x05 \x01(\t\x12\x12\n\nprocess_id\x18\x02 \x01(\t\x12\x1e\n\x16\x63urrent_host_cpu_usage\x18\x03 \x01(\x02\x12\x1e\n\x16\x63urrent_host_mem_usage\x18\x04 \x01(\x02"\xc9\n\n\x0fWorkerHeartbeat\x12\x1b\n\x13worker_instance_key\x18\x01 \x01(\t\x12\x17\n\x0fworker_identity\x18\x02 \x01(\t\x12\x39\n\thost_info\x18\x03 \x01(\x0b\x32&.temporal.api.worker.v1.WorkerHostInfo\x12\x12\n\ntask_queue\x18\x04 \x01(\t\x12O\n\x12\x64\x65ployment_version\x18\x05 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12\x10\n\x08sdk_name\x18\x06 \x01(\t\x12\x13\n\x0bsdk_version\x18\x07 \x01(\t\x12\x33\n\x06status\x18\x08 \x01(\x0e\x32#.temporal.api.enums.v1.WorkerStatus\x12.\n\nstart_time\x18\t \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x32\n\x0eheartbeat_time\x18\n \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12?\n\x1c\x65lapsed_since_last_heartbeat\x18\x0b \x01(\x0b\x32\x19.google.protobuf.Duration\x12I\n\x18workflow_task_slots_info\x18\x0c \x01(\x0b\x32\'.temporal.api.worker.v1.WorkerSlotsInfo\x12I\n\x18\x61\x63tivity_task_slots_info\x18\r \x01(\x0b\x32\'.temporal.api.worker.v1.WorkerSlotsInfo\x12\x46\n\x15nexus_task_slots_info\x18\x0e \x01(\x0b\x32\'.temporal.api.worker.v1.WorkerSlotsInfo\x12J\n\x19local_activity_slots_info\x18\x0f \x01(\x0b\x32\'.temporal.api.worker.v1.WorkerSlotsInfo\x12\x46\n\x14workflow_poller_info\x18\x10 \x01(\x0b\x32(.temporal.api.worker.v1.WorkerPollerInfo\x12M\n\x1bworkflow_sticky_poller_info\x18\x11 \x01(\x0b\x32(.temporal.api.worker.v1.WorkerPollerInfo\x12\x46\n\x14\x61\x63tivity_poller_info\x18\x12 \x01(\x0b\x32(.temporal.api.worker.v1.WorkerPollerInfo\x12\x43\n\x11nexus_poller_info\x18\x13 \x01(\x0b\x32(.temporal.api.worker.v1.WorkerPollerInfo\x12\x1e\n\x16total_sticky_cache_hit\x18\x14 \x01(\x05\x12\x1f\n\x17total_sticky_cache_miss\x18\x15 \x01(\x05\x12!\n\x19\x63urrent_sticky_cache_size\x18\x16 \x01(\x05\x12\x33\n\x07plugins\x18\x17 \x03(\x0b\x32".temporal.api.worker.v1.PluginInfo\x12:\n\x07\x64rivers\x18\x18 \x03(\x0b\x32).temporal.api.worker.v1.StorageDriverInfo\x12<\n\x0b\x65nvironment\x18\x19 \x01(\x0b\x32\'.temporal.api.worker.v1.EnvironmentInfo"O\n\nWorkerInfo\x12\x41\n\x10worker_heartbeat\x18\x01 \x01(\x0b\x32\'.temporal.api.worker.v1.WorkerHeartbeat"\xec\x03\n\x0eWorkerListInfo\x12\x1b\n\x13worker_instance_key\x18\x01 \x01(\t\x12\x17\n\x0fworker_identity\x18\x02 \x01(\t\x12\x12\n\ntask_queue\x18\x03 \x01(\t\x12O\n\x12\x64\x65ployment_version\x18\x04 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12\x10\n\x08sdk_name\x18\x05 \x01(\t\x12\x13\n\x0bsdk_version\x18\x06 \x01(\t\x12\x33\n\x06status\x18\x07 \x01(\x0e\x32#.temporal.api.enums.v1.WorkerStatus\x12.\n\nstart_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x11\n\thost_name\x18\t \x01(\t\x12\x1b\n\x13worker_grouping_key\x18\n \x01(\t\x12\x12\n\nprocess_id\x18\x0b \x01(\t\x12\x33\n\x07plugins\x18\x0c \x03(\x0b\x32".temporal.api.worker.v1.PluginInfo\x12:\n\x07\x64rivers\x18\r \x03(\x0b\x32).temporal.api.worker.v1.StorageDriverInfo"+\n\nPluginInfo\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t"!\n\x11StorageDriverInfo\x12\x0c\n\x04type\x18\x01 \x01(\t"\xa7\x11\n\x0f\x45nvironmentInfo\x12\x41\n\x08runtimes\x18\x01 \x03(\x0b\x32/.temporal.api.worker.v1.EnvironmentInfo.Runtime\x12X\n\x14hosting_environments\x18\x02 \x03(\x0b\x32:.temporal.api.worker.v1.EnvironmentInfo.HostingEnvironment\x12\x42\n\x08platform\x18\x03 \x01(\x0b\x32\x30.temporal.api.worker.v1.EnvironmentInfo.Platform\x1a\x94\x03\n\x07Runtime\x12I\n\x04type\x18\x01 \x01(\x0e\x32;.temporal.api.worker.v1.EnvironmentInfo.Runtime.RuntimeType\x12\x0f\n\x07version\x18\x02 \x01(\t"\xac\x02\n\x0bRuntimeType\x12\x1c\n\x18RUNTIME_TYPE_UNSPECIFIED\x10\x00\x12\x14\n\x10RUNTIME_TYPE_JVM\x10\x01\x12\x18\n\x14RUNTIME_TYPE_CPYTHON\x10\x02\x12\x15\n\x11RUNTIME_TYPE_NODE\x10\x03\x12\x14\n\x10RUNTIME_TYPE_BUN\x10\x04\x12\x16\n\x12RUNTIME_TYPE_CRUBY\x10\x05\x12\x13\n\x0fRUNTIME_TYPE_GO\x10\x06\x12!\n\x1dRUNTIME_TYPE_DOTNET_FRAMEWORK\x10\x07\x12\x1c\n\x18RUNTIME_TYPE_DOTNET_CORE\x10\x08\x12\x17\n\x13RUNTIME_TYPE_NATIVE\x10\t\x12\x1b\n\x17RUNTIME_TYPE_ROADRUNNER\x10\n\x1a\xd1\x04\n\x12HostingEnvironment\x12_\n\x04type\x18\x01 \x01(\x0e\x32Q.temporal.api.worker.v1.EnvironmentInfo.HostingEnvironment.HostingEnvironmentType\x12\x0f\n\x07version\x18\x02 \x01(\t"\xc8\x03\n\x16HostingEnvironmentType\x12(\n$HOSTING_ENVIRONMENT_TYPE_UNSPECIFIED\x10\x00\x12#\n\x1fHOSTING_ENVIRONMENT_TYPE_DOCKER\x10\x01\x12 \n\x1cHOSTING_ENVIRONMENT_TYPE_K8S\x10\x02\x12\'\n#HOSTING_ENVIRONMENT_TYPE_AWS_LAMBDA\x10\x03\x12$\n HOSTING_ENVIRONMENT_TYPE_AWS_ECS\x10\x04\x12-\n)HOSTING_ENVIRONMENT_TYPE_GOOGLE_CLOUD_RUN\x10\x06\x12.\n*HOSTING_ENVIRONMENT_TYPE_GOOGLE_APP_ENGINE\x10\x07\x12.\n*HOSTING_ENVIRONMENT_TYPE_AZURE_APP_SERVICE\x10\x08\x12,\n(HOSTING_ENVIRONMENT_TYPE_AZURE_FUNCTIONS\x10\t\x12\x31\n-HOSTING_ENVIRONMENT_TYPE_AZURE_CONTAINER_APPS\x10\n\x1a\xf1\x01\n\x08Platform\x12\x46\n\x05linux\x18\x01 \x01(\x0b\x32\x35.temporal.api.worker.v1.EnvironmentInfo.LinuxPlatformH\x00\x12\x46\n\x05macos\x18\x02 \x01(\x0b\x32\x35.temporal.api.worker.v1.EnvironmentInfo.MacOSPlatformH\x00\x12J\n\x07windows\x18\x03 \x01(\x0b\x32\x37.temporal.api.worker.v1.EnvironmentInfo.WindowsPlatformH\x00\x42\t\n\x07variant\x1a\xf3\x01\n\rLinuxPlatform\x12\x0f\n\x07version\x18\x01 \x01(\t\x12J\n\x0c\x61rchitecture\x18\x02 \x01(\x0e\x32\x34.temporal.api.worker.v1.EnvironmentInfo.Architecture\x12H\n\x04libc\x18\x03 \x01(\x0e\x32:.temporal.api.worker.v1.EnvironmentInfo.LinuxPlatform.Libc";\n\x04Libc\x12\x14\n\x10LIBC_UNSPECIFIED\x10\x00\x12\x0e\n\nLIBC_GLIBC\x10\x01\x12\r\n\tLIBC_MUSL\x10\x02\x1al\n\rMacOSPlatform\x12\x0f\n\x07version\x18\x01 \x01(\t\x12J\n\x0c\x61rchitecture\x18\x02 \x01(\x0e\x32\x34.temporal.api.worker.v1.EnvironmentInfo.Architecture\x1a\x91\x02\n\x0fWindowsPlatform\x12\x0f\n\x07version\x18\x01 \x01(\t\x12J\n\x0c\x61rchitecture\x18\x02 \x01(\x0e\x32\x34.temporal.api.worker.v1.EnvironmentInfo.Architecture\x12H\n\x03\x63rt\x18\x03 \x01(\x0e\x32;.temporal.api.worker.v1.EnvironmentInfo.WindowsPlatform.Crt"W\n\x03\x43rt\x12\x13\n\x0f\x43RT_UNSPECIFIED\x10\x00\x12\x0c\n\x08\x43RT_UCRT\x10\x01\x12\x0e\n\nCRT_MSVCRT\x10\x02\x12\r\n\tCRT_MINGW\x10\x03\x12\x0e\n\nCRT_CYGWIN\x10\x04"\\\n\x0c\x41rchitecture\x12\x1c\n\x18\x41RCHITECTURE_UNSPECIFIED\x10\x00\x12\x16\n\x12\x41RCHITECTURE_AMD64\x10\x01\x12\x16\n\x12\x41RCHITECTURE_ARM64\x10\x02"a\n\rWorkerCommand\x12H\n\x0f\x63\x61ncel_activity\x18\x01 \x01(\x0b\x32-.temporal.api.worker.v1.CancelActivityCommandH\x00\x42\x06\n\x04type"+\n\x15\x43\x61ncelActivityCommand\x12\x12\n\ntask_token\x18\x01 \x01(\x0c"f\n\x13WorkerCommandResult\x12G\n\x0f\x63\x61ncel_activity\x18\x01 \x01(\x0b\x32,.temporal.api.worker.v1.CancelActivityResultH\x00\x42\x06\n\x04type"\x16\n\x14\x43\x61ncelActivityResultB\x89\x01\n\x19io.temporal.api.worker.v1B\x0cMessageProtoP\x01Z#go.temporal.io/api/worker/v1;worker\xaa\x02\x18Temporalio.Api.Worker.V1\xea\x02\x1bTemporalio::Api::Worker::V1b\x06proto3' ) @@ -34,13 +34,43 @@ _WORKERHOSTINFO = DESCRIPTOR.message_types_by_name["WorkerHostInfo"] _WORKERHEARTBEAT = DESCRIPTOR.message_types_by_name["WorkerHeartbeat"] _WORKERINFO = DESCRIPTOR.message_types_by_name["WorkerInfo"] +_WORKERLISTINFO = DESCRIPTOR.message_types_by_name["WorkerListInfo"] _PLUGININFO = DESCRIPTOR.message_types_by_name["PluginInfo"] +_STORAGEDRIVERINFO = DESCRIPTOR.message_types_by_name["StorageDriverInfo"] +_ENVIRONMENTINFO = DESCRIPTOR.message_types_by_name["EnvironmentInfo"] +_ENVIRONMENTINFO_RUNTIME = _ENVIRONMENTINFO.nested_types_by_name["Runtime"] +_ENVIRONMENTINFO_HOSTINGENVIRONMENT = _ENVIRONMENTINFO.nested_types_by_name[ + "HostingEnvironment" +] +_ENVIRONMENTINFO_PLATFORM = _ENVIRONMENTINFO.nested_types_by_name["Platform"] +_ENVIRONMENTINFO_LINUXPLATFORM = _ENVIRONMENTINFO.nested_types_by_name["LinuxPlatform"] +_ENVIRONMENTINFO_MACOSPLATFORM = _ENVIRONMENTINFO.nested_types_by_name["MacOSPlatform"] +_ENVIRONMENTINFO_WINDOWSPLATFORM = _ENVIRONMENTINFO.nested_types_by_name[ + "WindowsPlatform" +] +_WORKERCOMMAND = DESCRIPTOR.message_types_by_name["WorkerCommand"] +_CANCELACTIVITYCOMMAND = DESCRIPTOR.message_types_by_name["CancelActivityCommand"] +_WORKERCOMMANDRESULT = DESCRIPTOR.message_types_by_name["WorkerCommandResult"] +_CANCELACTIVITYRESULT = DESCRIPTOR.message_types_by_name["CancelActivityResult"] +_ENVIRONMENTINFO_RUNTIME_RUNTIMETYPE = _ENVIRONMENTINFO_RUNTIME.enum_types_by_name[ + "RuntimeType" +] +_ENVIRONMENTINFO_HOSTINGENVIRONMENT_HOSTINGENVIRONMENTTYPE = ( + _ENVIRONMENTINFO_HOSTINGENVIRONMENT.enum_types_by_name["HostingEnvironmentType"] +) +_ENVIRONMENTINFO_LINUXPLATFORM_LIBC = _ENVIRONMENTINFO_LINUXPLATFORM.enum_types_by_name[ + "Libc" +] +_ENVIRONMENTINFO_WINDOWSPLATFORM_CRT = ( + _ENVIRONMENTINFO_WINDOWSPLATFORM.enum_types_by_name["Crt"] +) +_ENVIRONMENTINFO_ARCHITECTURE = _ENVIRONMENTINFO.enum_types_by_name["Architecture"] WorkerPollerInfo = _reflection.GeneratedProtocolMessageType( "WorkerPollerInfo", (_message.Message,), { "DESCRIPTOR": _WORKERPOLLERINFO, - "__module__": "temporal.api.worker.v1.message_pb2", + "__module__": "temporalio.api.worker.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.worker.v1.WorkerPollerInfo) }, ) @@ -51,7 +81,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKERSLOTSINFO, - "__module__": "temporal.api.worker.v1.message_pb2", + "__module__": "temporalio.api.worker.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.worker.v1.WorkerSlotsInfo) }, ) @@ -62,7 +92,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKERHOSTINFO, - "__module__": "temporal.api.worker.v1.message_pb2", + "__module__": "temporalio.api.worker.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.worker.v1.WorkerHostInfo) }, ) @@ -73,7 +103,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKERHEARTBEAT, - "__module__": "temporal.api.worker.v1.message_pb2", + "__module__": "temporalio.api.worker.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.worker.v1.WorkerHeartbeat) }, ) @@ -84,23 +114,160 @@ (_message.Message,), { "DESCRIPTOR": _WORKERINFO, - "__module__": "temporal.api.worker.v1.message_pb2", + "__module__": "temporalio.api.worker.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.worker.v1.WorkerInfo) }, ) _sym_db.RegisterMessage(WorkerInfo) +WorkerListInfo = _reflection.GeneratedProtocolMessageType( + "WorkerListInfo", + (_message.Message,), + { + "DESCRIPTOR": _WORKERLISTINFO, + "__module__": "temporalio.api.worker.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.worker.v1.WorkerListInfo) + }, +) +_sym_db.RegisterMessage(WorkerListInfo) + PluginInfo = _reflection.GeneratedProtocolMessageType( "PluginInfo", (_message.Message,), { "DESCRIPTOR": _PLUGININFO, - "__module__": "temporal.api.worker.v1.message_pb2", + "__module__": "temporalio.api.worker.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.worker.v1.PluginInfo) }, ) _sym_db.RegisterMessage(PluginInfo) +StorageDriverInfo = _reflection.GeneratedProtocolMessageType( + "StorageDriverInfo", + (_message.Message,), + { + "DESCRIPTOR": _STORAGEDRIVERINFO, + "__module__": "temporalio.api.worker.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.worker.v1.StorageDriverInfo) + }, +) +_sym_db.RegisterMessage(StorageDriverInfo) + +EnvironmentInfo = _reflection.GeneratedProtocolMessageType( + "EnvironmentInfo", + (_message.Message,), + { + "Runtime": _reflection.GeneratedProtocolMessageType( + "Runtime", + (_message.Message,), + { + "DESCRIPTOR": _ENVIRONMENTINFO_RUNTIME, + "__module__": "temporalio.api.worker.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.worker.v1.EnvironmentInfo.Runtime) + }, + ), + "HostingEnvironment": _reflection.GeneratedProtocolMessageType( + "HostingEnvironment", + (_message.Message,), + { + "DESCRIPTOR": _ENVIRONMENTINFO_HOSTINGENVIRONMENT, + "__module__": "temporalio.api.worker.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.worker.v1.EnvironmentInfo.HostingEnvironment) + }, + ), + "Platform": _reflection.GeneratedProtocolMessageType( + "Platform", + (_message.Message,), + { + "DESCRIPTOR": _ENVIRONMENTINFO_PLATFORM, + "__module__": "temporalio.api.worker.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.worker.v1.EnvironmentInfo.Platform) + }, + ), + "LinuxPlatform": _reflection.GeneratedProtocolMessageType( + "LinuxPlatform", + (_message.Message,), + { + "DESCRIPTOR": _ENVIRONMENTINFO_LINUXPLATFORM, + "__module__": "temporalio.api.worker.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.worker.v1.EnvironmentInfo.LinuxPlatform) + }, + ), + "MacOSPlatform": _reflection.GeneratedProtocolMessageType( + "MacOSPlatform", + (_message.Message,), + { + "DESCRIPTOR": _ENVIRONMENTINFO_MACOSPLATFORM, + "__module__": "temporalio.api.worker.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.worker.v1.EnvironmentInfo.MacOSPlatform) + }, + ), + "WindowsPlatform": _reflection.GeneratedProtocolMessageType( + "WindowsPlatform", + (_message.Message,), + { + "DESCRIPTOR": _ENVIRONMENTINFO_WINDOWSPLATFORM, + "__module__": "temporalio.api.worker.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.worker.v1.EnvironmentInfo.WindowsPlatform) + }, + ), + "DESCRIPTOR": _ENVIRONMENTINFO, + "__module__": "temporalio.api.worker.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.worker.v1.EnvironmentInfo) + }, +) +_sym_db.RegisterMessage(EnvironmentInfo) +_sym_db.RegisterMessage(EnvironmentInfo.Runtime) +_sym_db.RegisterMessage(EnvironmentInfo.HostingEnvironment) +_sym_db.RegisterMessage(EnvironmentInfo.Platform) +_sym_db.RegisterMessage(EnvironmentInfo.LinuxPlatform) +_sym_db.RegisterMessage(EnvironmentInfo.MacOSPlatform) +_sym_db.RegisterMessage(EnvironmentInfo.WindowsPlatform) + +WorkerCommand = _reflection.GeneratedProtocolMessageType( + "WorkerCommand", + (_message.Message,), + { + "DESCRIPTOR": _WORKERCOMMAND, + "__module__": "temporalio.api.worker.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.worker.v1.WorkerCommand) + }, +) +_sym_db.RegisterMessage(WorkerCommand) + +CancelActivityCommand = _reflection.GeneratedProtocolMessageType( + "CancelActivityCommand", + (_message.Message,), + { + "DESCRIPTOR": _CANCELACTIVITYCOMMAND, + "__module__": "temporalio.api.worker.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.worker.v1.CancelActivityCommand) + }, +) +_sym_db.RegisterMessage(CancelActivityCommand) + +WorkerCommandResult = _reflection.GeneratedProtocolMessageType( + "WorkerCommandResult", + (_message.Message,), + { + "DESCRIPTOR": _WORKERCOMMANDRESULT, + "__module__": "temporalio.api.worker.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.worker.v1.WorkerCommandResult) + }, +) +_sym_db.RegisterMessage(WorkerCommandResult) + +CancelActivityResult = _reflection.GeneratedProtocolMessageType( + "CancelActivityResult", + (_message.Message,), + { + "DESCRIPTOR": _CANCELACTIVITYRESULT, + "__module__": "temporalio.api.worker.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.worker.v1.CancelActivityResult) + }, +) +_sym_db.RegisterMessage(CancelActivityResult) + if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b"\n\031io.temporal.api.worker.v1B\014MessageProtoP\001Z#go.temporal.io/api/worker/v1;worker\252\002\030Temporalio.Api.Worker.V1\352\002\033Temporalio::Api::Worker::V1" @@ -109,11 +276,47 @@ _WORKERSLOTSINFO._serialized_start = 341 _WORKERSLOTSINFO._serialized_end = 582 _WORKERHOSTINFO._serialized_start = 585 - _WORKERHOSTINFO._serialized_end = 725 - _WORKERHEARTBEAT._serialized_start = 728 - _WORKERHEARTBEAT._serialized_end = 1959 - _WORKERINFO._serialized_start = 1961 - _WORKERINFO._serialized_end = 2040 - _PLUGININFO._serialized_start = 2042 - _PLUGININFO._serialized_end = 2085 + _WORKERHOSTINFO._serialized_end = 733 + _WORKERHEARTBEAT._serialized_start = 736 + _WORKERHEARTBEAT._serialized_end = 2089 + _WORKERINFO._serialized_start = 2091 + _WORKERINFO._serialized_end = 2170 + _WORKERLISTINFO._serialized_start = 2173 + _WORKERLISTINFO._serialized_end = 2665 + _PLUGININFO._serialized_start = 2667 + _PLUGININFO._serialized_end = 2710 + _STORAGEDRIVERINFO._serialized_start = 2712 + _STORAGEDRIVERINFO._serialized_end = 2745 + _ENVIRONMENTINFO._serialized_start = 2748 + _ENVIRONMENTINFO._serialized_end = 4963 + _ENVIRONMENTINFO_RUNTIME._serialized_start = 2993 + _ENVIRONMENTINFO_RUNTIME._serialized_end = 3397 + _ENVIRONMENTINFO_RUNTIME_RUNTIMETYPE._serialized_start = 3097 + _ENVIRONMENTINFO_RUNTIME_RUNTIMETYPE._serialized_end = 3397 + _ENVIRONMENTINFO_HOSTINGENVIRONMENT._serialized_start = 3400 + _ENVIRONMENTINFO_HOSTINGENVIRONMENT._serialized_end = 3993 + _ENVIRONMENTINFO_HOSTINGENVIRONMENT_HOSTINGENVIRONMENTTYPE._serialized_start = 3537 + _ENVIRONMENTINFO_HOSTINGENVIRONMENT_HOSTINGENVIRONMENTTYPE._serialized_end = 3993 + _ENVIRONMENTINFO_PLATFORM._serialized_start = 3996 + _ENVIRONMENTINFO_PLATFORM._serialized_end = 4237 + _ENVIRONMENTINFO_LINUXPLATFORM._serialized_start = 4240 + _ENVIRONMENTINFO_LINUXPLATFORM._serialized_end = 4483 + _ENVIRONMENTINFO_LINUXPLATFORM_LIBC._serialized_start = 4424 + _ENVIRONMENTINFO_LINUXPLATFORM_LIBC._serialized_end = 4483 + _ENVIRONMENTINFO_MACOSPLATFORM._serialized_start = 4485 + _ENVIRONMENTINFO_MACOSPLATFORM._serialized_end = 4593 + _ENVIRONMENTINFO_WINDOWSPLATFORM._serialized_start = 4596 + _ENVIRONMENTINFO_WINDOWSPLATFORM._serialized_end = 4869 + _ENVIRONMENTINFO_WINDOWSPLATFORM_CRT._serialized_start = 4782 + _ENVIRONMENTINFO_WINDOWSPLATFORM_CRT._serialized_end = 4869 + _ENVIRONMENTINFO_ARCHITECTURE._serialized_start = 4871 + _ENVIRONMENTINFO_ARCHITECTURE._serialized_end = 4963 + _WORKERCOMMAND._serialized_start = 4965 + _WORKERCOMMAND._serialized_end = 5062 + _CANCELACTIVITYCOMMAND._serialized_start = 5064 + _CANCELACTIVITYCOMMAND._serialized_end = 5107 + _WORKERCOMMANDRESULT._serialized_start = 5109 + _WORKERCOMMANDRESULT._serialized_end = 5211 + _CANCELACTIVITYRESULT._serialized_start = 5213 + _CANCELACTIVITYRESULT._serialized_end = 5235 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/worker/v1/message_pb2.pyi b/temporalio/api/worker/v1/message_pb2.pyi index e4c2a4725..9a630f998 100644 --- a/temporalio/api/worker/v1/message_pb2.pyi +++ b/temporalio/api/worker/v1/message_pb2.pyi @@ -6,17 +6,19 @@ isort:skip_file import builtins import collections.abc import sys +import typing import google.protobuf.descriptor import google.protobuf.duration_pb2 import google.protobuf.internal.containers +import google.protobuf.internal.enum_type_wrapper import google.protobuf.message import google.protobuf.timestamp_pb2 import temporalio.api.deployment.v1.message_pb2 import temporalio.api.enums.v1.common_pb2 -if sys.version_info >= (3, 8): +if sys.version_info >= (3, 10): import typing as typing_extensions else: import typing_extensions @@ -134,21 +136,19 @@ class WorkerHostInfo(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor HOST_NAME_FIELD_NUMBER: builtins.int - PROCESS_KEY_FIELD_NUMBER: builtins.int + WORKER_GROUPING_KEY_FIELD_NUMBER: builtins.int PROCESS_ID_FIELD_NUMBER: builtins.int CURRENT_HOST_CPU_USAGE_FIELD_NUMBER: builtins.int CURRENT_HOST_MEM_USAGE_FIELD_NUMBER: builtins.int host_name: builtins.str """Worker host identifier.""" - process_key: builtins.str - """Worker process identifier. This id should be unique for all _processes_ - running workers in the namespace, and should be shared by all workers - in the same process. + worker_grouping_key: builtins.str + """Worker grouping identifier. A key to group workers that share the same client+namespace+process. This will be used to build the worker command nexus task queue name: - "temporal-sys/worker-commands/{process_key}" + "temporal-sys/worker-commands/{worker_grouping_key}" """ process_id: builtins.str - """Worker process identifier. Unlike process_key, this id only needs to be unique + """Worker process identifier. This id only needs to be unique within one host (so using e.g. a unix pid would be appropriate). """ current_host_cpu_usage: builtins.float @@ -163,7 +163,7 @@ class WorkerHostInfo(google.protobuf.message.Message): self, *, host_name: builtins.str = ..., - process_key: builtins.str = ..., + worker_grouping_key: builtins.str = ..., process_id: builtins.str = ..., current_host_cpu_usage: builtins.float = ..., current_host_mem_usage: builtins.float = ..., @@ -179,8 +179,8 @@ class WorkerHostInfo(google.protobuf.message.Message): b"host_name", "process_id", b"process_id", - "process_key", - b"process_key", + "worker_grouping_key", + b"worker_grouping_key", ], ) -> None: ... @@ -218,6 +218,8 @@ class WorkerHeartbeat(google.protobuf.message.Message): TOTAL_STICKY_CACHE_MISS_FIELD_NUMBER: builtins.int CURRENT_STICKY_CACHE_SIZE_FIELD_NUMBER: builtins.int PLUGINS_FIELD_NUMBER: builtins.int + DRIVERS_FIELD_NUMBER: builtins.int + ENVIRONMENT_FIELD_NUMBER: builtins.int worker_instance_key: builtins.str """Worker identifier, should be unique for the namespace. It is distinct from worker identity, which is not necessarily namespace-unique. @@ -281,6 +283,16 @@ class WorkerHeartbeat(google.protobuf.message.Message): global___PluginInfo ]: """Plugins currently in use by this SDK.""" + @property + def drivers( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + global___StorageDriverInfo + ]: + """Storage drivers in use by this SDK.""" + @property + def environment(self) -> global___EnvironmentInfo: + """Information about the environment this SDK is running in.""" def __init__( self, *, @@ -309,6 +321,8 @@ class WorkerHeartbeat(google.protobuf.message.Message): total_sticky_cache_miss: builtins.int = ..., current_sticky_cache_size: builtins.int = ..., plugins: collections.abc.Iterable[global___PluginInfo] | None = ..., + drivers: collections.abc.Iterable[global___StorageDriverInfo] | None = ..., + environment: global___EnvironmentInfo | None = ..., ) -> None: ... def HasField( self, @@ -321,6 +335,8 @@ class WorkerHeartbeat(google.protobuf.message.Message): b"deployment_version", "elapsed_since_last_heartbeat", b"elapsed_since_last_heartbeat", + "environment", + b"environment", "heartbeat_time", b"heartbeat_time", "host_info", @@ -352,8 +368,12 @@ class WorkerHeartbeat(google.protobuf.message.Message): b"current_sticky_cache_size", "deployment_version", b"deployment_version", + "drivers", + b"drivers", "elapsed_since_last_heartbeat", b"elapsed_since_last_heartbeat", + "environment", + b"environment", "heartbeat_time", b"heartbeat_time", "host_info", @@ -396,6 +416,8 @@ class WorkerHeartbeat(google.protobuf.message.Message): global___WorkerHeartbeat = WorkerHeartbeat class WorkerInfo(google.protobuf.message.Message): + """Detailed worker information.""" + DESCRIPTOR: google.protobuf.descriptor.Descriptor WORKER_HEARTBEAT_FIELD_NUMBER: builtins.int @@ -417,6 +439,132 @@ class WorkerInfo(google.protobuf.message.Message): global___WorkerInfo = WorkerInfo +class WorkerListInfo(google.protobuf.message.Message): + """Limited worker information returned in the list response. + When adding fields here, ensure that it is also added to WorkerInfo (as it carries the full worker information). + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + WORKER_INSTANCE_KEY_FIELD_NUMBER: builtins.int + WORKER_IDENTITY_FIELD_NUMBER: builtins.int + TASK_QUEUE_FIELD_NUMBER: builtins.int + DEPLOYMENT_VERSION_FIELD_NUMBER: builtins.int + SDK_NAME_FIELD_NUMBER: builtins.int + SDK_VERSION_FIELD_NUMBER: builtins.int + STATUS_FIELD_NUMBER: builtins.int + START_TIME_FIELD_NUMBER: builtins.int + HOST_NAME_FIELD_NUMBER: builtins.int + WORKER_GROUPING_KEY_FIELD_NUMBER: builtins.int + PROCESS_ID_FIELD_NUMBER: builtins.int + PLUGINS_FIELD_NUMBER: builtins.int + DRIVERS_FIELD_NUMBER: builtins.int + worker_instance_key: builtins.str + """Worker identifier, should be unique for the namespace. + It is distinct from worker identity, which is not necessarily namespace-unique. + """ + worker_identity: builtins.str + """Worker identity, set by the client, may not be unique. + Usually host_name+(user group name)+process_id, but can be overwritten by the user. + """ + task_queue: builtins.str + """Task queue this worker is polling for tasks.""" + @property + def deployment_version( + self, + ) -> temporalio.api.deployment.v1.message_pb2.WorkerDeploymentVersion: ... + sdk_name: builtins.str + sdk_version: builtins.str + status: temporalio.api.enums.v1.common_pb2.WorkerStatus.ValueType + """Worker status. Defined by SDK.""" + @property + def start_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Worker start time. + It can be used to determine worker uptime. (current time - start time) + """ + host_name: builtins.str + """Worker host identifier.""" + worker_grouping_key: builtins.str + """Worker grouping identifier. A key to group workers that share the same client+namespace+process. + This will be used to build the worker command nexus task queue name: + "temporal-sys/worker-commands/{worker_grouping_key}" + """ + process_id: builtins.str + """Worker process identifier. This id only needs to be unique + within one host (so using e.g. a unix pid would be appropriate). + """ + @property + def plugins( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + global___PluginInfo + ]: + """Plugins currently in use by this SDK.""" + @property + def drivers( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + global___StorageDriverInfo + ]: + """Storage drivers in use by this SDK.""" + def __init__( + self, + *, + worker_instance_key: builtins.str = ..., + worker_identity: builtins.str = ..., + task_queue: builtins.str = ..., + deployment_version: temporalio.api.deployment.v1.message_pb2.WorkerDeploymentVersion + | None = ..., + sdk_name: builtins.str = ..., + sdk_version: builtins.str = ..., + status: temporalio.api.enums.v1.common_pb2.WorkerStatus.ValueType = ..., + start_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + host_name: builtins.str = ..., + worker_grouping_key: builtins.str = ..., + process_id: builtins.str = ..., + plugins: collections.abc.Iterable[global___PluginInfo] | None = ..., + drivers: collections.abc.Iterable[global___StorageDriverInfo] | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "deployment_version", b"deployment_version", "start_time", b"start_time" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "deployment_version", + b"deployment_version", + "drivers", + b"drivers", + "host_name", + b"host_name", + "plugins", + b"plugins", + "process_id", + b"process_id", + "sdk_name", + b"sdk_name", + "sdk_version", + b"sdk_version", + "start_time", + b"start_time", + "status", + b"status", + "task_queue", + b"task_queue", + "worker_grouping_key", + b"worker_grouping_key", + "worker_identity", + b"worker_identity", + "worker_instance_key", + b"worker_instance_key", + ], + ) -> None: ... + +global___WorkerListInfo = WorkerListInfo + class PluginInfo(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -438,3 +586,553 @@ class PluginInfo(google.protobuf.message.Message): ) -> None: ... global___PluginInfo = PluginInfo + +class StorageDriverInfo(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TYPE_FIELD_NUMBER: builtins.int + type: builtins.str + """The type of the driver, required.""" + def __init__( + self, + *, + type: builtins.str = ..., + ) -> None: ... + def ClearField( + self, field_name: typing_extensions.Literal["type", b"type"] + ) -> None: ... + +global___StorageDriverInfo = StorageDriverInfo + +class EnvironmentInfo(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class _Architecture: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _ArchitectureEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ + EnvironmentInfo._Architecture.ValueType + ], + builtins.type, + ): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + ARCHITECTURE_UNSPECIFIED: EnvironmentInfo._Architecture.ValueType # 0 + ARCHITECTURE_AMD64: EnvironmentInfo._Architecture.ValueType # 1 + ARCHITECTURE_ARM64: EnvironmentInfo._Architecture.ValueType # 2 + + class Architecture(_Architecture, metaclass=_ArchitectureEnumTypeWrapper): ... + ARCHITECTURE_UNSPECIFIED: EnvironmentInfo.Architecture.ValueType # 0 + ARCHITECTURE_AMD64: EnvironmentInfo.Architecture.ValueType # 1 + ARCHITECTURE_ARM64: EnvironmentInfo.Architecture.ValueType # 2 + + class Runtime(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class _RuntimeType: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _RuntimeTypeEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ + EnvironmentInfo.Runtime._RuntimeType.ValueType + ], + builtins.type, + ): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + RUNTIME_TYPE_UNSPECIFIED: ( + EnvironmentInfo.Runtime._RuntimeType.ValueType + ) # 0 + """Should never actually be set, exists to follow convention of having a default. + SDKs should just leave `runtimes` empty if none can be determined. + """ + RUNTIME_TYPE_JVM: EnvironmentInfo.Runtime._RuntimeType.ValueType # 1 + RUNTIME_TYPE_CPYTHON: EnvironmentInfo.Runtime._RuntimeType.ValueType # 2 + RUNTIME_TYPE_NODE: EnvironmentInfo.Runtime._RuntimeType.ValueType # 3 + RUNTIME_TYPE_BUN: EnvironmentInfo.Runtime._RuntimeType.ValueType # 4 + RUNTIME_TYPE_CRUBY: EnvironmentInfo.Runtime._RuntimeType.ValueType # 5 + RUNTIME_TYPE_GO: EnvironmentInfo.Runtime._RuntimeType.ValueType # 6 + RUNTIME_TYPE_DOTNET_FRAMEWORK: ( + EnvironmentInfo.Runtime._RuntimeType.ValueType + ) # 7 + RUNTIME_TYPE_DOTNET_CORE: ( + EnvironmentInfo.Runtime._RuntimeType.ValueType + ) # 8 + RUNTIME_TYPE_NATIVE: EnvironmentInfo.Runtime._RuntimeType.ValueType # 9 + RUNTIME_TYPE_ROADRUNNER: ( + EnvironmentInfo.Runtime._RuntimeType.ValueType + ) # 10 + + class RuntimeType(_RuntimeType, metaclass=_RuntimeTypeEnumTypeWrapper): ... + RUNTIME_TYPE_UNSPECIFIED: EnvironmentInfo.Runtime.RuntimeType.ValueType # 0 + """Should never actually be set, exists to follow convention of having a default. + SDKs should just leave `runtimes` empty if none can be determined. + """ + RUNTIME_TYPE_JVM: EnvironmentInfo.Runtime.RuntimeType.ValueType # 1 + RUNTIME_TYPE_CPYTHON: EnvironmentInfo.Runtime.RuntimeType.ValueType # 2 + RUNTIME_TYPE_NODE: EnvironmentInfo.Runtime.RuntimeType.ValueType # 3 + RUNTIME_TYPE_BUN: EnvironmentInfo.Runtime.RuntimeType.ValueType # 4 + RUNTIME_TYPE_CRUBY: EnvironmentInfo.Runtime.RuntimeType.ValueType # 5 + RUNTIME_TYPE_GO: EnvironmentInfo.Runtime.RuntimeType.ValueType # 6 + RUNTIME_TYPE_DOTNET_FRAMEWORK: ( + EnvironmentInfo.Runtime.RuntimeType.ValueType + ) # 7 + RUNTIME_TYPE_DOTNET_CORE: EnvironmentInfo.Runtime.RuntimeType.ValueType # 8 + RUNTIME_TYPE_NATIVE: EnvironmentInfo.Runtime.RuntimeType.ValueType # 9 + RUNTIME_TYPE_ROADRUNNER: EnvironmentInfo.Runtime.RuntimeType.ValueType # 10 + + TYPE_FIELD_NUMBER: builtins.int + VERSION_FIELD_NUMBER: builtins.int + type: global___EnvironmentInfo.Runtime.RuntimeType.ValueType + """The type of the runtime.""" + version: builtins.str + """The version of the runtime, if obtainable.""" + def __init__( + self, + *, + type: global___EnvironmentInfo.Runtime.RuntimeType.ValueType = ..., + version: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "type", b"type", "version", b"version" + ], + ) -> None: ... + + class HostingEnvironment(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class _HostingEnvironmentType: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _HostingEnvironmentTypeEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ + EnvironmentInfo.HostingEnvironment._HostingEnvironmentType.ValueType + ], + builtins.type, + ): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + HOSTING_ENVIRONMENT_TYPE_UNSPECIFIED: ( + EnvironmentInfo.HostingEnvironment._HostingEnvironmentType.ValueType + ) # 0 + """Should never actually be set, exists to follow convention of having a default. + SDKs should just leave `hosting_environments` empty if none can be determined. + """ + HOSTING_ENVIRONMENT_TYPE_DOCKER: ( + EnvironmentInfo.HostingEnvironment._HostingEnvironmentType.ValueType + ) # 1 + """Should always be in the list if we're running inside a docker container""" + HOSTING_ENVIRONMENT_TYPE_K8S: ( + EnvironmentInfo.HostingEnvironment._HostingEnvironmentType.ValueType + ) # 2 + """Should always be in the list if we're running inside any k8s environment""" + HOSTING_ENVIRONMENT_TYPE_AWS_LAMBDA: ( + EnvironmentInfo.HostingEnvironment._HostingEnvironmentType.ValueType + ) # 3 + """Detect via `AWS_LAMBDA_FUNCTION_NAME`""" + HOSTING_ENVIRONMENT_TYPE_AWS_ECS: ( + EnvironmentInfo.HostingEnvironment._HostingEnvironmentType.ValueType + ) # 4 + """Detect via `ECS_CONTAINER_METADATA_URI_V4` or `ECS_CONTAINER_METADATA_URI`""" + HOSTING_ENVIRONMENT_TYPE_GOOGLE_CLOUD_RUN: ( + EnvironmentInfo.HostingEnvironment._HostingEnvironmentType.ValueType + ) # 6 + """Detect via `K_SERVICE`""" + HOSTING_ENVIRONMENT_TYPE_GOOGLE_APP_ENGINE: ( + EnvironmentInfo.HostingEnvironment._HostingEnvironmentType.ValueType + ) # 7 + """Detect via `GAE_SERVICE`""" + HOSTING_ENVIRONMENT_TYPE_AZURE_APP_SERVICE: ( + EnvironmentInfo.HostingEnvironment._HostingEnvironmentType.ValueType + ) # 8 + """Detect via `WEBSITE_SITE_NAME`""" + HOSTING_ENVIRONMENT_TYPE_AZURE_FUNCTIONS: ( + EnvironmentInfo.HostingEnvironment._HostingEnvironmentType.ValueType + ) # 9 + """Detect via `FUNCTIONS_EXTENSION_VERSION`""" + HOSTING_ENVIRONMENT_TYPE_AZURE_CONTAINER_APPS: ( + EnvironmentInfo.HostingEnvironment._HostingEnvironmentType.ValueType + ) # 10 + """Detect via `CONTAINER_APP_NAME`""" + + class HostingEnvironmentType( + _HostingEnvironmentType, metaclass=_HostingEnvironmentTypeEnumTypeWrapper + ): + """What kind of hosting environment we're running in. This list is about what can actually be + detected reliably and is unrelated to what SDKs can actually run in. + """ + + HOSTING_ENVIRONMENT_TYPE_UNSPECIFIED: ( + EnvironmentInfo.HostingEnvironment.HostingEnvironmentType.ValueType + ) # 0 + """Should never actually be set, exists to follow convention of having a default. + SDKs should just leave `hosting_environments` empty if none can be determined. + """ + HOSTING_ENVIRONMENT_TYPE_DOCKER: ( + EnvironmentInfo.HostingEnvironment.HostingEnvironmentType.ValueType + ) # 1 + """Should always be in the list if we're running inside a docker container""" + HOSTING_ENVIRONMENT_TYPE_K8S: ( + EnvironmentInfo.HostingEnvironment.HostingEnvironmentType.ValueType + ) # 2 + """Should always be in the list if we're running inside any k8s environment""" + HOSTING_ENVIRONMENT_TYPE_AWS_LAMBDA: ( + EnvironmentInfo.HostingEnvironment.HostingEnvironmentType.ValueType + ) # 3 + """Detect via `AWS_LAMBDA_FUNCTION_NAME`""" + HOSTING_ENVIRONMENT_TYPE_AWS_ECS: ( + EnvironmentInfo.HostingEnvironment.HostingEnvironmentType.ValueType + ) # 4 + """Detect via `ECS_CONTAINER_METADATA_URI_V4` or `ECS_CONTAINER_METADATA_URI`""" + HOSTING_ENVIRONMENT_TYPE_GOOGLE_CLOUD_RUN: ( + EnvironmentInfo.HostingEnvironment.HostingEnvironmentType.ValueType + ) # 6 + """Detect via `K_SERVICE`""" + HOSTING_ENVIRONMENT_TYPE_GOOGLE_APP_ENGINE: ( + EnvironmentInfo.HostingEnvironment.HostingEnvironmentType.ValueType + ) # 7 + """Detect via `GAE_SERVICE`""" + HOSTING_ENVIRONMENT_TYPE_AZURE_APP_SERVICE: ( + EnvironmentInfo.HostingEnvironment.HostingEnvironmentType.ValueType + ) # 8 + """Detect via `WEBSITE_SITE_NAME`""" + HOSTING_ENVIRONMENT_TYPE_AZURE_FUNCTIONS: ( + EnvironmentInfo.HostingEnvironment.HostingEnvironmentType.ValueType + ) # 9 + """Detect via `FUNCTIONS_EXTENSION_VERSION`""" + HOSTING_ENVIRONMENT_TYPE_AZURE_CONTAINER_APPS: ( + EnvironmentInfo.HostingEnvironment.HostingEnvironmentType.ValueType + ) # 10 + """Detect via `CONTAINER_APP_NAME`""" + + TYPE_FIELD_NUMBER: builtins.int + VERSION_FIELD_NUMBER: builtins.int + type: ( + global___EnvironmentInfo.HostingEnvironment.HostingEnvironmentType.ValueType + ) + """The type of hosting environment.""" + version: builtins.str + """The version of the hosting environment, if obtainable.""" + def __init__( + self, + *, + type: global___EnvironmentInfo.HostingEnvironment.HostingEnvironmentType.ValueType = ..., + version: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "type", b"type", "version", b"version" + ], + ) -> None: ... + + class Platform(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + LINUX_FIELD_NUMBER: builtins.int + MACOS_FIELD_NUMBER: builtins.int + WINDOWS_FIELD_NUMBER: builtins.int + @property + def linux(self) -> global___EnvironmentInfo.LinuxPlatform: ... + @property + def macos(self) -> global___EnvironmentInfo.MacOSPlatform: ... + @property + def windows(self) -> global___EnvironmentInfo.WindowsPlatform: ... + def __init__( + self, + *, + linux: global___EnvironmentInfo.LinuxPlatform | None = ..., + macos: global___EnvironmentInfo.MacOSPlatform | None = ..., + windows: global___EnvironmentInfo.WindowsPlatform | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "linux", + b"linux", + "macos", + b"macos", + "variant", + b"variant", + "windows", + b"windows", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "linux", + b"linux", + "macos", + b"macos", + "variant", + b"variant", + "windows", + b"windows", + ], + ) -> None: ... + def WhichOneof( + self, oneof_group: typing_extensions.Literal["variant", b"variant"] + ) -> typing_extensions.Literal["linux", "macos", "windows"] | None: ... + + class LinuxPlatform(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class _Libc: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _LibcEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ + EnvironmentInfo.LinuxPlatform._Libc.ValueType + ], + builtins.type, + ): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + LIBC_UNSPECIFIED: EnvironmentInfo.LinuxPlatform._Libc.ValueType # 0 + LIBC_GLIBC: EnvironmentInfo.LinuxPlatform._Libc.ValueType # 1 + LIBC_MUSL: EnvironmentInfo.LinuxPlatform._Libc.ValueType # 2 + + class Libc(_Libc, metaclass=_LibcEnumTypeWrapper): ... + LIBC_UNSPECIFIED: EnvironmentInfo.LinuxPlatform.Libc.ValueType # 0 + LIBC_GLIBC: EnvironmentInfo.LinuxPlatform.Libc.ValueType # 1 + LIBC_MUSL: EnvironmentInfo.LinuxPlatform.Libc.ValueType # 2 + + VERSION_FIELD_NUMBER: builtins.int + ARCHITECTURE_FIELD_NUMBER: builtins.int + LIBC_FIELD_NUMBER: builtins.int + version: builtins.str + """The Linux kernel or distribution version, if obtainable.""" + architecture: global___EnvironmentInfo.Architecture.ValueType + """The architecture of the worker process.""" + libc: global___EnvironmentInfo.LinuxPlatform.Libc.ValueType + """The libc used by the worker process.""" + def __init__( + self, + *, + version: builtins.str = ..., + architecture: global___EnvironmentInfo.Architecture.ValueType = ..., + libc: global___EnvironmentInfo.LinuxPlatform.Libc.ValueType = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "architecture", b"architecture", "libc", b"libc", "version", b"version" + ], + ) -> None: ... + + class MacOSPlatform(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VERSION_FIELD_NUMBER: builtins.int + ARCHITECTURE_FIELD_NUMBER: builtins.int + version: builtins.str + """The macOS version, if obtainable.""" + architecture: global___EnvironmentInfo.Architecture.ValueType + """The architecture of the worker process.""" + def __init__( + self, + *, + version: builtins.str = ..., + architecture: global___EnvironmentInfo.Architecture.ValueType = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "architecture", b"architecture", "version", b"version" + ], + ) -> None: ... + + class WindowsPlatform(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class _Crt: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _CrtEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ + EnvironmentInfo.WindowsPlatform._Crt.ValueType + ], + builtins.type, + ): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + CRT_UNSPECIFIED: EnvironmentInfo.WindowsPlatform._Crt.ValueType # 0 + CRT_UCRT: EnvironmentInfo.WindowsPlatform._Crt.ValueType # 1 + CRT_MSVCRT: EnvironmentInfo.WindowsPlatform._Crt.ValueType # 2 + CRT_MINGW: EnvironmentInfo.WindowsPlatform._Crt.ValueType # 3 + CRT_CYGWIN: EnvironmentInfo.WindowsPlatform._Crt.ValueType # 4 + + class Crt(_Crt, metaclass=_CrtEnumTypeWrapper): ... + CRT_UNSPECIFIED: EnvironmentInfo.WindowsPlatform.Crt.ValueType # 0 + CRT_UCRT: EnvironmentInfo.WindowsPlatform.Crt.ValueType # 1 + CRT_MSVCRT: EnvironmentInfo.WindowsPlatform.Crt.ValueType # 2 + CRT_MINGW: EnvironmentInfo.WindowsPlatform.Crt.ValueType # 3 + CRT_CYGWIN: EnvironmentInfo.WindowsPlatform.Crt.ValueType # 4 + + VERSION_FIELD_NUMBER: builtins.int + ARCHITECTURE_FIELD_NUMBER: builtins.int + CRT_FIELD_NUMBER: builtins.int + version: builtins.str + """The Windows version, if obtainable.""" + architecture: global___EnvironmentInfo.Architecture.ValueType + """The architecture of the worker process.""" + crt: global___EnvironmentInfo.WindowsPlatform.Crt.ValueType + """The C runtime used by the worker process, if obtainable.""" + def __init__( + self, + *, + version: builtins.str = ..., + architecture: global___EnvironmentInfo.Architecture.ValueType = ..., + crt: global___EnvironmentInfo.WindowsPlatform.Crt.ValueType = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "architecture", b"architecture", "crt", b"crt", "version", b"version" + ], + ) -> None: ... + + RUNTIMES_FIELD_NUMBER: builtins.int + HOSTING_ENVIRONMENTS_FIELD_NUMBER: builtins.int + PLATFORM_FIELD_NUMBER: builtins.int + @property + def runtimes( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + global___EnvironmentInfo.Runtime + ]: + """The runtime(s) the SDK is operating in.""" + @property + def hosting_environments( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + global___EnvironmentInfo.HostingEnvironment + ]: + """The hosting environment(s) the SDK is operating in. Repeated to allow for layering (ex: Docker inside k8s).""" + @property + def platform(self) -> global___EnvironmentInfo.Platform: + """The platform the SDK is operating on.""" + def __init__( + self, + *, + runtimes: collections.abc.Iterable[global___EnvironmentInfo.Runtime] + | None = ..., + hosting_environments: collections.abc.Iterable[ + global___EnvironmentInfo.HostingEnvironment + ] + | None = ..., + platform: global___EnvironmentInfo.Platform | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["platform", b"platform"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "hosting_environments", + b"hosting_environments", + "platform", + b"platform", + "runtimes", + b"runtimes", + ], + ) -> None: ... + +global___EnvironmentInfo = EnvironmentInfo + +class WorkerCommand(google.protobuf.message.Message): + """A command sent from the server to a worker.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CANCEL_ACTIVITY_FIELD_NUMBER: builtins.int + @property + def cancel_activity(self) -> global___CancelActivityCommand: ... + def __init__( + self, + *, + cancel_activity: global___CancelActivityCommand | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "cancel_activity", b"cancel_activity", "type", b"type" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "cancel_activity", b"cancel_activity", "type", b"type" + ], + ) -> None: ... + def WhichOneof( + self, oneof_group: typing_extensions.Literal["type", b"type"] + ) -> typing_extensions.Literal["cancel_activity"] | None: ... + +global___WorkerCommand = WorkerCommand + +class CancelActivityCommand(google.protobuf.message.Message): + """Cancel an activity if it is still running. Otherwise, do nothing.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TASK_TOKEN_FIELD_NUMBER: builtins.int + task_token: builtins.bytes + def __init__( + self, + *, + task_token: builtins.bytes = ..., + ) -> None: ... + def ClearField( + self, field_name: typing_extensions.Literal["task_token", b"task_token"] + ) -> None: ... + +global___CancelActivityCommand = CancelActivityCommand + +class WorkerCommandResult(google.protobuf.message.Message): + """The result of executing a WorkerCommand.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CANCEL_ACTIVITY_FIELD_NUMBER: builtins.int + @property + def cancel_activity(self) -> global___CancelActivityResult: ... + def __init__( + self, + *, + cancel_activity: global___CancelActivityResult | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "cancel_activity", b"cancel_activity", "type", b"type" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "cancel_activity", b"cancel_activity", "type", b"type" + ], + ) -> None: ... + def WhichOneof( + self, oneof_group: typing_extensions.Literal["type", b"type"] + ) -> typing_extensions.Literal["cancel_activity"] | None: ... + +global___WorkerCommandResult = WorkerCommandResult + +class CancelActivityResult(google.protobuf.message.Message): + """Result of a CancelActivityCommand. + Treat both successful cancellation and no-op (activity is no longer running) as success. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___CancelActivityResult = CancelActivityResult diff --git a/temporalio/api/workflow/v1/__init__.py b/temporalio/api/workflow/v1/__init__.py index 30a251fa1..ae647ab67 100644 --- a/temporalio/api/workflow/v1/__init__.py +++ b/temporalio/api/workflow/v1/__init__.py @@ -18,6 +18,7 @@ WorkflowExecutionExtendedInfo, WorkflowExecutionInfo, WorkflowExecutionOptions, + WorkflowExecutionPauseInfo, WorkflowExecutionVersioningInfo, ) @@ -41,5 +42,6 @@ "WorkflowExecutionExtendedInfo", "WorkflowExecutionInfo", "WorkflowExecutionOptions", + "WorkflowExecutionPauseInfo", "WorkflowExecutionVersioningInfo", ] diff --git a/temporalio/api/workflow/v1/message_pb2.py b/temporalio/api/workflow/v1/message_pb2.py index 4e58a81c2..032f2b2b5 100644 --- a/temporalio/api/workflow/v1/message_pb2.py +++ b/temporalio/api/workflow/v1/message_pb2.py @@ -48,7 +48,7 @@ ) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n&temporal/api/workflow/v1/message.proto\x12\x18temporal.api.workflow.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a google/protobuf/field_mask.proto\x1a&temporal/api/activity/v1/message.proto\x1a"temporal/api/enums/v1/common.proto\x1a&temporal/api/enums/v1/event_type.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a$temporal/api/common/v1/message.proto\x1a(temporal/api/deployment/v1/message.proto\x1a%temporal/api/failure/v1/message.proto\x1a\'temporal/api/taskqueue/v1/message.proto\x1a\'temporal/api/sdk/v1/user_metadata.proto"\xab\t\n\x15WorkflowExecutionInfo\x12<\n\texecution\x18\x01 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x32\n\x04type\x18\x02 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12.\n\nstart_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12.\n\nclose_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12>\n\x06status\x18\x05 \x01(\x0e\x32..temporal.api.enums.v1.WorkflowExecutionStatus\x12\x16\n\x0ehistory_length\x18\x06 \x01(\x03\x12\x1b\n\x13parent_namespace_id\x18\x07 \x01(\t\x12\x43\n\x10parent_execution\x18\x08 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x32\n\x0e\x65xecution_time\x18\t \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12*\n\x04memo\x18\n \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo\x12\x43\n\x11search_attributes\x18\x0b \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12@\n\x11\x61uto_reset_points\x18\x0c \x01(\x0b\x32%.temporal.api.workflow.v1.ResetPoints\x12\x12\n\ntask_queue\x18\r \x01(\t\x12\x1e\n\x16state_transition_count\x18\x0e \x01(\x03\x12\x1a\n\x12history_size_bytes\x18\x0f \x01(\x03\x12X\n most_recent_worker_version_stamp\x18\x10 \x01(\x0b\x32*.temporal.api.common.v1.WorkerVersionStampB\x02\x18\x01\x12\x35\n\x12\x65xecution_duration\x18\x11 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x41\n\x0eroot_execution\x18\x12 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x1d\n\x11\x61ssigned_build_id\x18\x13 \x01(\tB\x02\x18\x01\x12\x1e\n\x12inherited_build_id\x18\x14 \x01(\tB\x02\x18\x01\x12\x14\n\x0c\x66irst_run_id\x18\x15 \x01(\t\x12R\n\x0fversioning_info\x18\x16 \x01(\x0b\x32\x39.temporal.api.workflow.v1.WorkflowExecutionVersioningInfo\x12\x1e\n\x16worker_deployment_name\x18\x17 \x01(\t\x12\x32\n\x08priority\x18\x18 \x01(\x0b\x32 .temporal.api.common.v1.Priority"\xfc\x03\n\x1dWorkflowExecutionExtendedInfo\x12=\n\x19\x65xecution_expiration_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x37\n\x13run_expiration_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x18\n\x10\x63\x61ncel_requested\x18\x03 \x01(\x08\x12\x33\n\x0flast_reset_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x37\n\x13original_start_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x14\n\x0creset_run_id\x18\x06 \x01(\t\x12\x65\n\x10request_id_infos\x18\x07 \x03(\x0b\x32K.temporal.api.workflow.v1.WorkflowExecutionExtendedInfo.RequestIdInfosEntry\x1a^\n\x13RequestIdInfosEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.temporal.api.workflow.v1.RequestIdInfo:\x02\x38\x01"\xf5\x03\n\x1fWorkflowExecutionVersioningInfo\x12;\n\x08\x62\x65havior\x18\x01 \x01(\x0e\x32).temporal.api.enums.v1.VersioningBehavior\x12>\n\ndeployment\x18\x02 \x01(\x0b\x32&.temporal.api.deployment.v1.DeploymentB\x02\x18\x01\x12\x13\n\x07version\x18\x05 \x01(\tB\x02\x18\x01\x12O\n\x12\x64\x65ployment_version\x18\x07 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12I\n\x13versioning_override\x18\x03 \x01(\x0b\x32,.temporal.api.workflow.v1.VersioningOverride\x12Q\n\x15\x64\x65ployment_transition\x18\x04 \x01(\x0b\x32..temporal.api.workflow.v1.DeploymentTransitionB\x02\x18\x01\x12Q\n\x12version_transition\x18\x06 \x01(\x0b\x32\x35.temporal.api.workflow.v1.DeploymentVersionTransition"R\n\x14\x44\x65ploymentTransition\x12:\n\ndeployment\x18\x01 \x01(\x0b\x32&.temporal.api.deployment.v1.Deployment"\x83\x01\n\x1b\x44\x65ploymentVersionTransition\x12\x13\n\x07version\x18\x01 \x01(\tB\x02\x18\x01\x12O\n\x12\x64\x65ployment_version\x18\x02 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion"\xc7\x02\n\x17WorkflowExecutionConfig\x12\x38\n\ntask_queue\x18\x01 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12=\n\x1aworkflow_execution_timeout\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x37\n\x14workflow_run_timeout\x18\x03 \x01(\x0b\x32\x19.google.protobuf.Duration\x12@\n\x1d\x64\x65\x66\x61ult_workflow_task_timeout\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\ruser_metadata\x18\x05 \x01(\x0b\x32!.temporal.api.sdk.v1.UserMetadata"\xbd\r\n\x13PendingActivityInfo\x12\x13\n\x0b\x61\x63tivity_id\x18\x01 \x01(\t\x12;\n\ractivity_type\x18\x02 \x01(\x0b\x32$.temporal.api.common.v1.ActivityType\x12:\n\x05state\x18\x03 \x01(\x0e\x32+.temporal.api.enums.v1.PendingActivityState\x12;\n\x11heartbeat_details\x18\x04 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x37\n\x13last_heartbeat_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x35\n\x11last_started_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0f\n\x07\x61ttempt\x18\x07 \x01(\x05\x12\x18\n\x10maximum_attempts\x18\x08 \x01(\x05\x12\x32\n\x0escheduled_time\x18\t \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x33\n\x0f\x65xpiration_time\x18\n \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x36\n\x0clast_failure\x18\x0b \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x1c\n\x14last_worker_identity\x18\x0c \x01(\t\x12;\n\x15use_workflow_build_id\x18\r \x01(\x0b\x32\x16.google.protobuf.EmptyB\x02\x18\x01H\x00\x12\x32\n$last_independently_assigned_build_id\x18\x0e \x01(\tB\x02\x18\x01H\x00\x12Q\n\x19last_worker_version_stamp\x18\x0f \x01(\x0b\x32*.temporal.api.common.v1.WorkerVersionStampB\x02\x18\x01\x12\x39\n\x16\x63urrent_retry_interval\x18\x10 \x01(\x0b\x32\x19.google.protobuf.Duration\x12>\n\x1alast_attempt_complete_time\x18\x11 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12>\n\x1anext_attempt_schedule_time\x18\x12 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0e\n\x06paused\x18\x13 \x01(\x08\x12\x43\n\x0flast_deployment\x18\x14 \x01(\x0b\x32&.temporal.api.deployment.v1.DeploymentB\x02\x18\x01\x12*\n\x1elast_worker_deployment_version\x18\x15 \x01(\tB\x02\x18\x01\x12T\n\x17last_deployment_version\x18\x19 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12\x32\n\x08priority\x18\x16 \x01(\x0b\x32 .temporal.api.common.v1.Priority\x12K\n\npause_info\x18\x17 \x01(\x0b\x32\x37.temporal.api.workflow.v1.PendingActivityInfo.PauseInfo\x12\x43\n\x10\x61\x63tivity_options\x18\x18 \x01(\x0b\x32).temporal.api.activity.v1.ActivityOptions\x1a\xcf\x02\n\tPauseInfo\x12.\n\npause_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12P\n\x06manual\x18\x02 \x01(\x0b\x32>.temporal.api.workflow.v1.PendingActivityInfo.PauseInfo.ManualH\x00\x12L\n\x04rule\x18\x04 \x01(\x0b\x32<.temporal.api.workflow.v1.PendingActivityInfo.PauseInfo.RuleH\x00\x1a*\n\x06Manual\x12\x10\n\x08identity\x18\x01 \x01(\t\x12\x0e\n\x06reason\x18\x02 \x01(\t\x1a\x39\n\x04Rule\x12\x0f\n\x07rule_id\x18\x01 \x01(\t\x12\x10\n\x08identity\x18\x02 \x01(\t\x12\x0e\n\x06reason\x18\x03 \x01(\tB\x0b\n\tpaused_byB\x13\n\x11\x61ssigned_build_id"\xb9\x01\n\x19PendingChildExecutionInfo\x12\x13\n\x0bworkflow_id\x18\x01 \x01(\t\x12\x0e\n\x06run_id\x18\x02 \x01(\t\x12\x1a\n\x12workflow_type_name\x18\x03 \x01(\t\x12\x14\n\x0cinitiated_id\x18\x04 \x01(\x03\x12\x45\n\x13parent_close_policy\x18\x05 \x01(\x0e\x32(.temporal.api.enums.v1.ParentClosePolicy"\x8d\x02\n\x17PendingWorkflowTaskInfo\x12>\n\x05state\x18\x01 \x01(\x0e\x32/.temporal.api.enums.v1.PendingWorkflowTaskState\x12\x32\n\x0escheduled_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12;\n\x17original_scheduled_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x30\n\x0cstarted_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0f\n\x07\x61ttempt\x18\x05 \x01(\x05"G\n\x0bResetPoints\x12\x38\n\x06points\x18\x01 \x03(\x0b\x32(.temporal.api.workflow.v1.ResetPointInfo"\xef\x01\n\x0eResetPointInfo\x12\x10\n\x08\x62uild_id\x18\x07 \x01(\t\x12\x1b\n\x0f\x62inary_checksum\x18\x01 \x01(\tB\x02\x18\x01\x12\x0e\n\x06run_id\x18\x02 \x01(\t\x12(\n first_workflow_task_completed_id\x18\x03 \x01(\x03\x12/\n\x0b\x63reate_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0b\x65xpire_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x12\n\nresettable\x18\x06 \x01(\x08"\x85\x07\n\x18NewWorkflowExecutionInfo\x12\x13\n\x0bworkflow_id\x18\x01 \x01(\t\x12;\n\rworkflow_type\x18\x02 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12\x38\n\ntask_queue\x18\x03 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12/\n\x05input\x18\x04 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12=\n\x1aworkflow_execution_timeout\x18\x05 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x37\n\x14workflow_run_timeout\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\x07 \x01(\x0b\x32\x19.google.protobuf.Duration\x12N\n\x18workflow_id_reuse_policy\x18\x08 \x01(\x0e\x32,.temporal.api.enums.v1.WorkflowIdReusePolicy\x12\x39\n\x0cretry_policy\x18\t \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x15\n\rcron_schedule\x18\n \x01(\t\x12*\n\x04memo\x18\x0b \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo\x12\x43\n\x11search_attributes\x18\x0c \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12.\n\x06header\x18\r \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12\x38\n\ruser_metadata\x18\x0e \x01(\x0b\x32!.temporal.api.sdk.v1.UserMetadata\x12I\n\x13versioning_override\x18\x0f \x01(\x0b\x32,.temporal.api.workflow.v1.VersioningOverride\x12\x32\n\x08priority\x18\x10 \x01(\x0b\x32 .temporal.api.common.v1.Priority"\xd2\x04\n\x0c\x43\x61llbackInfo\x12\x32\n\x08\x63\x61llback\x18\x01 \x01(\x0b\x32 .temporal.api.common.v1.Callback\x12?\n\x07trigger\x18\x02 \x01(\x0b\x32..temporal.api.workflow.v1.CallbackInfo.Trigger\x12\x35\n\x11registration_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x33\n\x05state\x18\x04 \x01(\x0e\x32$.temporal.api.enums.v1.CallbackState\x12\x0f\n\x07\x61ttempt\x18\x05 \x01(\x05\x12>\n\x1alast_attempt_complete_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12>\n\x14last_attempt_failure\x18\x07 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12>\n\x1anext_attempt_schedule_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x16\n\x0e\x62locked_reason\x18\t \x01(\t\x1a\x10\n\x0eWorkflowClosed\x1a\x66\n\x07Trigger\x12P\n\x0fworkflow_closed\x18\x01 \x01(\x0b\x32\x35.temporal.api.workflow.v1.CallbackInfo.WorkflowClosedH\x00\x42\t\n\x07variant"\x92\x05\n\x19PendingNexusOperationInfo\x12\x10\n\x08\x65ndpoint\x18\x01 \x01(\t\x12\x0f\n\x07service\x18\x02 \x01(\t\x12\x11\n\toperation\x18\x03 \x01(\t\x12\x18\n\x0coperation_id\x18\x04 \x01(\tB\x02\x18\x01\x12<\n\x19schedule_to_close_timeout\x18\x05 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x32\n\x0escheduled_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12@\n\x05state\x18\x07 \x01(\x0e\x32\x31.temporal.api.enums.v1.PendingNexusOperationState\x12\x0f\n\x07\x61ttempt\x18\x08 \x01(\x05\x12>\n\x1alast_attempt_complete_time\x18\t \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12>\n\x14last_attempt_failure\x18\n \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12>\n\x1anext_attempt_schedule_time\x18\x0b \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12S\n\x11\x63\x61ncellation_info\x18\x0c \x01(\x0b\x32\x38.temporal.api.workflow.v1.NexusOperationCancellationInfo\x12\x1a\n\x12scheduled_event_id\x18\r \x01(\x03\x12\x16\n\x0e\x62locked_reason\x18\x0e \x01(\t\x12\x17\n\x0foperation_token\x18\x0f \x01(\t"\x84\x03\n\x1eNexusOperationCancellationInfo\x12\x32\n\x0erequested_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x45\n\x05state\x18\x02 \x01(\x0e\x32\x36.temporal.api.enums.v1.NexusOperationCancellationState\x12\x0f\n\x07\x61ttempt\x18\x03 \x01(\x05\x12>\n\x1alast_attempt_complete_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12>\n\x14last_attempt_failure\x18\x05 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12>\n\x1anext_attempt_schedule_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x16\n\x0e\x62locked_reason\x18\x07 \x01(\t"e\n\x18WorkflowExecutionOptions\x12I\n\x13versioning_override\x18\x01 \x01(\x0b\x32,.temporal.api.workflow.v1.VersioningOverride"\xbd\x04\n\x12VersioningOverride\x12M\n\x06pinned\x18\x03 \x01(\x0b\x32;.temporal.api.workflow.v1.VersioningOverride.PinnedOverrideH\x00\x12\x16\n\x0c\x61uto_upgrade\x18\x04 \x01(\x08H\x00\x12?\n\x08\x62\x65havior\x18\x01 \x01(\x0e\x32).temporal.api.enums.v1.VersioningBehaviorB\x02\x18\x01\x12>\n\ndeployment\x18\x02 \x01(\x0b\x32&.temporal.api.deployment.v1.DeploymentB\x02\x18\x01\x12\x1a\n\x0epinned_version\x18\t \x01(\tB\x02\x18\x01\x1a\xad\x01\n\x0ePinnedOverride\x12U\n\x08\x62\x65havior\x18\x01 \x01(\x0e\x32\x43.temporal.api.workflow.v1.VersioningOverride.PinnedOverrideBehavior\x12\x44\n\x07version\x18\x02 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion"g\n\x16PinnedOverrideBehavior\x12(\n$PINNED_OVERRIDE_BEHAVIOR_UNSPECIFIED\x10\x00\x12#\n\x1fPINNED_OVERRIDE_BEHAVIOR_PINNED\x10\x01\x42\n\n\x08override"i\n\x11OnConflictOptions\x12\x19\n\x11\x61ttach_request_id\x18\x01 \x01(\x08\x12#\n\x1b\x61ttach_completion_callbacks\x18\x02 \x01(\x08\x12\x14\n\x0c\x61ttach_links\x18\x03 \x01(\x08"i\n\rRequestIdInfo\x12\x34\n\nevent_type\x18\x01 \x01(\x0e\x32 .temporal.api.enums.v1.EventType\x12\x10\n\x08\x65vent_id\x18\x02 \x01(\x03\x12\x10\n\x08\x62uffered\x18\x03 \x01(\x08"\xb7\x04\n\x12PostResetOperation\x12V\n\x0fsignal_workflow\x18\x01 \x01(\x0b\x32;.temporal.api.workflow.v1.PostResetOperation.SignalWorkflowH\x00\x12\x65\n\x17update_workflow_options\x18\x02 \x01(\x0b\x32\x42.temporal.api.workflow.v1.PostResetOperation.UpdateWorkflowOptionsH\x00\x1a\xb3\x01\n\x0eSignalWorkflow\x12\x13\n\x0bsignal_name\x18\x01 \x01(\t\x12/\n\x05input\x18\x02 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12.\n\x06header\x18\x03 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12+\n\x05links\x18\x04 \x03(\x0b\x32\x1c.temporal.api.common.v1.Link\x1a\xa0\x01\n\x15UpdateWorkflowOptions\x12V\n\x1aworkflow_execution_options\x18\x01 \x01(\x0b\x32\x32.temporal.api.workflow.v1.WorkflowExecutionOptions\x12/\n\x0bupdate_mask\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.FieldMaskB\t\n\x07variantB\x93\x01\n\x1bio.temporal.api.workflow.v1B\x0cMessageProtoP\x01Z\'go.temporal.io/api/workflow/v1;workflow\xaa\x02\x1aTemporalio.Api.Workflow.V1\xea\x02\x1dTemporalio::Api::Workflow::V1b\x06proto3' + b'\n&temporal/api/workflow/v1/message.proto\x12\x18temporal.api.workflow.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a google/protobuf/field_mask.proto\x1a&temporal/api/activity/v1/message.proto\x1a"temporal/api/enums/v1/common.proto\x1a&temporal/api/enums/v1/event_type.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a$temporal/api/common/v1/message.proto\x1a(temporal/api/deployment/v1/message.proto\x1a%temporal/api/failure/v1/message.proto\x1a\'temporal/api/taskqueue/v1/message.proto\x1a\'temporal/api/sdk/v1/user_metadata.proto"\xf0\t\n\x15WorkflowExecutionInfo\x12<\n\texecution\x18\x01 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x32\n\x04type\x18\x02 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12.\n\nstart_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12.\n\nclose_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12>\n\x06status\x18\x05 \x01(\x0e\x32..temporal.api.enums.v1.WorkflowExecutionStatus\x12\x16\n\x0ehistory_length\x18\x06 \x01(\x03\x12\x1b\n\x13parent_namespace_id\x18\x07 \x01(\t\x12\x43\n\x10parent_execution\x18\x08 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x32\n\x0e\x65xecution_time\x18\t \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12*\n\x04memo\x18\n \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo\x12\x43\n\x11search_attributes\x18\x0b \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12@\n\x11\x61uto_reset_points\x18\x0c \x01(\x0b\x32%.temporal.api.workflow.v1.ResetPoints\x12\x12\n\ntask_queue\x18\r \x01(\t\x12\x1e\n\x16state_transition_count\x18\x0e \x01(\x03\x12\x1a\n\x12history_size_bytes\x18\x0f \x01(\x03\x12X\n most_recent_worker_version_stamp\x18\x10 \x01(\x0b\x32*.temporal.api.common.v1.WorkerVersionStampB\x02\x18\x01\x12\x35\n\x12\x65xecution_duration\x18\x11 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x41\n\x0eroot_execution\x18\x12 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x1d\n\x11\x61ssigned_build_id\x18\x13 \x01(\tB\x02\x18\x01\x12\x1e\n\x12inherited_build_id\x18\x14 \x01(\tB\x02\x18\x01\x12\x14\n\x0c\x66irst_run_id\x18\x15 \x01(\t\x12R\n\x0fversioning_info\x18\x16 \x01(\x0b\x32\x39.temporal.api.workflow.v1.WorkflowExecutionVersioningInfo\x12\x1e\n\x16worker_deployment_name\x18\x17 \x01(\t\x12\x32\n\x08priority\x18\x18 \x01(\x0b\x32 .temporal.api.common.v1.Priority\x12#\n\x1b\x65xternal_payload_size_bytes\x18\x19 \x01(\x03\x12\x1e\n\x16\x65xternal_payload_count\x18\x1a \x01(\x03"\x8c\x05\n\x1dWorkflowExecutionExtendedInfo\x12=\n\x19\x65xecution_expiration_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x37\n\x13run_expiration_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x18\n\x10\x63\x61ncel_requested\x18\x03 \x01(\x08\x12\x33\n\x0flast_reset_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x37\n\x13original_start_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x14\n\x0creset_run_id\x18\x06 \x01(\t\x12\x65\n\x10request_id_infos\x18\x07 \x03(\x0b\x32K.temporal.api.workflow.v1.WorkflowExecutionExtendedInfo.RequestIdInfosEntry\x12H\n\npause_info\x18\x08 \x01(\x0b\x32\x34.temporal.api.workflow.v1.WorkflowExecutionPauseInfo\x12\x44\n\x12time_skipping_info\x18\t \x01(\x0b\x32(.temporal.api.common.v1.TimeSkippingInfo\x1a^\n\x13RequestIdInfosEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.temporal.api.workflow.v1.RequestIdInfo:\x02\x38\x01"\xfb\x04\n\x1fWorkflowExecutionVersioningInfo\x12;\n\x08\x62\x65havior\x18\x01 \x01(\x0e\x32).temporal.api.enums.v1.VersioningBehavior\x12>\n\ndeployment\x18\x02 \x01(\x0b\x32&.temporal.api.deployment.v1.DeploymentB\x02\x18\x01\x12\x13\n\x07version\x18\x05 \x01(\tB\x02\x18\x01\x12O\n\x12\x64\x65ployment_version\x18\x07 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12I\n\x13versioning_override\x18\x03 \x01(\x0b\x32,.temporal.api.workflow.v1.VersioningOverride\x12Q\n\x15\x64\x65ployment_transition\x18\x04 \x01(\x0b\x32..temporal.api.workflow.v1.DeploymentTransitionB\x02\x18\x01\x12Q\n\x12version_transition\x18\x06 \x01(\x0b\x32\x35.temporal.api.workflow.v1.DeploymentVersionTransition\x12\x17\n\x0frevision_number\x18\x08 \x01(\x03\x12k\n+continue_as_new_initial_versioning_behavior\x18\t \x01(\x0e\x32\x36.temporal.api.enums.v1.ContinueAsNewVersioningBehavior"R\n\x14\x44\x65ploymentTransition\x12:\n\ndeployment\x18\x01 \x01(\x0b\x32&.temporal.api.deployment.v1.Deployment"\x83\x01\n\x1b\x44\x65ploymentVersionTransition\x12\x13\n\x07version\x18\x01 \x01(\tB\x02\x18\x01\x12O\n\x12\x64\x65ployment_version\x18\x02 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion"\xc7\x02\n\x17WorkflowExecutionConfig\x12\x38\n\ntask_queue\x18\x01 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12=\n\x1aworkflow_execution_timeout\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x37\n\x14workflow_run_timeout\x18\x03 \x01(\x0b\x32\x19.google.protobuf.Duration\x12@\n\x1d\x64\x65\x66\x61ult_workflow_task_timeout\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\ruser_metadata\x18\x05 \x01(\x0b\x32!.temporal.api.sdk.v1.UserMetadata"\xbd\r\n\x13PendingActivityInfo\x12\x13\n\x0b\x61\x63tivity_id\x18\x01 \x01(\t\x12;\n\ractivity_type\x18\x02 \x01(\x0b\x32$.temporal.api.common.v1.ActivityType\x12:\n\x05state\x18\x03 \x01(\x0e\x32+.temporal.api.enums.v1.PendingActivityState\x12;\n\x11heartbeat_details\x18\x04 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x37\n\x13last_heartbeat_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x35\n\x11last_started_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0f\n\x07\x61ttempt\x18\x07 \x01(\x05\x12\x18\n\x10maximum_attempts\x18\x08 \x01(\x05\x12\x32\n\x0escheduled_time\x18\t \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x33\n\x0f\x65xpiration_time\x18\n \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x36\n\x0clast_failure\x18\x0b \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x1c\n\x14last_worker_identity\x18\x0c \x01(\t\x12;\n\x15use_workflow_build_id\x18\r \x01(\x0b\x32\x16.google.protobuf.EmptyB\x02\x18\x01H\x00\x12\x32\n$last_independently_assigned_build_id\x18\x0e \x01(\tB\x02\x18\x01H\x00\x12Q\n\x19last_worker_version_stamp\x18\x0f \x01(\x0b\x32*.temporal.api.common.v1.WorkerVersionStampB\x02\x18\x01\x12\x39\n\x16\x63urrent_retry_interval\x18\x10 \x01(\x0b\x32\x19.google.protobuf.Duration\x12>\n\x1alast_attempt_complete_time\x18\x11 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12>\n\x1anext_attempt_schedule_time\x18\x12 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0e\n\x06paused\x18\x13 \x01(\x08\x12\x43\n\x0flast_deployment\x18\x14 \x01(\x0b\x32&.temporal.api.deployment.v1.DeploymentB\x02\x18\x01\x12*\n\x1elast_worker_deployment_version\x18\x15 \x01(\tB\x02\x18\x01\x12T\n\x17last_deployment_version\x18\x19 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12\x32\n\x08priority\x18\x16 \x01(\x0b\x32 .temporal.api.common.v1.Priority\x12K\n\npause_info\x18\x17 \x01(\x0b\x32\x37.temporal.api.workflow.v1.PendingActivityInfo.PauseInfo\x12\x43\n\x10\x61\x63tivity_options\x18\x18 \x01(\x0b\x32).temporal.api.activity.v1.ActivityOptions\x1a\xcf\x02\n\tPauseInfo\x12.\n\npause_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12P\n\x06manual\x18\x02 \x01(\x0b\x32>.temporal.api.workflow.v1.PendingActivityInfo.PauseInfo.ManualH\x00\x12L\n\x04rule\x18\x04 \x01(\x0b\x32<.temporal.api.workflow.v1.PendingActivityInfo.PauseInfo.RuleH\x00\x1a*\n\x06Manual\x12\x10\n\x08identity\x18\x01 \x01(\t\x12\x0e\n\x06reason\x18\x02 \x01(\t\x1a\x39\n\x04Rule\x12\x0f\n\x07rule_id\x18\x01 \x01(\t\x12\x10\n\x08identity\x18\x02 \x01(\t\x12\x0e\n\x06reason\x18\x03 \x01(\tB\x0b\n\tpaused_byB\x13\n\x11\x61ssigned_build_id"\xb9\x01\n\x19PendingChildExecutionInfo\x12\x13\n\x0bworkflow_id\x18\x01 \x01(\t\x12\x0e\n\x06run_id\x18\x02 \x01(\t\x12\x1a\n\x12workflow_type_name\x18\x03 \x01(\t\x12\x14\n\x0cinitiated_id\x18\x04 \x01(\x03\x12\x45\n\x13parent_close_policy\x18\x05 \x01(\x0e\x32(.temporal.api.enums.v1.ParentClosePolicy"\x8d\x02\n\x17PendingWorkflowTaskInfo\x12>\n\x05state\x18\x01 \x01(\x0e\x32/.temporal.api.enums.v1.PendingWorkflowTaskState\x12\x32\n\x0escheduled_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12;\n\x17original_scheduled_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x30\n\x0cstarted_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0f\n\x07\x61ttempt\x18\x05 \x01(\x05"G\n\x0bResetPoints\x12\x38\n\x06points\x18\x01 \x03(\x0b\x32(.temporal.api.workflow.v1.ResetPointInfo"\xef\x01\n\x0eResetPointInfo\x12\x10\n\x08\x62uild_id\x18\x07 \x01(\t\x12\x1b\n\x0f\x62inary_checksum\x18\x01 \x01(\tB\x02\x18\x01\x12\x0e\n\x06run_id\x18\x02 \x01(\t\x12(\n first_workflow_task_completed_id\x18\x03 \x01(\x03\x12/\n\x0b\x63reate_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0b\x65xpire_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x12\n\nresettable\x18\x06 \x01(\x08"\x85\x07\n\x18NewWorkflowExecutionInfo\x12\x13\n\x0bworkflow_id\x18\x01 \x01(\t\x12;\n\rworkflow_type\x18\x02 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12\x38\n\ntask_queue\x18\x03 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12/\n\x05input\x18\x04 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12=\n\x1aworkflow_execution_timeout\x18\x05 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x37\n\x14workflow_run_timeout\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\x07 \x01(\x0b\x32\x19.google.protobuf.Duration\x12N\n\x18workflow_id_reuse_policy\x18\x08 \x01(\x0e\x32,.temporal.api.enums.v1.WorkflowIdReusePolicy\x12\x39\n\x0cretry_policy\x18\t \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x15\n\rcron_schedule\x18\n \x01(\t\x12*\n\x04memo\x18\x0b \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo\x12\x43\n\x11search_attributes\x18\x0c \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12.\n\x06header\x18\r \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12\x38\n\ruser_metadata\x18\x0e \x01(\x0b\x32!.temporal.api.sdk.v1.UserMetadata\x12I\n\x13versioning_override\x18\x0f \x01(\x0b\x32,.temporal.api.workflow.v1.VersioningOverride\x12\x32\n\x08priority\x18\x10 \x01(\x0b\x32 .temporal.api.common.v1.Priority"\x82\x06\n\x0c\x43\x61llbackInfo\x12\x32\n\x08\x63\x61llback\x18\x01 \x01(\x0b\x32 .temporal.api.common.v1.Callback\x12?\n\x07trigger\x18\x02 \x01(\x0b\x32..temporal.api.workflow.v1.CallbackInfo.Trigger\x12\x35\n\x11registration_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x33\n\x05state\x18\x04 \x01(\x0e\x32$.temporal.api.enums.v1.CallbackState\x12\x0f\n\x07\x61ttempt\x18\x05 \x01(\x05\x12>\n\x1alast_attempt_complete_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12>\n\x14last_attempt_failure\x18\x07 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12>\n\x1anext_attempt_schedule_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x16\n\x0e\x62locked_reason\x18\t \x01(\t\x1a\x10\n\x0eWorkflowClosed\x1a\x35\n UpdateWorkflowExecutionCompleted\x12\x11\n\tupdate_id\x18\x01 \x01(\t\x1a\xde\x01\n\x07Trigger\x12P\n\x0fworkflow_closed\x18\x01 \x01(\x0b\x32\x35.temporal.api.workflow.v1.CallbackInfo.WorkflowClosedH\x00\x12v\n#update_workflow_execution_completed\x18\x02 \x01(\x0b\x32G.temporal.api.workflow.v1.CallbackInfo.UpdateWorkflowExecutionCompletedH\x00\x42\t\n\x07variant"\x8b\x06\n\x19PendingNexusOperationInfo\x12\x10\n\x08\x65ndpoint\x18\x01 \x01(\t\x12\x0f\n\x07service\x18\x02 \x01(\t\x12\x11\n\toperation\x18\x03 \x01(\t\x12\x18\n\x0coperation_id\x18\x04 \x01(\tB\x02\x18\x01\x12<\n\x19schedule_to_close_timeout\x18\x05 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x32\n\x0escheduled_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12@\n\x05state\x18\x07 \x01(\x0e\x32\x31.temporal.api.enums.v1.PendingNexusOperationState\x12\x0f\n\x07\x61ttempt\x18\x08 \x01(\x05\x12>\n\x1alast_attempt_complete_time\x18\t \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12>\n\x14last_attempt_failure\x18\n \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12>\n\x1anext_attempt_schedule_time\x18\x0b \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12S\n\x11\x63\x61ncellation_info\x18\x0c \x01(\x0b\x32\x38.temporal.api.workflow.v1.NexusOperationCancellationInfo\x12\x1a\n\x12scheduled_event_id\x18\r \x01(\x03\x12\x16\n\x0e\x62locked_reason\x18\x0e \x01(\t\x12\x17\n\x0foperation_token\x18\x0f \x01(\t\x12<\n\x19schedule_to_start_timeout\x18\x10 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\x11 \x01(\x0b\x32\x19.google.protobuf.Duration"\x84\x03\n\x1eNexusOperationCancellationInfo\x12\x32\n\x0erequested_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x45\n\x05state\x18\x02 \x01(\x0e\x32\x36.temporal.api.enums.v1.NexusOperationCancellationState\x12\x0f\n\x07\x61ttempt\x18\x03 \x01(\x05\x12>\n\x1alast_attempt_complete_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12>\n\x14last_attempt_failure\x18\x05 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12>\n\x1anext_attempt_schedule_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x16\n\x0e\x62locked_reason\x18\x07 \x01(\t"\xe3\x01\n\x18WorkflowExecutionOptions\x12I\n\x13versioning_override\x18\x01 \x01(\x0b\x32,.temporal.api.workflow.v1.VersioningOverride\x12\x32\n\x08priority\x18\x02 \x01(\x0b\x32 .temporal.api.common.v1.Priority\x12H\n\x14time_skipping_config\x18\x03 \x01(\x0b\x32*.temporal.api.common.v1.TimeSkippingConfig"\xfa\x05\n\x12VersioningOverride\x12M\n\x06pinned\x18\x03 \x01(\x0b\x32;.temporal.api.workflow.v1.VersioningOverride.PinnedOverrideH\x00\x12\x16\n\x0c\x61uto_upgrade\x18\x04 \x01(\x08H\x00\x12P\n\x08one_time\x18\x05 \x01(\x0b\x32<.temporal.api.workflow.v1.VersioningOverride.OneTimeOverrideH\x00\x12?\n\x08\x62\x65havior\x18\x01 \x01(\x0e\x32).temporal.api.enums.v1.VersioningBehaviorB\x02\x18\x01\x12>\n\ndeployment\x18\x02 \x01(\x0b\x32&.temporal.api.deployment.v1.DeploymentB\x02\x18\x01\x12\x1a\n\x0epinned_version\x18\t \x01(\tB\x02\x18\x01\x1a\xad\x01\n\x0ePinnedOverride\x12U\n\x08\x62\x65havior\x18\x01 \x01(\x0e\x32\x43.temporal.api.workflow.v1.VersioningOverride.PinnedOverrideBehavior\x12\x44\n\x07version\x18\x02 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x1ai\n\x0fOneTimeOverride\x12V\n\x19target_deployment_version\x18\x01 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion"g\n\x16PinnedOverrideBehavior\x12(\n$PINNED_OVERRIDE_BEHAVIOR_UNSPECIFIED\x10\x00\x12#\n\x1fPINNED_OVERRIDE_BEHAVIOR_PINNED\x10\x01\x42\n\n\x08override"i\n\x11OnConflictOptions\x12\x19\n\x11\x61ttach_request_id\x18\x01 \x01(\x08\x12#\n\x1b\x61ttach_completion_callbacks\x18\x02 \x01(\x08\x12\x14\n\x0c\x61ttach_links\x18\x03 \x01(\x08"i\n\rRequestIdInfo\x12\x34\n\nevent_type\x18\x01 \x01(\x0e\x32 .temporal.api.enums.v1.EventType\x12\x10\n\x08\x65vent_id\x18\x02 \x01(\x03\x12\x10\n\x08\x62uffered\x18\x03 \x01(\x08"\xb7\x04\n\x12PostResetOperation\x12V\n\x0fsignal_workflow\x18\x01 \x01(\x0b\x32;.temporal.api.workflow.v1.PostResetOperation.SignalWorkflowH\x00\x12\x65\n\x17update_workflow_options\x18\x02 \x01(\x0b\x32\x42.temporal.api.workflow.v1.PostResetOperation.UpdateWorkflowOptionsH\x00\x1a\xb3\x01\n\x0eSignalWorkflow\x12\x13\n\x0bsignal_name\x18\x01 \x01(\t\x12/\n\x05input\x18\x02 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12.\n\x06header\x18\x03 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12+\n\x05links\x18\x04 \x03(\x0b\x32\x1c.temporal.api.common.v1.Link\x1a\xa0\x01\n\x15UpdateWorkflowOptions\x12V\n\x1aworkflow_execution_options\x18\x01 \x01(\x0b\x32\x32.temporal.api.workflow.v1.WorkflowExecutionOptions\x12/\n\x0bupdate_mask\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.FieldMaskB\t\n\x07variant"o\n\x1aWorkflowExecutionPauseInfo\x12\x10\n\x08identity\x18\x01 \x01(\t\x12/\n\x0bpaused_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0e\n\x06reason\x18\x03 \x01(\tB\x93\x01\n\x1bio.temporal.api.workflow.v1B\x0cMessageProtoP\x01Z\'go.temporal.io/api/workflow/v1;workflow\xaa\x02\x1aTemporalio.Api.Workflow.V1\xea\x02\x1dTemporalio::Api::Workflow::V1b\x06proto3' ) @@ -84,6 +84,9 @@ _NEWWORKFLOWEXECUTIONINFO = DESCRIPTOR.message_types_by_name["NewWorkflowExecutionInfo"] _CALLBACKINFO = DESCRIPTOR.message_types_by_name["CallbackInfo"] _CALLBACKINFO_WORKFLOWCLOSED = _CALLBACKINFO.nested_types_by_name["WorkflowClosed"] +_CALLBACKINFO_UPDATEWORKFLOWEXECUTIONCOMPLETED = _CALLBACKINFO.nested_types_by_name[ + "UpdateWorkflowExecutionCompleted" +] _CALLBACKINFO_TRIGGER = _CALLBACKINFO.nested_types_by_name["Trigger"] _PENDINGNEXUSOPERATIONINFO = DESCRIPTOR.message_types_by_name[ "PendingNexusOperationInfo" @@ -96,6 +99,9 @@ _VERSIONINGOVERRIDE_PINNEDOVERRIDE = _VERSIONINGOVERRIDE.nested_types_by_name[ "PinnedOverride" ] +_VERSIONINGOVERRIDE_ONETIMEOVERRIDE = _VERSIONINGOVERRIDE.nested_types_by_name[ + "OneTimeOverride" +] _ONCONFLICTOPTIONS = DESCRIPTOR.message_types_by_name["OnConflictOptions"] _REQUESTIDINFO = DESCRIPTOR.message_types_by_name["RequestIdInfo"] _POSTRESETOPERATION = DESCRIPTOR.message_types_by_name["PostResetOperation"] @@ -105,6 +111,9 @@ _POSTRESETOPERATION_UPDATEWORKFLOWOPTIONS = _POSTRESETOPERATION.nested_types_by_name[ "UpdateWorkflowOptions" ] +_WORKFLOWEXECUTIONPAUSEINFO = DESCRIPTOR.message_types_by_name[ + "WorkflowExecutionPauseInfo" +] _VERSIONINGOVERRIDE_PINNEDOVERRIDEBEHAVIOR = _VERSIONINGOVERRIDE.enum_types_by_name[ "PinnedOverrideBehavior" ] @@ -113,7 +122,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWEXECUTIONINFO, - "__module__": "temporal.api.workflow.v1.message_pb2", + "__module__": "temporalio.api.workflow.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.WorkflowExecutionInfo) }, ) @@ -128,12 +137,12 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWEXECUTIONEXTENDEDINFO_REQUESTIDINFOSENTRY, - "__module__": "temporal.api.workflow.v1.message_pb2", + "__module__": "temporalio.api.workflow.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.WorkflowExecutionExtendedInfo.RequestIdInfosEntry) }, ), "DESCRIPTOR": _WORKFLOWEXECUTIONEXTENDEDINFO, - "__module__": "temporal.api.workflow.v1.message_pb2", + "__module__": "temporalio.api.workflow.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.WorkflowExecutionExtendedInfo) }, ) @@ -145,7 +154,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWEXECUTIONVERSIONINGINFO, - "__module__": "temporal.api.workflow.v1.message_pb2", + "__module__": "temporalio.api.workflow.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.WorkflowExecutionVersioningInfo) }, ) @@ -156,7 +165,7 @@ (_message.Message,), { "DESCRIPTOR": _DEPLOYMENTTRANSITION, - "__module__": "temporal.api.workflow.v1.message_pb2", + "__module__": "temporalio.api.workflow.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.DeploymentTransition) }, ) @@ -167,7 +176,7 @@ (_message.Message,), { "DESCRIPTOR": _DEPLOYMENTVERSIONTRANSITION, - "__module__": "temporal.api.workflow.v1.message_pb2", + "__module__": "temporalio.api.workflow.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.DeploymentVersionTransition) }, ) @@ -178,7 +187,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWEXECUTIONCONFIG, - "__module__": "temporal.api.workflow.v1.message_pb2", + "__module__": "temporalio.api.workflow.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.WorkflowExecutionConfig) }, ) @@ -197,7 +206,7 @@ (_message.Message,), { "DESCRIPTOR": _PENDINGACTIVITYINFO_PAUSEINFO_MANUAL, - "__module__": "temporal.api.workflow.v1.message_pb2", + "__module__": "temporalio.api.workflow.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.PendingActivityInfo.PauseInfo.Manual) }, ), @@ -206,17 +215,17 @@ (_message.Message,), { "DESCRIPTOR": _PENDINGACTIVITYINFO_PAUSEINFO_RULE, - "__module__": "temporal.api.workflow.v1.message_pb2", + "__module__": "temporalio.api.workflow.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.PendingActivityInfo.PauseInfo.Rule) }, ), "DESCRIPTOR": _PENDINGACTIVITYINFO_PAUSEINFO, - "__module__": "temporal.api.workflow.v1.message_pb2", + "__module__": "temporalio.api.workflow.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.PendingActivityInfo.PauseInfo) }, ), "DESCRIPTOR": _PENDINGACTIVITYINFO, - "__module__": "temporal.api.workflow.v1.message_pb2", + "__module__": "temporalio.api.workflow.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.PendingActivityInfo) }, ) @@ -230,7 +239,7 @@ (_message.Message,), { "DESCRIPTOR": _PENDINGCHILDEXECUTIONINFO, - "__module__": "temporal.api.workflow.v1.message_pb2", + "__module__": "temporalio.api.workflow.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.PendingChildExecutionInfo) }, ) @@ -241,7 +250,7 @@ (_message.Message,), { "DESCRIPTOR": _PENDINGWORKFLOWTASKINFO, - "__module__": "temporal.api.workflow.v1.message_pb2", + "__module__": "temporalio.api.workflow.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.PendingWorkflowTaskInfo) }, ) @@ -252,7 +261,7 @@ (_message.Message,), { "DESCRIPTOR": _RESETPOINTS, - "__module__": "temporal.api.workflow.v1.message_pb2", + "__module__": "temporalio.api.workflow.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.ResetPoints) }, ) @@ -263,7 +272,7 @@ (_message.Message,), { "DESCRIPTOR": _RESETPOINTINFO, - "__module__": "temporal.api.workflow.v1.message_pb2", + "__module__": "temporalio.api.workflow.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.ResetPointInfo) }, ) @@ -274,7 +283,7 @@ (_message.Message,), { "DESCRIPTOR": _NEWWORKFLOWEXECUTIONINFO, - "__module__": "temporal.api.workflow.v1.message_pb2", + "__module__": "temporalio.api.workflow.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.NewWorkflowExecutionInfo) }, ) @@ -289,26 +298,36 @@ (_message.Message,), { "DESCRIPTOR": _CALLBACKINFO_WORKFLOWCLOSED, - "__module__": "temporal.api.workflow.v1.message_pb2", + "__module__": "temporalio.api.workflow.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.CallbackInfo.WorkflowClosed) }, ), + "UpdateWorkflowExecutionCompleted": _reflection.GeneratedProtocolMessageType( + "UpdateWorkflowExecutionCompleted", + (_message.Message,), + { + "DESCRIPTOR": _CALLBACKINFO_UPDATEWORKFLOWEXECUTIONCOMPLETED, + "__module__": "temporalio.api.workflow.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.CallbackInfo.UpdateWorkflowExecutionCompleted) + }, + ), "Trigger": _reflection.GeneratedProtocolMessageType( "Trigger", (_message.Message,), { "DESCRIPTOR": _CALLBACKINFO_TRIGGER, - "__module__": "temporal.api.workflow.v1.message_pb2", + "__module__": "temporalio.api.workflow.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.CallbackInfo.Trigger) }, ), "DESCRIPTOR": _CALLBACKINFO, - "__module__": "temporal.api.workflow.v1.message_pb2", + "__module__": "temporalio.api.workflow.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.CallbackInfo) }, ) _sym_db.RegisterMessage(CallbackInfo) _sym_db.RegisterMessage(CallbackInfo.WorkflowClosed) +_sym_db.RegisterMessage(CallbackInfo.UpdateWorkflowExecutionCompleted) _sym_db.RegisterMessage(CallbackInfo.Trigger) PendingNexusOperationInfo = _reflection.GeneratedProtocolMessageType( @@ -316,7 +335,7 @@ (_message.Message,), { "DESCRIPTOR": _PENDINGNEXUSOPERATIONINFO, - "__module__": "temporal.api.workflow.v1.message_pb2", + "__module__": "temporalio.api.workflow.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.PendingNexusOperationInfo) }, ) @@ -327,7 +346,7 @@ (_message.Message,), { "DESCRIPTOR": _NEXUSOPERATIONCANCELLATIONINFO, - "__module__": "temporal.api.workflow.v1.message_pb2", + "__module__": "temporalio.api.workflow.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.NexusOperationCancellationInfo) }, ) @@ -338,7 +357,7 @@ (_message.Message,), { "DESCRIPTOR": _WORKFLOWEXECUTIONOPTIONS, - "__module__": "temporal.api.workflow.v1.message_pb2", + "__module__": "temporalio.api.workflow.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.WorkflowExecutionOptions) }, ) @@ -353,24 +372,34 @@ (_message.Message,), { "DESCRIPTOR": _VERSIONINGOVERRIDE_PINNEDOVERRIDE, - "__module__": "temporal.api.workflow.v1.message_pb2", + "__module__": "temporalio.api.workflow.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.VersioningOverride.PinnedOverride) }, ), + "OneTimeOverride": _reflection.GeneratedProtocolMessageType( + "OneTimeOverride", + (_message.Message,), + { + "DESCRIPTOR": _VERSIONINGOVERRIDE_ONETIMEOVERRIDE, + "__module__": "temporalio.api.workflow.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.VersioningOverride.OneTimeOverride) + }, + ), "DESCRIPTOR": _VERSIONINGOVERRIDE, - "__module__": "temporal.api.workflow.v1.message_pb2", + "__module__": "temporalio.api.workflow.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.VersioningOverride) }, ) _sym_db.RegisterMessage(VersioningOverride) _sym_db.RegisterMessage(VersioningOverride.PinnedOverride) +_sym_db.RegisterMessage(VersioningOverride.OneTimeOverride) OnConflictOptions = _reflection.GeneratedProtocolMessageType( "OnConflictOptions", (_message.Message,), { "DESCRIPTOR": _ONCONFLICTOPTIONS, - "__module__": "temporal.api.workflow.v1.message_pb2", + "__module__": "temporalio.api.workflow.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.OnConflictOptions) }, ) @@ -381,7 +410,7 @@ (_message.Message,), { "DESCRIPTOR": _REQUESTIDINFO, - "__module__": "temporal.api.workflow.v1.message_pb2", + "__module__": "temporalio.api.workflow.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.RequestIdInfo) }, ) @@ -396,7 +425,7 @@ (_message.Message,), { "DESCRIPTOR": _POSTRESETOPERATION_SIGNALWORKFLOW, - "__module__": "temporal.api.workflow.v1.message_pb2", + "__module__": "temporalio.api.workflow.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.PostResetOperation.SignalWorkflow) }, ), @@ -405,12 +434,12 @@ (_message.Message,), { "DESCRIPTOR": _POSTRESETOPERATION_UPDATEWORKFLOWOPTIONS, - "__module__": "temporal.api.workflow.v1.message_pb2", + "__module__": "temporalio.api.workflow.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.PostResetOperation.UpdateWorkflowOptions) }, ), "DESCRIPTOR": _POSTRESETOPERATION, - "__module__": "temporal.api.workflow.v1.message_pb2", + "__module__": "temporalio.api.workflow.v1.message_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.PostResetOperation) }, ) @@ -418,6 +447,17 @@ _sym_db.RegisterMessage(PostResetOperation.SignalWorkflow) _sym_db.RegisterMessage(PostResetOperation.UpdateWorkflowOptions) +WorkflowExecutionPauseInfo = _reflection.GeneratedProtocolMessageType( + "WorkflowExecutionPauseInfo", + (_message.Message,), + { + "DESCRIPTOR": _WORKFLOWEXECUTIONPAUSEINFO, + "__module__": "temporalio.api.workflow.v1.message_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflow.v1.WorkflowExecutionPauseInfo) + }, +) +_sym_db.RegisterMessage(WorkflowExecutionPauseInfo) + if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b"\n\033io.temporal.api.workflow.v1B\014MessageProtoP\001Z'go.temporal.io/api/workflow/v1;workflow\252\002\032Temporalio.Api.Workflow.V1\352\002\035Temporalio::Api::Workflow::V1" @@ -494,63 +534,69 @@ "pinned_version" ]._serialized_options = b"\030\001" _WORKFLOWEXECUTIONINFO._serialized_start = 552 - _WORKFLOWEXECUTIONINFO._serialized_end = 1747 - _WORKFLOWEXECUTIONEXTENDEDINFO._serialized_start = 1750 - _WORKFLOWEXECUTIONEXTENDEDINFO._serialized_end = 2258 - _WORKFLOWEXECUTIONEXTENDEDINFO_REQUESTIDINFOSENTRY._serialized_start = 2164 - _WORKFLOWEXECUTIONEXTENDEDINFO_REQUESTIDINFOSENTRY._serialized_end = 2258 - _WORKFLOWEXECUTIONVERSIONINGINFO._serialized_start = 2261 - _WORKFLOWEXECUTIONVERSIONINGINFO._serialized_end = 2762 - _DEPLOYMENTTRANSITION._serialized_start = 2764 - _DEPLOYMENTTRANSITION._serialized_end = 2846 - _DEPLOYMENTVERSIONTRANSITION._serialized_start = 2849 - _DEPLOYMENTVERSIONTRANSITION._serialized_end = 2980 - _WORKFLOWEXECUTIONCONFIG._serialized_start = 2983 - _WORKFLOWEXECUTIONCONFIG._serialized_end = 3310 - _PENDINGACTIVITYINFO._serialized_start = 3313 - _PENDINGACTIVITYINFO._serialized_end = 5038 - _PENDINGACTIVITYINFO_PAUSEINFO._serialized_start = 4682 - _PENDINGACTIVITYINFO_PAUSEINFO._serialized_end = 5017 - _PENDINGACTIVITYINFO_PAUSEINFO_MANUAL._serialized_start = 4903 - _PENDINGACTIVITYINFO_PAUSEINFO_MANUAL._serialized_end = 4945 - _PENDINGACTIVITYINFO_PAUSEINFO_RULE._serialized_start = 4947 - _PENDINGACTIVITYINFO_PAUSEINFO_RULE._serialized_end = 5004 - _PENDINGCHILDEXECUTIONINFO._serialized_start = 5041 - _PENDINGCHILDEXECUTIONINFO._serialized_end = 5226 - _PENDINGWORKFLOWTASKINFO._serialized_start = 5229 - _PENDINGWORKFLOWTASKINFO._serialized_end = 5498 - _RESETPOINTS._serialized_start = 5500 - _RESETPOINTS._serialized_end = 5571 - _RESETPOINTINFO._serialized_start = 5574 - _RESETPOINTINFO._serialized_end = 5813 - _NEWWORKFLOWEXECUTIONINFO._serialized_start = 5816 - _NEWWORKFLOWEXECUTIONINFO._serialized_end = 6717 - _CALLBACKINFO._serialized_start = 6720 - _CALLBACKINFO._serialized_end = 7314 - _CALLBACKINFO_WORKFLOWCLOSED._serialized_start = 7194 - _CALLBACKINFO_WORKFLOWCLOSED._serialized_end = 7210 - _CALLBACKINFO_TRIGGER._serialized_start = 7212 - _CALLBACKINFO_TRIGGER._serialized_end = 7314 - _PENDINGNEXUSOPERATIONINFO._serialized_start = 7317 - _PENDINGNEXUSOPERATIONINFO._serialized_end = 7975 - _NEXUSOPERATIONCANCELLATIONINFO._serialized_start = 7978 - _NEXUSOPERATIONCANCELLATIONINFO._serialized_end = 8366 - _WORKFLOWEXECUTIONOPTIONS._serialized_start = 8368 - _WORKFLOWEXECUTIONOPTIONS._serialized_end = 8469 - _VERSIONINGOVERRIDE._serialized_start = 8472 - _VERSIONINGOVERRIDE._serialized_end = 9045 - _VERSIONINGOVERRIDE_PINNEDOVERRIDE._serialized_start = 8755 - _VERSIONINGOVERRIDE_PINNEDOVERRIDE._serialized_end = 8928 - _VERSIONINGOVERRIDE_PINNEDOVERRIDEBEHAVIOR._serialized_start = 8930 - _VERSIONINGOVERRIDE_PINNEDOVERRIDEBEHAVIOR._serialized_end = 9033 - _ONCONFLICTOPTIONS._serialized_start = 9047 - _ONCONFLICTOPTIONS._serialized_end = 9152 - _REQUESTIDINFO._serialized_start = 9154 - _REQUESTIDINFO._serialized_end = 9259 - _POSTRESETOPERATION._serialized_start = 9262 - _POSTRESETOPERATION._serialized_end = 9829 - _POSTRESETOPERATION_SIGNALWORKFLOW._serialized_start = 9476 - _POSTRESETOPERATION_SIGNALWORKFLOW._serialized_end = 9655 - _POSTRESETOPERATION_UPDATEWORKFLOWOPTIONS._serialized_start = 9658 - _POSTRESETOPERATION_UPDATEWORKFLOWOPTIONS._serialized_end = 9818 + _WORKFLOWEXECUTIONINFO._serialized_end = 1816 + _WORKFLOWEXECUTIONEXTENDEDINFO._serialized_start = 1819 + _WORKFLOWEXECUTIONEXTENDEDINFO._serialized_end = 2471 + _WORKFLOWEXECUTIONEXTENDEDINFO_REQUESTIDINFOSENTRY._serialized_start = 2377 + _WORKFLOWEXECUTIONEXTENDEDINFO_REQUESTIDINFOSENTRY._serialized_end = 2471 + _WORKFLOWEXECUTIONVERSIONINGINFO._serialized_start = 2474 + _WORKFLOWEXECUTIONVERSIONINGINFO._serialized_end = 3109 + _DEPLOYMENTTRANSITION._serialized_start = 3111 + _DEPLOYMENTTRANSITION._serialized_end = 3193 + _DEPLOYMENTVERSIONTRANSITION._serialized_start = 3196 + _DEPLOYMENTVERSIONTRANSITION._serialized_end = 3327 + _WORKFLOWEXECUTIONCONFIG._serialized_start = 3330 + _WORKFLOWEXECUTIONCONFIG._serialized_end = 3657 + _PENDINGACTIVITYINFO._serialized_start = 3660 + _PENDINGACTIVITYINFO._serialized_end = 5385 + _PENDINGACTIVITYINFO_PAUSEINFO._serialized_start = 5029 + _PENDINGACTIVITYINFO_PAUSEINFO._serialized_end = 5364 + _PENDINGACTIVITYINFO_PAUSEINFO_MANUAL._serialized_start = 5250 + _PENDINGACTIVITYINFO_PAUSEINFO_MANUAL._serialized_end = 5292 + _PENDINGACTIVITYINFO_PAUSEINFO_RULE._serialized_start = 5294 + _PENDINGACTIVITYINFO_PAUSEINFO_RULE._serialized_end = 5351 + _PENDINGCHILDEXECUTIONINFO._serialized_start = 5388 + _PENDINGCHILDEXECUTIONINFO._serialized_end = 5573 + _PENDINGWORKFLOWTASKINFO._serialized_start = 5576 + _PENDINGWORKFLOWTASKINFO._serialized_end = 5845 + _RESETPOINTS._serialized_start = 5847 + _RESETPOINTS._serialized_end = 5918 + _RESETPOINTINFO._serialized_start = 5921 + _RESETPOINTINFO._serialized_end = 6160 + _NEWWORKFLOWEXECUTIONINFO._serialized_start = 6163 + _NEWWORKFLOWEXECUTIONINFO._serialized_end = 7064 + _CALLBACKINFO._serialized_start = 7067 + _CALLBACKINFO._serialized_end = 7837 + _CALLBACKINFO_WORKFLOWCLOSED._serialized_start = 7541 + _CALLBACKINFO_WORKFLOWCLOSED._serialized_end = 7557 + _CALLBACKINFO_UPDATEWORKFLOWEXECUTIONCOMPLETED._serialized_start = 7559 + _CALLBACKINFO_UPDATEWORKFLOWEXECUTIONCOMPLETED._serialized_end = 7612 + _CALLBACKINFO_TRIGGER._serialized_start = 7615 + _CALLBACKINFO_TRIGGER._serialized_end = 7837 + _PENDINGNEXUSOPERATIONINFO._serialized_start = 7840 + _PENDINGNEXUSOPERATIONINFO._serialized_end = 8619 + _NEXUSOPERATIONCANCELLATIONINFO._serialized_start = 8622 + _NEXUSOPERATIONCANCELLATIONINFO._serialized_end = 9010 + _WORKFLOWEXECUTIONOPTIONS._serialized_start = 9013 + _WORKFLOWEXECUTIONOPTIONS._serialized_end = 9240 + _VERSIONINGOVERRIDE._serialized_start = 9243 + _VERSIONINGOVERRIDE._serialized_end = 10005 + _VERSIONINGOVERRIDE_PINNEDOVERRIDE._serialized_start = 9608 + _VERSIONINGOVERRIDE_PINNEDOVERRIDE._serialized_end = 9781 + _VERSIONINGOVERRIDE_ONETIMEOVERRIDE._serialized_start = 9783 + _VERSIONINGOVERRIDE_ONETIMEOVERRIDE._serialized_end = 9888 + _VERSIONINGOVERRIDE_PINNEDOVERRIDEBEHAVIOR._serialized_start = 9890 + _VERSIONINGOVERRIDE_PINNEDOVERRIDEBEHAVIOR._serialized_end = 9993 + _ONCONFLICTOPTIONS._serialized_start = 10007 + _ONCONFLICTOPTIONS._serialized_end = 10112 + _REQUESTIDINFO._serialized_start = 10114 + _REQUESTIDINFO._serialized_end = 10219 + _POSTRESETOPERATION._serialized_start = 10222 + _POSTRESETOPERATION._serialized_end = 10789 + _POSTRESETOPERATION_SIGNALWORKFLOW._serialized_start = 10436 + _POSTRESETOPERATION_SIGNALWORKFLOW._serialized_end = 10615 + _POSTRESETOPERATION_UPDATEWORKFLOWOPTIONS._serialized_start = 10618 + _POSTRESETOPERATION_UPDATEWORKFLOWOPTIONS._serialized_end = 10778 + _WORKFLOWEXECUTIONPAUSEINFO._serialized_start = 10791 + _WORKFLOWEXECUTIONPAUSEINFO._serialized_end = 10902 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/workflow/v1/message_pb2.pyi b/temporalio/api/workflow/v1/message_pb2.pyi index 869790809..b390d94f4 100644 --- a/temporalio/api/workflow/v1/message_pb2.pyi +++ b/temporalio/api/workflow/v1/message_pb2.pyi @@ -65,6 +65,8 @@ class WorkflowExecutionInfo(google.protobuf.message.Message): VERSIONING_INFO_FIELD_NUMBER: builtins.int WORKER_DEPLOYMENT_NAME_FIELD_NUMBER: builtins.int PRIORITY_FIELD_NUMBER: builtins.int + EXTERNAL_PAYLOAD_SIZE_BYTES_FIELD_NUMBER: builtins.int + EXTERNAL_PAYLOAD_COUNT_FIELD_NUMBER: builtins.int @property def execution(self) -> temporalio.api.common.v1.message_pb2.WorkflowExecution: ... @property @@ -154,12 +156,14 @@ class WorkflowExecutionInfo(google.protobuf.message.Message): Experimental. Versioning info is experimental and might change in the future. """ worker_deployment_name: builtins.str - """The name of Worker Deployment that completed the most recent workflow task. - Experimental. Worker Deployments are experimental and might change in the future. - """ + """The name of Worker Deployment that completed the most recent workflow task.""" @property def priority(self) -> temporalio.api.common.v1.message_pb2.Priority: """Priority metadata""" + external_payload_size_bytes: builtins.int + """Total size in bytes of all external payloads referenced in workflow history.""" + external_payload_count: builtins.int + """Count of external payloads referenced in workflow history.""" def __init__( self, *, @@ -191,6 +195,8 @@ class WorkflowExecutionInfo(google.protobuf.message.Message): versioning_info: global___WorkflowExecutionVersioningInfo | None = ..., worker_deployment_name: builtins.str = ..., priority: temporalio.api.common.v1.message_pb2.Priority | None = ..., + external_payload_size_bytes: builtins.int = ..., + external_payload_count: builtins.int = ..., ) -> None: ... def HasField( self, @@ -240,6 +246,10 @@ class WorkflowExecutionInfo(google.protobuf.message.Message): b"execution_duration", "execution_time", b"execution_time", + "external_payload_count", + b"external_payload_count", + "external_payload_size_bytes", + b"external_payload_size_bytes", "first_run_id", b"first_run_id", "history_length", @@ -315,6 +325,8 @@ class WorkflowExecutionExtendedInfo(google.protobuf.message.Message): ORIGINAL_START_TIME_FIELD_NUMBER: builtins.int RESET_RUN_ID_FIELD_NUMBER: builtins.int REQUEST_ID_INFOS_FIELD_NUMBER: builtins.int + PAUSE_INFO_FIELD_NUMBER: builtins.int + TIME_SKIPPING_INFO_FIELD_NUMBER: builtins.int @property def execution_expiration_time(self) -> google.protobuf.timestamp_pb2.Timestamp: """Workflow execution expiration time is defined as workflow start time plus expiration timeout. @@ -344,6 +356,16 @@ class WorkflowExecutionExtendedInfo(google.protobuf.message.Message): calls (eg: if SignalWithStartWorkflowExecution starts a new workflow, then the request ID is used in the StartWorkflowExecution request). """ + @property + def pause_info(self) -> global___WorkflowExecutionPauseInfo: + """Information about the workflow execution pause operation.""" + @property + def time_skipping_info( + self, + ) -> temporalio.api.common.v1.message_pb2.TimeSkippingInfo: + """Information about time skipping of the workflow execution. + If the execution has never enabled time skipping, it will be nil. + """ def __init__( self, *, @@ -355,6 +377,9 @@ class WorkflowExecutionExtendedInfo(google.protobuf.message.Message): reset_run_id: builtins.str = ..., request_id_infos: collections.abc.Mapping[builtins.str, global___RequestIdInfo] | None = ..., + pause_info: global___WorkflowExecutionPauseInfo | None = ..., + time_skipping_info: temporalio.api.common.v1.message_pb2.TimeSkippingInfo + | None = ..., ) -> None: ... def HasField( self, @@ -365,8 +390,12 @@ class WorkflowExecutionExtendedInfo(google.protobuf.message.Message): b"last_reset_time", "original_start_time", b"original_start_time", + "pause_info", + b"pause_info", "run_expiration_time", b"run_expiration_time", + "time_skipping_info", + b"time_skipping_info", ], ) -> builtins.bool: ... def ClearField( @@ -380,12 +409,16 @@ class WorkflowExecutionExtendedInfo(google.protobuf.message.Message): b"last_reset_time", "original_start_time", b"original_start_time", + "pause_info", + b"pause_info", "request_id_infos", b"request_id_infos", "reset_run_id", b"reset_run_id", "run_expiration_time", b"run_expiration_time", + "time_skipping_info", + b"time_skipping_info", ], ) -> None: ... @@ -405,17 +438,21 @@ class WorkflowExecutionVersioningInfo(google.protobuf.message.Message): VERSIONING_OVERRIDE_FIELD_NUMBER: builtins.int DEPLOYMENT_TRANSITION_FIELD_NUMBER: builtins.int VERSION_TRANSITION_FIELD_NUMBER: builtins.int + REVISION_NUMBER_FIELD_NUMBER: builtins.int + CONTINUE_AS_NEW_INITIAL_VERSIONING_BEHAVIOR_FIELD_NUMBER: builtins.int behavior: temporalio.api.enums.v1.workflow_pb2.VersioningBehavior.ValueType """Versioning behavior determines how the server should treat this execution when workers are upgraded. When present it means this workflow execution is versioned; UNSPECIFIED means unversioned. See the comments in `VersioningBehavior` enum for more info about different behaviors. - This field is first set after an execution completes its first workflow task on a versioned - worker, and set again on completion of every subsequent workflow task. - For child workflows of Pinned parents, this will be set to Pinned (along with `deployment_version`) when - the the child starts so that child's first workflow task goes to the same Version as the - parent. After the first workflow task, it depends on the child workflow itself if it wants - to stay pinned or become unpinned (according to Versioning Behavior set in the worker). + + Child workflows or CaN executions **inherit** their parent/previous run's effective Versioning + Behavior and Version (except when the new execution runs on a task queue not belonging to the + same deployment version as the parent/previous run's task queue). The first workflow task will + be dispatched according to the inherited behavior (or to the current version of the task-queue's + deployment in the case of AutoUpgrade.) After completion of their first workflow task the + Deployment Version and Behavior of the execution will update according to configuration on the worker. + Note that `behavior` is overridden by `versioning_override` if the latter is present. """ @property @@ -439,8 +476,13 @@ class WorkflowExecutionVersioningInfo(google.protobuf.message.Message): If present, and `behavior` is UNSPECIFIED, the last task of this workflow execution was completed by a worker that is not using versioning but _is_ passing Deployment Name and Build ID. - For child workflows of Pinned parents, this will be set to the parent's Pinned Version when - the child starts, so that the child's first workflow task goes to the same Version as the parent. + Child workflows or CaN executions **inherit** their parent/previous run's effective Versioning + Behavior and Version (except when the new execution runs on a task queue not belonging to the + same deployment version as the parent/previous run's task queue). The first workflow task will + be dispatched according to the inherited behavior (or to the current version of the task-queue's + deployment in the case of AutoUpgrade.) After completion of their first workflow task the + Deployment Version and Behavior of the execution will update according to configuration on the worker. + Note that if `versioning_override.behavior` is PINNED then `versioning_override.pinned_version` will override this value. """ @@ -495,6 +537,29 @@ class WorkflowExecutionVersioningInfo(google.protobuf.message.Message): Pending activities will not start new attempts during a transition. Once the transition is completed, pending activities will start their next attempt on the new version. """ + revision_number: builtins.int + """Monotonic counter reflecting the latest routing decision for this workflow execution. + Used for staleness detection between history and matching when dispatching tasks to workers. + Incremented when a workflow execution routes to a new deployment version, which happens + when a worker of the new deployment version completes a workflow task. + Note: Pinned tasks and sticky tasks send a value of 0 for this field since these tasks do not + face the problem of inconsistent dispatching that arises from eventual consistency between + task queues and their partitions. + """ + continue_as_new_initial_versioning_behavior: ( + temporalio.api.enums.v1.workflow_pb2.ContinueAsNewVersioningBehavior.ValueType + ) + """Experimental. + If this workflow is the result of a continue-as-new, this field is set to the initial_versioning_behavior + specified in that command. + Only used for the initial task of this run and the initial task of any retries of this run. + Not passed to children or to future continue-as-new. + + Note: In the first release of Upgrade-on-CaN, when the only ContinueAsNewVersioningBehavior was AutoUpgrade, + a non-empty InheritedAutoUpgradeInfo meant that the workflow should start as AutoUpgrade. So for compatibility + with ContinueAsNew history commands generated during that time, know that an UNSPECIFIED value here is equivalent + to ContinueAsNewVersioningBehaviorAutoUpgrade if the behavior of the workflow is AutoUpgrade. + """ def __init__( self, *, @@ -506,6 +571,8 @@ class WorkflowExecutionVersioningInfo(google.protobuf.message.Message): versioning_override: global___VersioningOverride | None = ..., deployment_transition: global___DeploymentTransition | None = ..., version_transition: global___DeploymentVersionTransition | None = ..., + revision_number: builtins.int = ..., + continue_as_new_initial_versioning_behavior: temporalio.api.enums.v1.workflow_pb2.ContinueAsNewVersioningBehavior.ValueType = ..., ) -> None: ... def HasField( self, @@ -527,12 +594,16 @@ class WorkflowExecutionVersioningInfo(google.protobuf.message.Message): field_name: typing_extensions.Literal[ "behavior", b"behavior", + "continue_as_new_initial_versioning_behavior", + b"continue_as_new_initial_versioning_behavior", "deployment", b"deployment", "deployment_transition", b"deployment_transition", "deployment_version", b"deployment_version", + "revision_number", + b"revision_number", "version", b"version", "version_transition", @@ -876,7 +947,9 @@ class PendingActivityInfo(google.protobuf.message.Message): """ @property def priority(self) -> temporalio.api.common.v1.message_pb2.Priority: - """Priority metadata""" + """Priority metadata. If this message is not present, or any fields are not + present, they inherit the values from the workflow. + """ @property def pause_info(self) -> global___PendingActivityInfo.PauseInfo: ... @property @@ -1406,32 +1479,70 @@ class CallbackInfo(google.protobuf.message.Message): self, ) -> None: ... + class UpdateWorkflowExecutionCompleted(google.protobuf.message.Message): + """Trigger for when a workflow update is completed.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + UPDATE_ID_FIELD_NUMBER: builtins.int + update_id: builtins.str + def __init__( + self, + *, + update_id: builtins.str = ..., + ) -> None: ... + def ClearField( + self, field_name: typing_extensions.Literal["update_id", b"update_id"] + ) -> None: ... + class Trigger(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor WORKFLOW_CLOSED_FIELD_NUMBER: builtins.int + UPDATE_WORKFLOW_EXECUTION_COMPLETED_FIELD_NUMBER: builtins.int @property def workflow_closed(self) -> global___CallbackInfo.WorkflowClosed: ... + @property + def update_workflow_execution_completed( + self, + ) -> global___CallbackInfo.UpdateWorkflowExecutionCompleted: ... def __init__( self, *, workflow_closed: global___CallbackInfo.WorkflowClosed | None = ..., + update_workflow_execution_completed: global___CallbackInfo.UpdateWorkflowExecutionCompleted + | None = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal[ - "variant", b"variant", "workflow_closed", b"workflow_closed" + "update_workflow_execution_completed", + b"update_workflow_execution_completed", + "variant", + b"variant", + "workflow_closed", + b"workflow_closed", ], ) -> builtins.bool: ... def ClearField( self, field_name: typing_extensions.Literal[ - "variant", b"variant", "workflow_closed", b"workflow_closed" + "update_workflow_execution_completed", + b"update_workflow_execution_completed", + "variant", + b"variant", + "workflow_closed", + b"workflow_closed", ], ) -> None: ... def WhichOneof( self, oneof_group: typing_extensions.Literal["variant", b"variant"] - ) -> typing_extensions.Literal["workflow_closed"] | None: ... + ) -> ( + typing_extensions.Literal[ + "workflow_closed", "update_workflow_execution_completed" + ] + | None + ): ... CALLBACK_FIELD_NUMBER: builtins.int TRIGGER_FIELD_NUMBER: builtins.int @@ -1546,6 +1657,8 @@ class PendingNexusOperationInfo(google.protobuf.message.Message): SCHEDULED_EVENT_ID_FIELD_NUMBER: builtins.int BLOCKED_REASON_FIELD_NUMBER: builtins.int OPERATION_TOKEN_FIELD_NUMBER: builtins.int + SCHEDULE_TO_START_TIMEOUT_FIELD_NUMBER: builtins.int + START_TO_CLOSE_TIMEOUT_FIELD_NUMBER: builtins.int endpoint: builtins.str """Endpoint name. Resolved to a URL via the cluster's endpoint registry. @@ -1572,7 +1685,9 @@ class PendingNexusOperationInfo(google.protobuf.message.Message): state: temporalio.api.enums.v1.common_pb2.PendingNexusOperationState.ValueType attempt: builtins.int """The number of attempts made to deliver the start operation request. - This number represents a minimum bound since the attempt is incremented after the request completes. + This number is approximate, it is incremented when a task is added to the history queue. + In practice, there could be more attempts if a task is executed but fails to commit, or less attempts if a task + was never executed. """ @property def last_attempt_complete_time(self) -> google.protobuf.timestamp_pb2.Timestamp: @@ -1593,6 +1708,18 @@ class PendingNexusOperationInfo(google.protobuf.message.Message): """If the state is BLOCKED, blocked reason provides additional information.""" operation_token: builtins.str """Operation token. Only set for asynchronous operations after a successful StartOperation call.""" + @property + def schedule_to_start_timeout(self) -> google.protobuf.duration_pb2.Duration: + """Schedule-to-start timeout for this operation. + (-- api-linter: core::0140::prepositions=disabled + aip.dev/not-precedent: "to" is used to indicate interval. --) + """ + @property + def start_to_close_timeout(self) -> google.protobuf.duration_pb2.Duration: + """Start-to-close timeout for this operation. + (-- api-linter: core::0140::prepositions=disabled + aip.dev/not-precedent: "to" is used to indicate interval. --) + """ def __init__( self, *, @@ -1614,6 +1741,8 @@ class PendingNexusOperationInfo(google.protobuf.message.Message): scheduled_event_id: builtins.int = ..., blocked_reason: builtins.str = ..., operation_token: builtins.str = ..., + schedule_to_start_timeout: google.protobuf.duration_pb2.Duration | None = ..., + start_to_close_timeout: google.protobuf.duration_pb2.Duration | None = ..., ) -> None: ... def HasField( self, @@ -1628,8 +1757,12 @@ class PendingNexusOperationInfo(google.protobuf.message.Message): b"next_attempt_schedule_time", "schedule_to_close_timeout", b"schedule_to_close_timeout", + "schedule_to_start_timeout", + b"schedule_to_start_timeout", "scheduled_time", b"scheduled_time", + "start_to_close_timeout", + b"start_to_close_timeout", ], ) -> builtins.bool: ... def ClearField( @@ -1657,12 +1790,16 @@ class PendingNexusOperationInfo(google.protobuf.message.Message): b"operation_token", "schedule_to_close_timeout", b"schedule_to_close_timeout", + "schedule_to_start_timeout", + b"schedule_to_start_timeout", "scheduled_event_id", b"scheduled_event_id", "scheduled_time", b"scheduled_time", "service", b"service", + "start_to_close_timeout", + b"start_to_close_timeout", "state", b"state", ], @@ -1754,24 +1891,56 @@ class WorkflowExecutionOptions(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor VERSIONING_OVERRIDE_FIELD_NUMBER: builtins.int + PRIORITY_FIELD_NUMBER: builtins.int + TIME_SKIPPING_CONFIG_FIELD_NUMBER: builtins.int @property def versioning_override(self) -> global___VersioningOverride: """If set, takes precedence over the Versioning Behavior sent by the SDK on Workflow Task completion.""" + @property + def priority(self) -> temporalio.api.common.v1.message_pb2.Priority: + """If set, overrides the workflow's priority sent by the SDK.""" + @property + def time_skipping_config( + self, + ) -> temporalio.api.common.v1.message_pb2.TimeSkippingConfig: + """The time-skipping configuration for this workflow execution. + When `fast_forward` is set, time will be fast-forwarded to a future point relative + to the current workflow timestamp. Each call takes effect, even if + `fast_forward` is set to the same duration, since the target time is recalculated + from the current timestamp on every call. + + This field must be updated as a whole; updating individual sub-fields is not supported. + When setting the update mask in `UpdateWorkflowExecutionOptionsRequest`, + `BatchOperationUpdateWorkflowExecutionOptions`, etc., use a mask that covers the entire field. + """ def __init__( self, *, versioning_override: global___VersioningOverride | None = ..., + priority: temporalio.api.common.v1.message_pb2.Priority | None = ..., + time_skipping_config: temporalio.api.common.v1.message_pb2.TimeSkippingConfig + | None = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal[ - "versioning_override", b"versioning_override" + "priority", + b"priority", + "time_skipping_config", + b"time_skipping_config", + "versioning_override", + b"versioning_override", ], ) -> builtins.bool: ... def ClearField( self, field_name: typing_extensions.Literal[ - "versioning_override", b"versioning_override" + "priority", + b"priority", + "time_skipping_config", + b"time_skipping_config", + "versioning_override", + b"versioning_override", ], ) -> None: ... @@ -1779,12 +1948,14 @@ global___WorkflowExecutionOptions = WorkflowExecutionOptions class VersioningOverride(google.protobuf.message.Message): """Used to override the versioning behavior (and pinned deployment version, if applicable) of a - specific workflow execution. If set, takes precedence over the worker-sent values. See - `WorkflowExecutionInfo.VersioningInfo` for more information. To remove the override, call - `UpdateWorkflowExecutionOptions` with a null `VersioningOverride`, and use the `update_mask` - to indicate that it should be mutated. - Pinned overrides are automatically inherited by child workflows, continue-as-new workflows, - workflow retries, and cron workflows. + specific workflow execution. If set, this override takes precedence over worker-sent values. + See `WorkflowExecutionInfo.VersioningInfo` for more information. + + To remove the override, call `UpdateWorkflowExecutionOptions` with a null + `VersioningOverride`, and use the `update_mask` to indicate that it should be mutated. + + Pinned behavior overrides are automatically inherited by child workflows, workflow retries, continue-as-new + workflows, and cron workflows. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -1811,9 +1982,7 @@ class VersioningOverride(google.protobuf.message.Message): class PinnedOverrideBehavior( _PinnedOverrideBehavior, metaclass=_PinnedOverrideBehaviorEnumTypeWrapper - ): - """Used to specify different sub-types of Pinned override that we plan to add in the future.""" - + ): ... PINNED_OVERRIDE_BEHAVIOR_UNSPECIFIED: ( VersioningOverride.PinnedOverrideBehavior.ValueType ) # 0 @@ -1836,7 +2005,15 @@ class VersioningOverride(google.protobuf.message.Message): def version( self, ) -> temporalio.api.deployment.v1.message_pb2.WorkerDeploymentVersion: - """Required.""" + """Specifies the Worker Deployment Version to pin this workflow to. + Required if the target workflow is not already pinned to a version. + + If omitted and the target workflow is already pinned, the effective + pinned version will be the existing pinned version. + + If omitted and the target workflow is not pinned, the override request + will be rejected with a PreconditionFailed error. + """ def __init__( self, *, @@ -1854,18 +2031,78 @@ class VersioningOverride(google.protobuf.message.Message): ], ) -> None: ... + class OneTimeOverride(google.protobuf.message.Message): + """Routes Workflow Tasks for this execution to `target_deployment_version` + until a Workflow Task completes on that version, then clears the override. + + This does not force the workflow's normal Versioning Behavior to become + Pinned. After the Workflow Task completes on `target_deployment_version`, + the workflow execution's normal Versioning Behavior and Deployment Version + are taken from the worker's completion response. + + Example: if an execution is one-time moved from version X to version Y, and + version Z later becomes current: + - if worker Y reports Pinned, the execution stays on Y; + - if worker Y reports AutoUpgrade, the execution routes to Z on a future + Workflow Task; + - if worker Y reports Pinned and the workflow uses upgrade-on-continue-as-new, + the current run stays on Y and the execution can route to Z after + continue-as-new. + + If no Workflow Task completes on `target_deployment_version`, this override + remains pending. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TARGET_DEPLOYMENT_VERSION_FIELD_NUMBER: builtins.int + @property + def target_deployment_version( + self, + ) -> temporalio.api.deployment.v1.message_pb2.WorkerDeploymentVersion: + """Required. Worker Deployment Version to receive the one-time Workflow Task.""" + def __init__( + self, + *, + target_deployment_version: temporalio.api.deployment.v1.message_pb2.WorkerDeploymentVersion + | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "target_deployment_version", b"target_deployment_version" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "target_deployment_version", b"target_deployment_version" + ], + ) -> None: ... + PINNED_FIELD_NUMBER: builtins.int AUTO_UPGRADE_FIELD_NUMBER: builtins.int + ONE_TIME_FIELD_NUMBER: builtins.int BEHAVIOR_FIELD_NUMBER: builtins.int DEPLOYMENT_FIELD_NUMBER: builtins.int PINNED_VERSION_FIELD_NUMBER: builtins.int @property def pinned(self) -> global___VersioningOverride.PinnedOverride: - """Send the next workflow task to the Version specified in the override.""" + """Override the workflow to have Pinned behavior. This is a sticky override: + Workflow Tasks continue to route according to this override until it is + explicitly removed. + """ auto_upgrade: builtins.bool - """Send the next workflow task to the Current Deployment Version - of its Task Queue when the next workflow task is dispatched. - """ + """Override the workflow to have AutoUpgrade behavior.""" + @property + def one_time(self) -> global___VersioningOverride.OneTimeOverride: + """Override Workflow Task routing to a specific Worker Deployment Version until + one Workflow Task completes there. After completion, the workflow execution's + Versioning Behavior and Deployment Version come from the worker's completion + response. + (-- api-linter: core::0142::time-field-type=disabled + aip.dev/not-precedent: one_time describes one-time routing semantics, not a timestamp or duration. --) + """ behavior: temporalio.api.enums.v1.workflow_pb2.VersioningBehavior.ValueType """Required. Deprecated. Use `override`. @@ -1887,6 +2124,7 @@ class VersioningOverride(google.protobuf.message.Message): *, pinned: global___VersioningOverride.PinnedOverride | None = ..., auto_upgrade: builtins.bool = ..., + one_time: global___VersioningOverride.OneTimeOverride | None = ..., behavior: temporalio.api.enums.v1.workflow_pb2.VersioningBehavior.ValueType = ..., deployment: temporalio.api.deployment.v1.message_pb2.Deployment | None = ..., pinned_version: builtins.str = ..., @@ -1898,6 +2136,8 @@ class VersioningOverride(google.protobuf.message.Message): b"auto_upgrade", "deployment", b"deployment", + "one_time", + b"one_time", "override", b"override", "pinned", @@ -1913,6 +2153,8 @@ class VersioningOverride(google.protobuf.message.Message): b"behavior", "deployment", b"deployment", + "one_time", + b"one_time", "override", b"override", "pinned", @@ -1923,7 +2165,7 @@ class VersioningOverride(google.protobuf.message.Message): ) -> None: ... def WhichOneof( self, oneof_group: typing_extensions.Literal["override", b"override"] - ) -> typing_extensions.Literal["pinned", "auto_upgrade"] | None: ... + ) -> typing_extensions.Literal["pinned", "auto_upgrade", "one_time"] | None: ... global___VersioningOverride = VersioningOverride @@ -2152,3 +2394,37 @@ class PostResetOperation(google.protobuf.message.Message): ): ... global___PostResetOperation = PostResetOperation + +class WorkflowExecutionPauseInfo(google.protobuf.message.Message): + """WorkflowExecutionPauseInfo contains the information about a workflow execution pause.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + IDENTITY_FIELD_NUMBER: builtins.int + PAUSED_TIME_FIELD_NUMBER: builtins.int + REASON_FIELD_NUMBER: builtins.int + identity: builtins.str + """The identity of the client who paused the workflow execution.""" + @property + def paused_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """The time when the workflow execution was paused.""" + reason: builtins.str + """The reason for pausing the workflow execution.""" + def __init__( + self, + *, + identity: builtins.str = ..., + paused_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + reason: builtins.str = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["paused_time", b"paused_time"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "identity", b"identity", "paused_time", b"paused_time", "reason", b"reason" + ], + ) -> None: ... + +global___WorkflowExecutionPauseInfo = WorkflowExecutionPauseInfo diff --git a/temporalio/api/workflowservice/v1/__init__.py b/temporalio/api/workflowservice/v1/__init__.py index 8f82668cb..ab72e6b09 100644 --- a/temporalio/api/workflowservice/v1/__init__.py +++ b/temporalio/api/workflowservice/v1/__init__.py @@ -1,10 +1,26 @@ from .request_response_pb2 import ( + CountActivityExecutionsRequest, + CountActivityExecutionsResponse, + CountNexusOperationExecutionsRequest, + CountNexusOperationExecutionsResponse, + CountSchedulesRequest, + CountSchedulesResponse, + CountWorkersRequest, + CountWorkersResponse, CountWorkflowExecutionsRequest, CountWorkflowExecutionsResponse, CreateScheduleRequest, CreateScheduleResponse, + CreateWorkerDeploymentRequest, + CreateWorkerDeploymentResponse, + CreateWorkerDeploymentVersionRequest, + CreateWorkerDeploymentVersionResponse, CreateWorkflowRuleRequest, CreateWorkflowRuleResponse, + DeleteActivityExecutionRequest, + DeleteActivityExecutionResponse, + DeleteNexusOperationExecutionRequest, + DeleteNexusOperationExecutionResponse, DeleteScheduleRequest, DeleteScheduleResponse, DeleteWorkerDeploymentRequest, @@ -17,12 +33,16 @@ DeleteWorkflowRuleResponse, DeprecateNamespaceRequest, DeprecateNamespaceResponse, + DescribeActivityExecutionRequest, + DescribeActivityExecutionResponse, DescribeBatchOperationRequest, DescribeBatchOperationResponse, DescribeDeploymentRequest, DescribeDeploymentResponse, DescribeNamespaceRequest, DescribeNamespaceResponse, + DescribeNexusOperationExecutionRequest, + DescribeNexusOperationExecutionResponse, DescribeScheduleRequest, DescribeScheduleResponse, DescribeTaskQueueRequest, @@ -31,6 +51,8 @@ DescribeWorkerDeploymentResponse, DescribeWorkerDeploymentVersionRequest, DescribeWorkerDeploymentVersionResponse, + DescribeWorkerRequest, + DescribeWorkerResponse, DescribeWorkflowExecutionRequest, DescribeWorkflowExecutionResponse, DescribeWorkflowRuleRequest, @@ -59,6 +81,8 @@ GetWorkflowExecutionHistoryResponse, GetWorkflowExecutionHistoryReverseRequest, GetWorkflowExecutionHistoryReverseResponse, + ListActivityExecutionsRequest, + ListActivityExecutionsResponse, ListArchivedWorkflowExecutionsRequest, ListArchivedWorkflowExecutionsResponse, ListBatchOperationsRequest, @@ -69,6 +93,8 @@ ListDeploymentsResponse, ListNamespacesRequest, ListNamespacesResponse, + ListNexusOperationExecutionsRequest, + ListNexusOperationExecutionsResponse, ListOpenWorkflowExecutionsRequest, ListOpenWorkflowExecutionsResponse, ListScheduleMatchingTimesRequest, @@ -87,12 +113,22 @@ ListWorkflowRulesResponse, PatchScheduleRequest, PatchScheduleResponse, + PauseActivityExecutionRequest, + PauseActivityExecutionResponse, PauseActivityRequest, PauseActivityResponse, + PauseWorkflowExecutionRequest, + PauseWorkflowExecutionResponse, + PollActivityExecutionRequest, + PollActivityExecutionResponse, PollActivityTaskQueueRequest, PollActivityTaskQueueResponse, + PollNexusOperationExecutionRequest, + PollNexusOperationExecutionResponse, PollNexusTaskQueueRequest, PollNexusTaskQueueResponse, + PollWorkflowExecutionTimeSkippingRequest, + PollWorkflowExecutionTimeSkippingResponse, PollWorkflowExecutionUpdateRequest, PollWorkflowExecutionUpdateResponse, PollWorkflowTaskQueueRequest, @@ -107,8 +143,14 @@ RecordWorkerHeartbeatResponse, RegisterNamespaceRequest, RegisterNamespaceResponse, + RequestCancelActivityExecutionRequest, + RequestCancelActivityExecutionResponse, + RequestCancelNexusOperationExecutionRequest, + RequestCancelNexusOperationExecutionResponse, RequestCancelWorkflowExecutionRequest, RequestCancelWorkflowExecutionResponse, + ResetActivityExecutionRequest, + ResetActivityExecutionResponse, ResetActivityRequest, ResetActivityResponse, ResetStickyTaskQueueRequest, @@ -143,6 +185,8 @@ SetCurrentDeploymentResponse, SetWorkerDeploymentCurrentVersionRequest, SetWorkerDeploymentCurrentVersionResponse, + SetWorkerDeploymentManagerRequest, + SetWorkerDeploymentManagerResponse, SetWorkerDeploymentRampingVersionRequest, SetWorkerDeploymentRampingVersionResponse, ShutdownWorkerRequest, @@ -151,18 +195,32 @@ SignalWithStartWorkflowExecutionResponse, SignalWorkflowExecutionRequest, SignalWorkflowExecutionResponse, + StartActivityExecutionRequest, + StartActivityExecutionResponse, StartBatchOperationRequest, StartBatchOperationResponse, + StartNexusOperationExecutionRequest, + StartNexusOperationExecutionResponse, StartWorkflowExecutionRequest, StartWorkflowExecutionResponse, StopBatchOperationRequest, StopBatchOperationResponse, + TerminateActivityExecutionRequest, + TerminateActivityExecutionResponse, + TerminateNexusOperationExecutionRequest, + TerminateNexusOperationExecutionResponse, TerminateWorkflowExecutionRequest, TerminateWorkflowExecutionResponse, TriggerWorkflowRuleRequest, TriggerWorkflowRuleResponse, + UnpauseActivityExecutionRequest, + UnpauseActivityExecutionResponse, UnpauseActivityRequest, UnpauseActivityResponse, + UnpauseWorkflowExecutionRequest, + UnpauseWorkflowExecutionResponse, + UpdateActivityExecutionOptionsRequest, + UpdateActivityExecutionOptionsResponse, UpdateActivityOptionsRequest, UpdateActivityOptionsResponse, UpdateNamespaceRequest, @@ -175,6 +233,8 @@ UpdateWorkerBuildIdCompatibilityResponse, UpdateWorkerConfigRequest, UpdateWorkerConfigResponse, + UpdateWorkerDeploymentVersionComputeConfigRequest, + UpdateWorkerDeploymentVersionComputeConfigResponse, UpdateWorkerDeploymentVersionMetadataRequest, UpdateWorkerDeploymentVersionMetadataResponse, UpdateWorkerVersioningRulesRequest, @@ -183,15 +243,33 @@ UpdateWorkflowExecutionOptionsResponse, UpdateWorkflowExecutionRequest, UpdateWorkflowExecutionResponse, + ValidateWorkerDeploymentVersionComputeConfigRequest, + ValidateWorkerDeploymentVersionComputeConfigResponse, ) __all__ = [ + "CountActivityExecutionsRequest", + "CountActivityExecutionsResponse", + "CountNexusOperationExecutionsRequest", + "CountNexusOperationExecutionsResponse", + "CountSchedulesRequest", + "CountSchedulesResponse", + "CountWorkersRequest", + "CountWorkersResponse", "CountWorkflowExecutionsRequest", "CountWorkflowExecutionsResponse", "CreateScheduleRequest", "CreateScheduleResponse", + "CreateWorkerDeploymentRequest", + "CreateWorkerDeploymentResponse", + "CreateWorkerDeploymentVersionRequest", + "CreateWorkerDeploymentVersionResponse", "CreateWorkflowRuleRequest", "CreateWorkflowRuleResponse", + "DeleteActivityExecutionRequest", + "DeleteActivityExecutionResponse", + "DeleteNexusOperationExecutionRequest", + "DeleteNexusOperationExecutionResponse", "DeleteScheduleRequest", "DeleteScheduleResponse", "DeleteWorkerDeploymentRequest", @@ -204,12 +282,16 @@ "DeleteWorkflowRuleResponse", "DeprecateNamespaceRequest", "DeprecateNamespaceResponse", + "DescribeActivityExecutionRequest", + "DescribeActivityExecutionResponse", "DescribeBatchOperationRequest", "DescribeBatchOperationResponse", "DescribeDeploymentRequest", "DescribeDeploymentResponse", "DescribeNamespaceRequest", "DescribeNamespaceResponse", + "DescribeNexusOperationExecutionRequest", + "DescribeNexusOperationExecutionResponse", "DescribeScheduleRequest", "DescribeScheduleResponse", "DescribeTaskQueueRequest", @@ -218,6 +300,8 @@ "DescribeWorkerDeploymentResponse", "DescribeWorkerDeploymentVersionRequest", "DescribeWorkerDeploymentVersionResponse", + "DescribeWorkerRequest", + "DescribeWorkerResponse", "DescribeWorkflowExecutionRequest", "DescribeWorkflowExecutionResponse", "DescribeWorkflowRuleRequest", @@ -246,6 +330,8 @@ "GetWorkflowExecutionHistoryResponse", "GetWorkflowExecutionHistoryReverseRequest", "GetWorkflowExecutionHistoryReverseResponse", + "ListActivityExecutionsRequest", + "ListActivityExecutionsResponse", "ListArchivedWorkflowExecutionsRequest", "ListArchivedWorkflowExecutionsResponse", "ListBatchOperationsRequest", @@ -256,6 +342,8 @@ "ListDeploymentsResponse", "ListNamespacesRequest", "ListNamespacesResponse", + "ListNexusOperationExecutionsRequest", + "ListNexusOperationExecutionsResponse", "ListOpenWorkflowExecutionsRequest", "ListOpenWorkflowExecutionsResponse", "ListScheduleMatchingTimesRequest", @@ -274,12 +362,22 @@ "ListWorkflowRulesResponse", "PatchScheduleRequest", "PatchScheduleResponse", + "PauseActivityExecutionRequest", + "PauseActivityExecutionResponse", "PauseActivityRequest", "PauseActivityResponse", + "PauseWorkflowExecutionRequest", + "PauseWorkflowExecutionResponse", + "PollActivityExecutionRequest", + "PollActivityExecutionResponse", "PollActivityTaskQueueRequest", "PollActivityTaskQueueResponse", + "PollNexusOperationExecutionRequest", + "PollNexusOperationExecutionResponse", "PollNexusTaskQueueRequest", "PollNexusTaskQueueResponse", + "PollWorkflowExecutionTimeSkippingRequest", + "PollWorkflowExecutionTimeSkippingResponse", "PollWorkflowExecutionUpdateRequest", "PollWorkflowExecutionUpdateResponse", "PollWorkflowTaskQueueRequest", @@ -294,8 +392,14 @@ "RecordWorkerHeartbeatResponse", "RegisterNamespaceRequest", "RegisterNamespaceResponse", + "RequestCancelActivityExecutionRequest", + "RequestCancelActivityExecutionResponse", + "RequestCancelNexusOperationExecutionRequest", + "RequestCancelNexusOperationExecutionResponse", "RequestCancelWorkflowExecutionRequest", "RequestCancelWorkflowExecutionResponse", + "ResetActivityExecutionRequest", + "ResetActivityExecutionResponse", "ResetActivityRequest", "ResetActivityResponse", "ResetStickyTaskQueueRequest", @@ -330,6 +434,8 @@ "SetCurrentDeploymentResponse", "SetWorkerDeploymentCurrentVersionRequest", "SetWorkerDeploymentCurrentVersionResponse", + "SetWorkerDeploymentManagerRequest", + "SetWorkerDeploymentManagerResponse", "SetWorkerDeploymentRampingVersionRequest", "SetWorkerDeploymentRampingVersionResponse", "ShutdownWorkerRequest", @@ -338,18 +444,32 @@ "SignalWithStartWorkflowExecutionResponse", "SignalWorkflowExecutionRequest", "SignalWorkflowExecutionResponse", + "StartActivityExecutionRequest", + "StartActivityExecutionResponse", "StartBatchOperationRequest", "StartBatchOperationResponse", + "StartNexusOperationExecutionRequest", + "StartNexusOperationExecutionResponse", "StartWorkflowExecutionRequest", "StartWorkflowExecutionResponse", "StopBatchOperationRequest", "StopBatchOperationResponse", + "TerminateActivityExecutionRequest", + "TerminateActivityExecutionResponse", + "TerminateNexusOperationExecutionRequest", + "TerminateNexusOperationExecutionResponse", "TerminateWorkflowExecutionRequest", "TerminateWorkflowExecutionResponse", "TriggerWorkflowRuleRequest", "TriggerWorkflowRuleResponse", + "UnpauseActivityExecutionRequest", + "UnpauseActivityExecutionResponse", "UnpauseActivityRequest", "UnpauseActivityResponse", + "UnpauseWorkflowExecutionRequest", + "UnpauseWorkflowExecutionResponse", + "UpdateActivityExecutionOptionsRequest", + "UpdateActivityExecutionOptionsResponse", "UpdateActivityOptionsRequest", "UpdateActivityOptionsResponse", "UpdateNamespaceRequest", @@ -362,6 +482,8 @@ "UpdateWorkerBuildIdCompatibilityResponse", "UpdateWorkerConfigRequest", "UpdateWorkerConfigResponse", + "UpdateWorkerDeploymentVersionComputeConfigRequest", + "UpdateWorkerDeploymentVersionComputeConfigResponse", "UpdateWorkerDeploymentVersionMetadataRequest", "UpdateWorkerDeploymentVersionMetadataResponse", "UpdateWorkerVersioningRulesRequest", @@ -370,6 +492,8 @@ "UpdateWorkflowExecutionOptionsResponse", "UpdateWorkflowExecutionRequest", "UpdateWorkflowExecutionResponse", + "ValidateWorkerDeploymentVersionComputeConfigRequest", + "ValidateWorkerDeploymentVersionComputeConfigResponse", ] # gRPC is optional diff --git a/temporalio/api/workflowservice/v1/request_response_pb2.py b/temporalio/api/workflowservice/v1/request_response_pb2.py index 5fd33bb8f..e1b2e01c4 100644 --- a/temporalio/api/workflowservice/v1/request_response_pb2.py +++ b/temporalio/api/workflowservice/v1/request_response_pb2.py @@ -30,9 +30,15 @@ from temporalio.api.common.v1 import ( message_pb2 as temporal_dot_api_dot_common_dot_v1_dot_message__pb2, ) +from temporalio.api.compute.v1 import ( + config_pb2 as temporal_dot_api_dot_compute_dot_v1_dot_config__pb2, +) from temporalio.api.deployment.v1 import ( message_pb2 as temporal_dot_api_dot_deployment_dot_v1_dot_message__pb2, ) +from temporalio.api.enums.v1 import ( + activity_pb2 as temporal_dot_api_dot_enums_dot_v1_dot_activity__pb2, +) from temporalio.api.enums.v1 import ( batch_operation_pb2 as temporal_dot_api_dot_enums_dot_v1_dot_batch__operation__pb2, ) @@ -48,6 +54,9 @@ from temporalio.api.enums.v1 import ( namespace_pb2 as temporal_dot_api_dot_enums_dot_v1_dot_namespace__pb2, ) +from temporalio.api.enums.v1 import ( + nexus_pb2 as temporal_dot_api_dot_enums_dot_v1_dot_nexus__pb2, +) from temporalio.api.enums.v1 import ( query_pb2 as temporal_dot_api_dot_enums_dot_v1_dot_query__pb2, ) @@ -57,6 +66,9 @@ from temporalio.api.enums.v1 import ( task_queue_pb2 as temporal_dot_api_dot_enums_dot_v1_dot_task__queue__pb2, ) +from temporalio.api.enums.v1 import ( + time_skipping_pb2 as temporal_dot_api_dot_enums_dot_v1_dot_time__skipping__pb2, +) from temporalio.api.enums.v1 import ( update_pb2 as temporal_dot_api_dot_enums_dot_v1_dot_update__pb2, ) @@ -119,7 +131,7 @@ ) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n6temporal/api/workflowservice/v1/request_response.proto\x12\x1ftemporal.api.workflowservice.v1\x1a+temporal/api/enums/v1/batch_operation.proto\x1a"temporal/api/enums/v1/common.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a%temporal/api/enums/v1/namespace.proto\x1a(temporal/api/enums/v1/failed_cause.proto\x1a!temporal/api/enums/v1/query.proto\x1a!temporal/api/enums/v1/reset.proto\x1a&temporal/api/enums/v1/task_queue.proto\x1a&temporal/api/enums/v1/deployment.proto\x1a"temporal/api/enums/v1/update.proto\x1a&temporal/api/activity/v1/message.proto\x1a$temporal/api/common/v1/message.proto\x1a%temporal/api/history/v1/message.proto\x1a&temporal/api/workflow/v1/message.proto\x1a%temporal/api/command/v1/message.proto\x1a(temporal/api/deployment/v1/message.proto\x1a%temporal/api/failure/v1/message.proto\x1a$temporal/api/filter/v1/message.proto\x1a&temporal/api/protocol/v1/message.proto\x1a\'temporal/api/namespace/v1/message.proto\x1a#temporal/api/query/v1/message.proto\x1a)temporal/api/replication/v1/message.proto\x1a#temporal/api/rules/v1/message.proto\x1a\'temporal/api/sdk/v1/worker_config.proto\x1a&temporal/api/schedule/v1/message.proto\x1a\'temporal/api/taskqueue/v1/message.proto\x1a$temporal/api/update/v1/message.proto\x1a%temporal/api/version/v1/message.proto\x1a#temporal/api/batch/v1/message.proto\x1a\x30temporal/api/sdk/v1/task_complete_metadata.proto\x1a\'temporal/api/sdk/v1/user_metadata.proto\x1a#temporal/api/nexus/v1/message.proto\x1a$temporal/api/worker/v1/message.proto\x1a\x1egoogle/protobuf/duration.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto"\x88\x05\n\x18RegisterNamespaceRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x13\n\x0bowner_email\x18\x03 \x01(\t\x12\x46\n#workflow_execution_retention_period\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\x12G\n\x08\x63lusters\x18\x05 \x03(\x0b\x32\x35.temporal.api.replication.v1.ClusterReplicationConfig\x12\x1b\n\x13\x61\x63tive_cluster_name\x18\x06 \x01(\t\x12Q\n\x04\x64\x61ta\x18\x07 \x03(\x0b\x32\x43.temporal.api.workflowservice.v1.RegisterNamespaceRequest.DataEntry\x12\x16\n\x0esecurity_token\x18\x08 \x01(\t\x12\x1b\n\x13is_global_namespace\x18\t \x01(\x08\x12\x44\n\x16history_archival_state\x18\n \x01(\x0e\x32$.temporal.api.enums.v1.ArchivalState\x12\x1c\n\x14history_archival_uri\x18\x0b \x01(\t\x12G\n\x19visibility_archival_state\x18\x0c \x01(\x0e\x32$.temporal.api.enums.v1.ArchivalState\x12\x1f\n\x17visibility_archival_uri\x18\r \x01(\t\x1a+\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"\x1b\n\x19RegisterNamespaceResponse"\x89\x01\n\x15ListNamespacesRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\x0c\x12\x44\n\x10namespace_filter\x18\x03 \x01(\x0b\x32*.temporal.api.namespace.v1.NamespaceFilter"\x81\x01\n\x16ListNamespacesResponse\x12N\n\nnamespaces\x18\x01 \x03(\x0b\x32:.temporal.api.workflowservice.v1.DescribeNamespaceResponse\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\x0c"9\n\x18\x44\x65scribeNamespaceRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\n\n\x02id\x18\x02 \x01(\t"\xec\x02\n\x19\x44\x65scribeNamespaceResponse\x12@\n\x0enamespace_info\x18\x01 \x01(\x0b\x32(.temporal.api.namespace.v1.NamespaceInfo\x12:\n\x06\x63onfig\x18\x02 \x01(\x0b\x32*.temporal.api.namespace.v1.NamespaceConfig\x12S\n\x12replication_config\x18\x03 \x01(\x0b\x32\x37.temporal.api.replication.v1.NamespaceReplicationConfig\x12\x18\n\x10\x66\x61ilover_version\x18\x04 \x01(\x03\x12\x1b\n\x13is_global_namespace\x18\x05 \x01(\x08\x12\x45\n\x10\x66\x61ilover_history\x18\x06 \x03(\x0b\x32+.temporal.api.replication.v1.FailoverStatus"\xcf\x02\n\x16UpdateNamespaceRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x43\n\x0bupdate_info\x18\x02 \x01(\x0b\x32..temporal.api.namespace.v1.UpdateNamespaceInfo\x12:\n\x06\x63onfig\x18\x03 \x01(\x0b\x32*.temporal.api.namespace.v1.NamespaceConfig\x12S\n\x12replication_config\x18\x04 \x01(\x0b\x32\x37.temporal.api.replication.v1.NamespaceReplicationConfig\x12\x16\n\x0esecurity_token\x18\x05 \x01(\t\x12\x19\n\x11\x64\x65lete_bad_binary\x18\x06 \x01(\t\x12\x19\n\x11promote_namespace\x18\x07 \x01(\x08"\xa3\x02\n\x17UpdateNamespaceResponse\x12@\n\x0enamespace_info\x18\x01 \x01(\x0b\x32(.temporal.api.namespace.v1.NamespaceInfo\x12:\n\x06\x63onfig\x18\x02 \x01(\x0b\x32*.temporal.api.namespace.v1.NamespaceConfig\x12S\n\x12replication_config\x18\x03 \x01(\x0b\x32\x37.temporal.api.replication.v1.NamespaceReplicationConfig\x12\x18\n\x10\x66\x61ilover_version\x18\x04 \x01(\x03\x12\x1b\n\x13is_global_namespace\x18\x05 \x01(\x08"F\n\x19\x44\x65precateNamespaceRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x16\n\x0esecurity_token\x18\x02 \x01(\t"\x1c\n\x1a\x44\x65precateNamespaceResponse"\xa9\x0b\n\x1dStartWorkflowExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12;\n\rworkflow_type\x18\x03 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12\x38\n\ntask_queue\x18\x04 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12/\n\x05input\x18\x05 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12=\n\x1aworkflow_execution_timeout\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x37\n\x14workflow_run_timeout\x18\x07 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x10\n\x08identity\x18\t \x01(\t\x12\x12\n\nrequest_id\x18\n \x01(\t\x12N\n\x18workflow_id_reuse_policy\x18\x0b \x01(\x0e\x32,.temporal.api.enums.v1.WorkflowIdReusePolicy\x12T\n\x1bworkflow_id_conflict_policy\x18\x16 \x01(\x0e\x32/.temporal.api.enums.v1.WorkflowIdConflictPolicy\x12\x39\n\x0cretry_policy\x18\x0c \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x15\n\rcron_schedule\x18\r \x01(\t\x12*\n\x04memo\x18\x0e \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo\x12\x43\n\x11search_attributes\x18\x0f \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12.\n\x06header\x18\x10 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12\x1f\n\x17request_eager_execution\x18\x11 \x01(\x08\x12;\n\x11\x63ontinued_failure\x18\x12 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12@\n\x16last_completion_result\x18\x13 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x37\n\x14workflow_start_delay\x18\x14 \x01(\x0b\x32\x19.google.protobuf.Duration\x12>\n\x14\x63ompletion_callbacks\x18\x15 \x03(\x0b\x32 .temporal.api.common.v1.Callback\x12\x38\n\ruser_metadata\x18\x17 \x01(\x0b\x32!.temporal.api.sdk.v1.UserMetadata\x12+\n\x05links\x18\x18 \x03(\x0b\x32\x1c.temporal.api.common.v1.Link\x12I\n\x13versioning_override\x18\x19 \x01(\x0b\x32,.temporal.api.workflow.v1.VersioningOverride\x12H\n\x13on_conflict_options\x18\x1a \x01(\x0b\x32+.temporal.api.workflow.v1.OnConflictOptions\x12\x32\n\x08priority\x18\x1b \x01(\x0b\x32 .temporal.api.common.v1.Priority"\x8a\x02\n\x1eStartWorkflowExecutionResponse\x12\x0e\n\x06run_id\x18\x01 \x01(\t\x12\x0f\n\x07started\x18\x03 \x01(\x08\x12>\n\x06status\x18\x05 \x01(\x0e\x32..temporal.api.enums.v1.WorkflowExecutionStatus\x12[\n\x13\x65\x61ger_workflow_task\x18\x02 \x01(\x0b\x32>.temporal.api.workflowservice.v1.PollWorkflowTaskQueueResponse\x12*\n\x04link\x18\x04 \x01(\x0b\x32\x1c.temporal.api.common.v1.Link"\xaa\x02\n"GetWorkflowExecutionHistoryRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12<\n\texecution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x19\n\x11maximum_page_size\x18\x03 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x04 \x01(\x0c\x12\x16\n\x0ewait_new_event\x18\x05 \x01(\x08\x12P\n\x19history_event_filter_type\x18\x06 \x01(\x0e\x32-.temporal.api.enums.v1.HistoryEventFilterType\x12\x15\n\rskip_archival\x18\x07 \x01(\x08"\xba\x01\n#GetWorkflowExecutionHistoryResponse\x12\x31\n\x07history\x18\x01 \x01(\x0b\x32 .temporal.api.history.v1.History\x12\x35\n\x0braw_history\x18\x02 \x03(\x0b\x32 .temporal.api.common.v1.DataBlob\x12\x17\n\x0fnext_page_token\x18\x03 \x01(\x0c\x12\x10\n\x08\x61rchived\x18\x04 \x01(\x08"\xb0\x01\n)GetWorkflowExecutionHistoryReverseRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12<\n\texecution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x19\n\x11maximum_page_size\x18\x03 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x04 \x01(\x0c"x\n*GetWorkflowExecutionHistoryReverseResponse\x12\x31\n\x07history\x18\x01 \x01(\x0b\x32 .temporal.api.history.v1.History\x12\x17\n\x0fnext_page_token\x18\x03 \x01(\x0c"\x8a\x03\n\x1cPollWorkflowTaskQueueRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x38\n\ntask_queue\x18\x02 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12\x10\n\x08identity\x18\x03 \x01(\t\x12\x1b\n\x0f\x62inary_checksum\x18\x04 \x01(\tB\x02\x18\x01\x12Z\n\x1bworker_version_capabilities\x18\x05 \x01(\x0b\x32\x31.temporal.api.common.v1.WorkerVersionCapabilitiesB\x02\x18\x01\x12O\n\x12\x64\x65ployment_options\x18\x06 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentOptions\x12\x41\n\x10worker_heartbeat\x18\x07 \x01(\x0b\x32\'.temporal.api.worker.v1.WorkerHeartbeat"\x91\x07\n\x1dPollWorkflowTaskQueueResponse\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12\x45\n\x12workflow_execution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12;\n\rworkflow_type\x18\x03 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12!\n\x19previous_started_event_id\x18\x04 \x01(\x03\x12\x18\n\x10started_event_id\x18\x05 \x01(\x03\x12\x0f\n\x07\x61ttempt\x18\x06 \x01(\x05\x12\x1a\n\x12\x62\x61\x63klog_count_hint\x18\x07 \x01(\x03\x12\x31\n\x07history\x18\x08 \x01(\x0b\x32 .temporal.api.history.v1.History\x12\x17\n\x0fnext_page_token\x18\t \x01(\x0c\x12\x33\n\x05query\x18\n \x01(\x0b\x32$.temporal.api.query.v1.WorkflowQuery\x12K\n\x1dworkflow_execution_task_queue\x18\x0b \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12\x32\n\x0escheduled_time\x18\x0c \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x30\n\x0cstarted_time\x18\r \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\\\n\x07queries\x18\x0e \x03(\x0b\x32K.temporal.api.workflowservice.v1.PollWorkflowTaskQueueResponse.QueriesEntry\x12\x33\n\x08messages\x18\x0f \x03(\x0b\x32!.temporal.api.protocol.v1.Message\x12Q\n\x17poller_scaling_decision\x18\x10 \x01(\x0b\x32\x30.temporal.api.taskqueue.v1.PollerScalingDecision\x1aT\n\x0cQueriesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x33\n\x05value\x18\x02 \x01(\x0b\x32$.temporal.api.query.v1.WorkflowQuery:\x02\x38\x01"\xb5\t\n#RespondWorkflowTaskCompletedRequest\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12\x32\n\x08\x63ommands\x18\x02 \x03(\x0b\x32 .temporal.api.command.v1.Command\x12\x10\n\x08identity\x18\x03 \x01(\t\x12O\n\x11sticky_attributes\x18\x04 \x01(\x0b\x32\x34.temporal.api.taskqueue.v1.StickyExecutionAttributes\x12 \n\x18return_new_workflow_task\x18\x05 \x01(\x08\x12&\n\x1e\x66orce_create_new_workflow_task\x18\x06 \x01(\x08\x12\x1b\n\x0f\x62inary_checksum\x18\x07 \x01(\tB\x02\x18\x01\x12m\n\rquery_results\x18\x08 \x03(\x0b\x32V.temporal.api.workflowservice.v1.RespondWorkflowTaskCompletedRequest.QueryResultsEntry\x12\x11\n\tnamespace\x18\t \x01(\t\x12L\n\x14worker_version_stamp\x18\n \x01(\x0b\x32*.temporal.api.common.v1.WorkerVersionStampB\x02\x18\x01\x12\x33\n\x08messages\x18\x0b \x03(\x0b\x32!.temporal.api.protocol.v1.Message\x12H\n\x0csdk_metadata\x18\x0c \x01(\x0b\x32\x32.temporal.api.sdk.v1.WorkflowTaskCompletedMetadata\x12\x43\n\x11metering_metadata\x18\r \x01(\x0b\x32(.temporal.api.common.v1.MeteringMetadata\x12g\n\x0c\x63\x61pabilities\x18\x0e \x01(\x0b\x32Q.temporal.api.workflowservice.v1.RespondWorkflowTaskCompletedRequest.Capabilities\x12>\n\ndeployment\x18\x0f \x01(\x0b\x32&.temporal.api.deployment.v1.DeploymentB\x02\x18\x01\x12\x46\n\x13versioning_behavior\x18\x10 \x01(\x0e\x32).temporal.api.enums.v1.VersioningBehavior\x12O\n\x12\x64\x65ployment_options\x18\x11 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentOptions\x1a_\n\x11QueryResultsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x39\n\x05value\x18\x02 \x01(\x0b\x32*.temporal.api.query.v1.WorkflowQueryResult:\x02\x38\x01\x1a\x45\n\x0c\x43\x61pabilities\x12\x35\n-discard_speculative_workflow_task_with_events\x18\x01 \x01(\x08"\xf5\x01\n$RespondWorkflowTaskCompletedResponse\x12U\n\rworkflow_task\x18\x01 \x01(\x0b\x32>.temporal.api.workflowservice.v1.PollWorkflowTaskQueueResponse\x12V\n\x0e\x61\x63tivity_tasks\x18\x02 \x03(\x0b\x32>.temporal.api.workflowservice.v1.PollActivityTaskQueueResponse\x12\x1e\n\x16reset_history_event_id\x18\x03 \x01(\x03"\xf8\x03\n RespondWorkflowTaskFailedRequest\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12=\n\x05\x63\x61use\x18\x02 \x01(\x0e\x32..temporal.api.enums.v1.WorkflowTaskFailedCause\x12\x31\n\x07\x66\x61ilure\x18\x03 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x10\n\x08identity\x18\x04 \x01(\t\x12\x1b\n\x0f\x62inary_checksum\x18\x05 \x01(\tB\x02\x18\x01\x12\x11\n\tnamespace\x18\x06 \x01(\t\x12\x33\n\x08messages\x18\x07 \x03(\x0b\x32!.temporal.api.protocol.v1.Message\x12\x46\n\x0eworker_version\x18\x08 \x01(\x0b\x32*.temporal.api.common.v1.WorkerVersionStampB\x02\x18\x01\x12>\n\ndeployment\x18\t \x01(\x0b\x32&.temporal.api.deployment.v1.DeploymentB\x02\x18\x01\x12O\n\x12\x64\x65ployment_options\x18\n \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentOptions"#\n!RespondWorkflowTaskFailedResponse"\xb8\x03\n\x1cPollActivityTaskQueueRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x38\n\ntask_queue\x18\x02 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12\x10\n\x08identity\x18\x03 \x01(\t\x12I\n\x13task_queue_metadata\x18\x04 \x01(\x0b\x32,.temporal.api.taskqueue.v1.TaskQueueMetadata\x12Z\n\x1bworker_version_capabilities\x18\x05 \x01(\x0b\x32\x31.temporal.api.common.v1.WorkerVersionCapabilitiesB\x02\x18\x01\x12O\n\x12\x64\x65ployment_options\x18\x06 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentOptions\x12\x41\n\x10worker_heartbeat\x18\x07 \x01(\x0b\x32\'.temporal.api.worker.v1.WorkerHeartbeat"\xef\x07\n\x1dPollActivityTaskQueueResponse\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12\x1a\n\x12workflow_namespace\x18\x02 \x01(\t\x12;\n\rworkflow_type\x18\x03 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12\x45\n\x12workflow_execution\x18\x04 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12;\n\ractivity_type\x18\x05 \x01(\x0b\x32$.temporal.api.common.v1.ActivityType\x12\x13\n\x0b\x61\x63tivity_id\x18\x06 \x01(\t\x12.\n\x06header\x18\x07 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12/\n\x05input\x18\x08 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12;\n\x11heartbeat_details\x18\t \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x32\n\x0escheduled_time\x18\n \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x42\n\x1e\x63urrent_attempt_scheduled_time\x18\x0b \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x30\n\x0cstarted_time\x18\x0c \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0f\n\x07\x61ttempt\x18\r \x01(\x05\x12<\n\x19schedule_to_close_timeout\x18\x0e \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\x0f \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x34\n\x11heartbeat_timeout\x18\x10 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x0cretry_policy\x18\x11 \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12Q\n\x17poller_scaling_decision\x18\x12 \x01(\x0b\x32\x30.temporal.api.taskqueue.v1.PollerScalingDecision\x12\x32\n\x08priority\x18\x13 \x01(\x0b\x32 .temporal.api.common.v1.Priority"\x90\x01\n"RecordActivityTaskHeartbeatRequest\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12\x31\n\x07\x64\x65tails\x18\x02 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x10\n\x08identity\x18\x03 \x01(\t\x12\x11\n\tnamespace\x18\x04 \x01(\t"p\n#RecordActivityTaskHeartbeatResponse\x12\x18\n\x10\x63\x61ncel_requested\x18\x01 \x01(\x08\x12\x17\n\x0f\x61\x63tivity_paused\x18\x02 \x01(\x08\x12\x16\n\x0e\x61\x63tivity_reset\x18\x03 \x01(\x08"\xba\x01\n&RecordActivityTaskHeartbeatByIdRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\t\x12\x13\n\x0b\x61\x63tivity_id\x18\x04 \x01(\t\x12\x31\n\x07\x64\x65tails\x18\x05 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x10\n\x08identity\x18\x06 \x01(\t"t\n\'RecordActivityTaskHeartbeatByIdResponse\x12\x18\n\x10\x63\x61ncel_requested\x18\x01 \x01(\x08\x12\x17\n\x0f\x61\x63tivity_paused\x18\x02 \x01(\x08\x12\x16\n\x0e\x61\x63tivity_reset\x18\x03 \x01(\x08"\xe9\x02\n#RespondActivityTaskCompletedRequest\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12\x30\n\x06result\x18\x02 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x10\n\x08identity\x18\x03 \x01(\t\x12\x11\n\tnamespace\x18\x04 \x01(\t\x12\x46\n\x0eworker_version\x18\x05 \x01(\x0b\x32*.temporal.api.common.v1.WorkerVersionStampB\x02\x18\x01\x12>\n\ndeployment\x18\x06 \x01(\x0b\x32&.temporal.api.deployment.v1.DeploymentB\x02\x18\x01\x12O\n\x12\x64\x65ployment_options\x18\x07 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentOptions"&\n$RespondActivityTaskCompletedResponse"\xba\x01\n\'RespondActivityTaskCompletedByIdRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\t\x12\x13\n\x0b\x61\x63tivity_id\x18\x04 \x01(\t\x12\x30\n\x06result\x18\x05 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x10\n\x08identity\x18\x06 \x01(\t"*\n(RespondActivityTaskCompletedByIdResponse"\xa9\x03\n RespondActivityTaskFailedRequest\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12\x31\n\x07\x66\x61ilure\x18\x02 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x10\n\x08identity\x18\x03 \x01(\t\x12\x11\n\tnamespace\x18\x04 \x01(\t\x12@\n\x16last_heartbeat_details\x18\x05 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x46\n\x0eworker_version\x18\x06 \x01(\x0b\x32*.temporal.api.common.v1.WorkerVersionStampB\x02\x18\x01\x12>\n\ndeployment\x18\x07 \x01(\x0b\x32&.temporal.api.deployment.v1.DeploymentB\x02\x18\x01\x12O\n\x12\x64\x65ployment_options\x18\x08 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentOptions"W\n!RespondActivityTaskFailedResponse\x12\x32\n\x08\x66\x61ilures\x18\x01 \x03(\x0b\x32 .temporal.api.failure.v1.Failure"\xfa\x01\n$RespondActivityTaskFailedByIdRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\t\x12\x13\n\x0b\x61\x63tivity_id\x18\x04 \x01(\t\x12\x31\n\x07\x66\x61ilure\x18\x05 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x10\n\x08identity\x18\x06 \x01(\t\x12@\n\x16last_heartbeat_details\x18\x07 \x01(\x0b\x32 .temporal.api.common.v1.Payloads"[\n%RespondActivityTaskFailedByIdResponse\x12\x32\n\x08\x66\x61ilures\x18\x01 \x03(\x0b\x32 .temporal.api.failure.v1.Failure"\xe9\x02\n"RespondActivityTaskCanceledRequest\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12\x31\n\x07\x64\x65tails\x18\x02 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x10\n\x08identity\x18\x03 \x01(\t\x12\x11\n\tnamespace\x18\x04 \x01(\t\x12\x46\n\x0eworker_version\x18\x05 \x01(\x0b\x32*.temporal.api.common.v1.WorkerVersionStampB\x02\x18\x01\x12>\n\ndeployment\x18\x06 \x01(\x0b\x32&.temporal.api.deployment.v1.DeploymentB\x02\x18\x01\x12O\n\x12\x64\x65ployment_options\x18\x07 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentOptions"%\n#RespondActivityTaskCanceledResponse"\x8b\x02\n&RespondActivityTaskCanceledByIdRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\t\x12\x13\n\x0b\x61\x63tivity_id\x18\x04 \x01(\t\x12\x31\n\x07\x64\x65tails\x18\x05 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x10\n\x08identity\x18\x06 \x01(\t\x12O\n\x12\x64\x65ployment_options\x18\x07 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentOptions")\n\'RespondActivityTaskCanceledByIdResponse"\x84\x02\n%RequestCancelWorkflowExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x45\n\x12workflow_execution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x10\n\x08identity\x18\x03 \x01(\t\x12\x12\n\nrequest_id\x18\x04 \x01(\t\x12\x1e\n\x16\x66irst_execution_run_id\x18\x05 \x01(\t\x12\x0e\n\x06reason\x18\x06 \x01(\t\x12+\n\x05links\x18\x07 \x03(\x0b\x32\x1c.temporal.api.common.v1.Link"(\n&RequestCancelWorkflowExecutionResponse"\xde\x02\n\x1eSignalWorkflowExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x45\n\x12workflow_execution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x13\n\x0bsignal_name\x18\x03 \x01(\t\x12/\n\x05input\x18\x04 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x10\n\x08identity\x18\x05 \x01(\t\x12\x12\n\nrequest_id\x18\x06 \x01(\t\x12\x13\n\x07\x63ontrol\x18\x07 \x01(\tB\x02\x18\x01\x12.\n\x06header\x18\x08 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12+\n\x05links\x18\n \x03(\x0b\x32\x1c.temporal.api.common.v1.LinkJ\x04\x08\t\x10\n"!\n\x1fSignalWorkflowExecutionResponse"\xf1\t\n\'SignalWithStartWorkflowExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12;\n\rworkflow_type\x18\x03 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12\x38\n\ntask_queue\x18\x04 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12/\n\x05input\x18\x05 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12=\n\x1aworkflow_execution_timeout\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x37\n\x14workflow_run_timeout\x18\x07 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x10\n\x08identity\x18\t \x01(\t\x12\x12\n\nrequest_id\x18\n \x01(\t\x12N\n\x18workflow_id_reuse_policy\x18\x0b \x01(\x0e\x32,.temporal.api.enums.v1.WorkflowIdReusePolicy\x12T\n\x1bworkflow_id_conflict_policy\x18\x16 \x01(\x0e\x32/.temporal.api.enums.v1.WorkflowIdConflictPolicy\x12\x13\n\x0bsignal_name\x18\x0c \x01(\t\x12\x36\n\x0csignal_input\x18\r \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x13\n\x07\x63ontrol\x18\x0e \x01(\tB\x02\x18\x01\x12\x39\n\x0cretry_policy\x18\x0f \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x15\n\rcron_schedule\x18\x10 \x01(\t\x12*\n\x04memo\x18\x11 \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo\x12\x43\n\x11search_attributes\x18\x12 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12.\n\x06header\x18\x13 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12\x37\n\x14workflow_start_delay\x18\x14 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\ruser_metadata\x18\x17 \x01(\x0b\x32!.temporal.api.sdk.v1.UserMetadata\x12+\n\x05links\x18\x18 \x03(\x0b\x32\x1c.temporal.api.common.v1.Link\x12I\n\x13versioning_override\x18\x19 \x01(\x0b\x32,.temporal.api.workflow.v1.VersioningOverride\x12\x32\n\x08priority\x18\x1a \x01(\x0b\x32 .temporal.api.common.v1.PriorityJ\x04\x08\x15\x10\x16"K\n(SignalWithStartWorkflowExecutionResponse\x12\x0e\n\x06run_id\x18\x01 \x01(\t\x12\x0f\n\x07started\x18\x02 \x01(\x08"\xc1\x03\n\x1dResetWorkflowExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x45\n\x12workflow_execution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x0e\n\x06reason\x18\x03 \x01(\t\x12%\n\x1dworkflow_task_finish_event_id\x18\x04 \x01(\x03\x12\x12\n\nrequest_id\x18\x05 \x01(\t\x12G\n\x12reset_reapply_type\x18\x06 \x01(\x0e\x32\'.temporal.api.enums.v1.ResetReapplyTypeB\x02\x18\x01\x12S\n\x1breset_reapply_exclude_types\x18\x07 \x03(\x0e\x32..temporal.api.enums.v1.ResetReapplyExcludeType\x12K\n\x15post_reset_operations\x18\x08 \x03(\x0b\x32,.temporal.api.workflow.v1.PostResetOperation\x12\x10\n\x08identity\x18\t \x01(\t"0\n\x1eResetWorkflowExecutionResponse\x12\x0e\n\x06run_id\x18\x01 \x01(\t"\x9f\x02\n!TerminateWorkflowExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x45\n\x12workflow_execution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x0e\n\x06reason\x18\x03 \x01(\t\x12\x31\n\x07\x64\x65tails\x18\x04 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x10\n\x08identity\x18\x05 \x01(\t\x12\x1e\n\x16\x66irst_execution_run_id\x18\x06 \x01(\t\x12+\n\x05links\x18\x07 \x03(\x0b\x32\x1c.temporal.api.common.v1.Link"$\n"TerminateWorkflowExecutionResponse"z\n\x1e\x44\x65leteWorkflowExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x45\n\x12workflow_execution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution"!\n\x1f\x44\x65leteWorkflowExecutionResponse"\xc9\x02\n!ListOpenWorkflowExecutionsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x19\n\x11maximum_page_size\x18\x02 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x03 \x01(\x0c\x12\x42\n\x11start_time_filter\x18\x04 \x01(\x0b\x32\'.temporal.api.filter.v1.StartTimeFilter\x12K\n\x10\x65xecution_filter\x18\x05 \x01(\x0b\x32/.temporal.api.filter.v1.WorkflowExecutionFilterH\x00\x12\x41\n\x0btype_filter\x18\x06 \x01(\x0b\x32*.temporal.api.filter.v1.WorkflowTypeFilterH\x00\x42\t\n\x07\x66ilters"\x82\x01\n"ListOpenWorkflowExecutionsResponse\x12\x43\n\nexecutions\x18\x01 \x03(\x0b\x32/.temporal.api.workflow.v1.WorkflowExecutionInfo\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\x0c"\x8a\x03\n#ListClosedWorkflowExecutionsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x19\n\x11maximum_page_size\x18\x02 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x03 \x01(\x0c\x12\x42\n\x11start_time_filter\x18\x04 \x01(\x0b\x32\'.temporal.api.filter.v1.StartTimeFilter\x12K\n\x10\x65xecution_filter\x18\x05 \x01(\x0b\x32/.temporal.api.filter.v1.WorkflowExecutionFilterH\x00\x12\x41\n\x0btype_filter\x18\x06 \x01(\x0b\x32*.temporal.api.filter.v1.WorkflowTypeFilterH\x00\x12=\n\rstatus_filter\x18\x07 \x01(\x0b\x32$.temporal.api.filter.v1.StatusFilterH\x00\x42\t\n\x07\x66ilters"\x84\x01\n$ListClosedWorkflowExecutionsResponse\x12\x43\n\nexecutions\x18\x01 \x03(\x0b\x32/.temporal.api.workflow.v1.WorkflowExecutionInfo\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\x0c"m\n\x1dListWorkflowExecutionsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x03 \x01(\x0c\x12\r\n\x05query\x18\x04 \x01(\t"~\n\x1eListWorkflowExecutionsResponse\x12\x43\n\nexecutions\x18\x01 \x03(\x0b\x32/.temporal.api.workflow.v1.WorkflowExecutionInfo\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\x0c"u\n%ListArchivedWorkflowExecutionsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x03 \x01(\x0c\x12\r\n\x05query\x18\x04 \x01(\t"\x86\x01\n&ListArchivedWorkflowExecutionsResponse\x12\x43\n\nexecutions\x18\x01 \x03(\x0b\x32/.temporal.api.workflow.v1.WorkflowExecutionInfo\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\x0c"m\n\x1dScanWorkflowExecutionsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x03 \x01(\x0c\x12\r\n\x05query\x18\x04 \x01(\t"~\n\x1eScanWorkflowExecutionsResponse\x12\x43\n\nexecutions\x18\x01 \x03(\x0b\x32/.temporal.api.workflow.v1.WorkflowExecutionInfo\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\x0c"B\n\x1e\x43ountWorkflowExecutionsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\r\n\x05query\x18\x02 \x01(\t"\xed\x01\n\x1f\x43ountWorkflowExecutionsResponse\x12\r\n\x05\x63ount\x18\x01 \x01(\x03\x12\x61\n\x06groups\x18\x02 \x03(\x0b\x32Q.temporal.api.workflowservice.v1.CountWorkflowExecutionsResponse.AggregationGroup\x1aX\n\x10\x41ggregationGroup\x12\x35\n\x0cgroup_values\x18\x01 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\r\n\x05\x63ount\x18\x02 \x01(\x03"\x1c\n\x1aGetSearchAttributesRequest"\xc9\x01\n\x1bGetSearchAttributesResponse\x12T\n\x04keys\x18\x01 \x03(\x0b\x32\x46.temporal.api.workflowservice.v1.GetSearchAttributesResponse.KeysEntry\x1aT\n\tKeysEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0e\x32\'.temporal.api.enums.v1.IndexedValueType:\x02\x38\x01"\xd0\x02\n RespondQueryTaskCompletedRequest\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12>\n\x0e\x63ompleted_type\x18\x02 \x01(\x0e\x32&.temporal.api.enums.v1.QueryResultType\x12\x36\n\x0cquery_result\x18\x03 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x15\n\rerror_message\x18\x04 \x01(\t\x12\x11\n\tnamespace\x18\x06 \x01(\t\x12\x31\n\x07\x66\x61ilure\x18\x07 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12=\n\x05\x63\x61use\x18\x08 \x01(\x0e\x32..temporal.api.enums.v1.WorkflowTaskFailedCauseJ\x04\x08\x05\x10\x06"#\n!RespondQueryTaskCompletedResponse"n\n\x1bResetStickyTaskQueueRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12<\n\texecution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution"\x1e\n\x1cResetStickyTaskQueueResponse"\xaa\x01\n\x15ShutdownWorkerRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x19\n\x11sticky_task_queue\x18\x02 \x01(\t\x12\x10\n\x08identity\x18\x03 \x01(\t\x12\x0e\n\x06reason\x18\x04 \x01(\t\x12\x41\n\x10worker_heartbeat\x18\x05 \x01(\x0b\x32\'.temporal.api.worker.v1.WorkerHeartbeat"\x18\n\x16ShutdownWorkerResponse"\xe9\x01\n\x14QueryWorkflowRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12<\n\texecution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x33\n\x05query\x18\x03 \x01(\x0b\x32$.temporal.api.query.v1.WorkflowQuery\x12K\n\x16query_reject_condition\x18\x04 \x01(\x0e\x32+.temporal.api.enums.v1.QueryRejectCondition"\x8d\x01\n\x15QueryWorkflowResponse\x12\x36\n\x0cquery_result\x18\x01 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12<\n\x0equery_rejected\x18\x02 \x01(\x0b\x32$.temporal.api.query.v1.QueryRejected"s\n DescribeWorkflowExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12<\n\texecution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution"\x99\x05\n!DescribeWorkflowExecutionResponse\x12K\n\x10\x65xecution_config\x18\x01 \x01(\x0b\x32\x31.temporal.api.workflow.v1.WorkflowExecutionConfig\x12P\n\x17workflow_execution_info\x18\x02 \x01(\x0b\x32/.temporal.api.workflow.v1.WorkflowExecutionInfo\x12I\n\x12pending_activities\x18\x03 \x03(\x0b\x32-.temporal.api.workflow.v1.PendingActivityInfo\x12M\n\x10pending_children\x18\x04 \x03(\x0b\x32\x33.temporal.api.workflow.v1.PendingChildExecutionInfo\x12P\n\x15pending_workflow_task\x18\x05 \x01(\x0b\x32\x31.temporal.api.workflow.v1.PendingWorkflowTaskInfo\x12\x39\n\tcallbacks\x18\x06 \x03(\x0b\x32&.temporal.api.workflow.v1.CallbackInfo\x12U\n\x18pending_nexus_operations\x18\x07 \x03(\x0b\x32\x33.temporal.api.workflow.v1.PendingNexusOperationInfo\x12W\n\x16workflow_extended_info\x18\x08 \x01(\x0b\x32\x37.temporal.api.workflow.v1.WorkflowExecutionExtendedInfo"\x90\x04\n\x18\x44\x65scribeTaskQueueRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x38\n\ntask_queue\x18\x02 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12=\n\x0ftask_queue_type\x18\x03 \x01(\x0e\x32$.temporal.api.enums.v1.TaskQueueType\x12\x14\n\x0creport_stats\x18\x08 \x01(\x08\x12\x15\n\rreport_config\x18\x0b \x01(\x08\x12%\n\x19include_task_queue_status\x18\x04 \x01(\x08\x42\x02\x18\x01\x12\x42\n\x08\x61pi_mode\x18\x05 \x01(\x0e\x32,.temporal.api.enums.v1.DescribeTaskQueueModeB\x02\x18\x01\x12J\n\x08versions\x18\x06 \x01(\x0b\x32\x34.temporal.api.taskqueue.v1.TaskQueueVersionSelectionB\x02\x18\x01\x12\x42\n\x10task_queue_types\x18\x07 \x03(\x0e\x32$.temporal.api.enums.v1.TaskQueueTypeB\x02\x18\x01\x12\x1a\n\x0ereport_pollers\x18\t \x01(\x08\x42\x02\x18\x01\x12$\n\x18report_task_reachability\x18\n \x01(\x08\x42\x02\x18\x01"\xec\x07\n\x19\x44\x65scribeTaskQueueResponse\x12\x36\n\x07pollers\x18\x01 \x03(\x0b\x32%.temporal.api.taskqueue.v1.PollerInfo\x12\x38\n\x05stats\x18\x05 \x01(\x0b\x32).temporal.api.taskqueue.v1.TaskQueueStats\x12q\n\x15stats_by_priority_key\x18\x08 \x03(\x0b\x32R.temporal.api.workflowservice.v1.DescribeTaskQueueResponse.StatsByPriorityKeyEntry\x12K\n\x0fversioning_info\x18\x04 \x01(\x0b\x32\x32.temporal.api.taskqueue.v1.TaskQueueVersioningInfo\x12:\n\x06\x63onfig\x18\x06 \x01(\x0b\x32*.temporal.api.taskqueue.v1.TaskQueueConfig\x12k\n\x14\x65\x66\x66\x65\x63tive_rate_limit\x18\x07 \x01(\x0b\x32M.temporal.api.workflowservice.v1.DescribeTaskQueueResponse.EffectiveRateLimit\x12I\n\x11task_queue_status\x18\x02 \x01(\x0b\x32*.temporal.api.taskqueue.v1.TaskQueueStatusB\x02\x18\x01\x12g\n\rversions_info\x18\x03 \x03(\x0b\x32L.temporal.api.workflowservice.v1.DescribeTaskQueueResponse.VersionsInfoEntryB\x02\x18\x01\x1a\x64\n\x17StatsByPriorityKeyEntry\x12\x0b\n\x03key\x18\x01 \x01(\x05\x12\x38\n\x05value\x18\x02 \x01(\x0b\x32).temporal.api.taskqueue.v1.TaskQueueStats:\x02\x38\x01\x1at\n\x12\x45\x66\x66\x65\x63tiveRateLimit\x12\x1b\n\x13requests_per_second\x18\x01 \x01(\x02\x12\x41\n\x11rate_limit_source\x18\x02 \x01(\x0e\x32&.temporal.api.enums.v1.RateLimitSource\x1a\x64\n\x11VersionsInfoEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12>\n\x05value\x18\x02 \x01(\x0b\x32/.temporal.api.taskqueue.v1.TaskQueueVersionInfo:\x02\x38\x01"\x17\n\x15GetClusterInfoRequest"\x8b\x03\n\x16GetClusterInfoResponse\x12h\n\x11supported_clients\x18\x01 \x03(\x0b\x32M.temporal.api.workflowservice.v1.GetClusterInfoResponse.SupportedClientsEntry\x12\x16\n\x0eserver_version\x18\x02 \x01(\t\x12\x12\n\ncluster_id\x18\x03 \x01(\t\x12:\n\x0cversion_info\x18\x04 \x01(\x0b\x32$.temporal.api.version.v1.VersionInfo\x12\x14\n\x0c\x63luster_name\x18\x05 \x01(\t\x12\x1b\n\x13history_shard_count\x18\x06 \x01(\x05\x12\x19\n\x11persistence_store\x18\x07 \x01(\t\x12\x18\n\x10visibility_store\x18\x08 \x01(\t\x1a\x37\n\x15SupportedClientsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"\x16\n\x14GetSystemInfoRequest"\xf4\x03\n\x15GetSystemInfoResponse\x12\x16\n\x0eserver_version\x18\x01 \x01(\t\x12Y\n\x0c\x63\x61pabilities\x18\x02 \x01(\x0b\x32\x43.temporal.api.workflowservice.v1.GetSystemInfoResponse.Capabilities\x1a\xe7\x02\n\x0c\x43\x61pabilities\x12\x1f\n\x17signal_and_query_header\x18\x01 \x01(\x08\x12&\n\x1einternal_error_differentiation\x18\x02 \x01(\x08\x12*\n"activity_failure_include_heartbeat\x18\x03 \x01(\x08\x12\x1a\n\x12supports_schedules\x18\x04 \x01(\x08\x12"\n\x1a\x65ncoded_failure_attributes\x18\x05 \x01(\x08\x12!\n\x19\x62uild_id_based_versioning\x18\x06 \x01(\x08\x12\x13\n\x0bupsert_memo\x18\x07 \x01(\x08\x12\x1c\n\x14\x65\x61ger_workflow_start\x18\x08 \x01(\x08\x12\x14\n\x0csdk_metadata\x18\t \x01(\x08\x12\'\n\x1f\x63ount_group_by_execution_status\x18\n \x01(\x08\x12\r\n\x05nexus\x18\x0b \x01(\x08"m\n\x1eListTaskQueuePartitionsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x38\n\ntask_queue\x18\x02 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue"\xdf\x01\n\x1fListTaskQueuePartitionsResponse\x12]\n\x1e\x61\x63tivity_task_queue_partitions\x18\x01 \x03(\x0b\x32\x35.temporal.api.taskqueue.v1.TaskQueuePartitionMetadata\x12]\n\x1eworkflow_task_queue_partitions\x18\x02 \x03(\x0b\x32\x35.temporal.api.taskqueue.v1.TaskQueuePartitionMetadata"\xcc\x02\n\x15\x43reateScheduleRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bschedule_id\x18\x02 \x01(\t\x12\x34\n\x08schedule\x18\x03 \x01(\x0b\x32".temporal.api.schedule.v1.Schedule\x12>\n\rinitial_patch\x18\x04 \x01(\x0b\x32\'.temporal.api.schedule.v1.SchedulePatch\x12\x10\n\x08identity\x18\x05 \x01(\t\x12\x12\n\nrequest_id\x18\x06 \x01(\t\x12*\n\x04memo\x18\x07 \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo\x12\x43\n\x11search_attributes\x18\x08 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes"0\n\x16\x43reateScheduleResponse\x12\x16\n\x0e\x63onflict_token\x18\x01 \x01(\x0c"A\n\x17\x44\x65scribeScheduleRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bschedule_id\x18\x02 \x01(\t"\x8f\x02\n\x18\x44\x65scribeScheduleResponse\x12\x34\n\x08schedule\x18\x01 \x01(\x0b\x32".temporal.api.schedule.v1.Schedule\x12\x34\n\x04info\x18\x02 \x01(\x0b\x32&.temporal.api.schedule.v1.ScheduleInfo\x12*\n\x04memo\x18\x03 \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo\x12\x43\n\x11search_attributes\x18\x04 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12\x16\n\x0e\x63onflict_token\x18\x05 \x01(\x0c"\xf8\x01\n\x15UpdateScheduleRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bschedule_id\x18\x02 \x01(\t\x12\x34\n\x08schedule\x18\x03 \x01(\x0b\x32".temporal.api.schedule.v1.Schedule\x12\x16\n\x0e\x63onflict_token\x18\x04 \x01(\x0c\x12\x10\n\x08identity\x18\x05 \x01(\t\x12\x12\n\nrequest_id\x18\x06 \x01(\t\x12\x43\n\x11search_attributes\x18\x07 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes"\x18\n\x16UpdateScheduleResponse"\x9c\x01\n\x14PatchScheduleRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bschedule_id\x18\x02 \x01(\t\x12\x36\n\x05patch\x18\x03 \x01(\x0b\x32\'.temporal.api.schedule.v1.SchedulePatch\x12\x10\n\x08identity\x18\x04 \x01(\t\x12\x12\n\nrequest_id\x18\x05 \x01(\t"\x17\n\x15PatchScheduleResponse"\xa8\x01\n ListScheduleMatchingTimesRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bschedule_id\x18\x02 \x01(\t\x12.\n\nstart_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12,\n\x08\x65nd_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"S\n!ListScheduleMatchingTimesResponse\x12.\n\nstart_time\x18\x01 \x03(\x0b\x32\x1a.google.protobuf.Timestamp"Q\n\x15\x44\x65leteScheduleRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bschedule_id\x18\x02 \x01(\t\x12\x10\n\x08identity\x18\x03 \x01(\t"\x18\n\x16\x44\x65leteScheduleResponse"l\n\x14ListSchedulesRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x19\n\x11maximum_page_size\x18\x02 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x03 \x01(\x0c\x12\r\n\x05query\x18\x04 \x01(\t"p\n\x15ListSchedulesResponse\x12>\n\tschedules\x18\x01 \x03(\x0b\x32+.temporal.api.schedule.v1.ScheduleListEntry\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\x0c"\x86\x05\n\'UpdateWorkerBuildIdCompatibilityRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x12\n\ntask_queue\x18\x02 \x01(\t\x12-\n#add_new_build_id_in_new_default_set\x18\x03 \x01(\tH\x00\x12\x87\x01\n\x1b\x61\x64\x64_new_compatible_build_id\x18\x04 \x01(\x0b\x32`.temporal.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest.AddNewCompatibleVersionH\x00\x12!\n\x17promote_set_by_build_id\x18\x05 \x01(\tH\x00\x12%\n\x1bpromote_build_id_within_set\x18\x06 \x01(\tH\x00\x12h\n\nmerge_sets\x18\x07 \x01(\x0b\x32R.temporal.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest.MergeSetsH\x00\x1ao\n\x17\x41\x64\x64NewCompatibleVersion\x12\x14\n\x0cnew_build_id\x18\x01 \x01(\t\x12$\n\x1c\x65xisting_compatible_build_id\x18\x02 \x01(\t\x12\x18\n\x10make_set_default\x18\x03 \x01(\x08\x1aI\n\tMergeSets\x12\x1c\n\x14primary_set_build_id\x18\x01 \x01(\t\x12\x1e\n\x16secondary_set_build_id\x18\x02 \x01(\tB\x0b\n\toperation"@\n(UpdateWorkerBuildIdCompatibilityResponseJ\x04\x08\x01\x10\x02R\x0eversion_set_id"_\n$GetWorkerBuildIdCompatibilityRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x12\n\ntask_queue\x18\x02 \x01(\t\x12\x10\n\x08max_sets\x18\x03 \x01(\x05"t\n%GetWorkerBuildIdCompatibilityResponse\x12K\n\x12major_version_sets\x18\x01 \x03(\x0b\x32/.temporal.api.taskqueue.v1.CompatibleVersionSet"\xb5\r\n"UpdateWorkerVersioningRulesRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x12\n\ntask_queue\x18\x02 \x01(\t\x12\x16\n\x0e\x63onflict_token\x18\x03 \x01(\x0c\x12\x81\x01\n\x16insert_assignment_rule\x18\x04 \x01(\x0b\x32_.temporal.api.workflowservice.v1.UpdateWorkerVersioningRulesRequest.InsertBuildIdAssignmentRuleH\x00\x12\x83\x01\n\x17replace_assignment_rule\x18\x05 \x01(\x0b\x32`.temporal.api.workflowservice.v1.UpdateWorkerVersioningRulesRequest.ReplaceBuildIdAssignmentRuleH\x00\x12\x81\x01\n\x16\x64\x65lete_assignment_rule\x18\x06 \x01(\x0b\x32_.temporal.api.workflowservice.v1.UpdateWorkerVersioningRulesRequest.DeleteBuildIdAssignmentRuleH\x00\x12\x8c\x01\n\x1c\x61\x64\x64_compatible_redirect_rule\x18\x07 \x01(\x0b\x32\x64.temporal.api.workflowservice.v1.UpdateWorkerVersioningRulesRequest.AddCompatibleBuildIdRedirectRuleH\x00\x12\x94\x01\n replace_compatible_redirect_rule\x18\x08 \x01(\x0b\x32h.temporal.api.workflowservice.v1.UpdateWorkerVersioningRulesRequest.ReplaceCompatibleBuildIdRedirectRuleH\x00\x12\x92\x01\n\x1f\x64\x65lete_compatible_redirect_rule\x18\t \x01(\x0b\x32g.temporal.api.workflowservice.v1.UpdateWorkerVersioningRulesRequest.DeleteCompatibleBuildIdRedirectRuleH\x00\x12l\n\x0f\x63ommit_build_id\x18\n \x01(\x0b\x32Q.temporal.api.workflowservice.v1.UpdateWorkerVersioningRulesRequest.CommitBuildIdH\x00\x1aq\n\x1bInsertBuildIdAssignmentRule\x12\x12\n\nrule_index\x18\x01 \x01(\x05\x12>\n\x04rule\x18\x02 \x01(\x0b\x32\x30.temporal.api.taskqueue.v1.BuildIdAssignmentRule\x1a\x81\x01\n\x1cReplaceBuildIdAssignmentRule\x12\x12\n\nrule_index\x18\x01 \x01(\x05\x12>\n\x04rule\x18\x02 \x01(\x0b\x32\x30.temporal.api.taskqueue.v1.BuildIdAssignmentRule\x12\r\n\x05\x66orce\x18\x03 \x01(\x08\x1a@\n\x1b\x44\x65leteBuildIdAssignmentRule\x12\x12\n\nrule_index\x18\x01 \x01(\x05\x12\r\n\x05\x66orce\x18\x02 \x01(\x08\x1aj\n AddCompatibleBuildIdRedirectRule\x12\x46\n\x04rule\x18\x01 \x01(\x0b\x32\x38.temporal.api.taskqueue.v1.CompatibleBuildIdRedirectRule\x1an\n$ReplaceCompatibleBuildIdRedirectRule\x12\x46\n\x04rule\x18\x01 \x01(\x0b\x32\x38.temporal.api.taskqueue.v1.CompatibleBuildIdRedirectRule\x1a>\n#DeleteCompatibleBuildIdRedirectRule\x12\x17\n\x0fsource_build_id\x18\x01 \x01(\t\x1a\x37\n\rCommitBuildId\x12\x17\n\x0ftarget_build_id\x18\x01 \x01(\t\x12\r\n\x05\x66orce\x18\x02 \x01(\x08\x42\x0b\n\toperation"\xfc\x01\n#UpdateWorkerVersioningRulesResponse\x12U\n\x10\x61ssignment_rules\x18\x01 \x03(\x0b\x32;.temporal.api.taskqueue.v1.TimestampedBuildIdAssignmentRule\x12\x66\n\x19\x63ompatible_redirect_rules\x18\x02 \x03(\x0b\x32\x43.temporal.api.taskqueue.v1.TimestampedCompatibleBuildIdRedirectRule\x12\x16\n\x0e\x63onflict_token\x18\x03 \x01(\x0c"H\n\x1fGetWorkerVersioningRulesRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x12\n\ntask_queue\x18\x02 \x01(\t"\xf9\x01\n GetWorkerVersioningRulesResponse\x12U\n\x10\x61ssignment_rules\x18\x01 \x03(\x0b\x32;.temporal.api.taskqueue.v1.TimestampedBuildIdAssignmentRule\x12\x66\n\x19\x63ompatible_redirect_rules\x18\x02 \x03(\x0b\x32\x43.temporal.api.taskqueue.v1.TimestampedCompatibleBuildIdRedirectRule\x12\x16\n\x0e\x63onflict_token\x18\x03 \x01(\x0c"\x9c\x01\n GetWorkerTaskReachabilityRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tbuild_ids\x18\x02 \x03(\t\x12\x13\n\x0btask_queues\x18\x03 \x03(\t\x12=\n\x0creachability\x18\x04 \x01(\x0e\x32\'.temporal.api.enums.v1.TaskReachability"r\n!GetWorkerTaskReachabilityResponse\x12M\n\x15\x62uild_id_reachability\x18\x01 \x03(\x0b\x32..temporal.api.taskqueue.v1.BuildIdReachability"\x85\x02\n\x1eUpdateWorkflowExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x45\n\x12workflow_execution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x1e\n\x16\x66irst_execution_run_id\x18\x03 \x01(\t\x12\x37\n\x0bwait_policy\x18\x04 \x01(\x0b\x32".temporal.api.update.v1.WaitPolicy\x12\x30\n\x07request\x18\x05 \x01(\x0b\x32\x1f.temporal.api.update.v1.Request"\xd7\x01\n\x1fUpdateWorkflowExecutionResponse\x12\x35\n\nupdate_ref\x18\x01 \x01(\x0b\x32!.temporal.api.update.v1.UpdateRef\x12\x30\n\x07outcome\x18\x02 \x01(\x0b\x32\x1f.temporal.api.update.v1.Outcome\x12K\n\x05stage\x18\x03 \x01(\x0e\x32<.temporal.api.enums.v1.UpdateWorkflowExecutionLifecycleStage"\xf4\x07\n\x1aStartBatchOperationRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x18\n\x10visibility_query\x18\x02 \x01(\t\x12\x0e\n\x06job_id\x18\x03 \x01(\t\x12\x0e\n\x06reason\x18\x04 \x01(\t\x12=\n\nexecutions\x18\x05 \x03(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12!\n\x19max_operations_per_second\x18\x06 \x01(\x02\x12Q\n\x15termination_operation\x18\n \x01(\x0b\x32\x30.temporal.api.batch.v1.BatchOperationTerminationH\x00\x12G\n\x10signal_operation\x18\x0b \x01(\x0b\x32+.temporal.api.batch.v1.BatchOperationSignalH\x00\x12S\n\x16\x63\x61ncellation_operation\x18\x0c \x01(\x0b\x32\x31.temporal.api.batch.v1.BatchOperationCancellationH\x00\x12K\n\x12\x64\x65letion_operation\x18\r \x01(\x0b\x32-.temporal.api.batch.v1.BatchOperationDeletionH\x00\x12\x45\n\x0freset_operation\x18\x0e \x01(\x0b\x32*.temporal.api.batch.v1.BatchOperationResetH\x00\x12p\n!update_workflow_options_operation\x18\x0f \x01(\x0b\x32\x43.temporal.api.batch.v1.BatchOperationUpdateWorkflowExecutionOptionsH\x00\x12^\n\x1cunpause_activities_operation\x18\x10 \x01(\x0b\x32\x36.temporal.api.batch.v1.BatchOperationUnpauseActivitiesH\x00\x12Z\n\x1areset_activities_operation\x18\x11 \x01(\x0b\x32\x34.temporal.api.batch.v1.BatchOperationResetActivitiesH\x00\x12g\n!update_activity_options_operation\x18\x12 \x01(\x0b\x32:.temporal.api.batch.v1.BatchOperationUpdateActivityOptionsH\x00\x42\x0b\n\toperation"\x1d\n\x1bStartBatchOperationResponse"`\n\x19StopBatchOperationRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x0e\n\x06job_id\x18\x02 \x01(\t\x12\x0e\n\x06reason\x18\x03 \x01(\t\x12\x10\n\x08identity\x18\x04 \x01(\t"\x1c\n\x1aStopBatchOperationResponse"B\n\x1d\x44\x65scribeBatchOperationRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x0e\n\x06job_id\x18\x02 \x01(\t"\x92\x03\n\x1e\x44\x65scribeBatchOperationResponse\x12\x41\n\x0eoperation_type\x18\x01 \x01(\x0e\x32).temporal.api.enums.v1.BatchOperationType\x12\x0e\n\x06job_id\x18\x02 \x01(\t\x12\x39\n\x05state\x18\x03 \x01(\x0e\x32*.temporal.api.enums.v1.BatchOperationState\x12.\n\nstart_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12.\n\nclose_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x1d\n\x15total_operation_count\x18\x06 \x01(\x03\x12 \n\x18\x63omplete_operation_count\x18\x07 \x01(\x03\x12\x1f\n\x17\x66\x61ilure_operation_count\x18\x08 \x01(\x03\x12\x10\n\x08identity\x18\t \x01(\t\x12\x0e\n\x06reason\x18\n \x01(\t"[\n\x1aListBatchOperationsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x03 \x01(\x0c"y\n\x1bListBatchOperationsResponse\x12\x41\n\x0eoperation_info\x18\x01 \x03(\x0b\x32).temporal.api.batch.v1.BatchOperationInfo\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\x0c"\xb9\x01\n"PollWorkflowExecutionUpdateRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x35\n\nupdate_ref\x18\x02 \x01(\x0b\x32!.temporal.api.update.v1.UpdateRef\x12\x10\n\x08identity\x18\x03 \x01(\t\x12\x37\n\x0bwait_policy\x18\x04 \x01(\x0b\x32".temporal.api.update.v1.WaitPolicy"\xdb\x01\n#PollWorkflowExecutionUpdateResponse\x12\x30\n\x07outcome\x18\x01 \x01(\x0b\x32\x1f.temporal.api.update.v1.Outcome\x12K\n\x05stage\x18\x02 \x01(\x0e\x32<.temporal.api.enums.v1.UpdateWorkflowExecutionLifecycleStage\x12\x35\n\nupdate_ref\x18\x03 \x01(\x0b\x32!.temporal.api.update.v1.UpdateRef"\xea\x02\n\x19PollNexusTaskQueueRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x10\n\x08identity\x18\x02 \x01(\t\x12\x38\n\ntask_queue\x18\x03 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12Z\n\x1bworker_version_capabilities\x18\x04 \x01(\x0b\x32\x31.temporal.api.common.v1.WorkerVersionCapabilitiesB\x02\x18\x01\x12O\n\x12\x64\x65ployment_options\x18\x06 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentOptions\x12\x41\n\x10worker_heartbeat\x18\x07 \x03(\x0b\x32\'.temporal.api.worker.v1.WorkerHeartbeat"\xb4\x01\n\x1aPollNexusTaskQueueResponse\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12/\n\x07request\x18\x02 \x01(\x0b\x32\x1e.temporal.api.nexus.v1.Request\x12Q\n\x17poller_scaling_decision\x18\x03 \x01(\x0b\x32\x30.temporal.api.taskqueue.v1.PollerScalingDecision"\x8e\x01\n RespondNexusTaskCompletedRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x10\n\x08identity\x18\x02 \x01(\t\x12\x12\n\ntask_token\x18\x03 \x01(\x0c\x12\x31\n\x08response\x18\x04 \x01(\x0b\x32\x1f.temporal.api.nexus.v1.Response"#\n!RespondNexusTaskCompletedResponse"\x8c\x01\n\x1dRespondNexusTaskFailedRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x10\n\x08identity\x18\x02 \x01(\t\x12\x12\n\ntask_token\x18\x03 \x01(\x0c\x12\x32\n\x05\x65rror\x18\x04 \x01(\x0b\x32#.temporal.api.nexus.v1.HandlerError" \n\x1eRespondNexusTaskFailedResponse"\xdf\x02\n\x1c\x45xecuteMultiOperationRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12[\n\noperations\x18\x02 \x03(\x0b\x32G.temporal.api.workflowservice.v1.ExecuteMultiOperationRequest.Operation\x1a\xce\x01\n\tOperation\x12X\n\x0estart_workflow\x18\x01 \x01(\x0b\x32>.temporal.api.workflowservice.v1.StartWorkflowExecutionRequestH\x00\x12Z\n\x0fupdate_workflow\x18\x02 \x01(\x0b\x32?.temporal.api.workflowservice.v1.UpdateWorkflowExecutionRequestH\x00\x42\x0b\n\toperation"\xcc\x02\n\x1d\x45xecuteMultiOperationResponse\x12Z\n\tresponses\x18\x01 \x03(\x0b\x32G.temporal.api.workflowservice.v1.ExecuteMultiOperationResponse.Response\x1a\xce\x01\n\x08Response\x12Y\n\x0estart_workflow\x18\x01 \x01(\x0b\x32?.temporal.api.workflowservice.v1.StartWorkflowExecutionResponseH\x00\x12[\n\x0fupdate_workflow\x18\x02 \x01(\x0b\x32@.temporal.api.workflowservice.v1.UpdateWorkflowExecutionResponseH\x00\x42\n\n\x08response"\xd0\x02\n\x1cUpdateActivityOptionsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12<\n\texecution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x10\n\x08identity\x18\x03 \x01(\t\x12\x43\n\x10\x61\x63tivity_options\x18\x04 \x01(\x0b\x32).temporal.api.activity.v1.ActivityOptions\x12/\n\x0bupdate_mask\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.FieldMask\x12\x0c\n\x02id\x18\x06 \x01(\tH\x00\x12\x0e\n\x04type\x18\x07 \x01(\tH\x00\x12\x13\n\tmatch_all\x18\t \x01(\x08H\x00\x12\x18\n\x10restore_original\x18\x08 \x01(\x08\x42\n\n\x08\x61\x63tivity"d\n\x1dUpdateActivityOptionsResponse\x12\x43\n\x10\x61\x63tivity_options\x18\x01 \x01(\x0b\x32).temporal.api.activity.v1.ActivityOptions"\xb3\x01\n\x14PauseActivityRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12<\n\texecution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x10\n\x08identity\x18\x03 \x01(\t\x12\x0c\n\x02id\x18\x04 \x01(\tH\x00\x12\x0e\n\x04type\x18\x05 \x01(\tH\x00\x12\x0e\n\x06reason\x18\x06 \x01(\tB\n\n\x08\x61\x63tivity"\x17\n\x15PauseActivityResponse"\x98\x02\n\x16UnpauseActivityRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12<\n\texecution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x10\n\x08identity\x18\x03 \x01(\t\x12\x0c\n\x02id\x18\x04 \x01(\tH\x00\x12\x0e\n\x04type\x18\x05 \x01(\tH\x00\x12\x15\n\x0bunpause_all\x18\x06 \x01(\x08H\x00\x12\x16\n\x0ereset_attempts\x18\x07 \x01(\x08\x12\x17\n\x0freset_heartbeat\x18\x08 \x01(\x08\x12)\n\x06jitter\x18\t \x01(\x0b\x32\x19.google.protobuf.DurationB\n\n\x08\x61\x63tivity"\x19\n\x17UnpauseActivityResponse"\xb3\x02\n\x14ResetActivityRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12<\n\texecution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x10\n\x08identity\x18\x03 \x01(\t\x12\x0c\n\x02id\x18\x04 \x01(\tH\x00\x12\x0e\n\x04type\x18\x05 \x01(\tH\x00\x12\x13\n\tmatch_all\x18\n \x01(\x08H\x00\x12\x17\n\x0freset_heartbeat\x18\x06 \x01(\x08\x12\x13\n\x0bkeep_paused\x18\x07 \x01(\x08\x12)\n\x06jitter\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12 \n\x18restore_original_options\x18\t \x01(\x08\x42\n\n\x08\x61\x63tivity"\x17\n\x15ResetActivityResponse"\x8a\x02\n%UpdateWorkflowExecutionOptionsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x45\n\x12workflow_execution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12V\n\x1aworkflow_execution_options\x18\x03 \x01(\x0b\x32\x32.temporal.api.workflow.v1.WorkflowExecutionOptions\x12/\n\x0bupdate_mask\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.FieldMask"\x80\x01\n&UpdateWorkflowExecutionOptionsResponse\x12V\n\x1aworkflow_execution_options\x18\x01 \x01(\x0b\x32\x32.temporal.api.workflow.v1.WorkflowExecutionOptions"j\n\x19\x44\x65scribeDeploymentRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12:\n\ndeployment\x18\x02 \x01(\x0b\x32&.temporal.api.deployment.v1.Deployment"a\n\x1a\x44\x65scribeDeploymentResponse\x12\x43\n\x0f\x64\x65ployment_info\x18\x01 \x01(\x0b\x32*.temporal.api.deployment.v1.DeploymentInfo"\xc2\x01\n&DescribeWorkerDeploymentVersionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x07version\x18\x02 \x01(\tB\x02\x18\x01\x12O\n\x12\x64\x65ployment_version\x18\x03 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12\x1f\n\x17report_task_queue_stats\x18\x04 \x01(\x08"\x8c\x05\n\'DescribeWorkerDeploymentVersionResponse\x12_\n\x1eworker_deployment_version_info\x18\x01 \x01(\x0b\x32\x37.temporal.api.deployment.v1.WorkerDeploymentVersionInfo\x12v\n\x13version_task_queues\x18\x02 \x03(\x0b\x32Y.temporal.api.workflowservice.v1.DescribeWorkerDeploymentVersionResponse.VersionTaskQueue\x1a\x87\x03\n\x10VersionTaskQueue\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x32\n\x04type\x18\x02 \x01(\x0e\x32$.temporal.api.enums.v1.TaskQueueType\x12\x38\n\x05stats\x18\x03 \x01(\x0b\x32).temporal.api.taskqueue.v1.TaskQueueStats\x12\x90\x01\n\x15stats_by_priority_key\x18\x04 \x03(\x0b\x32q.temporal.api.workflowservice.v1.DescribeWorkerDeploymentVersionResponse.VersionTaskQueue.StatsByPriorityKeyEntry\x1a\x64\n\x17StatsByPriorityKeyEntry\x12\x0b\n\x03key\x18\x01 \x01(\x05\x12\x38\n\x05value\x18\x02 \x01(\x0b\x32).temporal.api.taskqueue.v1.TaskQueueStats:\x02\x38\x01"M\n\x1f\x44\x65scribeWorkerDeploymentRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x17\n\x0f\x64\x65ployment_name\x18\x02 \x01(\t"\x8c\x01\n DescribeWorkerDeploymentResponse\x12\x16\n\x0e\x63onflict_token\x18\x01 \x01(\x0c\x12P\n\x16worker_deployment_info\x18\x02 \x01(\x0b\x32\x30.temporal.api.deployment.v1.WorkerDeploymentInfo"l\n\x16ListDeploymentsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x03 \x01(\x0c\x12\x13\n\x0bseries_name\x18\x04 \x01(\t"w\n\x17ListDeploymentsResponse\x12\x17\n\x0fnext_page_token\x18\x01 \x01(\x0c\x12\x43\n\x0b\x64\x65ployments\x18\x02 \x03(\x0b\x32..temporal.api.deployment.v1.DeploymentListInfo"\xcd\x01\n\x1bSetCurrentDeploymentRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12:\n\ndeployment\x18\x02 \x01(\x0b\x32&.temporal.api.deployment.v1.Deployment\x12\x10\n\x08identity\x18\x03 \x01(\t\x12M\n\x0fupdate_metadata\x18\x04 \x01(\x0b\x32\x34.temporal.api.deployment.v1.UpdateDeploymentMetadata"\xb9\x01\n\x1cSetCurrentDeploymentResponse\x12K\n\x17\x63urrent_deployment_info\x18\x01 \x01(\x0b\x32*.temporal.api.deployment.v1.DeploymentInfo\x12L\n\x18previous_deployment_info\x18\x02 \x01(\x0b\x32*.temporal.api.deployment.v1.DeploymentInfo"\xcb\x01\n(SetWorkerDeploymentCurrentVersionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x17\n\x0f\x64\x65ployment_name\x18\x02 \x01(\t\x12\x13\n\x07version\x18\x03 \x01(\tB\x02\x18\x01\x12\x10\n\x08\x62uild_id\x18\x07 \x01(\t\x12\x16\n\x0e\x63onflict_token\x18\x04 \x01(\x0c\x12\x10\n\x08identity\x18\x05 \x01(\t\x12"\n\x1aignore_missing_task_queues\x18\x06 \x01(\x08"\xbb\x01\n)SetWorkerDeploymentCurrentVersionResponse\x12\x16\n\x0e\x63onflict_token\x18\x01 \x01(\x0c\x12\x1c\n\x10previous_version\x18\x02 \x01(\tB\x02\x18\x01\x12X\n\x1bprevious_deployment_version\x18\x03 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion"\xdf\x01\n(SetWorkerDeploymentRampingVersionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x17\n\x0f\x64\x65ployment_name\x18\x02 \x01(\t\x12\x13\n\x07version\x18\x03 \x01(\tB\x02\x18\x01\x12\x10\n\x08\x62uild_id\x18\x08 \x01(\t\x12\x12\n\npercentage\x18\x04 \x01(\x02\x12\x16\n\x0e\x63onflict_token\x18\x05 \x01(\x0c\x12\x10\n\x08identity\x18\x06 \x01(\t\x12"\n\x1aignore_missing_task_queues\x18\x07 \x01(\x08"\xd8\x01\n)SetWorkerDeploymentRampingVersionResponse\x12\x16\n\x0e\x63onflict_token\x18\x01 \x01(\x0c\x12\x1c\n\x10previous_version\x18\x02 \x01(\tB\x02\x18\x01\x12X\n\x1bprevious_deployment_version\x18\x04 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12\x1b\n\x13previous_percentage\x18\x03 \x01(\x02"]\n\x1cListWorkerDeploymentsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x03 \x01(\x0c"\x9f\x05\n\x1dListWorkerDeploymentsResponse\x12\x17\n\x0fnext_page_token\x18\x01 \x01(\x0c\x12r\n\x12worker_deployments\x18\x02 \x03(\x0b\x32V.temporal.api.workflowservice.v1.ListWorkerDeploymentsResponse.WorkerDeploymentSummary\x1a\xf0\x03\n\x17WorkerDeploymentSummary\x12\x0c\n\x04name\x18\x01 \x01(\t\x12/\n\x0b\x63reate_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x41\n\x0erouting_config\x18\x03 \x01(\x0b\x32).temporal.api.deployment.v1.RoutingConfig\x12o\n\x16latest_version_summary\x18\x04 \x01(\x0b\x32O.temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary\x12p\n\x17\x63urrent_version_summary\x18\x05 \x01(\x0b\x32O.temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary\x12p\n\x17ramping_version_summary\x18\x06 \x01(\x0b\x32O.temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary"\xc8\x01\n$DeleteWorkerDeploymentVersionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x07version\x18\x02 \x01(\tB\x02\x18\x01\x12O\n\x12\x64\x65ployment_version\x18\x05 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12\x15\n\rskip_drainage\x18\x03 \x01(\x08\x12\x10\n\x08identity\x18\x04 \x01(\t"\'\n%DeleteWorkerDeploymentVersionResponse"]\n\x1d\x44\x65leteWorkerDeploymentRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x17\n\x0f\x64\x65ployment_name\x18\x02 \x01(\t\x12\x10\n\x08identity\x18\x03 \x01(\t" \n\x1e\x44\x65leteWorkerDeploymentResponse"\xa2\x03\n,UpdateWorkerDeploymentVersionMetadataRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x07version\x18\x02 \x01(\tB\x02\x18\x01\x12O\n\x12\x64\x65ployment_version\x18\x05 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12x\n\x0eupsert_entries\x18\x03 \x03(\x0b\x32`.temporal.api.workflowservice.v1.UpdateWorkerDeploymentVersionMetadataRequest.UpsertEntriesEntry\x12\x16\n\x0eremove_entries\x18\x04 \x03(\t\x12\x10\n\x08identity\x18\x06 \x01(\t\x1aU\n\x12UpsertEntriesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"n\n-UpdateWorkerDeploymentVersionMetadataResponse\x12=\n\x08metadata\x18\x01 \x01(\x0b\x32+.temporal.api.deployment.v1.VersionMetadata"E\n\x1bGetCurrentDeploymentRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bseries_name\x18\x02 \x01(\t"k\n\x1cGetCurrentDeploymentResponse\x12K\n\x17\x63urrent_deployment_info\x18\x01 \x01(\x0b\x32*.temporal.api.deployment.v1.DeploymentInfo"q\n GetDeploymentReachabilityRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12:\n\ndeployment\x18\x02 \x01(\x0b\x32&.temporal.api.deployment.v1.Deployment"\xe3\x01\n!GetDeploymentReachabilityResponse\x12\x43\n\x0f\x64\x65ployment_info\x18\x01 \x01(\x0b\x32*.temporal.api.deployment.v1.DeploymentInfo\x12\x43\n\x0creachability\x18\x02 \x01(\x0e\x32-.temporal.api.enums.v1.DeploymentReachability\x12\x34\n\x10last_update_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"\xb4\x01\n\x19\x43reateWorkflowRuleRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x35\n\x04spec\x18\x02 \x01(\x0b\x32\'.temporal.api.rules.v1.WorkflowRuleSpec\x12\x12\n\nforce_scan\x18\x03 \x01(\x08\x12\x12\n\nrequest_id\x18\x04 \x01(\t\x12\x10\n\x08identity\x18\x05 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x06 \x01(\t"_\n\x1a\x43reateWorkflowRuleResponse\x12\x31\n\x04rule\x18\x01 \x01(\x0b\x32#.temporal.api.rules.v1.WorkflowRule\x12\x0e\n\x06job_id\x18\x02 \x01(\t"A\n\x1b\x44\x65scribeWorkflowRuleRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x0f\n\x07rule_id\x18\x02 \x01(\t"Q\n\x1c\x44\x65scribeWorkflowRuleResponse\x12\x31\n\x04rule\x18\x01 \x01(\x0b\x32#.temporal.api.rules.v1.WorkflowRule"?\n\x19\x44\x65leteWorkflowRuleRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x0f\n\x07rule_id\x18\x02 \x01(\t"\x1c\n\x1a\x44\x65leteWorkflowRuleResponse"F\n\x18ListWorkflowRulesRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\x0c"h\n\x19ListWorkflowRulesResponse\x12\x32\n\x05rules\x18\x01 \x03(\x0b\x32#.temporal.api.rules.v1.WorkflowRule\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\x0c"\xce\x01\n\x1aTriggerWorkflowRuleRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12<\n\texecution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x0c\n\x02id\x18\x04 \x01(\tH\x00\x12\x37\n\x04spec\x18\x05 \x01(\x0b\x32\'.temporal.api.rules.v1.WorkflowRuleSpecH\x00\x12\x10\n\x08identity\x18\x03 \x01(\tB\x06\n\x04rule".\n\x1bTriggerWorkflowRuleResponse\x12\x0f\n\x07\x61pplied\x18\x01 \x01(\x08"\x86\x01\n\x1cRecordWorkerHeartbeatRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x10\n\x08identity\x18\x02 \x01(\t\x12\x41\n\x10worker_heartbeat\x18\x03 \x03(\x0b\x32\'.temporal.api.worker.v1.WorkerHeartbeat"\x1f\n\x1dRecordWorkerHeartbeatResponse"b\n\x12ListWorkersRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x03 \x01(\x0c\x12\r\n\x05query\x18\x04 \x01(\t"h\n\x13ListWorkersResponse\x12\x38\n\x0cworkers_info\x18\x01 \x03(\x0b\x32".temporal.api.worker.v1.WorkerInfo\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\x0c"\xe2\x03\n\x1cUpdateTaskQueueConfigRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x10\n\x08identity\x18\x02 \x01(\t\x12\x12\n\ntask_queue\x18\x03 \x01(\t\x12=\n\x0ftask_queue_type\x18\x04 \x01(\x0e\x32$.temporal.api.enums.v1.TaskQueueType\x12n\n\x17update_queue_rate_limit\x18\x05 \x01(\x0b\x32M.temporal.api.workflowservice.v1.UpdateTaskQueueConfigRequest.RateLimitUpdate\x12}\n&update_fairness_key_rate_limit_default\x18\x06 \x01(\x0b\x32M.temporal.api.workflowservice.v1.UpdateTaskQueueConfigRequest.RateLimitUpdate\x1a[\n\x0fRateLimitUpdate\x12\x38\n\nrate_limit\x18\x01 \x01(\x0b\x32$.temporal.api.taskqueue.v1.RateLimit\x12\x0e\n\x06reason\x18\x02 \x01(\t"[\n\x1dUpdateTaskQueueConfigResponse\x12:\n\x06\x63onfig\x18\x01 \x01(\x0b\x32*.temporal.api.taskqueue.v1.TaskQueueConfig"\x89\x01\n\x18\x46\x65tchWorkerConfigRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x10\n\x08identity\x18\x02 \x01(\t\x12\x0e\n\x06reason\x18\x03 \x01(\t\x12\x38\n\x08selector\x18\x06 \x01(\x0b\x32&.temporal.api.common.v1.WorkerSelector"U\n\x19\x46\x65tchWorkerConfigResponse\x12\x38\n\rworker_config\x18\x01 \x01(\x0b\x32!.temporal.api.sdk.v1.WorkerConfig"\xf5\x01\n\x19UpdateWorkerConfigRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x10\n\x08identity\x18\x02 \x01(\t\x12\x0e\n\x06reason\x18\x03 \x01(\t\x12\x38\n\rworker_config\x18\x04 \x01(\x0b\x32!.temporal.api.sdk.v1.WorkerConfig\x12/\n\x0bupdate_mask\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.FieldMask\x12\x38\n\x08selector\x18\x06 \x01(\x0b\x32&.temporal.api.common.v1.WorkerSelector"d\n\x1aUpdateWorkerConfigResponse\x12:\n\rworker_config\x18\x01 \x01(\x0b\x32!.temporal.api.sdk.v1.WorkerConfigH\x00\x42\n\n\x08responseB\xbe\x01\n"io.temporal.api.workflowservice.v1B\x14RequestResponseProtoP\x01Z5go.temporal.io/api/workflowservice/v1;workflowservice\xaa\x02!Temporalio.Api.WorkflowService.V1\xea\x02$Temporalio::Api::WorkflowService::V1b\x06proto3' + b'\n6temporal/api/workflowservice/v1/request_response.proto\x12\x1ftemporal.api.workflowservice.v1\x1a+temporal/api/enums/v1/batch_operation.proto\x1a"temporal/api/enums/v1/common.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a%temporal/api/enums/v1/namespace.proto\x1a(temporal/api/enums/v1/failed_cause.proto\x1a!temporal/api/enums/v1/query.proto\x1a!temporal/api/enums/v1/reset.proto\x1a&temporal/api/enums/v1/task_queue.proto\x1a&temporal/api/enums/v1/deployment.proto\x1a"temporal/api/enums/v1/update.proto\x1a)temporal/api/enums/v1/time_skipping.proto\x1a$temporal/api/enums/v1/activity.proto\x1a!temporal/api/enums/v1/nexus.proto\x1a&temporal/api/activity/v1/message.proto\x1a$temporal/api/common/v1/message.proto\x1a%temporal/api/history/v1/message.proto\x1a&temporal/api/workflow/v1/message.proto\x1a%temporal/api/command/v1/message.proto\x1a$temporal/api/compute/v1/config.proto\x1a(temporal/api/deployment/v1/message.proto\x1a%temporal/api/failure/v1/message.proto\x1a$temporal/api/filter/v1/message.proto\x1a&temporal/api/protocol/v1/message.proto\x1a\'temporal/api/namespace/v1/message.proto\x1a#temporal/api/query/v1/message.proto\x1a)temporal/api/replication/v1/message.proto\x1a#temporal/api/rules/v1/message.proto\x1a\'temporal/api/sdk/v1/worker_config.proto\x1a&temporal/api/schedule/v1/message.proto\x1a\'temporal/api/taskqueue/v1/message.proto\x1a$temporal/api/update/v1/message.proto\x1a%temporal/api/version/v1/message.proto\x1a#temporal/api/batch/v1/message.proto\x1a\x30temporal/api/sdk/v1/task_complete_metadata.proto\x1a\'temporal/api/sdk/v1/user_metadata.proto\x1a#temporal/api/nexus/v1/message.proto\x1a$temporal/api/worker/v1/message.proto\x1a\x1egoogle/protobuf/duration.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto"\x88\x05\n\x18RegisterNamespaceRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x13\n\x0bowner_email\x18\x03 \x01(\t\x12\x46\n#workflow_execution_retention_period\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\x12G\n\x08\x63lusters\x18\x05 \x03(\x0b\x32\x35.temporal.api.replication.v1.ClusterReplicationConfig\x12\x1b\n\x13\x61\x63tive_cluster_name\x18\x06 \x01(\t\x12Q\n\x04\x64\x61ta\x18\x07 \x03(\x0b\x32\x43.temporal.api.workflowservice.v1.RegisterNamespaceRequest.DataEntry\x12\x16\n\x0esecurity_token\x18\x08 \x01(\t\x12\x1b\n\x13is_global_namespace\x18\t \x01(\x08\x12\x44\n\x16history_archival_state\x18\n \x01(\x0e\x32$.temporal.api.enums.v1.ArchivalState\x12\x1c\n\x14history_archival_uri\x18\x0b \x01(\t\x12G\n\x19visibility_archival_state\x18\x0c \x01(\x0e\x32$.temporal.api.enums.v1.ArchivalState\x12\x1f\n\x17visibility_archival_uri\x18\r \x01(\t\x1a+\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"\x1b\n\x19RegisterNamespaceResponse"\x89\x01\n\x15ListNamespacesRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\x0c\x12\x44\n\x10namespace_filter\x18\x03 \x01(\x0b\x32*.temporal.api.namespace.v1.NamespaceFilter"\x81\x01\n\x16ListNamespacesResponse\x12N\n\nnamespaces\x18\x01 \x03(\x0b\x32:.temporal.api.workflowservice.v1.DescribeNamespaceResponse\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\x0c"S\n\x18\x44\x65scribeNamespaceRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\n\n\x02id\x18\x02 \x01(\t\x12\x18\n\x10weak_consistency\x18\x03 \x01(\x08"\x81\x04\n\x19\x44\x65scribeNamespaceResponse\x12@\n\x0enamespace_info\x18\x01 \x01(\x0b\x32(.temporal.api.namespace.v1.NamespaceInfo\x12:\n\x06\x63onfig\x18\x02 \x01(\x0b\x32*.temporal.api.namespace.v1.NamespaceConfig\x12S\n\x12replication_config\x18\x03 \x01(\x0b\x32\x37.temporal.api.replication.v1.NamespaceReplicationConfig\x12\x18\n\x10\x66\x61ilover_version\x18\x04 \x01(\x03\x12\x1b\n\x13is_global_namespace\x18\x05 \x01(\x08\x12\x45\n\x10\x66\x61ilover_history\x18\x06 \x03(\x0b\x32+.temporal.api.replication.v1.FailoverStatus\x12J\n\x12poller_group_infos\x18\x07 \x03(\x0b\x32*.temporal.api.taskqueue.v1.PollerGroupInfoB\x02\x18\x01\x12G\n\x12poller_groups_info\x18\x08 \x01(\x0b\x32+.temporal.api.taskqueue.v1.PollerGroupsInfo"\xcf\x02\n\x16UpdateNamespaceRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x43\n\x0bupdate_info\x18\x02 \x01(\x0b\x32..temporal.api.namespace.v1.UpdateNamespaceInfo\x12:\n\x06\x63onfig\x18\x03 \x01(\x0b\x32*.temporal.api.namespace.v1.NamespaceConfig\x12S\n\x12replication_config\x18\x04 \x01(\x0b\x32\x37.temporal.api.replication.v1.NamespaceReplicationConfig\x12\x16\n\x0esecurity_token\x18\x05 \x01(\t\x12\x19\n\x11\x64\x65lete_bad_binary\x18\x06 \x01(\t\x12\x19\n\x11promote_namespace\x18\x07 \x01(\x08"\xa3\x02\n\x17UpdateNamespaceResponse\x12@\n\x0enamespace_info\x18\x01 \x01(\x0b\x32(.temporal.api.namespace.v1.NamespaceInfo\x12:\n\x06\x63onfig\x18\x02 \x01(\x0b\x32*.temporal.api.namespace.v1.NamespaceConfig\x12S\n\x12replication_config\x18\x03 \x01(\x0b\x32\x37.temporal.api.replication.v1.NamespaceReplicationConfig\x12\x18\n\x10\x66\x61ilover_version\x18\x04 \x01(\x03\x12\x1b\n\x13is_global_namespace\x18\x05 \x01(\x08"F\n\x19\x44\x65precateNamespaceRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x16\n\x0esecurity_token\x18\x02 \x01(\t"\x1c\n\x1a\x44\x65precateNamespaceResponse"\xd1\x0c\n\x1dStartWorkflowExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12;\n\rworkflow_type\x18\x03 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12\x38\n\ntask_queue\x18\x04 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12/\n\x05input\x18\x05 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12=\n\x1aworkflow_execution_timeout\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x37\n\x14workflow_run_timeout\x18\x07 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x10\n\x08identity\x18\t \x01(\t\x12\x12\n\nrequest_id\x18\n \x01(\t\x12N\n\x18workflow_id_reuse_policy\x18\x0b \x01(\x0e\x32,.temporal.api.enums.v1.WorkflowIdReusePolicy\x12T\n\x1bworkflow_id_conflict_policy\x18\x16 \x01(\x0e\x32/.temporal.api.enums.v1.WorkflowIdConflictPolicy\x12\x39\n\x0cretry_policy\x18\x0c \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x15\n\rcron_schedule\x18\r \x01(\t\x12*\n\x04memo\x18\x0e \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo\x12\x43\n\x11search_attributes\x18\x0f \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12.\n\x06header\x18\x10 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12\x1f\n\x17request_eager_execution\x18\x11 \x01(\x08\x12;\n\x11\x63ontinued_failure\x18\x12 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12@\n\x16last_completion_result\x18\x13 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x37\n\x14workflow_start_delay\x18\x14 \x01(\x0b\x32\x19.google.protobuf.Duration\x12>\n\x14\x63ompletion_callbacks\x18\x15 \x03(\x0b\x32 .temporal.api.common.v1.Callback\x12\x38\n\ruser_metadata\x18\x17 \x01(\x0b\x32!.temporal.api.sdk.v1.UserMetadata\x12+\n\x05links\x18\x18 \x03(\x0b\x32\x1c.temporal.api.common.v1.Link\x12I\n\x13versioning_override\x18\x19 \x01(\x0b\x32,.temporal.api.workflow.v1.VersioningOverride\x12H\n\x13on_conflict_options\x18\x1a \x01(\x0b\x32+.temporal.api.workflow.v1.OnConflictOptions\x12\x32\n\x08priority\x18\x1b \x01(\x0b\x32 .temporal.api.common.v1.Priority\x12\\\n\x1f\x65\x61ger_worker_deployment_options\x18\x1c \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentOptions\x12H\n\x14time_skipping_config\x18\x1d \x01(\x0b\x32*.temporal.api.common.v1.TimeSkippingConfig"\xaa\x02\n\x1eStartWorkflowExecutionResponse\x12\x0e\n\x06run_id\x18\x01 \x01(\t\x12\x1e\n\x16\x66irst_execution_run_id\x18\x06 \x01(\t\x12\x0f\n\x07started\x18\x03 \x01(\x08\x12>\n\x06status\x18\x05 \x01(\x0e\x32..temporal.api.enums.v1.WorkflowExecutionStatus\x12[\n\x13\x65\x61ger_workflow_task\x18\x02 \x01(\x0b\x32>.temporal.api.workflowservice.v1.PollWorkflowTaskQueueResponse\x12*\n\x04link\x18\x04 \x01(\x0b\x32\x1c.temporal.api.common.v1.Link"\xaa\x02\n"GetWorkflowExecutionHistoryRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12<\n\texecution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x19\n\x11maximum_page_size\x18\x03 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x04 \x01(\x0c\x12\x16\n\x0ewait_new_event\x18\x05 \x01(\x08\x12P\n\x19history_event_filter_type\x18\x06 \x01(\x0e\x32-.temporal.api.enums.v1.HistoryEventFilterType\x12\x15\n\rskip_archival\x18\x07 \x01(\x08"\xba\x01\n#GetWorkflowExecutionHistoryResponse\x12\x31\n\x07history\x18\x01 \x01(\x0b\x32 .temporal.api.history.v1.History\x12\x35\n\x0braw_history\x18\x02 \x03(\x0b\x32 .temporal.api.common.v1.DataBlob\x12\x17\n\x0fnext_page_token\x18\x03 \x01(\x0c\x12\x10\n\x08\x61rchived\x18\x04 \x01(\x08"\xb0\x01\n)GetWorkflowExecutionHistoryReverseRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12<\n\texecution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x19\n\x11maximum_page_size\x18\x03 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x04 \x01(\x0c"x\n*GetWorkflowExecutionHistoryReverseResponse\x12\x31\n\x07history\x18\x01 \x01(\x0b\x32 .temporal.api.history.v1.History\x12\x17\n\x0fnext_page_token\x18\x03 \x01(\x0c"\xb8\x03\n\x1cPollWorkflowTaskQueueRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x38\n\ntask_queue\x18\x02 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12\x17\n\x0fpoller_group_id\x18\n \x01(\t\x12\x10\n\x08identity\x18\x03 \x01(\t\x12\x1b\n\x13worker_instance_key\x18\x08 \x01(\t\x12!\n\x19worker_control_task_queue\x18\t \x01(\t\x12\x1b\n\x0f\x62inary_checksum\x18\x04 \x01(\tB\x02\x18\x01\x12Z\n\x1bworker_version_capabilities\x18\x05 \x01(\x0b\x32\x31.temporal.api.common.v1.WorkerVersionCapabilitiesB\x02\x18\x01\x12O\n\x12\x64\x65ployment_options\x18\x06 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentOptionsJ\x04\x08\x07\x10\x08R\x10worker_heartbeat"\xbf\x08\n\x1dPollWorkflowTaskQueueResponse\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12\x45\n\x12workflow_execution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12;\n\rworkflow_type\x18\x03 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12!\n\x19previous_started_event_id\x18\x04 \x01(\x03\x12\x18\n\x10started_event_id\x18\x05 \x01(\x03\x12\x0f\n\x07\x61ttempt\x18\x06 \x01(\x05\x12\x1a\n\x12\x62\x61\x63klog_count_hint\x18\x07 \x01(\x03\x12\x31\n\x07history\x18\x08 \x01(\x0b\x32 .temporal.api.history.v1.History\x12\x17\n\x0fnext_page_token\x18\t \x01(\x0c\x12\x33\n\x05query\x18\n \x01(\x0b\x32$.temporal.api.query.v1.WorkflowQuery\x12K\n\x1dworkflow_execution_task_queue\x18\x0b \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12\x32\n\x0escheduled_time\x18\x0c \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x30\n\x0cstarted_time\x18\r \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\\\n\x07queries\x18\x0e \x03(\x0b\x32K.temporal.api.workflowservice.v1.PollWorkflowTaskQueueResponse.QueriesEntry\x12\x33\n\x08messages\x18\x0f \x03(\x0b\x32!.temporal.api.protocol.v1.Message\x12Q\n\x17poller_scaling_decision\x18\x10 \x01(\x0b\x32\x30.temporal.api.taskqueue.v1.PollerScalingDecision\x12\x17\n\x0fpoller_group_id\x18\x11 \x01(\t\x12J\n\x12poller_group_infos\x18\x12 \x03(\x0b\x32*.temporal.api.taskqueue.v1.PollerGroupInfoB\x02\x18\x01\x12G\n\x12poller_groups_info\x18\x13 \x01(\x0b\x32+.temporal.api.taskqueue.v1.PollerGroupsInfo\x1aT\n\x0cQueriesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x33\n\x05value\x18\x02 \x01(\x0b\x32$.temporal.api.query.v1.WorkflowQuery:\x02\x38\x01"\xba\n\n#RespondWorkflowTaskCompletedRequest\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12\x32\n\x08\x63ommands\x18\x02 \x03(\x0b\x32 .temporal.api.command.v1.Command\x12\x10\n\x08identity\x18\x03 \x01(\t\x12O\n\x11sticky_attributes\x18\x04 \x01(\x0b\x32\x34.temporal.api.taskqueue.v1.StickyExecutionAttributes\x12 \n\x18return_new_workflow_task\x18\x05 \x01(\x08\x12&\n\x1e\x66orce_create_new_workflow_task\x18\x06 \x01(\x08\x12\x1b\n\x0f\x62inary_checksum\x18\x07 \x01(\tB\x02\x18\x01\x12m\n\rquery_results\x18\x08 \x03(\x0b\x32V.temporal.api.workflowservice.v1.RespondWorkflowTaskCompletedRequest.QueryResultsEntry\x12\x11\n\tnamespace\x18\t \x01(\t\x12\x13\n\x0bresource_id\x18\x12 \x01(\t\x12L\n\x14worker_version_stamp\x18\n \x01(\x0b\x32*.temporal.api.common.v1.WorkerVersionStampB\x02\x18\x01\x12\x33\n\x08messages\x18\x0b \x03(\x0b\x32!.temporal.api.protocol.v1.Message\x12H\n\x0csdk_metadata\x18\x0c \x01(\x0b\x32\x32.temporal.api.sdk.v1.WorkflowTaskCompletedMetadata\x12\x43\n\x11metering_metadata\x18\r \x01(\x0b\x32(.temporal.api.common.v1.MeteringMetadata\x12g\n\x0c\x63\x61pabilities\x18\x0e \x01(\x0b\x32Q.temporal.api.workflowservice.v1.RespondWorkflowTaskCompletedRequest.Capabilities\x12>\n\ndeployment\x18\x0f \x01(\x0b\x32&.temporal.api.deployment.v1.DeploymentB\x02\x18\x01\x12\x46\n\x13versioning_behavior\x18\x10 \x01(\x0e\x32).temporal.api.enums.v1.VersioningBehavior\x12O\n\x12\x64\x65ployment_options\x18\x11 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentOptions\x12\x1b\n\x13worker_instance_key\x18\x13 \x01(\t\x12!\n\x19worker_control_task_queue\x18\x14 \x01(\t\x12\x13\n\x0bpage_number\x18\x15 \x01(\x05\x12\x19\n\x11intermediate_page\x18\x16 \x01(\x08\x1a_\n\x11QueryResultsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x39\n\x05value\x18\x02 \x01(\x0b\x32*.temporal.api.query.v1.WorkflowQueryResult:\x02\x38\x01\x1a\x45\n\x0c\x43\x61pabilities\x12\x35\n-discard_speculative_workflow_task_with_events\x18\x01 \x01(\x08"\xf5\x01\n$RespondWorkflowTaskCompletedResponse\x12U\n\rworkflow_task\x18\x01 \x01(\x0b\x32>.temporal.api.workflowservice.v1.PollWorkflowTaskQueueResponse\x12V\n\x0e\x61\x63tivity_tasks\x18\x02 \x03(\x0b\x32>.temporal.api.workflowservice.v1.PollActivityTaskQueueResponse\x12\x1e\n\x16reset_history_event_id\x18\x03 \x01(\x03"\x8d\x04\n RespondWorkflowTaskFailedRequest\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12=\n\x05\x63\x61use\x18\x02 \x01(\x0e\x32..temporal.api.enums.v1.WorkflowTaskFailedCause\x12\x31\n\x07\x66\x61ilure\x18\x03 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x10\n\x08identity\x18\x04 \x01(\t\x12\x1b\n\x0f\x62inary_checksum\x18\x05 \x01(\tB\x02\x18\x01\x12\x11\n\tnamespace\x18\x06 \x01(\t\x12\x13\n\x0bresource_id\x18\x0b \x01(\t\x12\x33\n\x08messages\x18\x07 \x03(\x0b\x32!.temporal.api.protocol.v1.Message\x12\x46\n\x0eworker_version\x18\x08 \x01(\x0b\x32*.temporal.api.common.v1.WorkerVersionStampB\x02\x18\x01\x12>\n\ndeployment\x18\t \x01(\x0b\x32&.temporal.api.deployment.v1.DeploymentB\x02\x18\x01\x12O\n\x12\x64\x65ployment_options\x18\n \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentOptions"#\n!RespondWorkflowTaskFailedResponse"\xe6\x03\n\x1cPollActivityTaskQueueRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x38\n\ntask_queue\x18\x02 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12\x17\n\x0fpoller_group_id\x18\n \x01(\t\x12\x10\n\x08identity\x18\x03 \x01(\t\x12\x1b\n\x13worker_instance_key\x18\x08 \x01(\t\x12!\n\x19worker_control_task_queue\x18\t \x01(\t\x12I\n\x13task_queue_metadata\x18\x04 \x01(\x0b\x32,.temporal.api.taskqueue.v1.TaskQueueMetadata\x12Z\n\x1bworker_version_capabilities\x18\x05 \x01(\x0b\x32\x31.temporal.api.common.v1.WorkerVersionCapabilitiesB\x02\x18\x01\x12O\n\x12\x64\x65ployment_options\x18\x06 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentOptionsJ\x04\x08\x07\x10\x08R\x10worker_heartbeat"\x9d\t\n\x1dPollActivityTaskQueueResponse\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12\x1a\n\x12workflow_namespace\x18\x02 \x01(\t\x12;\n\rworkflow_type\x18\x03 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12\x45\n\x12workflow_execution\x18\x04 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12;\n\ractivity_type\x18\x05 \x01(\x0b\x32$.temporal.api.common.v1.ActivityType\x12\x13\n\x0b\x61\x63tivity_id\x18\x06 \x01(\t\x12.\n\x06header\x18\x07 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12/\n\x05input\x18\x08 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12;\n\x11heartbeat_details\x18\t \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x32\n\x0escheduled_time\x18\n \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x42\n\x1e\x63urrent_attempt_scheduled_time\x18\x0b \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x30\n\x0cstarted_time\x18\x0c \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0f\n\x07\x61ttempt\x18\r \x01(\x05\x12<\n\x19schedule_to_close_timeout\x18\x0e \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\x0f \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x34\n\x11heartbeat_timeout\x18\x10 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x0cretry_policy\x18\x11 \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12Q\n\x17poller_scaling_decision\x18\x12 \x01(\x0b\x32\x30.temporal.api.taskqueue.v1.PollerScalingDecision\x12\x32\n\x08priority\x18\x13 \x01(\x0b\x32 .temporal.api.common.v1.Priority\x12\x17\n\x0f\x61\x63tivity_run_id\x18\x14 \x01(\t\x12J\n\x12poller_group_infos\x18\x15 \x03(\x0b\x32*.temporal.api.taskqueue.v1.PollerGroupInfoB\x02\x18\x01\x12G\n\x12poller_groups_info\x18\x16 \x01(\x0b\x32+.temporal.api.taskqueue.v1.PollerGroupsInfo"\xa5\x01\n"RecordActivityTaskHeartbeatRequest\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12\x31\n\x07\x64\x65tails\x18\x02 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x10\n\x08identity\x18\x03 \x01(\t\x12\x11\n\tnamespace\x18\x04 \x01(\t\x12\x13\n\x0bresource_id\x18\x05 \x01(\t"p\n#RecordActivityTaskHeartbeatResponse\x12\x18\n\x10\x63\x61ncel_requested\x18\x01 \x01(\x08\x12\x17\n\x0f\x61\x63tivity_paused\x18\x02 \x01(\x08\x12\x16\n\x0e\x61\x63tivity_reset\x18\x03 \x01(\x08"\xcf\x01\n&RecordActivityTaskHeartbeatByIdRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\t\x12\x13\n\x0b\x61\x63tivity_id\x18\x04 \x01(\t\x12\x31\n\x07\x64\x65tails\x18\x05 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x10\n\x08identity\x18\x06 \x01(\t\x12\x13\n\x0bresource_id\x18\x07 \x01(\t"t\n\'RecordActivityTaskHeartbeatByIdResponse\x12\x18\n\x10\x63\x61ncel_requested\x18\x01 \x01(\x08\x12\x17\n\x0f\x61\x63tivity_paused\x18\x02 \x01(\x08\x12\x16\n\x0e\x61\x63tivity_reset\x18\x03 \x01(\x08"\xfe\x02\n#RespondActivityTaskCompletedRequest\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12\x30\n\x06result\x18\x02 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x10\n\x08identity\x18\x03 \x01(\t\x12\x11\n\tnamespace\x18\x04 \x01(\t\x12\x13\n\x0bresource_id\x18\x08 \x01(\t\x12\x46\n\x0eworker_version\x18\x05 \x01(\x0b\x32*.temporal.api.common.v1.WorkerVersionStampB\x02\x18\x01\x12>\n\ndeployment\x18\x06 \x01(\x0b\x32&.temporal.api.deployment.v1.DeploymentB\x02\x18\x01\x12O\n\x12\x64\x65ployment_options\x18\x07 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentOptions"&\n$RespondActivityTaskCompletedResponse"\xcf\x01\n\'RespondActivityTaskCompletedByIdRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\t\x12\x13\n\x0b\x61\x63tivity_id\x18\x04 \x01(\t\x12\x30\n\x06result\x18\x05 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x10\n\x08identity\x18\x06 \x01(\t\x12\x13\n\x0bresource_id\x18\x07 \x01(\t"*\n(RespondActivityTaskCompletedByIdResponse"\xbe\x03\n RespondActivityTaskFailedRequest\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12\x31\n\x07\x66\x61ilure\x18\x02 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x10\n\x08identity\x18\x03 \x01(\t\x12\x11\n\tnamespace\x18\x04 \x01(\t\x12\x13\n\x0bresource_id\x18\t \x01(\t\x12@\n\x16last_heartbeat_details\x18\x05 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x46\n\x0eworker_version\x18\x06 \x01(\x0b\x32*.temporal.api.common.v1.WorkerVersionStampB\x02\x18\x01\x12>\n\ndeployment\x18\x07 \x01(\x0b\x32&.temporal.api.deployment.v1.DeploymentB\x02\x18\x01\x12O\n\x12\x64\x65ployment_options\x18\x08 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentOptions"W\n!RespondActivityTaskFailedResponse\x12\x32\n\x08\x66\x61ilures\x18\x01 \x03(\x0b\x32 .temporal.api.failure.v1.Failure"\x8f\x02\n$RespondActivityTaskFailedByIdRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\t\x12\x13\n\x0b\x61\x63tivity_id\x18\x04 \x01(\t\x12\x31\n\x07\x66\x61ilure\x18\x05 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x10\n\x08identity\x18\x06 \x01(\t\x12@\n\x16last_heartbeat_details\x18\x07 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x13\n\x0bresource_id\x18\x08 \x01(\t"[\n%RespondActivityTaskFailedByIdResponse\x12\x32\n\x08\x66\x61ilures\x18\x01 \x03(\x0b\x32 .temporal.api.failure.v1.Failure"\xfe\x02\n"RespondActivityTaskCanceledRequest\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12\x31\n\x07\x64\x65tails\x18\x02 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x10\n\x08identity\x18\x03 \x01(\t\x12\x11\n\tnamespace\x18\x04 \x01(\t\x12\x13\n\x0bresource_id\x18\x08 \x01(\t\x12\x46\n\x0eworker_version\x18\x05 \x01(\x0b\x32*.temporal.api.common.v1.WorkerVersionStampB\x02\x18\x01\x12>\n\ndeployment\x18\x06 \x01(\x0b\x32&.temporal.api.deployment.v1.DeploymentB\x02\x18\x01\x12O\n\x12\x64\x65ployment_options\x18\x07 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentOptions"%\n#RespondActivityTaskCanceledResponse"\xa0\x02\n&RespondActivityTaskCanceledByIdRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\t\x12\x13\n\x0b\x61\x63tivity_id\x18\x04 \x01(\t\x12\x31\n\x07\x64\x65tails\x18\x05 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x10\n\x08identity\x18\x06 \x01(\t\x12O\n\x12\x64\x65ployment_options\x18\x07 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentOptions\x12\x13\n\x0bresource_id\x18\x08 \x01(\t")\n\'RespondActivityTaskCanceledByIdResponse"\x84\x02\n%RequestCancelWorkflowExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x45\n\x12workflow_execution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x10\n\x08identity\x18\x03 \x01(\t\x12\x12\n\nrequest_id\x18\x04 \x01(\t\x12\x1e\n\x16\x66irst_execution_run_id\x18\x05 \x01(\t\x12\x0e\n\x06reason\x18\x06 \x01(\t\x12+\n\x05links\x18\x07 \x03(\x0b\x32\x1c.temporal.api.common.v1.Link"(\n&RequestCancelWorkflowExecutionResponse"\xde\x02\n\x1eSignalWorkflowExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x45\n\x12workflow_execution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x13\n\x0bsignal_name\x18\x03 \x01(\t\x12/\n\x05input\x18\x04 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x10\n\x08identity\x18\x05 \x01(\t\x12\x12\n\nrequest_id\x18\x06 \x01(\t\x12\x13\n\x07\x63ontrol\x18\x07 \x01(\tB\x02\x18\x01\x12.\n\x06header\x18\x08 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12+\n\x05links\x18\n \x03(\x0b\x32\x1c.temporal.api.common.v1.LinkJ\x04\x08\t\x10\n"M\n\x1fSignalWorkflowExecutionResponse\x12*\n\x04link\x18\x01 \x01(\x0b\x32\x1c.temporal.api.common.v1.Link"\xbb\n\n\'SignalWithStartWorkflowExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12;\n\rworkflow_type\x18\x03 \x01(\x0b\x32$.temporal.api.common.v1.WorkflowType\x12\x38\n\ntask_queue\x18\x04 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12/\n\x05input\x18\x05 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12=\n\x1aworkflow_execution_timeout\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x37\n\x14workflow_run_timeout\x18\x07 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x10\n\x08identity\x18\t \x01(\t\x12\x12\n\nrequest_id\x18\n \x01(\t\x12N\n\x18workflow_id_reuse_policy\x18\x0b \x01(\x0e\x32,.temporal.api.enums.v1.WorkflowIdReusePolicy\x12T\n\x1bworkflow_id_conflict_policy\x18\x16 \x01(\x0e\x32/.temporal.api.enums.v1.WorkflowIdConflictPolicy\x12\x13\n\x0bsignal_name\x18\x0c \x01(\t\x12\x36\n\x0csignal_input\x18\r \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x13\n\x07\x63ontrol\x18\x0e \x01(\tB\x02\x18\x01\x12\x39\n\x0cretry_policy\x18\x0f \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x15\n\rcron_schedule\x18\x10 \x01(\t\x12*\n\x04memo\x18\x11 \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo\x12\x43\n\x11search_attributes\x18\x12 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12.\n\x06header\x18\x13 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12\x37\n\x14workflow_start_delay\x18\x14 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\ruser_metadata\x18\x17 \x01(\x0b\x32!.temporal.api.sdk.v1.UserMetadata\x12+\n\x05links\x18\x18 \x03(\x0b\x32\x1c.temporal.api.common.v1.Link\x12I\n\x13versioning_override\x18\x19 \x01(\x0b\x32,.temporal.api.workflow.v1.VersioningOverride\x12\x32\n\x08priority\x18\x1a \x01(\x0b\x32 .temporal.api.common.v1.Priority\x12H\n\x14time_skipping_config\x18\x1b \x01(\x0b\x32*.temporal.api.common.v1.TimeSkippingConfigJ\x04\x08\x15\x10\x16"\x9e\x01\n(SignalWithStartWorkflowExecutionResponse\x12\x0e\n\x06run_id\x18\x01 \x01(\t\x12\x1e\n\x16\x66irst_execution_run_id\x18\x04 \x01(\t\x12\x0f\n\x07started\x18\x02 \x01(\x08\x12\x31\n\x0bsignal_link\x18\x03 \x01(\x0b\x32\x1c.temporal.api.common.v1.Link"\xc1\x03\n\x1dResetWorkflowExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x45\n\x12workflow_execution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x0e\n\x06reason\x18\x03 \x01(\t\x12%\n\x1dworkflow_task_finish_event_id\x18\x04 \x01(\x03\x12\x12\n\nrequest_id\x18\x05 \x01(\t\x12G\n\x12reset_reapply_type\x18\x06 \x01(\x0e\x32\'.temporal.api.enums.v1.ResetReapplyTypeB\x02\x18\x01\x12S\n\x1breset_reapply_exclude_types\x18\x07 \x03(\x0e\x32..temporal.api.enums.v1.ResetReapplyExcludeType\x12K\n\x15post_reset_operations\x18\x08 \x03(\x0b\x32,.temporal.api.workflow.v1.PostResetOperation\x12\x10\n\x08identity\x18\t \x01(\t"0\n\x1eResetWorkflowExecutionResponse\x12\x0e\n\x06run_id\x18\x01 \x01(\t"\x9f\x02\n!TerminateWorkflowExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x45\n\x12workflow_execution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x0e\n\x06reason\x18\x03 \x01(\t\x12\x31\n\x07\x64\x65tails\x18\x04 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x10\n\x08identity\x18\x05 \x01(\t\x12\x1e\n\x16\x66irst_execution_run_id\x18\x06 \x01(\t\x12+\n\x05links\x18\x07 \x03(\x0b\x32\x1c.temporal.api.common.v1.Link"$\n"TerminateWorkflowExecutionResponse"z\n\x1e\x44\x65leteWorkflowExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x45\n\x12workflow_execution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution"!\n\x1f\x44\x65leteWorkflowExecutionResponse"\xc9\x02\n!ListOpenWorkflowExecutionsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x19\n\x11maximum_page_size\x18\x02 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x03 \x01(\x0c\x12\x42\n\x11start_time_filter\x18\x04 \x01(\x0b\x32\'.temporal.api.filter.v1.StartTimeFilter\x12K\n\x10\x65xecution_filter\x18\x05 \x01(\x0b\x32/.temporal.api.filter.v1.WorkflowExecutionFilterH\x00\x12\x41\n\x0btype_filter\x18\x06 \x01(\x0b\x32*.temporal.api.filter.v1.WorkflowTypeFilterH\x00\x42\t\n\x07\x66ilters"\x82\x01\n"ListOpenWorkflowExecutionsResponse\x12\x43\n\nexecutions\x18\x01 \x03(\x0b\x32/.temporal.api.workflow.v1.WorkflowExecutionInfo\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\x0c"\x8a\x03\n#ListClosedWorkflowExecutionsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x19\n\x11maximum_page_size\x18\x02 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x03 \x01(\x0c\x12\x42\n\x11start_time_filter\x18\x04 \x01(\x0b\x32\'.temporal.api.filter.v1.StartTimeFilter\x12K\n\x10\x65xecution_filter\x18\x05 \x01(\x0b\x32/.temporal.api.filter.v1.WorkflowExecutionFilterH\x00\x12\x41\n\x0btype_filter\x18\x06 \x01(\x0b\x32*.temporal.api.filter.v1.WorkflowTypeFilterH\x00\x12=\n\rstatus_filter\x18\x07 \x01(\x0b\x32$.temporal.api.filter.v1.StatusFilterH\x00\x42\t\n\x07\x66ilters"\x84\x01\n$ListClosedWorkflowExecutionsResponse\x12\x43\n\nexecutions\x18\x01 \x03(\x0b\x32/.temporal.api.workflow.v1.WorkflowExecutionInfo\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\x0c"m\n\x1dListWorkflowExecutionsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x03 \x01(\x0c\x12\r\n\x05query\x18\x04 \x01(\t"~\n\x1eListWorkflowExecutionsResponse\x12\x43\n\nexecutions\x18\x01 \x03(\x0b\x32/.temporal.api.workflow.v1.WorkflowExecutionInfo\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\x0c"u\n%ListArchivedWorkflowExecutionsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x03 \x01(\x0c\x12\r\n\x05query\x18\x04 \x01(\t"\x86\x01\n&ListArchivedWorkflowExecutionsResponse\x12\x43\n\nexecutions\x18\x01 \x03(\x0b\x32/.temporal.api.workflow.v1.WorkflowExecutionInfo\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\x0c"m\n\x1dScanWorkflowExecutionsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x03 \x01(\x0c\x12\r\n\x05query\x18\x04 \x01(\t"~\n\x1eScanWorkflowExecutionsResponse\x12\x43\n\nexecutions\x18\x01 \x03(\x0b\x32/.temporal.api.workflow.v1.WorkflowExecutionInfo\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\x0c"B\n\x1e\x43ountWorkflowExecutionsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\r\n\x05query\x18\x02 \x01(\t"\xed\x01\n\x1f\x43ountWorkflowExecutionsResponse\x12\r\n\x05\x63ount\x18\x01 \x01(\x03\x12\x61\n\x06groups\x18\x02 \x03(\x0b\x32Q.temporal.api.workflowservice.v1.CountWorkflowExecutionsResponse.AggregationGroup\x1aX\n\x10\x41ggregationGroup\x12\x35\n\x0cgroup_values\x18\x01 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\r\n\x05\x63ount\x18\x02 \x01(\x03"\x1c\n\x1aGetSearchAttributesRequest"\xc9\x01\n\x1bGetSearchAttributesResponse\x12T\n\x04keys\x18\x01 \x03(\x0b\x32\x46.temporal.api.workflowservice.v1.GetSearchAttributesResponse.KeysEntry\x1aT\n\tKeysEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0e\x32\'.temporal.api.enums.v1.IndexedValueType:\x02\x38\x01"\xe9\x02\n RespondQueryTaskCompletedRequest\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12>\n\x0e\x63ompleted_type\x18\x02 \x01(\x0e\x32&.temporal.api.enums.v1.QueryResultType\x12\x36\n\x0cquery_result\x18\x03 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x15\n\rerror_message\x18\x04 \x01(\t\x12\x11\n\tnamespace\x18\x06 \x01(\t\x12\x31\n\x07\x66\x61ilure\x18\x07 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12=\n\x05\x63\x61use\x18\x08 \x01(\x0e\x32..temporal.api.enums.v1.WorkflowTaskFailedCause\x12\x17\n\x0fpoller_group_id\x18\t \x01(\tJ\x04\x08\x05\x10\x06"#\n!RespondQueryTaskCompletedResponse"n\n\x1bResetStickyTaskQueueRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12<\n\texecution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution"\x1e\n\x1cResetStickyTaskQueueResponse"\x9b\x02\n\x15ShutdownWorkerRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x19\n\x11sticky_task_queue\x18\x02 \x01(\t\x12\x10\n\x08identity\x18\x03 \x01(\t\x12\x0e\n\x06reason\x18\x04 \x01(\t\x12\x41\n\x10worker_heartbeat\x18\x05 \x01(\x0b\x32\'.temporal.api.worker.v1.WorkerHeartbeat\x12\x1b\n\x13worker_instance_key\x18\x06 \x01(\t\x12\x12\n\ntask_queue\x18\x07 \x01(\t\x12>\n\x10task_queue_types\x18\x08 \x03(\x0e\x32$.temporal.api.enums.v1.TaskQueueType"\x18\n\x16ShutdownWorkerResponse"\xe9\x01\n\x14QueryWorkflowRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12<\n\texecution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x33\n\x05query\x18\x03 \x01(\x0b\x32$.temporal.api.query.v1.WorkflowQuery\x12K\n\x16query_reject_condition\x18\x04 \x01(\x0e\x32+.temporal.api.enums.v1.QueryRejectCondition"\xb9\x01\n\x15QueryWorkflowResponse\x12\x36\n\x0cquery_result\x18\x01 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12<\n\x0equery_rejected\x18\x02 \x01(\x0b\x32$.temporal.api.query.v1.QueryRejected\x12*\n\x04link\x18\x03 \x01(\x0b\x32\x1c.temporal.api.common.v1.Link"s\n DescribeWorkflowExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12<\n\texecution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution"\x99\x05\n!DescribeWorkflowExecutionResponse\x12K\n\x10\x65xecution_config\x18\x01 \x01(\x0b\x32\x31.temporal.api.workflow.v1.WorkflowExecutionConfig\x12P\n\x17workflow_execution_info\x18\x02 \x01(\x0b\x32/.temporal.api.workflow.v1.WorkflowExecutionInfo\x12I\n\x12pending_activities\x18\x03 \x03(\x0b\x32-.temporal.api.workflow.v1.PendingActivityInfo\x12M\n\x10pending_children\x18\x04 \x03(\x0b\x32\x33.temporal.api.workflow.v1.PendingChildExecutionInfo\x12P\n\x15pending_workflow_task\x18\x05 \x01(\x0b\x32\x31.temporal.api.workflow.v1.PendingWorkflowTaskInfo\x12\x39\n\tcallbacks\x18\x06 \x03(\x0b\x32&.temporal.api.workflow.v1.CallbackInfo\x12U\n\x18pending_nexus_operations\x18\x07 \x03(\x0b\x32\x33.temporal.api.workflow.v1.PendingNexusOperationInfo\x12W\n\x16workflow_extended_info\x18\x08 \x01(\x0b\x32\x37.temporal.api.workflow.v1.WorkflowExecutionExtendedInfo"\x90\x04\n\x18\x44\x65scribeTaskQueueRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x38\n\ntask_queue\x18\x02 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12=\n\x0ftask_queue_type\x18\x03 \x01(\x0e\x32$.temporal.api.enums.v1.TaskQueueType\x12\x14\n\x0creport_stats\x18\x08 \x01(\x08\x12\x15\n\rreport_config\x18\x0b \x01(\x08\x12%\n\x19include_task_queue_status\x18\x04 \x01(\x08\x42\x02\x18\x01\x12\x42\n\x08\x61pi_mode\x18\x05 \x01(\x0e\x32,.temporal.api.enums.v1.DescribeTaskQueueModeB\x02\x18\x01\x12J\n\x08versions\x18\x06 \x01(\x0b\x32\x34.temporal.api.taskqueue.v1.TaskQueueVersionSelectionB\x02\x18\x01\x12\x42\n\x10task_queue_types\x18\x07 \x03(\x0e\x32$.temporal.api.enums.v1.TaskQueueTypeB\x02\x18\x01\x12\x1a\n\x0ereport_pollers\x18\t \x01(\x08\x42\x02\x18\x01\x12$\n\x18report_task_reachability\x18\n \x01(\x08\x42\x02\x18\x01"\xec\x07\n\x19\x44\x65scribeTaskQueueResponse\x12\x36\n\x07pollers\x18\x01 \x03(\x0b\x32%.temporal.api.taskqueue.v1.PollerInfo\x12\x38\n\x05stats\x18\x05 \x01(\x0b\x32).temporal.api.taskqueue.v1.TaskQueueStats\x12q\n\x15stats_by_priority_key\x18\x08 \x03(\x0b\x32R.temporal.api.workflowservice.v1.DescribeTaskQueueResponse.StatsByPriorityKeyEntry\x12K\n\x0fversioning_info\x18\x04 \x01(\x0b\x32\x32.temporal.api.taskqueue.v1.TaskQueueVersioningInfo\x12:\n\x06\x63onfig\x18\x06 \x01(\x0b\x32*.temporal.api.taskqueue.v1.TaskQueueConfig\x12k\n\x14\x65\x66\x66\x65\x63tive_rate_limit\x18\x07 \x01(\x0b\x32M.temporal.api.workflowservice.v1.DescribeTaskQueueResponse.EffectiveRateLimit\x12I\n\x11task_queue_status\x18\x02 \x01(\x0b\x32*.temporal.api.taskqueue.v1.TaskQueueStatusB\x02\x18\x01\x12g\n\rversions_info\x18\x03 \x03(\x0b\x32L.temporal.api.workflowservice.v1.DescribeTaskQueueResponse.VersionsInfoEntryB\x02\x18\x01\x1a\x64\n\x17StatsByPriorityKeyEntry\x12\x0b\n\x03key\x18\x01 \x01(\x05\x12\x38\n\x05value\x18\x02 \x01(\x0b\x32).temporal.api.taskqueue.v1.TaskQueueStats:\x02\x38\x01\x1at\n\x12\x45\x66\x66\x65\x63tiveRateLimit\x12\x1b\n\x13requests_per_second\x18\x01 \x01(\x02\x12\x41\n\x11rate_limit_source\x18\x02 \x01(\x0e\x32&.temporal.api.enums.v1.RateLimitSource\x1a\x64\n\x11VersionsInfoEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12>\n\x05value\x18\x02 \x01(\x0b\x32/.temporal.api.taskqueue.v1.TaskQueueVersionInfo:\x02\x38\x01"\x17\n\x15GetClusterInfoRequest"\xd1\x03\n\x16GetClusterInfoResponse\x12h\n\x11supported_clients\x18\x01 \x03(\x0b\x32M.temporal.api.workflowservice.v1.GetClusterInfoResponse.SupportedClientsEntry\x12\x16\n\x0eserver_version\x18\x02 \x01(\t\x12\x12\n\ncluster_id\x18\x03 \x01(\t\x12:\n\x0cversion_info\x18\x04 \x01(\x0b\x32$.temporal.api.version.v1.VersionInfo\x12\x14\n\x0c\x63luster_name\x18\x05 \x01(\t\x12\x1b\n\x13history_shard_count\x18\x06 \x01(\x05\x12\x19\n\x11persistence_store\x18\x07 \x01(\t\x12\x18\n\x10visibility_store\x18\x08 \x01(\t\x12 \n\x18initial_failover_version\x18\t \x01(\x03\x12"\n\x1a\x66\x61ilover_version_increment\x18\n \x01(\x03\x1a\x37\n\x15SupportedClientsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"\x16\n\x14GetSystemInfoRequest"\xc1\x04\n\x15GetSystemInfoResponse\x12\x16\n\x0eserver_version\x18\x01 \x01(\t\x12Y\n\x0c\x63\x61pabilities\x18\x02 \x01(\x0b\x32\x43.temporal.api.workflowservice.v1.GetSystemInfoResponse.Capabilities\x1a\xb4\x03\n\x0c\x43\x61pabilities\x12\x1f\n\x17signal_and_query_header\x18\x01 \x01(\x08\x12&\n\x1einternal_error_differentiation\x18\x02 \x01(\x08\x12*\n"activity_failure_include_heartbeat\x18\x03 \x01(\x08\x12\x1a\n\x12supports_schedules\x18\x04 \x01(\x08\x12"\n\x1a\x65ncoded_failure_attributes\x18\x05 \x01(\x08\x12!\n\x19\x62uild_id_based_versioning\x18\x06 \x01(\x08\x12\x13\n\x0bupsert_memo\x18\x07 \x01(\x08\x12\x1c\n\x14\x65\x61ger_workflow_start\x18\x08 \x01(\x08\x12\x14\n\x0csdk_metadata\x18\t \x01(\x08\x12\'\n\x1f\x63ount_group_by_execution_status\x18\n \x01(\x08\x12\r\n\x05nexus\x18\x0b \x01(\x08\x12!\n\x19server_scaled_deployments\x18\x0c \x01(\x08\x12(\n server_scaled_provider_cloud_run\x18\r \x01(\x08"m\n\x1eListTaskQueuePartitionsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x38\n\ntask_queue\x18\x02 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue"\xdf\x01\n\x1fListTaskQueuePartitionsResponse\x12]\n\x1e\x61\x63tivity_task_queue_partitions\x18\x01 \x03(\x0b\x32\x35.temporal.api.taskqueue.v1.TaskQueuePartitionMetadata\x12]\n\x1eworkflow_task_queue_partitions\x18\x02 \x03(\x0b\x32\x35.temporal.api.taskqueue.v1.TaskQueuePartitionMetadata"\xcc\x02\n\x15\x43reateScheduleRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bschedule_id\x18\x02 \x01(\t\x12\x34\n\x08schedule\x18\x03 \x01(\x0b\x32".temporal.api.schedule.v1.Schedule\x12>\n\rinitial_patch\x18\x04 \x01(\x0b\x32\'.temporal.api.schedule.v1.SchedulePatch\x12\x10\n\x08identity\x18\x05 \x01(\t\x12\x12\n\nrequest_id\x18\x06 \x01(\t\x12*\n\x04memo\x18\x07 \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo\x12\x43\n\x11search_attributes\x18\x08 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes"0\n\x16\x43reateScheduleResponse\x12\x16\n\x0e\x63onflict_token\x18\x01 \x01(\x0c"A\n\x17\x44\x65scribeScheduleRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bschedule_id\x18\x02 \x01(\t"\x8f\x02\n\x18\x44\x65scribeScheduleResponse\x12\x34\n\x08schedule\x18\x01 \x01(\x0b\x32".temporal.api.schedule.v1.Schedule\x12\x34\n\x04info\x18\x02 \x01(\x0b\x32&.temporal.api.schedule.v1.ScheduleInfo\x12*\n\x04memo\x18\x03 \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo\x12\x43\n\x11search_attributes\x18\x04 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12\x16\n\x0e\x63onflict_token\x18\x05 \x01(\x0c"\xa4\x02\n\x15UpdateScheduleRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bschedule_id\x18\x02 \x01(\t\x12\x34\n\x08schedule\x18\x03 \x01(\x0b\x32".temporal.api.schedule.v1.Schedule\x12\x16\n\x0e\x63onflict_token\x18\x04 \x01(\x0c\x12\x10\n\x08identity\x18\x05 \x01(\t\x12\x12\n\nrequest_id\x18\x06 \x01(\t\x12\x43\n\x11search_attributes\x18\x07 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12*\n\x04memo\x18\x08 \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo"\x18\n\x16UpdateScheduleResponse"\x9c\x01\n\x14PatchScheduleRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bschedule_id\x18\x02 \x01(\t\x12\x36\n\x05patch\x18\x03 \x01(\x0b\x32\'.temporal.api.schedule.v1.SchedulePatch\x12\x10\n\x08identity\x18\x04 \x01(\t\x12\x12\n\nrequest_id\x18\x05 \x01(\t"\x17\n\x15PatchScheduleResponse"\xa8\x01\n ListScheduleMatchingTimesRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bschedule_id\x18\x02 \x01(\t\x12.\n\nstart_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12,\n\x08\x65nd_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"S\n!ListScheduleMatchingTimesResponse\x12.\n\nstart_time\x18\x01 \x03(\x0b\x32\x1a.google.protobuf.Timestamp"Q\n\x15\x44\x65leteScheduleRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bschedule_id\x18\x02 \x01(\t\x12\x10\n\x08identity\x18\x03 \x01(\t"\x18\n\x16\x44\x65leteScheduleResponse"l\n\x14ListSchedulesRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x19\n\x11maximum_page_size\x18\x02 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x03 \x01(\x0c\x12\r\n\x05query\x18\x04 \x01(\t"p\n\x15ListSchedulesResponse\x12>\n\tschedules\x18\x01 \x03(\x0b\x32+.temporal.api.schedule.v1.ScheduleListEntry\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\x0c"9\n\x15\x43ountSchedulesRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\r\n\x05query\x18\x02 \x01(\t"\xdb\x01\n\x16\x43ountSchedulesResponse\x12\r\n\x05\x63ount\x18\x01 \x01(\x03\x12X\n\x06groups\x18\x02 \x03(\x0b\x32H.temporal.api.workflowservice.v1.CountSchedulesResponse.AggregationGroup\x1aX\n\x10\x41ggregationGroup\x12\x35\n\x0cgroup_values\x18\x01 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\r\n\x05\x63ount\x18\x02 \x01(\x03"\x86\x05\n\'UpdateWorkerBuildIdCompatibilityRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x12\n\ntask_queue\x18\x02 \x01(\t\x12-\n#add_new_build_id_in_new_default_set\x18\x03 \x01(\tH\x00\x12\x87\x01\n\x1b\x61\x64\x64_new_compatible_build_id\x18\x04 \x01(\x0b\x32`.temporal.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest.AddNewCompatibleVersionH\x00\x12!\n\x17promote_set_by_build_id\x18\x05 \x01(\tH\x00\x12%\n\x1bpromote_build_id_within_set\x18\x06 \x01(\tH\x00\x12h\n\nmerge_sets\x18\x07 \x01(\x0b\x32R.temporal.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest.MergeSetsH\x00\x1ao\n\x17\x41\x64\x64NewCompatibleVersion\x12\x14\n\x0cnew_build_id\x18\x01 \x01(\t\x12$\n\x1c\x65xisting_compatible_build_id\x18\x02 \x01(\t\x12\x18\n\x10make_set_default\x18\x03 \x01(\x08\x1aI\n\tMergeSets\x12\x1c\n\x14primary_set_build_id\x18\x01 \x01(\t\x12\x1e\n\x16secondary_set_build_id\x18\x02 \x01(\tB\x0b\n\toperation"@\n(UpdateWorkerBuildIdCompatibilityResponseJ\x04\x08\x01\x10\x02R\x0eversion_set_id"_\n$GetWorkerBuildIdCompatibilityRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x12\n\ntask_queue\x18\x02 \x01(\t\x12\x10\n\x08max_sets\x18\x03 \x01(\x05"t\n%GetWorkerBuildIdCompatibilityResponse\x12K\n\x12major_version_sets\x18\x01 \x03(\x0b\x32/.temporal.api.taskqueue.v1.CompatibleVersionSet"\xb5\r\n"UpdateWorkerVersioningRulesRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x12\n\ntask_queue\x18\x02 \x01(\t\x12\x16\n\x0e\x63onflict_token\x18\x03 \x01(\x0c\x12\x81\x01\n\x16insert_assignment_rule\x18\x04 \x01(\x0b\x32_.temporal.api.workflowservice.v1.UpdateWorkerVersioningRulesRequest.InsertBuildIdAssignmentRuleH\x00\x12\x83\x01\n\x17replace_assignment_rule\x18\x05 \x01(\x0b\x32`.temporal.api.workflowservice.v1.UpdateWorkerVersioningRulesRequest.ReplaceBuildIdAssignmentRuleH\x00\x12\x81\x01\n\x16\x64\x65lete_assignment_rule\x18\x06 \x01(\x0b\x32_.temporal.api.workflowservice.v1.UpdateWorkerVersioningRulesRequest.DeleteBuildIdAssignmentRuleH\x00\x12\x8c\x01\n\x1c\x61\x64\x64_compatible_redirect_rule\x18\x07 \x01(\x0b\x32\x64.temporal.api.workflowservice.v1.UpdateWorkerVersioningRulesRequest.AddCompatibleBuildIdRedirectRuleH\x00\x12\x94\x01\n replace_compatible_redirect_rule\x18\x08 \x01(\x0b\x32h.temporal.api.workflowservice.v1.UpdateWorkerVersioningRulesRequest.ReplaceCompatibleBuildIdRedirectRuleH\x00\x12\x92\x01\n\x1f\x64\x65lete_compatible_redirect_rule\x18\t \x01(\x0b\x32g.temporal.api.workflowservice.v1.UpdateWorkerVersioningRulesRequest.DeleteCompatibleBuildIdRedirectRuleH\x00\x12l\n\x0f\x63ommit_build_id\x18\n \x01(\x0b\x32Q.temporal.api.workflowservice.v1.UpdateWorkerVersioningRulesRequest.CommitBuildIdH\x00\x1aq\n\x1bInsertBuildIdAssignmentRule\x12\x12\n\nrule_index\x18\x01 \x01(\x05\x12>\n\x04rule\x18\x02 \x01(\x0b\x32\x30.temporal.api.taskqueue.v1.BuildIdAssignmentRule\x1a\x81\x01\n\x1cReplaceBuildIdAssignmentRule\x12\x12\n\nrule_index\x18\x01 \x01(\x05\x12>\n\x04rule\x18\x02 \x01(\x0b\x32\x30.temporal.api.taskqueue.v1.BuildIdAssignmentRule\x12\r\n\x05\x66orce\x18\x03 \x01(\x08\x1a@\n\x1b\x44\x65leteBuildIdAssignmentRule\x12\x12\n\nrule_index\x18\x01 \x01(\x05\x12\r\n\x05\x66orce\x18\x02 \x01(\x08\x1aj\n AddCompatibleBuildIdRedirectRule\x12\x46\n\x04rule\x18\x01 \x01(\x0b\x32\x38.temporal.api.taskqueue.v1.CompatibleBuildIdRedirectRule\x1an\n$ReplaceCompatibleBuildIdRedirectRule\x12\x46\n\x04rule\x18\x01 \x01(\x0b\x32\x38.temporal.api.taskqueue.v1.CompatibleBuildIdRedirectRule\x1a>\n#DeleteCompatibleBuildIdRedirectRule\x12\x17\n\x0fsource_build_id\x18\x01 \x01(\t\x1a\x37\n\rCommitBuildId\x12\x17\n\x0ftarget_build_id\x18\x01 \x01(\t\x12\r\n\x05\x66orce\x18\x02 \x01(\x08\x42\x0b\n\toperation"\xfc\x01\n#UpdateWorkerVersioningRulesResponse\x12U\n\x10\x61ssignment_rules\x18\x01 \x03(\x0b\x32;.temporal.api.taskqueue.v1.TimestampedBuildIdAssignmentRule\x12\x66\n\x19\x63ompatible_redirect_rules\x18\x02 \x03(\x0b\x32\x43.temporal.api.taskqueue.v1.TimestampedCompatibleBuildIdRedirectRule\x12\x16\n\x0e\x63onflict_token\x18\x03 \x01(\x0c"H\n\x1fGetWorkerVersioningRulesRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x12\n\ntask_queue\x18\x02 \x01(\t"\xf9\x01\n GetWorkerVersioningRulesResponse\x12U\n\x10\x61ssignment_rules\x18\x01 \x03(\x0b\x32;.temporal.api.taskqueue.v1.TimestampedBuildIdAssignmentRule\x12\x66\n\x19\x63ompatible_redirect_rules\x18\x02 \x03(\x0b\x32\x43.temporal.api.taskqueue.v1.TimestampedCompatibleBuildIdRedirectRule\x12\x16\n\x0e\x63onflict_token\x18\x03 \x01(\x0c"\x9c\x01\n GetWorkerTaskReachabilityRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tbuild_ids\x18\x02 \x03(\t\x12\x13\n\x0btask_queues\x18\x03 \x03(\t\x12=\n\x0creachability\x18\x04 \x01(\x0e\x32\'.temporal.api.enums.v1.TaskReachability"r\n!GetWorkerTaskReachabilityResponse\x12M\n\x15\x62uild_id_reachability\x18\x01 \x03(\x0b\x32..temporal.api.taskqueue.v1.BuildIdReachability"\x85\x02\n\x1eUpdateWorkflowExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x45\n\x12workflow_execution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x1e\n\x16\x66irst_execution_run_id\x18\x03 \x01(\t\x12\x37\n\x0bwait_policy\x18\x04 \x01(\x0b\x32".temporal.api.update.v1.WaitPolicy\x12\x30\n\x07request\x18\x05 \x01(\x0b\x32\x1f.temporal.api.update.v1.Request"\x83\x02\n\x1fUpdateWorkflowExecutionResponse\x12\x35\n\nupdate_ref\x18\x01 \x01(\x0b\x32!.temporal.api.update.v1.UpdateRef\x12\x30\n\x07outcome\x18\x02 \x01(\x0b\x32\x1f.temporal.api.update.v1.Outcome\x12K\n\x05stage\x18\x03 \x01(\x0e\x32<.temporal.api.enums.v1.UpdateWorkflowExecutionLifecycleStage\x12*\n\x04link\x18\x04 \x01(\x0b\x32\x1c.temporal.api.common.v1.Link"\xd6\n\n\x1aStartBatchOperationRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x18\n\x10visibility_query\x18\x02 \x01(\t\x12\x0e\n\x06job_id\x18\x03 \x01(\t\x12\x0e\n\x06reason\x18\x04 \x01(\t\x12\x41\n\nexecutions\x18\x05 \x03(\x0b\x32).temporal.api.common.v1.WorkflowExecutionB\x02\x18\x01\x12<\n\x11target_executions\x18\x16 \x03(\x0b\x32!.temporal.api.common.v1.Execution\x12!\n\x19max_operations_per_second\x18\x06 \x01(\x02\x12Q\n\x15termination_operation\x18\n \x01(\x0b\x32\x30.temporal.api.batch.v1.BatchOperationTerminationH\x00\x12G\n\x10signal_operation\x18\x0b \x01(\x0b\x32+.temporal.api.batch.v1.BatchOperationSignalH\x00\x12S\n\x16\x63\x61ncellation_operation\x18\x0c \x01(\x0b\x32\x31.temporal.api.batch.v1.BatchOperationCancellationH\x00\x12K\n\x12\x64\x65letion_operation\x18\r \x01(\x0b\x32-.temporal.api.batch.v1.BatchOperationDeletionH\x00\x12\x45\n\x0freset_operation\x18\x0e \x01(\x0b\x32*.temporal.api.batch.v1.BatchOperationResetH\x00\x12p\n!update_workflow_options_operation\x18\x0f \x01(\x0b\x32\x43.temporal.api.batch.v1.BatchOperationUpdateWorkflowExecutionOptionsH\x00\x12^\n\x1cunpause_activities_operation\x18\x10 \x01(\x0b\x32\x36.temporal.api.batch.v1.BatchOperationUnpauseActivitiesH\x00\x12Z\n\x1areset_activities_operation\x18\x11 \x01(\x0b\x32\x34.temporal.api.batch.v1.BatchOperationResetActivitiesH\x00\x12g\n!update_activity_options_operation\x18\x12 \x01(\x0b\x32:.temporal.api.batch.v1.BatchOperationUpdateActivityOptionsH\x00\x12\\\n\x1b\x63\x61ncel_activities_operation\x18\x13 \x01(\x0b\x32\x35.temporal.api.batch.v1.BatchOperationCancelActivitiesH\x00\x12\x62\n\x1eterminate_activities_operation\x18\x14 \x01(\x0b\x32\x38.temporal.api.batch.v1.BatchOperationTerminateActivitiesH\x00\x12\\\n\x1b\x64\x65lete_activities_operation\x18\x15 \x01(\x0b\x32\x35.temporal.api.batch.v1.BatchOperationDeleteActivitiesH\x00\x42\x0b\n\toperation"\x1d\n\x1bStartBatchOperationResponse"`\n\x19StopBatchOperationRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x0e\n\x06job_id\x18\x02 \x01(\t\x12\x0e\n\x06reason\x18\x03 \x01(\t\x12\x10\n\x08identity\x18\x04 \x01(\t"\x1c\n\x1aStopBatchOperationResponse"B\n\x1d\x44\x65scribeBatchOperationRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x0e\n\x06job_id\x18\x02 \x01(\t"\xd8\x03\n\x1e\x44\x65scribeBatchOperationResponse\x12\x41\n\x0eoperation_type\x18\x01 \x01(\x0e\x32).temporal.api.enums.v1.BatchOperationType\x12\x0e\n\x06job_id\x18\x02 \x01(\t\x12\x39\n\x05state\x18\x03 \x01(\x0e\x32*.temporal.api.enums.v1.BatchOperationState\x12.\n\nstart_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12.\n\nclose_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x1d\n\x15total_operation_count\x18\x06 \x01(\x03\x12 \n\x18\x63omplete_operation_count\x18\x07 \x01(\x03\x12\x1f\n\x17\x66\x61ilure_operation_count\x18\x08 \x01(\x03\x12\x10\n\x08identity\x18\t \x01(\t\x12\x0e\n\x06reason\x18\n \x01(\t\x12\r\n\x05query\x18\x0b \x01(\t\x12\x35\n\nexecutions\x18\x0c \x03(\x0b\x32!.temporal.api.common.v1.Execution"[\n\x1aListBatchOperationsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x03 \x01(\x0c"y\n\x1bListBatchOperationsResponse\x12\x41\n\x0eoperation_info\x18\x01 \x03(\x0b\x32).temporal.api.batch.v1.BatchOperationInfo\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\x0c"\xb9\x01\n"PollWorkflowExecutionUpdateRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x35\n\nupdate_ref\x18\x02 \x01(\x0b\x32!.temporal.api.update.v1.UpdateRef\x12\x10\n\x08identity\x18\x03 \x01(\t\x12\x37\n\x0bwait_policy\x18\x04 \x01(\x0b\x32".temporal.api.update.v1.WaitPolicy"\xdb\x01\n#PollWorkflowExecutionUpdateResponse\x12\x30\n\x07outcome\x18\x01 \x01(\x0b\x32\x1f.temporal.api.update.v1.Outcome\x12K\n\x05stage\x18\x02 \x01(\x0e\x32<.temporal.api.enums.v1.UpdateWorkflowExecutionLifecycleStage\x12\x35\n\nupdate_ref\x18\x03 \x01(\x0b\x32!.temporal.api.update.v1.UpdateRef"\xa0\x03\n\x19PollNexusTaskQueueRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x38\n\ntask_queue\x18\x03 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12\x17\n\x0fpoller_group_id\x18\t \x01(\t\x12\x10\n\x08identity\x18\x02 \x01(\t\x12\x1b\n\x13worker_instance_key\x18\x08 \x01(\t\x12Z\n\x1bworker_version_capabilities\x18\x04 \x01(\x0b\x32\x31.temporal.api.common.v1.WorkerVersionCapabilitiesB\x02\x18\x01\x12O\n\x12\x64\x65ployment_options\x18\x06 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentOptions\x12\x41\n\x10worker_heartbeat\x18\x07 \x03(\x0b\x32\'.temporal.api.worker.v1.WorkerHeartbeat"\xe2\x02\n\x1aPollNexusTaskQueueResponse\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12/\n\x07request\x18\x02 \x01(\x0b\x32\x1e.temporal.api.nexus.v1.Request\x12Q\n\x17poller_scaling_decision\x18\x03 \x01(\x0b\x32\x30.temporal.api.taskqueue.v1.PollerScalingDecision\x12\x17\n\x0fpoller_group_id\x18\x04 \x01(\t\x12J\n\x12poller_group_infos\x18\x05 \x03(\x0b\x32*.temporal.api.taskqueue.v1.PollerGroupInfoB\x02\x18\x01\x12G\n\x12poller_groups_info\x18\x06 \x01(\x0b\x32+.temporal.api.taskqueue.v1.PollerGroupsInfo"\xa7\x01\n RespondNexusTaskCompletedRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x10\n\x08identity\x18\x02 \x01(\t\x12\x12\n\ntask_token\x18\x03 \x01(\x0c\x12\x31\n\x08response\x18\x04 \x01(\x0b\x32\x1f.temporal.api.nexus.v1.Response\x12\x17\n\x0fpoller_group_id\x18\x05 \x01(\t"#\n!RespondNexusTaskCompletedResponse"\xdc\x01\n\x1dRespondNexusTaskFailedRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x10\n\x08identity\x18\x02 \x01(\t\x12\x12\n\ntask_token\x18\x03 \x01(\x0c\x12\x36\n\x05\x65rror\x18\x04 \x01(\x0b\x32#.temporal.api.nexus.v1.HandlerErrorB\x02\x18\x01\x12\x31\n\x07\x66\x61ilure\x18\x05 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x17\n\x0fpoller_group_id\x18\x06 \x01(\t" \n\x1eRespondNexusTaskFailedResponse"\xf4\x02\n\x1c\x45xecuteMultiOperationRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12[\n\noperations\x18\x02 \x03(\x0b\x32G.temporal.api.workflowservice.v1.ExecuteMultiOperationRequest.Operation\x12\x13\n\x0bresource_id\x18\x03 \x01(\t\x1a\xce\x01\n\tOperation\x12X\n\x0estart_workflow\x18\x01 \x01(\x0b\x32>.temporal.api.workflowservice.v1.StartWorkflowExecutionRequestH\x00\x12Z\n\x0fupdate_workflow\x18\x02 \x01(\x0b\x32?.temporal.api.workflowservice.v1.UpdateWorkflowExecutionRequestH\x00\x42\x0b\n\toperation"\xcc\x02\n\x1d\x45xecuteMultiOperationResponse\x12Z\n\tresponses\x18\x01 \x03(\x0b\x32G.temporal.api.workflowservice.v1.ExecuteMultiOperationResponse.Response\x1a\xce\x01\n\x08Response\x12Y\n\x0estart_workflow\x18\x01 \x01(\x0b\x32?.temporal.api.workflowservice.v1.StartWorkflowExecutionResponseH\x00\x12[\n\x0fupdate_workflow\x18\x02 \x01(\x0b\x32@.temporal.api.workflowservice.v1.UpdateWorkflowExecutionResponseH\x00\x42\n\n\x08response"\xd0\x02\n\x1cUpdateActivityOptionsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12<\n\texecution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x10\n\x08identity\x18\x03 \x01(\t\x12\x43\n\x10\x61\x63tivity_options\x18\x04 \x01(\x0b\x32).temporal.api.activity.v1.ActivityOptions\x12/\n\x0bupdate_mask\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.FieldMask\x12\x0c\n\x02id\x18\x06 \x01(\tH\x00\x12\x0e\n\x04type\x18\x07 \x01(\tH\x00\x12\x13\n\tmatch_all\x18\t \x01(\x08H\x00\x12\x18\n\x10restore_original\x18\x08 \x01(\x08\x42\n\n\x08\x61\x63tivity"\xbf\x02\n%UpdateActivityExecutionOptionsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x13\n\x0b\x61\x63tivity_id\x18\x03 \x01(\t\x12\x0e\n\x06run_id\x18\x04 \x01(\t\x12\x10\n\x08identity\x18\x05 \x01(\t\x12\x43\n\x10\x61\x63tivity_options\x18\x06 \x01(\x0b\x32).temporal.api.activity.v1.ActivityOptions\x12/\n\x0bupdate_mask\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.FieldMask\x12\x18\n\x10restore_original\x18\x08 \x01(\x08\x12\x13\n\x0bresource_id\x18\t \x01(\t\x12\x12\n\nrequest_id\x18\n \x01(\t"d\n\x1dUpdateActivityOptionsResponse\x12\x43\n\x10\x61\x63tivity_options\x18\x01 \x01(\x0b\x32).temporal.api.activity.v1.ActivityOptions"m\n&UpdateActivityExecutionOptionsResponse\x12\x43\n\x10\x61\x63tivity_options\x18\x01 \x01(\x0b\x32).temporal.api.activity.v1.ActivityOptions"\xc7\x01\n\x14PauseActivityRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12<\n\texecution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x10\n\x08identity\x18\x03 \x01(\t\x12\x0c\n\x02id\x18\x04 \x01(\tH\x00\x12\x0e\n\x04type\x18\x05 \x01(\tH\x00\x12\x0e\n\x06reason\x18\x06 \x01(\t\x12\x12\n\nrequest_id\x18\x07 \x01(\tB\n\n\x08\x61\x63tivity"\xb7\x01\n\x1dPauseActivityExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x13\n\x0b\x61\x63tivity_id\x18\x03 \x01(\t\x12\x0e\n\x06run_id\x18\x04 \x01(\t\x12\x10\n\x08identity\x18\x05 \x01(\t\x12\x0e\n\x06reason\x18\x06 \x01(\t\x12\x13\n\x0bresource_id\x18\x07 \x01(\t\x12\x12\n\nrequest_id\x18\x08 \x01(\t"\x17\n\x15PauseActivityResponse" \n\x1ePauseActivityExecutionResponse"\x98\x02\n\x16UnpauseActivityRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12<\n\texecution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x10\n\x08identity\x18\x03 \x01(\t\x12\x0c\n\x02id\x18\x04 \x01(\tH\x00\x12\x0e\n\x04type\x18\x05 \x01(\tH\x00\x12\x15\n\x0bunpause_all\x18\x06 \x01(\x08H\x00\x12\x16\n\x0ereset_attempts\x18\x07 \x01(\x08\x12\x17\n\x0freset_heartbeat\x18\x08 \x01(\x08\x12)\n\x06jitter\x18\t \x01(\x0b\x32\x19.google.protobuf.DurationB\n\n\x08\x61\x63tivity"\x91\x02\n\x1fUnpauseActivityExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x13\n\x0b\x61\x63tivity_id\x18\x03 \x01(\t\x12\x0e\n\x06run_id\x18\x04 \x01(\t\x12\x10\n\x08identity\x18\x05 \x01(\t\x12\x0e\n\x06reason\x18\x08 \x01(\t\x12)\n\x06jitter\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x13\n\x0bresource_id\x18\n \x01(\t\x12\x12\n\nrequest_id\x18\x0b \x01(\tJ\x04\x08\x06\x10\x07J\x04\x08\x07\x10\x08R\x0ereset_attemptsR\x0freset_heartbeat"\x19\n\x17UnpauseActivityResponse""\n UnpauseActivityExecutionResponse"\xb3\x02\n\x14ResetActivityRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12<\n\texecution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x10\n\x08identity\x18\x03 \x01(\t\x12\x0c\n\x02id\x18\x04 \x01(\tH\x00\x12\x0e\n\x04type\x18\x05 \x01(\tH\x00\x12\x13\n\tmatch_all\x18\n \x01(\x08H\x00\x12\x17\n\x0freset_heartbeat\x18\x06 \x01(\x08\x12\x13\n\x0bkeep_paused\x18\x07 \x01(\x08\x12)\n\x06jitter\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12 \n\x18restore_original_options\x18\t \x01(\x08\x42\n\n\x08\x61\x63tivity"\xa2\x02\n\x1dResetActivityExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x13\n\x0b\x61\x63tivity_id\x18\x03 \x01(\t\x12\x0e\n\x06run_id\x18\x04 \x01(\t\x12\x10\n\x08identity\x18\x05 \x01(\t\x12\x13\n\x0bkeep_paused\x18\x06 \x01(\x08\x12)\n\x06jitter\x18\x07 \x01(\x0b\x32\x19.google.protobuf.Duration\x12 \n\x18restore_original_options\x18\x08 \x01(\x08\x12\x13\n\x0bresource_id\x18\t \x01(\t\x12\x12\n\nrequest_id\x18\n \x01(\t\x12\x17\n\x0freset_heartbeat\x18\x0b \x01(\x08"\x17\n\x15ResetActivityResponse" \n\x1eResetActivityExecutionResponse"\x9c\x02\n%UpdateWorkflowExecutionOptionsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x45\n\x12workflow_execution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12V\n\x1aworkflow_execution_options\x18\x03 \x01(\x0b\x32\x32.temporal.api.workflow.v1.WorkflowExecutionOptions\x12/\n\x0bupdate_mask\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.FieldMask\x12\x10\n\x08identity\x18\x05 \x01(\t"\xb1\x01\n&UpdateWorkflowExecutionOptionsResponse\x12V\n\x1aworkflow_execution_options\x18\x01 \x01(\x0b\x32\x32.temporal.api.workflow.v1.WorkflowExecutionOptions\x12/\n\x0bupdate_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"j\n\x19\x44\x65scribeDeploymentRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12:\n\ndeployment\x18\x02 \x01(\x0b\x32&.temporal.api.deployment.v1.Deployment"a\n\x1a\x44\x65scribeDeploymentResponse\x12\x43\n\x0f\x64\x65ployment_info\x18\x01 \x01(\x0b\x32*.temporal.api.deployment.v1.DeploymentInfo"\xc2\x01\n&DescribeWorkerDeploymentVersionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x07version\x18\x02 \x01(\tB\x02\x18\x01\x12O\n\x12\x64\x65ployment_version\x18\x03 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12\x1f\n\x17report_task_queue_stats\x18\x04 \x01(\x08"\x8c\x05\n\'DescribeWorkerDeploymentVersionResponse\x12_\n\x1eworker_deployment_version_info\x18\x01 \x01(\x0b\x32\x37.temporal.api.deployment.v1.WorkerDeploymentVersionInfo\x12v\n\x13version_task_queues\x18\x02 \x03(\x0b\x32Y.temporal.api.workflowservice.v1.DescribeWorkerDeploymentVersionResponse.VersionTaskQueue\x1a\x87\x03\n\x10VersionTaskQueue\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x32\n\x04type\x18\x02 \x01(\x0e\x32$.temporal.api.enums.v1.TaskQueueType\x12\x38\n\x05stats\x18\x03 \x01(\x0b\x32).temporal.api.taskqueue.v1.TaskQueueStats\x12\x90\x01\n\x15stats_by_priority_key\x18\x04 \x03(\x0b\x32q.temporal.api.workflowservice.v1.DescribeWorkerDeploymentVersionResponse.VersionTaskQueue.StatsByPriorityKeyEntry\x1a\x64\n\x17StatsByPriorityKeyEntry\x12\x0b\n\x03key\x18\x01 \x01(\x05\x12\x38\n\x05value\x18\x02 \x01(\x0b\x32).temporal.api.taskqueue.v1.TaskQueueStats:\x02\x38\x01"M\n\x1f\x44\x65scribeWorkerDeploymentRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x17\n\x0f\x64\x65ployment_name\x18\x02 \x01(\t"\x8c\x01\n DescribeWorkerDeploymentResponse\x12\x16\n\x0e\x63onflict_token\x18\x01 \x01(\x0c\x12P\n\x16worker_deployment_info\x18\x02 \x01(\x0b\x32\x30.temporal.api.deployment.v1.WorkerDeploymentInfo"l\n\x16ListDeploymentsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x03 \x01(\x0c\x12\x13\n\x0bseries_name\x18\x04 \x01(\t"w\n\x17ListDeploymentsResponse\x12\x17\n\x0fnext_page_token\x18\x01 \x01(\x0c\x12\x43\n\x0b\x64\x65ployments\x18\x02 \x03(\x0b\x32..temporal.api.deployment.v1.DeploymentListInfo"\xcd\x01\n\x1bSetCurrentDeploymentRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12:\n\ndeployment\x18\x02 \x01(\x0b\x32&.temporal.api.deployment.v1.Deployment\x12\x10\n\x08identity\x18\x03 \x01(\t\x12M\n\x0fupdate_metadata\x18\x04 \x01(\x0b\x32\x34.temporal.api.deployment.v1.UpdateDeploymentMetadata"\xb9\x01\n\x1cSetCurrentDeploymentResponse\x12K\n\x17\x63urrent_deployment_info\x18\x01 \x01(\x0b\x32*.temporal.api.deployment.v1.DeploymentInfo\x12L\n\x18previous_deployment_info\x18\x02 \x01(\x0b\x32*.temporal.api.deployment.v1.DeploymentInfo"\xe5\x01\n(SetWorkerDeploymentCurrentVersionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x17\n\x0f\x64\x65ployment_name\x18\x02 \x01(\t\x12\x13\n\x07version\x18\x03 \x01(\tB\x02\x18\x01\x12\x10\n\x08\x62uild_id\x18\x07 \x01(\t\x12\x16\n\x0e\x63onflict_token\x18\x04 \x01(\x0c\x12\x10\n\x08identity\x18\x05 \x01(\t\x12"\n\x1aignore_missing_task_queues\x18\x06 \x01(\x08\x12\x18\n\x10\x61llow_no_pollers\x18\t \x01(\x08"\xbf\x01\n)SetWorkerDeploymentCurrentVersionResponse\x12\x16\n\x0e\x63onflict_token\x18\x01 \x01(\x0c\x12\x1c\n\x10previous_version\x18\x02 \x01(\tB\x02\x18\x01\x12\\\n\x1bprevious_deployment_version\x18\x03 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersionB\x02\x18\x01"\xf9\x01\n(SetWorkerDeploymentRampingVersionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x17\n\x0f\x64\x65ployment_name\x18\x02 \x01(\t\x12\x13\n\x07version\x18\x03 \x01(\tB\x02\x18\x01\x12\x10\n\x08\x62uild_id\x18\x08 \x01(\t\x12\x12\n\npercentage\x18\x04 \x01(\x02\x12\x16\n\x0e\x63onflict_token\x18\x05 \x01(\x0c\x12\x10\n\x08identity\x18\x06 \x01(\t\x12"\n\x1aignore_missing_task_queues\x18\x07 \x01(\x08\x12\x18\n\x10\x61llow_no_pollers\x18\n \x01(\x08"\xe0\x01\n)SetWorkerDeploymentRampingVersionResponse\x12\x16\n\x0e\x63onflict_token\x18\x01 \x01(\x0c\x12\x1c\n\x10previous_version\x18\x02 \x01(\tB\x02\x18\x01\x12\\\n\x1bprevious_deployment_version\x18\x04 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersionB\x02\x18\x01\x12\x1f\n\x13previous_percentage\x18\x03 \x01(\x02\x42\x02\x18\x01"q\n\x1d\x43reateWorkerDeploymentRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x17\n\x0f\x64\x65ployment_name\x18\x02 \x01(\t\x12\x10\n\x08identity\x18\x04 \x01(\t\x12\x12\n\nrequest_id\x18\x05 \x01(\t"8\n\x1e\x43reateWorkerDeploymentResponse\x12\x16\n\x0e\x63onflict_token\x18\x01 \x01(\x0c"]\n\x1cListWorkerDeploymentsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x03 \x01(\x0c"\x9f\x05\n\x1dListWorkerDeploymentsResponse\x12\x17\n\x0fnext_page_token\x18\x01 \x01(\x0c\x12r\n\x12worker_deployments\x18\x02 \x03(\x0b\x32V.temporal.api.workflowservice.v1.ListWorkerDeploymentsResponse.WorkerDeploymentSummary\x1a\xf0\x03\n\x17WorkerDeploymentSummary\x12\x0c\n\x04name\x18\x01 \x01(\t\x12/\n\x0b\x63reate_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x41\n\x0erouting_config\x18\x03 \x01(\x0b\x32).temporal.api.deployment.v1.RoutingConfig\x12o\n\x16latest_version_summary\x18\x04 \x01(\x0b\x32O.temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary\x12p\n\x17\x63urrent_version_summary\x18\x05 \x01(\x0b\x32O.temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary\x12p\n\x17ramping_version_summary\x18\x06 \x01(\x0b\x32O.temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary"\xf0\x01\n$CreateWorkerDeploymentVersionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12O\n\x12\x64\x65ployment_version\x18\x02 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12>\n\x0e\x63ompute_config\x18\x04 \x01(\x0b\x32&.temporal.api.compute.v1.ComputeConfig\x12\x10\n\x08identity\x18\x03 \x01(\t\x12\x12\n\nrequest_id\x18\x05 \x01(\t"\'\n%CreateWorkerDeploymentVersionResponse"\xc8\x01\n$DeleteWorkerDeploymentVersionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x07version\x18\x02 \x01(\tB\x02\x18\x01\x12O\n\x12\x64\x65ployment_version\x18\x05 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12\x15\n\rskip_drainage\x18\x03 \x01(\x08\x12\x10\n\x08identity\x18\x04 \x01(\t"\'\n%DeleteWorkerDeploymentVersionResponse"]\n\x1d\x44\x65leteWorkerDeploymentRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x17\n\x0f\x64\x65ployment_name\x18\x02 \x01(\t\x12\x10\n\x08identity\x18\x03 \x01(\t" \n\x1e\x44\x65leteWorkerDeploymentResponse"\x84\x04\n1UpdateWorkerDeploymentVersionComputeConfigRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12O\n\x12\x64\x65ployment_version\x18\x02 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12\x99\x01\n\x1d\x63ompute_config_scaling_groups\x18\x06 \x03(\x0b\x32r.temporal.api.workflowservice.v1.UpdateWorkerDeploymentVersionComputeConfigRequest.ComputeConfigScalingGroupsEntry\x12,\n$remove_compute_config_scaling_groups\x18\x07 \x03(\t\x12\x10\n\x08identity\x18\x03 \x01(\t\x12\x12\n\nrequest_id\x18\x04 \x01(\t\x1a{\n\x1f\x43omputeConfigScalingGroupsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12G\n\x05value\x18\x02 \x01(\x0b\x32\x38.temporal.api.compute.v1.ComputeConfigScalingGroupUpdate:\x02\x38\x01"4\n2UpdateWorkerDeploymentVersionComputeConfigResponse"\xf4\x03\n3ValidateWorkerDeploymentVersionComputeConfigRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12O\n\x12\x64\x65ployment_version\x18\x02 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12\x9b\x01\n\x1d\x63ompute_config_scaling_groups\x18\x06 \x03(\x0b\x32t.temporal.api.workflowservice.v1.ValidateWorkerDeploymentVersionComputeConfigRequest.ComputeConfigScalingGroupsEntry\x12,\n$remove_compute_config_scaling_groups\x18\x07 \x03(\t\x12\x10\n\x08identity\x18\x03 \x01(\t\x1a{\n\x1f\x43omputeConfigScalingGroupsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12G\n\x05value\x18\x02 \x01(\x0b\x32\x38.temporal.api.compute.v1.ComputeConfigScalingGroupUpdate:\x02\x38\x01"6\n4ValidateWorkerDeploymentVersionComputeConfigResponse"\xa2\x03\n,UpdateWorkerDeploymentVersionMetadataRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x07version\x18\x02 \x01(\tB\x02\x18\x01\x12O\n\x12\x64\x65ployment_version\x18\x05 \x01(\x0b\x32\x33.temporal.api.deployment.v1.WorkerDeploymentVersion\x12x\n\x0eupsert_entries\x18\x03 \x03(\x0b\x32`.temporal.api.workflowservice.v1.UpdateWorkerDeploymentVersionMetadataRequest.UpsertEntriesEntry\x12\x16\n\x0eremove_entries\x18\x04 \x03(\t\x12\x10\n\x08identity\x18\x06 \x01(\t\x1aU\n\x12UpsertEntriesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"n\n-UpdateWorkerDeploymentVersionMetadataResponse\x12=\n\x08metadata\x18\x01 \x01(\x0b\x32+.temporal.api.deployment.v1.VersionMetadata"\xbd\x01\n!SetWorkerDeploymentManagerRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x17\n\x0f\x64\x65ployment_name\x18\x02 \x01(\t\x12\x1a\n\x10manager_identity\x18\x03 \x01(\tH\x00\x12\x0e\n\x04self\x18\x04 \x01(\x08H\x00\x12\x16\n\x0e\x63onflict_token\x18\x05 \x01(\x0c\x12\x10\n\x08identity\x18\x06 \x01(\tB\x16\n\x14new_manager_identity"c\n"SetWorkerDeploymentManagerResponse\x12\x16\n\x0e\x63onflict_token\x18\x01 \x01(\x0c\x12%\n\x19previous_manager_identity\x18\x02 \x01(\tB\x02\x18\x01"E\n\x1bGetCurrentDeploymentRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bseries_name\x18\x02 \x01(\t"k\n\x1cGetCurrentDeploymentResponse\x12K\n\x17\x63urrent_deployment_info\x18\x01 \x01(\x0b\x32*.temporal.api.deployment.v1.DeploymentInfo"q\n GetDeploymentReachabilityRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12:\n\ndeployment\x18\x02 \x01(\x0b\x32&.temporal.api.deployment.v1.Deployment"\xe3\x01\n!GetDeploymentReachabilityResponse\x12\x43\n\x0f\x64\x65ployment_info\x18\x01 \x01(\x0b\x32*.temporal.api.deployment.v1.DeploymentInfo\x12\x43\n\x0creachability\x18\x02 \x01(\x0e\x32-.temporal.api.enums.v1.DeploymentReachability\x12\x34\n\x10last_update_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"\xb4\x01\n\x19\x43reateWorkflowRuleRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x35\n\x04spec\x18\x02 \x01(\x0b\x32\'.temporal.api.rules.v1.WorkflowRuleSpec\x12\x12\n\nforce_scan\x18\x03 \x01(\x08\x12\x12\n\nrequest_id\x18\x04 \x01(\t\x12\x10\n\x08identity\x18\x05 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x06 \x01(\t"_\n\x1a\x43reateWorkflowRuleResponse\x12\x31\n\x04rule\x18\x01 \x01(\x0b\x32#.temporal.api.rules.v1.WorkflowRule\x12\x0e\n\x06job_id\x18\x02 \x01(\t"A\n\x1b\x44\x65scribeWorkflowRuleRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x0f\n\x07rule_id\x18\x02 \x01(\t"Q\n\x1c\x44\x65scribeWorkflowRuleResponse\x12\x31\n\x04rule\x18\x01 \x01(\x0b\x32#.temporal.api.rules.v1.WorkflowRule"?\n\x19\x44\x65leteWorkflowRuleRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x0f\n\x07rule_id\x18\x02 \x01(\t"\x1c\n\x1a\x44\x65leteWorkflowRuleResponse"F\n\x18ListWorkflowRulesRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\x0c"h\n\x19ListWorkflowRulesResponse\x12\x32\n\x05rules\x18\x01 \x03(\x0b\x32#.temporal.api.rules.v1.WorkflowRule\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\x0c"\xce\x01\n\x1aTriggerWorkflowRuleRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12<\n\texecution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x0c\n\x02id\x18\x04 \x01(\tH\x00\x12\x37\n\x04spec\x18\x05 \x01(\x0b\x32\'.temporal.api.rules.v1.WorkflowRuleSpecH\x00\x12\x10\n\x08identity\x18\x03 \x01(\tB\x06\n\x04rule".\n\x1bTriggerWorkflowRuleResponse\x12\x0f\n\x07\x61pplied\x18\x01 \x01(\x08"\x9b\x01\n\x1cRecordWorkerHeartbeatRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x10\n\x08identity\x18\x02 \x01(\t\x12\x41\n\x10worker_heartbeat\x18\x03 \x03(\x0b\x32\'.temporal.api.worker.v1.WorkerHeartbeat\x12\x13\n\x0bresource_id\x18\x04 \x01(\t"\x1f\n\x1dRecordWorkerHeartbeatResponse"\x82\x01\n\x12ListWorkersRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x03 \x01(\x0c\x12\r\n\x05query\x18\x04 \x01(\t\x12\x1e\n\x16include_system_workers\x18\x05 \x01(\x08"\xa5\x01\n\x13ListWorkersResponse\x12<\n\x0cworkers_info\x18\x01 \x03(\x0b\x32".temporal.api.worker.v1.WorkerInfoB\x02\x18\x01\x12\x37\n\x07workers\x18\x03 \x03(\x0b\x32&.temporal.api.worker.v1.WorkerListInfo\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\x0c"\xd5\x05\n\x1cUpdateTaskQueueConfigRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x10\n\x08identity\x18\x02 \x01(\t\x12\x12\n\ntask_queue\x18\x03 \x01(\t\x12=\n\x0ftask_queue_type\x18\x04 \x01(\x0e\x32$.temporal.api.enums.v1.TaskQueueType\x12n\n\x17update_queue_rate_limit\x18\x05 \x01(\x0b\x32M.temporal.api.workflowservice.v1.UpdateTaskQueueConfigRequest.RateLimitUpdate\x12}\n&update_fairness_key_rate_limit_default\x18\x06 \x01(\x0b\x32M.temporal.api.workflowservice.v1.UpdateTaskQueueConfigRequest.RateLimitUpdate\x12\x84\x01\n\x1dset_fairness_weight_overrides\x18\x07 \x03(\x0b\x32].temporal.api.workflowservice.v1.UpdateTaskQueueConfigRequest.SetFairnessWeightOverridesEntry\x12\'\n\x1funset_fairness_weight_overrides\x18\x08 \x03(\t\x1a[\n\x0fRateLimitUpdate\x12\x38\n\nrate_limit\x18\x01 \x01(\x0b\x32$.temporal.api.taskqueue.v1.RateLimit\x12\x0e\n\x06reason\x18\x02 \x01(\t\x1a\x41\n\x1fSetFairnessWeightOverridesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x02:\x02\x38\x01"[\n\x1dUpdateTaskQueueConfigResponse\x12:\n\x06\x63onfig\x18\x01 \x01(\x0b\x32*.temporal.api.taskqueue.v1.TaskQueueConfig"\x9e\x01\n\x18\x46\x65tchWorkerConfigRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x10\n\x08identity\x18\x02 \x01(\t\x12\x0e\n\x06reason\x18\x03 \x01(\t\x12\x38\n\x08selector\x18\x06 \x01(\x0b\x32&.temporal.api.common.v1.WorkerSelector\x12\x13\n\x0bresource_id\x18\x07 \x01(\t"U\n\x19\x46\x65tchWorkerConfigResponse\x12\x38\n\rworker_config\x18\x01 \x01(\x0b\x32!.temporal.api.sdk.v1.WorkerConfig"\x8a\x02\n\x19UpdateWorkerConfigRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x10\n\x08identity\x18\x02 \x01(\t\x12\x0e\n\x06reason\x18\x03 \x01(\t\x12\x38\n\rworker_config\x18\x04 \x01(\x0b\x32!.temporal.api.sdk.v1.WorkerConfig\x12/\n\x0bupdate_mask\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.FieldMask\x12\x38\n\x08selector\x18\x06 \x01(\x0b\x32&.temporal.api.common.v1.WorkerSelector\x12\x13\n\x0bresource_id\x18\x07 \x01(\t"d\n\x1aUpdateWorkerConfigResponse\x12:\n\rworker_config\x18\x01 \x01(\x0b\x32!.temporal.api.sdk.v1.WorkerConfigH\x00\x42\n\n\x08response"G\n\x15\x44\x65scribeWorkerRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x1b\n\x13worker_instance_key\x18\x02 \x01(\t"Q\n\x16\x44\x65scribeWorkerResponse\x12\x37\n\x0bworker_info\x18\x01 \x01(\x0b\x32".temporal.api.worker.v1.WorkerInfo"W\n\x13\x43ountWorkersRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\r\n\x05query\x18\x02 \x01(\t\x12\x1e\n\x16include_system_workers\x18\x03 \x01(\x08"%\n\x14\x43ountWorkersResponse\x12\r\n\x05\x63ount\x18\x01 \x01(\x03"\x8d\x01\n\x1dPauseWorkflowExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\t\x12\x10\n\x08identity\x18\x04 \x01(\t\x12\x0e\n\x06reason\x18\x05 \x01(\t\x12\x12\n\nrequest_id\x18\x06 \x01(\t" \n\x1ePauseWorkflowExecutionResponse"\x8f\x01\n\x1fUnpauseWorkflowExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\t\x12\x10\n\x08identity\x18\x04 \x01(\t\x12\x0e\n\x06reason\x18\x05 \x01(\t\x12\x12\n\nrequest_id\x18\x06 \x01(\t""\n UnpauseWorkflowExecutionResponse"\x99\t\n\x1dStartActivityExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x10\n\x08identity\x18\x02 \x01(\t\x12\x12\n\nrequest_id\x18\x03 \x01(\t\x12\x13\n\x0b\x61\x63tivity_id\x18\x04 \x01(\t\x12;\n\ractivity_type\x18\x05 \x01(\x0b\x32$.temporal.api.common.v1.ActivityType\x12\x38\n\ntask_queue\x18\x06 \x01(\x0b\x32$.temporal.api.taskqueue.v1.TaskQueue\x12<\n\x19schedule_to_close_timeout\x18\x07 \x01(\x0b\x32\x19.google.protobuf.Duration\x12<\n\x19schedule_to_start_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x34\n\x11heartbeat_timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x0cretry_policy\x18\x0b \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12/\n\x05input\x18\x0c \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x45\n\x0fid_reuse_policy\x18\r \x01(\x0e\x32,.temporal.api.enums.v1.ActivityIdReusePolicy\x12K\n\x12id_conflict_policy\x18\x0e \x01(\x0e\x32/.temporal.api.enums.v1.ActivityIdConflictPolicy\x12\x43\n\x11search_attributes\x18\x0f \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12.\n\x06header\x18\x10 \x01(\x0b\x32\x1e.temporal.api.common.v1.Header\x12\x38\n\ruser_metadata\x18\x11 \x01(\x0b\x32!.temporal.api.sdk.v1.UserMetadata\x12\x32\n\x08priority\x18\x12 \x01(\x0b\x32 .temporal.api.common.v1.Priority\x12>\n\x14\x63ompletion_callbacks\x18\x13 \x03(\x0b\x32 .temporal.api.common.v1.Callback\x12+\n\x05links\x18\x14 \x03(\x0b\x32\x1c.temporal.api.common.v1.Link\x12\x46\n\x13on_conflict_options\x18\x15 \x01(\x0b\x32).temporal.api.common.v1.OnConflictOptions\x12.\n\x0bstart_delay\x18\x16 \x01(\x0b\x32\x19.google.protobuf.Duration"m\n\x1eStartActivityExecutionResponse\x12\x0e\n\x06run_id\x18\x01 \x01(\t\x12\x0f\n\x07started\x18\x02 \x01(\x08\x12*\n\x04link\x18\x03 \x01(\x0b\x32\x1c.temporal.api.common.v1.Link"\xe4\x01\n DescribeActivityExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0b\x61\x63tivity_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\t\x12\x15\n\rinclude_input\x18\x04 \x01(\x08\x12\x17\n\x0finclude_outcome\x18\x05 \x01(\x08\x12\x17\n\x0flong_poll_token\x18\x06 \x01(\x0c\x12!\n\x19include_heartbeat_details\x18\x07 \x01(\x08\x12\x1c\n\x14include_last_failure\x18\x08 \x01(\x08"\xbc\x02\n!DescribeActivityExecutionResponse\x12\x0e\n\x06run_id\x18\x01 \x01(\t\x12=\n\x04info\x18\x02 \x01(\x0b\x32/.temporal.api.activity.v1.ActivityExecutionInfo\x12/\n\x05input\x18\x03 \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x43\n\x07outcome\x18\x04 \x01(\x0b\x32\x32.temporal.api.activity.v1.ActivityExecutionOutcome\x12\x17\n\x0flong_poll_token\x18\x05 \x01(\x0c\x12\x39\n\tcallbacks\x18\x06 \x03(\x0b\x32&.temporal.api.activity.v1.CallbackInfo"V\n\x1cPollActivityExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0b\x61\x63tivity_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\t"t\n\x1dPollActivityExecutionResponse\x12\x0e\n\x06run_id\x18\x01 \x01(\t\x12\x43\n\x07outcome\x18\x02 \x01(\x0b\x32\x32.temporal.api.activity.v1.ActivityExecutionOutcome"m\n\x1dListActivityExecutionsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x03 \x01(\x0c\x12\r\n\x05query\x18\x04 \x01(\t"\x82\x01\n\x1eListActivityExecutionsResponse\x12G\n\nexecutions\x18\x01 \x03(\x0b\x32\x33.temporal.api.activity.v1.ActivityExecutionListInfo\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\x0c"\xd1\x06\n#StartNexusOperationExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x10\n\x08identity\x18\x02 \x01(\t\x12\x12\n\nrequest_id\x18\x03 \x01(\t\x12\x14\n\x0coperation_id\x18\x04 \x01(\t\x12\x10\n\x08\x65ndpoint\x18\x05 \x01(\t\x12\x0f\n\x07service\x18\x06 \x01(\t\x12\x11\n\toperation\x18\x07 \x01(\t\x12<\n\x19schedule_to_close_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12<\n\x19schedule_to_start_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.Duration\x12.\n\x05input\x18\x0b \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12K\n\x0fid_reuse_policy\x18\x0c \x01(\x0e\x32\x32.temporal.api.enums.v1.NexusOperationIdReusePolicy\x12Q\n\x12id_conflict_policy\x18\r \x01(\x0e\x32\x35.temporal.api.enums.v1.NexusOperationIdConflictPolicy\x12\x43\n\x11search_attributes\x18\x0e \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12k\n\x0cnexus_header\x18\x0f \x03(\x0b\x32U.temporal.api.workflowservice.v1.StartNexusOperationExecutionRequest.NexusHeaderEntry\x12\x38\n\ruser_metadata\x18\x10 \x01(\x0b\x32!.temporal.api.sdk.v1.UserMetadata\x1a\x32\n\x10NexusHeaderEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"G\n$StartNexusOperationExecutionResponse\x12\x0e\n\x06run_id\x18\x01 \x01(\t\x12\x0f\n\x07started\x18\x02 \x01(\x08"\xaa\x01\n&DescribeNexusOperationExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x14\n\x0coperation_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\t\x12\x15\n\rinclude_input\x18\x04 \x01(\x08\x12\x17\n\x0finclude_outcome\x18\x05 \x01(\x08\x12\x17\n\x0flong_poll_token\x18\x06 \x01(\x0c"\xb7\x02\n\'DescribeNexusOperationExecutionResponse\x12\x0e\n\x06run_id\x18\x01 \x01(\t\x12@\n\x04info\x18\x02 \x01(\x0b\x32\x32.temporal.api.nexus.v1.NexusOperationExecutionInfo\x12.\n\x05input\x18\x03 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x31\n\x06result\x18\x04 \x01(\x0b\x32\x1f.temporal.api.common.v1.PayloadH\x00\x12\x33\n\x07\x66\x61ilure\x18\x05 \x01(\x0b\x32 .temporal.api.failure.v1.FailureH\x00\x12\x17\n\x0flong_poll_token\x18\x06 \x01(\x0c\x42\t\n\x07outcome"\xa1\x01\n"PollNexusOperationExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x14\n\x0coperation_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\t\x12\x42\n\nwait_stage\x18\x04 \x01(\x0e\x32..temporal.api.enums.v1.NexusOperationWaitStage"\x85\x02\n#PollNexusOperationExecutionResponse\x12\x0e\n\x06run_id\x18\x01 \x01(\t\x12\x42\n\nwait_stage\x18\x02 \x01(\x0e\x32..temporal.api.enums.v1.NexusOperationWaitStage\x12\x17\n\x0foperation_token\x18\x03 \x01(\t\x12\x31\n\x06result\x18\x04 \x01(\x0b\x32\x1f.temporal.api.common.v1.PayloadH\x00\x12\x33\n\x07\x66\x61ilure\x18\x05 \x01(\x0b\x32 .temporal.api.failure.v1.FailureH\x00\x42\t\n\x07outcome"s\n#ListNexusOperationExecutionsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x03 \x01(\x0c\x12\r\n\x05query\x18\x04 \x01(\t"\x8b\x01\n$ListNexusOperationExecutionsResponse\x12J\n\noperations\x18\x01 \x03(\x0b\x32\x36.temporal.api.nexus.v1.NexusOperationExecutionListInfo\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\x0c"B\n\x1e\x43ountActivityExecutionsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\r\n\x05query\x18\x02 \x01(\t"\xed\x01\n\x1f\x43ountActivityExecutionsResponse\x12\r\n\x05\x63ount\x18\x01 \x01(\x03\x12\x61\n\x06groups\x18\x02 \x03(\x0b\x32Q.temporal.api.workflowservice.v1.CountActivityExecutionsResponse.AggregationGroup\x1aX\n\x10\x41ggregationGroup\x12\x35\n\x0cgroup_values\x18\x01 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\r\n\x05\x63ount\x18\x02 \x01(\x03"H\n$CountNexusOperationExecutionsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\r\n\x05query\x18\x02 \x01(\t"\xf9\x01\n%CountNexusOperationExecutionsResponse\x12\r\n\x05\x63ount\x18\x01 \x01(\x03\x12g\n\x06groups\x18\x02 \x03(\x0b\x32W.temporal.api.workflowservice.v1.CountNexusOperationExecutionsResponse.AggregationGroup\x1aX\n\x10\x41ggregationGroup\x12\x35\n\x0cgroup_values\x18\x01 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\r\n\x05\x63ount\x18\x02 \x01(\x03"\x95\x01\n%RequestCancelActivityExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0b\x61\x63tivity_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\t\x12\x10\n\x08identity\x18\x04 \x01(\t\x12\x12\n\nrequest_id\x18\x05 \x01(\t\x12\x0e\n\x06reason\x18\x06 \x01(\t"(\n&RequestCancelActivityExecutionResponse"\x91\x01\n!TerminateActivityExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0b\x61\x63tivity_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\t\x12\x10\n\x08identity\x18\x04 \x01(\t\x12\x12\n\nrequest_id\x18\x05 \x01(\t\x12\x0e\n\x06reason\x18\x06 \x01(\t"$\n"TerminateActivityExecutionResponse"X\n\x1e\x44\x65leteActivityExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0b\x61\x63tivity_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\t"!\n\x1f\x44\x65leteActivityExecutionResponse"\x9c\x01\n+RequestCancelNexusOperationExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x14\n\x0coperation_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\t\x12\x10\n\x08identity\x18\x04 \x01(\t\x12\x12\n\nrequest_id\x18\x05 \x01(\t\x12\x0e\n\x06reason\x18\x06 \x01(\t".\n,RequestCancelNexusOperationExecutionResponse"\x98\x01\n\'TerminateNexusOperationExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x14\n\x0coperation_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\t\x12\x10\n\x08identity\x18\x04 \x01(\t\x12\x12\n\nrequest_id\x18\x05 \x01(\t\x12\x0e\n\x06reason\x18\x06 \x01(\t"*\n(TerminateNexusOperationExecutionResponse"_\n$DeleteNexusOperationExecutionRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x14\n\x0coperation_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\t"\'\n%DeleteNexusOperationExecutionResponse"\x9d\x01\n(PollWorkflowExecutionTimeSkippingRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x45\n\x12workflow_execution\x18\x02 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x17\n\x0f\x66\x61st_forward_id\x18\x03 \x01(\t"\xe8\x01\n)PollWorkflowExecutionTimeSkippingResponse\x12T\n\x1b\x66\x61st_forward_polling_result\x18\x01 \x01(\x0e\x32/.temporal.api.enums.v1.FastForwardPollingResult\x12\x15\n\rfailed_reason\x18\x02 \x01(\t\x12N\n\x11\x66\x61st_forward_info\x18\x03 \x01(\x0b\x32\x33.temporal.api.common.v1.TimeSkippingFastForwardInfoB\xbe\x01\n"io.temporal.api.workflowservice.v1B\x14RequestResponseProtoP\x01Z5go.temporal.io/api/workflowservice/v1;workflowservice\xaa\x02!Temporalio.Api.WorkflowService.V1\xea\x02$Temporalio::Api::WorkflowService::V1b\x06proto3' ) @@ -396,6 +408,11 @@ _DELETESCHEDULERESPONSE = DESCRIPTOR.message_types_by_name["DeleteScheduleResponse"] _LISTSCHEDULESREQUEST = DESCRIPTOR.message_types_by_name["ListSchedulesRequest"] _LISTSCHEDULESRESPONSE = DESCRIPTOR.message_types_by_name["ListSchedulesResponse"] +_COUNTSCHEDULESREQUEST = DESCRIPTOR.message_types_by_name["CountSchedulesRequest"] +_COUNTSCHEDULESRESPONSE = DESCRIPTOR.message_types_by_name["CountSchedulesResponse"] +_COUNTSCHEDULESRESPONSE_AGGREGATIONGROUP = _COUNTSCHEDULESRESPONSE.nested_types_by_name[ + "AggregationGroup" +] _UPDATEWORKERBUILDIDCOMPATIBILITYREQUEST = DESCRIPTOR.message_types_by_name[ "UpdateWorkerBuildIdCompatibilityRequest" ] @@ -536,15 +553,39 @@ _UPDATEACTIVITYOPTIONSREQUEST = DESCRIPTOR.message_types_by_name[ "UpdateActivityOptionsRequest" ] +_UPDATEACTIVITYEXECUTIONOPTIONSREQUEST = DESCRIPTOR.message_types_by_name[ + "UpdateActivityExecutionOptionsRequest" +] _UPDATEACTIVITYOPTIONSRESPONSE = DESCRIPTOR.message_types_by_name[ "UpdateActivityOptionsResponse" ] +_UPDATEACTIVITYEXECUTIONOPTIONSRESPONSE = DESCRIPTOR.message_types_by_name[ + "UpdateActivityExecutionOptionsResponse" +] _PAUSEACTIVITYREQUEST = DESCRIPTOR.message_types_by_name["PauseActivityRequest"] +_PAUSEACTIVITYEXECUTIONREQUEST = DESCRIPTOR.message_types_by_name[ + "PauseActivityExecutionRequest" +] _PAUSEACTIVITYRESPONSE = DESCRIPTOR.message_types_by_name["PauseActivityResponse"] +_PAUSEACTIVITYEXECUTIONRESPONSE = DESCRIPTOR.message_types_by_name[ + "PauseActivityExecutionResponse" +] _UNPAUSEACTIVITYREQUEST = DESCRIPTOR.message_types_by_name["UnpauseActivityRequest"] +_UNPAUSEACTIVITYEXECUTIONREQUEST = DESCRIPTOR.message_types_by_name[ + "UnpauseActivityExecutionRequest" +] _UNPAUSEACTIVITYRESPONSE = DESCRIPTOR.message_types_by_name["UnpauseActivityResponse"] +_UNPAUSEACTIVITYEXECUTIONRESPONSE = DESCRIPTOR.message_types_by_name[ + "UnpauseActivityExecutionResponse" +] _RESETACTIVITYREQUEST = DESCRIPTOR.message_types_by_name["ResetActivityRequest"] +_RESETACTIVITYEXECUTIONREQUEST = DESCRIPTOR.message_types_by_name[ + "ResetActivityExecutionRequest" +] _RESETACTIVITYRESPONSE = DESCRIPTOR.message_types_by_name["ResetActivityResponse"] +_RESETACTIVITYEXECUTIONRESPONSE = DESCRIPTOR.message_types_by_name[ + "ResetActivityExecutionResponse" +] _UPDATEWORKFLOWEXECUTIONOPTIONSREQUEST = DESCRIPTOR.message_types_by_name[ "UpdateWorkflowExecutionOptionsRequest" ] @@ -597,6 +638,12 @@ _SETWORKERDEPLOYMENTRAMPINGVERSIONRESPONSE = DESCRIPTOR.message_types_by_name[ "SetWorkerDeploymentRampingVersionResponse" ] +_CREATEWORKERDEPLOYMENTREQUEST = DESCRIPTOR.message_types_by_name[ + "CreateWorkerDeploymentRequest" +] +_CREATEWORKERDEPLOYMENTRESPONSE = DESCRIPTOR.message_types_by_name[ + "CreateWorkerDeploymentResponse" +] _LISTWORKERDEPLOYMENTSREQUEST = DESCRIPTOR.message_types_by_name[ "ListWorkerDeploymentsRequest" ] @@ -606,6 +653,12 @@ _LISTWORKERDEPLOYMENTSRESPONSE_WORKERDEPLOYMENTSUMMARY = ( _LISTWORKERDEPLOYMENTSRESPONSE.nested_types_by_name["WorkerDeploymentSummary"] ) +_CREATEWORKERDEPLOYMENTVERSIONREQUEST = DESCRIPTOR.message_types_by_name[ + "CreateWorkerDeploymentVersionRequest" +] +_CREATEWORKERDEPLOYMENTVERSIONRESPONSE = DESCRIPTOR.message_types_by_name[ + "CreateWorkerDeploymentVersionResponse" +] _DELETEWORKERDEPLOYMENTVERSIONREQUEST = DESCRIPTOR.message_types_by_name[ "DeleteWorkerDeploymentVersionRequest" ] @@ -618,6 +671,30 @@ _DELETEWORKERDEPLOYMENTRESPONSE = DESCRIPTOR.message_types_by_name[ "DeleteWorkerDeploymentResponse" ] +_UPDATEWORKERDEPLOYMENTVERSIONCOMPUTECONFIGREQUEST = DESCRIPTOR.message_types_by_name[ + "UpdateWorkerDeploymentVersionComputeConfigRequest" +] +_UPDATEWORKERDEPLOYMENTVERSIONCOMPUTECONFIGREQUEST_COMPUTECONFIGSCALINGGROUPSENTRY = ( + _UPDATEWORKERDEPLOYMENTVERSIONCOMPUTECONFIGREQUEST.nested_types_by_name[ + "ComputeConfigScalingGroupsEntry" + ] +) +_UPDATEWORKERDEPLOYMENTVERSIONCOMPUTECONFIGRESPONSE = DESCRIPTOR.message_types_by_name[ + "UpdateWorkerDeploymentVersionComputeConfigResponse" +] +_VALIDATEWORKERDEPLOYMENTVERSIONCOMPUTECONFIGREQUEST = DESCRIPTOR.message_types_by_name[ + "ValidateWorkerDeploymentVersionComputeConfigRequest" +] +_VALIDATEWORKERDEPLOYMENTVERSIONCOMPUTECONFIGREQUEST_COMPUTECONFIGSCALINGGROUPSENTRY = ( + _VALIDATEWORKERDEPLOYMENTVERSIONCOMPUTECONFIGREQUEST.nested_types_by_name[ + "ComputeConfigScalingGroupsEntry" + ] +) +_VALIDATEWORKERDEPLOYMENTVERSIONCOMPUTECONFIGRESPONSE = ( + DESCRIPTOR.message_types_by_name[ + "ValidateWorkerDeploymentVersionComputeConfigResponse" + ] +) _UPDATEWORKERDEPLOYMENTVERSIONMETADATAREQUEST = DESCRIPTOR.message_types_by_name[ "UpdateWorkerDeploymentVersionMetadataRequest" ] @@ -629,6 +706,12 @@ _UPDATEWORKERDEPLOYMENTVERSIONMETADATARESPONSE = DESCRIPTOR.message_types_by_name[ "UpdateWorkerDeploymentVersionMetadataResponse" ] +_SETWORKERDEPLOYMENTMANAGERREQUEST = DESCRIPTOR.message_types_by_name[ + "SetWorkerDeploymentManagerRequest" +] +_SETWORKERDEPLOYMENTMANAGERRESPONSE = DESCRIPTOR.message_types_by_name[ + "SetWorkerDeploymentManagerResponse" +] _GETCURRENTDEPLOYMENTREQUEST = DESCRIPTOR.message_types_by_name[ "GetCurrentDeploymentRequest" ] @@ -683,6 +766,11 @@ _UPDATETASKQUEUECONFIGREQUEST_RATELIMITUPDATE = ( _UPDATETASKQUEUECONFIGREQUEST.nested_types_by_name["RateLimitUpdate"] ) +_UPDATETASKQUEUECONFIGREQUEST_SETFAIRNESSWEIGHTOVERRIDESENTRY = ( + _UPDATETASKQUEUECONFIGREQUEST.nested_types_by_name[ + "SetFairnessWeightOverridesEntry" + ] +) _UPDATETASKQUEUECONFIGRESPONSE = DESCRIPTOR.message_types_by_name[ "UpdateTaskQueueConfigResponse" ] @@ -696,6 +784,133 @@ _UPDATEWORKERCONFIGRESPONSE = DESCRIPTOR.message_types_by_name[ "UpdateWorkerConfigResponse" ] +_DESCRIBEWORKERREQUEST = DESCRIPTOR.message_types_by_name["DescribeWorkerRequest"] +_DESCRIBEWORKERRESPONSE = DESCRIPTOR.message_types_by_name["DescribeWorkerResponse"] +_COUNTWORKERSREQUEST = DESCRIPTOR.message_types_by_name["CountWorkersRequest"] +_COUNTWORKERSRESPONSE = DESCRIPTOR.message_types_by_name["CountWorkersResponse"] +_PAUSEWORKFLOWEXECUTIONREQUEST = DESCRIPTOR.message_types_by_name[ + "PauseWorkflowExecutionRequest" +] +_PAUSEWORKFLOWEXECUTIONRESPONSE = DESCRIPTOR.message_types_by_name[ + "PauseWorkflowExecutionResponse" +] +_UNPAUSEWORKFLOWEXECUTIONREQUEST = DESCRIPTOR.message_types_by_name[ + "UnpauseWorkflowExecutionRequest" +] +_UNPAUSEWORKFLOWEXECUTIONRESPONSE = DESCRIPTOR.message_types_by_name[ + "UnpauseWorkflowExecutionResponse" +] +_STARTACTIVITYEXECUTIONREQUEST = DESCRIPTOR.message_types_by_name[ + "StartActivityExecutionRequest" +] +_STARTACTIVITYEXECUTIONRESPONSE = DESCRIPTOR.message_types_by_name[ + "StartActivityExecutionResponse" +] +_DESCRIBEACTIVITYEXECUTIONREQUEST = DESCRIPTOR.message_types_by_name[ + "DescribeActivityExecutionRequest" +] +_DESCRIBEACTIVITYEXECUTIONRESPONSE = DESCRIPTOR.message_types_by_name[ + "DescribeActivityExecutionResponse" +] +_POLLACTIVITYEXECUTIONREQUEST = DESCRIPTOR.message_types_by_name[ + "PollActivityExecutionRequest" +] +_POLLACTIVITYEXECUTIONRESPONSE = DESCRIPTOR.message_types_by_name[ + "PollActivityExecutionResponse" +] +_LISTACTIVITYEXECUTIONSREQUEST = DESCRIPTOR.message_types_by_name[ + "ListActivityExecutionsRequest" +] +_LISTACTIVITYEXECUTIONSRESPONSE = DESCRIPTOR.message_types_by_name[ + "ListActivityExecutionsResponse" +] +_STARTNEXUSOPERATIONEXECUTIONREQUEST = DESCRIPTOR.message_types_by_name[ + "StartNexusOperationExecutionRequest" +] +_STARTNEXUSOPERATIONEXECUTIONREQUEST_NEXUSHEADERENTRY = ( + _STARTNEXUSOPERATIONEXECUTIONREQUEST.nested_types_by_name["NexusHeaderEntry"] +) +_STARTNEXUSOPERATIONEXECUTIONRESPONSE = DESCRIPTOR.message_types_by_name[ + "StartNexusOperationExecutionResponse" +] +_DESCRIBENEXUSOPERATIONEXECUTIONREQUEST = DESCRIPTOR.message_types_by_name[ + "DescribeNexusOperationExecutionRequest" +] +_DESCRIBENEXUSOPERATIONEXECUTIONRESPONSE = DESCRIPTOR.message_types_by_name[ + "DescribeNexusOperationExecutionResponse" +] +_POLLNEXUSOPERATIONEXECUTIONREQUEST = DESCRIPTOR.message_types_by_name[ + "PollNexusOperationExecutionRequest" +] +_POLLNEXUSOPERATIONEXECUTIONRESPONSE = DESCRIPTOR.message_types_by_name[ + "PollNexusOperationExecutionResponse" +] +_LISTNEXUSOPERATIONEXECUTIONSREQUEST = DESCRIPTOR.message_types_by_name[ + "ListNexusOperationExecutionsRequest" +] +_LISTNEXUSOPERATIONEXECUTIONSRESPONSE = DESCRIPTOR.message_types_by_name[ + "ListNexusOperationExecutionsResponse" +] +_COUNTACTIVITYEXECUTIONSREQUEST = DESCRIPTOR.message_types_by_name[ + "CountActivityExecutionsRequest" +] +_COUNTACTIVITYEXECUTIONSRESPONSE = DESCRIPTOR.message_types_by_name[ + "CountActivityExecutionsResponse" +] +_COUNTACTIVITYEXECUTIONSRESPONSE_AGGREGATIONGROUP = ( + _COUNTACTIVITYEXECUTIONSRESPONSE.nested_types_by_name["AggregationGroup"] +) +_COUNTNEXUSOPERATIONEXECUTIONSREQUEST = DESCRIPTOR.message_types_by_name[ + "CountNexusOperationExecutionsRequest" +] +_COUNTNEXUSOPERATIONEXECUTIONSRESPONSE = DESCRIPTOR.message_types_by_name[ + "CountNexusOperationExecutionsResponse" +] +_COUNTNEXUSOPERATIONEXECUTIONSRESPONSE_AGGREGATIONGROUP = ( + _COUNTNEXUSOPERATIONEXECUTIONSRESPONSE.nested_types_by_name["AggregationGroup"] +) +_REQUESTCANCELACTIVITYEXECUTIONREQUEST = DESCRIPTOR.message_types_by_name[ + "RequestCancelActivityExecutionRequest" +] +_REQUESTCANCELACTIVITYEXECUTIONRESPONSE = DESCRIPTOR.message_types_by_name[ + "RequestCancelActivityExecutionResponse" +] +_TERMINATEACTIVITYEXECUTIONREQUEST = DESCRIPTOR.message_types_by_name[ + "TerminateActivityExecutionRequest" +] +_TERMINATEACTIVITYEXECUTIONRESPONSE = DESCRIPTOR.message_types_by_name[ + "TerminateActivityExecutionResponse" +] +_DELETEACTIVITYEXECUTIONREQUEST = DESCRIPTOR.message_types_by_name[ + "DeleteActivityExecutionRequest" +] +_DELETEACTIVITYEXECUTIONRESPONSE = DESCRIPTOR.message_types_by_name[ + "DeleteActivityExecutionResponse" +] +_REQUESTCANCELNEXUSOPERATIONEXECUTIONREQUEST = DESCRIPTOR.message_types_by_name[ + "RequestCancelNexusOperationExecutionRequest" +] +_REQUESTCANCELNEXUSOPERATIONEXECUTIONRESPONSE = DESCRIPTOR.message_types_by_name[ + "RequestCancelNexusOperationExecutionResponse" +] +_TERMINATENEXUSOPERATIONEXECUTIONREQUEST = DESCRIPTOR.message_types_by_name[ + "TerminateNexusOperationExecutionRequest" +] +_TERMINATENEXUSOPERATIONEXECUTIONRESPONSE = DESCRIPTOR.message_types_by_name[ + "TerminateNexusOperationExecutionResponse" +] +_DELETENEXUSOPERATIONEXECUTIONREQUEST = DESCRIPTOR.message_types_by_name[ + "DeleteNexusOperationExecutionRequest" +] +_DELETENEXUSOPERATIONEXECUTIONRESPONSE = DESCRIPTOR.message_types_by_name[ + "DeleteNexusOperationExecutionResponse" +] +_POLLWORKFLOWEXECUTIONTIMESKIPPINGREQUEST = DESCRIPTOR.message_types_by_name[ + "PollWorkflowExecutionTimeSkippingRequest" +] +_POLLWORKFLOWEXECUTIONTIMESKIPPINGRESPONSE = DESCRIPTOR.message_types_by_name[ + "PollWorkflowExecutionTimeSkippingResponse" +] RegisterNamespaceRequest = _reflection.GeneratedProtocolMessageType( "RegisterNamespaceRequest", (_message.Message,), @@ -705,12 +920,12 @@ (_message.Message,), { "DESCRIPTOR": _REGISTERNAMESPACEREQUEST_DATAENTRY, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RegisterNamespaceRequest.DataEntry) }, ), "DESCRIPTOR": _REGISTERNAMESPACEREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RegisterNamespaceRequest) }, ) @@ -722,7 +937,7 @@ (_message.Message,), { "DESCRIPTOR": _REGISTERNAMESPACERESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RegisterNamespaceResponse) }, ) @@ -733,7 +948,7 @@ (_message.Message,), { "DESCRIPTOR": _LISTNAMESPACESREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ListNamespacesRequest) }, ) @@ -744,7 +959,7 @@ (_message.Message,), { "DESCRIPTOR": _LISTNAMESPACESRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ListNamespacesResponse) }, ) @@ -755,7 +970,7 @@ (_message.Message,), { "DESCRIPTOR": _DESCRIBENAMESPACEREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DescribeNamespaceRequest) }, ) @@ -766,7 +981,7 @@ (_message.Message,), { "DESCRIPTOR": _DESCRIBENAMESPACERESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DescribeNamespaceResponse) }, ) @@ -777,7 +992,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATENAMESPACEREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateNamespaceRequest) }, ) @@ -788,7 +1003,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATENAMESPACERESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateNamespaceResponse) }, ) @@ -799,7 +1014,7 @@ (_message.Message,), { "DESCRIPTOR": _DEPRECATENAMESPACEREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DeprecateNamespaceRequest) }, ) @@ -810,7 +1025,7 @@ (_message.Message,), { "DESCRIPTOR": _DEPRECATENAMESPACERESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DeprecateNamespaceResponse) }, ) @@ -821,7 +1036,7 @@ (_message.Message,), { "DESCRIPTOR": _STARTWORKFLOWEXECUTIONREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.StartWorkflowExecutionRequest) }, ) @@ -832,7 +1047,7 @@ (_message.Message,), { "DESCRIPTOR": _STARTWORKFLOWEXECUTIONRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.StartWorkflowExecutionResponse) }, ) @@ -843,7 +1058,7 @@ (_message.Message,), { "DESCRIPTOR": _GETWORKFLOWEXECUTIONHISTORYREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryRequest) }, ) @@ -854,7 +1069,7 @@ (_message.Message,), { "DESCRIPTOR": _GETWORKFLOWEXECUTIONHISTORYRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryResponse) }, ) @@ -865,7 +1080,7 @@ (_message.Message,), { "DESCRIPTOR": _GETWORKFLOWEXECUTIONHISTORYREVERSEREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryReverseRequest) }, ) @@ -876,7 +1091,7 @@ (_message.Message,), { "DESCRIPTOR": _GETWORKFLOWEXECUTIONHISTORYREVERSERESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryReverseResponse) }, ) @@ -887,7 +1102,7 @@ (_message.Message,), { "DESCRIPTOR": _POLLWORKFLOWTASKQUEUEREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.PollWorkflowTaskQueueRequest) }, ) @@ -902,12 +1117,12 @@ (_message.Message,), { "DESCRIPTOR": _POLLWORKFLOWTASKQUEUERESPONSE_QUERIESENTRY, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.PollWorkflowTaskQueueResponse.QueriesEntry) }, ), "DESCRIPTOR": _POLLWORKFLOWTASKQUEUERESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.PollWorkflowTaskQueueResponse) }, ) @@ -923,7 +1138,7 @@ (_message.Message,), { "DESCRIPTOR": _RESPONDWORKFLOWTASKCOMPLETEDREQUEST_QUERYRESULTSENTRY, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RespondWorkflowTaskCompletedRequest.QueryResultsEntry) }, ), @@ -932,12 +1147,12 @@ (_message.Message,), { "DESCRIPTOR": _RESPONDWORKFLOWTASKCOMPLETEDREQUEST_CAPABILITIES, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RespondWorkflowTaskCompletedRequest.Capabilities) }, ), "DESCRIPTOR": _RESPONDWORKFLOWTASKCOMPLETEDREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RespondWorkflowTaskCompletedRequest) }, ) @@ -950,7 +1165,7 @@ (_message.Message,), { "DESCRIPTOR": _RESPONDWORKFLOWTASKCOMPLETEDRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RespondWorkflowTaskCompletedResponse) }, ) @@ -961,7 +1176,7 @@ (_message.Message,), { "DESCRIPTOR": _RESPONDWORKFLOWTASKFAILEDREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RespondWorkflowTaskFailedRequest) }, ) @@ -972,7 +1187,7 @@ (_message.Message,), { "DESCRIPTOR": _RESPONDWORKFLOWTASKFAILEDRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RespondWorkflowTaskFailedResponse) }, ) @@ -983,7 +1198,7 @@ (_message.Message,), { "DESCRIPTOR": _POLLACTIVITYTASKQUEUEREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.PollActivityTaskQueueRequest) }, ) @@ -994,7 +1209,7 @@ (_message.Message,), { "DESCRIPTOR": _POLLACTIVITYTASKQUEUERESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.PollActivityTaskQueueResponse) }, ) @@ -1005,7 +1220,7 @@ (_message.Message,), { "DESCRIPTOR": _RECORDACTIVITYTASKHEARTBEATREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RecordActivityTaskHeartbeatRequest) }, ) @@ -1016,7 +1231,7 @@ (_message.Message,), { "DESCRIPTOR": _RECORDACTIVITYTASKHEARTBEATRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RecordActivityTaskHeartbeatResponse) }, ) @@ -1027,7 +1242,7 @@ (_message.Message,), { "DESCRIPTOR": _RECORDACTIVITYTASKHEARTBEATBYIDREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RecordActivityTaskHeartbeatByIdRequest) }, ) @@ -1038,7 +1253,7 @@ (_message.Message,), { "DESCRIPTOR": _RECORDACTIVITYTASKHEARTBEATBYIDRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RecordActivityTaskHeartbeatByIdResponse) }, ) @@ -1049,7 +1264,7 @@ (_message.Message,), { "DESCRIPTOR": _RESPONDACTIVITYTASKCOMPLETEDREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RespondActivityTaskCompletedRequest) }, ) @@ -1060,7 +1275,7 @@ (_message.Message,), { "DESCRIPTOR": _RESPONDACTIVITYTASKCOMPLETEDRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RespondActivityTaskCompletedResponse) }, ) @@ -1071,7 +1286,7 @@ (_message.Message,), { "DESCRIPTOR": _RESPONDACTIVITYTASKCOMPLETEDBYIDREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RespondActivityTaskCompletedByIdRequest) }, ) @@ -1082,7 +1297,7 @@ (_message.Message,), { "DESCRIPTOR": _RESPONDACTIVITYTASKCOMPLETEDBYIDRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RespondActivityTaskCompletedByIdResponse) }, ) @@ -1093,7 +1308,7 @@ (_message.Message,), { "DESCRIPTOR": _RESPONDACTIVITYTASKFAILEDREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RespondActivityTaskFailedRequest) }, ) @@ -1104,7 +1319,7 @@ (_message.Message,), { "DESCRIPTOR": _RESPONDACTIVITYTASKFAILEDRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RespondActivityTaskFailedResponse) }, ) @@ -1115,7 +1330,7 @@ (_message.Message,), { "DESCRIPTOR": _RESPONDACTIVITYTASKFAILEDBYIDREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RespondActivityTaskFailedByIdRequest) }, ) @@ -1126,7 +1341,7 @@ (_message.Message,), { "DESCRIPTOR": _RESPONDACTIVITYTASKFAILEDBYIDRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RespondActivityTaskFailedByIdResponse) }, ) @@ -1137,7 +1352,7 @@ (_message.Message,), { "DESCRIPTOR": _RESPONDACTIVITYTASKCANCELEDREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RespondActivityTaskCanceledRequest) }, ) @@ -1148,7 +1363,7 @@ (_message.Message,), { "DESCRIPTOR": _RESPONDACTIVITYTASKCANCELEDRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RespondActivityTaskCanceledResponse) }, ) @@ -1159,7 +1374,7 @@ (_message.Message,), { "DESCRIPTOR": _RESPONDACTIVITYTASKCANCELEDBYIDREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RespondActivityTaskCanceledByIdRequest) }, ) @@ -1170,7 +1385,7 @@ (_message.Message,), { "DESCRIPTOR": _RESPONDACTIVITYTASKCANCELEDBYIDRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RespondActivityTaskCanceledByIdResponse) }, ) @@ -1181,7 +1396,7 @@ (_message.Message,), { "DESCRIPTOR": _REQUESTCANCELWORKFLOWEXECUTIONREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RequestCancelWorkflowExecutionRequest) }, ) @@ -1192,7 +1407,7 @@ (_message.Message,), { "DESCRIPTOR": _REQUESTCANCELWORKFLOWEXECUTIONRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RequestCancelWorkflowExecutionResponse) }, ) @@ -1203,7 +1418,7 @@ (_message.Message,), { "DESCRIPTOR": _SIGNALWORKFLOWEXECUTIONREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.SignalWorkflowExecutionRequest) }, ) @@ -1214,7 +1429,7 @@ (_message.Message,), { "DESCRIPTOR": _SIGNALWORKFLOWEXECUTIONRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.SignalWorkflowExecutionResponse) }, ) @@ -1225,7 +1440,7 @@ (_message.Message,), { "DESCRIPTOR": _SIGNALWITHSTARTWORKFLOWEXECUTIONREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest) }, ) @@ -1236,7 +1451,7 @@ (_message.Message,), { "DESCRIPTOR": _SIGNALWITHSTARTWORKFLOWEXECUTIONRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse) }, ) @@ -1247,7 +1462,7 @@ (_message.Message,), { "DESCRIPTOR": _RESETWORKFLOWEXECUTIONREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ResetWorkflowExecutionRequest) }, ) @@ -1258,7 +1473,7 @@ (_message.Message,), { "DESCRIPTOR": _RESETWORKFLOWEXECUTIONRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ResetWorkflowExecutionResponse) }, ) @@ -1269,7 +1484,7 @@ (_message.Message,), { "DESCRIPTOR": _TERMINATEWORKFLOWEXECUTIONREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.TerminateWorkflowExecutionRequest) }, ) @@ -1280,7 +1495,7 @@ (_message.Message,), { "DESCRIPTOR": _TERMINATEWORKFLOWEXECUTIONRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.TerminateWorkflowExecutionResponse) }, ) @@ -1291,7 +1506,7 @@ (_message.Message,), { "DESCRIPTOR": _DELETEWORKFLOWEXECUTIONREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DeleteWorkflowExecutionRequest) }, ) @@ -1302,7 +1517,7 @@ (_message.Message,), { "DESCRIPTOR": _DELETEWORKFLOWEXECUTIONRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DeleteWorkflowExecutionResponse) }, ) @@ -1313,7 +1528,7 @@ (_message.Message,), { "DESCRIPTOR": _LISTOPENWORKFLOWEXECUTIONSREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ListOpenWorkflowExecutionsRequest) }, ) @@ -1324,7 +1539,7 @@ (_message.Message,), { "DESCRIPTOR": _LISTOPENWORKFLOWEXECUTIONSRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ListOpenWorkflowExecutionsResponse) }, ) @@ -1335,7 +1550,7 @@ (_message.Message,), { "DESCRIPTOR": _LISTCLOSEDWORKFLOWEXECUTIONSREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ListClosedWorkflowExecutionsRequest) }, ) @@ -1346,7 +1561,7 @@ (_message.Message,), { "DESCRIPTOR": _LISTCLOSEDWORKFLOWEXECUTIONSRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ListClosedWorkflowExecutionsResponse) }, ) @@ -1357,7 +1572,7 @@ (_message.Message,), { "DESCRIPTOR": _LISTWORKFLOWEXECUTIONSREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ListWorkflowExecutionsRequest) }, ) @@ -1368,7 +1583,7 @@ (_message.Message,), { "DESCRIPTOR": _LISTWORKFLOWEXECUTIONSRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ListWorkflowExecutionsResponse) }, ) @@ -1379,7 +1594,7 @@ (_message.Message,), { "DESCRIPTOR": _LISTARCHIVEDWORKFLOWEXECUTIONSREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ListArchivedWorkflowExecutionsRequest) }, ) @@ -1390,7 +1605,7 @@ (_message.Message,), { "DESCRIPTOR": _LISTARCHIVEDWORKFLOWEXECUTIONSRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ListArchivedWorkflowExecutionsResponse) }, ) @@ -1401,7 +1616,7 @@ (_message.Message,), { "DESCRIPTOR": _SCANWORKFLOWEXECUTIONSREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ScanWorkflowExecutionsRequest) }, ) @@ -1412,7 +1627,7 @@ (_message.Message,), { "DESCRIPTOR": _SCANWORKFLOWEXECUTIONSRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ScanWorkflowExecutionsResponse) }, ) @@ -1423,7 +1638,7 @@ (_message.Message,), { "DESCRIPTOR": _COUNTWORKFLOWEXECUTIONSREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.CountWorkflowExecutionsRequest) }, ) @@ -1438,12 +1653,12 @@ (_message.Message,), { "DESCRIPTOR": _COUNTWORKFLOWEXECUTIONSRESPONSE_AGGREGATIONGROUP, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.CountWorkflowExecutionsResponse.AggregationGroup) }, ), "DESCRIPTOR": _COUNTWORKFLOWEXECUTIONSRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.CountWorkflowExecutionsResponse) }, ) @@ -1455,7 +1670,7 @@ (_message.Message,), { "DESCRIPTOR": _GETSEARCHATTRIBUTESREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.GetSearchAttributesRequest) }, ) @@ -1470,12 +1685,12 @@ (_message.Message,), { "DESCRIPTOR": _GETSEARCHATTRIBUTESRESPONSE_KEYSENTRY, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.GetSearchAttributesResponse.KeysEntry) }, ), "DESCRIPTOR": _GETSEARCHATTRIBUTESRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.GetSearchAttributesResponse) }, ) @@ -1487,7 +1702,7 @@ (_message.Message,), { "DESCRIPTOR": _RESPONDQUERYTASKCOMPLETEDREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RespondQueryTaskCompletedRequest) }, ) @@ -1498,7 +1713,7 @@ (_message.Message,), { "DESCRIPTOR": _RESPONDQUERYTASKCOMPLETEDRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RespondQueryTaskCompletedResponse) }, ) @@ -1509,7 +1724,7 @@ (_message.Message,), { "DESCRIPTOR": _RESETSTICKYTASKQUEUEREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ResetStickyTaskQueueRequest) }, ) @@ -1520,7 +1735,7 @@ (_message.Message,), { "DESCRIPTOR": _RESETSTICKYTASKQUEUERESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ResetStickyTaskQueueResponse) }, ) @@ -1531,7 +1746,7 @@ (_message.Message,), { "DESCRIPTOR": _SHUTDOWNWORKERREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ShutdownWorkerRequest) }, ) @@ -1542,7 +1757,7 @@ (_message.Message,), { "DESCRIPTOR": _SHUTDOWNWORKERRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ShutdownWorkerResponse) }, ) @@ -1553,7 +1768,7 @@ (_message.Message,), { "DESCRIPTOR": _QUERYWORKFLOWREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.QueryWorkflowRequest) }, ) @@ -1564,7 +1779,7 @@ (_message.Message,), { "DESCRIPTOR": _QUERYWORKFLOWRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.QueryWorkflowResponse) }, ) @@ -1575,7 +1790,7 @@ (_message.Message,), { "DESCRIPTOR": _DESCRIBEWORKFLOWEXECUTIONREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DescribeWorkflowExecutionRequest) }, ) @@ -1586,7 +1801,7 @@ (_message.Message,), { "DESCRIPTOR": _DESCRIBEWORKFLOWEXECUTIONRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DescribeWorkflowExecutionResponse) }, ) @@ -1597,7 +1812,7 @@ (_message.Message,), { "DESCRIPTOR": _DESCRIBETASKQUEUEREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DescribeTaskQueueRequest) }, ) @@ -1612,7 +1827,7 @@ (_message.Message,), { "DESCRIPTOR": _DESCRIBETASKQUEUERESPONSE_STATSBYPRIORITYKEYENTRY, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DescribeTaskQueueResponse.StatsByPriorityKeyEntry) }, ), @@ -1621,7 +1836,7 @@ (_message.Message,), { "DESCRIPTOR": _DESCRIBETASKQUEUERESPONSE_EFFECTIVERATELIMIT, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DescribeTaskQueueResponse.EffectiveRateLimit) }, ), @@ -1630,12 +1845,12 @@ (_message.Message,), { "DESCRIPTOR": _DESCRIBETASKQUEUERESPONSE_VERSIONSINFOENTRY, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DescribeTaskQueueResponse.VersionsInfoEntry) }, ), "DESCRIPTOR": _DESCRIBETASKQUEUERESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DescribeTaskQueueResponse) }, ) @@ -1649,7 +1864,7 @@ (_message.Message,), { "DESCRIPTOR": _GETCLUSTERINFOREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.GetClusterInfoRequest) }, ) @@ -1664,12 +1879,12 @@ (_message.Message,), { "DESCRIPTOR": _GETCLUSTERINFORESPONSE_SUPPORTEDCLIENTSENTRY, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.GetClusterInfoResponse.SupportedClientsEntry) }, ), "DESCRIPTOR": _GETCLUSTERINFORESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.GetClusterInfoResponse) }, ) @@ -1681,7 +1896,7 @@ (_message.Message,), { "DESCRIPTOR": _GETSYSTEMINFOREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.GetSystemInfoRequest) }, ) @@ -1696,12 +1911,12 @@ (_message.Message,), { "DESCRIPTOR": _GETSYSTEMINFORESPONSE_CAPABILITIES, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.GetSystemInfoResponse.Capabilities) }, ), "DESCRIPTOR": _GETSYSTEMINFORESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.GetSystemInfoResponse) }, ) @@ -1713,7 +1928,7 @@ (_message.Message,), { "DESCRIPTOR": _LISTTASKQUEUEPARTITIONSREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ListTaskQueuePartitionsRequest) }, ) @@ -1724,7 +1939,7 @@ (_message.Message,), { "DESCRIPTOR": _LISTTASKQUEUEPARTITIONSRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ListTaskQueuePartitionsResponse) }, ) @@ -1735,7 +1950,7 @@ (_message.Message,), { "DESCRIPTOR": _CREATESCHEDULEREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.CreateScheduleRequest) }, ) @@ -1746,7 +1961,7 @@ (_message.Message,), { "DESCRIPTOR": _CREATESCHEDULERESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.CreateScheduleResponse) }, ) @@ -1757,7 +1972,7 @@ (_message.Message,), { "DESCRIPTOR": _DESCRIBESCHEDULEREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DescribeScheduleRequest) }, ) @@ -1768,7 +1983,7 @@ (_message.Message,), { "DESCRIPTOR": _DESCRIBESCHEDULERESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DescribeScheduleResponse) }, ) @@ -1779,7 +1994,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATESCHEDULEREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateScheduleRequest) }, ) @@ -1790,7 +2005,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATESCHEDULERESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateScheduleResponse) }, ) @@ -1801,7 +2016,7 @@ (_message.Message,), { "DESCRIPTOR": _PATCHSCHEDULEREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.PatchScheduleRequest) }, ) @@ -1812,7 +2027,7 @@ (_message.Message,), { "DESCRIPTOR": _PATCHSCHEDULERESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.PatchScheduleResponse) }, ) @@ -1823,7 +2038,7 @@ (_message.Message,), { "DESCRIPTOR": _LISTSCHEDULEMATCHINGTIMESREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ListScheduleMatchingTimesRequest) }, ) @@ -1834,7 +2049,7 @@ (_message.Message,), { "DESCRIPTOR": _LISTSCHEDULEMATCHINGTIMESRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ListScheduleMatchingTimesResponse) }, ) @@ -1845,7 +2060,7 @@ (_message.Message,), { "DESCRIPTOR": _DELETESCHEDULEREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DeleteScheduleRequest) }, ) @@ -1856,7 +2071,7 @@ (_message.Message,), { "DESCRIPTOR": _DELETESCHEDULERESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DeleteScheduleResponse) }, ) @@ -1867,7 +2082,7 @@ (_message.Message,), { "DESCRIPTOR": _LISTSCHEDULESREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ListSchedulesRequest) }, ) @@ -1878,12 +2093,44 @@ (_message.Message,), { "DESCRIPTOR": _LISTSCHEDULESRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ListSchedulesResponse) }, ) _sym_db.RegisterMessage(ListSchedulesResponse) +CountSchedulesRequest = _reflection.GeneratedProtocolMessageType( + "CountSchedulesRequest", + (_message.Message,), + { + "DESCRIPTOR": _COUNTSCHEDULESREQUEST, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.CountSchedulesRequest) + }, +) +_sym_db.RegisterMessage(CountSchedulesRequest) + +CountSchedulesResponse = _reflection.GeneratedProtocolMessageType( + "CountSchedulesResponse", + (_message.Message,), + { + "AggregationGroup": _reflection.GeneratedProtocolMessageType( + "AggregationGroup", + (_message.Message,), + { + "DESCRIPTOR": _COUNTSCHEDULESRESPONSE_AGGREGATIONGROUP, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.CountSchedulesResponse.AggregationGroup) + }, + ), + "DESCRIPTOR": _COUNTSCHEDULESRESPONSE, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.CountSchedulesResponse) + }, +) +_sym_db.RegisterMessage(CountSchedulesResponse) +_sym_db.RegisterMessage(CountSchedulesResponse.AggregationGroup) + UpdateWorkerBuildIdCompatibilityRequest = _reflection.GeneratedProtocolMessageType( "UpdateWorkerBuildIdCompatibilityRequest", (_message.Message,), @@ -1893,7 +2140,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATEWORKERBUILDIDCOMPATIBILITYREQUEST_ADDNEWCOMPATIBLEVERSION, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest.AddNewCompatibleVersion) }, ), @@ -1902,12 +2149,12 @@ (_message.Message,), { "DESCRIPTOR": _UPDATEWORKERBUILDIDCOMPATIBILITYREQUEST_MERGESETS, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest.MergeSets) }, ), "DESCRIPTOR": _UPDATEWORKERBUILDIDCOMPATIBILITYREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest) }, ) @@ -1920,7 +2167,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATEWORKERBUILDIDCOMPATIBILITYRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityResponse) }, ) @@ -1931,7 +2178,7 @@ (_message.Message,), { "DESCRIPTOR": _GETWORKERBUILDIDCOMPATIBILITYREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.GetWorkerBuildIdCompatibilityRequest) }, ) @@ -1942,7 +2189,7 @@ (_message.Message,), { "DESCRIPTOR": _GETWORKERBUILDIDCOMPATIBILITYRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.GetWorkerBuildIdCompatibilityResponse) }, ) @@ -1957,7 +2204,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATEWORKERVERSIONINGRULESREQUEST_INSERTBUILDIDASSIGNMENTRULE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateWorkerVersioningRulesRequest.InsertBuildIdAssignmentRule) }, ), @@ -1966,7 +2213,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATEWORKERVERSIONINGRULESREQUEST_REPLACEBUILDIDASSIGNMENTRULE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateWorkerVersioningRulesRequest.ReplaceBuildIdAssignmentRule) }, ), @@ -1975,7 +2222,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATEWORKERVERSIONINGRULESREQUEST_DELETEBUILDIDASSIGNMENTRULE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateWorkerVersioningRulesRequest.DeleteBuildIdAssignmentRule) }, ), @@ -1984,7 +2231,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATEWORKERVERSIONINGRULESREQUEST_ADDCOMPATIBLEBUILDIDREDIRECTRULE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateWorkerVersioningRulesRequest.AddCompatibleBuildIdRedirectRule) }, ), @@ -1993,7 +2240,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATEWORKERVERSIONINGRULESREQUEST_REPLACECOMPATIBLEBUILDIDREDIRECTRULE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateWorkerVersioningRulesRequest.ReplaceCompatibleBuildIdRedirectRule) }, ), @@ -2002,7 +2249,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATEWORKERVERSIONINGRULESREQUEST_DELETECOMPATIBLEBUILDIDREDIRECTRULE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateWorkerVersioningRulesRequest.DeleteCompatibleBuildIdRedirectRule) }, ), @@ -2011,12 +2258,12 @@ (_message.Message,), { "DESCRIPTOR": _UPDATEWORKERVERSIONINGRULESREQUEST_COMMITBUILDID, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateWorkerVersioningRulesRequest.CommitBuildId) }, ), "DESCRIPTOR": _UPDATEWORKERVERSIONINGRULESREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateWorkerVersioningRulesRequest) }, ) @@ -2040,7 +2287,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATEWORKERVERSIONINGRULESRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateWorkerVersioningRulesResponse) }, ) @@ -2051,7 +2298,7 @@ (_message.Message,), { "DESCRIPTOR": _GETWORKERVERSIONINGRULESREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.GetWorkerVersioningRulesRequest) }, ) @@ -2062,7 +2309,7 @@ (_message.Message,), { "DESCRIPTOR": _GETWORKERVERSIONINGRULESRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.GetWorkerVersioningRulesResponse) }, ) @@ -2073,7 +2320,7 @@ (_message.Message,), { "DESCRIPTOR": _GETWORKERTASKREACHABILITYREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.GetWorkerTaskReachabilityRequest) }, ) @@ -2084,7 +2331,7 @@ (_message.Message,), { "DESCRIPTOR": _GETWORKERTASKREACHABILITYRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.GetWorkerTaskReachabilityResponse) }, ) @@ -2095,7 +2342,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATEWORKFLOWEXECUTIONREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateWorkflowExecutionRequest) }, ) @@ -2106,7 +2353,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATEWORKFLOWEXECUTIONRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateWorkflowExecutionResponse) }, ) @@ -2117,7 +2364,7 @@ (_message.Message,), { "DESCRIPTOR": _STARTBATCHOPERATIONREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.StartBatchOperationRequest) }, ) @@ -2128,7 +2375,7 @@ (_message.Message,), { "DESCRIPTOR": _STARTBATCHOPERATIONRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.StartBatchOperationResponse) }, ) @@ -2139,7 +2386,7 @@ (_message.Message,), { "DESCRIPTOR": _STOPBATCHOPERATIONREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.StopBatchOperationRequest) }, ) @@ -2150,7 +2397,7 @@ (_message.Message,), { "DESCRIPTOR": _STOPBATCHOPERATIONRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.StopBatchOperationResponse) }, ) @@ -2161,7 +2408,7 @@ (_message.Message,), { "DESCRIPTOR": _DESCRIBEBATCHOPERATIONREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DescribeBatchOperationRequest) }, ) @@ -2172,7 +2419,7 @@ (_message.Message,), { "DESCRIPTOR": _DESCRIBEBATCHOPERATIONRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DescribeBatchOperationResponse) }, ) @@ -2183,7 +2430,7 @@ (_message.Message,), { "DESCRIPTOR": _LISTBATCHOPERATIONSREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ListBatchOperationsRequest) }, ) @@ -2194,7 +2441,7 @@ (_message.Message,), { "DESCRIPTOR": _LISTBATCHOPERATIONSRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ListBatchOperationsResponse) }, ) @@ -2205,7 +2452,7 @@ (_message.Message,), { "DESCRIPTOR": _POLLWORKFLOWEXECUTIONUPDATEREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.PollWorkflowExecutionUpdateRequest) }, ) @@ -2216,7 +2463,7 @@ (_message.Message,), { "DESCRIPTOR": _POLLWORKFLOWEXECUTIONUPDATERESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.PollWorkflowExecutionUpdateResponse) }, ) @@ -2227,7 +2474,7 @@ (_message.Message,), { "DESCRIPTOR": _POLLNEXUSTASKQUEUEREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.PollNexusTaskQueueRequest) }, ) @@ -2238,7 +2485,7 @@ (_message.Message,), { "DESCRIPTOR": _POLLNEXUSTASKQUEUERESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.PollNexusTaskQueueResponse) }, ) @@ -2249,7 +2496,7 @@ (_message.Message,), { "DESCRIPTOR": _RESPONDNEXUSTASKCOMPLETEDREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RespondNexusTaskCompletedRequest) }, ) @@ -2260,7 +2507,7 @@ (_message.Message,), { "DESCRIPTOR": _RESPONDNEXUSTASKCOMPLETEDRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RespondNexusTaskCompletedResponse) }, ) @@ -2271,7 +2518,7 @@ (_message.Message,), { "DESCRIPTOR": _RESPONDNEXUSTASKFAILEDREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RespondNexusTaskFailedRequest) }, ) @@ -2282,7 +2529,7 @@ (_message.Message,), { "DESCRIPTOR": _RESPONDNEXUSTASKFAILEDRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RespondNexusTaskFailedResponse) }, ) @@ -2297,12 +2544,12 @@ (_message.Message,), { "DESCRIPTOR": _EXECUTEMULTIOPERATIONREQUEST_OPERATION, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ExecuteMultiOperationRequest.Operation) }, ), "DESCRIPTOR": _EXECUTEMULTIOPERATIONREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ExecuteMultiOperationRequest) }, ) @@ -2318,12 +2565,12 @@ (_message.Message,), { "DESCRIPTOR": _EXECUTEMULTIOPERATIONRESPONSE_RESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ExecuteMultiOperationResponse.Response) }, ), "DESCRIPTOR": _EXECUTEMULTIOPERATIONRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ExecuteMultiOperationResponse) }, ) @@ -2335,95 +2582,183 @@ (_message.Message,), { "DESCRIPTOR": _UPDATEACTIVITYOPTIONSREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateActivityOptionsRequest) }, ) _sym_db.RegisterMessage(UpdateActivityOptionsRequest) +UpdateActivityExecutionOptionsRequest = _reflection.GeneratedProtocolMessageType( + "UpdateActivityExecutionOptionsRequest", + (_message.Message,), + { + "DESCRIPTOR": _UPDATEACTIVITYEXECUTIONOPTIONSREQUEST, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateActivityExecutionOptionsRequest) + }, +) +_sym_db.RegisterMessage(UpdateActivityExecutionOptionsRequest) + UpdateActivityOptionsResponse = _reflection.GeneratedProtocolMessageType( "UpdateActivityOptionsResponse", (_message.Message,), { "DESCRIPTOR": _UPDATEACTIVITYOPTIONSRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateActivityOptionsResponse) }, ) _sym_db.RegisterMessage(UpdateActivityOptionsResponse) +UpdateActivityExecutionOptionsResponse = _reflection.GeneratedProtocolMessageType( + "UpdateActivityExecutionOptionsResponse", + (_message.Message,), + { + "DESCRIPTOR": _UPDATEACTIVITYEXECUTIONOPTIONSRESPONSE, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateActivityExecutionOptionsResponse) + }, +) +_sym_db.RegisterMessage(UpdateActivityExecutionOptionsResponse) + PauseActivityRequest = _reflection.GeneratedProtocolMessageType( "PauseActivityRequest", (_message.Message,), { "DESCRIPTOR": _PAUSEACTIVITYREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.PauseActivityRequest) }, ) _sym_db.RegisterMessage(PauseActivityRequest) +PauseActivityExecutionRequest = _reflection.GeneratedProtocolMessageType( + "PauseActivityExecutionRequest", + (_message.Message,), + { + "DESCRIPTOR": _PAUSEACTIVITYEXECUTIONREQUEST, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.PauseActivityExecutionRequest) + }, +) +_sym_db.RegisterMessage(PauseActivityExecutionRequest) + PauseActivityResponse = _reflection.GeneratedProtocolMessageType( "PauseActivityResponse", (_message.Message,), { "DESCRIPTOR": _PAUSEACTIVITYRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.PauseActivityResponse) }, ) _sym_db.RegisterMessage(PauseActivityResponse) +PauseActivityExecutionResponse = _reflection.GeneratedProtocolMessageType( + "PauseActivityExecutionResponse", + (_message.Message,), + { + "DESCRIPTOR": _PAUSEACTIVITYEXECUTIONRESPONSE, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.PauseActivityExecutionResponse) + }, +) +_sym_db.RegisterMessage(PauseActivityExecutionResponse) + UnpauseActivityRequest = _reflection.GeneratedProtocolMessageType( "UnpauseActivityRequest", (_message.Message,), { "DESCRIPTOR": _UNPAUSEACTIVITYREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UnpauseActivityRequest) }, ) _sym_db.RegisterMessage(UnpauseActivityRequest) +UnpauseActivityExecutionRequest = _reflection.GeneratedProtocolMessageType( + "UnpauseActivityExecutionRequest", + (_message.Message,), + { + "DESCRIPTOR": _UNPAUSEACTIVITYEXECUTIONREQUEST, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UnpauseActivityExecutionRequest) + }, +) +_sym_db.RegisterMessage(UnpauseActivityExecutionRequest) + UnpauseActivityResponse = _reflection.GeneratedProtocolMessageType( "UnpauseActivityResponse", (_message.Message,), { "DESCRIPTOR": _UNPAUSEACTIVITYRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UnpauseActivityResponse) }, ) _sym_db.RegisterMessage(UnpauseActivityResponse) +UnpauseActivityExecutionResponse = _reflection.GeneratedProtocolMessageType( + "UnpauseActivityExecutionResponse", + (_message.Message,), + { + "DESCRIPTOR": _UNPAUSEACTIVITYEXECUTIONRESPONSE, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UnpauseActivityExecutionResponse) + }, +) +_sym_db.RegisterMessage(UnpauseActivityExecutionResponse) + ResetActivityRequest = _reflection.GeneratedProtocolMessageType( "ResetActivityRequest", (_message.Message,), { "DESCRIPTOR": _RESETACTIVITYREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ResetActivityRequest) }, ) _sym_db.RegisterMessage(ResetActivityRequest) +ResetActivityExecutionRequest = _reflection.GeneratedProtocolMessageType( + "ResetActivityExecutionRequest", + (_message.Message,), + { + "DESCRIPTOR": _RESETACTIVITYEXECUTIONREQUEST, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ResetActivityExecutionRequest) + }, +) +_sym_db.RegisterMessage(ResetActivityExecutionRequest) + ResetActivityResponse = _reflection.GeneratedProtocolMessageType( "ResetActivityResponse", (_message.Message,), { "DESCRIPTOR": _RESETACTIVITYRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ResetActivityResponse) }, ) _sym_db.RegisterMessage(ResetActivityResponse) +ResetActivityExecutionResponse = _reflection.GeneratedProtocolMessageType( + "ResetActivityExecutionResponse", + (_message.Message,), + { + "DESCRIPTOR": _RESETACTIVITYEXECUTIONRESPONSE, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ResetActivityExecutionResponse) + }, +) +_sym_db.RegisterMessage(ResetActivityExecutionResponse) + UpdateWorkflowExecutionOptionsRequest = _reflection.GeneratedProtocolMessageType( "UpdateWorkflowExecutionOptionsRequest", (_message.Message,), { "DESCRIPTOR": _UPDATEWORKFLOWEXECUTIONOPTIONSREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateWorkflowExecutionOptionsRequest) }, ) @@ -2434,7 +2769,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATEWORKFLOWEXECUTIONOPTIONSRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateWorkflowExecutionOptionsResponse) }, ) @@ -2445,7 +2780,7 @@ (_message.Message,), { "DESCRIPTOR": _DESCRIBEDEPLOYMENTREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DescribeDeploymentRequest) }, ) @@ -2456,7 +2791,7 @@ (_message.Message,), { "DESCRIPTOR": _DESCRIBEDEPLOYMENTRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DescribeDeploymentResponse) }, ) @@ -2467,7 +2802,7 @@ (_message.Message,), { "DESCRIPTOR": _DESCRIBEWORKERDEPLOYMENTVERSIONREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DescribeWorkerDeploymentVersionRequest) }, ) @@ -2486,17 +2821,17 @@ (_message.Message,), { "DESCRIPTOR": _DESCRIBEWORKERDEPLOYMENTVERSIONRESPONSE_VERSIONTASKQUEUE_STATSBYPRIORITYKEYENTRY, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DescribeWorkerDeploymentVersionResponse.VersionTaskQueue.StatsByPriorityKeyEntry) }, ), "DESCRIPTOR": _DESCRIBEWORKERDEPLOYMENTVERSIONRESPONSE_VERSIONTASKQUEUE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DescribeWorkerDeploymentVersionResponse.VersionTaskQueue) }, ), "DESCRIPTOR": _DESCRIBEWORKERDEPLOYMENTVERSIONRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DescribeWorkerDeploymentVersionResponse) }, ) @@ -2511,7 +2846,7 @@ (_message.Message,), { "DESCRIPTOR": _DESCRIBEWORKERDEPLOYMENTREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DescribeWorkerDeploymentRequest) }, ) @@ -2522,7 +2857,7 @@ (_message.Message,), { "DESCRIPTOR": _DESCRIBEWORKERDEPLOYMENTRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DescribeWorkerDeploymentResponse) }, ) @@ -2533,7 +2868,7 @@ (_message.Message,), { "DESCRIPTOR": _LISTDEPLOYMENTSREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ListDeploymentsRequest) }, ) @@ -2544,7 +2879,7 @@ (_message.Message,), { "DESCRIPTOR": _LISTDEPLOYMENTSRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ListDeploymentsResponse) }, ) @@ -2555,7 +2890,7 @@ (_message.Message,), { "DESCRIPTOR": _SETCURRENTDEPLOYMENTREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.SetCurrentDeploymentRequest) }, ) @@ -2566,7 +2901,7 @@ (_message.Message,), { "DESCRIPTOR": _SETCURRENTDEPLOYMENTRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.SetCurrentDeploymentResponse) }, ) @@ -2577,7 +2912,7 @@ (_message.Message,), { "DESCRIPTOR": _SETWORKERDEPLOYMENTCURRENTVERSIONREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.SetWorkerDeploymentCurrentVersionRequest) }, ) @@ -2588,7 +2923,7 @@ (_message.Message,), { "DESCRIPTOR": _SETWORKERDEPLOYMENTCURRENTVERSIONRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.SetWorkerDeploymentCurrentVersionResponse) }, ) @@ -2599,7 +2934,7 @@ (_message.Message,), { "DESCRIPTOR": _SETWORKERDEPLOYMENTRAMPINGVERSIONREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.SetWorkerDeploymentRampingVersionRequest) }, ) @@ -2610,18 +2945,40 @@ (_message.Message,), { "DESCRIPTOR": _SETWORKERDEPLOYMENTRAMPINGVERSIONRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.SetWorkerDeploymentRampingVersionResponse) }, ) _sym_db.RegisterMessage(SetWorkerDeploymentRampingVersionResponse) +CreateWorkerDeploymentRequest = _reflection.GeneratedProtocolMessageType( + "CreateWorkerDeploymentRequest", + (_message.Message,), + { + "DESCRIPTOR": _CREATEWORKERDEPLOYMENTREQUEST, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.CreateWorkerDeploymentRequest) + }, +) +_sym_db.RegisterMessage(CreateWorkerDeploymentRequest) + +CreateWorkerDeploymentResponse = _reflection.GeneratedProtocolMessageType( + "CreateWorkerDeploymentResponse", + (_message.Message,), + { + "DESCRIPTOR": _CREATEWORKERDEPLOYMENTRESPONSE, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.CreateWorkerDeploymentResponse) + }, +) +_sym_db.RegisterMessage(CreateWorkerDeploymentResponse) + ListWorkerDeploymentsRequest = _reflection.GeneratedProtocolMessageType( "ListWorkerDeploymentsRequest", (_message.Message,), { "DESCRIPTOR": _LISTWORKERDEPLOYMENTSREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ListWorkerDeploymentsRequest) }, ) @@ -2636,24 +2993,46 @@ (_message.Message,), { "DESCRIPTOR": _LISTWORKERDEPLOYMENTSRESPONSE_WORKERDEPLOYMENTSUMMARY, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ListWorkerDeploymentsResponse.WorkerDeploymentSummary) }, ), "DESCRIPTOR": _LISTWORKERDEPLOYMENTSRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ListWorkerDeploymentsResponse) }, ) _sym_db.RegisterMessage(ListWorkerDeploymentsResponse) _sym_db.RegisterMessage(ListWorkerDeploymentsResponse.WorkerDeploymentSummary) +CreateWorkerDeploymentVersionRequest = _reflection.GeneratedProtocolMessageType( + "CreateWorkerDeploymentVersionRequest", + (_message.Message,), + { + "DESCRIPTOR": _CREATEWORKERDEPLOYMENTVERSIONREQUEST, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.CreateWorkerDeploymentVersionRequest) + }, +) +_sym_db.RegisterMessage(CreateWorkerDeploymentVersionRequest) + +CreateWorkerDeploymentVersionResponse = _reflection.GeneratedProtocolMessageType( + "CreateWorkerDeploymentVersionResponse", + (_message.Message,), + { + "DESCRIPTOR": _CREATEWORKERDEPLOYMENTVERSIONRESPONSE, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.CreateWorkerDeploymentVersionResponse) + }, +) +_sym_db.RegisterMessage(CreateWorkerDeploymentVersionResponse) + DeleteWorkerDeploymentVersionRequest = _reflection.GeneratedProtocolMessageType( "DeleteWorkerDeploymentVersionRequest", (_message.Message,), { "DESCRIPTOR": _DELETEWORKERDEPLOYMENTVERSIONREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DeleteWorkerDeploymentVersionRequest) }, ) @@ -2664,7 +3043,7 @@ (_message.Message,), { "DESCRIPTOR": _DELETEWORKERDEPLOYMENTVERSIONRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DeleteWorkerDeploymentVersionResponse) }, ) @@ -2675,7 +3054,7 @@ (_message.Message,), { "DESCRIPTOR": _DELETEWORKERDEPLOYMENTREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DeleteWorkerDeploymentRequest) }, ) @@ -2686,12 +3065,84 @@ (_message.Message,), { "DESCRIPTOR": _DELETEWORKERDEPLOYMENTRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DeleteWorkerDeploymentResponse) }, ) _sym_db.RegisterMessage(DeleteWorkerDeploymentResponse) +UpdateWorkerDeploymentVersionComputeConfigRequest = _reflection.GeneratedProtocolMessageType( + "UpdateWorkerDeploymentVersionComputeConfigRequest", + (_message.Message,), + { + "ComputeConfigScalingGroupsEntry": _reflection.GeneratedProtocolMessageType( + "ComputeConfigScalingGroupsEntry", + (_message.Message,), + { + "DESCRIPTOR": _UPDATEWORKERDEPLOYMENTVERSIONCOMPUTECONFIGREQUEST_COMPUTECONFIGSCALINGGROUPSENTRY, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateWorkerDeploymentVersionComputeConfigRequest.ComputeConfigScalingGroupsEntry) + }, + ), + "DESCRIPTOR": _UPDATEWORKERDEPLOYMENTVERSIONCOMPUTECONFIGREQUEST, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateWorkerDeploymentVersionComputeConfigRequest) + }, +) +_sym_db.RegisterMessage(UpdateWorkerDeploymentVersionComputeConfigRequest) +_sym_db.RegisterMessage( + UpdateWorkerDeploymentVersionComputeConfigRequest.ComputeConfigScalingGroupsEntry +) + +UpdateWorkerDeploymentVersionComputeConfigResponse = ( + _reflection.GeneratedProtocolMessageType( + "UpdateWorkerDeploymentVersionComputeConfigResponse", + (_message.Message,), + { + "DESCRIPTOR": _UPDATEWORKERDEPLOYMENTVERSIONCOMPUTECONFIGRESPONSE, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateWorkerDeploymentVersionComputeConfigResponse) + }, + ) +) +_sym_db.RegisterMessage(UpdateWorkerDeploymentVersionComputeConfigResponse) + +ValidateWorkerDeploymentVersionComputeConfigRequest = _reflection.GeneratedProtocolMessageType( + "ValidateWorkerDeploymentVersionComputeConfigRequest", + (_message.Message,), + { + "ComputeConfigScalingGroupsEntry": _reflection.GeneratedProtocolMessageType( + "ComputeConfigScalingGroupsEntry", + (_message.Message,), + { + "DESCRIPTOR": _VALIDATEWORKERDEPLOYMENTVERSIONCOMPUTECONFIGREQUEST_COMPUTECONFIGSCALINGGROUPSENTRY, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ValidateWorkerDeploymentVersionComputeConfigRequest.ComputeConfigScalingGroupsEntry) + }, + ), + "DESCRIPTOR": _VALIDATEWORKERDEPLOYMENTVERSIONCOMPUTECONFIGREQUEST, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ValidateWorkerDeploymentVersionComputeConfigRequest) + }, +) +_sym_db.RegisterMessage(ValidateWorkerDeploymentVersionComputeConfigRequest) +_sym_db.RegisterMessage( + ValidateWorkerDeploymentVersionComputeConfigRequest.ComputeConfigScalingGroupsEntry +) + +ValidateWorkerDeploymentVersionComputeConfigResponse = ( + _reflection.GeneratedProtocolMessageType( + "ValidateWorkerDeploymentVersionComputeConfigResponse", + (_message.Message,), + { + "DESCRIPTOR": _VALIDATEWORKERDEPLOYMENTVERSIONCOMPUTECONFIGRESPONSE, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ValidateWorkerDeploymentVersionComputeConfigResponse) + }, + ) +) +_sym_db.RegisterMessage(ValidateWorkerDeploymentVersionComputeConfigResponse) + UpdateWorkerDeploymentVersionMetadataRequest = _reflection.GeneratedProtocolMessageType( "UpdateWorkerDeploymentVersionMetadataRequest", (_message.Message,), @@ -2701,12 +3152,12 @@ (_message.Message,), { "DESCRIPTOR": _UPDATEWORKERDEPLOYMENTVERSIONMETADATAREQUEST_UPSERTENTRIESENTRY, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateWorkerDeploymentVersionMetadataRequest.UpsertEntriesEntry) }, ), "DESCRIPTOR": _UPDATEWORKERDEPLOYMENTVERSIONMETADATAREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateWorkerDeploymentVersionMetadataRequest) }, ) @@ -2719,19 +3170,41 @@ (_message.Message,), { "DESCRIPTOR": _UPDATEWORKERDEPLOYMENTVERSIONMETADATARESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateWorkerDeploymentVersionMetadataResponse) }, ) ) _sym_db.RegisterMessage(UpdateWorkerDeploymentVersionMetadataResponse) +SetWorkerDeploymentManagerRequest = _reflection.GeneratedProtocolMessageType( + "SetWorkerDeploymentManagerRequest", + (_message.Message,), + { + "DESCRIPTOR": _SETWORKERDEPLOYMENTMANAGERREQUEST, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.SetWorkerDeploymentManagerRequest) + }, +) +_sym_db.RegisterMessage(SetWorkerDeploymentManagerRequest) + +SetWorkerDeploymentManagerResponse = _reflection.GeneratedProtocolMessageType( + "SetWorkerDeploymentManagerResponse", + (_message.Message,), + { + "DESCRIPTOR": _SETWORKERDEPLOYMENTMANAGERRESPONSE, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.SetWorkerDeploymentManagerResponse) + }, +) +_sym_db.RegisterMessage(SetWorkerDeploymentManagerResponse) + GetCurrentDeploymentRequest = _reflection.GeneratedProtocolMessageType( "GetCurrentDeploymentRequest", (_message.Message,), { "DESCRIPTOR": _GETCURRENTDEPLOYMENTREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.GetCurrentDeploymentRequest) }, ) @@ -2742,7 +3215,7 @@ (_message.Message,), { "DESCRIPTOR": _GETCURRENTDEPLOYMENTRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.GetCurrentDeploymentResponse) }, ) @@ -2753,7 +3226,7 @@ (_message.Message,), { "DESCRIPTOR": _GETDEPLOYMENTREACHABILITYREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.GetDeploymentReachabilityRequest) }, ) @@ -2764,7 +3237,7 @@ (_message.Message,), { "DESCRIPTOR": _GETDEPLOYMENTREACHABILITYRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.GetDeploymentReachabilityResponse) }, ) @@ -2775,7 +3248,7 @@ (_message.Message,), { "DESCRIPTOR": _CREATEWORKFLOWRULEREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.CreateWorkflowRuleRequest) }, ) @@ -2786,7 +3259,7 @@ (_message.Message,), { "DESCRIPTOR": _CREATEWORKFLOWRULERESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.CreateWorkflowRuleResponse) }, ) @@ -2797,7 +3270,7 @@ (_message.Message,), { "DESCRIPTOR": _DESCRIBEWORKFLOWRULEREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DescribeWorkflowRuleRequest) }, ) @@ -2808,7 +3281,7 @@ (_message.Message,), { "DESCRIPTOR": _DESCRIBEWORKFLOWRULERESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DescribeWorkflowRuleResponse) }, ) @@ -2819,7 +3292,7 @@ (_message.Message,), { "DESCRIPTOR": _DELETEWORKFLOWRULEREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DeleteWorkflowRuleRequest) }, ) @@ -2830,7 +3303,7 @@ (_message.Message,), { "DESCRIPTOR": _DELETEWORKFLOWRULERESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DeleteWorkflowRuleResponse) }, ) @@ -2841,7 +3314,7 @@ (_message.Message,), { "DESCRIPTOR": _LISTWORKFLOWRULESREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ListWorkflowRulesRequest) }, ) @@ -2852,7 +3325,7 @@ (_message.Message,), { "DESCRIPTOR": _LISTWORKFLOWRULESRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ListWorkflowRulesResponse) }, ) @@ -2863,7 +3336,7 @@ (_message.Message,), { "DESCRIPTOR": _TRIGGERWORKFLOWRULEREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.TriggerWorkflowRuleRequest) }, ) @@ -2874,7 +3347,7 @@ (_message.Message,), { "DESCRIPTOR": _TRIGGERWORKFLOWRULERESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.TriggerWorkflowRuleResponse) }, ) @@ -2885,7 +3358,7 @@ (_message.Message,), { "DESCRIPTOR": _RECORDWORKERHEARTBEATREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RecordWorkerHeartbeatRequest) }, ) @@ -2896,7 +3369,7 @@ (_message.Message,), { "DESCRIPTOR": _RECORDWORKERHEARTBEATRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RecordWorkerHeartbeatResponse) }, ) @@ -2907,7 +3380,7 @@ (_message.Message,), { "DESCRIPTOR": _LISTWORKERSREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ListWorkersRequest) }, ) @@ -2918,7 +3391,7 @@ (_message.Message,), { "DESCRIPTOR": _LISTWORKERSRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ListWorkersResponse) }, ) @@ -2933,24 +3406,34 @@ (_message.Message,), { "DESCRIPTOR": _UPDATETASKQUEUECONFIGREQUEST_RATELIMITUPDATE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateTaskQueueConfigRequest.RateLimitUpdate) }, ), + "SetFairnessWeightOverridesEntry": _reflection.GeneratedProtocolMessageType( + "SetFairnessWeightOverridesEntry", + (_message.Message,), + { + "DESCRIPTOR": _UPDATETASKQUEUECONFIGREQUEST_SETFAIRNESSWEIGHTOVERRIDESENTRY, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateTaskQueueConfigRequest.SetFairnessWeightOverridesEntry) + }, + ), "DESCRIPTOR": _UPDATETASKQUEUECONFIGREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateTaskQueueConfigRequest) }, ) _sym_db.RegisterMessage(UpdateTaskQueueConfigRequest) _sym_db.RegisterMessage(UpdateTaskQueueConfigRequest.RateLimitUpdate) +_sym_db.RegisterMessage(UpdateTaskQueueConfigRequest.SetFairnessWeightOverridesEntry) UpdateTaskQueueConfigResponse = _reflection.GeneratedProtocolMessageType( "UpdateTaskQueueConfigResponse", (_message.Message,), { "DESCRIPTOR": _UPDATETASKQUEUECONFIGRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateTaskQueueConfigResponse) }, ) @@ -2961,7 +3444,7 @@ (_message.Message,), { "DESCRIPTOR": _FETCHWORKERCONFIGREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.FetchWorkerConfigRequest) }, ) @@ -2972,7 +3455,7 @@ (_message.Message,), { "DESCRIPTOR": _FETCHWORKERCONFIGRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.FetchWorkerConfigResponse) }, ) @@ -2983,7 +3466,7 @@ (_message.Message,), { "DESCRIPTOR": _UPDATEWORKERCONFIGREQUEST, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateWorkerConfigRequest) }, ) @@ -2994,17 +3477,513 @@ (_message.Message,), { "DESCRIPTOR": _UPDATEWORKERCONFIGRESPONSE, - "__module__": "temporal.api.workflowservice.v1.request_response_pb2", + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UpdateWorkerConfigResponse) }, ) _sym_db.RegisterMessage(UpdateWorkerConfigResponse) +DescribeWorkerRequest = _reflection.GeneratedProtocolMessageType( + "DescribeWorkerRequest", + (_message.Message,), + { + "DESCRIPTOR": _DESCRIBEWORKERREQUEST, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DescribeWorkerRequest) + }, +) +_sym_db.RegisterMessage(DescribeWorkerRequest) + +DescribeWorkerResponse = _reflection.GeneratedProtocolMessageType( + "DescribeWorkerResponse", + (_message.Message,), + { + "DESCRIPTOR": _DESCRIBEWORKERRESPONSE, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DescribeWorkerResponse) + }, +) +_sym_db.RegisterMessage(DescribeWorkerResponse) + +CountWorkersRequest = _reflection.GeneratedProtocolMessageType( + "CountWorkersRequest", + (_message.Message,), + { + "DESCRIPTOR": _COUNTWORKERSREQUEST, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.CountWorkersRequest) + }, +) +_sym_db.RegisterMessage(CountWorkersRequest) + +CountWorkersResponse = _reflection.GeneratedProtocolMessageType( + "CountWorkersResponse", + (_message.Message,), + { + "DESCRIPTOR": _COUNTWORKERSRESPONSE, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.CountWorkersResponse) + }, +) +_sym_db.RegisterMessage(CountWorkersResponse) + +PauseWorkflowExecutionRequest = _reflection.GeneratedProtocolMessageType( + "PauseWorkflowExecutionRequest", + (_message.Message,), + { + "DESCRIPTOR": _PAUSEWORKFLOWEXECUTIONREQUEST, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.PauseWorkflowExecutionRequest) + }, +) +_sym_db.RegisterMessage(PauseWorkflowExecutionRequest) + +PauseWorkflowExecutionResponse = _reflection.GeneratedProtocolMessageType( + "PauseWorkflowExecutionResponse", + (_message.Message,), + { + "DESCRIPTOR": _PAUSEWORKFLOWEXECUTIONRESPONSE, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.PauseWorkflowExecutionResponse) + }, +) +_sym_db.RegisterMessage(PauseWorkflowExecutionResponse) + +UnpauseWorkflowExecutionRequest = _reflection.GeneratedProtocolMessageType( + "UnpauseWorkflowExecutionRequest", + (_message.Message,), + { + "DESCRIPTOR": _UNPAUSEWORKFLOWEXECUTIONREQUEST, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UnpauseWorkflowExecutionRequest) + }, +) +_sym_db.RegisterMessage(UnpauseWorkflowExecutionRequest) + +UnpauseWorkflowExecutionResponse = _reflection.GeneratedProtocolMessageType( + "UnpauseWorkflowExecutionResponse", + (_message.Message,), + { + "DESCRIPTOR": _UNPAUSEWORKFLOWEXECUTIONRESPONSE, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.UnpauseWorkflowExecutionResponse) + }, +) +_sym_db.RegisterMessage(UnpauseWorkflowExecutionResponse) + +StartActivityExecutionRequest = _reflection.GeneratedProtocolMessageType( + "StartActivityExecutionRequest", + (_message.Message,), + { + "DESCRIPTOR": _STARTACTIVITYEXECUTIONREQUEST, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.StartActivityExecutionRequest) + }, +) +_sym_db.RegisterMessage(StartActivityExecutionRequest) + +StartActivityExecutionResponse = _reflection.GeneratedProtocolMessageType( + "StartActivityExecutionResponse", + (_message.Message,), + { + "DESCRIPTOR": _STARTACTIVITYEXECUTIONRESPONSE, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.StartActivityExecutionResponse) + }, +) +_sym_db.RegisterMessage(StartActivityExecutionResponse) + +DescribeActivityExecutionRequest = _reflection.GeneratedProtocolMessageType( + "DescribeActivityExecutionRequest", + (_message.Message,), + { + "DESCRIPTOR": _DESCRIBEACTIVITYEXECUTIONREQUEST, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DescribeActivityExecutionRequest) + }, +) +_sym_db.RegisterMessage(DescribeActivityExecutionRequest) + +DescribeActivityExecutionResponse = _reflection.GeneratedProtocolMessageType( + "DescribeActivityExecutionResponse", + (_message.Message,), + { + "DESCRIPTOR": _DESCRIBEACTIVITYEXECUTIONRESPONSE, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DescribeActivityExecutionResponse) + }, +) +_sym_db.RegisterMessage(DescribeActivityExecutionResponse) + +PollActivityExecutionRequest = _reflection.GeneratedProtocolMessageType( + "PollActivityExecutionRequest", + (_message.Message,), + { + "DESCRIPTOR": _POLLACTIVITYEXECUTIONREQUEST, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.PollActivityExecutionRequest) + }, +) +_sym_db.RegisterMessage(PollActivityExecutionRequest) + +PollActivityExecutionResponse = _reflection.GeneratedProtocolMessageType( + "PollActivityExecutionResponse", + (_message.Message,), + { + "DESCRIPTOR": _POLLACTIVITYEXECUTIONRESPONSE, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.PollActivityExecutionResponse) + }, +) +_sym_db.RegisterMessage(PollActivityExecutionResponse) + +ListActivityExecutionsRequest = _reflection.GeneratedProtocolMessageType( + "ListActivityExecutionsRequest", + (_message.Message,), + { + "DESCRIPTOR": _LISTACTIVITYEXECUTIONSREQUEST, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ListActivityExecutionsRequest) + }, +) +_sym_db.RegisterMessage(ListActivityExecutionsRequest) + +ListActivityExecutionsResponse = _reflection.GeneratedProtocolMessageType( + "ListActivityExecutionsResponse", + (_message.Message,), + { + "DESCRIPTOR": _LISTACTIVITYEXECUTIONSRESPONSE, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ListActivityExecutionsResponse) + }, +) +_sym_db.RegisterMessage(ListActivityExecutionsResponse) + +StartNexusOperationExecutionRequest = _reflection.GeneratedProtocolMessageType( + "StartNexusOperationExecutionRequest", + (_message.Message,), + { + "NexusHeaderEntry": _reflection.GeneratedProtocolMessageType( + "NexusHeaderEntry", + (_message.Message,), + { + "DESCRIPTOR": _STARTNEXUSOPERATIONEXECUTIONREQUEST_NEXUSHEADERENTRY, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.StartNexusOperationExecutionRequest.NexusHeaderEntry) + }, + ), + "DESCRIPTOR": _STARTNEXUSOPERATIONEXECUTIONREQUEST, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.StartNexusOperationExecutionRequest) + }, +) +_sym_db.RegisterMessage(StartNexusOperationExecutionRequest) +_sym_db.RegisterMessage(StartNexusOperationExecutionRequest.NexusHeaderEntry) + +StartNexusOperationExecutionResponse = _reflection.GeneratedProtocolMessageType( + "StartNexusOperationExecutionResponse", + (_message.Message,), + { + "DESCRIPTOR": _STARTNEXUSOPERATIONEXECUTIONRESPONSE, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.StartNexusOperationExecutionResponse) + }, +) +_sym_db.RegisterMessage(StartNexusOperationExecutionResponse) + +DescribeNexusOperationExecutionRequest = _reflection.GeneratedProtocolMessageType( + "DescribeNexusOperationExecutionRequest", + (_message.Message,), + { + "DESCRIPTOR": _DESCRIBENEXUSOPERATIONEXECUTIONREQUEST, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DescribeNexusOperationExecutionRequest) + }, +) +_sym_db.RegisterMessage(DescribeNexusOperationExecutionRequest) + +DescribeNexusOperationExecutionResponse = _reflection.GeneratedProtocolMessageType( + "DescribeNexusOperationExecutionResponse", + (_message.Message,), + { + "DESCRIPTOR": _DESCRIBENEXUSOPERATIONEXECUTIONRESPONSE, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DescribeNexusOperationExecutionResponse) + }, +) +_sym_db.RegisterMessage(DescribeNexusOperationExecutionResponse) + +PollNexusOperationExecutionRequest = _reflection.GeneratedProtocolMessageType( + "PollNexusOperationExecutionRequest", + (_message.Message,), + { + "DESCRIPTOR": _POLLNEXUSOPERATIONEXECUTIONREQUEST, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.PollNexusOperationExecutionRequest) + }, +) +_sym_db.RegisterMessage(PollNexusOperationExecutionRequest) + +PollNexusOperationExecutionResponse = _reflection.GeneratedProtocolMessageType( + "PollNexusOperationExecutionResponse", + (_message.Message,), + { + "DESCRIPTOR": _POLLNEXUSOPERATIONEXECUTIONRESPONSE, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.PollNexusOperationExecutionResponse) + }, +) +_sym_db.RegisterMessage(PollNexusOperationExecutionResponse) + +ListNexusOperationExecutionsRequest = _reflection.GeneratedProtocolMessageType( + "ListNexusOperationExecutionsRequest", + (_message.Message,), + { + "DESCRIPTOR": _LISTNEXUSOPERATIONEXECUTIONSREQUEST, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ListNexusOperationExecutionsRequest) + }, +) +_sym_db.RegisterMessage(ListNexusOperationExecutionsRequest) + +ListNexusOperationExecutionsResponse = _reflection.GeneratedProtocolMessageType( + "ListNexusOperationExecutionsResponse", + (_message.Message,), + { + "DESCRIPTOR": _LISTNEXUSOPERATIONEXECUTIONSRESPONSE, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.ListNexusOperationExecutionsResponse) + }, +) +_sym_db.RegisterMessage(ListNexusOperationExecutionsResponse) + +CountActivityExecutionsRequest = _reflection.GeneratedProtocolMessageType( + "CountActivityExecutionsRequest", + (_message.Message,), + { + "DESCRIPTOR": _COUNTACTIVITYEXECUTIONSREQUEST, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.CountActivityExecutionsRequest) + }, +) +_sym_db.RegisterMessage(CountActivityExecutionsRequest) + +CountActivityExecutionsResponse = _reflection.GeneratedProtocolMessageType( + "CountActivityExecutionsResponse", + (_message.Message,), + { + "AggregationGroup": _reflection.GeneratedProtocolMessageType( + "AggregationGroup", + (_message.Message,), + { + "DESCRIPTOR": _COUNTACTIVITYEXECUTIONSRESPONSE_AGGREGATIONGROUP, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.CountActivityExecutionsResponse.AggregationGroup) + }, + ), + "DESCRIPTOR": _COUNTACTIVITYEXECUTIONSRESPONSE, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.CountActivityExecutionsResponse) + }, +) +_sym_db.RegisterMessage(CountActivityExecutionsResponse) +_sym_db.RegisterMessage(CountActivityExecutionsResponse.AggregationGroup) + +CountNexusOperationExecutionsRequest = _reflection.GeneratedProtocolMessageType( + "CountNexusOperationExecutionsRequest", + (_message.Message,), + { + "DESCRIPTOR": _COUNTNEXUSOPERATIONEXECUTIONSREQUEST, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.CountNexusOperationExecutionsRequest) + }, +) +_sym_db.RegisterMessage(CountNexusOperationExecutionsRequest) + +CountNexusOperationExecutionsResponse = _reflection.GeneratedProtocolMessageType( + "CountNexusOperationExecutionsResponse", + (_message.Message,), + { + "AggregationGroup": _reflection.GeneratedProtocolMessageType( + "AggregationGroup", + (_message.Message,), + { + "DESCRIPTOR": _COUNTNEXUSOPERATIONEXECUTIONSRESPONSE_AGGREGATIONGROUP, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.CountNexusOperationExecutionsResponse.AggregationGroup) + }, + ), + "DESCRIPTOR": _COUNTNEXUSOPERATIONEXECUTIONSRESPONSE, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.CountNexusOperationExecutionsResponse) + }, +) +_sym_db.RegisterMessage(CountNexusOperationExecutionsResponse) +_sym_db.RegisterMessage(CountNexusOperationExecutionsResponse.AggregationGroup) + +RequestCancelActivityExecutionRequest = _reflection.GeneratedProtocolMessageType( + "RequestCancelActivityExecutionRequest", + (_message.Message,), + { + "DESCRIPTOR": _REQUESTCANCELACTIVITYEXECUTIONREQUEST, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RequestCancelActivityExecutionRequest) + }, +) +_sym_db.RegisterMessage(RequestCancelActivityExecutionRequest) + +RequestCancelActivityExecutionResponse = _reflection.GeneratedProtocolMessageType( + "RequestCancelActivityExecutionResponse", + (_message.Message,), + { + "DESCRIPTOR": _REQUESTCANCELACTIVITYEXECUTIONRESPONSE, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RequestCancelActivityExecutionResponse) + }, +) +_sym_db.RegisterMessage(RequestCancelActivityExecutionResponse) + +TerminateActivityExecutionRequest = _reflection.GeneratedProtocolMessageType( + "TerminateActivityExecutionRequest", + (_message.Message,), + { + "DESCRIPTOR": _TERMINATEACTIVITYEXECUTIONREQUEST, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.TerminateActivityExecutionRequest) + }, +) +_sym_db.RegisterMessage(TerminateActivityExecutionRequest) + +TerminateActivityExecutionResponse = _reflection.GeneratedProtocolMessageType( + "TerminateActivityExecutionResponse", + (_message.Message,), + { + "DESCRIPTOR": _TERMINATEACTIVITYEXECUTIONRESPONSE, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.TerminateActivityExecutionResponse) + }, +) +_sym_db.RegisterMessage(TerminateActivityExecutionResponse) + +DeleteActivityExecutionRequest = _reflection.GeneratedProtocolMessageType( + "DeleteActivityExecutionRequest", + (_message.Message,), + { + "DESCRIPTOR": _DELETEACTIVITYEXECUTIONREQUEST, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DeleteActivityExecutionRequest) + }, +) +_sym_db.RegisterMessage(DeleteActivityExecutionRequest) + +DeleteActivityExecutionResponse = _reflection.GeneratedProtocolMessageType( + "DeleteActivityExecutionResponse", + (_message.Message,), + { + "DESCRIPTOR": _DELETEACTIVITYEXECUTIONRESPONSE, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DeleteActivityExecutionResponse) + }, +) +_sym_db.RegisterMessage(DeleteActivityExecutionResponse) + +RequestCancelNexusOperationExecutionRequest = _reflection.GeneratedProtocolMessageType( + "RequestCancelNexusOperationExecutionRequest", + (_message.Message,), + { + "DESCRIPTOR": _REQUESTCANCELNEXUSOPERATIONEXECUTIONREQUEST, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RequestCancelNexusOperationExecutionRequest) + }, +) +_sym_db.RegisterMessage(RequestCancelNexusOperationExecutionRequest) + +RequestCancelNexusOperationExecutionResponse = _reflection.GeneratedProtocolMessageType( + "RequestCancelNexusOperationExecutionResponse", + (_message.Message,), + { + "DESCRIPTOR": _REQUESTCANCELNEXUSOPERATIONEXECUTIONRESPONSE, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.RequestCancelNexusOperationExecutionResponse) + }, +) +_sym_db.RegisterMessage(RequestCancelNexusOperationExecutionResponse) + +TerminateNexusOperationExecutionRequest = _reflection.GeneratedProtocolMessageType( + "TerminateNexusOperationExecutionRequest", + (_message.Message,), + { + "DESCRIPTOR": _TERMINATENEXUSOPERATIONEXECUTIONREQUEST, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.TerminateNexusOperationExecutionRequest) + }, +) +_sym_db.RegisterMessage(TerminateNexusOperationExecutionRequest) + +TerminateNexusOperationExecutionResponse = _reflection.GeneratedProtocolMessageType( + "TerminateNexusOperationExecutionResponse", + (_message.Message,), + { + "DESCRIPTOR": _TERMINATENEXUSOPERATIONEXECUTIONRESPONSE, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.TerminateNexusOperationExecutionResponse) + }, +) +_sym_db.RegisterMessage(TerminateNexusOperationExecutionResponse) + +DeleteNexusOperationExecutionRequest = _reflection.GeneratedProtocolMessageType( + "DeleteNexusOperationExecutionRequest", + (_message.Message,), + { + "DESCRIPTOR": _DELETENEXUSOPERATIONEXECUTIONREQUEST, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DeleteNexusOperationExecutionRequest) + }, +) +_sym_db.RegisterMessage(DeleteNexusOperationExecutionRequest) + +DeleteNexusOperationExecutionResponse = _reflection.GeneratedProtocolMessageType( + "DeleteNexusOperationExecutionResponse", + (_message.Message,), + { + "DESCRIPTOR": _DELETENEXUSOPERATIONEXECUTIONRESPONSE, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.DeleteNexusOperationExecutionResponse) + }, +) +_sym_db.RegisterMessage(DeleteNexusOperationExecutionResponse) + +PollWorkflowExecutionTimeSkippingRequest = _reflection.GeneratedProtocolMessageType( + "PollWorkflowExecutionTimeSkippingRequest", + (_message.Message,), + { + "DESCRIPTOR": _POLLWORKFLOWEXECUTIONTIMESKIPPINGREQUEST, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.PollWorkflowExecutionTimeSkippingRequest) + }, +) +_sym_db.RegisterMessage(PollWorkflowExecutionTimeSkippingRequest) + +PollWorkflowExecutionTimeSkippingResponse = _reflection.GeneratedProtocolMessageType( + "PollWorkflowExecutionTimeSkippingResponse", + (_message.Message,), + { + "DESCRIPTOR": _POLLWORKFLOWEXECUTIONTIMESKIPPINGRESPONSE, + "__module__": "temporalio.api.workflowservice.v1.request_response_pb2", + # @@protoc_insertion_point(class_scope:temporal.api.workflowservice.v1.PollWorkflowExecutionTimeSkippingResponse) + }, +) +_sym_db.RegisterMessage(PollWorkflowExecutionTimeSkippingResponse) + if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'\n"io.temporal.api.workflowservice.v1B\024RequestResponseProtoP\001Z5go.temporal.io/api/workflowservice/v1;workflowservice\252\002!Temporalio.Api.WorkflowService.V1\352\002$Temporalio::Api::WorkflowService::V1' _REGISTERNAMESPACEREQUEST_DATAENTRY._options = None _REGISTERNAMESPACEREQUEST_DATAENTRY._serialized_options = b"8\001" + _DESCRIBENAMESPACERESPONSE.fields_by_name["poller_group_infos"]._options = None + _DESCRIBENAMESPACERESPONSE.fields_by_name[ + "poller_group_infos" + ]._serialized_options = b"\030\001" _POLLWORKFLOWTASKQUEUEREQUEST.fields_by_name["binary_checksum"]._options = None _POLLWORKFLOWTASKQUEUEREQUEST.fields_by_name[ "binary_checksum" @@ -3017,6 +3996,10 @@ ]._serialized_options = b"\030\001" _POLLWORKFLOWTASKQUEUERESPONSE_QUERIESENTRY._options = None _POLLWORKFLOWTASKQUEUERESPONSE_QUERIESENTRY._serialized_options = b"8\001" + _POLLWORKFLOWTASKQUEUERESPONSE.fields_by_name["poller_group_infos"]._options = None + _POLLWORKFLOWTASKQUEUERESPONSE.fields_by_name[ + "poller_group_infos" + ]._serialized_options = b"\030\001" _RESPONDWORKFLOWTASKCOMPLETEDREQUEST_QUERYRESULTSENTRY._options = None _RESPONDWORKFLOWTASKCOMPLETEDREQUEST_QUERYRESULTSENTRY._serialized_options = ( b"8\001" @@ -3055,6 +4038,10 @@ _POLLACTIVITYTASKQUEUEREQUEST.fields_by_name[ "worker_version_capabilities" ]._serialized_options = b"\030\001" + _POLLACTIVITYTASKQUEUERESPONSE.fields_by_name["poller_group_infos"]._options = None + _POLLACTIVITYTASKQUEUERESPONSE.fields_by_name[ + "poller_group_infos" + ]._serialized_options = b"\030\001" _RESPONDACTIVITYTASKCOMPLETEDREQUEST.fields_by_name[ "worker_version" ]._options = None @@ -3135,12 +4122,24 @@ ]._serialized_options = b"\030\001" _GETCLUSTERINFORESPONSE_SUPPORTEDCLIENTSENTRY._options = None _GETCLUSTERINFORESPONSE_SUPPORTEDCLIENTSENTRY._serialized_options = b"8\001" + _STARTBATCHOPERATIONREQUEST.fields_by_name["executions"]._options = None + _STARTBATCHOPERATIONREQUEST.fields_by_name[ + "executions" + ]._serialized_options = b"\030\001" _POLLNEXUSTASKQUEUEREQUEST.fields_by_name[ "worker_version_capabilities" ]._options = None _POLLNEXUSTASKQUEUEREQUEST.fields_by_name[ "worker_version_capabilities" ]._serialized_options = b"\030\001" + _POLLNEXUSTASKQUEUERESPONSE.fields_by_name["poller_group_infos"]._options = None + _POLLNEXUSTASKQUEUERESPONSE.fields_by_name[ + "poller_group_infos" + ]._serialized_options = b"\030\001" + _RESPONDNEXUSTASKFAILEDREQUEST.fields_by_name["error"]._options = None + _RESPONDNEXUSTASKFAILEDREQUEST.fields_by_name[ + "error" + ]._serialized_options = b"\030\001" _DESCRIBEWORKERDEPLOYMENTVERSIONREQUEST.fields_by_name["version"]._options = None _DESCRIBEWORKERDEPLOYMENTVERSIONREQUEST.fields_by_name[ "version" @@ -3157,6 +4156,12 @@ _SETWORKERDEPLOYMENTCURRENTVERSIONRESPONSE.fields_by_name[ "previous_version" ]._serialized_options = b"\030\001" + _SETWORKERDEPLOYMENTCURRENTVERSIONRESPONSE.fields_by_name[ + "previous_deployment_version" + ]._options = None + _SETWORKERDEPLOYMENTCURRENTVERSIONRESPONSE.fields_by_name[ + "previous_deployment_version" + ]._serialized_options = b"\030\001" _SETWORKERDEPLOYMENTRAMPINGVERSIONREQUEST.fields_by_name["version"]._options = None _SETWORKERDEPLOYMENTRAMPINGVERSIONREQUEST.fields_by_name[ "version" @@ -3167,10 +4172,26 @@ _SETWORKERDEPLOYMENTRAMPINGVERSIONRESPONSE.fields_by_name[ "previous_version" ]._serialized_options = b"\030\001" + _SETWORKERDEPLOYMENTRAMPINGVERSIONRESPONSE.fields_by_name[ + "previous_deployment_version" + ]._options = None + _SETWORKERDEPLOYMENTRAMPINGVERSIONRESPONSE.fields_by_name[ + "previous_deployment_version" + ]._serialized_options = b"\030\001" + _SETWORKERDEPLOYMENTRAMPINGVERSIONRESPONSE.fields_by_name[ + "previous_percentage" + ]._options = None + _SETWORKERDEPLOYMENTRAMPINGVERSIONRESPONSE.fields_by_name[ + "previous_percentage" + ]._serialized_options = b"\030\001" _DELETEWORKERDEPLOYMENTVERSIONREQUEST.fields_by_name["version"]._options = None _DELETEWORKERDEPLOYMENTVERSIONREQUEST.fields_by_name[ "version" ]._serialized_options = b"\030\001" + _UPDATEWORKERDEPLOYMENTVERSIONCOMPUTECONFIGREQUEST_COMPUTECONFIGSCALINGGROUPSENTRY._options = None + _UPDATEWORKERDEPLOYMENTVERSIONCOMPUTECONFIGREQUEST_COMPUTECONFIGSCALINGGROUPSENTRY._serialized_options = b"8\001" + _VALIDATEWORKERDEPLOYMENTVERSIONCOMPUTECONFIGREQUEST_COMPUTECONFIGSCALINGGROUPSENTRY._options = None + _VALIDATEWORKERDEPLOYMENTVERSIONCOMPUTECONFIGREQUEST_COMPUTECONFIGSCALINGGROUPSENTRY._serialized_options = b"8\001" _UPDATEWORKERDEPLOYMENTVERSIONMETADATAREQUEST_UPSERTENTRIESENTRY._options = None _UPDATEWORKERDEPLOYMENTVERSIONMETADATAREQUEST_UPSERTENTRIESENTRY._serialized_options = b"8\001" _UPDATEWORKERDEPLOYMENTVERSIONMETADATAREQUEST.fields_by_name[ @@ -3179,436 +4200,592 @@ _UPDATEWORKERDEPLOYMENTVERSIONMETADATAREQUEST.fields_by_name[ "version" ]._serialized_options = b"\030\001" - _REGISTERNAMESPACEREQUEST._serialized_start = 1492 - _REGISTERNAMESPACEREQUEST._serialized_end = 2140 - _REGISTERNAMESPACEREQUEST_DATAENTRY._serialized_start = 2097 - _REGISTERNAMESPACEREQUEST_DATAENTRY._serialized_end = 2140 - _REGISTERNAMESPACERESPONSE._serialized_start = 2142 - _REGISTERNAMESPACERESPONSE._serialized_end = 2169 - _LISTNAMESPACESREQUEST._serialized_start = 2172 - _LISTNAMESPACESREQUEST._serialized_end = 2309 - _LISTNAMESPACESRESPONSE._serialized_start = 2312 - _LISTNAMESPACESRESPONSE._serialized_end = 2441 - _DESCRIBENAMESPACEREQUEST._serialized_start = 2443 - _DESCRIBENAMESPACEREQUEST._serialized_end = 2500 - _DESCRIBENAMESPACERESPONSE._serialized_start = 2503 - _DESCRIBENAMESPACERESPONSE._serialized_end = 2867 - _UPDATENAMESPACEREQUEST._serialized_start = 2870 - _UPDATENAMESPACEREQUEST._serialized_end = 3205 - _UPDATENAMESPACERESPONSE._serialized_start = 3208 - _UPDATENAMESPACERESPONSE._serialized_end = 3499 - _DEPRECATENAMESPACEREQUEST._serialized_start = 3501 - _DEPRECATENAMESPACEREQUEST._serialized_end = 3571 - _DEPRECATENAMESPACERESPONSE._serialized_start = 3573 - _DEPRECATENAMESPACERESPONSE._serialized_end = 3601 - _STARTWORKFLOWEXECUTIONREQUEST._serialized_start = 3604 - _STARTWORKFLOWEXECUTIONREQUEST._serialized_end = 5053 - _STARTWORKFLOWEXECUTIONRESPONSE._serialized_start = 5056 - _STARTWORKFLOWEXECUTIONRESPONSE._serialized_end = 5322 - _GETWORKFLOWEXECUTIONHISTORYREQUEST._serialized_start = 5325 - _GETWORKFLOWEXECUTIONHISTORYREQUEST._serialized_end = 5623 - _GETWORKFLOWEXECUTIONHISTORYRESPONSE._serialized_start = 5626 - _GETWORKFLOWEXECUTIONHISTORYRESPONSE._serialized_end = 5812 - _GETWORKFLOWEXECUTIONHISTORYREVERSEREQUEST._serialized_start = 5815 - _GETWORKFLOWEXECUTIONHISTORYREVERSEREQUEST._serialized_end = 5991 - _GETWORKFLOWEXECUTIONHISTORYREVERSERESPONSE._serialized_start = 5993 - _GETWORKFLOWEXECUTIONHISTORYREVERSERESPONSE._serialized_end = 6113 - _POLLWORKFLOWTASKQUEUEREQUEST._serialized_start = 6116 - _POLLWORKFLOWTASKQUEUEREQUEST._serialized_end = 6510 - _POLLWORKFLOWTASKQUEUERESPONSE._serialized_start = 6513 - _POLLWORKFLOWTASKQUEUERESPONSE._serialized_end = 7426 - _POLLWORKFLOWTASKQUEUERESPONSE_QUERIESENTRY._serialized_start = 7342 - _POLLWORKFLOWTASKQUEUERESPONSE_QUERIESENTRY._serialized_end = 7426 - _RESPONDWORKFLOWTASKCOMPLETEDREQUEST._serialized_start = 7429 - _RESPONDWORKFLOWTASKCOMPLETEDREQUEST._serialized_end = 8634 - _RESPONDWORKFLOWTASKCOMPLETEDREQUEST_QUERYRESULTSENTRY._serialized_start = 8468 - _RESPONDWORKFLOWTASKCOMPLETEDREQUEST_QUERYRESULTSENTRY._serialized_end = 8563 - _RESPONDWORKFLOWTASKCOMPLETEDREQUEST_CAPABILITIES._serialized_start = 8565 - _RESPONDWORKFLOWTASKCOMPLETEDREQUEST_CAPABILITIES._serialized_end = 8634 - _RESPONDWORKFLOWTASKCOMPLETEDRESPONSE._serialized_start = 8637 - _RESPONDWORKFLOWTASKCOMPLETEDRESPONSE._serialized_end = 8882 - _RESPONDWORKFLOWTASKFAILEDREQUEST._serialized_start = 8885 - _RESPONDWORKFLOWTASKFAILEDREQUEST._serialized_end = 9389 - _RESPONDWORKFLOWTASKFAILEDRESPONSE._serialized_start = 9391 - _RESPONDWORKFLOWTASKFAILEDRESPONSE._serialized_end = 9426 - _POLLACTIVITYTASKQUEUEREQUEST._serialized_start = 9429 - _POLLACTIVITYTASKQUEUEREQUEST._serialized_end = 9869 - _POLLACTIVITYTASKQUEUERESPONSE._serialized_start = 9872 - _POLLACTIVITYTASKQUEUERESPONSE._serialized_end = 10879 - _RECORDACTIVITYTASKHEARTBEATREQUEST._serialized_start = 10882 - _RECORDACTIVITYTASKHEARTBEATREQUEST._serialized_end = 11026 - _RECORDACTIVITYTASKHEARTBEATRESPONSE._serialized_start = 11028 - _RECORDACTIVITYTASKHEARTBEATRESPONSE._serialized_end = 11140 - _RECORDACTIVITYTASKHEARTBEATBYIDREQUEST._serialized_start = 11143 - _RECORDACTIVITYTASKHEARTBEATBYIDREQUEST._serialized_end = 11329 - _RECORDACTIVITYTASKHEARTBEATBYIDRESPONSE._serialized_start = 11331 - _RECORDACTIVITYTASKHEARTBEATBYIDRESPONSE._serialized_end = 11447 - _RESPONDACTIVITYTASKCOMPLETEDREQUEST._serialized_start = 11450 - _RESPONDACTIVITYTASKCOMPLETEDREQUEST._serialized_end = 11811 - _RESPONDACTIVITYTASKCOMPLETEDRESPONSE._serialized_start = 11813 - _RESPONDACTIVITYTASKCOMPLETEDRESPONSE._serialized_end = 11851 - _RESPONDACTIVITYTASKCOMPLETEDBYIDREQUEST._serialized_start = 11854 - _RESPONDACTIVITYTASKCOMPLETEDBYIDREQUEST._serialized_end = 12040 - _RESPONDACTIVITYTASKCOMPLETEDBYIDRESPONSE._serialized_start = 12042 - _RESPONDACTIVITYTASKCOMPLETEDBYIDRESPONSE._serialized_end = 12084 - _RESPONDACTIVITYTASKFAILEDREQUEST._serialized_start = 12087 - _RESPONDACTIVITYTASKFAILEDREQUEST._serialized_end = 12512 - _RESPONDACTIVITYTASKFAILEDRESPONSE._serialized_start = 12514 - _RESPONDACTIVITYTASKFAILEDRESPONSE._serialized_end = 12601 - _RESPONDACTIVITYTASKFAILEDBYIDREQUEST._serialized_start = 12604 - _RESPONDACTIVITYTASKFAILEDBYIDREQUEST._serialized_end = 12854 - _RESPONDACTIVITYTASKFAILEDBYIDRESPONSE._serialized_start = 12856 - _RESPONDACTIVITYTASKFAILEDBYIDRESPONSE._serialized_end = 12947 - _RESPONDACTIVITYTASKCANCELEDREQUEST._serialized_start = 12950 - _RESPONDACTIVITYTASKCANCELEDREQUEST._serialized_end = 13311 - _RESPONDACTIVITYTASKCANCELEDRESPONSE._serialized_start = 13313 - _RESPONDACTIVITYTASKCANCELEDRESPONSE._serialized_end = 13350 - _RESPONDACTIVITYTASKCANCELEDBYIDREQUEST._serialized_start = 13353 - _RESPONDACTIVITYTASKCANCELEDBYIDREQUEST._serialized_end = 13620 - _RESPONDACTIVITYTASKCANCELEDBYIDRESPONSE._serialized_start = 13622 - _RESPONDACTIVITYTASKCANCELEDBYIDRESPONSE._serialized_end = 13663 - _REQUESTCANCELWORKFLOWEXECUTIONREQUEST._serialized_start = 13666 - _REQUESTCANCELWORKFLOWEXECUTIONREQUEST._serialized_end = 13926 - _REQUESTCANCELWORKFLOWEXECUTIONRESPONSE._serialized_start = 13928 - _REQUESTCANCELWORKFLOWEXECUTIONRESPONSE._serialized_end = 13968 - _SIGNALWORKFLOWEXECUTIONREQUEST._serialized_start = 13971 - _SIGNALWORKFLOWEXECUTIONREQUEST._serialized_end = 14321 - _SIGNALWORKFLOWEXECUTIONRESPONSE._serialized_start = 14323 - _SIGNALWORKFLOWEXECUTIONRESPONSE._serialized_end = 14356 - _SIGNALWITHSTARTWORKFLOWEXECUTIONREQUEST._serialized_start = 14359 - _SIGNALWITHSTARTWORKFLOWEXECUTIONREQUEST._serialized_end = 15624 - _SIGNALWITHSTARTWORKFLOWEXECUTIONRESPONSE._serialized_start = 15626 - _SIGNALWITHSTARTWORKFLOWEXECUTIONRESPONSE._serialized_end = 15701 - _RESETWORKFLOWEXECUTIONREQUEST._serialized_start = 15704 - _RESETWORKFLOWEXECUTIONREQUEST._serialized_end = 16153 - _RESETWORKFLOWEXECUTIONRESPONSE._serialized_start = 16155 - _RESETWORKFLOWEXECUTIONRESPONSE._serialized_end = 16203 - _TERMINATEWORKFLOWEXECUTIONREQUEST._serialized_start = 16206 - _TERMINATEWORKFLOWEXECUTIONREQUEST._serialized_end = 16493 - _TERMINATEWORKFLOWEXECUTIONRESPONSE._serialized_start = 16495 - _TERMINATEWORKFLOWEXECUTIONRESPONSE._serialized_end = 16531 - _DELETEWORKFLOWEXECUTIONREQUEST._serialized_start = 16533 - _DELETEWORKFLOWEXECUTIONREQUEST._serialized_end = 16655 - _DELETEWORKFLOWEXECUTIONRESPONSE._serialized_start = 16657 - _DELETEWORKFLOWEXECUTIONRESPONSE._serialized_end = 16690 - _LISTOPENWORKFLOWEXECUTIONSREQUEST._serialized_start = 16693 - _LISTOPENWORKFLOWEXECUTIONSREQUEST._serialized_end = 17022 - _LISTOPENWORKFLOWEXECUTIONSRESPONSE._serialized_start = 17025 - _LISTOPENWORKFLOWEXECUTIONSRESPONSE._serialized_end = 17155 - _LISTCLOSEDWORKFLOWEXECUTIONSREQUEST._serialized_start = 17158 - _LISTCLOSEDWORKFLOWEXECUTIONSREQUEST._serialized_end = 17552 - _LISTCLOSEDWORKFLOWEXECUTIONSRESPONSE._serialized_start = 17555 - _LISTCLOSEDWORKFLOWEXECUTIONSRESPONSE._serialized_end = 17687 - _LISTWORKFLOWEXECUTIONSREQUEST._serialized_start = 17689 - _LISTWORKFLOWEXECUTIONSREQUEST._serialized_end = 17798 - _LISTWORKFLOWEXECUTIONSRESPONSE._serialized_start = 17800 - _LISTWORKFLOWEXECUTIONSRESPONSE._serialized_end = 17926 - _LISTARCHIVEDWORKFLOWEXECUTIONSREQUEST._serialized_start = 17928 - _LISTARCHIVEDWORKFLOWEXECUTIONSREQUEST._serialized_end = 18045 - _LISTARCHIVEDWORKFLOWEXECUTIONSRESPONSE._serialized_start = 18048 - _LISTARCHIVEDWORKFLOWEXECUTIONSRESPONSE._serialized_end = 18182 - _SCANWORKFLOWEXECUTIONSREQUEST._serialized_start = 18184 - _SCANWORKFLOWEXECUTIONSREQUEST._serialized_end = 18293 - _SCANWORKFLOWEXECUTIONSRESPONSE._serialized_start = 18295 - _SCANWORKFLOWEXECUTIONSRESPONSE._serialized_end = 18421 - _COUNTWORKFLOWEXECUTIONSREQUEST._serialized_start = 18423 - _COUNTWORKFLOWEXECUTIONSREQUEST._serialized_end = 18489 - _COUNTWORKFLOWEXECUTIONSRESPONSE._serialized_start = 18492 - _COUNTWORKFLOWEXECUTIONSRESPONSE._serialized_end = 18729 - _COUNTWORKFLOWEXECUTIONSRESPONSE_AGGREGATIONGROUP._serialized_start = 18641 - _COUNTWORKFLOWEXECUTIONSRESPONSE_AGGREGATIONGROUP._serialized_end = 18729 - _GETSEARCHATTRIBUTESREQUEST._serialized_start = 18731 - _GETSEARCHATTRIBUTESREQUEST._serialized_end = 18759 - _GETSEARCHATTRIBUTESRESPONSE._serialized_start = 18762 - _GETSEARCHATTRIBUTESRESPONSE._serialized_end = 18963 - _GETSEARCHATTRIBUTESRESPONSE_KEYSENTRY._serialized_start = 18879 - _GETSEARCHATTRIBUTESRESPONSE_KEYSENTRY._serialized_end = 18963 - _RESPONDQUERYTASKCOMPLETEDREQUEST._serialized_start = 18966 - _RESPONDQUERYTASKCOMPLETEDREQUEST._serialized_end = 19302 - _RESPONDQUERYTASKCOMPLETEDRESPONSE._serialized_start = 19304 - _RESPONDQUERYTASKCOMPLETEDRESPONSE._serialized_end = 19339 - _RESETSTICKYTASKQUEUEREQUEST._serialized_start = 19341 - _RESETSTICKYTASKQUEUEREQUEST._serialized_end = 19451 - _RESETSTICKYTASKQUEUERESPONSE._serialized_start = 19453 - _RESETSTICKYTASKQUEUERESPONSE._serialized_end = 19483 - _SHUTDOWNWORKERREQUEST._serialized_start = 19486 - _SHUTDOWNWORKERREQUEST._serialized_end = 19656 - _SHUTDOWNWORKERRESPONSE._serialized_start = 19658 - _SHUTDOWNWORKERRESPONSE._serialized_end = 19682 - _QUERYWORKFLOWREQUEST._serialized_start = 19685 - _QUERYWORKFLOWREQUEST._serialized_end = 19918 - _QUERYWORKFLOWRESPONSE._serialized_start = 19921 - _QUERYWORKFLOWRESPONSE._serialized_end = 20062 - _DESCRIBEWORKFLOWEXECUTIONREQUEST._serialized_start = 20064 - _DESCRIBEWORKFLOWEXECUTIONREQUEST._serialized_end = 20179 - _DESCRIBEWORKFLOWEXECUTIONRESPONSE._serialized_start = 20182 - _DESCRIBEWORKFLOWEXECUTIONRESPONSE._serialized_end = 20847 - _DESCRIBETASKQUEUEREQUEST._serialized_start = 20850 - _DESCRIBETASKQUEUEREQUEST._serialized_end = 21378 - _DESCRIBETASKQUEUERESPONSE._serialized_start = 21381 - _DESCRIBETASKQUEUERESPONSE._serialized_end = 22385 - _DESCRIBETASKQUEUERESPONSE_STATSBYPRIORITYKEYENTRY._serialized_start = 22065 - _DESCRIBETASKQUEUERESPONSE_STATSBYPRIORITYKEYENTRY._serialized_end = 22165 - _DESCRIBETASKQUEUERESPONSE_EFFECTIVERATELIMIT._serialized_start = 22167 - _DESCRIBETASKQUEUERESPONSE_EFFECTIVERATELIMIT._serialized_end = 22283 - _DESCRIBETASKQUEUERESPONSE_VERSIONSINFOENTRY._serialized_start = 22285 - _DESCRIBETASKQUEUERESPONSE_VERSIONSINFOENTRY._serialized_end = 22385 - _GETCLUSTERINFOREQUEST._serialized_start = 22387 - _GETCLUSTERINFOREQUEST._serialized_end = 22410 - _GETCLUSTERINFORESPONSE._serialized_start = 22413 - _GETCLUSTERINFORESPONSE._serialized_end = 22808 - _GETCLUSTERINFORESPONSE_SUPPORTEDCLIENTSENTRY._serialized_start = 22753 - _GETCLUSTERINFORESPONSE_SUPPORTEDCLIENTSENTRY._serialized_end = 22808 - _GETSYSTEMINFOREQUEST._serialized_start = 22810 - _GETSYSTEMINFOREQUEST._serialized_end = 22832 - _GETSYSTEMINFORESPONSE._serialized_start = 22835 - _GETSYSTEMINFORESPONSE._serialized_end = 23335 - _GETSYSTEMINFORESPONSE_CAPABILITIES._serialized_start = 22976 - _GETSYSTEMINFORESPONSE_CAPABILITIES._serialized_end = 23335 - _LISTTASKQUEUEPARTITIONSREQUEST._serialized_start = 23337 - _LISTTASKQUEUEPARTITIONSREQUEST._serialized_end = 23446 - _LISTTASKQUEUEPARTITIONSRESPONSE._serialized_start = 23449 - _LISTTASKQUEUEPARTITIONSRESPONSE._serialized_end = 23672 - _CREATESCHEDULEREQUEST._serialized_start = 23675 - _CREATESCHEDULEREQUEST._serialized_end = 24007 - _CREATESCHEDULERESPONSE._serialized_start = 24009 - _CREATESCHEDULERESPONSE._serialized_end = 24057 - _DESCRIBESCHEDULEREQUEST._serialized_start = 24059 - _DESCRIBESCHEDULEREQUEST._serialized_end = 24124 - _DESCRIBESCHEDULERESPONSE._serialized_start = 24127 - _DESCRIBESCHEDULERESPONSE._serialized_end = 24398 - _UPDATESCHEDULEREQUEST._serialized_start = 24401 - _UPDATESCHEDULEREQUEST._serialized_end = 24649 - _UPDATESCHEDULERESPONSE._serialized_start = 24651 - _UPDATESCHEDULERESPONSE._serialized_end = 24675 - _PATCHSCHEDULEREQUEST._serialized_start = 24678 - _PATCHSCHEDULEREQUEST._serialized_end = 24834 - _PATCHSCHEDULERESPONSE._serialized_start = 24836 - _PATCHSCHEDULERESPONSE._serialized_end = 24859 - _LISTSCHEDULEMATCHINGTIMESREQUEST._serialized_start = 24862 - _LISTSCHEDULEMATCHINGTIMESREQUEST._serialized_end = 25030 - _LISTSCHEDULEMATCHINGTIMESRESPONSE._serialized_start = 25032 - _LISTSCHEDULEMATCHINGTIMESRESPONSE._serialized_end = 25115 - _DELETESCHEDULEREQUEST._serialized_start = 25117 - _DELETESCHEDULEREQUEST._serialized_end = 25198 - _DELETESCHEDULERESPONSE._serialized_start = 25200 - _DELETESCHEDULERESPONSE._serialized_end = 25224 - _LISTSCHEDULESREQUEST._serialized_start = 25226 - _LISTSCHEDULESREQUEST._serialized_end = 25334 - _LISTSCHEDULESRESPONSE._serialized_start = 25336 - _LISTSCHEDULESRESPONSE._serialized_end = 25448 - _UPDATEWORKERBUILDIDCOMPATIBILITYREQUEST._serialized_start = 25451 - _UPDATEWORKERBUILDIDCOMPATIBILITYREQUEST._serialized_end = 26097 - _UPDATEWORKERBUILDIDCOMPATIBILITYREQUEST_ADDNEWCOMPATIBLEVERSION._serialized_start = 25898 + _SETWORKERDEPLOYMENTMANAGERRESPONSE.fields_by_name[ + "previous_manager_identity" + ]._options = None + _SETWORKERDEPLOYMENTMANAGERRESPONSE.fields_by_name[ + "previous_manager_identity" + ]._serialized_options = b"\030\001" + _LISTWORKERSRESPONSE.fields_by_name["workers_info"]._options = None + _LISTWORKERSRESPONSE.fields_by_name[ + "workers_info" + ]._serialized_options = b"\030\001" + _UPDATETASKQUEUECONFIGREQUEST_SETFAIRNESSWEIGHTOVERRIDESENTRY._options = None + _UPDATETASKQUEUECONFIGREQUEST_SETFAIRNESSWEIGHTOVERRIDESENTRY._serialized_options = b"8\001" + _STARTNEXUSOPERATIONEXECUTIONREQUEST_NEXUSHEADERENTRY._options = None + _STARTNEXUSOPERATIONEXECUTIONREQUEST_NEXUSHEADERENTRY._serialized_options = b"8\001" + _REGISTERNAMESPACEREQUEST._serialized_start = 1646 + _REGISTERNAMESPACEREQUEST._serialized_end = 2294 + _REGISTERNAMESPACEREQUEST_DATAENTRY._serialized_start = 2251 + _REGISTERNAMESPACEREQUEST_DATAENTRY._serialized_end = 2294 + _REGISTERNAMESPACERESPONSE._serialized_start = 2296 + _REGISTERNAMESPACERESPONSE._serialized_end = 2323 + _LISTNAMESPACESREQUEST._serialized_start = 2326 + _LISTNAMESPACESREQUEST._serialized_end = 2463 + _LISTNAMESPACESRESPONSE._serialized_start = 2466 + _LISTNAMESPACESRESPONSE._serialized_end = 2595 + _DESCRIBENAMESPACEREQUEST._serialized_start = 2597 + _DESCRIBENAMESPACEREQUEST._serialized_end = 2680 + _DESCRIBENAMESPACERESPONSE._serialized_start = 2683 + _DESCRIBENAMESPACERESPONSE._serialized_end = 3196 + _UPDATENAMESPACEREQUEST._serialized_start = 3199 + _UPDATENAMESPACEREQUEST._serialized_end = 3534 + _UPDATENAMESPACERESPONSE._serialized_start = 3537 + _UPDATENAMESPACERESPONSE._serialized_end = 3828 + _DEPRECATENAMESPACEREQUEST._serialized_start = 3830 + _DEPRECATENAMESPACEREQUEST._serialized_end = 3900 + _DEPRECATENAMESPACERESPONSE._serialized_start = 3902 + _DEPRECATENAMESPACERESPONSE._serialized_end = 3930 + _STARTWORKFLOWEXECUTIONREQUEST._serialized_start = 3933 + _STARTWORKFLOWEXECUTIONREQUEST._serialized_end = 5550 + _STARTWORKFLOWEXECUTIONRESPONSE._serialized_start = 5553 + _STARTWORKFLOWEXECUTIONRESPONSE._serialized_end = 5851 + _GETWORKFLOWEXECUTIONHISTORYREQUEST._serialized_start = 5854 + _GETWORKFLOWEXECUTIONHISTORYREQUEST._serialized_end = 6152 + _GETWORKFLOWEXECUTIONHISTORYRESPONSE._serialized_start = 6155 + _GETWORKFLOWEXECUTIONHISTORYRESPONSE._serialized_end = 6341 + _GETWORKFLOWEXECUTIONHISTORYREVERSEREQUEST._serialized_start = 6344 + _GETWORKFLOWEXECUTIONHISTORYREVERSEREQUEST._serialized_end = 6520 + _GETWORKFLOWEXECUTIONHISTORYREVERSERESPONSE._serialized_start = 6522 + _GETWORKFLOWEXECUTIONHISTORYREVERSERESPONSE._serialized_end = 6642 + _POLLWORKFLOWTASKQUEUEREQUEST._serialized_start = 6645 + _POLLWORKFLOWTASKQUEUEREQUEST._serialized_end = 7085 + _POLLWORKFLOWTASKQUEUERESPONSE._serialized_start = 7088 + _POLLWORKFLOWTASKQUEUERESPONSE._serialized_end = 8175 + _POLLWORKFLOWTASKQUEUERESPONSE_QUERIESENTRY._serialized_start = 8091 + _POLLWORKFLOWTASKQUEUERESPONSE_QUERIESENTRY._serialized_end = 8175 + _RESPONDWORKFLOWTASKCOMPLETEDREQUEST._serialized_start = 8178 + _RESPONDWORKFLOWTASKCOMPLETEDREQUEST._serialized_end = 9516 + _RESPONDWORKFLOWTASKCOMPLETEDREQUEST_QUERYRESULTSENTRY._serialized_start = 9350 + _RESPONDWORKFLOWTASKCOMPLETEDREQUEST_QUERYRESULTSENTRY._serialized_end = 9445 + _RESPONDWORKFLOWTASKCOMPLETEDREQUEST_CAPABILITIES._serialized_start = 9447 + _RESPONDWORKFLOWTASKCOMPLETEDREQUEST_CAPABILITIES._serialized_end = 9516 + _RESPONDWORKFLOWTASKCOMPLETEDRESPONSE._serialized_start = 9519 + _RESPONDWORKFLOWTASKCOMPLETEDRESPONSE._serialized_end = 9764 + _RESPONDWORKFLOWTASKFAILEDREQUEST._serialized_start = 9767 + _RESPONDWORKFLOWTASKFAILEDREQUEST._serialized_end = 10292 + _RESPONDWORKFLOWTASKFAILEDRESPONSE._serialized_start = 10294 + _RESPONDWORKFLOWTASKFAILEDRESPONSE._serialized_end = 10329 + _POLLACTIVITYTASKQUEUEREQUEST._serialized_start = 10332 + _POLLACTIVITYTASKQUEUEREQUEST._serialized_end = 10818 + _POLLACTIVITYTASKQUEUERESPONSE._serialized_start = 10821 + _POLLACTIVITYTASKQUEUERESPONSE._serialized_end = 12002 + _RECORDACTIVITYTASKHEARTBEATREQUEST._serialized_start = 12005 + _RECORDACTIVITYTASKHEARTBEATREQUEST._serialized_end = 12170 + _RECORDACTIVITYTASKHEARTBEATRESPONSE._serialized_start = 12172 + _RECORDACTIVITYTASKHEARTBEATRESPONSE._serialized_end = 12284 + _RECORDACTIVITYTASKHEARTBEATBYIDREQUEST._serialized_start = 12287 + _RECORDACTIVITYTASKHEARTBEATBYIDREQUEST._serialized_end = 12494 + _RECORDACTIVITYTASKHEARTBEATBYIDRESPONSE._serialized_start = 12496 + _RECORDACTIVITYTASKHEARTBEATBYIDRESPONSE._serialized_end = 12612 + _RESPONDACTIVITYTASKCOMPLETEDREQUEST._serialized_start = 12615 + _RESPONDACTIVITYTASKCOMPLETEDREQUEST._serialized_end = 12997 + _RESPONDACTIVITYTASKCOMPLETEDRESPONSE._serialized_start = 12999 + _RESPONDACTIVITYTASKCOMPLETEDRESPONSE._serialized_end = 13037 + _RESPONDACTIVITYTASKCOMPLETEDBYIDREQUEST._serialized_start = 13040 + _RESPONDACTIVITYTASKCOMPLETEDBYIDREQUEST._serialized_end = 13247 + _RESPONDACTIVITYTASKCOMPLETEDBYIDRESPONSE._serialized_start = 13249 + _RESPONDACTIVITYTASKCOMPLETEDBYIDRESPONSE._serialized_end = 13291 + _RESPONDACTIVITYTASKFAILEDREQUEST._serialized_start = 13294 + _RESPONDACTIVITYTASKFAILEDREQUEST._serialized_end = 13740 + _RESPONDACTIVITYTASKFAILEDRESPONSE._serialized_start = 13742 + _RESPONDACTIVITYTASKFAILEDRESPONSE._serialized_end = 13829 + _RESPONDACTIVITYTASKFAILEDBYIDREQUEST._serialized_start = 13832 + _RESPONDACTIVITYTASKFAILEDBYIDREQUEST._serialized_end = 14103 + _RESPONDACTIVITYTASKFAILEDBYIDRESPONSE._serialized_start = 14105 + _RESPONDACTIVITYTASKFAILEDBYIDRESPONSE._serialized_end = 14196 + _RESPONDACTIVITYTASKCANCELEDREQUEST._serialized_start = 14199 + _RESPONDACTIVITYTASKCANCELEDREQUEST._serialized_end = 14581 + _RESPONDACTIVITYTASKCANCELEDRESPONSE._serialized_start = 14583 + _RESPONDACTIVITYTASKCANCELEDRESPONSE._serialized_end = 14620 + _RESPONDACTIVITYTASKCANCELEDBYIDREQUEST._serialized_start = 14623 + _RESPONDACTIVITYTASKCANCELEDBYIDREQUEST._serialized_end = 14911 + _RESPONDACTIVITYTASKCANCELEDBYIDRESPONSE._serialized_start = 14913 + _RESPONDACTIVITYTASKCANCELEDBYIDRESPONSE._serialized_end = 14954 + _REQUESTCANCELWORKFLOWEXECUTIONREQUEST._serialized_start = 14957 + _REQUESTCANCELWORKFLOWEXECUTIONREQUEST._serialized_end = 15217 + _REQUESTCANCELWORKFLOWEXECUTIONRESPONSE._serialized_start = 15219 + _REQUESTCANCELWORKFLOWEXECUTIONRESPONSE._serialized_end = 15259 + _SIGNALWORKFLOWEXECUTIONREQUEST._serialized_start = 15262 + _SIGNALWORKFLOWEXECUTIONREQUEST._serialized_end = 15612 + _SIGNALWORKFLOWEXECUTIONRESPONSE._serialized_start = 15614 + _SIGNALWORKFLOWEXECUTIONRESPONSE._serialized_end = 15691 + _SIGNALWITHSTARTWORKFLOWEXECUTIONREQUEST._serialized_start = 15694 + _SIGNALWITHSTARTWORKFLOWEXECUTIONREQUEST._serialized_end = 17033 + _SIGNALWITHSTARTWORKFLOWEXECUTIONRESPONSE._serialized_start = 17036 + _SIGNALWITHSTARTWORKFLOWEXECUTIONRESPONSE._serialized_end = 17194 + _RESETWORKFLOWEXECUTIONREQUEST._serialized_start = 17197 + _RESETWORKFLOWEXECUTIONREQUEST._serialized_end = 17646 + _RESETWORKFLOWEXECUTIONRESPONSE._serialized_start = 17648 + _RESETWORKFLOWEXECUTIONRESPONSE._serialized_end = 17696 + _TERMINATEWORKFLOWEXECUTIONREQUEST._serialized_start = 17699 + _TERMINATEWORKFLOWEXECUTIONREQUEST._serialized_end = 17986 + _TERMINATEWORKFLOWEXECUTIONRESPONSE._serialized_start = 17988 + _TERMINATEWORKFLOWEXECUTIONRESPONSE._serialized_end = 18024 + _DELETEWORKFLOWEXECUTIONREQUEST._serialized_start = 18026 + _DELETEWORKFLOWEXECUTIONREQUEST._serialized_end = 18148 + _DELETEWORKFLOWEXECUTIONRESPONSE._serialized_start = 18150 + _DELETEWORKFLOWEXECUTIONRESPONSE._serialized_end = 18183 + _LISTOPENWORKFLOWEXECUTIONSREQUEST._serialized_start = 18186 + _LISTOPENWORKFLOWEXECUTIONSREQUEST._serialized_end = 18515 + _LISTOPENWORKFLOWEXECUTIONSRESPONSE._serialized_start = 18518 + _LISTOPENWORKFLOWEXECUTIONSRESPONSE._serialized_end = 18648 + _LISTCLOSEDWORKFLOWEXECUTIONSREQUEST._serialized_start = 18651 + _LISTCLOSEDWORKFLOWEXECUTIONSREQUEST._serialized_end = 19045 + _LISTCLOSEDWORKFLOWEXECUTIONSRESPONSE._serialized_start = 19048 + _LISTCLOSEDWORKFLOWEXECUTIONSRESPONSE._serialized_end = 19180 + _LISTWORKFLOWEXECUTIONSREQUEST._serialized_start = 19182 + _LISTWORKFLOWEXECUTIONSREQUEST._serialized_end = 19291 + _LISTWORKFLOWEXECUTIONSRESPONSE._serialized_start = 19293 + _LISTWORKFLOWEXECUTIONSRESPONSE._serialized_end = 19419 + _LISTARCHIVEDWORKFLOWEXECUTIONSREQUEST._serialized_start = 19421 + _LISTARCHIVEDWORKFLOWEXECUTIONSREQUEST._serialized_end = 19538 + _LISTARCHIVEDWORKFLOWEXECUTIONSRESPONSE._serialized_start = 19541 + _LISTARCHIVEDWORKFLOWEXECUTIONSRESPONSE._serialized_end = 19675 + _SCANWORKFLOWEXECUTIONSREQUEST._serialized_start = 19677 + _SCANWORKFLOWEXECUTIONSREQUEST._serialized_end = 19786 + _SCANWORKFLOWEXECUTIONSRESPONSE._serialized_start = 19788 + _SCANWORKFLOWEXECUTIONSRESPONSE._serialized_end = 19914 + _COUNTWORKFLOWEXECUTIONSREQUEST._serialized_start = 19916 + _COUNTWORKFLOWEXECUTIONSREQUEST._serialized_end = 19982 + _COUNTWORKFLOWEXECUTIONSRESPONSE._serialized_start = 19985 + _COUNTWORKFLOWEXECUTIONSRESPONSE._serialized_end = 20222 + _COUNTWORKFLOWEXECUTIONSRESPONSE_AGGREGATIONGROUP._serialized_start = 20134 + _COUNTWORKFLOWEXECUTIONSRESPONSE_AGGREGATIONGROUP._serialized_end = 20222 + _GETSEARCHATTRIBUTESREQUEST._serialized_start = 20224 + _GETSEARCHATTRIBUTESREQUEST._serialized_end = 20252 + _GETSEARCHATTRIBUTESRESPONSE._serialized_start = 20255 + _GETSEARCHATTRIBUTESRESPONSE._serialized_end = 20456 + _GETSEARCHATTRIBUTESRESPONSE_KEYSENTRY._serialized_start = 20372 + _GETSEARCHATTRIBUTESRESPONSE_KEYSENTRY._serialized_end = 20456 + _RESPONDQUERYTASKCOMPLETEDREQUEST._serialized_start = 20459 + _RESPONDQUERYTASKCOMPLETEDREQUEST._serialized_end = 20820 + _RESPONDQUERYTASKCOMPLETEDRESPONSE._serialized_start = 20822 + _RESPONDQUERYTASKCOMPLETEDRESPONSE._serialized_end = 20857 + _RESETSTICKYTASKQUEUEREQUEST._serialized_start = 20859 + _RESETSTICKYTASKQUEUEREQUEST._serialized_end = 20969 + _RESETSTICKYTASKQUEUERESPONSE._serialized_start = 20971 + _RESETSTICKYTASKQUEUERESPONSE._serialized_end = 21001 + _SHUTDOWNWORKERREQUEST._serialized_start = 21004 + _SHUTDOWNWORKERREQUEST._serialized_end = 21287 + _SHUTDOWNWORKERRESPONSE._serialized_start = 21289 + _SHUTDOWNWORKERRESPONSE._serialized_end = 21313 + _QUERYWORKFLOWREQUEST._serialized_start = 21316 + _QUERYWORKFLOWREQUEST._serialized_end = 21549 + _QUERYWORKFLOWRESPONSE._serialized_start = 21552 + _QUERYWORKFLOWRESPONSE._serialized_end = 21737 + _DESCRIBEWORKFLOWEXECUTIONREQUEST._serialized_start = 21739 + _DESCRIBEWORKFLOWEXECUTIONREQUEST._serialized_end = 21854 + _DESCRIBEWORKFLOWEXECUTIONRESPONSE._serialized_start = 21857 + _DESCRIBEWORKFLOWEXECUTIONRESPONSE._serialized_end = 22522 + _DESCRIBETASKQUEUEREQUEST._serialized_start = 22525 + _DESCRIBETASKQUEUEREQUEST._serialized_end = 23053 + _DESCRIBETASKQUEUERESPONSE._serialized_start = 23056 + _DESCRIBETASKQUEUERESPONSE._serialized_end = 24060 + _DESCRIBETASKQUEUERESPONSE_STATSBYPRIORITYKEYENTRY._serialized_start = 23740 + _DESCRIBETASKQUEUERESPONSE_STATSBYPRIORITYKEYENTRY._serialized_end = 23840 + _DESCRIBETASKQUEUERESPONSE_EFFECTIVERATELIMIT._serialized_start = 23842 + _DESCRIBETASKQUEUERESPONSE_EFFECTIVERATELIMIT._serialized_end = 23958 + _DESCRIBETASKQUEUERESPONSE_VERSIONSINFOENTRY._serialized_start = 23960 + _DESCRIBETASKQUEUERESPONSE_VERSIONSINFOENTRY._serialized_end = 24060 + _GETCLUSTERINFOREQUEST._serialized_start = 24062 + _GETCLUSTERINFOREQUEST._serialized_end = 24085 + _GETCLUSTERINFORESPONSE._serialized_start = 24088 + _GETCLUSTERINFORESPONSE._serialized_end = 24553 + _GETCLUSTERINFORESPONSE_SUPPORTEDCLIENTSENTRY._serialized_start = 24498 + _GETCLUSTERINFORESPONSE_SUPPORTEDCLIENTSENTRY._serialized_end = 24553 + _GETSYSTEMINFOREQUEST._serialized_start = 24555 + _GETSYSTEMINFOREQUEST._serialized_end = 24577 + _GETSYSTEMINFORESPONSE._serialized_start = 24580 + _GETSYSTEMINFORESPONSE._serialized_end = 25157 + _GETSYSTEMINFORESPONSE_CAPABILITIES._serialized_start = 24721 + _GETSYSTEMINFORESPONSE_CAPABILITIES._serialized_end = 25157 + _LISTTASKQUEUEPARTITIONSREQUEST._serialized_start = 25159 + _LISTTASKQUEUEPARTITIONSREQUEST._serialized_end = 25268 + _LISTTASKQUEUEPARTITIONSRESPONSE._serialized_start = 25271 + _LISTTASKQUEUEPARTITIONSRESPONSE._serialized_end = 25494 + _CREATESCHEDULEREQUEST._serialized_start = 25497 + _CREATESCHEDULEREQUEST._serialized_end = 25829 + _CREATESCHEDULERESPONSE._serialized_start = 25831 + _CREATESCHEDULERESPONSE._serialized_end = 25879 + _DESCRIBESCHEDULEREQUEST._serialized_start = 25881 + _DESCRIBESCHEDULEREQUEST._serialized_end = 25946 + _DESCRIBESCHEDULERESPONSE._serialized_start = 25949 + _DESCRIBESCHEDULERESPONSE._serialized_end = 26220 + _UPDATESCHEDULEREQUEST._serialized_start = 26223 + _UPDATESCHEDULEREQUEST._serialized_end = 26515 + _UPDATESCHEDULERESPONSE._serialized_start = 26517 + _UPDATESCHEDULERESPONSE._serialized_end = 26541 + _PATCHSCHEDULEREQUEST._serialized_start = 26544 + _PATCHSCHEDULEREQUEST._serialized_end = 26700 + _PATCHSCHEDULERESPONSE._serialized_start = 26702 + _PATCHSCHEDULERESPONSE._serialized_end = 26725 + _LISTSCHEDULEMATCHINGTIMESREQUEST._serialized_start = 26728 + _LISTSCHEDULEMATCHINGTIMESREQUEST._serialized_end = 26896 + _LISTSCHEDULEMATCHINGTIMESRESPONSE._serialized_start = 26898 + _LISTSCHEDULEMATCHINGTIMESRESPONSE._serialized_end = 26981 + _DELETESCHEDULEREQUEST._serialized_start = 26983 + _DELETESCHEDULEREQUEST._serialized_end = 27064 + _DELETESCHEDULERESPONSE._serialized_start = 27066 + _DELETESCHEDULERESPONSE._serialized_end = 27090 + _LISTSCHEDULESREQUEST._serialized_start = 27092 + _LISTSCHEDULESREQUEST._serialized_end = 27200 + _LISTSCHEDULESRESPONSE._serialized_start = 27202 + _LISTSCHEDULESRESPONSE._serialized_end = 27314 + _COUNTSCHEDULESREQUEST._serialized_start = 27316 + _COUNTSCHEDULESREQUEST._serialized_end = 27373 + _COUNTSCHEDULESRESPONSE._serialized_start = 27376 + _COUNTSCHEDULESRESPONSE._serialized_end = 27595 + _COUNTSCHEDULESRESPONSE_AGGREGATIONGROUP._serialized_start = 20134 + _COUNTSCHEDULESRESPONSE_AGGREGATIONGROUP._serialized_end = 20222 + _UPDATEWORKERBUILDIDCOMPATIBILITYREQUEST._serialized_start = 27598 + _UPDATEWORKERBUILDIDCOMPATIBILITYREQUEST._serialized_end = 28244 + _UPDATEWORKERBUILDIDCOMPATIBILITYREQUEST_ADDNEWCOMPATIBLEVERSION._serialized_start = 28045 _UPDATEWORKERBUILDIDCOMPATIBILITYREQUEST_ADDNEWCOMPATIBLEVERSION._serialized_end = ( - 26009 + 28156 ) - _UPDATEWORKERBUILDIDCOMPATIBILITYREQUEST_MERGESETS._serialized_start = 26011 - _UPDATEWORKERBUILDIDCOMPATIBILITYREQUEST_MERGESETS._serialized_end = 26084 - _UPDATEWORKERBUILDIDCOMPATIBILITYRESPONSE._serialized_start = 26099 - _UPDATEWORKERBUILDIDCOMPATIBILITYRESPONSE._serialized_end = 26163 - _GETWORKERBUILDIDCOMPATIBILITYREQUEST._serialized_start = 26165 - _GETWORKERBUILDIDCOMPATIBILITYREQUEST._serialized_end = 26260 - _GETWORKERBUILDIDCOMPATIBILITYRESPONSE._serialized_start = 26262 - _GETWORKERBUILDIDCOMPATIBILITYRESPONSE._serialized_end = 26378 - _UPDATEWORKERVERSIONINGRULESREQUEST._serialized_start = 26381 - _UPDATEWORKERVERSIONINGRULESREQUEST._serialized_end = 28098 - _UPDATEWORKERVERSIONINGRULESREQUEST_INSERTBUILDIDASSIGNMENTRULE._serialized_start = 27433 + _UPDATEWORKERBUILDIDCOMPATIBILITYREQUEST_MERGESETS._serialized_start = 28158 + _UPDATEWORKERBUILDIDCOMPATIBILITYREQUEST_MERGESETS._serialized_end = 28231 + _UPDATEWORKERBUILDIDCOMPATIBILITYRESPONSE._serialized_start = 28246 + _UPDATEWORKERBUILDIDCOMPATIBILITYRESPONSE._serialized_end = 28310 + _GETWORKERBUILDIDCOMPATIBILITYREQUEST._serialized_start = 28312 + _GETWORKERBUILDIDCOMPATIBILITYREQUEST._serialized_end = 28407 + _GETWORKERBUILDIDCOMPATIBILITYRESPONSE._serialized_start = 28409 + _GETWORKERBUILDIDCOMPATIBILITYRESPONSE._serialized_end = 28525 + _UPDATEWORKERVERSIONINGRULESREQUEST._serialized_start = 28528 + _UPDATEWORKERVERSIONINGRULESREQUEST._serialized_end = 30245 + _UPDATEWORKERVERSIONINGRULESREQUEST_INSERTBUILDIDASSIGNMENTRULE._serialized_start = 29580 _UPDATEWORKERVERSIONINGRULESREQUEST_INSERTBUILDIDASSIGNMENTRULE._serialized_end = ( - 27546 + 29693 ) - _UPDATEWORKERVERSIONINGRULESREQUEST_REPLACEBUILDIDASSIGNMENTRULE._serialized_start = 27549 + _UPDATEWORKERVERSIONINGRULESREQUEST_REPLACEBUILDIDASSIGNMENTRULE._serialized_start = 29696 _UPDATEWORKERVERSIONINGRULESREQUEST_REPLACEBUILDIDASSIGNMENTRULE._serialized_end = ( - 27678 + 29825 ) - _UPDATEWORKERVERSIONINGRULESREQUEST_DELETEBUILDIDASSIGNMENTRULE._serialized_start = 27680 + _UPDATEWORKERVERSIONINGRULESREQUEST_DELETEBUILDIDASSIGNMENTRULE._serialized_start = 29827 _UPDATEWORKERVERSIONINGRULESREQUEST_DELETEBUILDIDASSIGNMENTRULE._serialized_end = ( - 27744 + 29891 ) - _UPDATEWORKERVERSIONINGRULESREQUEST_ADDCOMPATIBLEBUILDIDREDIRECTRULE._serialized_start = 27746 - _UPDATEWORKERVERSIONINGRULESREQUEST_ADDCOMPATIBLEBUILDIDREDIRECTRULE._serialized_end = 27852 - _UPDATEWORKERVERSIONINGRULESREQUEST_REPLACECOMPATIBLEBUILDIDREDIRECTRULE._serialized_start = 27854 - _UPDATEWORKERVERSIONINGRULESREQUEST_REPLACECOMPATIBLEBUILDIDREDIRECTRULE._serialized_end = 27964 - _UPDATEWORKERVERSIONINGRULESREQUEST_DELETECOMPATIBLEBUILDIDREDIRECTRULE._serialized_start = 27966 - _UPDATEWORKERVERSIONINGRULESREQUEST_DELETECOMPATIBLEBUILDIDREDIRECTRULE._serialized_end = 28028 - _UPDATEWORKERVERSIONINGRULESREQUEST_COMMITBUILDID._serialized_start = 28030 - _UPDATEWORKERVERSIONINGRULESREQUEST_COMMITBUILDID._serialized_end = 28085 - _UPDATEWORKERVERSIONINGRULESRESPONSE._serialized_start = 28101 - _UPDATEWORKERVERSIONINGRULESRESPONSE._serialized_end = 28353 - _GETWORKERVERSIONINGRULESREQUEST._serialized_start = 28355 - _GETWORKERVERSIONINGRULESREQUEST._serialized_end = 28427 - _GETWORKERVERSIONINGRULESRESPONSE._serialized_start = 28430 - _GETWORKERVERSIONINGRULESRESPONSE._serialized_end = 28679 - _GETWORKERTASKREACHABILITYREQUEST._serialized_start = 28682 - _GETWORKERTASKREACHABILITYREQUEST._serialized_end = 28838 - _GETWORKERTASKREACHABILITYRESPONSE._serialized_start = 28840 - _GETWORKERTASKREACHABILITYRESPONSE._serialized_end = 28954 - _UPDATEWORKFLOWEXECUTIONREQUEST._serialized_start = 28957 - _UPDATEWORKFLOWEXECUTIONREQUEST._serialized_end = 29218 - _UPDATEWORKFLOWEXECUTIONRESPONSE._serialized_start = 29221 - _UPDATEWORKFLOWEXECUTIONRESPONSE._serialized_end = 29436 - _STARTBATCHOPERATIONREQUEST._serialized_start = 29439 - _STARTBATCHOPERATIONREQUEST._serialized_end = 30451 - _STARTBATCHOPERATIONRESPONSE._serialized_start = 30453 - _STARTBATCHOPERATIONRESPONSE._serialized_end = 30482 - _STOPBATCHOPERATIONREQUEST._serialized_start = 30484 - _STOPBATCHOPERATIONREQUEST._serialized_end = 30580 - _STOPBATCHOPERATIONRESPONSE._serialized_start = 30582 - _STOPBATCHOPERATIONRESPONSE._serialized_end = 30610 - _DESCRIBEBATCHOPERATIONREQUEST._serialized_start = 30612 - _DESCRIBEBATCHOPERATIONREQUEST._serialized_end = 30678 - _DESCRIBEBATCHOPERATIONRESPONSE._serialized_start = 30681 - _DESCRIBEBATCHOPERATIONRESPONSE._serialized_end = 31083 - _LISTBATCHOPERATIONSREQUEST._serialized_start = 31085 - _LISTBATCHOPERATIONSREQUEST._serialized_end = 31176 - _LISTBATCHOPERATIONSRESPONSE._serialized_start = 31178 - _LISTBATCHOPERATIONSRESPONSE._serialized_end = 31299 - _POLLWORKFLOWEXECUTIONUPDATEREQUEST._serialized_start = 31302 - _POLLWORKFLOWEXECUTIONUPDATEREQUEST._serialized_end = 31487 - _POLLWORKFLOWEXECUTIONUPDATERESPONSE._serialized_start = 31490 - _POLLWORKFLOWEXECUTIONUPDATERESPONSE._serialized_end = 31709 - _POLLNEXUSTASKQUEUEREQUEST._serialized_start = 31712 - _POLLNEXUSTASKQUEUEREQUEST._serialized_end = 32074 - _POLLNEXUSTASKQUEUERESPONSE._serialized_start = 32077 - _POLLNEXUSTASKQUEUERESPONSE._serialized_end = 32257 - _RESPONDNEXUSTASKCOMPLETEDREQUEST._serialized_start = 32260 - _RESPONDNEXUSTASKCOMPLETEDREQUEST._serialized_end = 32402 - _RESPONDNEXUSTASKCOMPLETEDRESPONSE._serialized_start = 32404 - _RESPONDNEXUSTASKCOMPLETEDRESPONSE._serialized_end = 32439 - _RESPONDNEXUSTASKFAILEDREQUEST._serialized_start = 32442 - _RESPONDNEXUSTASKFAILEDREQUEST._serialized_end = 32582 - _RESPONDNEXUSTASKFAILEDRESPONSE._serialized_start = 32584 - _RESPONDNEXUSTASKFAILEDRESPONSE._serialized_end = 32616 - _EXECUTEMULTIOPERATIONREQUEST._serialized_start = 32619 - _EXECUTEMULTIOPERATIONREQUEST._serialized_end = 32970 - _EXECUTEMULTIOPERATIONREQUEST_OPERATION._serialized_start = 32764 - _EXECUTEMULTIOPERATIONREQUEST_OPERATION._serialized_end = 32970 - _EXECUTEMULTIOPERATIONRESPONSE._serialized_start = 32973 - _EXECUTEMULTIOPERATIONRESPONSE._serialized_end = 33305 - _EXECUTEMULTIOPERATIONRESPONSE_RESPONSE._serialized_start = 33099 - _EXECUTEMULTIOPERATIONRESPONSE_RESPONSE._serialized_end = 33305 - _UPDATEACTIVITYOPTIONSREQUEST._serialized_start = 33308 - _UPDATEACTIVITYOPTIONSREQUEST._serialized_end = 33644 - _UPDATEACTIVITYOPTIONSRESPONSE._serialized_start = 33646 - _UPDATEACTIVITYOPTIONSRESPONSE._serialized_end = 33746 - _PAUSEACTIVITYREQUEST._serialized_start = 33749 - _PAUSEACTIVITYREQUEST._serialized_end = 33928 - _PAUSEACTIVITYRESPONSE._serialized_start = 33930 - _PAUSEACTIVITYRESPONSE._serialized_end = 33953 - _UNPAUSEACTIVITYREQUEST._serialized_start = 33956 - _UNPAUSEACTIVITYREQUEST._serialized_end = 34236 - _UNPAUSEACTIVITYRESPONSE._serialized_start = 34238 - _UNPAUSEACTIVITYRESPONSE._serialized_end = 34263 - _RESETACTIVITYREQUEST._serialized_start = 34266 - _RESETACTIVITYREQUEST._serialized_end = 34573 - _RESETACTIVITYRESPONSE._serialized_start = 34575 - _RESETACTIVITYRESPONSE._serialized_end = 34598 - _UPDATEWORKFLOWEXECUTIONOPTIONSREQUEST._serialized_start = 34601 - _UPDATEWORKFLOWEXECUTIONOPTIONSREQUEST._serialized_end = 34867 - _UPDATEWORKFLOWEXECUTIONOPTIONSRESPONSE._serialized_start = 34870 - _UPDATEWORKFLOWEXECUTIONOPTIONSRESPONSE._serialized_end = 34998 - _DESCRIBEDEPLOYMENTREQUEST._serialized_start = 35000 - _DESCRIBEDEPLOYMENTREQUEST._serialized_end = 35106 - _DESCRIBEDEPLOYMENTRESPONSE._serialized_start = 35108 - _DESCRIBEDEPLOYMENTRESPONSE._serialized_end = 35205 - _DESCRIBEWORKERDEPLOYMENTVERSIONREQUEST._serialized_start = 35208 - _DESCRIBEWORKERDEPLOYMENTVERSIONREQUEST._serialized_end = 35402 - _DESCRIBEWORKERDEPLOYMENTVERSIONRESPONSE._serialized_start = 35405 - _DESCRIBEWORKERDEPLOYMENTVERSIONRESPONSE._serialized_end = 36057 - _DESCRIBEWORKERDEPLOYMENTVERSIONRESPONSE_VERSIONTASKQUEUE._serialized_start = 35666 - _DESCRIBEWORKERDEPLOYMENTVERSIONRESPONSE_VERSIONTASKQUEUE._serialized_end = 36057 - _DESCRIBEWORKERDEPLOYMENTVERSIONRESPONSE_VERSIONTASKQUEUE_STATSBYPRIORITYKEYENTRY._serialized_start = 22065 - _DESCRIBEWORKERDEPLOYMENTVERSIONRESPONSE_VERSIONTASKQUEUE_STATSBYPRIORITYKEYENTRY._serialized_end = 22165 - _DESCRIBEWORKERDEPLOYMENTREQUEST._serialized_start = 36059 - _DESCRIBEWORKERDEPLOYMENTREQUEST._serialized_end = 36136 - _DESCRIBEWORKERDEPLOYMENTRESPONSE._serialized_start = 36139 - _DESCRIBEWORKERDEPLOYMENTRESPONSE._serialized_end = 36279 - _LISTDEPLOYMENTSREQUEST._serialized_start = 36281 - _LISTDEPLOYMENTSREQUEST._serialized_end = 36389 - _LISTDEPLOYMENTSRESPONSE._serialized_start = 36391 - _LISTDEPLOYMENTSRESPONSE._serialized_end = 36510 - _SETCURRENTDEPLOYMENTREQUEST._serialized_start = 36513 - _SETCURRENTDEPLOYMENTREQUEST._serialized_end = 36718 - _SETCURRENTDEPLOYMENTRESPONSE._serialized_start = 36721 - _SETCURRENTDEPLOYMENTRESPONSE._serialized_end = 36906 - _SETWORKERDEPLOYMENTCURRENTVERSIONREQUEST._serialized_start = 36909 - _SETWORKERDEPLOYMENTCURRENTVERSIONREQUEST._serialized_end = 37112 - _SETWORKERDEPLOYMENTCURRENTVERSIONRESPONSE._serialized_start = 37115 - _SETWORKERDEPLOYMENTCURRENTVERSIONRESPONSE._serialized_end = 37302 - _SETWORKERDEPLOYMENTRAMPINGVERSIONREQUEST._serialized_start = 37305 - _SETWORKERDEPLOYMENTRAMPINGVERSIONREQUEST._serialized_end = 37528 - _SETWORKERDEPLOYMENTRAMPINGVERSIONRESPONSE._serialized_start = 37531 - _SETWORKERDEPLOYMENTRAMPINGVERSIONRESPONSE._serialized_end = 37747 - _LISTWORKERDEPLOYMENTSREQUEST._serialized_start = 37749 - _LISTWORKERDEPLOYMENTSREQUEST._serialized_end = 37842 - _LISTWORKERDEPLOYMENTSRESPONSE._serialized_start = 37845 - _LISTWORKERDEPLOYMENTSRESPONSE._serialized_end = 38516 - _LISTWORKERDEPLOYMENTSRESPONSE_WORKERDEPLOYMENTSUMMARY._serialized_start = 38020 - _LISTWORKERDEPLOYMENTSRESPONSE_WORKERDEPLOYMENTSUMMARY._serialized_end = 38516 - _DELETEWORKERDEPLOYMENTVERSIONREQUEST._serialized_start = 38519 - _DELETEWORKERDEPLOYMENTVERSIONREQUEST._serialized_end = 38719 - _DELETEWORKERDEPLOYMENTVERSIONRESPONSE._serialized_start = 38721 - _DELETEWORKERDEPLOYMENTVERSIONRESPONSE._serialized_end = 38760 - _DELETEWORKERDEPLOYMENTREQUEST._serialized_start = 38762 - _DELETEWORKERDEPLOYMENTREQUEST._serialized_end = 38855 - _DELETEWORKERDEPLOYMENTRESPONSE._serialized_start = 38857 - _DELETEWORKERDEPLOYMENTRESPONSE._serialized_end = 38889 - _UPDATEWORKERDEPLOYMENTVERSIONMETADATAREQUEST._serialized_start = 38892 - _UPDATEWORKERDEPLOYMENTVERSIONMETADATAREQUEST._serialized_end = 39310 - _UPDATEWORKERDEPLOYMENTVERSIONMETADATAREQUEST_UPSERTENTRIESENTRY._serialized_start = 39225 + _UPDATEWORKERVERSIONINGRULESREQUEST_ADDCOMPATIBLEBUILDIDREDIRECTRULE._serialized_start = 29893 + _UPDATEWORKERVERSIONINGRULESREQUEST_ADDCOMPATIBLEBUILDIDREDIRECTRULE._serialized_end = 29999 + _UPDATEWORKERVERSIONINGRULESREQUEST_REPLACECOMPATIBLEBUILDIDREDIRECTRULE._serialized_start = 30001 + _UPDATEWORKERVERSIONINGRULESREQUEST_REPLACECOMPATIBLEBUILDIDREDIRECTRULE._serialized_end = 30111 + _UPDATEWORKERVERSIONINGRULESREQUEST_DELETECOMPATIBLEBUILDIDREDIRECTRULE._serialized_start = 30113 + _UPDATEWORKERVERSIONINGRULESREQUEST_DELETECOMPATIBLEBUILDIDREDIRECTRULE._serialized_end = 30175 + _UPDATEWORKERVERSIONINGRULESREQUEST_COMMITBUILDID._serialized_start = 30177 + _UPDATEWORKERVERSIONINGRULESREQUEST_COMMITBUILDID._serialized_end = 30232 + _UPDATEWORKERVERSIONINGRULESRESPONSE._serialized_start = 30248 + _UPDATEWORKERVERSIONINGRULESRESPONSE._serialized_end = 30500 + _GETWORKERVERSIONINGRULESREQUEST._serialized_start = 30502 + _GETWORKERVERSIONINGRULESREQUEST._serialized_end = 30574 + _GETWORKERVERSIONINGRULESRESPONSE._serialized_start = 30577 + _GETWORKERVERSIONINGRULESRESPONSE._serialized_end = 30826 + _GETWORKERTASKREACHABILITYREQUEST._serialized_start = 30829 + _GETWORKERTASKREACHABILITYREQUEST._serialized_end = 30985 + _GETWORKERTASKREACHABILITYRESPONSE._serialized_start = 30987 + _GETWORKERTASKREACHABILITYRESPONSE._serialized_end = 31101 + _UPDATEWORKFLOWEXECUTIONREQUEST._serialized_start = 31104 + _UPDATEWORKFLOWEXECUTIONREQUEST._serialized_end = 31365 + _UPDATEWORKFLOWEXECUTIONRESPONSE._serialized_start = 31368 + _UPDATEWORKFLOWEXECUTIONRESPONSE._serialized_end = 31627 + _STARTBATCHOPERATIONREQUEST._serialized_start = 31630 + _STARTBATCHOPERATIONREQUEST._serialized_end = 32996 + _STARTBATCHOPERATIONRESPONSE._serialized_start = 32998 + _STARTBATCHOPERATIONRESPONSE._serialized_end = 33027 + _STOPBATCHOPERATIONREQUEST._serialized_start = 33029 + _STOPBATCHOPERATIONREQUEST._serialized_end = 33125 + _STOPBATCHOPERATIONRESPONSE._serialized_start = 33127 + _STOPBATCHOPERATIONRESPONSE._serialized_end = 33155 + _DESCRIBEBATCHOPERATIONREQUEST._serialized_start = 33157 + _DESCRIBEBATCHOPERATIONREQUEST._serialized_end = 33223 + _DESCRIBEBATCHOPERATIONRESPONSE._serialized_start = 33226 + _DESCRIBEBATCHOPERATIONRESPONSE._serialized_end = 33698 + _LISTBATCHOPERATIONSREQUEST._serialized_start = 33700 + _LISTBATCHOPERATIONSREQUEST._serialized_end = 33791 + _LISTBATCHOPERATIONSRESPONSE._serialized_start = 33793 + _LISTBATCHOPERATIONSRESPONSE._serialized_end = 33914 + _POLLWORKFLOWEXECUTIONUPDATEREQUEST._serialized_start = 33917 + _POLLWORKFLOWEXECUTIONUPDATEREQUEST._serialized_end = 34102 + _POLLWORKFLOWEXECUTIONUPDATERESPONSE._serialized_start = 34105 + _POLLWORKFLOWEXECUTIONUPDATERESPONSE._serialized_end = 34324 + _POLLNEXUSTASKQUEUEREQUEST._serialized_start = 34327 + _POLLNEXUSTASKQUEUEREQUEST._serialized_end = 34743 + _POLLNEXUSTASKQUEUERESPONSE._serialized_start = 34746 + _POLLNEXUSTASKQUEUERESPONSE._serialized_end = 35100 + _RESPONDNEXUSTASKCOMPLETEDREQUEST._serialized_start = 35103 + _RESPONDNEXUSTASKCOMPLETEDREQUEST._serialized_end = 35270 + _RESPONDNEXUSTASKCOMPLETEDRESPONSE._serialized_start = 35272 + _RESPONDNEXUSTASKCOMPLETEDRESPONSE._serialized_end = 35307 + _RESPONDNEXUSTASKFAILEDREQUEST._serialized_start = 35310 + _RESPONDNEXUSTASKFAILEDREQUEST._serialized_end = 35530 + _RESPONDNEXUSTASKFAILEDRESPONSE._serialized_start = 35532 + _RESPONDNEXUSTASKFAILEDRESPONSE._serialized_end = 35564 + _EXECUTEMULTIOPERATIONREQUEST._serialized_start = 35567 + _EXECUTEMULTIOPERATIONREQUEST._serialized_end = 35939 + _EXECUTEMULTIOPERATIONREQUEST_OPERATION._serialized_start = 35733 + _EXECUTEMULTIOPERATIONREQUEST_OPERATION._serialized_end = 35939 + _EXECUTEMULTIOPERATIONRESPONSE._serialized_start = 35942 + _EXECUTEMULTIOPERATIONRESPONSE._serialized_end = 36274 + _EXECUTEMULTIOPERATIONRESPONSE_RESPONSE._serialized_start = 36068 + _EXECUTEMULTIOPERATIONRESPONSE_RESPONSE._serialized_end = 36274 + _UPDATEACTIVITYOPTIONSREQUEST._serialized_start = 36277 + _UPDATEACTIVITYOPTIONSREQUEST._serialized_end = 36613 + _UPDATEACTIVITYEXECUTIONOPTIONSREQUEST._serialized_start = 36616 + _UPDATEACTIVITYEXECUTIONOPTIONSREQUEST._serialized_end = 36935 + _UPDATEACTIVITYOPTIONSRESPONSE._serialized_start = 36937 + _UPDATEACTIVITYOPTIONSRESPONSE._serialized_end = 37037 + _UPDATEACTIVITYEXECUTIONOPTIONSRESPONSE._serialized_start = 37039 + _UPDATEACTIVITYEXECUTIONOPTIONSRESPONSE._serialized_end = 37148 + _PAUSEACTIVITYREQUEST._serialized_start = 37151 + _PAUSEACTIVITYREQUEST._serialized_end = 37350 + _PAUSEACTIVITYEXECUTIONREQUEST._serialized_start = 37353 + _PAUSEACTIVITYEXECUTIONREQUEST._serialized_end = 37536 + _PAUSEACTIVITYRESPONSE._serialized_start = 37538 + _PAUSEACTIVITYRESPONSE._serialized_end = 37561 + _PAUSEACTIVITYEXECUTIONRESPONSE._serialized_start = 37563 + _PAUSEACTIVITYEXECUTIONRESPONSE._serialized_end = 37595 + _UNPAUSEACTIVITYREQUEST._serialized_start = 37598 + _UNPAUSEACTIVITYREQUEST._serialized_end = 37878 + _UNPAUSEACTIVITYEXECUTIONREQUEST._serialized_start = 37881 + _UNPAUSEACTIVITYEXECUTIONREQUEST._serialized_end = 38154 + _UNPAUSEACTIVITYRESPONSE._serialized_start = 38156 + _UNPAUSEACTIVITYRESPONSE._serialized_end = 38181 + _UNPAUSEACTIVITYEXECUTIONRESPONSE._serialized_start = 38183 + _UNPAUSEACTIVITYEXECUTIONRESPONSE._serialized_end = 38217 + _RESETACTIVITYREQUEST._serialized_start = 38220 + _RESETACTIVITYREQUEST._serialized_end = 38527 + _RESETACTIVITYEXECUTIONREQUEST._serialized_start = 38530 + _RESETACTIVITYEXECUTIONREQUEST._serialized_end = 38820 + _RESETACTIVITYRESPONSE._serialized_start = 38822 + _RESETACTIVITYRESPONSE._serialized_end = 38845 + _RESETACTIVITYEXECUTIONRESPONSE._serialized_start = 38847 + _RESETACTIVITYEXECUTIONRESPONSE._serialized_end = 38879 + _UPDATEWORKFLOWEXECUTIONOPTIONSREQUEST._serialized_start = 38882 + _UPDATEWORKFLOWEXECUTIONOPTIONSREQUEST._serialized_end = 39166 + _UPDATEWORKFLOWEXECUTIONOPTIONSRESPONSE._serialized_start = 39169 + _UPDATEWORKFLOWEXECUTIONOPTIONSRESPONSE._serialized_end = 39346 + _DESCRIBEDEPLOYMENTREQUEST._serialized_start = 39348 + _DESCRIBEDEPLOYMENTREQUEST._serialized_end = 39454 + _DESCRIBEDEPLOYMENTRESPONSE._serialized_start = 39456 + _DESCRIBEDEPLOYMENTRESPONSE._serialized_end = 39553 + _DESCRIBEWORKERDEPLOYMENTVERSIONREQUEST._serialized_start = 39556 + _DESCRIBEWORKERDEPLOYMENTVERSIONREQUEST._serialized_end = 39750 + _DESCRIBEWORKERDEPLOYMENTVERSIONRESPONSE._serialized_start = 39753 + _DESCRIBEWORKERDEPLOYMENTVERSIONRESPONSE._serialized_end = 40405 + _DESCRIBEWORKERDEPLOYMENTVERSIONRESPONSE_VERSIONTASKQUEUE._serialized_start = 40014 + _DESCRIBEWORKERDEPLOYMENTVERSIONRESPONSE_VERSIONTASKQUEUE._serialized_end = 40405 + _DESCRIBEWORKERDEPLOYMENTVERSIONRESPONSE_VERSIONTASKQUEUE_STATSBYPRIORITYKEYENTRY._serialized_start = 23740 + _DESCRIBEWORKERDEPLOYMENTVERSIONRESPONSE_VERSIONTASKQUEUE_STATSBYPRIORITYKEYENTRY._serialized_end = 23840 + _DESCRIBEWORKERDEPLOYMENTREQUEST._serialized_start = 40407 + _DESCRIBEWORKERDEPLOYMENTREQUEST._serialized_end = 40484 + _DESCRIBEWORKERDEPLOYMENTRESPONSE._serialized_start = 40487 + _DESCRIBEWORKERDEPLOYMENTRESPONSE._serialized_end = 40627 + _LISTDEPLOYMENTSREQUEST._serialized_start = 40629 + _LISTDEPLOYMENTSREQUEST._serialized_end = 40737 + _LISTDEPLOYMENTSRESPONSE._serialized_start = 40739 + _LISTDEPLOYMENTSRESPONSE._serialized_end = 40858 + _SETCURRENTDEPLOYMENTREQUEST._serialized_start = 40861 + _SETCURRENTDEPLOYMENTREQUEST._serialized_end = 41066 + _SETCURRENTDEPLOYMENTRESPONSE._serialized_start = 41069 + _SETCURRENTDEPLOYMENTRESPONSE._serialized_end = 41254 + _SETWORKERDEPLOYMENTCURRENTVERSIONREQUEST._serialized_start = 41257 + _SETWORKERDEPLOYMENTCURRENTVERSIONREQUEST._serialized_end = 41486 + _SETWORKERDEPLOYMENTCURRENTVERSIONRESPONSE._serialized_start = 41489 + _SETWORKERDEPLOYMENTCURRENTVERSIONRESPONSE._serialized_end = 41680 + _SETWORKERDEPLOYMENTRAMPINGVERSIONREQUEST._serialized_start = 41683 + _SETWORKERDEPLOYMENTRAMPINGVERSIONREQUEST._serialized_end = 41932 + _SETWORKERDEPLOYMENTRAMPINGVERSIONRESPONSE._serialized_start = 41935 + _SETWORKERDEPLOYMENTRAMPINGVERSIONRESPONSE._serialized_end = 42159 + _CREATEWORKERDEPLOYMENTREQUEST._serialized_start = 42161 + _CREATEWORKERDEPLOYMENTREQUEST._serialized_end = 42274 + _CREATEWORKERDEPLOYMENTRESPONSE._serialized_start = 42276 + _CREATEWORKERDEPLOYMENTRESPONSE._serialized_end = 42332 + _LISTWORKERDEPLOYMENTSREQUEST._serialized_start = 42334 + _LISTWORKERDEPLOYMENTSREQUEST._serialized_end = 42427 + _LISTWORKERDEPLOYMENTSRESPONSE._serialized_start = 42430 + _LISTWORKERDEPLOYMENTSRESPONSE._serialized_end = 43101 + _LISTWORKERDEPLOYMENTSRESPONSE_WORKERDEPLOYMENTSUMMARY._serialized_start = 42605 + _LISTWORKERDEPLOYMENTSRESPONSE_WORKERDEPLOYMENTSUMMARY._serialized_end = 43101 + _CREATEWORKERDEPLOYMENTVERSIONREQUEST._serialized_start = 43104 + _CREATEWORKERDEPLOYMENTVERSIONREQUEST._serialized_end = 43344 + _CREATEWORKERDEPLOYMENTVERSIONRESPONSE._serialized_start = 43346 + _CREATEWORKERDEPLOYMENTVERSIONRESPONSE._serialized_end = 43385 + _DELETEWORKERDEPLOYMENTVERSIONREQUEST._serialized_start = 43388 + _DELETEWORKERDEPLOYMENTVERSIONREQUEST._serialized_end = 43588 + _DELETEWORKERDEPLOYMENTVERSIONRESPONSE._serialized_start = 43590 + _DELETEWORKERDEPLOYMENTVERSIONRESPONSE._serialized_end = 43629 + _DELETEWORKERDEPLOYMENTREQUEST._serialized_start = 43631 + _DELETEWORKERDEPLOYMENTREQUEST._serialized_end = 43724 + _DELETEWORKERDEPLOYMENTRESPONSE._serialized_start = 43726 + _DELETEWORKERDEPLOYMENTRESPONSE._serialized_end = 43758 + _UPDATEWORKERDEPLOYMENTVERSIONCOMPUTECONFIGREQUEST._serialized_start = 43761 + _UPDATEWORKERDEPLOYMENTVERSIONCOMPUTECONFIGREQUEST._serialized_end = 44277 + _UPDATEWORKERDEPLOYMENTVERSIONCOMPUTECONFIGREQUEST_COMPUTECONFIGSCALINGGROUPSENTRY._serialized_start = 44154 + _UPDATEWORKERDEPLOYMENTVERSIONCOMPUTECONFIGREQUEST_COMPUTECONFIGSCALINGGROUPSENTRY._serialized_end = 44277 + _UPDATEWORKERDEPLOYMENTVERSIONCOMPUTECONFIGRESPONSE._serialized_start = 44279 + _UPDATEWORKERDEPLOYMENTVERSIONCOMPUTECONFIGRESPONSE._serialized_end = 44331 + _VALIDATEWORKERDEPLOYMENTVERSIONCOMPUTECONFIGREQUEST._serialized_start = 44334 + _VALIDATEWORKERDEPLOYMENTVERSIONCOMPUTECONFIGREQUEST._serialized_end = 44834 + _VALIDATEWORKERDEPLOYMENTVERSIONCOMPUTECONFIGREQUEST_COMPUTECONFIGSCALINGGROUPSENTRY._serialized_start = 44154 + _VALIDATEWORKERDEPLOYMENTVERSIONCOMPUTECONFIGREQUEST_COMPUTECONFIGSCALINGGROUPSENTRY._serialized_end = 44277 + _VALIDATEWORKERDEPLOYMENTVERSIONCOMPUTECONFIGRESPONSE._serialized_start = 44836 + _VALIDATEWORKERDEPLOYMENTVERSIONCOMPUTECONFIGRESPONSE._serialized_end = 44890 + _UPDATEWORKERDEPLOYMENTVERSIONMETADATAREQUEST._serialized_start = 44893 + _UPDATEWORKERDEPLOYMENTVERSIONMETADATAREQUEST._serialized_end = 45311 + _UPDATEWORKERDEPLOYMENTVERSIONMETADATAREQUEST_UPSERTENTRIESENTRY._serialized_start = 45226 _UPDATEWORKERDEPLOYMENTVERSIONMETADATAREQUEST_UPSERTENTRIESENTRY._serialized_end = ( - 39310 + 45311 + ) + _UPDATEWORKERDEPLOYMENTVERSIONMETADATARESPONSE._serialized_start = 45313 + _UPDATEWORKERDEPLOYMENTVERSIONMETADATARESPONSE._serialized_end = 45423 + _SETWORKERDEPLOYMENTMANAGERREQUEST._serialized_start = 45426 + _SETWORKERDEPLOYMENTMANAGERREQUEST._serialized_end = 45615 + _SETWORKERDEPLOYMENTMANAGERRESPONSE._serialized_start = 45617 + _SETWORKERDEPLOYMENTMANAGERRESPONSE._serialized_end = 45716 + _GETCURRENTDEPLOYMENTREQUEST._serialized_start = 45718 + _GETCURRENTDEPLOYMENTREQUEST._serialized_end = 45787 + _GETCURRENTDEPLOYMENTRESPONSE._serialized_start = 45789 + _GETCURRENTDEPLOYMENTRESPONSE._serialized_end = 45896 + _GETDEPLOYMENTREACHABILITYREQUEST._serialized_start = 45898 + _GETDEPLOYMENTREACHABILITYREQUEST._serialized_end = 46011 + _GETDEPLOYMENTREACHABILITYRESPONSE._serialized_start = 46014 + _GETDEPLOYMENTREACHABILITYRESPONSE._serialized_end = 46241 + _CREATEWORKFLOWRULEREQUEST._serialized_start = 46244 + _CREATEWORKFLOWRULEREQUEST._serialized_end = 46424 + _CREATEWORKFLOWRULERESPONSE._serialized_start = 46426 + _CREATEWORKFLOWRULERESPONSE._serialized_end = 46521 + _DESCRIBEWORKFLOWRULEREQUEST._serialized_start = 46523 + _DESCRIBEWORKFLOWRULEREQUEST._serialized_end = 46588 + _DESCRIBEWORKFLOWRULERESPONSE._serialized_start = 46590 + _DESCRIBEWORKFLOWRULERESPONSE._serialized_end = 46671 + _DELETEWORKFLOWRULEREQUEST._serialized_start = 46673 + _DELETEWORKFLOWRULEREQUEST._serialized_end = 46736 + _DELETEWORKFLOWRULERESPONSE._serialized_start = 46738 + _DELETEWORKFLOWRULERESPONSE._serialized_end = 46766 + _LISTWORKFLOWRULESREQUEST._serialized_start = 46768 + _LISTWORKFLOWRULESREQUEST._serialized_end = 46838 + _LISTWORKFLOWRULESRESPONSE._serialized_start = 46840 + _LISTWORKFLOWRULESRESPONSE._serialized_end = 46944 + _TRIGGERWORKFLOWRULEREQUEST._serialized_start = 46947 + _TRIGGERWORKFLOWRULEREQUEST._serialized_end = 47153 + _TRIGGERWORKFLOWRULERESPONSE._serialized_start = 47155 + _TRIGGERWORKFLOWRULERESPONSE._serialized_end = 47201 + _RECORDWORKERHEARTBEATREQUEST._serialized_start = 47204 + _RECORDWORKERHEARTBEATREQUEST._serialized_end = 47359 + _RECORDWORKERHEARTBEATRESPONSE._serialized_start = 47361 + _RECORDWORKERHEARTBEATRESPONSE._serialized_end = 47392 + _LISTWORKERSREQUEST._serialized_start = 47395 + _LISTWORKERSREQUEST._serialized_end = 47525 + _LISTWORKERSRESPONSE._serialized_start = 47528 + _LISTWORKERSRESPONSE._serialized_end = 47693 + _UPDATETASKQUEUECONFIGREQUEST._serialized_start = 47696 + _UPDATETASKQUEUECONFIGREQUEST._serialized_end = 48421 + _UPDATETASKQUEUECONFIGREQUEST_RATELIMITUPDATE._serialized_start = 48263 + _UPDATETASKQUEUECONFIGREQUEST_RATELIMITUPDATE._serialized_end = 48354 + _UPDATETASKQUEUECONFIGREQUEST_SETFAIRNESSWEIGHTOVERRIDESENTRY._serialized_start = ( + 48356 + ) + _UPDATETASKQUEUECONFIGREQUEST_SETFAIRNESSWEIGHTOVERRIDESENTRY._serialized_end = ( + 48421 ) - _UPDATEWORKERDEPLOYMENTVERSIONMETADATARESPONSE._serialized_start = 39312 - _UPDATEWORKERDEPLOYMENTVERSIONMETADATARESPONSE._serialized_end = 39422 - _GETCURRENTDEPLOYMENTREQUEST._serialized_start = 39424 - _GETCURRENTDEPLOYMENTREQUEST._serialized_end = 39493 - _GETCURRENTDEPLOYMENTRESPONSE._serialized_start = 39495 - _GETCURRENTDEPLOYMENTRESPONSE._serialized_end = 39602 - _GETDEPLOYMENTREACHABILITYREQUEST._serialized_start = 39604 - _GETDEPLOYMENTREACHABILITYREQUEST._serialized_end = 39717 - _GETDEPLOYMENTREACHABILITYRESPONSE._serialized_start = 39720 - _GETDEPLOYMENTREACHABILITYRESPONSE._serialized_end = 39947 - _CREATEWORKFLOWRULEREQUEST._serialized_start = 39950 - _CREATEWORKFLOWRULEREQUEST._serialized_end = 40130 - _CREATEWORKFLOWRULERESPONSE._serialized_start = 40132 - _CREATEWORKFLOWRULERESPONSE._serialized_end = 40227 - _DESCRIBEWORKFLOWRULEREQUEST._serialized_start = 40229 - _DESCRIBEWORKFLOWRULEREQUEST._serialized_end = 40294 - _DESCRIBEWORKFLOWRULERESPONSE._serialized_start = 40296 - _DESCRIBEWORKFLOWRULERESPONSE._serialized_end = 40377 - _DELETEWORKFLOWRULEREQUEST._serialized_start = 40379 - _DELETEWORKFLOWRULEREQUEST._serialized_end = 40442 - _DELETEWORKFLOWRULERESPONSE._serialized_start = 40444 - _DELETEWORKFLOWRULERESPONSE._serialized_end = 40472 - _LISTWORKFLOWRULESREQUEST._serialized_start = 40474 - _LISTWORKFLOWRULESREQUEST._serialized_end = 40544 - _LISTWORKFLOWRULESRESPONSE._serialized_start = 40546 - _LISTWORKFLOWRULESRESPONSE._serialized_end = 40650 - _TRIGGERWORKFLOWRULEREQUEST._serialized_start = 40653 - _TRIGGERWORKFLOWRULEREQUEST._serialized_end = 40859 - _TRIGGERWORKFLOWRULERESPONSE._serialized_start = 40861 - _TRIGGERWORKFLOWRULERESPONSE._serialized_end = 40907 - _RECORDWORKERHEARTBEATREQUEST._serialized_start = 40910 - _RECORDWORKERHEARTBEATREQUEST._serialized_end = 41044 - _RECORDWORKERHEARTBEATRESPONSE._serialized_start = 41046 - _RECORDWORKERHEARTBEATRESPONSE._serialized_end = 41077 - _LISTWORKERSREQUEST._serialized_start = 41079 - _LISTWORKERSREQUEST._serialized_end = 41177 - _LISTWORKERSRESPONSE._serialized_start = 41179 - _LISTWORKERSRESPONSE._serialized_end = 41283 - _UPDATETASKQUEUECONFIGREQUEST._serialized_start = 41286 - _UPDATETASKQUEUECONFIGREQUEST._serialized_end = 41768 - _UPDATETASKQUEUECONFIGREQUEST_RATELIMITUPDATE._serialized_start = 41677 - _UPDATETASKQUEUECONFIGREQUEST_RATELIMITUPDATE._serialized_end = 41768 - _UPDATETASKQUEUECONFIGRESPONSE._serialized_start = 41770 - _UPDATETASKQUEUECONFIGRESPONSE._serialized_end = 41861 - _FETCHWORKERCONFIGREQUEST._serialized_start = 41864 - _FETCHWORKERCONFIGREQUEST._serialized_end = 42001 - _FETCHWORKERCONFIGRESPONSE._serialized_start = 42003 - _FETCHWORKERCONFIGRESPONSE._serialized_end = 42088 - _UPDATEWORKERCONFIGREQUEST._serialized_start = 42091 - _UPDATEWORKERCONFIGREQUEST._serialized_end = 42336 - _UPDATEWORKERCONFIGRESPONSE._serialized_start = 42338 - _UPDATEWORKERCONFIGRESPONSE._serialized_end = 42438 + _UPDATETASKQUEUECONFIGRESPONSE._serialized_start = 48423 + _UPDATETASKQUEUECONFIGRESPONSE._serialized_end = 48514 + _FETCHWORKERCONFIGREQUEST._serialized_start = 48517 + _FETCHWORKERCONFIGREQUEST._serialized_end = 48675 + _FETCHWORKERCONFIGRESPONSE._serialized_start = 48677 + _FETCHWORKERCONFIGRESPONSE._serialized_end = 48762 + _UPDATEWORKERCONFIGREQUEST._serialized_start = 48765 + _UPDATEWORKERCONFIGREQUEST._serialized_end = 49031 + _UPDATEWORKERCONFIGRESPONSE._serialized_start = 49033 + _UPDATEWORKERCONFIGRESPONSE._serialized_end = 49133 + _DESCRIBEWORKERREQUEST._serialized_start = 49135 + _DESCRIBEWORKERREQUEST._serialized_end = 49206 + _DESCRIBEWORKERRESPONSE._serialized_start = 49208 + _DESCRIBEWORKERRESPONSE._serialized_end = 49289 + _COUNTWORKERSREQUEST._serialized_start = 49291 + _COUNTWORKERSREQUEST._serialized_end = 49378 + _COUNTWORKERSRESPONSE._serialized_start = 49380 + _COUNTWORKERSRESPONSE._serialized_end = 49417 + _PAUSEWORKFLOWEXECUTIONREQUEST._serialized_start = 49420 + _PAUSEWORKFLOWEXECUTIONREQUEST._serialized_end = 49561 + _PAUSEWORKFLOWEXECUTIONRESPONSE._serialized_start = 49563 + _PAUSEWORKFLOWEXECUTIONRESPONSE._serialized_end = 49595 + _UNPAUSEWORKFLOWEXECUTIONREQUEST._serialized_start = 49598 + _UNPAUSEWORKFLOWEXECUTIONREQUEST._serialized_end = 49741 + _UNPAUSEWORKFLOWEXECUTIONRESPONSE._serialized_start = 49743 + _UNPAUSEWORKFLOWEXECUTIONRESPONSE._serialized_end = 49777 + _STARTACTIVITYEXECUTIONREQUEST._serialized_start = 49780 + _STARTACTIVITYEXECUTIONREQUEST._serialized_end = 50957 + _STARTACTIVITYEXECUTIONRESPONSE._serialized_start = 50959 + _STARTACTIVITYEXECUTIONRESPONSE._serialized_end = 51068 + _DESCRIBEACTIVITYEXECUTIONREQUEST._serialized_start = 51071 + _DESCRIBEACTIVITYEXECUTIONREQUEST._serialized_end = 51299 + _DESCRIBEACTIVITYEXECUTIONRESPONSE._serialized_start = 51302 + _DESCRIBEACTIVITYEXECUTIONRESPONSE._serialized_end = 51618 + _POLLACTIVITYEXECUTIONREQUEST._serialized_start = 51620 + _POLLACTIVITYEXECUTIONREQUEST._serialized_end = 51706 + _POLLACTIVITYEXECUTIONRESPONSE._serialized_start = 51708 + _POLLACTIVITYEXECUTIONRESPONSE._serialized_end = 51824 + _LISTACTIVITYEXECUTIONSREQUEST._serialized_start = 51826 + _LISTACTIVITYEXECUTIONSREQUEST._serialized_end = 51935 + _LISTACTIVITYEXECUTIONSRESPONSE._serialized_start = 51938 + _LISTACTIVITYEXECUTIONSRESPONSE._serialized_end = 52068 + _STARTNEXUSOPERATIONEXECUTIONREQUEST._serialized_start = 52071 + _STARTNEXUSOPERATIONEXECUTIONREQUEST._serialized_end = 52920 + _STARTNEXUSOPERATIONEXECUTIONREQUEST_NEXUSHEADERENTRY._serialized_start = 52870 + _STARTNEXUSOPERATIONEXECUTIONREQUEST_NEXUSHEADERENTRY._serialized_end = 52920 + _STARTNEXUSOPERATIONEXECUTIONRESPONSE._serialized_start = 52922 + _STARTNEXUSOPERATIONEXECUTIONRESPONSE._serialized_end = 52993 + _DESCRIBENEXUSOPERATIONEXECUTIONREQUEST._serialized_start = 52996 + _DESCRIBENEXUSOPERATIONEXECUTIONREQUEST._serialized_end = 53166 + _DESCRIBENEXUSOPERATIONEXECUTIONRESPONSE._serialized_start = 53169 + _DESCRIBENEXUSOPERATIONEXECUTIONRESPONSE._serialized_end = 53480 + _POLLNEXUSOPERATIONEXECUTIONREQUEST._serialized_start = 53483 + _POLLNEXUSOPERATIONEXECUTIONREQUEST._serialized_end = 53644 + _POLLNEXUSOPERATIONEXECUTIONRESPONSE._serialized_start = 53647 + _POLLNEXUSOPERATIONEXECUTIONRESPONSE._serialized_end = 53908 + _LISTNEXUSOPERATIONEXECUTIONSREQUEST._serialized_start = 53910 + _LISTNEXUSOPERATIONEXECUTIONSREQUEST._serialized_end = 54025 + _LISTNEXUSOPERATIONEXECUTIONSRESPONSE._serialized_start = 54028 + _LISTNEXUSOPERATIONEXECUTIONSRESPONSE._serialized_end = 54167 + _COUNTACTIVITYEXECUTIONSREQUEST._serialized_start = 54169 + _COUNTACTIVITYEXECUTIONSREQUEST._serialized_end = 54235 + _COUNTACTIVITYEXECUTIONSRESPONSE._serialized_start = 54238 + _COUNTACTIVITYEXECUTIONSRESPONSE._serialized_end = 54475 + _COUNTACTIVITYEXECUTIONSRESPONSE_AGGREGATIONGROUP._serialized_start = 20134 + _COUNTACTIVITYEXECUTIONSRESPONSE_AGGREGATIONGROUP._serialized_end = 20222 + _COUNTNEXUSOPERATIONEXECUTIONSREQUEST._serialized_start = 54477 + _COUNTNEXUSOPERATIONEXECUTIONSREQUEST._serialized_end = 54549 + _COUNTNEXUSOPERATIONEXECUTIONSRESPONSE._serialized_start = 54552 + _COUNTNEXUSOPERATIONEXECUTIONSRESPONSE._serialized_end = 54801 + _COUNTNEXUSOPERATIONEXECUTIONSRESPONSE_AGGREGATIONGROUP._serialized_start = 20134 + _COUNTNEXUSOPERATIONEXECUTIONSRESPONSE_AGGREGATIONGROUP._serialized_end = 20222 + _REQUESTCANCELACTIVITYEXECUTIONREQUEST._serialized_start = 54804 + _REQUESTCANCELACTIVITYEXECUTIONREQUEST._serialized_end = 54953 + _REQUESTCANCELACTIVITYEXECUTIONRESPONSE._serialized_start = 54955 + _REQUESTCANCELACTIVITYEXECUTIONRESPONSE._serialized_end = 54995 + _TERMINATEACTIVITYEXECUTIONREQUEST._serialized_start = 54998 + _TERMINATEACTIVITYEXECUTIONREQUEST._serialized_end = 55143 + _TERMINATEACTIVITYEXECUTIONRESPONSE._serialized_start = 55145 + _TERMINATEACTIVITYEXECUTIONRESPONSE._serialized_end = 55181 + _DELETEACTIVITYEXECUTIONREQUEST._serialized_start = 55183 + _DELETEACTIVITYEXECUTIONREQUEST._serialized_end = 55271 + _DELETEACTIVITYEXECUTIONRESPONSE._serialized_start = 55273 + _DELETEACTIVITYEXECUTIONRESPONSE._serialized_end = 55306 + _REQUESTCANCELNEXUSOPERATIONEXECUTIONREQUEST._serialized_start = 55309 + _REQUESTCANCELNEXUSOPERATIONEXECUTIONREQUEST._serialized_end = 55465 + _REQUESTCANCELNEXUSOPERATIONEXECUTIONRESPONSE._serialized_start = 55467 + _REQUESTCANCELNEXUSOPERATIONEXECUTIONRESPONSE._serialized_end = 55513 + _TERMINATENEXUSOPERATIONEXECUTIONREQUEST._serialized_start = 55516 + _TERMINATENEXUSOPERATIONEXECUTIONREQUEST._serialized_end = 55668 + _TERMINATENEXUSOPERATIONEXECUTIONRESPONSE._serialized_start = 55670 + _TERMINATENEXUSOPERATIONEXECUTIONRESPONSE._serialized_end = 55712 + _DELETENEXUSOPERATIONEXECUTIONREQUEST._serialized_start = 55714 + _DELETENEXUSOPERATIONEXECUTIONREQUEST._serialized_end = 55809 + _DELETENEXUSOPERATIONEXECUTIONRESPONSE._serialized_start = 55811 + _DELETENEXUSOPERATIONEXECUTIONRESPONSE._serialized_end = 55850 + _POLLWORKFLOWEXECUTIONTIMESKIPPINGREQUEST._serialized_start = 55853 + _POLLWORKFLOWEXECUTIONTIMESKIPPINGREQUEST._serialized_end = 56010 + _POLLWORKFLOWEXECUTIONTIMESKIPPINGRESPONSE._serialized_start = 56013 + _POLLWORKFLOWEXECUTIONTIMESKIPPINGRESPONSE._serialized_end = 56245 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/workflowservice/v1/request_response_pb2.pyi b/temporalio/api/workflowservice/v1/request_response_pb2.pyi index a0be16462..eb9243716 100644 --- a/temporalio/api/workflowservice/v1/request_response_pb2.pyi +++ b/temporalio/api/workflowservice/v1/request_response_pb2.pyi @@ -18,15 +18,19 @@ import temporalio.api.activity.v1.message_pb2 import temporalio.api.batch.v1.message_pb2 import temporalio.api.command.v1.message_pb2 import temporalio.api.common.v1.message_pb2 +import temporalio.api.compute.v1.config_pb2 import temporalio.api.deployment.v1.message_pb2 +import temporalio.api.enums.v1.activity_pb2 import temporalio.api.enums.v1.batch_operation_pb2 import temporalio.api.enums.v1.common_pb2 import temporalio.api.enums.v1.deployment_pb2 import temporalio.api.enums.v1.failed_cause_pb2 import temporalio.api.enums.v1.namespace_pb2 +import temporalio.api.enums.v1.nexus_pb2 import temporalio.api.enums.v1.query_pb2 import temporalio.api.enums.v1.reset_pb2 import temporalio.api.enums.v1.task_queue_pb2 +import temporalio.api.enums.v1.time_skipping_pb2 import temporalio.api.enums.v1.update_pb2 import temporalio.api.enums.v1.workflow_pb2 import temporalio.api.failure.v1.message_pb2 @@ -262,17 +266,32 @@ class DescribeNamespaceRequest(google.protobuf.message.Message): NAMESPACE_FIELD_NUMBER: builtins.int ID_FIELD_NUMBER: builtins.int + WEAK_CONSISTENCY_FIELD_NUMBER: builtins.int namespace: builtins.str id: builtins.str + weak_consistency: builtins.bool + """If true, the server may serve the response from an eventually-consistent + source instead of reading through to persistence. Defaults to false, + which preserves read-after-write consistency. SDKs should set this when + fetching namespace capabilities on worker/client startup. + """ def __init__( self, *, namespace: builtins.str = ..., id: builtins.str = ..., + weak_consistency: builtins.bool = ..., ) -> None: ... def ClearField( self, - field_name: typing_extensions.Literal["id", b"id", "namespace", b"namespace"], + field_name: typing_extensions.Literal[ + "id", + b"id", + "namespace", + b"namespace", + "weak_consistency", + b"weak_consistency", + ], ) -> None: ... global___DescribeNamespaceRequest = DescribeNamespaceRequest @@ -286,6 +305,8 @@ class DescribeNamespaceResponse(google.protobuf.message.Message): FAILOVER_VERSION_FIELD_NUMBER: builtins.int IS_GLOBAL_NAMESPACE_FIELD_NUMBER: builtins.int FAILOVER_HISTORY_FIELD_NUMBER: builtins.int + POLLER_GROUP_INFOS_FIELD_NUMBER: builtins.int + POLLER_GROUPS_INFO_FIELD_NUMBER: builtins.int @property def namespace_info( self, @@ -307,6 +328,26 @@ class DescribeNamespaceResponse(google.protobuf.message.Message): """Contains the historical state of failover_versions for the cluster, truncated to contain only the last N states to ensure that the list does not grow unbounded. """ + @property + def poller_group_infos( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.taskqueue.v1.message_pb2.PollerGroupInfo + ]: + """Deprecated. Use `poller_groups_info` instead, which carries a version so the client can + ignore stale updates. + The initial info that client should use for poller group assignment. This information is + updated through poll response. Client is supposed to use the info received in the latest + poll response. + """ + @property + def poller_groups_info( + self, + ) -> temporalio.api.taskqueue.v1.message_pb2.PollerGroupsInfo: + """The initial, versioned info that client should use for poller group assignment. This + information is updated through poll responses. Client is supposed to use the info with the + highest version it has received. + """ def __init__( self, *, @@ -321,6 +362,12 @@ class DescribeNamespaceResponse(google.protobuf.message.Message): temporalio.api.replication.v1.message_pb2.FailoverStatus ] | None = ..., + poller_group_infos: collections.abc.Iterable[ + temporalio.api.taskqueue.v1.message_pb2.PollerGroupInfo + ] + | None = ..., + poller_groups_info: temporalio.api.taskqueue.v1.message_pb2.PollerGroupsInfo + | None = ..., ) -> None: ... def HasField( self, @@ -329,6 +376,8 @@ class DescribeNamespaceResponse(google.protobuf.message.Message): b"config", "namespace_info", b"namespace_info", + "poller_groups_info", + b"poller_groups_info", "replication_config", b"replication_config", ], @@ -346,6 +395,10 @@ class DescribeNamespaceResponse(google.protobuf.message.Message): b"is_global_namespace", "namespace_info", b"namespace_info", + "poller_group_infos", + b"poller_group_infos", + "poller_groups_info", + b"poller_groups_info", "replication_config", b"replication_config", ], @@ -549,6 +602,8 @@ class StartWorkflowExecutionRequest(google.protobuf.message.Message): VERSIONING_OVERRIDE_FIELD_NUMBER: builtins.int ON_CONFLICT_OPTIONS_FIELD_NUMBER: builtins.int PRIORITY_FIELD_NUMBER: builtins.int + EAGER_WORKER_DEPLOYMENT_OPTIONS_FIELD_NUMBER: builtins.int + TIME_SKIPPING_CONFIG_FIELD_NUMBER: builtins.int namespace: builtins.str workflow_id: builtins.str @property @@ -619,8 +674,8 @@ class StartWorkflowExecutionRequest(google.protobuf.message.Message): ) -> temporalio.api.common.v1.message_pb2.Payloads: ... @property def workflow_start_delay(self) -> google.protobuf.duration_pb2.Duration: - """Time to wait before dispatching the first workflow task. Cannot be used with `cron_schedule`. - If the workflow gets a signal before the delay, a workflow task will be dispatched and the rest + """Time to wait before making the first workflow task available for dispatch. Cannot be used with `cron_schedule`. + If the workflow gets a signal before the delay, a workflow task will be made available for dispatch and the rest of the delay will be ignored. """ @property @@ -665,6 +720,16 @@ class StartWorkflowExecutionRequest(google.protobuf.message.Message): @property def priority(self) -> temporalio.api.common.v1.message_pb2.Priority: """Priority metadata""" + @property + def eager_worker_deployment_options( + self, + ) -> temporalio.api.deployment.v1.message_pb2.WorkerDeploymentOptions: + """Deployment Options of the worker who will process the eager task. Passed when `request_eager_execution=true`.""" + @property + def time_skipping_config( + self, + ) -> temporalio.api.common.v1.message_pb2.TimeSkippingConfig: + """Time-skipping configuration. If not set, time skipping is disabled.""" def __init__( self, *, @@ -704,12 +769,18 @@ class StartWorkflowExecutionRequest(google.protobuf.message.Message): on_conflict_options: temporalio.api.workflow.v1.message_pb2.OnConflictOptions | None = ..., priority: temporalio.api.common.v1.message_pb2.Priority | None = ..., + eager_worker_deployment_options: temporalio.api.deployment.v1.message_pb2.WorkerDeploymentOptions + | None = ..., + time_skipping_config: temporalio.api.common.v1.message_pb2.TimeSkippingConfig + | None = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal[ "continued_failure", b"continued_failure", + "eager_worker_deployment_options", + b"eager_worker_deployment_options", "header", b"header", "input", @@ -728,6 +799,8 @@ class StartWorkflowExecutionRequest(google.protobuf.message.Message): b"search_attributes", "task_queue", b"task_queue", + "time_skipping_config", + b"time_skipping_config", "user_metadata", b"user_metadata", "versioning_override", @@ -753,6 +826,8 @@ class StartWorkflowExecutionRequest(google.protobuf.message.Message): b"continued_failure", "cron_schedule", b"cron_schedule", + "eager_worker_deployment_options", + b"eager_worker_deployment_options", "header", b"header", "identity", @@ -781,6 +856,8 @@ class StartWorkflowExecutionRequest(google.protobuf.message.Message): b"search_attributes", "task_queue", b"task_queue", + "time_skipping_config", + b"time_skipping_config", "user_metadata", b"user_metadata", "versioning_override", @@ -810,12 +887,15 @@ class StartWorkflowExecutionResponse(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor RUN_ID_FIELD_NUMBER: builtins.int + FIRST_EXECUTION_RUN_ID_FIELD_NUMBER: builtins.int STARTED_FIELD_NUMBER: builtins.int STATUS_FIELD_NUMBER: builtins.int EAGER_WORKFLOW_TASK_FIELD_NUMBER: builtins.int LINK_FIELD_NUMBER: builtins.int run_id: builtins.str """The run id of the workflow that was started - or used (via WorkflowIdConflictPolicy USE_EXISTING).""" + first_execution_run_id: builtins.str + """If the workflow was started as a result of a de-dupe, this field will contain the run id of the first execution in the chain.""" started: builtins.bool """If true, a new workflow was started.""" status: temporalio.api.enums.v1.workflow_pb2.WorkflowExecutionStatus.ValueType @@ -835,6 +915,7 @@ class StartWorkflowExecutionResponse(google.protobuf.message.Message): self, *, run_id: builtins.str = ..., + first_execution_run_id: builtins.str = ..., started: builtins.bool = ..., status: temporalio.api.enums.v1.workflow_pb2.WorkflowExecutionStatus.ValueType = ..., eager_workflow_task: global___PollWorkflowTaskQueueResponse | None = ..., @@ -851,6 +932,8 @@ class StartWorkflowExecutionResponse(google.protobuf.message.Message): field_name: typing_extensions.Literal[ "eager_workflow_task", b"eager_workflow_task", + "first_execution_run_id", + b"first_execution_run_id", "link", b"link", "run_id", @@ -1055,16 +1138,32 @@ class PollWorkflowTaskQueueRequest(google.protobuf.message.Message): NAMESPACE_FIELD_NUMBER: builtins.int TASK_QUEUE_FIELD_NUMBER: builtins.int + POLLER_GROUP_ID_FIELD_NUMBER: builtins.int IDENTITY_FIELD_NUMBER: builtins.int + WORKER_INSTANCE_KEY_FIELD_NUMBER: builtins.int + WORKER_CONTROL_TASK_QUEUE_FIELD_NUMBER: builtins.int BINARY_CHECKSUM_FIELD_NUMBER: builtins.int WORKER_VERSION_CAPABILITIES_FIELD_NUMBER: builtins.int DEPLOYMENT_OPTIONS_FIELD_NUMBER: builtins.int - WORKER_HEARTBEAT_FIELD_NUMBER: builtins.int namespace: builtins.str @property def task_queue(self) -> temporalio.api.taskqueue.v1.message_pb2.TaskQueue: ... + poller_group_id: builtins.str + """Unless this is the first poll, the client must pass one of the poller group IDs received in + `poller_group_infos` of the last the PollWorkflowTaskQueueResponse according to the + instructions. If not set, the poll is routed randomly which can cause it to be blocked + without receiving a task while the queue actually has tasks in another server location. + """ identity: builtins.str """The identity of the worker/client who is polling this task queue""" + worker_instance_key: builtins.str + """A unique key for this worker instance, used for tracking worker lifecycle. + This is guaranteed to be unique, whereas identity is not guaranteed to be unique. + """ + worker_control_task_queue: builtins.str + """A dedicated per-worker Nexus task queue on which the server sends control + tasks (e.g. activity cancellation) to this specific worker instance. + """ binary_checksum: builtins.str """Deprecated. Use deployment_options instead. Each worker process should provide an ID unique to the specific set of code it is running @@ -1082,25 +1181,21 @@ class PollWorkflowTaskQueueRequest(google.protobuf.message.Message): def deployment_options( self, ) -> temporalio.api.deployment.v1.message_pb2.WorkerDeploymentOptions: - """Worker deployment options that user has set in the worker. - Experimental. Worker Deployments are experimental and might significantly change in the future. - """ - @property - def worker_heartbeat(self) -> temporalio.api.worker.v1.message_pb2.WorkerHeartbeat: - """Worker info to be sent to the server.""" + """Worker deployment options that user has set in the worker.""" def __init__( self, *, namespace: builtins.str = ..., task_queue: temporalio.api.taskqueue.v1.message_pb2.TaskQueue | None = ..., + poller_group_id: builtins.str = ..., identity: builtins.str = ..., + worker_instance_key: builtins.str = ..., + worker_control_task_queue: builtins.str = ..., binary_checksum: builtins.str = ..., worker_version_capabilities: temporalio.api.common.v1.message_pb2.WorkerVersionCapabilities | None = ..., deployment_options: temporalio.api.deployment.v1.message_pb2.WorkerDeploymentOptions | None = ..., - worker_heartbeat: temporalio.api.worker.v1.message_pb2.WorkerHeartbeat - | None = ..., ) -> None: ... def HasField( self, @@ -1109,8 +1204,6 @@ class PollWorkflowTaskQueueRequest(google.protobuf.message.Message): b"deployment_options", "task_queue", b"task_queue", - "worker_heartbeat", - b"worker_heartbeat", "worker_version_capabilities", b"worker_version_capabilities", ], @@ -1126,10 +1219,14 @@ class PollWorkflowTaskQueueRequest(google.protobuf.message.Message): b"identity", "namespace", b"namespace", + "poller_group_id", + b"poller_group_id", "task_queue", b"task_queue", - "worker_heartbeat", - b"worker_heartbeat", + "worker_control_task_queue", + b"worker_control_task_queue", + "worker_instance_key", + b"worker_instance_key", "worker_version_capabilities", b"worker_version_capabilities", ], @@ -1178,6 +1275,9 @@ class PollWorkflowTaskQueueResponse(google.protobuf.message.Message): QUERIES_FIELD_NUMBER: builtins.int MESSAGES_FIELD_NUMBER: builtins.int POLLER_SCALING_DECISION_FIELD_NUMBER: builtins.int + POLLER_GROUP_ID_FIELD_NUMBER: builtins.int + POLLER_GROUP_INFOS_FIELD_NUMBER: builtins.int + POLLER_GROUPS_INFO_FIELD_NUMBER: builtins.int task_token: builtins.bytes """A unique identifier for this task""" @property @@ -1261,6 +1361,37 @@ class PollWorkflowTaskQueueResponse(google.protobuf.message.Message): self, ) -> temporalio.api.taskqueue.v1.message_pb2.PollerScalingDecision: """Server-advised information the SDK may use to adjust its poller count.""" + poller_group_id: builtins.str + """This poller group ID identifies the owner of the workflow task awaiting for query response. + Corresponding RespondQueryTaskCompleted should pass this value for proper routing. + """ + @property + def poller_group_infos( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.taskqueue.v1.message_pb2.PollerGroupInfo + ]: + """Deprecated. Use `poller_groups_info` instead, which carries a version so the client can + ignore stale updates. + The weighted list of poller groups IDs that client should use for future polls to this task + queue. Client is expected to: + 1. Maintain minimum number of pollers no less than the number of groups. + 2. Try to assign the next poll to a group without any pending polls, + 3. If every group has some pending polls, assign the next poll to a group randomly + according to the weights. + """ + @property + def poller_groups_info( + self, + ) -> temporalio.api.taskqueue.v1.message_pb2.PollerGroupsInfo: + """The weighted, versioned list of poller groups IDs that client should use for future polls to + this task queue. Client should ignore this if it has already applied a snapshot with a + version greater than or equal to `poller_groups_info.version`. Client is expected to: + 1. Maintain minimum number of pollers no less than the number of groups. + 2. Try to assign the next poll to a group without any pending polls, + 3. If every group has some pending polls, assign the next poll to a group randomly + according to the weights. + """ def __init__( self, *, @@ -1289,12 +1420,21 @@ class PollWorkflowTaskQueueResponse(google.protobuf.message.Message): | None = ..., poller_scaling_decision: temporalio.api.taskqueue.v1.message_pb2.PollerScalingDecision | None = ..., + poller_group_id: builtins.str = ..., + poller_group_infos: collections.abc.Iterable[ + temporalio.api.taskqueue.v1.message_pb2.PollerGroupInfo + ] + | None = ..., + poller_groups_info: temporalio.api.taskqueue.v1.message_pb2.PollerGroupsInfo + | None = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal[ "history", b"history", + "poller_groups_info", + b"poller_groups_info", "poller_scaling_decision", b"poller_scaling_decision", "query", @@ -1324,6 +1464,12 @@ class PollWorkflowTaskQueueResponse(google.protobuf.message.Message): b"messages", "next_page_token", b"next_page_token", + "poller_group_id", + b"poller_group_id", + "poller_group_infos", + b"poller_group_infos", + "poller_groups_info", + b"poller_groups_info", "poller_scaling_decision", b"poller_scaling_decision", "previous_started_event_id", @@ -1412,6 +1558,7 @@ class RespondWorkflowTaskCompletedRequest(google.protobuf.message.Message): BINARY_CHECKSUM_FIELD_NUMBER: builtins.int QUERY_RESULTS_FIELD_NUMBER: builtins.int NAMESPACE_FIELD_NUMBER: builtins.int + RESOURCE_ID_FIELD_NUMBER: builtins.int WORKER_VERSION_STAMP_FIELD_NUMBER: builtins.int MESSAGES_FIELD_NUMBER: builtins.int SDK_METADATA_FIELD_NUMBER: builtins.int @@ -1420,6 +1567,10 @@ class RespondWorkflowTaskCompletedRequest(google.protobuf.message.Message): DEPLOYMENT_FIELD_NUMBER: builtins.int VERSIONING_BEHAVIOR_FIELD_NUMBER: builtins.int DEPLOYMENT_OPTIONS_FIELD_NUMBER: builtins.int + WORKER_INSTANCE_KEY_FIELD_NUMBER: builtins.int + WORKER_CONTROL_TASK_QUEUE_FIELD_NUMBER: builtins.int + PAGE_NUMBER_FIELD_NUMBER: builtins.int + INTERMEDIATE_PAGE_FIELD_NUMBER: builtins.int task_token: builtins.bytes """The task token as received in `PollWorkflowTaskQueueResponse`""" @property @@ -1460,6 +1611,8 @@ class RespondWorkflowTaskCompletedRequest(google.protobuf.message.Message): ]: """Responses to the `queries` field in the task being responded to""" namespace: builtins.str + resource_id: builtins.str + """Resource ID for routing. Contains the workflow ID from the original task.""" @property def worker_version_stamp( self, @@ -1508,6 +1661,24 @@ class RespondWorkflowTaskCompletedRequest(google.protobuf.message.Message): self, ) -> temporalio.api.deployment.v1.message_pb2.WorkerDeploymentOptions: """Worker deployment options that user has set in the worker.""" + worker_instance_key: builtins.str + """A unique key for this worker instance, used for tracking worker lifecycle. + This is guaranteed to be unique, whereas identity is not guaranteed to be unique. + """ + worker_control_task_queue: builtins.str + """A dedicated per-worker Nexus task queue on which the server sends control + tasks (e.g. activity cancellation) to this specific worker instance. + """ + page_number: builtins.int + """0-indexed page number when the workflow task completion is split across multiple + requests ("pages"). 0 for single-page requests. May only be set to non-zero value + when the namespace capability workflow_task_completion_pagination is true. + """ + intermediate_page: builtins.bool + """True for non-final pages of a paginated workflow task completion. The final page's + `page_number` tells the server how many intermediate pages (0..page_number-1) preceded it. + May only be used when the namespace capability workflow_task_completion_pagination is true. + """ def __init__( self, *, @@ -1527,6 +1698,7 @@ class RespondWorkflowTaskCompletedRequest(google.protobuf.message.Message): ] | None = ..., namespace: builtins.str = ..., + resource_id: builtins.str = ..., worker_version_stamp: temporalio.api.common.v1.message_pb2.WorkerVersionStamp | None = ..., messages: collections.abc.Iterable[ @@ -1543,6 +1715,10 @@ class RespondWorkflowTaskCompletedRequest(google.protobuf.message.Message): versioning_behavior: temporalio.api.enums.v1.workflow_pb2.VersioningBehavior.ValueType = ..., deployment_options: temporalio.api.deployment.v1.message_pb2.WorkerDeploymentOptions | None = ..., + worker_instance_key: builtins.str = ..., + worker_control_task_queue: builtins.str = ..., + page_number: builtins.int = ..., + intermediate_page: builtins.bool = ..., ) -> None: ... def HasField( self, @@ -1580,14 +1756,20 @@ class RespondWorkflowTaskCompletedRequest(google.protobuf.message.Message): b"force_create_new_workflow_task", "identity", b"identity", + "intermediate_page", + b"intermediate_page", "messages", b"messages", "metering_metadata", b"metering_metadata", "namespace", b"namespace", + "page_number", + b"page_number", "query_results", b"query_results", + "resource_id", + b"resource_id", "return_new_workflow_task", b"return_new_workflow_task", "sdk_metadata", @@ -1598,6 +1780,10 @@ class RespondWorkflowTaskCompletedRequest(google.protobuf.message.Message): b"task_token", "versioning_behavior", b"versioning_behavior", + "worker_control_task_queue", + b"worker_control_task_queue", + "worker_instance_key", + b"worker_instance_key", "worker_version_stamp", b"worker_version_stamp", ], @@ -1660,6 +1846,7 @@ class RespondWorkflowTaskFailedRequest(google.protobuf.message.Message): IDENTITY_FIELD_NUMBER: builtins.int BINARY_CHECKSUM_FIELD_NUMBER: builtins.int NAMESPACE_FIELD_NUMBER: builtins.int + RESOURCE_ID_FIELD_NUMBER: builtins.int MESSAGES_FIELD_NUMBER: builtins.int WORKER_VERSION_FIELD_NUMBER: builtins.int DEPLOYMENT_FIELD_NUMBER: builtins.int @@ -1680,6 +1867,8 @@ class RespondWorkflowTaskFailedRequest(google.protobuf.message.Message): Worker process' unique binary id """ namespace: builtins.str + resource_id: builtins.str + """Resource ID for routing. Contains the workflow ID from the original task.""" @property def messages( self, @@ -1714,6 +1903,7 @@ class RespondWorkflowTaskFailedRequest(google.protobuf.message.Message): identity: builtins.str = ..., binary_checksum: builtins.str = ..., namespace: builtins.str = ..., + resource_id: builtins.str = ..., messages: collections.abc.Iterable[ temporalio.api.protocol.v1.message_pb2.Message ] @@ -1756,6 +1946,8 @@ class RespondWorkflowTaskFailedRequest(google.protobuf.message.Message): b"messages", "namespace", b"namespace", + "resource_id", + b"resource_id", "task_token", b"task_token", "worker_version", @@ -1779,16 +1971,32 @@ class PollActivityTaskQueueRequest(google.protobuf.message.Message): NAMESPACE_FIELD_NUMBER: builtins.int TASK_QUEUE_FIELD_NUMBER: builtins.int + POLLER_GROUP_ID_FIELD_NUMBER: builtins.int IDENTITY_FIELD_NUMBER: builtins.int + WORKER_INSTANCE_KEY_FIELD_NUMBER: builtins.int + WORKER_CONTROL_TASK_QUEUE_FIELD_NUMBER: builtins.int TASK_QUEUE_METADATA_FIELD_NUMBER: builtins.int WORKER_VERSION_CAPABILITIES_FIELD_NUMBER: builtins.int DEPLOYMENT_OPTIONS_FIELD_NUMBER: builtins.int - WORKER_HEARTBEAT_FIELD_NUMBER: builtins.int namespace: builtins.str @property def task_queue(self) -> temporalio.api.taskqueue.v1.message_pb2.TaskQueue: ... + poller_group_id: builtins.str + """Unless this is the first poll, the client must pass one of the poller group IDs received in + `poller_group_infos` of the last the PollActivityTaskQueueResponse according to the + instructions. If not set, the poll is routed randomly which can cause it to be blocked + without receiving a task while the queue actually has tasks in another server location. + """ identity: builtins.str """The identity of the worker/client""" + worker_instance_key: builtins.str + """A unique key for this worker instance, used for tracking worker lifecycle. + This is guaranteed to be unique, whereas identity is not guaranteed to be unique. + """ + worker_control_task_queue: builtins.str + """A dedicated per-worker Nexus task queue on which the server sends control + tasks (e.g. activity cancellation) to this specific worker instance. + """ @property def task_queue_metadata( self, @@ -1806,23 +2014,21 @@ class PollActivityTaskQueueRequest(google.protobuf.message.Message): self, ) -> temporalio.api.deployment.v1.message_pb2.WorkerDeploymentOptions: """Worker deployment options that user has set in the worker.""" - @property - def worker_heartbeat(self) -> temporalio.api.worker.v1.message_pb2.WorkerHeartbeat: - """Worker info to be sent to the server.""" def __init__( self, *, namespace: builtins.str = ..., task_queue: temporalio.api.taskqueue.v1.message_pb2.TaskQueue | None = ..., + poller_group_id: builtins.str = ..., identity: builtins.str = ..., + worker_instance_key: builtins.str = ..., + worker_control_task_queue: builtins.str = ..., task_queue_metadata: temporalio.api.taskqueue.v1.message_pb2.TaskQueueMetadata | None = ..., worker_version_capabilities: temporalio.api.common.v1.message_pb2.WorkerVersionCapabilities | None = ..., deployment_options: temporalio.api.deployment.v1.message_pb2.WorkerDeploymentOptions | None = ..., - worker_heartbeat: temporalio.api.worker.v1.message_pb2.WorkerHeartbeat - | None = ..., ) -> None: ... def HasField( self, @@ -1833,8 +2039,6 @@ class PollActivityTaskQueueRequest(google.protobuf.message.Message): b"task_queue", "task_queue_metadata", b"task_queue_metadata", - "worker_heartbeat", - b"worker_heartbeat", "worker_version_capabilities", b"worker_version_capabilities", ], @@ -1848,12 +2052,16 @@ class PollActivityTaskQueueRequest(google.protobuf.message.Message): b"identity", "namespace", b"namespace", + "poller_group_id", + b"poller_group_id", "task_queue", b"task_queue", "task_queue_metadata", b"task_queue_metadata", - "worker_heartbeat", - b"worker_heartbeat", + "worker_control_task_queue", + b"worker_control_task_queue", + "worker_instance_key", + b"worker_instance_key", "worker_version_capabilities", b"worker_version_capabilities", ], @@ -1883,24 +2091,31 @@ class PollActivityTaskQueueResponse(google.protobuf.message.Message): RETRY_POLICY_FIELD_NUMBER: builtins.int POLLER_SCALING_DECISION_FIELD_NUMBER: builtins.int PRIORITY_FIELD_NUMBER: builtins.int + ACTIVITY_RUN_ID_FIELD_NUMBER: builtins.int + POLLER_GROUP_INFOS_FIELD_NUMBER: builtins.int + POLLER_GROUPS_INFO_FIELD_NUMBER: builtins.int task_token: builtins.bytes """A unique identifier for this task""" workflow_namespace: builtins.str - """The namespace the workflow which requested this activity lives in""" + """The namespace of the activity. If this is a workflow activity then this is the namespace of + the workflow also. If this is a standalone activity then the name of this field is + misleading, but retained for compatibility with workflow activities. + """ @property def workflow_type(self) -> temporalio.api.common.v1.message_pb2.WorkflowType: - """Type of the requesting workflow""" + """Type of the requesting workflow (if this is a workflow activity).""" @property def workflow_execution( self, ) -> temporalio.api.common.v1.message_pb2.WorkflowExecution: - """Execution info of the requesting workflow""" + """Execution info of the requesting workflow (if this is a workflow activity)""" @property def activity_type(self) -> temporalio.api.common.v1.message_pb2.ActivityType: ... activity_id: builtins.str """The autogenerated or user specified identifier of this activity. Can be used to complete the activity via `RespondActivityTaskCompletedById`. May be re-used as long as the last usage has resolved, but unique IDs for every activity invocation is a good idea. + Note that only a workflow activity ID may be autogenerated. """ @property def header(self) -> temporalio.api.common.v1.message_pb2.Header: @@ -1957,6 +2172,33 @@ class PollActivityTaskQueueResponse(google.protobuf.message.Message): @property def priority(self) -> temporalio.api.common.v1.message_pb2.Priority: """Priority metadata""" + activity_run_id: builtins.str + """The run ID of the activity execution, only set for standalone activities.""" + @property + def poller_group_infos( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.taskqueue.v1.message_pb2.PollerGroupInfo + ]: + """The weighted list of poller groups IDs that client should use for future polls to this task + queue. Client is expected to: + 1. Maintain minimum number of pollers no less than the number of groups. + 2. Try to assign the next poll to a group without any pending polls, + 3. If every group has some pending polls, assign the next poll to a group randomly + according to the weights. + """ + @property + def poller_groups_info( + self, + ) -> temporalio.api.taskqueue.v1.message_pb2.PollerGroupsInfo: + """The weighted, versioned list of poller groups IDs that client should use for future polls to + this task queue. Client should ignore this if it has already applied a snapshot with a + version greater than or equal to `poller_groups_info.version`. Client is expected to: + 1. Maintain minimum number of pollers no less than the number of groups. + 2. Try to assign the next poll to a group without any pending polls, + 3. If every group has some pending polls, assign the next poll to a group randomly + according to the weights. + """ def __init__( self, *, @@ -1982,6 +2224,13 @@ class PollActivityTaskQueueResponse(google.protobuf.message.Message): poller_scaling_decision: temporalio.api.taskqueue.v1.message_pb2.PollerScalingDecision | None = ..., priority: temporalio.api.common.v1.message_pb2.Priority | None = ..., + activity_run_id: builtins.str = ..., + poller_group_infos: collections.abc.Iterable[ + temporalio.api.taskqueue.v1.message_pb2.PollerGroupInfo + ] + | None = ..., + poller_groups_info: temporalio.api.taskqueue.v1.message_pb2.PollerGroupsInfo + | None = ..., ) -> None: ... def HasField( self, @@ -1998,6 +2247,8 @@ class PollActivityTaskQueueResponse(google.protobuf.message.Message): b"heartbeat_timeout", "input", b"input", + "poller_groups_info", + b"poller_groups_info", "poller_scaling_decision", b"poller_scaling_decision", "priority", @@ -2023,6 +2274,8 @@ class PollActivityTaskQueueResponse(google.protobuf.message.Message): field_name: typing_extensions.Literal[ "activity_id", b"activity_id", + "activity_run_id", + b"activity_run_id", "activity_type", b"activity_type", "attempt", @@ -2037,6 +2290,10 @@ class PollActivityTaskQueueResponse(google.protobuf.message.Message): b"heartbeat_timeout", "input", b"input", + "poller_group_infos", + b"poller_group_infos", + "poller_groups_info", + b"poller_groups_info", "poller_scaling_decision", b"poller_scaling_decision", "priority", @@ -2071,6 +2328,7 @@ class RecordActivityTaskHeartbeatRequest(google.protobuf.message.Message): DETAILS_FIELD_NUMBER: builtins.int IDENTITY_FIELD_NUMBER: builtins.int NAMESPACE_FIELD_NUMBER: builtins.int + RESOURCE_ID_FIELD_NUMBER: builtins.int task_token: builtins.bytes """The task token as received in `PollActivityTaskQueueResponse`""" @property @@ -2079,6 +2337,8 @@ class RecordActivityTaskHeartbeatRequest(google.protobuf.message.Message): identity: builtins.str """The identity of the worker/client""" namespace: builtins.str + resource_id: builtins.str + """Resource ID for routing. Contains the workflow ID or activity ID for standalone activities.""" def __init__( self, *, @@ -2086,6 +2346,7 @@ class RecordActivityTaskHeartbeatRequest(google.protobuf.message.Message): details: temporalio.api.common.v1.message_pb2.Payloads | None = ..., identity: builtins.str = ..., namespace: builtins.str = ..., + resource_id: builtins.str = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal["details", b"details"] @@ -2099,6 +2360,8 @@ class RecordActivityTaskHeartbeatRequest(google.protobuf.message.Message): b"identity", "namespace", b"namespace", + "resource_id", + b"resource_id", "task_token", b"task_token", ], @@ -2152,12 +2415,15 @@ class RecordActivityTaskHeartbeatByIdRequest(google.protobuf.message.Message): ACTIVITY_ID_FIELD_NUMBER: builtins.int DETAILS_FIELD_NUMBER: builtins.int IDENTITY_FIELD_NUMBER: builtins.int + RESOURCE_ID_FIELD_NUMBER: builtins.int namespace: builtins.str """Namespace of the workflow which scheduled this activity""" workflow_id: builtins.str - """Id of the workflow which scheduled this activity""" + """Id of the workflow which scheduled this activity, leave empty to target a standalone activity""" run_id: builtins.str - """Run Id of the workflow which scheduled this activity""" + """For a workflow activity - the run ID of the workflow which scheduled this activity. + For a standalone activity - the run ID of the activity. + """ activity_id: builtins.str """Id of the activity we're heartbeating""" @property @@ -2165,6 +2431,8 @@ class RecordActivityTaskHeartbeatByIdRequest(google.protobuf.message.Message): """Arbitrary data, of which the most recent call is kept, to store for this activity""" identity: builtins.str """The identity of the worker/client""" + resource_id: builtins.str + """Resource ID for routing. Contains "workflow:workflow_id" or "activity:activity_id" for standalone activities.""" def __init__( self, *, @@ -2174,6 +2442,7 @@ class RecordActivityTaskHeartbeatByIdRequest(google.protobuf.message.Message): activity_id: builtins.str = ..., details: temporalio.api.common.v1.message_pb2.Payloads | None = ..., identity: builtins.str = ..., + resource_id: builtins.str = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal["details", b"details"] @@ -2189,6 +2458,8 @@ class RecordActivityTaskHeartbeatByIdRequest(google.protobuf.message.Message): b"identity", "namespace", b"namespace", + "resource_id", + b"resource_id", "run_id", b"run_id", "workflow_id", @@ -2244,6 +2515,7 @@ class RespondActivityTaskCompletedRequest(google.protobuf.message.Message): RESULT_FIELD_NUMBER: builtins.int IDENTITY_FIELD_NUMBER: builtins.int NAMESPACE_FIELD_NUMBER: builtins.int + RESOURCE_ID_FIELD_NUMBER: builtins.int WORKER_VERSION_FIELD_NUMBER: builtins.int DEPLOYMENT_FIELD_NUMBER: builtins.int DEPLOYMENT_OPTIONS_FIELD_NUMBER: builtins.int @@ -2255,6 +2527,8 @@ class RespondActivityTaskCompletedRequest(google.protobuf.message.Message): identity: builtins.str """The identity of the worker/client""" namespace: builtins.str + resource_id: builtins.str + """Resource ID for routing. Contains the workflow ID or activity ID for standalone activities.""" @property def worker_version(self) -> temporalio.api.common.v1.message_pb2.WorkerVersionStamp: """Version info of the worker who processed this task. This message's `build_id` field should @@ -2280,6 +2554,7 @@ class RespondActivityTaskCompletedRequest(google.protobuf.message.Message): result: temporalio.api.common.v1.message_pb2.Payloads | None = ..., identity: builtins.str = ..., namespace: builtins.str = ..., + resource_id: builtins.str = ..., worker_version: temporalio.api.common.v1.message_pb2.WorkerVersionStamp | None = ..., deployment: temporalio.api.deployment.v1.message_pb2.Deployment | None = ..., @@ -2310,6 +2585,8 @@ class RespondActivityTaskCompletedRequest(google.protobuf.message.Message): b"identity", "namespace", b"namespace", + "resource_id", + b"resource_id", "result", b"result", "task_token", @@ -2339,12 +2616,15 @@ class RespondActivityTaskCompletedByIdRequest(google.protobuf.message.Message): ACTIVITY_ID_FIELD_NUMBER: builtins.int RESULT_FIELD_NUMBER: builtins.int IDENTITY_FIELD_NUMBER: builtins.int + RESOURCE_ID_FIELD_NUMBER: builtins.int namespace: builtins.str """Namespace of the workflow which scheduled this activity""" workflow_id: builtins.str - """Id of the workflow which scheduled this activity""" + """Id of the workflow which scheduled this activity, leave empty to target a standalone activity""" run_id: builtins.str - """Run Id of the workflow which scheduled this activity""" + """For a workflow activity - the run ID of the workflow which scheduled this activity. + For a standalone activity - the run ID of the activity. + """ activity_id: builtins.str """Id of the activity to complete""" @property @@ -2352,6 +2632,8 @@ class RespondActivityTaskCompletedByIdRequest(google.protobuf.message.Message): """The serialized result of activity execution""" identity: builtins.str """The identity of the worker/client""" + resource_id: builtins.str + """Resource ID for routing. Contains "workflow:workflow_id" or "activity:activity_id" for standalone activities.""" def __init__( self, *, @@ -2361,6 +2643,7 @@ class RespondActivityTaskCompletedByIdRequest(google.protobuf.message.Message): activity_id: builtins.str = ..., result: temporalio.api.common.v1.message_pb2.Payloads | None = ..., identity: builtins.str = ..., + resource_id: builtins.str = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal["result", b"result"] @@ -2374,6 +2657,8 @@ class RespondActivityTaskCompletedByIdRequest(google.protobuf.message.Message): b"identity", "namespace", b"namespace", + "resource_id", + b"resource_id", "result", b"result", "run_id", @@ -2405,6 +2690,7 @@ class RespondActivityTaskFailedRequest(google.protobuf.message.Message): FAILURE_FIELD_NUMBER: builtins.int IDENTITY_FIELD_NUMBER: builtins.int NAMESPACE_FIELD_NUMBER: builtins.int + RESOURCE_ID_FIELD_NUMBER: builtins.int LAST_HEARTBEAT_DETAILS_FIELD_NUMBER: builtins.int WORKER_VERSION_FIELD_NUMBER: builtins.int DEPLOYMENT_FIELD_NUMBER: builtins.int @@ -2417,6 +2703,8 @@ class RespondActivityTaskFailedRequest(google.protobuf.message.Message): identity: builtins.str """The identity of the worker/client""" namespace: builtins.str + resource_id: builtins.str + """Resource ID for routing. Contains the workflow ID or activity ID for standalone activities.""" @property def last_heartbeat_details(self) -> temporalio.api.common.v1.message_pb2.Payloads: """Additional details to be stored as last activity heartbeat""" @@ -2445,6 +2733,7 @@ class RespondActivityTaskFailedRequest(google.protobuf.message.Message): failure: temporalio.api.failure.v1.message_pb2.Failure | None = ..., identity: builtins.str = ..., namespace: builtins.str = ..., + resource_id: builtins.str = ..., last_heartbeat_details: temporalio.api.common.v1.message_pb2.Payloads | None = ..., worker_version: temporalio.api.common.v1.message_pb2.WorkerVersionStamp @@ -2483,6 +2772,8 @@ class RespondActivityTaskFailedRequest(google.protobuf.message.Message): b"last_heartbeat_details", "namespace", b"namespace", + "resource_id", + b"resource_id", "task_token", b"task_token", "worker_version", @@ -2529,12 +2820,15 @@ class RespondActivityTaskFailedByIdRequest(google.protobuf.message.Message): FAILURE_FIELD_NUMBER: builtins.int IDENTITY_FIELD_NUMBER: builtins.int LAST_HEARTBEAT_DETAILS_FIELD_NUMBER: builtins.int + RESOURCE_ID_FIELD_NUMBER: builtins.int namespace: builtins.str """Namespace of the workflow which scheduled this activity""" workflow_id: builtins.str - """Id of the workflow which scheduled this activity""" + """Id of the workflow which scheduled this activity, leave empty to target a standalone activity""" run_id: builtins.str - """Run Id of the workflow which scheduled this activity""" + """For a workflow activity - the run ID of the workflow which scheduled this activity. + For a standalone activity - the run ID of the activity. + """ activity_id: builtins.str """Id of the activity to fail""" @property @@ -2545,6 +2839,8 @@ class RespondActivityTaskFailedByIdRequest(google.protobuf.message.Message): @property def last_heartbeat_details(self) -> temporalio.api.common.v1.message_pb2.Payloads: """Additional details to be stored as last activity heartbeat""" + resource_id: builtins.str + """Resource ID for routing. Contains "workflow:workflow_id" or "activity:activity_id" for standalone activities.""" def __init__( self, *, @@ -2556,6 +2852,7 @@ class RespondActivityTaskFailedByIdRequest(google.protobuf.message.Message): identity: builtins.str = ..., last_heartbeat_details: temporalio.api.common.v1.message_pb2.Payloads | None = ..., + resource_id: builtins.str = ..., ) -> None: ... def HasField( self, @@ -2576,6 +2873,8 @@ class RespondActivityTaskFailedByIdRequest(google.protobuf.message.Message): b"last_heartbeat_details", "namespace", b"namespace", + "resource_id", + b"resource_id", "run_id", b"run_id", "workflow_id", @@ -2619,6 +2918,7 @@ class RespondActivityTaskCanceledRequest(google.protobuf.message.Message): DETAILS_FIELD_NUMBER: builtins.int IDENTITY_FIELD_NUMBER: builtins.int NAMESPACE_FIELD_NUMBER: builtins.int + RESOURCE_ID_FIELD_NUMBER: builtins.int WORKER_VERSION_FIELD_NUMBER: builtins.int DEPLOYMENT_FIELD_NUMBER: builtins.int DEPLOYMENT_OPTIONS_FIELD_NUMBER: builtins.int @@ -2630,6 +2930,8 @@ class RespondActivityTaskCanceledRequest(google.protobuf.message.Message): identity: builtins.str """The identity of the worker/client""" namespace: builtins.str + resource_id: builtins.str + """Resource ID for routing. Contains the workflow ID or activity ID for standalone activities.""" @property def worker_version(self) -> temporalio.api.common.v1.message_pb2.WorkerVersionStamp: """Version info of the worker who processed this task. This message's `build_id` field should @@ -2655,6 +2957,7 @@ class RespondActivityTaskCanceledRequest(google.protobuf.message.Message): details: temporalio.api.common.v1.message_pb2.Payloads | None = ..., identity: builtins.str = ..., namespace: builtins.str = ..., + resource_id: builtins.str = ..., worker_version: temporalio.api.common.v1.message_pb2.WorkerVersionStamp | None = ..., deployment: temporalio.api.deployment.v1.message_pb2.Deployment | None = ..., @@ -2687,6 +2990,8 @@ class RespondActivityTaskCanceledRequest(google.protobuf.message.Message): b"identity", "namespace", b"namespace", + "resource_id", + b"resource_id", "task_token", b"task_token", "worker_version", @@ -2715,12 +3020,15 @@ class RespondActivityTaskCanceledByIdRequest(google.protobuf.message.Message): DETAILS_FIELD_NUMBER: builtins.int IDENTITY_FIELD_NUMBER: builtins.int DEPLOYMENT_OPTIONS_FIELD_NUMBER: builtins.int + RESOURCE_ID_FIELD_NUMBER: builtins.int namespace: builtins.str """Namespace of the workflow which scheduled this activity""" workflow_id: builtins.str - """Id of the workflow which scheduled this activity""" + """Id of the workflow which scheduled this activity, leave empty to target a standalone activity""" run_id: builtins.str - """Run Id of the workflow which scheduled this activity""" + """For a workflow activity - the run ID of the workflow which scheduled this activity. + For a standalone activity - the run ID of the activity. + """ activity_id: builtins.str """Id of the activity to confirm is cancelled""" @property @@ -2733,6 +3041,8 @@ class RespondActivityTaskCanceledByIdRequest(google.protobuf.message.Message): self, ) -> temporalio.api.deployment.v1.message_pb2.WorkerDeploymentOptions: """Worker deployment options that user has set in the worker.""" + resource_id: builtins.str + """Resource ID for routing. Contains "workflow:workflow_id" or "activity:activity_id" for standalone activities.""" def __init__( self, *, @@ -2744,6 +3054,7 @@ class RespondActivityTaskCanceledByIdRequest(google.protobuf.message.Message): identity: builtins.str = ..., deployment_options: temporalio.api.deployment.v1.message_pb2.WorkerDeploymentOptions | None = ..., + resource_id: builtins.str = ..., ) -> None: ... def HasField( self, @@ -2764,6 +3075,8 @@ class RespondActivityTaskCanceledByIdRequest(google.protobuf.message.Message): b"identity", "namespace", b"namespace", + "resource_id", + b"resource_id", "run_id", b"run_id", "workflow_id", @@ -2967,8 +3280,23 @@ global___SignalWorkflowExecutionRequest = SignalWorkflowExecutionRequest class SignalWorkflowExecutionResponse(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor + LINK_FIELD_NUMBER: builtins.int + @property + def link(self) -> temporalio.api.common.v1.message_pb2.Link: + """Link to be associated with the WorkflowExecutionSignaled event. + Added on the response to propagate the backlink. + Available from Temporal server 1.31 and up. + """ def __init__( self, + *, + link: temporalio.api.common.v1.message_pb2.Link | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["link", b"link"] + ) -> builtins.bool: ... + def ClearField( + self, field_name: typing_extensions.Literal["link", b"link"] ) -> None: ... global___SignalWorkflowExecutionResponse = SignalWorkflowExecutionResponse @@ -3001,6 +3329,7 @@ class SignalWithStartWorkflowExecutionRequest(google.protobuf.message.Message): LINKS_FIELD_NUMBER: builtins.int VERSIONING_OVERRIDE_FIELD_NUMBER: builtins.int PRIORITY_FIELD_NUMBER: builtins.int + TIME_SKIPPING_CONFIG_FIELD_NUMBER: builtins.int namespace: builtins.str workflow_id: builtins.str @property @@ -3063,9 +3392,9 @@ class SignalWithStartWorkflowExecutionRequest(google.protobuf.message.Message): def header(self) -> temporalio.api.common.v1.message_pb2.Header: ... @property def workflow_start_delay(self) -> google.protobuf.duration_pb2.Duration: - """Time to wait before dispatching the first workflow task. Cannot be used with `cron_schedule`. + """Time to wait before making the first workflow task available for dispatch. Cannot be used with `cron_schedule`. Note that the signal will be delivered with the first workflow task. If the workflow gets - another SignalWithStartWorkflow before the delay a workflow task will be dispatched immediately + another SignalWithStartWorkflow before the delay a workflow task will be made available for dispatch immediately and the rest of the delay period will be ignored, even if that request also had a delay. Signal via SignalWorkflowExecution will not unblock the workflow. """ @@ -3092,6 +3421,11 @@ class SignalWithStartWorkflowExecutionRequest(google.protobuf.message.Message): @property def priority(self) -> temporalio.api.common.v1.message_pb2.Priority: """Priority metadata""" + @property + def time_skipping_config( + self, + ) -> temporalio.api.common.v1.message_pb2.TimeSkippingConfig: + """Time-skipping configuration. If not set, time skipping is disabled.""" def __init__( self, *, @@ -3124,6 +3458,8 @@ class SignalWithStartWorkflowExecutionRequest(google.protobuf.message.Message): versioning_override: temporalio.api.workflow.v1.message_pb2.VersioningOverride | None = ..., priority: temporalio.api.common.v1.message_pb2.Priority | None = ..., + time_skipping_config: temporalio.api.common.v1.message_pb2.TimeSkippingConfig + | None = ..., ) -> None: ... def HasField( self, @@ -3144,6 +3480,8 @@ class SignalWithStartWorkflowExecutionRequest(google.protobuf.message.Message): b"signal_input", "task_queue", b"task_queue", + "time_skipping_config", + b"time_skipping_config", "user_metadata", b"user_metadata", "versioning_override", @@ -3193,6 +3531,8 @@ class SignalWithStartWorkflowExecutionRequest(google.protobuf.message.Message): b"signal_name", "task_queue", b"task_queue", + "time_skipping_config", + b"time_skipping_config", "user_metadata", b"user_metadata", "versioning_override", @@ -3224,21 +3564,43 @@ class SignalWithStartWorkflowExecutionResponse(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor RUN_ID_FIELD_NUMBER: builtins.int + FIRST_EXECUTION_RUN_ID_FIELD_NUMBER: builtins.int STARTED_FIELD_NUMBER: builtins.int + SIGNAL_LINK_FIELD_NUMBER: builtins.int run_id: builtins.str """The run id of the workflow that was started - or just signaled, if it was already running.""" + first_execution_run_id: builtins.str + """If the workflow was started as a result of a de-dupe, this field will contain the run id of the first execution in the chain.""" started: builtins.bool """If true, a new workflow was started.""" + @property + def signal_link(self) -> temporalio.api.common.v1.message_pb2.Link: + """Link to be associated with the WorkflowExecutionSignaled event. + Added on the response to propagate the backlink. + Available from Temporal server 1.31 and up. + """ def __init__( self, *, run_id: builtins.str = ..., + first_execution_run_id: builtins.str = ..., started: builtins.bool = ..., + signal_link: temporalio.api.common.v1.message_pb2.Link | None = ..., ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["signal_link", b"signal_link"] + ) -> builtins.bool: ... def ClearField( self, field_name: typing_extensions.Literal[ - "run_id", b"run_id", "started", b"started" + "first_execution_run_id", + b"first_execution_run_id", + "run_id", + b"run_id", + "signal_link", + b"signal_link", + "started", + b"started", ], ) -> None: ... @@ -4058,6 +4420,7 @@ class RespondQueryTaskCompletedRequest(google.protobuf.message.Message): NAMESPACE_FIELD_NUMBER: builtins.int FAILURE_FIELD_NUMBER: builtins.int CAUSE_FIELD_NUMBER: builtins.int + POLLER_GROUP_ID_FIELD_NUMBER: builtins.int task_token: builtins.bytes completed_type: temporalio.api.enums.v1.query_pb2.QueryResultType.ValueType @property @@ -4086,6 +4449,10 @@ class RespondQueryTaskCompletedRequest(google.protobuf.message.Message): """Why did the task fail? It's important to note that many of the variants in this enum cannot apply to worker responses. See the type's doc for more. """ + poller_group_id: builtins.str + """Client must forward the poller_group_id received in PollWorkflowTaskQueueResponse for proper + routing of the response. + """ def __init__( self, *, @@ -4096,6 +4463,7 @@ class RespondQueryTaskCompletedRequest(google.protobuf.message.Message): namespace: builtins.str = ..., failure: temporalio.api.failure.v1.message_pb2.Failure | None = ..., cause: temporalio.api.enums.v1.failed_cause_pb2.WorkflowTaskFailedCause.ValueType = ..., + poller_group_id: builtins.str = ..., ) -> None: ... def HasField( self, @@ -4116,6 +4484,8 @@ class RespondQueryTaskCompletedRequest(google.protobuf.message.Message): b"failure", "namespace", b"namespace", + "poller_group_id", + b"poller_group_id", "query_result", b"query_result", "task_token", @@ -4177,14 +4547,40 @@ class ShutdownWorkerRequest(google.protobuf.message.Message): IDENTITY_FIELD_NUMBER: builtins.int REASON_FIELD_NUMBER: builtins.int WORKER_HEARTBEAT_FIELD_NUMBER: builtins.int + WORKER_INSTANCE_KEY_FIELD_NUMBER: builtins.int + TASK_QUEUE_FIELD_NUMBER: builtins.int + TASK_QUEUE_TYPES_FIELD_NUMBER: builtins.int namespace: builtins.str sticky_task_queue: builtins.str + """sticky_task_queue may not always be populated. We want to ensure all workers + send a shutdown request to update worker state for heartbeating, as well + as cancel pending poll calls early, instead of waiting for timeouts. + """ identity: builtins.str reason: builtins.str @property def worker_heartbeat( self, ) -> temporalio.api.worker.v1.message_pb2.WorkerHeartbeat: ... + worker_instance_key: builtins.str + """Technically this is also sent in the WorkerHeartbeat, but + since worker heartbeating can be turned off, this needs + to be a separate, top-level field. + """ + task_queue: builtins.str + """Task queue name the worker is polling on. This allows server to cancel + all outstanding poll RPC calls from SDK. This avoids a race condition that + can lead to tasks being lost. + """ + @property + def task_queue_types( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[ + temporalio.api.enums.v1.task_queue_pb2.TaskQueueType.ValueType + ]: + """Task queue types that help server cancel outstanding poll RPC + calls from SDK. This avoids a race condition that can lead to tasks being lost. + """ def __init__( self, *, @@ -4194,6 +4590,12 @@ class ShutdownWorkerRequest(google.protobuf.message.Message): reason: builtins.str = ..., worker_heartbeat: temporalio.api.worker.v1.message_pb2.WorkerHeartbeat | None = ..., + worker_instance_key: builtins.str = ..., + task_queue: builtins.str = ..., + task_queue_types: collections.abc.Iterable[ + temporalio.api.enums.v1.task_queue_pb2.TaskQueueType.ValueType + ] + | None = ..., ) -> None: ... def HasField( self, @@ -4210,8 +4612,14 @@ class ShutdownWorkerRequest(google.protobuf.message.Message): b"reason", "sticky_task_queue", b"sticky_task_queue", + "task_queue", + b"task_queue", + "task_queue_types", + b"task_queue_types", "worker_heartbeat", b"worker_heartbeat", + "worker_instance_key", + b"worker_instance_key", ], ) -> None: ... @@ -4279,26 +4687,41 @@ class QueryWorkflowResponse(google.protobuf.message.Message): QUERY_RESULT_FIELD_NUMBER: builtins.int QUERY_REJECTED_FIELD_NUMBER: builtins.int + LINK_FIELD_NUMBER: builtins.int @property def query_result(self) -> temporalio.api.common.v1.message_pb2.Payloads: ... @property def query_rejected(self) -> temporalio.api.query.v1.message_pb2.QueryRejected: ... + @property + def link(self) -> temporalio.api.common.v1.message_pb2.Link: + """Holds the link to the Workflow execution that processed the Query.""" def __init__( self, *, query_result: temporalio.api.common.v1.message_pb2.Payloads | None = ..., query_rejected: temporalio.api.query.v1.message_pb2.QueryRejected | None = ..., + link: temporalio.api.common.v1.message_pb2.Link | None = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal[ - "query_rejected", b"query_rejected", "query_result", b"query_result" + "link", + b"link", + "query_rejected", + b"query_rejected", + "query_result", + b"query_result", ], ) -> builtins.bool: ... def ClearField( self, field_name: typing_extensions.Literal[ - "query_rejected", b"query_rejected", "query_result", b"query_result" + "link", + b"link", + "query_rejected", + b"query_rejected", + "query_result", + b"query_result", ], ) -> None: ... @@ -4820,6 +5243,8 @@ class GetClusterInfoResponse(google.protobuf.message.Message): HISTORY_SHARD_COUNT_FIELD_NUMBER: builtins.int PERSISTENCE_STORE_FIELD_NUMBER: builtins.int VISIBILITY_STORE_FIELD_NUMBER: builtins.int + INITIAL_FAILOVER_VERSION_FIELD_NUMBER: builtins.int + FAILOVER_VERSION_INCREMENT_FIELD_NUMBER: builtins.int @property def supported_clients( self, @@ -4835,6 +5260,8 @@ class GetClusterInfoResponse(google.protobuf.message.Message): history_shard_count: builtins.int persistence_store: builtins.str visibility_store: builtins.str + initial_failover_version: builtins.int + failover_version_increment: builtins.int def __init__( self, *, @@ -4847,6 +5274,8 @@ class GetClusterInfoResponse(google.protobuf.message.Message): history_shard_count: builtins.int = ..., persistence_store: builtins.str = ..., visibility_store: builtins.str = ..., + initial_failover_version: builtins.int = ..., + failover_version_increment: builtins.int = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal["version_info", b"version_info"] @@ -4858,8 +5287,12 @@ class GetClusterInfoResponse(google.protobuf.message.Message): b"cluster_id", "cluster_name", b"cluster_name", + "failover_version_increment", + b"failover_version_increment", "history_shard_count", b"history_shard_count", + "initial_failover_version", + b"initial_failover_version", "persistence_store", b"persistence_store", "server_version", @@ -4903,6 +5336,8 @@ class GetSystemInfoResponse(google.protobuf.message.Message): SDK_METADATA_FIELD_NUMBER: builtins.int COUNT_GROUP_BY_EXECUTION_STATUS_FIELD_NUMBER: builtins.int NEXUS_FIELD_NUMBER: builtins.int + SERVER_SCALED_DEPLOYMENTS_FIELD_NUMBER: builtins.int + SERVER_SCALED_PROVIDER_CLOUD_RUN_FIELD_NUMBER: builtins.int signal_and_query_header: builtins.bool """True if signal and query headers are supported.""" internal_error_differentiation: builtins.bool @@ -4939,6 +5374,16 @@ class GetSystemInfoResponse(google.protobuf.message.Message): """True if the server supports Nexus operations. This flag is dependent both on server version and for Nexus to be enabled via server configuration. """ + server_scaled_deployments: builtins.bool + """True if the server supports server-scaled deployments. + This flag is dependent both on server version and for server-scaled deployments + to be enabled via server configuration. + """ + server_scaled_provider_cloud_run: builtins.bool + """True if the server supports the Cloud Run compute provider for + server-scaled deployments. Dependent on server version and the + provider being enabled via server configuration. + """ def __init__( self, *, @@ -4953,6 +5398,8 @@ class GetSystemInfoResponse(google.protobuf.message.Message): sdk_metadata: builtins.bool = ..., count_group_by_execution_status: builtins.bool = ..., nexus: builtins.bool = ..., + server_scaled_deployments: builtins.bool = ..., + server_scaled_provider_cloud_run: builtins.bool = ..., ) -> None: ... def ClearField( self, @@ -4973,6 +5420,10 @@ class GetSystemInfoResponse(google.protobuf.message.Message): b"nexus", "sdk_metadata", b"sdk_metadata", + "server_scaled_deployments", + b"server_scaled_deployments", + "server_scaled_provider_cloud_run", + b"server_scaled_provider_cloud_run", "signal_and_query_header", b"signal_and_query_header", "supports_schedules", @@ -5285,6 +5736,7 @@ class UpdateScheduleRequest(google.protobuf.message.Message): IDENTITY_FIELD_NUMBER: builtins.int REQUEST_ID_FIELD_NUMBER: builtins.int SEARCH_ATTRIBUTES_FIELD_NUMBER: builtins.int + MEMO_FIELD_NUMBER: builtins.int namespace: builtins.str """The namespace of the schedule to update.""" schedule_id: builtins.str @@ -5314,6 +5766,12 @@ class UpdateScheduleRequest(google.protobuf.message.Message): Note: you cannot only update the search attributes with `UpdateScheduleRequest`, you must also set the `schedule` field; otherwise, it will unset the schedule. """ + @property + def memo(self) -> temporalio.api.common.v1.message_pb2.Memo: + """Schedule memo to replace. If set, replaces the entire memo. + Do not set this field if you do not want to update the memo. + A non-null empty object will clear the memo. + """ def __init__( self, *, @@ -5325,11 +5783,17 @@ class UpdateScheduleRequest(google.protobuf.message.Message): request_id: builtins.str = ..., search_attributes: temporalio.api.common.v1.message_pb2.SearchAttributes | None = ..., + memo: temporalio.api.common.v1.message_pb2.Memo | None = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal[ - "schedule", b"schedule", "search_attributes", b"search_attributes" + "memo", + b"memo", + "schedule", + b"schedule", + "search_attributes", + b"search_attributes", ], ) -> builtins.bool: ... def ClearField( @@ -5339,6 +5803,8 @@ class UpdateScheduleRequest(google.protobuf.message.Message): b"conflict_token", "identity", b"identity", + "memo", + b"memo", "namespace", b"namespace", "request_id", @@ -5599,6 +6065,94 @@ class ListSchedulesResponse(google.protobuf.message.Message): global___ListSchedulesResponse = ListSchedulesResponse +class CountSchedulesRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + QUERY_FIELD_NUMBER: builtins.int + namespace: builtins.str + query: builtins.str + """Visibility query, see https://docs.temporal.io/list-filter for the syntax.""" + def __init__( + self, + *, + namespace: builtins.str = ..., + query: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "namespace", b"namespace", "query", b"query" + ], + ) -> None: ... + +global___CountSchedulesRequest = CountSchedulesRequest + +class CountSchedulesResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class AggregationGroup(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + GROUP_VALUES_FIELD_NUMBER: builtins.int + COUNT_FIELD_NUMBER: builtins.int + @property + def group_values( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.common.v1.message_pb2.Payload + ]: ... + count: builtins.int + def __init__( + self, + *, + group_values: collections.abc.Iterable[ + temporalio.api.common.v1.message_pb2.Payload + ] + | None = ..., + count: builtins.int = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "count", b"count", "group_values", b"group_values" + ], + ) -> None: ... + + COUNT_FIELD_NUMBER: builtins.int + GROUPS_FIELD_NUMBER: builtins.int + count: builtins.int + """If `query` is not grouping by any field, the count is an approximate number + of schedules that match the query. + If `query` is grouping by a field, the count is simply the sum of the counts + of the groups returned in the response. This number can be smaller than the + total number of schedules matching the query. + """ + @property + def groups( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + global___CountSchedulesResponse.AggregationGroup + ]: + """Contains the groups if the request is grouping by a field. + The list might not be complete, and the counts of each group is approximate. + """ + def __init__( + self, + *, + count: builtins.int = ..., + groups: collections.abc.Iterable[ + global___CountSchedulesResponse.AggregationGroup + ] + | None = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal["count", b"count", "groups", b"groups"], + ) -> None: ... + +global___CountSchedulesResponse = CountSchedulesResponse + class UpdateWorkerBuildIdCompatibilityRequest(google.protobuf.message.Message): """[cleanup-wv-pre-release]""" @@ -6532,6 +7086,7 @@ class UpdateWorkflowExecutionResponse(google.protobuf.message.Message): UPDATE_REF_FIELD_NUMBER: builtins.int OUTCOME_FIELD_NUMBER: builtins.int STAGE_FIELD_NUMBER: builtins.int + LINK_FIELD_NUMBER: builtins.int @property def update_ref(self) -> temporalio.api.update.v1.message_pb2.UpdateRef: """Enough information for subsequent poll calls if needed. Never null.""" @@ -6553,23 +7108,34 @@ class UpdateWorkflowExecutionResponse(google.protobuf.message.Message): request WaitPolicy, and before the context deadline expired; clients may may then retry the call as needed. """ + @property + def link(self) -> temporalio.api.common.v1.message_pb2.Link: + """Link to the update event. May be null if the update has not yet been accepted.""" def __init__( self, *, update_ref: temporalio.api.update.v1.message_pb2.UpdateRef | None = ..., outcome: temporalio.api.update.v1.message_pb2.Outcome | None = ..., stage: temporalio.api.enums.v1.update_pb2.UpdateWorkflowExecutionLifecycleStage.ValueType = ..., + link: temporalio.api.common.v1.message_pb2.Link | None = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal[ - "outcome", b"outcome", "update_ref", b"update_ref" + "link", b"link", "outcome", b"outcome", "update_ref", b"update_ref" ], ) -> builtins.bool: ... def ClearField( self, field_name: typing_extensions.Literal[ - "outcome", b"outcome", "stage", b"stage", "update_ref", b"update_ref" + "link", + b"link", + "outcome", + b"outcome", + "stage", + b"stage", + "update_ref", + b"update_ref", ], ) -> None: ... @@ -6583,6 +7149,7 @@ class StartBatchOperationRequest(google.protobuf.message.Message): JOB_ID_FIELD_NUMBER: builtins.int REASON_FIELD_NUMBER: builtins.int EXECUTIONS_FIELD_NUMBER: builtins.int + TARGET_EXECUTIONS_FIELD_NUMBER: builtins.int MAX_OPERATIONS_PER_SECOND_FIELD_NUMBER: builtins.int TERMINATION_OPERATION_FIELD_NUMBER: builtins.int SIGNAL_OPERATION_FIELD_NUMBER: builtins.int @@ -6593,6 +7160,9 @@ class StartBatchOperationRequest(google.protobuf.message.Message): UNPAUSE_ACTIVITIES_OPERATION_FIELD_NUMBER: builtins.int RESET_ACTIVITIES_OPERATION_FIELD_NUMBER: builtins.int UPDATE_ACTIVITY_OPTIONS_OPERATION_FIELD_NUMBER: builtins.int + CANCEL_ACTIVITIES_OPERATION_FIELD_NUMBER: builtins.int + TERMINATE_ACTIVITIES_OPERATION_FIELD_NUMBER: builtins.int + DELETE_ACTIVITIES_OPERATION_FIELD_NUMBER: builtins.int namespace: builtins.str """Namespace that contains the batch operation""" visibility_query: builtins.str @@ -6611,10 +7181,20 @@ class StartBatchOperationRequest(google.protobuf.message.Message): ]: """Executions to apply the batch operation This field and `visibility_query` are mutually exclusive + DEPRECATED: Use `target_executions` instead. """ - max_operations_per_second: builtins.float - """Limit for the number of operations processed per second within this batch. - Its purpose is to reduce the stress on the system caused by batch operations, which helps to prevent system + @property + def target_executions( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.common.v1.message_pb2.Execution + ]: + """Target executions to apply the batch operation. This field and `visibility_query` + are mutually exclusive. + """ + max_operations_per_second: builtins.float + """Limit for the number of operations processed per second within this batch. + Its purpose is to reduce the stress on the system caused by batch operations, which helps to prevent system overload and minimize potential delays in executing ongoing tasks for user workers. Note that when no explicit limit is provided, the server will operate according to its limit defined by the dynamic configuration key `worker.batcherRPS`. This also applies if the value in this field exceeds the @@ -6658,6 +7238,18 @@ class StartBatchOperationRequest(google.protobuf.message.Message): def update_activity_options_operation( self, ) -> temporalio.api.batch.v1.message_pb2.BatchOperationUpdateActivityOptions: ... + @property + def cancel_activities_operation( + self, + ) -> temporalio.api.batch.v1.message_pb2.BatchOperationCancelActivities: ... + @property + def terminate_activities_operation( + self, + ) -> temporalio.api.batch.v1.message_pb2.BatchOperationTerminateActivities: ... + @property + def delete_activities_operation( + self, + ) -> temporalio.api.batch.v1.message_pb2.BatchOperationDeleteActivities: ... def __init__( self, *, @@ -6669,6 +7261,10 @@ class StartBatchOperationRequest(google.protobuf.message.Message): temporalio.api.common.v1.message_pb2.WorkflowExecution ] | None = ..., + target_executions: collections.abc.Iterable[ + temporalio.api.common.v1.message_pb2.Execution + ] + | None = ..., max_operations_per_second: builtins.float = ..., termination_operation: temporalio.api.batch.v1.message_pb2.BatchOperationTermination | None = ..., @@ -6688,12 +7284,22 @@ class StartBatchOperationRequest(google.protobuf.message.Message): | None = ..., update_activity_options_operation: temporalio.api.batch.v1.message_pb2.BatchOperationUpdateActivityOptions | None = ..., + cancel_activities_operation: temporalio.api.batch.v1.message_pb2.BatchOperationCancelActivities + | None = ..., + terminate_activities_operation: temporalio.api.batch.v1.message_pb2.BatchOperationTerminateActivities + | None = ..., + delete_activities_operation: temporalio.api.batch.v1.message_pb2.BatchOperationDeleteActivities + | None = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal[ + "cancel_activities_operation", + b"cancel_activities_operation", "cancellation_operation", b"cancellation_operation", + "delete_activities_operation", + b"delete_activities_operation", "deletion_operation", b"deletion_operation", "operation", @@ -6704,6 +7310,8 @@ class StartBatchOperationRequest(google.protobuf.message.Message): b"reset_operation", "signal_operation", b"signal_operation", + "terminate_activities_operation", + b"terminate_activities_operation", "termination_operation", b"termination_operation", "unpause_activities_operation", @@ -6717,8 +7325,12 @@ class StartBatchOperationRequest(google.protobuf.message.Message): def ClearField( self, field_name: typing_extensions.Literal[ + "cancel_activities_operation", + b"cancel_activities_operation", "cancellation_operation", b"cancellation_operation", + "delete_activities_operation", + b"delete_activities_operation", "deletion_operation", b"deletion_operation", "executions", @@ -6739,6 +7351,10 @@ class StartBatchOperationRequest(google.protobuf.message.Message): b"reset_operation", "signal_operation", b"signal_operation", + "target_executions", + b"target_executions", + "terminate_activities_operation", + b"terminate_activities_operation", "termination_operation", b"termination_operation", "unpause_activities_operation", @@ -6764,6 +7380,9 @@ class StartBatchOperationRequest(google.protobuf.message.Message): "unpause_activities_operation", "reset_activities_operation", "update_activity_options_operation", + "cancel_activities_operation", + "terminate_activities_operation", + "delete_activities_operation", ] | None ): ... @@ -6864,6 +7483,8 @@ class DescribeBatchOperationResponse(google.protobuf.message.Message): FAILURE_OPERATION_COUNT_FIELD_NUMBER: builtins.int IDENTITY_FIELD_NUMBER: builtins.int REASON_FIELD_NUMBER: builtins.int + QUERY_FIELD_NUMBER: builtins.int + EXECUTIONS_FIELD_NUMBER: builtins.int operation_type: ( temporalio.api.enums.v1.batch_operation_pb2.BatchOperationType.ValueType ) @@ -6888,6 +7509,15 @@ class DescribeBatchOperationResponse(google.protobuf.message.Message): """Identity indicates the operator identity""" reason: builtins.str """Reason indicates the reason to stop a operation""" + query: builtins.str + """Query is the visibility query that defines the group of workflow to apply the batch operation""" + @property + def executions( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.common.v1.message_pb2.Execution + ]: + """Executions is the list of workflow OR standalone activity executions to apply the batch operation""" def __init__( self, *, @@ -6901,6 +7531,11 @@ class DescribeBatchOperationResponse(google.protobuf.message.Message): failure_operation_count: builtins.int = ..., identity: builtins.str = ..., reason: builtins.str = ..., + query: builtins.str = ..., + executions: collections.abc.Iterable[ + temporalio.api.common.v1.message_pb2.Execution + ] + | None = ..., ) -> None: ... def HasField( self, @@ -6915,6 +7550,8 @@ class DescribeBatchOperationResponse(google.protobuf.message.Message): b"close_time", "complete_operation_count", b"complete_operation_count", + "executions", + b"executions", "failure_operation_count", b"failure_operation_count", "identity", @@ -6923,6 +7560,8 @@ class DescribeBatchOperationResponse(google.protobuf.message.Message): b"job_id", "operation_type", b"operation_type", + "query", + b"query", "reason", b"reason", "start_time", @@ -7106,16 +7745,28 @@ class PollNexusTaskQueueRequest(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor NAMESPACE_FIELD_NUMBER: builtins.int - IDENTITY_FIELD_NUMBER: builtins.int TASK_QUEUE_FIELD_NUMBER: builtins.int + POLLER_GROUP_ID_FIELD_NUMBER: builtins.int + IDENTITY_FIELD_NUMBER: builtins.int + WORKER_INSTANCE_KEY_FIELD_NUMBER: builtins.int WORKER_VERSION_CAPABILITIES_FIELD_NUMBER: builtins.int DEPLOYMENT_OPTIONS_FIELD_NUMBER: builtins.int WORKER_HEARTBEAT_FIELD_NUMBER: builtins.int namespace: builtins.str - identity: builtins.str - """The identity of the client who initiated this request.""" @property def task_queue(self) -> temporalio.api.taskqueue.v1.message_pb2.TaskQueue: ... + poller_group_id: builtins.str + """Unless this is the first poll, the client must pass one of the poller group IDs received in + `poller_group_infos` of the last the PollNexusTaskQueueResponse according to the + instructions. If not set, the poll is routed randomly which can cause it to be blocked + without receiving a task while the queue actually has tasks in another server location. + """ + identity: builtins.str + """The identity of the client who initiated this request.""" + worker_instance_key: builtins.str + """A unique key for this worker instance, used for tracking worker lifecycle. + This is guaranteed to be unique, whereas identity is not guaranteed to be unique. + """ @property def worker_version_capabilities( self, @@ -7140,8 +7791,10 @@ class PollNexusTaskQueueRequest(google.protobuf.message.Message): self, *, namespace: builtins.str = ..., - identity: builtins.str = ..., task_queue: temporalio.api.taskqueue.v1.message_pb2.TaskQueue | None = ..., + poller_group_id: builtins.str = ..., + identity: builtins.str = ..., + worker_instance_key: builtins.str = ..., worker_version_capabilities: temporalio.api.common.v1.message_pb2.WorkerVersionCapabilities | None = ..., deployment_options: temporalio.api.deployment.v1.message_pb2.WorkerDeploymentOptions @@ -7171,10 +7824,14 @@ class PollNexusTaskQueueRequest(google.protobuf.message.Message): b"identity", "namespace", b"namespace", + "poller_group_id", + b"poller_group_id", "task_queue", b"task_queue", "worker_heartbeat", b"worker_heartbeat", + "worker_instance_key", + b"worker_instance_key", "worker_version_capabilities", b"worker_version_capabilities", ], @@ -7188,6 +7845,9 @@ class PollNexusTaskQueueResponse(google.protobuf.message.Message): TASK_TOKEN_FIELD_NUMBER: builtins.int REQUEST_FIELD_NUMBER: builtins.int POLLER_SCALING_DECISION_FIELD_NUMBER: builtins.int + POLLER_GROUP_ID_FIELD_NUMBER: builtins.int + POLLER_GROUP_INFOS_FIELD_NUMBER: builtins.int + POLLER_GROUPS_INFO_FIELD_NUMBER: builtins.int task_token: builtins.bytes """An opaque unique identifier for this task for correlating a completion request the embedded request.""" @property @@ -7198,6 +7858,37 @@ class PollNexusTaskQueueResponse(google.protobuf.message.Message): self, ) -> temporalio.api.taskqueue.v1.message_pb2.PollerScalingDecision: """Server-advised information the SDK may use to adjust its poller count.""" + poller_group_id: builtins.str + """This poller group ID identifies the owner of the nexus task awaiting for synchronous + response. + Corresponding `RespondNexusTaskCompleted` and `RespondNexusTaskFailed` calls should pass this + value for proper response routing. + """ + @property + def poller_group_infos( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.taskqueue.v1.message_pb2.PollerGroupInfo + ]: + """The weighted list of poller groups IDs that client should use for future polls to this task + queue. Client is expected to: + 1. Maintain minimum number of pollers no less than the number of groups. + 2. Try to assign the next poll to a group without any pending polls, + 3. If every group has some pending polls, assign the next poll to a group randomly + according to the weights. + """ + @property + def poller_groups_info( + self, + ) -> temporalio.api.taskqueue.v1.message_pb2.PollerGroupsInfo: + """The weighted, versioned list of poller groups IDs that client should use for future polls to + this task queue. Client should ignore this if it has already applied a snapshot with a + version greater than or equal to `poller_groups_info.version`. Client is expected to: + 1. Maintain minimum number of pollers no less than the number of groups. + 2. Try to assign the next poll to a group without any pending polls, + 3. If every group has some pending polls, assign the next poll to a group randomly + according to the weights. + """ def __init__( self, *, @@ -7205,16 +7896,34 @@ class PollNexusTaskQueueResponse(google.protobuf.message.Message): request: temporalio.api.nexus.v1.message_pb2.Request | None = ..., poller_scaling_decision: temporalio.api.taskqueue.v1.message_pb2.PollerScalingDecision | None = ..., + poller_group_id: builtins.str = ..., + poller_group_infos: collections.abc.Iterable[ + temporalio.api.taskqueue.v1.message_pb2.PollerGroupInfo + ] + | None = ..., + poller_groups_info: temporalio.api.taskqueue.v1.message_pb2.PollerGroupsInfo + | None = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal[ - "poller_scaling_decision", b"poller_scaling_decision", "request", b"request" + "poller_groups_info", + b"poller_groups_info", + "poller_scaling_decision", + b"poller_scaling_decision", + "request", + b"request", ], ) -> builtins.bool: ... def ClearField( self, field_name: typing_extensions.Literal[ + "poller_group_id", + b"poller_group_id", + "poller_group_infos", + b"poller_group_infos", + "poller_groups_info", + b"poller_groups_info", "poller_scaling_decision", b"poller_scaling_decision", "request", @@ -7233,6 +7942,7 @@ class RespondNexusTaskCompletedRequest(google.protobuf.message.Message): IDENTITY_FIELD_NUMBER: builtins.int TASK_TOKEN_FIELD_NUMBER: builtins.int RESPONSE_FIELD_NUMBER: builtins.int + POLLER_GROUP_ID_FIELD_NUMBER: builtins.int namespace: builtins.str identity: builtins.str """The identity of the client who initiated this request.""" @@ -7241,6 +7951,10 @@ class RespondNexusTaskCompletedRequest(google.protobuf.message.Message): @property def response(self) -> temporalio.api.nexus.v1.message_pb2.Response: """Embedded response to be translated into a frontend response.""" + poller_group_id: builtins.str + """Client must forward the poller_group_id received in PollNexusTaskQueueResponse for proper + routing of the response. + """ def __init__( self, *, @@ -7248,6 +7962,7 @@ class RespondNexusTaskCompletedRequest(google.protobuf.message.Message): identity: builtins.str = ..., task_token: builtins.bytes = ..., response: temporalio.api.nexus.v1.message_pb2.Response | None = ..., + poller_group_id: builtins.str = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal["response", b"response"] @@ -7259,6 +7974,8 @@ class RespondNexusTaskCompletedRequest(google.protobuf.message.Message): b"identity", "namespace", b"namespace", + "poller_group_id", + b"poller_group_id", "response", b"response", "task_token", @@ -7284,6 +8001,8 @@ class RespondNexusTaskFailedRequest(google.protobuf.message.Message): IDENTITY_FIELD_NUMBER: builtins.int TASK_TOKEN_FIELD_NUMBER: builtins.int ERROR_FIELD_NUMBER: builtins.int + FAILURE_FIELD_NUMBER: builtins.int + POLLER_GROUP_ID_FIELD_NUMBER: builtins.int namespace: builtins.str identity: builtins.str """The identity of the client who initiated this request.""" @@ -7291,7 +8010,14 @@ class RespondNexusTaskFailedRequest(google.protobuf.message.Message): """A unique identifier for this task.""" @property def error(self) -> temporalio.api.nexus.v1.message_pb2.HandlerError: - """The error the handler failed with.""" + """Deprecated. Use the failure field instead.""" + @property + def failure(self) -> temporalio.api.failure.v1.message_pb2.Failure: + """The error the handler failed with. Must contain a NexusHandlerFailureInfo object.""" + poller_group_id: builtins.str + """Client must forward the poller_group_id received in PollNexusTaskQueueResponse for proper + routing of the response. + """ def __init__( self, *, @@ -7299,19 +8025,26 @@ class RespondNexusTaskFailedRequest(google.protobuf.message.Message): identity: builtins.str = ..., task_token: builtins.bytes = ..., error: temporalio.api.nexus.v1.message_pb2.HandlerError | None = ..., + failure: temporalio.api.failure.v1.message_pb2.Failure | None = ..., + poller_group_id: builtins.str = ..., ) -> None: ... def HasField( - self, field_name: typing_extensions.Literal["error", b"error"] + self, + field_name: typing_extensions.Literal["error", b"error", "failure", b"failure"], ) -> builtins.bool: ... def ClearField( self, field_name: typing_extensions.Literal[ "error", b"error", + "failure", + b"failure", "identity", b"identity", "namespace", b"namespace", + "poller_group_id", + b"poller_group_id", "task_token", b"task_token", ], @@ -7383,6 +8116,7 @@ class ExecuteMultiOperationRequest(google.protobuf.message.Message): NAMESPACE_FIELD_NUMBER: builtins.int OPERATIONS_FIELD_NUMBER: builtins.int + RESOURCE_ID_FIELD_NUMBER: builtins.int namespace: builtins.str @property def operations( @@ -7399,6 +8133,8 @@ class ExecuteMultiOperationRequest(google.protobuf.message.Message): Note that additional operation-specific restrictions have to be considered. """ + resource_id: builtins.str + """Resource ID for routing. Should match operations[0].start_workflow.workflow_id""" def __init__( self, *, @@ -7407,11 +8143,17 @@ class ExecuteMultiOperationRequest(google.protobuf.message.Message): global___ExecuteMultiOperationRequest.Operation ] | None = ..., + resource_id: builtins.str = ..., ) -> None: ... def ClearField( self, field_name: typing_extensions.Literal[ - "namespace", b"namespace", "operations", b"operations" + "namespace", + b"namespace", + "operations", + b"operations", + "resource_id", + b"resource_id", ], ) -> None: ... @@ -7491,7 +8233,9 @@ class ExecuteMultiOperationResponse(google.protobuf.message.Message): global___ExecuteMultiOperationResponse = ExecuteMultiOperationResponse class UpdateActivityOptionsRequest(google.protobuf.message.Message): - """NOTE: keep in sync with temporalio.api.batch.v1.BatchOperationUpdateActivityOptions""" + """NOTE: keep in sync with temporalio.api.batch.v1.BatchOperationUpdateActivityOptions + Deprecated. Use `UpdateActivityExecutionOptionsRequest`. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -7528,7 +8272,7 @@ class UpdateActivityOptionsRequest(google.protobuf.message.Message): restore_original: builtins.bool """If set, the activity options will be restored to the default. Default options are then options activity was created with. - They are part of the first SCHEDULE event. + They are part of the first schedule event. This flag cannot be combined with any other option; if you supply restore_original together with other options, the request will be rejected. """ @@ -7596,7 +8340,102 @@ class UpdateActivityOptionsRequest(google.protobuf.message.Message): global___UpdateActivityOptionsRequest = UpdateActivityOptionsRequest +class UpdateActivityExecutionOptionsRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + WORKFLOW_ID_FIELD_NUMBER: builtins.int + ACTIVITY_ID_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + IDENTITY_FIELD_NUMBER: builtins.int + ACTIVITY_OPTIONS_FIELD_NUMBER: builtins.int + UPDATE_MASK_FIELD_NUMBER: builtins.int + RESTORE_ORIGINAL_FIELD_NUMBER: builtins.int + RESOURCE_ID_FIELD_NUMBER: builtins.int + REQUEST_ID_FIELD_NUMBER: builtins.int + namespace: builtins.str + """Namespace of the workflow which scheduled this activity""" + workflow_id: builtins.str + """If provided, targets a workflow activity for the given workflow ID. + If empty, targets a standalone activity. + """ + activity_id: builtins.str + """The ID of the activity to target.""" + run_id: builtins.str + """Run ID of the workflow or standalone activity. If empty, targets the latest run.""" + identity: builtins.str + """The identity of the client who initiated this request""" + @property + def activity_options( + self, + ) -> temporalio.api.activity.v1.message_pb2.ActivityOptions: + """Activity options. Partial updates are accepted and controlled by update_mask""" + @property + def update_mask(self) -> google.protobuf.field_mask_pb2.FieldMask: + """Controls which fields from `activity_options` will be applied""" + restore_original: builtins.bool + """If set, the activity options will be restored to the default. + Default options are then options activity was created with. + They are part of the first schedule event. + This flag cannot be combined with any other option; if you supply + restore_original together with other options, the request will be rejected. + """ + resource_id: builtins.str + """Resource ID for routing. Contains "workflow:{workflow_id}" for workflow activities or "activity:{activity_id}" for standalone activities.""" + request_id: builtins.str + """Used to de-dupe update requests.""" + def __init__( + self, + *, + namespace: builtins.str = ..., + workflow_id: builtins.str = ..., + activity_id: builtins.str = ..., + run_id: builtins.str = ..., + identity: builtins.str = ..., + activity_options: temporalio.api.activity.v1.message_pb2.ActivityOptions + | None = ..., + update_mask: google.protobuf.field_mask_pb2.FieldMask | None = ..., + restore_original: builtins.bool = ..., + resource_id: builtins.str = ..., + request_id: builtins.str = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "activity_options", b"activity_options", "update_mask", b"update_mask" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "activity_id", + b"activity_id", + "activity_options", + b"activity_options", + "identity", + b"identity", + "namespace", + b"namespace", + "request_id", + b"request_id", + "resource_id", + b"resource_id", + "restore_original", + b"restore_original", + "run_id", + b"run_id", + "update_mask", + b"update_mask", + "workflow_id", + b"workflow_id", + ], + ) -> None: ... + +global___UpdateActivityExecutionOptionsRequest = UpdateActivityExecutionOptionsRequest + class UpdateActivityOptionsResponse(google.protobuf.message.Message): + """Deprecated. Use `UpdateActivityExecutionOptionsResponse`.""" + DESCRIPTOR: google.protobuf.descriptor.Descriptor ACTIVITY_OPTIONS_FIELD_NUMBER: builtins.int @@ -7622,7 +8461,35 @@ class UpdateActivityOptionsResponse(google.protobuf.message.Message): global___UpdateActivityOptionsResponse = UpdateActivityOptionsResponse +class UpdateActivityExecutionOptionsResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ACTIVITY_OPTIONS_FIELD_NUMBER: builtins.int + @property + def activity_options( + self, + ) -> temporalio.api.activity.v1.message_pb2.ActivityOptions: + """Activity options after an update""" + def __init__( + self, + *, + activity_options: temporalio.api.activity.v1.message_pb2.ActivityOptions + | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["activity_options", b"activity_options"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["activity_options", b"activity_options"], + ) -> None: ... + +global___UpdateActivityExecutionOptionsResponse = UpdateActivityExecutionOptionsResponse + class PauseActivityRequest(google.protobuf.message.Message): + """Deprecated. Use `PauseActivityExecutionRequest`.""" + DESCRIPTOR: google.protobuf.descriptor.Descriptor NAMESPACE_FIELD_NUMBER: builtins.int @@ -7631,6 +8498,7 @@ class PauseActivityRequest(google.protobuf.message.Message): ID_FIELD_NUMBER: builtins.int TYPE_FIELD_NUMBER: builtins.int REASON_FIELD_NUMBER: builtins.int + REQUEST_ID_FIELD_NUMBER: builtins.int namespace: builtins.str """Namespace of the workflow which scheduled this activity.""" @property @@ -7641,9 +8509,13 @@ class PauseActivityRequest(google.protobuf.message.Message): id: builtins.str """Only the activity with this ID will be paused.""" type: builtins.str - """Pause all running activities of this type.""" + """Pause all running activities of this type. + Note: Experimental - the behavior of pause by activity type might change in a future release. + """ reason: builtins.str """Reason to pause the activity.""" + request_id: builtins.str + """Used to de-dupe pause requests.""" def __init__( self, *, @@ -7653,6 +8525,7 @@ class PauseActivityRequest(google.protobuf.message.Message): id: builtins.str = ..., type: builtins.str = ..., reason: builtins.str = ..., + request_id: builtins.str = ..., ) -> None: ... def HasField( self, @@ -7682,6 +8555,8 @@ class PauseActivityRequest(google.protobuf.message.Message): b"namespace", "reason", b"reason", + "request_id", + b"request_id", "type", b"type", ], @@ -7692,7 +8567,74 @@ class PauseActivityRequest(google.protobuf.message.Message): global___PauseActivityRequest = PauseActivityRequest +class PauseActivityExecutionRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + WORKFLOW_ID_FIELD_NUMBER: builtins.int + ACTIVITY_ID_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + IDENTITY_FIELD_NUMBER: builtins.int + REASON_FIELD_NUMBER: builtins.int + RESOURCE_ID_FIELD_NUMBER: builtins.int + REQUEST_ID_FIELD_NUMBER: builtins.int + namespace: builtins.str + """Namespace of the workflow which scheduled this activity.""" + workflow_id: builtins.str + """If provided, pause a workflow activity (or activities) for the given workflow ID. + If empty, targets a standalone activity. + """ + activity_id: builtins.str + """The ID of the activity to target.""" + run_id: builtins.str + """Run ID of the workflow or standalone activity. If empty, targets the latest run.""" + identity: builtins.str + """The identity of the client who initiated this request.""" + reason: builtins.str + """Reason to pause the activity.""" + resource_id: builtins.str + """Resource ID for routing. Contains "workflow:{workflow_id}" for workflow activities or "activity:{activity_id}" for standalone activities.""" + request_id: builtins.str + """Used to de-dupe pause requests.""" + def __init__( + self, + *, + namespace: builtins.str = ..., + workflow_id: builtins.str = ..., + activity_id: builtins.str = ..., + run_id: builtins.str = ..., + identity: builtins.str = ..., + reason: builtins.str = ..., + resource_id: builtins.str = ..., + request_id: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "activity_id", + b"activity_id", + "identity", + b"identity", + "namespace", + b"namespace", + "reason", + b"reason", + "request_id", + b"request_id", + "resource_id", + b"resource_id", + "run_id", + b"run_id", + "workflow_id", + b"workflow_id", + ], + ) -> None: ... + +global___PauseActivityExecutionRequest = PauseActivityExecutionRequest + class PauseActivityResponse(google.protobuf.message.Message): + """Deprecated. Use `PauseActivityExecutionResponse`.""" + DESCRIPTOR: google.protobuf.descriptor.Descriptor def __init__( @@ -7701,7 +8643,18 @@ class PauseActivityResponse(google.protobuf.message.Message): global___PauseActivityResponse = PauseActivityResponse +class PauseActivityExecutionResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___PauseActivityExecutionResponse = PauseActivityExecutionResponse + class UnpauseActivityRequest(google.protobuf.message.Message): + """Deprecated. Use `UnpauseActivityExecutionRequest`.""" + DESCRIPTOR: google.protobuf.descriptor.Descriptor NAMESPACE_FIELD_NUMBER: builtins.int @@ -7794,22 +8747,110 @@ class UnpauseActivityRequest(google.protobuf.message.Message): global___UnpauseActivityRequest = UnpauseActivityRequest -class UnpauseActivityResponse(google.protobuf.message.Message): - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - def __init__( - self, - ) -> None: ... - -global___UnpauseActivityResponse = UnpauseActivityResponse - -class ResetActivityRequest(google.protobuf.message.Message): - """NOTE: keep in sync with temporalio.api.batch.v1.BatchOperationResetActivities""" - +class UnpauseActivityExecutionRequest(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor NAMESPACE_FIELD_NUMBER: builtins.int - EXECUTION_FIELD_NUMBER: builtins.int + WORKFLOW_ID_FIELD_NUMBER: builtins.int + ACTIVITY_ID_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + IDENTITY_FIELD_NUMBER: builtins.int + REASON_FIELD_NUMBER: builtins.int + JITTER_FIELD_NUMBER: builtins.int + RESOURCE_ID_FIELD_NUMBER: builtins.int + REQUEST_ID_FIELD_NUMBER: builtins.int + namespace: builtins.str + """Namespace of the workflow which scheduled this activity.""" + workflow_id: builtins.str + """If provided, targets a workflow activity for the given workflow ID. + If empty, targets a standalone activity. + """ + activity_id: builtins.str + """The ID of the activity to target.""" + run_id: builtins.str + """Run ID of the workflow or standalone activity. If empty, targets the latest run.""" + identity: builtins.str + """The identity of the client who initiated this request.""" + reason: builtins.str + """Reason to unpause the activity.""" + @property + def jitter(self) -> google.protobuf.duration_pb2.Duration: + """If set, the activity will start at a random time within the specified jitter duration.""" + resource_id: builtins.str + """Resource ID for routing. Contains "workflow:{workflow_id}" for workflow activities or "activity:{activity_id}" for standalone activities.""" + request_id: builtins.str + """Used to de-dupe unpause requests.""" + def __init__( + self, + *, + namespace: builtins.str = ..., + workflow_id: builtins.str = ..., + activity_id: builtins.str = ..., + run_id: builtins.str = ..., + identity: builtins.str = ..., + reason: builtins.str = ..., + jitter: google.protobuf.duration_pb2.Duration | None = ..., + resource_id: builtins.str = ..., + request_id: builtins.str = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["jitter", b"jitter"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "activity_id", + b"activity_id", + "identity", + b"identity", + "jitter", + b"jitter", + "namespace", + b"namespace", + "reason", + b"reason", + "request_id", + b"request_id", + "resource_id", + b"resource_id", + "run_id", + b"run_id", + "workflow_id", + b"workflow_id", + ], + ) -> None: ... + +global___UnpauseActivityExecutionRequest = UnpauseActivityExecutionRequest + +class UnpauseActivityResponse(google.protobuf.message.Message): + """Deprecated. Use `UnpauseActivityExecutionResponse`.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___UnpauseActivityResponse = UnpauseActivityResponse + +class UnpauseActivityExecutionResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___UnpauseActivityExecutionResponse = UnpauseActivityExecutionResponse + +class ResetActivityRequest(google.protobuf.message.Message): + """NOTE: keep in sync with temporalio.api.batch.v1.BatchOperationResetActivities + Deprecated. Use `ResetActivityExecutionRequest`. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + EXECUTION_FIELD_NUMBER: builtins.int IDENTITY_FIELD_NUMBER: builtins.int ID_FIELD_NUMBER: builtins.int TYPE_FIELD_NUMBER: builtins.int @@ -7845,7 +8886,7 @@ class ResetActivityRequest(google.protobuf.message.Message): restore_original_options: builtins.bool """If set, the activity options will be restored to the defaults. Default options are then options activity was created with. - They are part of the first SCHEDULE event. + They are part of the first schedule event. """ def __init__( self, @@ -7911,7 +8952,104 @@ class ResetActivityRequest(google.protobuf.message.Message): global___ResetActivityRequest = ResetActivityRequest +class ResetActivityExecutionRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + WORKFLOW_ID_FIELD_NUMBER: builtins.int + ACTIVITY_ID_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + IDENTITY_FIELD_NUMBER: builtins.int + KEEP_PAUSED_FIELD_NUMBER: builtins.int + JITTER_FIELD_NUMBER: builtins.int + RESTORE_ORIGINAL_OPTIONS_FIELD_NUMBER: builtins.int + RESOURCE_ID_FIELD_NUMBER: builtins.int + REQUEST_ID_FIELD_NUMBER: builtins.int + RESET_HEARTBEAT_FIELD_NUMBER: builtins.int + namespace: builtins.str + """Namespace of the workflow which scheduled this activity.""" + workflow_id: builtins.str + """If provided, targets a workflow activity for the given workflow ID. + If empty, targets a standalone activity. + """ + activity_id: builtins.str + """The ID of the activity to target.""" + run_id: builtins.str + """Run ID of the workflow or standalone activity. If empty, targets the latest run.""" + identity: builtins.str + """The identity of the client who initiated this request.""" + keep_paused: builtins.bool + """If activity is paused, it will remain paused after reset""" + @property + def jitter(self) -> google.protobuf.duration_pb2.Duration: + """If set, and activity is in backoff, the activity will start at a random time within the specified jitter duration. + (unless it is paused and keep_paused is set) + """ + restore_original_options: builtins.bool + """If set, the activity options will be restored to the defaults. + Default options are then options activity was created with. + They are part of the first schedule event. + """ + resource_id: builtins.str + """Resource ID for routing. Contains "workflow:{workflow_id}" for workflow activities or "activity:{activity_id}" for standalone activities.""" + request_id: builtins.str + """Used to de-dupe reset requests.""" + reset_heartbeat: builtins.bool + """Reset persisted heartbeat details. + Reset always resets the attempt counter. Passing this flag causes reset to additionally + discard any persisted heartbeat details. + """ + def __init__( + self, + *, + namespace: builtins.str = ..., + workflow_id: builtins.str = ..., + activity_id: builtins.str = ..., + run_id: builtins.str = ..., + identity: builtins.str = ..., + keep_paused: builtins.bool = ..., + jitter: google.protobuf.duration_pb2.Duration | None = ..., + restore_original_options: builtins.bool = ..., + resource_id: builtins.str = ..., + request_id: builtins.str = ..., + reset_heartbeat: builtins.bool = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["jitter", b"jitter"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "activity_id", + b"activity_id", + "identity", + b"identity", + "jitter", + b"jitter", + "keep_paused", + b"keep_paused", + "namespace", + b"namespace", + "request_id", + b"request_id", + "reset_heartbeat", + b"reset_heartbeat", + "resource_id", + b"resource_id", + "restore_original_options", + b"restore_original_options", + "run_id", + b"run_id", + "workflow_id", + b"workflow_id", + ], + ) -> None: ... + +global___ResetActivityExecutionRequest = ResetActivityExecutionRequest + class ResetActivityResponse(google.protobuf.message.Message): + """Deprecated. Use `ResetActivityExecutionRequest`.""" + DESCRIPTOR: google.protobuf.descriptor.Descriptor def __init__( @@ -7920,6 +9058,15 @@ class ResetActivityResponse(google.protobuf.message.Message): global___ResetActivityResponse = ResetActivityResponse +class ResetActivityExecutionResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___ResetActivityExecutionResponse = ResetActivityExecutionResponse + class UpdateWorkflowExecutionOptionsRequest(google.protobuf.message.Message): """Keep the parameters in sync with: - temporalio.api.batch.v1.BatchOperationUpdateWorkflowExecutionOptions. @@ -7932,6 +9079,7 @@ class UpdateWorkflowExecutionOptionsRequest(google.protobuf.message.Message): WORKFLOW_EXECUTION_FIELD_NUMBER: builtins.int WORKFLOW_EXECUTION_OPTIONS_FIELD_NUMBER: builtins.int UPDATE_MASK_FIELD_NUMBER: builtins.int + IDENTITY_FIELD_NUMBER: builtins.int namespace: builtins.str """The namespace name of the target Workflow.""" @property @@ -7952,6 +9100,8 @@ class UpdateWorkflowExecutionOptionsRequest(google.protobuf.message.Message): """Controls which fields from `workflow_execution_options` will be applied. To unset a field, set it to null and use the update mask to indicate that it should be mutated. """ + identity: builtins.str + """Optional. The identity of the client who initiated this request.""" def __init__( self, *, @@ -7961,6 +9111,7 @@ class UpdateWorkflowExecutionOptionsRequest(google.protobuf.message.Message): workflow_execution_options: temporalio.api.workflow.v1.message_pb2.WorkflowExecutionOptions | None = ..., update_mask: google.protobuf.field_mask_pb2.FieldMask | None = ..., + identity: builtins.str = ..., ) -> None: ... def HasField( self, @@ -7976,6 +9127,8 @@ class UpdateWorkflowExecutionOptionsRequest(google.protobuf.message.Message): def ClearField( self, field_name: typing_extensions.Literal[ + "identity", + b"identity", "namespace", b"namespace", "update_mask", @@ -7993,27 +9146,43 @@ class UpdateWorkflowExecutionOptionsResponse(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor WORKFLOW_EXECUTION_OPTIONS_FIELD_NUMBER: builtins.int + UPDATE_TIME_FIELD_NUMBER: builtins.int @property def workflow_execution_options( self, ) -> temporalio.api.workflow.v1.message_pb2.WorkflowExecutionOptions: """Workflow Execution options after update.""" + @property + def update_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """The Workflow Execution time when the options were updated. When time skipping is + enabled, this is the workflow's virtual time rather than wall-clock time. + + This timestamp cannot be used for time-skipping fast-forward verification, + use `fast_forward_id` in `PollWorkflowExecutionTimeSkippingRequest` instead. + """ def __init__( self, *, workflow_execution_options: temporalio.api.workflow.v1.message_pb2.WorkflowExecutionOptions | None = ..., + update_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal[ - "workflow_execution_options", b"workflow_execution_options" + "update_time", + b"update_time", + "workflow_execution_options", + b"workflow_execution_options", ], ) -> builtins.bool: ... def ClearField( self, field_name: typing_extensions.Literal[ - "workflow_execution_options", b"workflow_execution_options" + "update_time", + b"update_time", + "workflow_execution_options", + b"workflow_execution_options", ], ) -> None: ... @@ -8487,6 +9656,7 @@ class SetWorkerDeploymentCurrentVersionRequest(google.protobuf.message.Message): CONFLICT_TOKEN_FIELD_NUMBER: builtins.int IDENTITY_FIELD_NUMBER: builtins.int IGNORE_MISSING_TASK_QUEUES_FIELD_NUMBER: builtins.int + ALLOW_NO_POLLERS_FIELD_NUMBER: builtins.int namespace: builtins.str deployment_name: builtins.str version: builtins.str @@ -8519,6 +9689,11 @@ class SetWorkerDeploymentCurrentVersionRequest(google.protobuf.message.Message): pollers have not reached to the server yet. Only set this if you expect those pollers to never arrive. """ + allow_no_pollers: builtins.bool + """Optional. By default this request will be rejected if no pollers have been seen for the proposed + Current Version, in order to protect users from routing tasks to pollers that do not exist, leading + to possible timeouts. Pass `true` here to bypass this protection. + """ def __init__( self, *, @@ -8529,10 +9704,13 @@ class SetWorkerDeploymentCurrentVersionRequest(google.protobuf.message.Message): conflict_token: builtins.bytes = ..., identity: builtins.str = ..., ignore_missing_task_queues: builtins.bool = ..., + allow_no_pollers: builtins.bool = ..., ) -> None: ... def ClearField( self, field_name: typing_extensions.Literal[ + "allow_no_pollers", + b"allow_no_pollers", "build_id", b"build_id", "conflict_token", @@ -8571,7 +9749,12 @@ class SetWorkerDeploymentCurrentVersionResponse(google.protobuf.message.Message) def previous_deployment_version( self, ) -> temporalio.api.deployment.v1.message_pb2.WorkerDeploymentVersion: - """The version that was current before executing this operation.""" + """The version that was current before executing this operation. + Deprecated in favor of idempotency of the API. Use `DescribeWorkerDeployment` to get the + Current version info before calling this API. By passing the `conflict_token` got from the + `DescribeWorkerDeployment` call to this API you can ensure there is no interfering changes + between the two calls. + """ def __init__( self, *, @@ -8615,6 +9798,7 @@ class SetWorkerDeploymentRampingVersionRequest(google.protobuf.message.Message): CONFLICT_TOKEN_FIELD_NUMBER: builtins.int IDENTITY_FIELD_NUMBER: builtins.int IGNORE_MISSING_TASK_QUEUES_FIELD_NUMBER: builtins.int + ALLOW_NO_POLLERS_FIELD_NUMBER: builtins.int namespace: builtins.str deployment_name: builtins.str version: builtins.str @@ -8652,6 +9836,11 @@ class SetWorkerDeploymentRampingVersionRequest(google.protobuf.message.Message): that the percentage changes. Also note that the check is against the deployment's Current Version, not the previous Ramping Version. """ + allow_no_pollers: builtins.bool + """Optional. By default this request will be rejected if no pollers have been seen for the proposed + Current Version, in order to protect users from routing tasks to pollers that do not exist, leading + to possible timeouts. Pass `true` here to bypass this protection. + """ def __init__( self, *, @@ -8663,10 +9852,13 @@ class SetWorkerDeploymentRampingVersionRequest(google.protobuf.message.Message): conflict_token: builtins.bytes = ..., identity: builtins.str = ..., ignore_missing_task_queues: builtins.bool = ..., + allow_no_pollers: builtins.bool = ..., ) -> None: ... def ClearField( self, field_name: typing_extensions.Literal[ + "allow_no_pollers", + b"allow_no_pollers", "build_id", b"build_id", "conflict_token", @@ -8708,9 +9900,19 @@ class SetWorkerDeploymentRampingVersionResponse(google.protobuf.message.Message) def previous_deployment_version( self, ) -> temporalio.api.deployment.v1.message_pb2.WorkerDeploymentVersion: - """The version that was ramping before executing this operation.""" + """The version that was ramping before executing this operation. + Deprecated in favor of idempotency of the API. Use `DescribeWorkerDeployment` to get the + Ramping version info before calling this API. By passing the `conflict_token` got from the + `DescribeWorkerDeployment` call to this API you can ensure there is no interfering changes + between the two calls. + """ previous_percentage: builtins.float - """The ramping version percentage before executing this operation.""" + """The ramping version percentage before executing this operation. + Deprecated in favor of idempotency of the API. Use `DescribeWorkerDeployment` to get the + Ramping version info before calling this API. By passing the `conflict_token` got from the + `DescribeWorkerDeployment` call to this API you can ensure there is no interfering changes + between the two calls. + """ def __init__( self, *, @@ -8744,6 +9946,68 @@ global___SetWorkerDeploymentRampingVersionResponse = ( SetWorkerDeploymentRampingVersionResponse ) +class CreateWorkerDeploymentRequest(google.protobuf.message.Message): + """Creates a new WorkerDeployment.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + DEPLOYMENT_NAME_FIELD_NUMBER: builtins.int + IDENTITY_FIELD_NUMBER: builtins.int + REQUEST_ID_FIELD_NUMBER: builtins.int + namespace: builtins.str + deployment_name: builtins.str + """The name of the Worker Deployment to create. If a Worker Deployment with + this name already exists, an error will be returned. + """ + identity: builtins.str + """Optional. The identity of the client who initiated this request.""" + request_id: builtins.str + """A unique identifier for this create request for idempotence. Typically UUIDv4.""" + def __init__( + self, + *, + namespace: builtins.str = ..., + deployment_name: builtins.str = ..., + identity: builtins.str = ..., + request_id: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "deployment_name", + b"deployment_name", + "identity", + b"identity", + "namespace", + b"namespace", + "request_id", + b"request_id", + ], + ) -> None: ... + +global___CreateWorkerDeploymentRequest = CreateWorkerDeploymentRequest + +class CreateWorkerDeploymentResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CONFLICT_TOKEN_FIELD_NUMBER: builtins.int + conflict_token: builtins.bytes + """This value is returned so that it can be optionally passed to APIs that + write to the WorkerDeployment state to ensure that the state did not + change between this API call and a future write. + """ + def __init__( + self, + *, + conflict_token: builtins.bytes = ..., + ) -> None: ... + def ClearField( + self, field_name: typing_extensions.Literal["conflict_token", b"conflict_token"] + ) -> None: ... + +global___CreateWorkerDeploymentResponse = CreateWorkerDeploymentResponse + class ListWorkerDeploymentsRequest(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -8890,37 +10154,111 @@ class ListWorkerDeploymentsResponse(google.protobuf.message.Message): global___ListWorkerDeploymentsResponse = ListWorkerDeploymentsResponse -class DeleteWorkerDeploymentVersionRequest(google.protobuf.message.Message): - """Used for manual deletion of Versions. User can delete a Version only when all the - following conditions are met: - - It is not the Current or Ramping Version of its Deployment. - - It has no active pollers (none of the task queues in the Version have pollers) - - It is not draining (see WorkerDeploymentVersionInfo.drainage_info). This condition - can be skipped by passing `skip-drainage=true`. - """ +class CreateWorkerDeploymentVersionRequest(google.protobuf.message.Message): + """Creates a new WorkerDeploymentVersion.""" DESCRIPTOR: google.protobuf.descriptor.Descriptor NAMESPACE_FIELD_NUMBER: builtins.int - VERSION_FIELD_NUMBER: builtins.int DEPLOYMENT_VERSION_FIELD_NUMBER: builtins.int - SKIP_DRAINAGE_FIELD_NUMBER: builtins.int + COMPUTE_CONFIG_FIELD_NUMBER: builtins.int IDENTITY_FIELD_NUMBER: builtins.int + REQUEST_ID_FIELD_NUMBER: builtins.int namespace: builtins.str - version: builtins.str - """Deprecated. Use `deployment_version`.""" @property def deployment_version( self, ) -> temporalio.api.deployment.v1.message_pb2.WorkerDeploymentVersion: """Required.""" - skip_drainage: builtins.bool - """Pass to force deletion even if the Version is draining. In this case the open pinned - workflows will be stuck until manually moved to another version by UpdateWorkflowExecutionOptions. - """ + @property + def compute_config(self) -> temporalio.api.compute.v1.config_pb2.ComputeConfig: + """Optional. Contains the new worker compute configuration for the Worker + Deployment. Used for worker scale management. + """ identity: builtins.str """Optional. The identity of the client who initiated this request.""" - def __init__( + request_id: builtins.str + """A unique identifier for this create request for idempotence. Typically UUIDv4. + If a second request with the same ID is recieved, it is considered a successful no-op. + Retrying with a different request ID for the same deployment name + build ID is an error. + """ + def __init__( + self, + *, + namespace: builtins.str = ..., + deployment_version: temporalio.api.deployment.v1.message_pb2.WorkerDeploymentVersion + | None = ..., + compute_config: temporalio.api.compute.v1.config_pb2.ComputeConfig | None = ..., + identity: builtins.str = ..., + request_id: builtins.str = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "compute_config", + b"compute_config", + "deployment_version", + b"deployment_version", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "compute_config", + b"compute_config", + "deployment_version", + b"deployment_version", + "identity", + b"identity", + "namespace", + b"namespace", + "request_id", + b"request_id", + ], + ) -> None: ... + +global___CreateWorkerDeploymentVersionRequest = CreateWorkerDeploymentVersionRequest + +class CreateWorkerDeploymentVersionResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___CreateWorkerDeploymentVersionResponse = CreateWorkerDeploymentVersionResponse + +class DeleteWorkerDeploymentVersionRequest(google.protobuf.message.Message): + """Used for manual deletion of Versions. User can delete a Version only when all the + following conditions are met: + - It is not the Current or Ramping Version of its Deployment. + - It has no active pollers (none of the task queues in the Version have pollers) + - It is not draining (see WorkerDeploymentVersionInfo.drainage_info). This condition + can be skipped by passing `skip-drainage=true`. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + VERSION_FIELD_NUMBER: builtins.int + DEPLOYMENT_VERSION_FIELD_NUMBER: builtins.int + SKIP_DRAINAGE_FIELD_NUMBER: builtins.int + IDENTITY_FIELD_NUMBER: builtins.int + namespace: builtins.str + version: builtins.str + """Deprecated. Use `deployment_version`.""" + @property + def deployment_version( + self, + ) -> temporalio.api.deployment.v1.message_pb2.WorkerDeploymentVersion: + """Required.""" + skip_drainage: builtins.bool + """Pass to force deletion even if the Version is draining. In this case the open pinned + workflows will be stuck until manually moved to another version by UpdateWorkflowExecutionOptions. + """ + identity: builtins.str + """Optional. The identity of the client who initiated this request.""" + def __init__( self, *, namespace: builtins.str = ..., @@ -9007,6 +10345,243 @@ class DeleteWorkerDeploymentResponse(google.protobuf.message.Message): global___DeleteWorkerDeploymentResponse = DeleteWorkerDeploymentResponse +class UpdateWorkerDeploymentVersionComputeConfigRequest( + google.protobuf.message.Message +): + """Used to update the compute config of a Worker Deployment Version.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class ComputeConfigScalingGroupsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value( + self, + ) -> temporalio.api.compute.v1.config_pb2.ComputeConfigScalingGroupUpdate: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: temporalio.api.compute.v1.config_pb2.ComputeConfigScalingGroupUpdate + | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["value", b"value"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["key", b"key", "value", b"value"], + ) -> None: ... + + NAMESPACE_FIELD_NUMBER: builtins.int + DEPLOYMENT_VERSION_FIELD_NUMBER: builtins.int + COMPUTE_CONFIG_SCALING_GROUPS_FIELD_NUMBER: builtins.int + REMOVE_COMPUTE_CONFIG_SCALING_GROUPS_FIELD_NUMBER: builtins.int + IDENTITY_FIELD_NUMBER: builtins.int + REQUEST_ID_FIELD_NUMBER: builtins.int + namespace: builtins.str + @property + def deployment_version( + self, + ) -> temporalio.api.deployment.v1.message_pb2.WorkerDeploymentVersion: + """Required.""" + @property + def compute_config_scaling_groups( + self, + ) -> google.protobuf.internal.containers.MessageMap[ + builtins.str, + temporalio.api.compute.v1.config_pb2.ComputeConfigScalingGroupUpdate, + ]: + """Optional. Contains the compute config scaling groups to add or update for the Worker + Deployment. + """ + @property + def remove_compute_config_scaling_groups( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """Optional. Contains the compute config scaling groups to remove from the Worker Deployment.""" + identity: builtins.str + """Optional. The identity of the client who initiated this request.""" + request_id: builtins.str + """A unique identifier for this create request for idempotence. Typically UUIDv4. + If a second request with the same ID is recieved, it is considered a successful no-op. + Retrying with a different request ID for the same deployment name + build ID is an error. + """ + def __init__( + self, + *, + namespace: builtins.str = ..., + deployment_version: temporalio.api.deployment.v1.message_pb2.WorkerDeploymentVersion + | None = ..., + compute_config_scaling_groups: collections.abc.Mapping[ + builtins.str, + temporalio.api.compute.v1.config_pb2.ComputeConfigScalingGroupUpdate, + ] + | None = ..., + remove_compute_config_scaling_groups: collections.abc.Iterable[builtins.str] + | None = ..., + identity: builtins.str = ..., + request_id: builtins.str = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "deployment_version", b"deployment_version" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "compute_config_scaling_groups", + b"compute_config_scaling_groups", + "deployment_version", + b"deployment_version", + "identity", + b"identity", + "namespace", + b"namespace", + "remove_compute_config_scaling_groups", + b"remove_compute_config_scaling_groups", + "request_id", + b"request_id", + ], + ) -> None: ... + +global___UpdateWorkerDeploymentVersionComputeConfigRequest = ( + UpdateWorkerDeploymentVersionComputeConfigRequest +) + +class UpdateWorkerDeploymentVersionComputeConfigResponse( + google.protobuf.message.Message +): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___UpdateWorkerDeploymentVersionComputeConfigResponse = ( + UpdateWorkerDeploymentVersionComputeConfigResponse +) + +class ValidateWorkerDeploymentVersionComputeConfigRequest( + google.protobuf.message.Message +): + """Used to validate the compute config without attaching it to a Worker Deployment Version.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class ComputeConfigScalingGroupsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value( + self, + ) -> temporalio.api.compute.v1.config_pb2.ComputeConfigScalingGroupUpdate: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: temporalio.api.compute.v1.config_pb2.ComputeConfigScalingGroupUpdate + | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["value", b"value"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["key", b"key", "value", b"value"], + ) -> None: ... + + NAMESPACE_FIELD_NUMBER: builtins.int + DEPLOYMENT_VERSION_FIELD_NUMBER: builtins.int + COMPUTE_CONFIG_SCALING_GROUPS_FIELD_NUMBER: builtins.int + REMOVE_COMPUTE_CONFIG_SCALING_GROUPS_FIELD_NUMBER: builtins.int + IDENTITY_FIELD_NUMBER: builtins.int + namespace: builtins.str + @property + def deployment_version( + self, + ) -> temporalio.api.deployment.v1.message_pb2.WorkerDeploymentVersion: + """Required.""" + @property + def compute_config_scaling_groups( + self, + ) -> google.protobuf.internal.containers.MessageMap[ + builtins.str, + temporalio.api.compute.v1.config_pb2.ComputeConfigScalingGroupUpdate, + ]: + """Optional. Contains the compute config scaling groups to add or update for the Worker + Deployment. + """ + @property + def remove_compute_config_scaling_groups( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """Optional. Contains the compute config scaling groups to remove from the Worker Deployment.""" + identity: builtins.str + """Optional. The identity of the client who initiated this request.""" + def __init__( + self, + *, + namespace: builtins.str = ..., + deployment_version: temporalio.api.deployment.v1.message_pb2.WorkerDeploymentVersion + | None = ..., + compute_config_scaling_groups: collections.abc.Mapping[ + builtins.str, + temporalio.api.compute.v1.config_pb2.ComputeConfigScalingGroupUpdate, + ] + | None = ..., + remove_compute_config_scaling_groups: collections.abc.Iterable[builtins.str] + | None = ..., + identity: builtins.str = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "deployment_version", b"deployment_version" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "compute_config_scaling_groups", + b"compute_config_scaling_groups", + "deployment_version", + b"deployment_version", + "identity", + b"identity", + "namespace", + b"namespace", + "remove_compute_config_scaling_groups", + b"remove_compute_config_scaling_groups", + ], + ) -> None: ... + +global___ValidateWorkerDeploymentVersionComputeConfigRequest = ( + ValidateWorkerDeploymentVersionComputeConfigRequest +) + +class ValidateWorkerDeploymentVersionComputeConfigResponse( + google.protobuf.message.Message +): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___ValidateWorkerDeploymentVersionComputeConfigResponse = ( + ValidateWorkerDeploymentVersionComputeConfigResponse +) + class UpdateWorkerDeploymentVersionMetadataRequest(google.protobuf.message.Message): """Used to update the user-defined metadata of a Worker Deployment Version.""" @@ -9126,21 +10701,133 @@ global___UpdateWorkerDeploymentVersionMetadataResponse = ( UpdateWorkerDeploymentVersionMetadataResponse ) -class GetCurrentDeploymentRequest(google.protobuf.message.Message): - """Returns the Current Deployment of a deployment series. - [cleanup-wv-pre-release] Pre-release deployment APIs, clean up later - """ +class SetWorkerDeploymentManagerRequest(google.protobuf.message.Message): + """Update the ManagerIdentity of a Worker Deployment.""" DESCRIPTOR: google.protobuf.descriptor.Descriptor NAMESPACE_FIELD_NUMBER: builtins.int - SERIES_NAME_FIELD_NUMBER: builtins.int + DEPLOYMENT_NAME_FIELD_NUMBER: builtins.int + MANAGER_IDENTITY_FIELD_NUMBER: builtins.int + SELF_FIELD_NUMBER: builtins.int + CONFLICT_TOKEN_FIELD_NUMBER: builtins.int + IDENTITY_FIELD_NUMBER: builtins.int namespace: builtins.str - series_name: builtins.str - def __init__( - self, - *, - namespace: builtins.str = ..., + deployment_name: builtins.str + manager_identity: builtins.str + """Arbitrary value for `manager_identity`. + Empty will unset the field. + """ + self: builtins.bool + """True will set `manager_identity` to `identity`.""" + conflict_token: builtins.bytes + """Optional. This can be the value of conflict_token from a Describe, or another Worker + Deployment API. Passing a non-nil conflict token will cause this request to fail if the + Deployment's configuration has been modified between the API call that generated the + token and this one. + """ + identity: builtins.str + """Required. The identity of the client who initiated this request.""" + def __init__( + # pyright: reportSelfClsParameterName=false + self_, + *, + namespace: builtins.str = ..., + deployment_name: builtins.str = ..., + manager_identity: builtins.str = ..., + self: builtins.bool = ..., + conflict_token: builtins.bytes = ..., + identity: builtins.str = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "manager_identity", + b"manager_identity", + "new_manager_identity", + b"new_manager_identity", + "self", + b"self", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "conflict_token", + b"conflict_token", + "deployment_name", + b"deployment_name", + "identity", + b"identity", + "manager_identity", + b"manager_identity", + "namespace", + b"namespace", + "new_manager_identity", + b"new_manager_identity", + "self", + b"self", + ], + ) -> None: ... + def WhichOneof( + self, + oneof_group: typing_extensions.Literal[ + "new_manager_identity", b"new_manager_identity" + ], + ) -> typing_extensions.Literal["manager_identity", "self"] | None: ... + +global___SetWorkerDeploymentManagerRequest = SetWorkerDeploymentManagerRequest + +class SetWorkerDeploymentManagerResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CONFLICT_TOKEN_FIELD_NUMBER: builtins.int + PREVIOUS_MANAGER_IDENTITY_FIELD_NUMBER: builtins.int + conflict_token: builtins.bytes + """This value is returned so that it can be optionally passed to APIs + that write to the Worker Deployment state to ensure that the state + did not change between this API call and a future write. + """ + previous_manager_identity: builtins.str + """What the `manager_identity` field was before this change. + Deprecated in favor of idempotency of the API. Use `DescribeWorkerDeployment` to get the + manager identity before calling this API. By passing the `conflict_token` got from the + `DescribeWorkerDeployment` call to this API you can ensure there is no interfering changes + between the two calls. + """ + def __init__( + self, + *, + conflict_token: builtins.bytes = ..., + previous_manager_identity: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "conflict_token", + b"conflict_token", + "previous_manager_identity", + b"previous_manager_identity", + ], + ) -> None: ... + +global___SetWorkerDeploymentManagerResponse = SetWorkerDeploymentManagerResponse + +class GetCurrentDeploymentRequest(google.protobuf.message.Message): + """Returns the Current Deployment of a deployment series. + [cleanup-wv-pre-release] Pre-release deployment APIs, clean up later + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + SERIES_NAME_FIELD_NUMBER: builtins.int + namespace: builtins.str + series_name: builtins.str + def __init__( + self, + *, + namespace: builtins.str = ..., series_name: builtins.str = ..., ) -> None: ... def ClearField( @@ -9552,6 +11239,7 @@ class RecordWorkerHeartbeatRequest(google.protobuf.message.Message): NAMESPACE_FIELD_NUMBER: builtins.int IDENTITY_FIELD_NUMBER: builtins.int WORKER_HEARTBEAT_FIELD_NUMBER: builtins.int + RESOURCE_ID_FIELD_NUMBER: builtins.int namespace: builtins.str """Namespace this worker belongs to.""" identity: builtins.str @@ -9562,6 +11250,8 @@ class RecordWorkerHeartbeatRequest(google.protobuf.message.Message): ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ temporalio.api.worker.v1.message_pb2.WorkerHeartbeat ]: ... + resource_id: builtins.str + """Resource ID for routing. Contains the worker grouping key.""" def __init__( self, *, @@ -9571,6 +11261,7 @@ class RecordWorkerHeartbeatRequest(google.protobuf.message.Message): temporalio.api.worker.v1.message_pb2.WorkerHeartbeat ] | None = ..., + resource_id: builtins.str = ..., ) -> None: ... def ClearField( self, @@ -9579,6 +11270,8 @@ class RecordWorkerHeartbeatRequest(google.protobuf.message.Message): b"identity", "namespace", b"namespace", + "resource_id", + b"resource_id", "worker_heartbeat", b"worker_heartbeat", ], @@ -9602,12 +11295,13 @@ class ListWorkersRequest(google.protobuf.message.Message): PAGE_SIZE_FIELD_NUMBER: builtins.int NEXT_PAGE_TOKEN_FIELD_NUMBER: builtins.int QUERY_FIELD_NUMBER: builtins.int + INCLUDE_SYSTEM_WORKERS_FIELD_NUMBER: builtins.int namespace: builtins.str page_size: builtins.int next_page_token: builtins.bytes query: builtins.str - """`query` in ListWorkers is used to filter workers based on worker status info. - The following worker status attributes are expected are supported as part of the query: + """`query` in ListWorkers is used to filter workers based on worker attributes. + Supported attributes: * WorkerInstanceKey * WorkerIdentity * HostName @@ -9617,9 +11311,11 @@ class ListWorkersRequest(google.protobuf.message.Message): * SdkName * SdkVersion * StartTime - * LastHeartbeatTime * Status - Currently metrics are not supported as a part of ListWorkers query. + """ + include_system_workers: builtins.bool + """When true, the response will include system workers that are created implicitly + by the server and not by the user. By default, system workers are excluded. """ def __init__( self, @@ -9628,10 +11324,13 @@ class ListWorkersRequest(google.protobuf.message.Message): page_size: builtins.int = ..., next_page_token: builtins.bytes = ..., query: builtins.str = ..., + include_system_workers: builtins.bool = ..., ) -> None: ... def ClearField( self, field_name: typing_extensions.Literal[ + "include_system_workers", + b"include_system_workers", "namespace", b"namespace", "next_page_token", @@ -9649,13 +11348,24 @@ class ListWorkersResponse(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor WORKERS_INFO_FIELD_NUMBER: builtins.int + WORKERS_FIELD_NUMBER: builtins.int NEXT_PAGE_TOKEN_FIELD_NUMBER: builtins.int @property def workers_info( self, ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ temporalio.api.worker.v1.message_pb2.WorkerInfo - ]: ... + ]: + """Deprecated: Use workers instead. This field returns full WorkerInfo which + includes expensive runtime metrics. We will stop populating this field in the future. + """ + @property + def workers( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.worker.v1.message_pb2.WorkerListInfo + ]: + """Limited worker information.""" next_page_token: builtins.bytes """Next page token""" def __init__( @@ -9665,12 +11375,21 @@ class ListWorkersResponse(google.protobuf.message.Message): temporalio.api.worker.v1.message_pb2.WorkerInfo ] | None = ..., + workers: collections.abc.Iterable[ + temporalio.api.worker.v1.message_pb2.WorkerListInfo + ] + | None = ..., next_page_token: builtins.bytes = ..., ) -> None: ... def ClearField( self, field_name: typing_extensions.Literal[ - "next_page_token", b"next_page_token", "workers_info", b"workers_info" + "next_page_token", + b"next_page_token", + "workers", + b"workers", + "workers_info", + b"workers_info", ], ) -> None: ... @@ -9705,12 +11424,32 @@ class UpdateTaskQueueConfigRequest(google.protobuf.message.Message): ], ) -> None: ... + class SetFairnessWeightOverridesEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.float + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.float = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal["key", b"key", "value", b"value"], + ) -> None: ... + NAMESPACE_FIELD_NUMBER: builtins.int IDENTITY_FIELD_NUMBER: builtins.int TASK_QUEUE_FIELD_NUMBER: builtins.int TASK_QUEUE_TYPE_FIELD_NUMBER: builtins.int UPDATE_QUEUE_RATE_LIMIT_FIELD_NUMBER: builtins.int UPDATE_FAIRNESS_KEY_RATE_LIMIT_DEFAULT_FIELD_NUMBER: builtins.int + SET_FAIRNESS_WEIGHT_OVERRIDES_FIELD_NUMBER: builtins.int + UNSET_FAIRNESS_WEIGHT_OVERRIDES_FIELD_NUMBER: builtins.int namespace: builtins.str identity: builtins.str task_queue: builtins.str @@ -9733,6 +11472,21 @@ class UpdateTaskQueueConfigRequest(google.protobuf.message.Message): If not set, this configuration is unchanged. If the `rate_limit` field in the `RateLimitUpdate` is missing, remove the existing rate limit. """ + @property + def set_fairness_weight_overrides( + self, + ) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.float]: + """If set, overrides the fairness weight for each specified fairness key. + Fairness keys not listed in this map will keep their existing overrides (if any). + """ + @property + def unset_fairness_weight_overrides( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """If set, removes any existing fairness weight overrides for each specified fairness key. + Fairness weights for corresponding keys fall back to the values set during task creation (if any), + or to the default weight of 1.0. + """ def __init__( self, *, @@ -9744,6 +11498,12 @@ class UpdateTaskQueueConfigRequest(google.protobuf.message.Message): | None = ..., update_fairness_key_rate_limit_default: global___UpdateTaskQueueConfigRequest.RateLimitUpdate | None = ..., + set_fairness_weight_overrides: collections.abc.Mapping[ + builtins.str, builtins.float + ] + | None = ..., + unset_fairness_weight_overrides: collections.abc.Iterable[builtins.str] + | None = ..., ) -> None: ... def HasField( self, @@ -9761,10 +11521,14 @@ class UpdateTaskQueueConfigRequest(google.protobuf.message.Message): b"identity", "namespace", b"namespace", + "set_fairness_weight_overrides", + b"set_fairness_weight_overrides", "task_queue", b"task_queue", "task_queue_type", b"task_queue_type", + "unset_fairness_weight_overrides", + b"unset_fairness_weight_overrides", "update_fairness_key_rate_limit_default", b"update_fairness_key_rate_limit_default", "update_queue_rate_limit", @@ -9801,6 +11565,7 @@ class FetchWorkerConfigRequest(google.protobuf.message.Message): IDENTITY_FIELD_NUMBER: builtins.int REASON_FIELD_NUMBER: builtins.int SELECTOR_FIELD_NUMBER: builtins.int + RESOURCE_ID_FIELD_NUMBER: builtins.int namespace: builtins.str """Namespace this worker belongs to.""" identity: builtins.str @@ -9812,6 +11577,8 @@ class FetchWorkerConfigRequest(google.protobuf.message.Message): """Defines which workers should receive this command. only single worker is supported at this time. """ + resource_id: builtins.str + """Resource ID for routing. Contains the worker grouping key.""" def __init__( self, *, @@ -9819,6 +11586,7 @@ class FetchWorkerConfigRequest(google.protobuf.message.Message): identity: builtins.str = ..., reason: builtins.str = ..., selector: temporalio.api.common.v1.message_pb2.WorkerSelector | None = ..., + resource_id: builtins.str = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal["selector", b"selector"] @@ -9832,6 +11600,8 @@ class FetchWorkerConfigRequest(google.protobuf.message.Message): b"namespace", "reason", b"reason", + "resource_id", + b"resource_id", "selector", b"selector", ], @@ -9870,6 +11640,7 @@ class UpdateWorkerConfigRequest(google.protobuf.message.Message): WORKER_CONFIG_FIELD_NUMBER: builtins.int UPDATE_MASK_FIELD_NUMBER: builtins.int SELECTOR_FIELD_NUMBER: builtins.int + RESOURCE_ID_FIELD_NUMBER: builtins.int namespace: builtins.str """Namespace this worker belongs to.""" identity: builtins.str @@ -9887,6 +11658,8 @@ class UpdateWorkerConfigRequest(google.protobuf.message.Message): @property def selector(self) -> temporalio.api.common.v1.message_pb2.WorkerSelector: """Defines which workers should receive this command.""" + resource_id: builtins.str + """Resource ID for routing. Contains the worker grouping key.""" def __init__( self, *, @@ -9897,6 +11670,7 @@ class UpdateWorkerConfigRequest(google.protobuf.message.Message): | None = ..., update_mask: google.protobuf.field_mask_pb2.FieldMask | None = ..., selector: temporalio.api.common.v1.message_pb2.WorkerSelector | None = ..., + resource_id: builtins.str = ..., ) -> None: ... def HasField( self, @@ -9918,6 +11692,8 @@ class UpdateWorkerConfigRequest(google.protobuf.message.Message): b"namespace", "reason", b"reason", + "resource_id", + b"resource_id", "selector", b"selector", "update_mask", @@ -9959,3 +11735,1907 @@ class UpdateWorkerConfigResponse(google.protobuf.message.Message): ) -> typing_extensions.Literal["worker_config"] | None: ... global___UpdateWorkerConfigResponse = UpdateWorkerConfigResponse + +class DescribeWorkerRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + WORKER_INSTANCE_KEY_FIELD_NUMBER: builtins.int + namespace: builtins.str + """Namespace this worker belongs to.""" + worker_instance_key: builtins.str + """Worker instance key to describe.""" + def __init__( + self, + *, + namespace: builtins.str = ..., + worker_instance_key: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "namespace", b"namespace", "worker_instance_key", b"worker_instance_key" + ], + ) -> None: ... + +global___DescribeWorkerRequest = DescribeWorkerRequest + +class DescribeWorkerResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + WORKER_INFO_FIELD_NUMBER: builtins.int + @property + def worker_info(self) -> temporalio.api.worker.v1.message_pb2.WorkerInfo: ... + def __init__( + self, + *, + worker_info: temporalio.api.worker.v1.message_pb2.WorkerInfo | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["worker_info", b"worker_info"] + ) -> builtins.bool: ... + def ClearField( + self, field_name: typing_extensions.Literal["worker_info", b"worker_info"] + ) -> None: ... + +global___DescribeWorkerResponse = DescribeWorkerResponse + +class CountWorkersRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + QUERY_FIELD_NUMBER: builtins.int + INCLUDE_SYSTEM_WORKERS_FIELD_NUMBER: builtins.int + namespace: builtins.str + query: builtins.str + """Query to filter workers before counting. + Supported filter fields are the same as in ListWorkersRequest. + """ + include_system_workers: builtins.bool + """When true, the count will include system workers that are created implicitly + by the server and not by the user. By default, system workers are excluded. + """ + def __init__( + self, + *, + namespace: builtins.str = ..., + query: builtins.str = ..., + include_system_workers: builtins.bool = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "include_system_workers", + b"include_system_workers", + "namespace", + b"namespace", + "query", + b"query", + ], + ) -> None: ... + +global___CountWorkersRequest = CountWorkersRequest + +class CountWorkersResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + COUNT_FIELD_NUMBER: builtins.int + count: builtins.int + """Number of workers matching the query.""" + def __init__( + self, + *, + count: builtins.int = ..., + ) -> None: ... + def ClearField( + self, field_name: typing_extensions.Literal["count", b"count"] + ) -> None: ... + +global___CountWorkersResponse = CountWorkersResponse + +class PauseWorkflowExecutionRequest(google.protobuf.message.Message): + """Request to pause a workflow execution.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + WORKFLOW_ID_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + IDENTITY_FIELD_NUMBER: builtins.int + REASON_FIELD_NUMBER: builtins.int + REQUEST_ID_FIELD_NUMBER: builtins.int + namespace: builtins.str + """Namespace of the workflow to pause.""" + workflow_id: builtins.str + """ID of the workflow execution to be paused. Required.""" + run_id: builtins.str + """Run ID of the workflow execution to be paused. Optional. If not provided, the current run of the workflow will be paused.""" + identity: builtins.str + """The identity of the client who initiated this request.""" + reason: builtins.str + """Reason to pause the workflow execution.""" + request_id: builtins.str + """A unique identifier for this pause request for idempotence. Typically UUIDv4.""" + def __init__( + self, + *, + namespace: builtins.str = ..., + workflow_id: builtins.str = ..., + run_id: builtins.str = ..., + identity: builtins.str = ..., + reason: builtins.str = ..., + request_id: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "identity", + b"identity", + "namespace", + b"namespace", + "reason", + b"reason", + "request_id", + b"request_id", + "run_id", + b"run_id", + "workflow_id", + b"workflow_id", + ], + ) -> None: ... + +global___PauseWorkflowExecutionRequest = PauseWorkflowExecutionRequest + +class PauseWorkflowExecutionResponse(google.protobuf.message.Message): + """Response to a successful PauseWorkflowExecution request.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___PauseWorkflowExecutionResponse = PauseWorkflowExecutionResponse + +class UnpauseWorkflowExecutionRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + WORKFLOW_ID_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + IDENTITY_FIELD_NUMBER: builtins.int + REASON_FIELD_NUMBER: builtins.int + REQUEST_ID_FIELD_NUMBER: builtins.int + namespace: builtins.str + """Namespace of the workflow to unpause.""" + workflow_id: builtins.str + """ID of the workflow execution to be paused. Required.""" + run_id: builtins.str + """Run ID of the workflow execution to be paused. Optional. If not provided, the current run of the workflow will be paused.""" + identity: builtins.str + """The identity of the client who initiated this request.""" + reason: builtins.str + """Reason to unpause the workflow execution.""" + request_id: builtins.str + """A unique identifier for this unpause request for idempotence. Typically UUIDv4.""" + def __init__( + self, + *, + namespace: builtins.str = ..., + workflow_id: builtins.str = ..., + run_id: builtins.str = ..., + identity: builtins.str = ..., + reason: builtins.str = ..., + request_id: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "identity", + b"identity", + "namespace", + b"namespace", + "reason", + b"reason", + "request_id", + b"request_id", + "run_id", + b"run_id", + "workflow_id", + b"workflow_id", + ], + ) -> None: ... + +global___UnpauseWorkflowExecutionRequest = UnpauseWorkflowExecutionRequest + +class UnpauseWorkflowExecutionResponse(google.protobuf.message.Message): + """Response to a successful UnpauseWorkflowExecution request.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___UnpauseWorkflowExecutionResponse = UnpauseWorkflowExecutionResponse + +class StartActivityExecutionRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + IDENTITY_FIELD_NUMBER: builtins.int + REQUEST_ID_FIELD_NUMBER: builtins.int + ACTIVITY_ID_FIELD_NUMBER: builtins.int + ACTIVITY_TYPE_FIELD_NUMBER: builtins.int + TASK_QUEUE_FIELD_NUMBER: builtins.int + SCHEDULE_TO_CLOSE_TIMEOUT_FIELD_NUMBER: builtins.int + SCHEDULE_TO_START_TIMEOUT_FIELD_NUMBER: builtins.int + START_TO_CLOSE_TIMEOUT_FIELD_NUMBER: builtins.int + HEARTBEAT_TIMEOUT_FIELD_NUMBER: builtins.int + RETRY_POLICY_FIELD_NUMBER: builtins.int + INPUT_FIELD_NUMBER: builtins.int + ID_REUSE_POLICY_FIELD_NUMBER: builtins.int + ID_CONFLICT_POLICY_FIELD_NUMBER: builtins.int + SEARCH_ATTRIBUTES_FIELD_NUMBER: builtins.int + HEADER_FIELD_NUMBER: builtins.int + USER_METADATA_FIELD_NUMBER: builtins.int + PRIORITY_FIELD_NUMBER: builtins.int + COMPLETION_CALLBACKS_FIELD_NUMBER: builtins.int + LINKS_FIELD_NUMBER: builtins.int + ON_CONFLICT_OPTIONS_FIELD_NUMBER: builtins.int + START_DELAY_FIELD_NUMBER: builtins.int + namespace: builtins.str + identity: builtins.str + """The identity of the client who initiated this request""" + request_id: builtins.str + """A unique identifier for this start request. Typically UUIDv4.""" + activity_id: builtins.str + """Identifier for this activity. Required. This identifier should be meaningful in the user's + own system. It must be unique among activities in the same namespace, subject to the rules + imposed by id_reuse_policy and id_conflict_policy. + """ + @property + def activity_type(self) -> temporalio.api.common.v1.message_pb2.ActivityType: + """The type of the activity, a string that corresponds to a registered activity on a worker.""" + @property + def task_queue(self) -> temporalio.api.taskqueue.v1.message_pb2.TaskQueue: + """Task queue to schedule this activity on.""" + @property + def schedule_to_close_timeout(self) -> google.protobuf.duration_pb2.Duration: + """Indicates how long the caller is willing to wait for an activity completion. Limits how long + retries will be attempted. Either this or `start_to_close_timeout` must be specified. + + (-- api-linter: core::0140::prepositions=disabled + aip.dev/not-precedent: "to" is used to indicate interval. --) + """ + @property + def schedule_to_start_timeout(self) -> google.protobuf.duration_pb2.Duration: + """Limits time an activity task can stay in a task queue before a worker picks it up. This + timeout is always non retryable, as all a retry would achieve is to put it back into the same + queue. Defaults to `schedule_to_close_timeout` if not specified. + + (-- api-linter: core::0140::prepositions=disabled + aip.dev/not-precedent: "to" is used to indicate interval. --) + """ + @property + def start_to_close_timeout(self) -> google.protobuf.duration_pb2.Duration: + """Maximum time an activity is allowed to execute after being picked up by a worker. This + timeout is always retryable. Either this or `schedule_to_close_timeout` must be + specified. + + (-- api-linter: core::0140::prepositions=disabled + aip.dev/not-precedent: "to" is used to indicate interval. --) + """ + @property + def heartbeat_timeout(self) -> google.protobuf.duration_pb2.Duration: + """Maximum permitted time between successful worker heartbeats.""" + @property + def retry_policy(self) -> temporalio.api.common.v1.message_pb2.RetryPolicy: + """The retry policy for the activity. Will never exceed `schedule_to_close_timeout`.""" + @property + def input(self) -> temporalio.api.common.v1.message_pb2.Payloads: + """Serialized arguments to the activity. These are passed as arguments to the activity function.""" + id_reuse_policy: ( + temporalio.api.enums.v1.activity_pb2.ActivityIdReusePolicy.ValueType + ) + """Defines whether to allow re-using the activity id from a previously *closed* activity. + The default policy is ACTIVITY_ID_REUSE_POLICY_ALLOW_DUPLICATE. + """ + id_conflict_policy: ( + temporalio.api.enums.v1.activity_pb2.ActivityIdConflictPolicy.ValueType + ) + """Defines how to resolve an activity id conflict with a *running* activity. + The default policy is ACTIVITY_ID_CONFLICT_POLICY_FAIL. + """ + @property + def search_attributes( + self, + ) -> temporalio.api.common.v1.message_pb2.SearchAttributes: + """Search attributes for indexing.""" + @property + def header(self) -> temporalio.api.common.v1.message_pb2.Header: + """Header for context propagation and tracing purposes.""" + @property + def user_metadata(self) -> temporalio.api.sdk.v1.user_metadata_pb2.UserMetadata: + """Metadata for use by user interfaces to display the fixed as-of-start summary and details of the activity.""" + @property + def priority(self) -> temporalio.api.common.v1.message_pb2.Priority: + """Priority metadata.""" + @property + def completion_callbacks( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.common.v1.message_pb2.Callback + ]: + """Callbacks to be called by the server when this activity reaches a terminal state. + Callback addresses must be whitelisted in the server's dynamic configuration. + """ + @property + def links( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.common.v1.message_pb2.Link + ]: + """Links to be associated with the activity. Callbacks may also have associated links; + links already included with a callback should not be duplicated here. + """ + @property + def on_conflict_options( + self, + ) -> temporalio.api.common.v1.message_pb2.OnConflictOptions: + """Options for handling conflicts when using ACTIVITY_ID_CONFLICT_POLICY_USE_EXISTING.""" + @property + def start_delay(self) -> google.protobuf.duration_pb2.Duration: + """Time to wait before making the first activity task available for dispatch. This delay is not applied to retry attempts.""" + def __init__( + self, + *, + namespace: builtins.str = ..., + identity: builtins.str = ..., + request_id: builtins.str = ..., + activity_id: builtins.str = ..., + activity_type: temporalio.api.common.v1.message_pb2.ActivityType | None = ..., + task_queue: temporalio.api.taskqueue.v1.message_pb2.TaskQueue | None = ..., + schedule_to_close_timeout: google.protobuf.duration_pb2.Duration | None = ..., + schedule_to_start_timeout: google.protobuf.duration_pb2.Duration | None = ..., + start_to_close_timeout: google.protobuf.duration_pb2.Duration | None = ..., + heartbeat_timeout: google.protobuf.duration_pb2.Duration | None = ..., + retry_policy: temporalio.api.common.v1.message_pb2.RetryPolicy | None = ..., + input: temporalio.api.common.v1.message_pb2.Payloads | None = ..., + id_reuse_policy: temporalio.api.enums.v1.activity_pb2.ActivityIdReusePolicy.ValueType = ..., + id_conflict_policy: temporalio.api.enums.v1.activity_pb2.ActivityIdConflictPolicy.ValueType = ..., + search_attributes: temporalio.api.common.v1.message_pb2.SearchAttributes + | None = ..., + header: temporalio.api.common.v1.message_pb2.Header | None = ..., + user_metadata: temporalio.api.sdk.v1.user_metadata_pb2.UserMetadata + | None = ..., + priority: temporalio.api.common.v1.message_pb2.Priority | None = ..., + completion_callbacks: collections.abc.Iterable[ + temporalio.api.common.v1.message_pb2.Callback + ] + | None = ..., + links: collections.abc.Iterable[temporalio.api.common.v1.message_pb2.Link] + | None = ..., + on_conflict_options: temporalio.api.common.v1.message_pb2.OnConflictOptions + | None = ..., + start_delay: google.protobuf.duration_pb2.Duration | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "activity_type", + b"activity_type", + "header", + b"header", + "heartbeat_timeout", + b"heartbeat_timeout", + "input", + b"input", + "on_conflict_options", + b"on_conflict_options", + "priority", + b"priority", + "retry_policy", + b"retry_policy", + "schedule_to_close_timeout", + b"schedule_to_close_timeout", + "schedule_to_start_timeout", + b"schedule_to_start_timeout", + "search_attributes", + b"search_attributes", + "start_delay", + b"start_delay", + "start_to_close_timeout", + b"start_to_close_timeout", + "task_queue", + b"task_queue", + "user_metadata", + b"user_metadata", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "activity_id", + b"activity_id", + "activity_type", + b"activity_type", + "completion_callbacks", + b"completion_callbacks", + "header", + b"header", + "heartbeat_timeout", + b"heartbeat_timeout", + "id_conflict_policy", + b"id_conflict_policy", + "id_reuse_policy", + b"id_reuse_policy", + "identity", + b"identity", + "input", + b"input", + "links", + b"links", + "namespace", + b"namespace", + "on_conflict_options", + b"on_conflict_options", + "priority", + b"priority", + "request_id", + b"request_id", + "retry_policy", + b"retry_policy", + "schedule_to_close_timeout", + b"schedule_to_close_timeout", + "schedule_to_start_timeout", + b"schedule_to_start_timeout", + "search_attributes", + b"search_attributes", + "start_delay", + b"start_delay", + "start_to_close_timeout", + b"start_to_close_timeout", + "task_queue", + b"task_queue", + "user_metadata", + b"user_metadata", + ], + ) -> None: ... + +global___StartActivityExecutionRequest = StartActivityExecutionRequest + +class StartActivityExecutionResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + RUN_ID_FIELD_NUMBER: builtins.int + STARTED_FIELD_NUMBER: builtins.int + LINK_FIELD_NUMBER: builtins.int + run_id: builtins.str + """The run ID of the activity that was started - or used (via ACTIVITY_ID_CONFLICT_POLICY_USE_EXISTING).""" + started: builtins.bool + """If true, a new activity was started.""" + @property + def link(self) -> temporalio.api.common.v1.message_pb2.Link: + """Link to the started activity.""" + def __init__( + self, + *, + run_id: builtins.str = ..., + started: builtins.bool = ..., + link: temporalio.api.common.v1.message_pb2.Link | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["link", b"link"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "link", b"link", "run_id", b"run_id", "started", b"started" + ], + ) -> None: ... + +global___StartActivityExecutionResponse = StartActivityExecutionResponse + +class DescribeActivityExecutionRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + ACTIVITY_ID_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + INCLUDE_INPUT_FIELD_NUMBER: builtins.int + INCLUDE_OUTCOME_FIELD_NUMBER: builtins.int + LONG_POLL_TOKEN_FIELD_NUMBER: builtins.int + INCLUDE_HEARTBEAT_DETAILS_FIELD_NUMBER: builtins.int + INCLUDE_LAST_FAILURE_FIELD_NUMBER: builtins.int + namespace: builtins.str + activity_id: builtins.str + run_id: builtins.str + """Activity run ID. If empty the request targets the latest run.""" + include_input: builtins.bool + """Include the input field in the response.""" + include_outcome: builtins.bool + """Include the outcome (result/failure) in the response if the activity has completed.""" + long_poll_token: builtins.bytes + """Token from a previous DescribeActivityExecutionResponse. If present, long-poll until activity + state changes from the state encoded in this token. If absent, return current state immediately. + If present, run_id must also be present. + Note that activity state may change multiple times between requests, therefore it is not + guaranteed that a client making a sequence of long-poll requests will see a complete + sequence of state changes. + """ + include_heartbeat_details: builtins.bool + """Include the heartbeat_details field inside info in the response if available.""" + include_last_failure: builtins.bool + """Include the last_failure field inside info in the response if available.""" + def __init__( + self, + *, + namespace: builtins.str = ..., + activity_id: builtins.str = ..., + run_id: builtins.str = ..., + include_input: builtins.bool = ..., + include_outcome: builtins.bool = ..., + long_poll_token: builtins.bytes = ..., + include_heartbeat_details: builtins.bool = ..., + include_last_failure: builtins.bool = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "activity_id", + b"activity_id", + "include_heartbeat_details", + b"include_heartbeat_details", + "include_input", + b"include_input", + "include_last_failure", + b"include_last_failure", + "include_outcome", + b"include_outcome", + "long_poll_token", + b"long_poll_token", + "namespace", + b"namespace", + "run_id", + b"run_id", + ], + ) -> None: ... + +global___DescribeActivityExecutionRequest = DescribeActivityExecutionRequest + +class DescribeActivityExecutionResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + RUN_ID_FIELD_NUMBER: builtins.int + INFO_FIELD_NUMBER: builtins.int + INPUT_FIELD_NUMBER: builtins.int + OUTCOME_FIELD_NUMBER: builtins.int + LONG_POLL_TOKEN_FIELD_NUMBER: builtins.int + CALLBACKS_FIELD_NUMBER: builtins.int + run_id: builtins.str + """The run ID of the activity, useful when run_id was not specified in the request.""" + @property + def info(self) -> temporalio.api.activity.v1.message_pb2.ActivityExecutionInfo: + """Information about the activity execution. Fields heartbeat_details and last_failure are omitted unless + the request has include_heartbeat_details or include_last_failure set to true, respectively. + """ + @property + def input(self) -> temporalio.api.common.v1.message_pb2.Payloads: + """Serialized activity input, passed as arguments to the activity function. + Only set if include_input was true in the request. + """ + @property + def outcome( + self, + ) -> temporalio.api.activity.v1.message_pb2.ActivityExecutionOutcome: + """Only set if the activity is completed and include_outcome was true in the request.""" + long_poll_token: builtins.bytes + """Token for follow-on long-poll requests. Absent only if the activity is complete.""" + @property + def callbacks( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.activity.v1.message_pb2.CallbackInfo + ]: + """Callbacks attached to this activity execution and their current state.""" + def __init__( + self, + *, + run_id: builtins.str = ..., + info: temporalio.api.activity.v1.message_pb2.ActivityExecutionInfo | None = ..., + input: temporalio.api.common.v1.message_pb2.Payloads | None = ..., + outcome: temporalio.api.activity.v1.message_pb2.ActivityExecutionOutcome + | None = ..., + long_poll_token: builtins.bytes = ..., + callbacks: collections.abc.Iterable[ + temporalio.api.activity.v1.message_pb2.CallbackInfo + ] + | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "info", b"info", "input", b"input", "outcome", b"outcome" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "callbacks", + b"callbacks", + "info", + b"info", + "input", + b"input", + "long_poll_token", + b"long_poll_token", + "outcome", + b"outcome", + "run_id", + b"run_id", + ], + ) -> None: ... + +global___DescribeActivityExecutionResponse = DescribeActivityExecutionResponse + +class PollActivityExecutionRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + ACTIVITY_ID_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + namespace: builtins.str + activity_id: builtins.str + run_id: builtins.str + """Activity run ID. If empty the request targets the latest run.""" + def __init__( + self, + *, + namespace: builtins.str = ..., + activity_id: builtins.str = ..., + run_id: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "activity_id", + b"activity_id", + "namespace", + b"namespace", + "run_id", + b"run_id", + ], + ) -> None: ... + +global___PollActivityExecutionRequest = PollActivityExecutionRequest + +class PollActivityExecutionResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + RUN_ID_FIELD_NUMBER: builtins.int + OUTCOME_FIELD_NUMBER: builtins.int + run_id: builtins.str + """The run ID of the activity, useful when run_id was not specified in the request.""" + @property + def outcome( + self, + ) -> temporalio.api.activity.v1.message_pb2.ActivityExecutionOutcome: ... + def __init__( + self, + *, + run_id: builtins.str = ..., + outcome: temporalio.api.activity.v1.message_pb2.ActivityExecutionOutcome + | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["outcome", b"outcome"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "outcome", b"outcome", "run_id", b"run_id" + ], + ) -> None: ... + +global___PollActivityExecutionResponse = PollActivityExecutionResponse + +class ListActivityExecutionsRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + PAGE_SIZE_FIELD_NUMBER: builtins.int + NEXT_PAGE_TOKEN_FIELD_NUMBER: builtins.int + QUERY_FIELD_NUMBER: builtins.int + namespace: builtins.str + page_size: builtins.int + """Max number of executions to return per page.""" + next_page_token: builtins.bytes + """Token returned in ListActivityExecutionsResponse.""" + query: builtins.str + """Visibility query, see https://docs.temporal.io/list-filter for the syntax.""" + def __init__( + self, + *, + namespace: builtins.str = ..., + page_size: builtins.int = ..., + next_page_token: builtins.bytes = ..., + query: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "namespace", + b"namespace", + "next_page_token", + b"next_page_token", + "page_size", + b"page_size", + "query", + b"query", + ], + ) -> None: ... + +global___ListActivityExecutionsRequest = ListActivityExecutionsRequest + +class ListActivityExecutionsResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + EXECUTIONS_FIELD_NUMBER: builtins.int + NEXT_PAGE_TOKEN_FIELD_NUMBER: builtins.int + @property + def executions( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.activity.v1.message_pb2.ActivityExecutionListInfo + ]: ... + next_page_token: builtins.bytes + """Token to use to fetch the next page. If empty, there is no next page.""" + def __init__( + self, + *, + executions: collections.abc.Iterable[ + temporalio.api.activity.v1.message_pb2.ActivityExecutionListInfo + ] + | None = ..., + next_page_token: builtins.bytes = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "executions", b"executions", "next_page_token", b"next_page_token" + ], + ) -> None: ... + +global___ListActivityExecutionsResponse = ListActivityExecutionsResponse + +class StartNexusOperationExecutionRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class NexusHeaderEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal["key", b"key", "value", b"value"], + ) -> None: ... + + NAMESPACE_FIELD_NUMBER: builtins.int + IDENTITY_FIELD_NUMBER: builtins.int + REQUEST_ID_FIELD_NUMBER: builtins.int + OPERATION_ID_FIELD_NUMBER: builtins.int + ENDPOINT_FIELD_NUMBER: builtins.int + SERVICE_FIELD_NUMBER: builtins.int + OPERATION_FIELD_NUMBER: builtins.int + SCHEDULE_TO_CLOSE_TIMEOUT_FIELD_NUMBER: builtins.int + SCHEDULE_TO_START_TIMEOUT_FIELD_NUMBER: builtins.int + START_TO_CLOSE_TIMEOUT_FIELD_NUMBER: builtins.int + INPUT_FIELD_NUMBER: builtins.int + ID_REUSE_POLICY_FIELD_NUMBER: builtins.int + ID_CONFLICT_POLICY_FIELD_NUMBER: builtins.int + SEARCH_ATTRIBUTES_FIELD_NUMBER: builtins.int + NEXUS_HEADER_FIELD_NUMBER: builtins.int + USER_METADATA_FIELD_NUMBER: builtins.int + namespace: builtins.str + identity: builtins.str + """The identity of the client who initiated this request.""" + request_id: builtins.str + """A unique identifier for this caller-side start request. Typically UUIDv4. + StartOperation requests sent to the handler will use a server-generated request ID. + """ + operation_id: builtins.str + """Identifier for this operation. This is a caller-side ID, distinct from any internal + operation identifiers generated by the handler. Must be unique among operations in the + same namespace, subject to the rules imposed by id_reuse_policy and id_conflict_policy. + """ + endpoint: builtins.str + """Endpoint name, resolved to a URL via the cluster's endpoint registry.""" + service: builtins.str + """Service name.""" + operation: builtins.str + """Operation name.""" + @property + def schedule_to_close_timeout(self) -> google.protobuf.duration_pb2.Duration: + """Schedule-to-close timeout for this operation. + Indicates how long the caller is willing to wait for operation completion. + Calls are retried internally by the server. + (-- api-linter: core::0140::prepositions=disabled + aip.dev/not-precedent: "to" is used to indicate interval. --) + """ + @property + def schedule_to_start_timeout(self) -> google.protobuf.duration_pb2.Duration: + """Schedule-to-start timeout for this operation. + Indicates how long the caller is willing to wait for the operation to be started (or completed if synchronous) + by the handler. + If not set or zero, no schedule-to-start timeout is enforced. + (-- api-linter: core::0140::prepositions=disabled + aip.dev/not-precedent: "to" is used to indicate interval. --) + """ + @property + def start_to_close_timeout(self) -> google.protobuf.duration_pb2.Duration: + """Start-to-close timeout for this operation. + Indicates how long the caller is willing to wait for an asynchronous operation to complete after it has been + started. Synchronous operations ignore this timeout. + If not set or zero, no start-to-close timeout is enforced. + (-- api-linter: core::0140::prepositions=disabled + aip.dev/not-precedent: "to" is used to indicate interval. --) + """ + @property + def input(self) -> temporalio.api.common.v1.message_pb2.Payload: + """Serialized input to the operation. Passed as the request payload.""" + id_reuse_policy: ( + temporalio.api.enums.v1.nexus_pb2.NexusOperationIdReusePolicy.ValueType + ) + """Defines whether to allow re-using the operation id from a previously *closed* operation. + The default policy is NEXUS_OPERATION_ID_REUSE_POLICY_ALLOW_DUPLICATE. + """ + id_conflict_policy: ( + temporalio.api.enums.v1.nexus_pb2.NexusOperationIdConflictPolicy.ValueType + ) + """Defines how to resolve an operation id conflict with a *running* operation. + The default policy is NEXUS_OPERATION_ID_CONFLICT_POLICY_FAIL. + """ + @property + def search_attributes( + self, + ) -> temporalio.api.common.v1.message_pb2.SearchAttributes: + """Search attributes for indexing.""" + @property + def nexus_header( + self, + ) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: + """Header to attach to the Nexus request. + Users are responsible for encrypting sensitive data in this header as it is stored in workflow history and + transmitted to external services as-is. + This is useful for propagating tracing information. + Note these headers are not the same as Temporal headers on internal activities and child workflows, these are + transmitted to Nexus operations that may be external and are not traditional payloads. + """ + @property + def user_metadata(self) -> temporalio.api.sdk.v1.user_metadata_pb2.UserMetadata: + """Metadata for use by user interfaces to display the fixed as-of-start summary and details of the operation.""" + def __init__( + self, + *, + namespace: builtins.str = ..., + identity: builtins.str = ..., + request_id: builtins.str = ..., + operation_id: builtins.str = ..., + endpoint: builtins.str = ..., + service: builtins.str = ..., + operation: builtins.str = ..., + schedule_to_close_timeout: google.protobuf.duration_pb2.Duration | None = ..., + schedule_to_start_timeout: google.protobuf.duration_pb2.Duration | None = ..., + start_to_close_timeout: google.protobuf.duration_pb2.Duration | None = ..., + input: temporalio.api.common.v1.message_pb2.Payload | None = ..., + id_reuse_policy: temporalio.api.enums.v1.nexus_pb2.NexusOperationIdReusePolicy.ValueType = ..., + id_conflict_policy: temporalio.api.enums.v1.nexus_pb2.NexusOperationIdConflictPolicy.ValueType = ..., + search_attributes: temporalio.api.common.v1.message_pb2.SearchAttributes + | None = ..., + nexus_header: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + user_metadata: temporalio.api.sdk.v1.user_metadata_pb2.UserMetadata + | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "input", + b"input", + "schedule_to_close_timeout", + b"schedule_to_close_timeout", + "schedule_to_start_timeout", + b"schedule_to_start_timeout", + "search_attributes", + b"search_attributes", + "start_to_close_timeout", + b"start_to_close_timeout", + "user_metadata", + b"user_metadata", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "endpoint", + b"endpoint", + "id_conflict_policy", + b"id_conflict_policy", + "id_reuse_policy", + b"id_reuse_policy", + "identity", + b"identity", + "input", + b"input", + "namespace", + b"namespace", + "nexus_header", + b"nexus_header", + "operation", + b"operation", + "operation_id", + b"operation_id", + "request_id", + b"request_id", + "schedule_to_close_timeout", + b"schedule_to_close_timeout", + "schedule_to_start_timeout", + b"schedule_to_start_timeout", + "search_attributes", + b"search_attributes", + "service", + b"service", + "start_to_close_timeout", + b"start_to_close_timeout", + "user_metadata", + b"user_metadata", + ], + ) -> None: ... + +global___StartNexusOperationExecutionRequest = StartNexusOperationExecutionRequest + +class StartNexusOperationExecutionResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + RUN_ID_FIELD_NUMBER: builtins.int + STARTED_FIELD_NUMBER: builtins.int + run_id: builtins.str + """The run ID of the operation that was started - or used (via NEXUS_OPERATION_ID_CONFLICT_POLICY_USE_EXISTING).""" + started: builtins.bool + """If true, a new operation was started.""" + def __init__( + self, + *, + run_id: builtins.str = ..., + started: builtins.bool = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "run_id", b"run_id", "started", b"started" + ], + ) -> None: ... + +global___StartNexusOperationExecutionResponse = StartNexusOperationExecutionResponse + +class DescribeNexusOperationExecutionRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + OPERATION_ID_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + INCLUDE_INPUT_FIELD_NUMBER: builtins.int + INCLUDE_OUTCOME_FIELD_NUMBER: builtins.int + LONG_POLL_TOKEN_FIELD_NUMBER: builtins.int + namespace: builtins.str + operation_id: builtins.str + run_id: builtins.str + """Operation run ID. If empty the request targets the latest run.""" + include_input: builtins.bool + """Include the input field in the response.""" + include_outcome: builtins.bool + """Include the outcome (result/failure) in the response if the operation has completed.""" + long_poll_token: builtins.bytes + """Token from a previous DescribeNexusOperationExecutionResponse. If present, this RPC will long-poll until operation + state changes from the state encoded in this token. If absent, return current state immediately. + If present, run_id must also be present. + Note that operation state may change multiple times between requests, therefore it is not + guaranteed that a client making a sequence of long-poll requests will see a complete + sequence of state changes. + """ + def __init__( + self, + *, + namespace: builtins.str = ..., + operation_id: builtins.str = ..., + run_id: builtins.str = ..., + include_input: builtins.bool = ..., + include_outcome: builtins.bool = ..., + long_poll_token: builtins.bytes = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "include_input", + b"include_input", + "include_outcome", + b"include_outcome", + "long_poll_token", + b"long_poll_token", + "namespace", + b"namespace", + "operation_id", + b"operation_id", + "run_id", + b"run_id", + ], + ) -> None: ... + +global___DescribeNexusOperationExecutionRequest = DescribeNexusOperationExecutionRequest + +class DescribeNexusOperationExecutionResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + RUN_ID_FIELD_NUMBER: builtins.int + INFO_FIELD_NUMBER: builtins.int + INPUT_FIELD_NUMBER: builtins.int + RESULT_FIELD_NUMBER: builtins.int + FAILURE_FIELD_NUMBER: builtins.int + LONG_POLL_TOKEN_FIELD_NUMBER: builtins.int + run_id: builtins.str + """The run ID of the operation, useful when run_id was not specified in the request.""" + @property + def info(self) -> temporalio.api.nexus.v1.message_pb2.NexusOperationExecutionInfo: + """Information about the operation.""" + @property + def input(self) -> temporalio.api.common.v1.message_pb2.Payload: + """Serialized operation input, passed as the request payload. + Only set if include_input was true in the request. + """ + @property + def result(self) -> temporalio.api.common.v1.message_pb2.Payload: + """The result if the operation completed successfully.""" + @property + def failure(self) -> temporalio.api.failure.v1.message_pb2.Failure: + """The failure if the operation completed unsuccessfully.""" + long_poll_token: builtins.bytes + """Token for follow-on long-poll requests. Absent only if the operation is complete.""" + def __init__( + self, + *, + run_id: builtins.str = ..., + info: temporalio.api.nexus.v1.message_pb2.NexusOperationExecutionInfo + | None = ..., + input: temporalio.api.common.v1.message_pb2.Payload | None = ..., + result: temporalio.api.common.v1.message_pb2.Payload | None = ..., + failure: temporalio.api.failure.v1.message_pb2.Failure | None = ..., + long_poll_token: builtins.bytes = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "failure", + b"failure", + "info", + b"info", + "input", + b"input", + "outcome", + b"outcome", + "result", + b"result", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "failure", + b"failure", + "info", + b"info", + "input", + b"input", + "long_poll_token", + b"long_poll_token", + "outcome", + b"outcome", + "result", + b"result", + "run_id", + b"run_id", + ], + ) -> None: ... + def WhichOneof( + self, oneof_group: typing_extensions.Literal["outcome", b"outcome"] + ) -> typing_extensions.Literal["result", "failure"] | None: ... + +global___DescribeNexusOperationExecutionResponse = ( + DescribeNexusOperationExecutionResponse +) + +class PollNexusOperationExecutionRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + OPERATION_ID_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + WAIT_STAGE_FIELD_NUMBER: builtins.int + namespace: builtins.str + operation_id: builtins.str + run_id: builtins.str + """Operation run ID. If empty the request targets the latest run.""" + wait_stage: temporalio.api.enums.v1.nexus_pb2.NexusOperationWaitStage.ValueType + """Stage to wait for. The operation may be in a more advanced stage when the poll is unblocked.""" + def __init__( + self, + *, + namespace: builtins.str = ..., + operation_id: builtins.str = ..., + run_id: builtins.str = ..., + wait_stage: temporalio.api.enums.v1.nexus_pb2.NexusOperationWaitStage.ValueType = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "namespace", + b"namespace", + "operation_id", + b"operation_id", + "run_id", + b"run_id", + "wait_stage", + b"wait_stage", + ], + ) -> None: ... + +global___PollNexusOperationExecutionRequest = PollNexusOperationExecutionRequest + +class PollNexusOperationExecutionResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + RUN_ID_FIELD_NUMBER: builtins.int + WAIT_STAGE_FIELD_NUMBER: builtins.int + OPERATION_TOKEN_FIELD_NUMBER: builtins.int + RESULT_FIELD_NUMBER: builtins.int + FAILURE_FIELD_NUMBER: builtins.int + run_id: builtins.str + """The run ID of the operation, useful when run_id was not specified in the request.""" + wait_stage: temporalio.api.enums.v1.nexus_pb2.NexusOperationWaitStage.ValueType + """The current stage of the operation. May be more advanced than the stage requested in the poll.""" + operation_token: builtins.str + """Operation token. Only populated for asynchronous operations after a successful StartOperation call.""" + @property + def result(self) -> temporalio.api.common.v1.message_pb2.Payload: + """The result if the operation completed successfully.""" + @property + def failure(self) -> temporalio.api.failure.v1.message_pb2.Failure: + """The failure if the operation completed unsuccessfully.""" + def __init__( + self, + *, + run_id: builtins.str = ..., + wait_stage: temporalio.api.enums.v1.nexus_pb2.NexusOperationWaitStage.ValueType = ..., + operation_token: builtins.str = ..., + result: temporalio.api.common.v1.message_pb2.Payload | None = ..., + failure: temporalio.api.failure.v1.message_pb2.Failure | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "failure", b"failure", "outcome", b"outcome", "result", b"result" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "failure", + b"failure", + "operation_token", + b"operation_token", + "outcome", + b"outcome", + "result", + b"result", + "run_id", + b"run_id", + "wait_stage", + b"wait_stage", + ], + ) -> None: ... + def WhichOneof( + self, oneof_group: typing_extensions.Literal["outcome", b"outcome"] + ) -> typing_extensions.Literal["result", "failure"] | None: ... + +global___PollNexusOperationExecutionResponse = PollNexusOperationExecutionResponse + +class ListNexusOperationExecutionsRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + PAGE_SIZE_FIELD_NUMBER: builtins.int + NEXT_PAGE_TOKEN_FIELD_NUMBER: builtins.int + QUERY_FIELD_NUMBER: builtins.int + namespace: builtins.str + page_size: builtins.int + """Max number of operations to return per page.""" + next_page_token: builtins.bytes + """Token returned in ListNexusOperationExecutionsResponse.""" + query: builtins.str + """Visibility query, see https://docs.temporal.io/list-filter for the syntax. + Search attributes that are avaialble for Nexus operations include: + - OperationId + - RunId + - Endpoint + - Service + - Operation + - RequestId + - StartTime + - ExecutionTime + - CloseTime + - ExecutionStatus + - ExecutionDuration + - StateTransitionCount + """ + def __init__( + self, + *, + namespace: builtins.str = ..., + page_size: builtins.int = ..., + next_page_token: builtins.bytes = ..., + query: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "namespace", + b"namespace", + "next_page_token", + b"next_page_token", + "page_size", + b"page_size", + "query", + b"query", + ], + ) -> None: ... + +global___ListNexusOperationExecutionsRequest = ListNexusOperationExecutionsRequest + +class ListNexusOperationExecutionsResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + OPERATIONS_FIELD_NUMBER: builtins.int + NEXT_PAGE_TOKEN_FIELD_NUMBER: builtins.int + @property + def operations( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.nexus.v1.message_pb2.NexusOperationExecutionListInfo + ]: ... + next_page_token: builtins.bytes + """Token to use to fetch the next page. If empty, there is no next page.""" + def __init__( + self, + *, + operations: collections.abc.Iterable[ + temporalio.api.nexus.v1.message_pb2.NexusOperationExecutionListInfo + ] + | None = ..., + next_page_token: builtins.bytes = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "next_page_token", b"next_page_token", "operations", b"operations" + ], + ) -> None: ... + +global___ListNexusOperationExecutionsResponse = ListNexusOperationExecutionsResponse + +class CountActivityExecutionsRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + QUERY_FIELD_NUMBER: builtins.int + namespace: builtins.str + query: builtins.str + """Visibility query, see https://docs.temporal.io/list-filter for the syntax.""" + def __init__( + self, + *, + namespace: builtins.str = ..., + query: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "namespace", b"namespace", "query", b"query" + ], + ) -> None: ... + +global___CountActivityExecutionsRequest = CountActivityExecutionsRequest + +class CountActivityExecutionsResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class AggregationGroup(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + GROUP_VALUES_FIELD_NUMBER: builtins.int + COUNT_FIELD_NUMBER: builtins.int + @property + def group_values( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.common.v1.message_pb2.Payload + ]: ... + count: builtins.int + def __init__( + self, + *, + group_values: collections.abc.Iterable[ + temporalio.api.common.v1.message_pb2.Payload + ] + | None = ..., + count: builtins.int = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "count", b"count", "group_values", b"group_values" + ], + ) -> None: ... + + COUNT_FIELD_NUMBER: builtins.int + GROUPS_FIELD_NUMBER: builtins.int + count: builtins.int + """If `query` is not grouping by any field, the count is an approximate number + of activities that match the query. + If `query` is grouping by a field, the count is simply the sum of the counts + of the groups returned in the response. This number can be smaller than the + total number of activities matching the query. + """ + @property + def groups( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + global___CountActivityExecutionsResponse.AggregationGroup + ]: + """Contains the groups if the request is grouping by a field. + The list might not be complete, and the counts of each group is approximate. + """ + def __init__( + self, + *, + count: builtins.int = ..., + groups: collections.abc.Iterable[ + global___CountActivityExecutionsResponse.AggregationGroup + ] + | None = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal["count", b"count", "groups", b"groups"], + ) -> None: ... + +global___CountActivityExecutionsResponse = CountActivityExecutionsResponse + +class CountNexusOperationExecutionsRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + QUERY_FIELD_NUMBER: builtins.int + namespace: builtins.str + query: builtins.str + """Visibility query, see https://docs.temporal.io/list-filter for the syntax. + See also ListNexusOperationExecutionsRequest for search attributes available for Nexus operations. + """ + def __init__( + self, + *, + namespace: builtins.str = ..., + query: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "namespace", b"namespace", "query", b"query" + ], + ) -> None: ... + +global___CountNexusOperationExecutionsRequest = CountNexusOperationExecutionsRequest + +class CountNexusOperationExecutionsResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class AggregationGroup(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + GROUP_VALUES_FIELD_NUMBER: builtins.int + COUNT_FIELD_NUMBER: builtins.int + @property + def group_values( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.common.v1.message_pb2.Payload + ]: ... + count: builtins.int + def __init__( + self, + *, + group_values: collections.abc.Iterable[ + temporalio.api.common.v1.message_pb2.Payload + ] + | None = ..., + count: builtins.int = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "count", b"count", "group_values", b"group_values" + ], + ) -> None: ... + + COUNT_FIELD_NUMBER: builtins.int + GROUPS_FIELD_NUMBER: builtins.int + count: builtins.int + """If `query` is not grouping by any field, the count is an approximate number + of operations that match the query. + If `query` is grouping by a field, the count is simply the sum of the counts + of the groups returned in the response. This number can be smaller than the + total number of operations matching the query. + """ + @property + def groups( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + global___CountNexusOperationExecutionsResponse.AggregationGroup + ]: + """Contains the groups if the request is grouping by a field. + The list might not be complete, and the counts of each group is approximate. + """ + def __init__( + self, + *, + count: builtins.int = ..., + groups: collections.abc.Iterable[ + global___CountNexusOperationExecutionsResponse.AggregationGroup + ] + | None = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal["count", b"count", "groups", b"groups"], + ) -> None: ... + +global___CountNexusOperationExecutionsResponse = CountNexusOperationExecutionsResponse + +class RequestCancelActivityExecutionRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + ACTIVITY_ID_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + IDENTITY_FIELD_NUMBER: builtins.int + REQUEST_ID_FIELD_NUMBER: builtins.int + REASON_FIELD_NUMBER: builtins.int + namespace: builtins.str + activity_id: builtins.str + run_id: builtins.str + """Activity run ID. If empty, targets the latest run.""" + identity: builtins.str + """The identity of the worker/client.""" + request_id: builtins.str + """Used to de-dupe cancellation requests.""" + reason: builtins.str + """Reason for requesting the cancellation, recorded and available via the PollActivityExecution API. + Not propagated to a worker if an activity attempt is currently running. + """ + def __init__( + self, + *, + namespace: builtins.str = ..., + activity_id: builtins.str = ..., + run_id: builtins.str = ..., + identity: builtins.str = ..., + request_id: builtins.str = ..., + reason: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "activity_id", + b"activity_id", + "identity", + b"identity", + "namespace", + b"namespace", + "reason", + b"reason", + "request_id", + b"request_id", + "run_id", + b"run_id", + ], + ) -> None: ... + +global___RequestCancelActivityExecutionRequest = RequestCancelActivityExecutionRequest + +class RequestCancelActivityExecutionResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___RequestCancelActivityExecutionResponse = RequestCancelActivityExecutionResponse + +class TerminateActivityExecutionRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + ACTIVITY_ID_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + IDENTITY_FIELD_NUMBER: builtins.int + REQUEST_ID_FIELD_NUMBER: builtins.int + REASON_FIELD_NUMBER: builtins.int + namespace: builtins.str + activity_id: builtins.str + run_id: builtins.str + """Activity run ID. If empty, targets the latest run.""" + identity: builtins.str + """The identity of the worker/client.""" + request_id: builtins.str + """Used to de-dupe termination requests.""" + reason: builtins.str + """Reason for requesting the termination, recorded in in the activity's result failure outcome.""" + def __init__( + self, + *, + namespace: builtins.str = ..., + activity_id: builtins.str = ..., + run_id: builtins.str = ..., + identity: builtins.str = ..., + request_id: builtins.str = ..., + reason: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "activity_id", + b"activity_id", + "identity", + b"identity", + "namespace", + b"namespace", + "reason", + b"reason", + "request_id", + b"request_id", + "run_id", + b"run_id", + ], + ) -> None: ... + +global___TerminateActivityExecutionRequest = TerminateActivityExecutionRequest + +class TerminateActivityExecutionResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___TerminateActivityExecutionResponse = TerminateActivityExecutionResponse + +class DeleteActivityExecutionRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + ACTIVITY_ID_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + namespace: builtins.str + activity_id: builtins.str + run_id: builtins.str + """Activity run ID, targets the latest run if run_id is empty.""" + def __init__( + self, + *, + namespace: builtins.str = ..., + activity_id: builtins.str = ..., + run_id: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "activity_id", + b"activity_id", + "namespace", + b"namespace", + "run_id", + b"run_id", + ], + ) -> None: ... + +global___DeleteActivityExecutionRequest = DeleteActivityExecutionRequest + +class DeleteActivityExecutionResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___DeleteActivityExecutionResponse = DeleteActivityExecutionResponse + +class RequestCancelNexusOperationExecutionRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + OPERATION_ID_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + IDENTITY_FIELD_NUMBER: builtins.int + REQUEST_ID_FIELD_NUMBER: builtins.int + REASON_FIELD_NUMBER: builtins.int + namespace: builtins.str + operation_id: builtins.str + run_id: builtins.str + """Operation run ID, targets the latest run if empty.""" + identity: builtins.str + """The identity of the client who initiated this request.""" + request_id: builtins.str + """Used to de-dupe cancellation requests.""" + reason: builtins.str + """Reason for requesting the cancellation, recorded and available via the DescribeNexusOperationExecution API.""" + def __init__( + self, + *, + namespace: builtins.str = ..., + operation_id: builtins.str = ..., + run_id: builtins.str = ..., + identity: builtins.str = ..., + request_id: builtins.str = ..., + reason: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "identity", + b"identity", + "namespace", + b"namespace", + "operation_id", + b"operation_id", + "reason", + b"reason", + "request_id", + b"request_id", + "run_id", + b"run_id", + ], + ) -> None: ... + +global___RequestCancelNexusOperationExecutionRequest = ( + RequestCancelNexusOperationExecutionRequest +) + +class RequestCancelNexusOperationExecutionResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___RequestCancelNexusOperationExecutionResponse = ( + RequestCancelNexusOperationExecutionResponse +) + +class TerminateNexusOperationExecutionRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + OPERATION_ID_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + IDENTITY_FIELD_NUMBER: builtins.int + REQUEST_ID_FIELD_NUMBER: builtins.int + REASON_FIELD_NUMBER: builtins.int + namespace: builtins.str + operation_id: builtins.str + run_id: builtins.str + """Operation run ID, targets the latest run if empty.""" + identity: builtins.str + """The identity of the client who initiated this request.""" + request_id: builtins.str + """Used to de-dupe termination requests.""" + reason: builtins.str + """Reason for requesting the termination, recorded in the operation's result failure outcome.""" + def __init__( + self, + *, + namespace: builtins.str = ..., + operation_id: builtins.str = ..., + run_id: builtins.str = ..., + identity: builtins.str = ..., + request_id: builtins.str = ..., + reason: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "identity", + b"identity", + "namespace", + b"namespace", + "operation_id", + b"operation_id", + "reason", + b"reason", + "request_id", + b"request_id", + "run_id", + b"run_id", + ], + ) -> None: ... + +global___TerminateNexusOperationExecutionRequest = ( + TerminateNexusOperationExecutionRequest +) + +class TerminateNexusOperationExecutionResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___TerminateNexusOperationExecutionResponse = ( + TerminateNexusOperationExecutionResponse +) + +class DeleteNexusOperationExecutionRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + OPERATION_ID_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + namespace: builtins.str + operation_id: builtins.str + run_id: builtins.str + """Operation run ID, targets the latest run if empty.""" + def __init__( + self, + *, + namespace: builtins.str = ..., + operation_id: builtins.str = ..., + run_id: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "namespace", + b"namespace", + "operation_id", + b"operation_id", + "run_id", + b"run_id", + ], + ) -> None: ... + +global___DeleteNexusOperationExecutionRequest = DeleteNexusOperationExecutionRequest + +class DeleteNexusOperationExecutionResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___DeleteNexusOperationExecutionResponse = DeleteNexusOperationExecutionResponse + +class PollWorkflowExecutionTimeSkippingRequest(google.protobuf.message.Message): + """A long-poll request that blocks according to a time-skipping waiting policy on the workflow + execution. Currently the only supported policy is waiting for completion of the fast-forward + identified by `fast_forward_id`; the poll also returns once anything else settles that outcome + (e.g. the execution ends or time skipping is disabled). + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + WORKFLOW_EXECUTION_FIELD_NUMBER: builtins.int + FAST_FORWARD_ID_FIELD_NUMBER: builtins.int + namespace: builtins.str + @property + def workflow_execution( + self, + ) -> temporalio.api.common.v1.message_pb2.WorkflowExecution: ... + fast_forward_id: builtins.str + """Required. Identifies the fast-forward whose completion the caller wants to wait for. + Must match the `fast_forward_id` set in the execution's TimeSkippingConfig. + """ + def __init__( + self, + *, + namespace: builtins.str = ..., + workflow_execution: temporalio.api.common.v1.message_pb2.WorkflowExecution + | None = ..., + fast_forward_id: builtins.str = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "workflow_execution", b"workflow_execution" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "fast_forward_id", + b"fast_forward_id", + "namespace", + b"namespace", + "workflow_execution", + b"workflow_execution", + ], + ) -> None: ... + +global___PollWorkflowExecutionTimeSkippingRequest = ( + PollWorkflowExecutionTimeSkippingRequest +) + +class PollWorkflowExecutionTimeSkippingResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FAST_FORWARD_POLLING_RESULT_FIELD_NUMBER: builtins.int + FAILED_REASON_FIELD_NUMBER: builtins.int + FAST_FORWARD_INFO_FIELD_NUMBER: builtins.int + fast_forward_polling_result: ( + temporalio.api.enums.v1.time_skipping_pb2.FastForwardPollingResult.ValueType + ) + """The outcome of the poll for the fast-forward identified by the request's `fast_forward_id`.""" + failed_reason: builtins.str + """Set only when the result is FAST_FORWARD_POLLING_RESULT_FAST_FORWARD_FAILED; explains why + the fast-forward can no longer complete. + """ + @property + def fast_forward_info( + self, + ) -> temporalio.api.common.v1.message_pb2.TimeSkippingFastForwardInfo: + """The execution's current fast-forward, if any.""" + def __init__( + self, + *, + fast_forward_polling_result: temporalio.api.enums.v1.time_skipping_pb2.FastForwardPollingResult.ValueType = ..., + failed_reason: builtins.str = ..., + fast_forward_info: temporalio.api.common.v1.message_pb2.TimeSkippingFastForwardInfo + | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "fast_forward_info", b"fast_forward_info" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "failed_reason", + b"failed_reason", + "fast_forward_info", + b"fast_forward_info", + "fast_forward_polling_result", + b"fast_forward_polling_result", + ], + ) -> None: ... + +global___PollWorkflowExecutionTimeSkippingResponse = ( + PollWorkflowExecutionTimeSkippingResponse +) diff --git a/temporalio/api/workflowservice/v1/service_pb2.py b/temporalio/api/workflowservice/v1/service_pb2.py index 5cc1619ef..92da5d6c2 100644 --- a/temporalio/api/workflowservice/v1/service_pb2.py +++ b/temporalio/api/workflowservice/v1/service_pb2.py @@ -16,12 +16,18 @@ from google.api import annotations_pb2 as google_dot_api_dot_annotations__pb2 +from temporalio.api.dependencies.nexusannotations.v1 import ( + options_pb2 as nexusannotations_dot_v1_dot_options__pb2, +) +from temporalio.api.protometa.v1 import ( + annotations_pb2 as temporal_dot_api_dot_protometa_dot_v1_dot_annotations__pb2, +) from temporalio.api.workflowservice.v1 import ( request_response_pb2 as temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2, ) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n-temporal/api/workflowservice/v1/service.proto\x12\x1ftemporal.api.workflowservice.v1\x1a\x36temporal/api/workflowservice/v1/request_response.proto\x1a\x1cgoogle/api/annotations.proto2\x8f\xbd\x01\n\x0fWorkflowService\x12\xc3\x01\n\x11RegisterNamespace\x12\x39.temporal.api.workflowservice.v1.RegisterNamespaceRequest\x1a:.temporal.api.workflowservice.v1.RegisterNamespaceResponse"7\x82\xd3\xe4\x93\x02\x31"\x13/cluster/namespaces:\x01*Z\x17"\x12/api/v1/namespaces:\x01*\x12\xd5\x01\n\x11\x44\x65scribeNamespace\x12\x39.temporal.api.workflowservice.v1.DescribeNamespaceRequest\x1a:.temporal.api.workflowservice.v1.DescribeNamespaceResponse"I\x82\xd3\xe4\x93\x02\x43\x12\x1f/cluster/namespaces/{namespace}Z \x12\x1e/api/v1/namespaces/{namespace}\x12\xb4\x01\n\x0eListNamespaces\x12\x36.temporal.api.workflowservice.v1.ListNamespacesRequest\x1a\x37.temporal.api.workflowservice.v1.ListNamespacesResponse"1\x82\xd3\xe4\x93\x02+\x12\x13/cluster/namespacesZ\x14\x12\x12/api/v1/namespaces\x12\xe3\x01\n\x0fUpdateNamespace\x12\x37.temporal.api.workflowservice.v1.UpdateNamespaceRequest\x1a\x38.temporal.api.workflowservice.v1.UpdateNamespaceResponse"]\x82\xd3\xe4\x93\x02W"&/cluster/namespaces/{namespace}/update:\x01*Z*"%/api/v1/namespaces/{namespace}/update:\x01*\x12\x8f\x01\n\x12\x44\x65precateNamespace\x12:.temporal.api.workflowservice.v1.DeprecateNamespaceRequest\x1a;.temporal.api.workflowservice.v1.DeprecateNamespaceResponse"\x00\x12\x92\x02\n\x16StartWorkflowExecution\x12>.temporal.api.workflowservice.v1.StartWorkflowExecutionRequest\x1a?.temporal.api.workflowservice.v1.StartWorkflowExecutionResponse"w\x82\xd3\xe4\x93\x02q"//namespaces/{namespace}/workflows/{workflow_id}:\x01*Z;"6/api/v1/namespaces/{namespace}/workflows/{workflow_id}:\x01*\x12\xa5\x02\n\x15\x45xecuteMultiOperation\x12=.temporal.api.workflowservice.v1.ExecuteMultiOperationRequest\x1a>.temporal.api.workflowservice.v1.ExecuteMultiOperationResponse"\x8c\x01\x82\xd3\xe4\x93\x02\x85\x01"9/namespaces/{namespace}/workflows/execute-multi-operation:\x01*ZE"@/api/v1/namespaces/{namespace}/workflows/execute-multi-operation:\x01*\x12\xc1\x02\n\x1bGetWorkflowExecutionHistory\x12\x43.temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryRequest\x1a\x44.temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryResponse"\x96\x01\x82\xd3\xe4\x93\x02\x8f\x01\x12\x41/namespaces/{namespace}/workflows/{execution.workflow_id}/historyZJ\x12H/api/v1/namespaces/{namespace}/workflows/{execution.workflow_id}/history\x12\xe6\x02\n"GetWorkflowExecutionHistoryReverse\x12J.temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryReverseRequest\x1aK.temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryReverseResponse"\xa6\x01\x82\xd3\xe4\x93\x02\x9f\x01\x12I/namespaces/{namespace}/workflows/{execution.workflow_id}/history-reverseZR\x12P/api/v1/namespaces/{namespace}/workflows/{execution.workflow_id}/history-reverse\x12\x98\x01\n\x15PollWorkflowTaskQueue\x12=.temporal.api.workflowservice.v1.PollWorkflowTaskQueueRequest\x1a>.temporal.api.workflowservice.v1.PollWorkflowTaskQueueResponse"\x00\x12\xad\x01\n\x1cRespondWorkflowTaskCompleted\x12\x44.temporal.api.workflowservice.v1.RespondWorkflowTaskCompletedRequest\x1a\x45.temporal.api.workflowservice.v1.RespondWorkflowTaskCompletedResponse"\x00\x12\xa4\x01\n\x19RespondWorkflowTaskFailed\x12\x41.temporal.api.workflowservice.v1.RespondWorkflowTaskFailedRequest\x1a\x42.temporal.api.workflowservice.v1.RespondWorkflowTaskFailedResponse"\x00\x12\x98\x01\n\x15PollActivityTaskQueue\x12=.temporal.api.workflowservice.v1.PollActivityTaskQueueRequest\x1a>.temporal.api.workflowservice.v1.PollActivityTaskQueueResponse"\x00\x12\x9b\x02\n\x1bRecordActivityTaskHeartbeat\x12\x43.temporal.api.workflowservice.v1.RecordActivityTaskHeartbeatRequest\x1a\x44.temporal.api.workflowservice.v1.RecordActivityTaskHeartbeatResponse"q\x82\xd3\xe4\x93\x02k",/namespaces/{namespace}/activities/heartbeat:\x01*Z8"3/api/v1/namespaces/{namespace}/activities/heartbeat:\x01*\x12\xb3\x02\n\x1fRecordActivityTaskHeartbeatById\x12G.temporal.api.workflowservice.v1.RecordActivityTaskHeartbeatByIdRequest\x1aH.temporal.api.workflowservice.v1.RecordActivityTaskHeartbeatByIdResponse"}\x82\xd3\xe4\x93\x02w"2/namespaces/{namespace}/activities/heartbeat-by-id:\x01*Z>"9/api/v1/namespaces/{namespace}/activities/heartbeat-by-id:\x01*\x12\x9c\x02\n\x1cRespondActivityTaskCompleted\x12\x44.temporal.api.workflowservice.v1.RespondActivityTaskCompletedRequest\x1a\x45.temporal.api.workflowservice.v1.RespondActivityTaskCompletedResponse"o\x82\xd3\xe4\x93\x02i"+/namespaces/{namespace}/activities/complete:\x01*Z7"2/api/v1/namespaces/{namespace}/activities/complete:\x01*\x12\xb4\x02\n RespondActivityTaskCompletedById\x12H.temporal.api.workflowservice.v1.RespondActivityTaskCompletedByIdRequest\x1aI.temporal.api.workflowservice.v1.RespondActivityTaskCompletedByIdResponse"{\x82\xd3\xe4\x93\x02u"1/namespaces/{namespace}/activities/complete-by-id:\x01*Z="8/api/v1/namespaces/{namespace}/activities/complete-by-id:\x01*\x12\x8b\x02\n\x19RespondActivityTaskFailed\x12\x41.temporal.api.workflowservice.v1.RespondActivityTaskFailedRequest\x1a\x42.temporal.api.workflowservice.v1.RespondActivityTaskFailedResponse"g\x82\xd3\xe4\x93\x02\x61"\'/namespaces/{namespace}/activities/fail:\x01*Z3"./api/v1/namespaces/{namespace}/activities/fail:\x01*\x12\xa3\x02\n\x1dRespondActivityTaskFailedById\x12\x45.temporal.api.workflowservice.v1.RespondActivityTaskFailedByIdRequest\x1a\x46.temporal.api.workflowservice.v1.RespondActivityTaskFailedByIdResponse"s\x82\xd3\xe4\x93\x02m"-/namespaces/{namespace}/activities/fail-by-id:\x01*Z9"4/api/v1/namespaces/{namespace}/activities/fail-by-id:\x01*\x12\x95\x02\n\x1bRespondActivityTaskCanceled\x12\x43.temporal.api.workflowservice.v1.RespondActivityTaskCanceledRequest\x1a\x44.temporal.api.workflowservice.v1.RespondActivityTaskCanceledResponse"k\x82\xd3\xe4\x93\x02\x65")/namespaces/{namespace}/activities/cancel:\x01*Z5"0/api/v1/namespaces/{namespace}/activities/cancel:\x01*\x12\xad\x02\n\x1fRespondActivityTaskCanceledById\x12G.temporal.api.workflowservice.v1.RespondActivityTaskCanceledByIdRequest\x1aH.temporal.api.workflowservice.v1.RespondActivityTaskCanceledByIdResponse"w\x82\xd3\xe4\x93\x02q"//namespaces/{namespace}/activities/cancel-by-id:\x01*Z;"6/api/v1/namespaces/{namespace}/activities/cancel-by-id:\x01*\x12\xe0\x02\n\x1eRequestCancelWorkflowExecution\x12\x46.temporal.api.workflowservice.v1.RequestCancelWorkflowExecutionRequest\x1aG.temporal.api.workflowservice.v1.RequestCancelWorkflowExecutionResponse"\xac\x01\x82\xd3\xe4\x93\x02\xa5\x01"I/namespaces/{namespace}/workflows/{workflow_execution.workflow_id}/cancel:\x01*ZU"P/api/v1/namespaces/{namespace}/workflows/{workflow_execution.workflow_id}/cancel:\x01*\x12\xe7\x02\n\x17SignalWorkflowExecution\x12?.temporal.api.workflowservice.v1.SignalWorkflowExecutionRequest\x1a@.temporal.api.workflowservice.v1.SignalWorkflowExecutionResponse"\xc8\x01\x82\xd3\xe4\x93\x02\xc1\x01"W/namespaces/{namespace}/workflows/{workflow_execution.workflow_id}/signal/{signal_name}:\x01*Zc"^/api/v1/namespaces/{namespace}/workflows/{workflow_execution.workflow_id}/signal/{signal_name}:\x01*\x12\xf2\x02\n SignalWithStartWorkflowExecution\x12H.temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest\x1aI.temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse"\xb8\x01\x82\xd3\xe4\x93\x02\xb1\x01"O/namespaces/{namespace}/workflows/{workflow_id}/signal-with-start/{signal_name}:\x01*Z["V/api/v1/namespaces/{namespace}/workflows/{workflow_id}/signal-with-start/{signal_name}:\x01*\x12\xc6\x02\n\x16ResetWorkflowExecution\x12>.temporal.api.workflowservice.v1.ResetWorkflowExecutionRequest\x1a?.temporal.api.workflowservice.v1.ResetWorkflowExecutionResponse"\xaa\x01\x82\xd3\xe4\x93\x02\xa3\x01"H/namespaces/{namespace}/workflows/{workflow_execution.workflow_id}/reset:\x01*ZT"O/api/v1/namespaces/{namespace}/workflows/{workflow_execution.workflow_id}/reset:\x01*\x12\xda\x02\n\x1aTerminateWorkflowExecution\x12\x42.temporal.api.workflowservice.v1.TerminateWorkflowExecutionRequest\x1a\x43.temporal.api.workflowservice.v1.TerminateWorkflowExecutionResponse"\xb2\x01\x82\xd3\xe4\x93\x02\xab\x01"L/namespaces/{namespace}/workflows/{workflow_execution.workflow_id}/terminate:\x01*ZX"S/api/v1/namespaces/{namespace}/workflows/{workflow_execution.workflow_id}/terminate:\x01*\x12\x9e\x01\n\x17\x44\x65leteWorkflowExecution\x12?.temporal.api.workflowservice.v1.DeleteWorkflowExecutionRequest\x1a@.temporal.api.workflowservice.v1.DeleteWorkflowExecutionResponse"\x00\x12\xa7\x01\n\x1aListOpenWorkflowExecutions\x12\x42.temporal.api.workflowservice.v1.ListOpenWorkflowExecutionsRequest\x1a\x43.temporal.api.workflowservice.v1.ListOpenWorkflowExecutionsResponse"\x00\x12\xad\x01\n\x1cListClosedWorkflowExecutions\x12\x44.temporal.api.workflowservice.v1.ListClosedWorkflowExecutionsRequest\x1a\x45.temporal.api.workflowservice.v1.ListClosedWorkflowExecutionsResponse"\x00\x12\xf0\x01\n\x16ListWorkflowExecutions\x12>.temporal.api.workflowservice.v1.ListWorkflowExecutionsRequest\x1a?.temporal.api.workflowservice.v1.ListWorkflowExecutionsResponse"U\x82\xd3\xe4\x93\x02O\x12!/namespaces/{namespace}/workflowsZ*\x12(/api/v1/namespaces/{namespace}/workflows\x12\x9a\x02\n\x1eListArchivedWorkflowExecutions\x12\x46.temporal.api.workflowservice.v1.ListArchivedWorkflowExecutionsRequest\x1aG.temporal.api.workflowservice.v1.ListArchivedWorkflowExecutionsResponse"g\x82\xd3\xe4\x93\x02\x61\x12*/namespaces/{namespace}/archived-workflowsZ3\x12\x31/api/v1/namespaces/{namespace}/archived-workflows\x12\x9b\x01\n\x16ScanWorkflowExecutions\x12>.temporal.api.workflowservice.v1.ScanWorkflowExecutionsRequest\x1a?.temporal.api.workflowservice.v1.ScanWorkflowExecutionsResponse"\x00\x12\xfd\x01\n\x17\x43ountWorkflowExecutions\x12?.temporal.api.workflowservice.v1.CountWorkflowExecutionsRequest\x1a@.temporal.api.workflowservice.v1.CountWorkflowExecutionsResponse"_\x82\xd3\xe4\x93\x02Y\x12&/namespaces/{namespace}/workflow-countZ/\x12-/api/v1/namespaces/{namespace}/workflow-count\x12\x92\x01\n\x13GetSearchAttributes\x12;.temporal.api.workflowservice.v1.GetSearchAttributesRequest\x1a<.temporal.api.workflowservice.v1.GetSearchAttributesResponse"\x00\x12\xa4\x01\n\x19RespondQueryTaskCompleted\x12\x41.temporal.api.workflowservice.v1.RespondQueryTaskCompletedRequest\x1a\x42.temporal.api.workflowservice.v1.RespondQueryTaskCompletedResponse"\x00\x12\x95\x01\n\x14ResetStickyTaskQueue\x12<.temporal.api.workflowservice.v1.ResetStickyTaskQueueRequest\x1a=.temporal.api.workflowservice.v1.ResetStickyTaskQueueResponse"\x00\x12\x83\x01\n\x0eShutdownWorker\x12\x36.temporal.api.workflowservice.v1.ShutdownWorkerRequest\x1a\x37.temporal.api.workflowservice.v1.ShutdownWorkerResponse"\x00\x12\xbf\x02\n\rQueryWorkflow\x12\x35.temporal.api.workflowservice.v1.QueryWorkflowRequest\x1a\x36.temporal.api.workflowservice.v1.QueryWorkflowResponse"\xbe\x01\x82\xd3\xe4\x93\x02\xb7\x01"R/namespaces/{namespace}/workflows/{execution.workflow_id}/query/{query.query_type}:\x01*Z^"Y/api/v1/namespaces/{namespace}/workflows/{execution.workflow_id}/query/{query.query_type}:\x01*\x12\xaa\x02\n\x19\x44\x65scribeWorkflowExecution\x12\x41.temporal.api.workflowservice.v1.DescribeWorkflowExecutionRequest\x1a\x42.temporal.api.workflowservice.v1.DescribeWorkflowExecutionResponse"\x85\x01\x82\xd3\xe4\x93\x02\x7f\x12\x39/namespaces/{namespace}/workflows/{execution.workflow_id}ZB\x12@/api/v1/namespaces/{namespace}/workflows/{execution.workflow_id}\x12\x89\x02\n\x11\x44\x65scribeTaskQueue\x12\x39.temporal.api.workflowservice.v1.DescribeTaskQueueRequest\x1a:.temporal.api.workflowservice.v1.DescribeTaskQueueResponse"}\x82\xd3\xe4\x93\x02w\x12\x35/namespaces/{namespace}/task-queues/{task_queue.name}Z>\x12/namespaces/{namespace}/schedules/{schedule_id}/matching-timesZG\x12\x45/api/v1/namespaces/{namespace}/schedules/{schedule_id}/matching-times\x12\xf4\x01\n\x0e\x44\x65leteSchedule\x12\x36.temporal.api.workflowservice.v1.DeleteScheduleRequest\x1a\x37.temporal.api.workflowservice.v1.DeleteScheduleResponse"q\x82\xd3\xe4\x93\x02k*//namespaces/{namespace}/schedules/{schedule_id}Z8*6/api/v1/namespaces/{namespace}/schedules/{schedule_id}\x12\xd5\x01\n\rListSchedules\x12\x35.temporal.api.workflowservice.v1.ListSchedulesRequest\x1a\x36.temporal.api.workflowservice.v1.ListSchedulesResponse"U\x82\xd3\xe4\x93\x02O\x12!/namespaces/{namespace}/schedulesZ*\x12(/api/v1/namespaces/{namespace}/schedules\x12\xb9\x01\n UpdateWorkerBuildIdCompatibility\x12H.temporal.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest\x1aI.temporal.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityResponse"\x00\x12\xe1\x02\n\x1dGetWorkerBuildIdCompatibility\x12\x45.temporal.api.workflowservice.v1.GetWorkerBuildIdCompatibilityRequest\x1a\x46.temporal.api.workflowservice.v1.GetWorkerBuildIdCompatibilityResponse"\xb0\x01\x82\xd3\xe4\x93\x02\xa9\x01\x12N/namespaces/{namespace}/task-queues/{task_queue}/worker-build-id-compatibilityZW\x12U/api/v1/namespaces/{namespace}/task-queues/{task_queue}/worker-build-id-compatibility\x12\xaa\x01\n\x1bUpdateWorkerVersioningRules\x12\x43.temporal.api.workflowservice.v1.UpdateWorkerVersioningRulesRequest\x1a\x44.temporal.api.workflowservice.v1.UpdateWorkerVersioningRulesResponse"\x00\x12\xc6\x02\n\x18GetWorkerVersioningRules\x12@.temporal.api.workflowservice.v1.GetWorkerVersioningRulesRequest\x1a\x41.temporal.api.workflowservice.v1.GetWorkerVersioningRulesResponse"\xa4\x01\x82\xd3\xe4\x93\x02\x9d\x01\x12H/namespaces/{namespace}/task-queues/{task_queue}/worker-versioning-rulesZQ\x12O/api/v1/namespaces/{namespace}/task-queues/{task_queue}/worker-versioning-rules\x12\x97\x02\n\x19GetWorkerTaskReachability\x12\x41.temporal.api.workflowservice.v1.GetWorkerTaskReachabilityRequest\x1a\x42.temporal.api.workflowservice.v1.GetWorkerTaskReachabilityResponse"s\x82\xd3\xe4\x93\x02m\x12\x30/namespaces/{namespace}/worker-task-reachabilityZ9\x12\x37/api/v1/namespaces/{namespace}/worker-task-reachability\x12\xc8\x02\n\x12\x44\x65scribeDeployment\x12:.temporal.api.workflowservice.v1.DescribeDeploymentRequest\x1a;.temporal.api.workflowservice.v1.DescribeDeploymentResponse"\xb8\x01\x82\xd3\xe4\x93\x02\xb1\x01\x12R/namespaces/{namespace}/deployments/{deployment.series_name}/{deployment.build_id}Z[\x12Y/api/v1/namespaces/{namespace}/deployments/{deployment.series_name}/{deployment.build_id}\x12\xb5\x03\n\x1f\x44\x65scribeWorkerDeploymentVersion\x12G.temporal.api.workflowservice.v1.DescribeWorkerDeploymentVersionRequest\x1aH.temporal.api.workflowservice.v1.DescribeWorkerDeploymentVersionResponse"\xfe\x01\x82\xd3\xe4\x93\x02\xf7\x01\x12u/namespaces/{namespace}/worker-deployment-versions/{deployment_version.deployment_name}/{deployment_version.build_id}Z~\x12|/api/v1/namespaces/{namespace}/worker-deployment-versions/{deployment_version.deployment_name}/{deployment_version.build_id}\x12\xdf\x01\n\x0fListDeployments\x12\x37.temporal.api.workflowservice.v1.ListDeploymentsRequest\x1a\x38.temporal.api.workflowservice.v1.ListDeploymentsResponse"Y\x82\xd3\xe4\x93\x02S\x12#/namespaces/{namespace}/deploymentsZ,\x12*/api/v1/namespaces/{namespace}/deployments\x12\xf7\x02\n\x19GetDeploymentReachability\x12\x41.temporal.api.workflowservice.v1.GetDeploymentReachabilityRequest\x1a\x42.temporal.api.workflowservice.v1.GetDeploymentReachabilityResponse"\xd2\x01\x82\xd3\xe4\x93\x02\xcb\x01\x12_/namespaces/{namespace}/deployments/{deployment.series_name}/{deployment.build_id}/reachabilityZh\x12\x66/api/v1/namespaces/{namespace}/deployments/{deployment.series_name}/{deployment.build_id}/reachability\x12\x99\x02\n\x14GetCurrentDeployment\x12<.temporal.api.workflowservice.v1.GetCurrentDeploymentRequest\x1a=.temporal.api.workflowservice.v1.GetCurrentDeploymentResponse"\x83\x01\x82\xd3\xe4\x93\x02}\x12\x38/namespaces/{namespace}/current-deployment/{series_name}ZA\x12?/api/v1/namespaces/{namespace}/current-deployment/{series_name}\x12\xb6\x02\n\x14SetCurrentDeployment\x12<.temporal.api.workflowservice.v1.SetCurrentDeploymentRequest\x1a=.temporal.api.workflowservice.v1.SetCurrentDeploymentResponse"\xa0\x01\x82\xd3\xe4\x93\x02\x99\x01"C/namespaces/{namespace}/current-deployment/{deployment.series_name}:\x01*ZO"J/api/v1/namespaces/{namespace}/current-deployment/{deployment.series_name}:\x01*\x12\xf7\x02\n!SetWorkerDeploymentCurrentVersion\x12I.temporal.api.workflowservice.v1.SetWorkerDeploymentCurrentVersionRequest\x1aJ.temporal.api.workflowservice.v1.SetWorkerDeploymentCurrentVersionResponse"\xba\x01\x82\xd3\xe4\x93\x02\xb3\x01"P/namespaces/{namespace}/worker-deployments/{deployment_name}/set-current-version:\x01*Z\\"W/api/v1/namespaces/{namespace}/worker-deployments/{deployment_name}/set-current-version:\x01*\x12\xae\x02\n\x18\x44\x65scribeWorkerDeployment\x12@.temporal.api.workflowservice.v1.DescribeWorkerDeploymentRequest\x1a\x41.temporal.api.workflowservice.v1.DescribeWorkerDeploymentResponse"\x8c\x01\x82\xd3\xe4\x93\x02\x85\x01\x12.temporal.api.workflowservice.v1.DeleteWorkerDeploymentRequest\x1a?.temporal.api.workflowservice.v1.DeleteWorkerDeploymentResponse"\x8c\x01\x82\xd3\xe4\x93\x02\x85\x01*.temporal.api.workflowservice.v1.ListWorkerDeploymentsResponse"g\x82\xd3\xe4\x93\x02\x61\x12*/namespaces/{namespace}/worker-deploymentsZ3\x12\x31/api/v1/namespaces/{namespace}/worker-deployments\x12\xf0\x03\n%UpdateWorkerDeploymentVersionMetadata\x12M.temporal.api.workflowservice.v1.UpdateWorkerDeploymentVersionMetadataRequest\x1aN.temporal.api.workflowservice.v1.UpdateWorkerDeploymentVersionMetadataResponse"\xa7\x02\x82\xd3\xe4\x93\x02\xa0\x02"\x85\x01/namespaces/{namespace}/worker-deployment-versions/{deployment_version.deployment_name}/{deployment_version.build_id}/update-metadata:\x01*Z\x92\x01"\x8c\x01/api/v1/namespaces/{namespace}/worker-deployment-versions/{deployment_version.deployment_name}/{deployment_version.build_id}/update-metadata:\x01*\x12\xf5\x02\n\x17UpdateWorkflowExecution\x12?.temporal.api.workflowservice.v1.UpdateWorkflowExecutionRequest\x1a@.temporal.api.workflowservice.v1.UpdateWorkflowExecutionResponse"\xd6\x01\x82\xd3\xe4\x93\x02\xcf\x01"^/namespaces/{namespace}/workflows/{workflow_execution.workflow_id}/update/{request.input.name}:\x01*Zj"e/api/v1/namespaces/{namespace}/workflows/{workflow_execution.workflow_id}/update/{request.input.name}:\x01*\x12\xaa\x01\n\x1bPollWorkflowExecutionUpdate\x12\x43.temporal.api.workflowservice.v1.PollWorkflowExecutionUpdateRequest\x1a\x44.temporal.api.workflowservice.v1.PollWorkflowExecutionUpdateResponse"\x00\x12\x8d\x02\n\x13StartBatchOperation\x12;.temporal.api.workflowservice.v1.StartBatchOperationRequest\x1a<.temporal.api.workflowservice.v1.StartBatchOperationResponse"{\x82\xd3\xe4\x93\x02u"1/namespaces/{namespace}/batch-operations/{job_id}:\x01*Z="8/api/v1/namespaces/{namespace}/batch-operations/{job_id}:\x01*\x12\x95\x02\n\x12StopBatchOperation\x12:.temporal.api.workflowservice.v1.StopBatchOperationRequest\x1a;.temporal.api.workflowservice.v1.StopBatchOperationResponse"\x85\x01\x82\xd3\xe4\x93\x02\x7f"6/namespaces/{namespace}/batch-operations/{job_id}/stop:\x01*ZB"=/api/v1/namespaces/{namespace}/batch-operations/{job_id}/stop:\x01*\x12\x90\x02\n\x16\x44\x65scribeBatchOperation\x12>.temporal.api.workflowservice.v1.DescribeBatchOperationRequest\x1a?.temporal.api.workflowservice.v1.DescribeBatchOperationResponse"u\x82\xd3\xe4\x93\x02o\x12\x31/namespaces/{namespace}/batch-operations/{job_id}Z:\x12\x38/api/v1/namespaces/{namespace}/batch-operations/{job_id}\x12\xf5\x01\n\x13ListBatchOperations\x12;.temporal.api.workflowservice.v1.ListBatchOperationsRequest\x1a<.temporal.api.workflowservice.v1.ListBatchOperationsResponse"c\x82\xd3\xe4\x93\x02]\x12(/namespaces/{namespace}/batch-operationsZ1\x12//api/v1/namespaces/{namespace}/batch-operations\x12\x8f\x01\n\x12PollNexusTaskQueue\x12:.temporal.api.workflowservice.v1.PollNexusTaskQueueRequest\x1a;.temporal.api.workflowservice.v1.PollNexusTaskQueueResponse"\x00\x12\xa4\x01\n\x19RespondNexusTaskCompleted\x12\x41.temporal.api.workflowservice.v1.RespondNexusTaskCompletedRequest\x1a\x42.temporal.api.workflowservice.v1.RespondNexusTaskCompletedResponse"\x00\x12\x9b\x01\n\x16RespondNexusTaskFailed\x12>.temporal.api.workflowservice.v1.RespondNexusTaskFailedRequest\x1a?.temporal.api.workflowservice.v1.RespondNexusTaskFailedResponse"\x00\x12\x93\x02\n\x15UpdateActivityOptions\x12=.temporal.api.workflowservice.v1.UpdateActivityOptionsRequest\x1a>.temporal.api.workflowservice.v1.UpdateActivityOptionsResponse"{\x82\xd3\xe4\x93\x02u"1/namespaces/{namespace}/activities/update-options:\x01*Z="8/api/v1/namespaces/{namespace}/activities/update-options:\x01*\x12\xf0\x02\n\x1eUpdateWorkflowExecutionOptions\x12\x46.temporal.api.workflowservice.v1.UpdateWorkflowExecutionOptionsRequest\x1aG.temporal.api.workflowservice.v1.UpdateWorkflowExecutionOptionsResponse"\xbc\x01\x82\xd3\xe4\x93\x02\xb5\x01"Q/namespaces/{namespace}/workflows/{workflow_execution.workflow_id}/update-options:\x01*Z]"X/api/v1/namespaces/{namespace}/workflows/{workflow_execution.workflow_id}/update-options:\x01*\x12\xe9\x01\n\rPauseActivity\x12\x35.temporal.api.workflowservice.v1.PauseActivityRequest\x1a\x36.temporal.api.workflowservice.v1.PauseActivityResponse"i\x82\xd3\xe4\x93\x02\x63"(/namespaces/{namespace}/activities/pause:\x01*Z4"//api/v1/namespaces/{namespace}/activities/pause:\x01*\x12\xf3\x01\n\x0fUnpauseActivity\x12\x37.temporal.api.workflowservice.v1.UnpauseActivityRequest\x1a\x38.temporal.api.workflowservice.v1.UnpauseActivityResponse"m\x82\xd3\xe4\x93\x02g"*/namespaces/{namespace}/activities/unpause:\x01*Z6"1/api/v1/namespaces/{namespace}/activities/unpause:\x01*\x12\xe9\x01\n\rResetActivity\x12\x35.temporal.api.workflowservice.v1.ResetActivityRequest\x1a\x36.temporal.api.workflowservice.v1.ResetActivityResponse"i\x82\xd3\xe4\x93\x02\x63"(/namespaces/{namespace}/activities/reset:\x01*Z4"//api/v1/namespaces/{namespace}/activities/reset:\x01*\x12\xf4\x01\n\x12\x43reateWorkflowRule\x12:.temporal.api.workflowservice.v1.CreateWorkflowRuleRequest\x1a;.temporal.api.workflowservice.v1.CreateWorkflowRuleResponse"e\x82\xd3\xe4\x93\x02_"&/namespaces/{namespace}/workflow-rules:\x01*Z2"-/api/v1/namespaces/{namespace}/workflow-rules:\x01*\x12\x88\x02\n\x14\x44\x65scribeWorkflowRule\x12<.temporal.api.workflowservice.v1.DescribeWorkflowRuleRequest\x1a=.temporal.api.workflowservice.v1.DescribeWorkflowRuleResponse"s\x82\xd3\xe4\x93\x02m\x12\x30/namespaces/{namespace}/workflow-rules/{rule_id}Z9\x12\x37/api/v1/namespaces/{namespace}/workflow-rules/{rule_id}\x12\x82\x02\n\x12\x44\x65leteWorkflowRule\x12:.temporal.api.workflowservice.v1.DeleteWorkflowRuleRequest\x1a;.temporal.api.workflowservice.v1.DeleteWorkflowRuleResponse"s\x82\xd3\xe4\x93\x02m*0/namespaces/{namespace}/workflow-rules/{rule_id}Z9*7/api/v1/namespaces/{namespace}/workflow-rules/{rule_id}\x12\xeb\x01\n\x11ListWorkflowRules\x12\x39.temporal.api.workflowservice.v1.ListWorkflowRulesRequest\x1a:.temporal.api.workflowservice.v1.ListWorkflowRulesResponse"_\x82\xd3\xe4\x93\x02Y\x12&/namespaces/{namespace}/workflow-rulesZ/\x12-/api/v1/namespaces/{namespace}/workflow-rules\x12\xb9\x02\n\x13TriggerWorkflowRule\x12;.temporal.api.workflowservice.v1.TriggerWorkflowRuleRequest\x1a<.temporal.api.workflowservice.v1.TriggerWorkflowRuleResponse"\xa6\x01\x82\xd3\xe4\x93\x02\x9f\x01"F/namespaces/{namespace}/workflows/{execution.workflow_id}/trigger-rule:\x01*ZR"M/api/v1/namespaces/{namespace}/workflows/{execution.workflow_id}/trigger-rule:\x01*\x12\x83\x02\n\x15RecordWorkerHeartbeat\x12=.temporal.api.workflowservice.v1.RecordWorkerHeartbeatRequest\x1a>.temporal.api.workflowservice.v1.RecordWorkerHeartbeatResponse"k\x82\xd3\xe4\x93\x02\x65")/namespaces/{namespace}/workers/heartbeat:\x01*Z5"0/api/v1/namespaces/{namespace}/workers/heartbeat:\x01*\x12\xcb\x01\n\x0bListWorkers\x12\x33.temporal.api.workflowservice.v1.ListWorkersRequest\x1a\x34.temporal.api.workflowservice.v1.ListWorkersResponse"Q\x82\xd3\xe4\x93\x02K\x12\x1f/namespaces/{namespace}/workersZ(\x12&/api/v1/namespaces/{namespace}/workers\x12\xaf\x02\n\x15UpdateTaskQueueConfig\x12=.temporal.api.workflowservice.v1.UpdateTaskQueueConfigRequest\x1a>.temporal.api.workflowservice.v1.UpdateTaskQueueConfigResponse"\x96\x01\x82\xd3\xe4\x93\x02\x8f\x01">/namespaces/{namespace}/task-queues/{task_queue}/update-config:\x01*ZJ"E/api/v1/namespaces/{namespace}/task-queues/{task_queue}/update-config:\x01*\x12\xfd\x01\n\x11\x46\x65tchWorkerConfig\x12\x39.temporal.api.workflowservice.v1.FetchWorkerConfigRequest\x1a:.temporal.api.workflowservice.v1.FetchWorkerConfigResponse"q\x82\xd3\xe4\x93\x02k",/namespaces/{namespace}/workers/fetch-config:\x01*Z8"3/api/v1/namespaces/{namespace}/workers/fetch-config:\x01*\x12\x82\x02\n\x12UpdateWorkerConfig\x12:.temporal.api.workflowservice.v1.UpdateWorkerConfigRequest\x1a;.temporal.api.workflowservice.v1.UpdateWorkerConfigResponse"s\x82\xd3\xe4\x93\x02m"-/namespaces/{namespace}/workers/update-config:\x01*Z9"4/api/v1/namespaces/{namespace}/workers/update-config:\x01*B\xb6\x01\n"io.temporal.api.workflowservice.v1B\x0cServiceProtoP\x01Z5go.temporal.io/api/workflowservice/v1;workflowservice\xaa\x02!Temporalio.Api.WorkflowService.V1\xea\x02$Temporalio::Api::WorkflowService::V1b\x06proto3' + b'\n-temporal/api/workflowservice/v1/service.proto\x12\x1ftemporal.api.workflowservice.v1\x1a\x1cgoogle/api/annotations.proto\x1a!nexusannotations/v1/options.proto\x1a+temporal/api/protometa/v1/annotations.proto\x1a\x36temporal/api/workflowservice/v1/request_response.proto2\xdc\xb2\x02\n\x0fWorkflowService\x12\xc3\x01\n\x11RegisterNamespace\x12\x39.temporal.api.workflowservice.v1.RegisterNamespaceRequest\x1a:.temporal.api.workflowservice.v1.RegisterNamespaceResponse"7\x82\xd3\xe4\x93\x02\x31"\x13/cluster/namespaces:\x01*Z\x17"\x12/api/v1/namespaces:\x01*\x12\xd5\x01\n\x11\x44\x65scribeNamespace\x12\x39.temporal.api.workflowservice.v1.DescribeNamespaceRequest\x1a:.temporal.api.workflowservice.v1.DescribeNamespaceResponse"I\x82\xd3\xe4\x93\x02\x43\x12\x1f/cluster/namespaces/{namespace}Z \x12\x1e/api/v1/namespaces/{namespace}\x12\xb4\x01\n\x0eListNamespaces\x12\x36.temporal.api.workflowservice.v1.ListNamespacesRequest\x1a\x37.temporal.api.workflowservice.v1.ListNamespacesResponse"1\x82\xd3\xe4\x93\x02+\x12\x13/cluster/namespacesZ\x14\x12\x12/api/v1/namespaces\x12\xe3\x01\n\x0fUpdateNamespace\x12\x37.temporal.api.workflowservice.v1.UpdateNamespaceRequest\x1a\x38.temporal.api.workflowservice.v1.UpdateNamespaceResponse"]\x82\xd3\xe4\x93\x02W"&/cluster/namespaces/{namespace}/update:\x01*Z*"%/api/v1/namespaces/{namespace}/update:\x01*\x12\x8f\x01\n\x12\x44\x65precateNamespace\x12:.temporal.api.workflowservice.v1.DeprecateNamespaceRequest\x1a;.temporal.api.workflowservice.v1.DeprecateNamespaceResponse"\x00\x12\xc6\x02\n\x16StartWorkflowExecution\x12>.temporal.api.workflowservice.v1.StartWorkflowExecutionRequest\x1a?.temporal.api.workflowservice.v1.StartWorkflowExecutionResponse"\xaa\x01\x82\xd3\xe4\x93\x02q"//namespaces/{namespace}/workflows/{workflow_id}:\x01*Z;"6/api/v1/namespaces/{namespace}/workflows/{workflow_id}:\x01*\x8a\x9d\xcc\x1b.\n\x14temporal-resource-id\x12\x16workflow:{workflow_id}\x12\xc2\x01\n\x15\x45xecuteMultiOperation\x12=.temporal.api.workflowservice.v1.ExecuteMultiOperationRequest\x1a>.temporal.api.workflowservice.v1.ExecuteMultiOperationResponse"*\x8a\x9d\xcc\x1b%\n\x14temporal-resource-id\x12\r{resource_id}\x12\xfe\x02\n\x1bGetWorkflowExecutionHistory\x12\x43.temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryRequest\x1a\x44.temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryResponse"\xd3\x01\x82\xd3\xe4\x93\x02\x8f\x01\x12\x41/namespaces/{namespace}/workflows/{execution.workflow_id}/historyZJ\x12H/api/v1/namespaces/{namespace}/workflows/{execution.workflow_id}/history\x8a\x9d\xcc\x1b\x38\n\x14temporal-resource-id\x12 workflow:{execution.workflow_id}\x12\xa3\x03\n"GetWorkflowExecutionHistoryReverse\x12J.temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryReverseRequest\x1aK.temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryReverseResponse"\xe3\x01\x82\xd3\xe4\x93\x02\x9f\x01\x12I/namespaces/{namespace}/workflows/{execution.workflow_id}/history-reverseZR\x12P/api/v1/namespaces/{namespace}/workflows/{execution.workflow_id}/history-reverse\x8a\x9d\xcc\x1b\x38\n\x14temporal-resource-id\x12 workflow:{execution.workflow_id}\x12\xcd\x01\n\x15PollWorkflowTaskQueue\x12=.temporal.api.workflowservice.v1.PollWorkflowTaskQueueRequest\x1a>.temporal.api.workflowservice.v1.PollWorkflowTaskQueueResponse"5\x8a\x9d\xcc\x1b\x30\n\x14temporal-resource-id\x12\x18poller:{poller_group_id}\x12\xd7\x01\n\x1cRespondWorkflowTaskCompleted\x12\x44.temporal.api.workflowservice.v1.RespondWorkflowTaskCompletedRequest\x1a\x45.temporal.api.workflowservice.v1.RespondWorkflowTaskCompletedResponse"*\x8a\x9d\xcc\x1b%\n\x14temporal-resource-id\x12\r{resource_id}\x12\xce\x01\n\x19RespondWorkflowTaskFailed\x12\x41.temporal.api.workflowservice.v1.RespondWorkflowTaskFailedRequest\x1a\x42.temporal.api.workflowservice.v1.RespondWorkflowTaskFailedResponse"*\x8a\x9d\xcc\x1b%\n\x14temporal-resource-id\x12\r{resource_id}\x12\xcd\x01\n\x15PollActivityTaskQueue\x12=.temporal.api.workflowservice.v1.PollActivityTaskQueueRequest\x1a>.temporal.api.workflowservice.v1.PollActivityTaskQueueResponse"5\x8a\x9d\xcc\x1b\x30\n\x14temporal-resource-id\x12\x18poller:{poller_group_id}\x12\xc2\x02\n\x1bRecordActivityTaskHeartbeat\x12\x43.temporal.api.workflowservice.v1.RecordActivityTaskHeartbeatRequest\x1a\x44.temporal.api.workflowservice.v1.RecordActivityTaskHeartbeatResponse"\x97\x01\x82\xd3\xe4\x93\x02g"*/namespaces/{namespace}/activity-heartbeat:\x01*Z6"1/api/v1/namespaces/{namespace}/activity-heartbeat:\x01*\x8a\x9d\xcc\x1b%\n\x14temporal-resource-id\x12\r{resource_id}\x12\xa8\x04\n\x1fRecordActivityTaskHeartbeatById\x12G.temporal.api.workflowservice.v1.RecordActivityTaskHeartbeatByIdRequest\x1aH.temporal.api.workflowservice.v1.RecordActivityTaskHeartbeatByIdResponse"\xf1\x02\x82\xd3\xe4\x93\x02\xc0\x02":/namespaces/{namespace}/activities/{activity_id}/heartbeat:\x01*ZF"A/api/v1/namespaces/{namespace}/activities/{activity_id}/heartbeat:\x01*ZW"R/namespaces/{namespace}/workflows/{workflow_id}/activities/{activity_id}/heartbeat:\x01*Z^"Y/api/v1/namespaces/{namespace}/workflows/{workflow_id}/activities/{activity_id}/heartbeat:\x01*\x8a\x9d\xcc\x1b%\n\x14temporal-resource-id\x12\r{resource_id}\x12\xc3\x02\n\x1cRespondActivityTaskCompleted\x12\x44.temporal.api.workflowservice.v1.RespondActivityTaskCompletedRequest\x1a\x45.temporal.api.workflowservice.v1.RespondActivityTaskCompletedResponse"\x95\x01\x82\xd3\xe4\x93\x02\x65")/namespaces/{namespace}/activity-complete:\x01*Z5"0/api/v1/namespaces/{namespace}/activity-complete:\x01*\x8a\x9d\xcc\x1b%\n\x14temporal-resource-id\x12\r{resource_id}\x12\xa7\x04\n RespondActivityTaskCompletedById\x12H.temporal.api.workflowservice.v1.RespondActivityTaskCompletedByIdRequest\x1aI.temporal.api.workflowservice.v1.RespondActivityTaskCompletedByIdResponse"\xed\x02\x82\xd3\xe4\x93\x02\xbc\x02"9/namespaces/{namespace}/activities/{activity_id}/complete:\x01*ZE"@/api/v1/namespaces/{namespace}/activities/{activity_id}/complete:\x01*ZV"Q/namespaces/{namespace}/workflows/{workflow_id}/activities/{activity_id}/complete:\x01*Z]"X/api/v1/namespaces/{namespace}/workflows/{workflow_id}/activities/{activity_id}/complete:\x01*\x8a\x9d\xcc\x1b%\n\x14temporal-resource-id\x12\r{resource_id}\x12\xb2\x02\n\x19RespondActivityTaskFailed\x12\x41.temporal.api.workflowservice.v1.RespondActivityTaskFailedRequest\x1a\x42.temporal.api.workflowservice.v1.RespondActivityTaskFailedResponse"\x8d\x01\x82\xd3\xe4\x93\x02]"%/namespaces/{namespace}/activity-fail:\x01*Z1",/api/v1/namespaces/{namespace}/activity-fail:\x01*\x8a\x9d\xcc\x1b%\n\x14temporal-resource-id\x12\r{resource_id}\x12\x8e\x04\n\x1dRespondActivityTaskFailedById\x12\x45.temporal.api.workflowservice.v1.RespondActivityTaskFailedByIdRequest\x1a\x46.temporal.api.workflowservice.v1.RespondActivityTaskFailedByIdResponse"\xdd\x02\x82\xd3\xe4\x93\x02\xac\x02"5/namespaces/{namespace}/activities/{activity_id}/fail:\x01*ZA".temporal.api.workflowservice.v1.ResetWorkflowExecutionRequest\x1a?.temporal.api.workflowservice.v1.ResetWorkflowExecutionResponse"\xf0\x01\x82\xd3\xe4\x93\x02\xa3\x01"H/namespaces/{namespace}/workflows/{workflow_execution.workflow_id}/reset:\x01*ZT"O/api/v1/namespaces/{namespace}/workflows/{workflow_execution.workflow_id}/reset:\x01*\x8a\x9d\xcc\x1b\x41\n\x14temporal-resource-id\x12)workflow:{workflow_execution.workflow_id}\x12\xa0\x03\n\x1aTerminateWorkflowExecution\x12\x42.temporal.api.workflowservice.v1.TerminateWorkflowExecutionRequest\x1a\x43.temporal.api.workflowservice.v1.TerminateWorkflowExecutionResponse"\xf8\x01\x82\xd3\xe4\x93\x02\xab\x01"L/namespaces/{namespace}/workflows/{workflow_execution.workflow_id}/terminate:\x01*ZX"S/api/v1/namespaces/{namespace}/workflows/{workflow_execution.workflow_id}/terminate:\x01*\x8a\x9d\xcc\x1b\x41\n\x14temporal-resource-id\x12)workflow:{workflow_execution.workflow_id}\x12\xe4\x01\n\x17\x44\x65leteWorkflowExecution\x12?.temporal.api.workflowservice.v1.DeleteWorkflowExecutionRequest\x1a@.temporal.api.workflowservice.v1.DeleteWorkflowExecutionResponse"F\x8a\x9d\xcc\x1b\x41\n\x14temporal-resource-id\x12)workflow:{workflow_execution.workflow_id}\x12\xa7\x01\n\x1aListOpenWorkflowExecutions\x12\x42.temporal.api.workflowservice.v1.ListOpenWorkflowExecutionsRequest\x1a\x43.temporal.api.workflowservice.v1.ListOpenWorkflowExecutionsResponse"\x00\x12\xad\x01\n\x1cListClosedWorkflowExecutions\x12\x44.temporal.api.workflowservice.v1.ListClosedWorkflowExecutionsRequest\x1a\x45.temporal.api.workflowservice.v1.ListClosedWorkflowExecutionsResponse"\x00\x12\xf0\x01\n\x16ListWorkflowExecutions\x12>.temporal.api.workflowservice.v1.ListWorkflowExecutionsRequest\x1a?.temporal.api.workflowservice.v1.ListWorkflowExecutionsResponse"U\x82\xd3\xe4\x93\x02O\x12!/namespaces/{namespace}/workflowsZ*\x12(/api/v1/namespaces/{namespace}/workflows\x12\x9a\x02\n\x1eListArchivedWorkflowExecutions\x12\x46.temporal.api.workflowservice.v1.ListArchivedWorkflowExecutionsRequest\x1aG.temporal.api.workflowservice.v1.ListArchivedWorkflowExecutionsResponse"g\x82\xd3\xe4\x93\x02\x61\x12*/namespaces/{namespace}/archived-workflowsZ3\x12\x31/api/v1/namespaces/{namespace}/archived-workflows\x12\x9b\x01\n\x16ScanWorkflowExecutions\x12>.temporal.api.workflowservice.v1.ScanWorkflowExecutionsRequest\x1a?.temporal.api.workflowservice.v1.ScanWorkflowExecutionsResponse"\x00\x12\xfd\x01\n\x17\x43ountWorkflowExecutions\x12?.temporal.api.workflowservice.v1.CountWorkflowExecutionsRequest\x1a@.temporal.api.workflowservice.v1.CountWorkflowExecutionsResponse"_\x82\xd3\xe4\x93\x02Y\x12&/namespaces/{namespace}/workflow-countZ/\x12-/api/v1/namespaces/{namespace}/workflow-count\x12\x92\x01\n\x13GetSearchAttributes\x12;.temporal.api.workflowservice.v1.GetSearchAttributesRequest\x1a<.temporal.api.workflowservice.v1.GetSearchAttributesResponse"\x00\x12\xd9\x01\n\x19RespondQueryTaskCompleted\x12\x41.temporal.api.workflowservice.v1.RespondQueryTaskCompletedRequest\x1a\x42.temporal.api.workflowservice.v1.RespondQueryTaskCompletedResponse"5\x8a\x9d\xcc\x1b\x30\n\x14temporal-resource-id\x12\x18poller:{poller_group_id}\x12\xd2\x01\n\x14ResetStickyTaskQueue\x12<.temporal.api.workflowservice.v1.ResetStickyTaskQueueRequest\x1a=.temporal.api.workflowservice.v1.ResetStickyTaskQueueResponse"=\x8a\x9d\xcc\x1b\x38\n\x14temporal-resource-id\x12 workflow:{execution.workflow_id}\x12\x83\x01\n\x0eShutdownWorker\x12\x36.temporal.api.workflowservice.v1.ShutdownWorkerRequest\x1a\x37.temporal.api.workflowservice.v1.ShutdownWorkerResponse"\x00\x12\xfc\x02\n\rQueryWorkflow\x12\x35.temporal.api.workflowservice.v1.QueryWorkflowRequest\x1a\x36.temporal.api.workflowservice.v1.QueryWorkflowResponse"\xfb\x01\x82\xd3\xe4\x93\x02\xb7\x01"R/namespaces/{namespace}/workflows/{execution.workflow_id}/query/{query.query_type}:\x01*Z^"Y/api/v1/namespaces/{namespace}/workflows/{execution.workflow_id}/query/{query.query_type}:\x01*\x8a\x9d\xcc\x1b\x38\n\x14temporal-resource-id\x12 workflow:{execution.workflow_id}\x12\xe7\x02\n\x19\x44\x65scribeWorkflowExecution\x12\x41.temporal.api.workflowservice.v1.DescribeWorkflowExecutionRequest\x1a\x42.temporal.api.workflowservice.v1.DescribeWorkflowExecutionResponse"\xc2\x01\x82\xd3\xe4\x93\x02\x7f\x12\x39/namespaces/{namespace}/workflows/{execution.workflow_id}ZB\x12@/api/v1/namespaces/{namespace}/workflows/{execution.workflow_id}\x8a\x9d\xcc\x1b\x38\n\x14temporal-resource-id\x12 workflow:{execution.workflow_id}\x12\xc2\x02\n\x11\x44\x65scribeTaskQueue\x12\x39.temporal.api.workflowservice.v1.DescribeTaskQueueRequest\x1a:.temporal.api.workflowservice.v1.DescribeTaskQueueResponse"\xb5\x01\x82\xd3\xe4\x93\x02w\x12\x35/namespaces/{namespace}/task-queues/{task_queue.name}Z>\x12/namespaces/{namespace}/schedules/{schedule_id}/matching-timesZG\x12\x45/api/v1/namespaces/{namespace}/schedules/{schedule_id}/matching-times\x8a\x9d\xcc\x1b.\n\x14temporal-resource-id\x12\x16schedule:{schedule_id}\x12\xa8\x02\n\x0e\x44\x65leteSchedule\x12\x36.temporal.api.workflowservice.v1.DeleteScheduleRequest\x1a\x37.temporal.api.workflowservice.v1.DeleteScheduleResponse"\xa4\x01\x82\xd3\xe4\x93\x02k*//namespaces/{namespace}/schedules/{schedule_id}Z8*6/api/v1/namespaces/{namespace}/schedules/{schedule_id}\x8a\x9d\xcc\x1b.\n\x14temporal-resource-id\x12\x16schedule:{schedule_id}\x12\xd5\x01\n\rListSchedules\x12\x35.temporal.api.workflowservice.v1.ListSchedulesRequest\x1a\x36.temporal.api.workflowservice.v1.ListSchedulesResponse"U\x82\xd3\xe4\x93\x02O\x12!/namespaces/{namespace}/schedulesZ*\x12(/api/v1/namespaces/{namespace}/schedules\x12\xe2\x01\n\x0e\x43ountSchedules\x12\x36.temporal.api.workflowservice.v1.CountSchedulesRequest\x1a\x37.temporal.api.workflowservice.v1.CountSchedulesResponse"_\x82\xd3\xe4\x93\x02Y\x12&/namespaces/{namespace}/schedule-countZ/\x12-/api/v1/namespaces/{namespace}/schedule-count\x12\xb9\x01\n UpdateWorkerBuildIdCompatibility\x12H.temporal.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest\x1aI.temporal.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityResponse"\x00\x12\xe1\x02\n\x1dGetWorkerBuildIdCompatibility\x12\x45.temporal.api.workflowservice.v1.GetWorkerBuildIdCompatibilityRequest\x1a\x46.temporal.api.workflowservice.v1.GetWorkerBuildIdCompatibilityResponse"\xb0\x01\x82\xd3\xe4\x93\x02\xa9\x01\x12N/namespaces/{namespace}/task-queues/{task_queue}/worker-build-id-compatibilityZW\x12U/api/v1/namespaces/{namespace}/task-queues/{task_queue}/worker-build-id-compatibility\x12\xaa\x01\n\x1bUpdateWorkerVersioningRules\x12\x43.temporal.api.workflowservice.v1.UpdateWorkerVersioningRulesRequest\x1a\x44.temporal.api.workflowservice.v1.UpdateWorkerVersioningRulesResponse"\x00\x12\xc6\x02\n\x18GetWorkerVersioningRules\x12@.temporal.api.workflowservice.v1.GetWorkerVersioningRulesRequest\x1a\x41.temporal.api.workflowservice.v1.GetWorkerVersioningRulesResponse"\xa4\x01\x82\xd3\xe4\x93\x02\x9d\x01\x12H/namespaces/{namespace}/task-queues/{task_queue}/worker-versioning-rulesZQ\x12O/api/v1/namespaces/{namespace}/task-queues/{task_queue}/worker-versioning-rules\x12\x97\x02\n\x19GetWorkerTaskReachability\x12\x41.temporal.api.workflowservice.v1.GetWorkerTaskReachabilityRequest\x1a\x42.temporal.api.workflowservice.v1.GetWorkerTaskReachabilityResponse"s\x82\xd3\xe4\x93\x02m\x12\x30/namespaces/{namespace}/worker-task-reachabilityZ9\x12\x37/api/v1/namespaces/{namespace}/worker-task-reachability\x12\xc8\x02\n\x12\x44\x65scribeDeployment\x12:.temporal.api.workflowservice.v1.DescribeDeploymentRequest\x1a;.temporal.api.workflowservice.v1.DescribeDeploymentResponse"\xb8\x01\x82\xd3\xe4\x93\x02\xb1\x01\x12R/namespaces/{namespace}/deployments/{deployment.series_name}/{deployment.build_id}Z[\x12Y/api/v1/namespaces/{namespace}/deployments/{deployment.series_name}/{deployment.build_id}\x12\x81\x04\n\x1f\x44\x65scribeWorkerDeploymentVersion\x12G.temporal.api.workflowservice.v1.DescribeWorkerDeploymentVersionRequest\x1aH.temporal.api.workflowservice.v1.DescribeWorkerDeploymentVersionResponse"\xca\x02\x82\xd3\xe4\x93\x02\xf7\x01\x12u/namespaces/{namespace}/worker-deployment-versions/{deployment_version.deployment_name}/{deployment_version.build_id}Z~\x12|/api/v1/namespaces/{namespace}/worker-deployment-versions/{deployment_version.deployment_name}/{deployment_version.build_id}\x8a\x9d\xcc\x1bG\n\x14temporal-resource-id\x12/deployment:{deployment_version.deployment_name}\x12\xdf\x01\n\x0fListDeployments\x12\x37.temporal.api.workflowservice.v1.ListDeploymentsRequest\x1a\x38.temporal.api.workflowservice.v1.ListDeploymentsResponse"Y\x82\xd3\xe4\x93\x02S\x12#/namespaces/{namespace}/deploymentsZ,\x12*/api/v1/namespaces/{namespace}/deployments\x12\xf7\x02\n\x19GetDeploymentReachability\x12\x41.temporal.api.workflowservice.v1.GetDeploymentReachabilityRequest\x1a\x42.temporal.api.workflowservice.v1.GetDeploymentReachabilityResponse"\xd2\x01\x82\xd3\xe4\x93\x02\xcb\x01\x12_/namespaces/{namespace}/deployments/{deployment.series_name}/{deployment.build_id}/reachabilityZh\x12\x66/api/v1/namespaces/{namespace}/deployments/{deployment.series_name}/{deployment.build_id}/reachability\x12\x99\x02\n\x14GetCurrentDeployment\x12<.temporal.api.workflowservice.v1.GetCurrentDeploymentRequest\x1a=.temporal.api.workflowservice.v1.GetCurrentDeploymentResponse"\x83\x01\x82\xd3\xe4\x93\x02}\x12\x38/namespaces/{namespace}/current-deployment/{series_name}ZA\x12?/api/v1/namespaces/{namespace}/current-deployment/{series_name}\x12\xb6\x02\n\x14SetCurrentDeployment\x12<.temporal.api.workflowservice.v1.SetCurrentDeploymentRequest\x1a=.temporal.api.workflowservice.v1.SetCurrentDeploymentResponse"\xa0\x01\x82\xd3\xe4\x93\x02\x99\x01"C/namespaces/{namespace}/current-deployment/{deployment.series_name}:\x01*ZO"J/api/v1/namespaces/{namespace}/current-deployment/{deployment.series_name}:\x01*\x12\xb0\x03\n!SetWorkerDeploymentCurrentVersion\x12I.temporal.api.workflowservice.v1.SetWorkerDeploymentCurrentVersionRequest\x1aJ.temporal.api.workflowservice.v1.SetWorkerDeploymentCurrentVersionResponse"\xf3\x01\x82\xd3\xe4\x93\x02\xb3\x01"P/namespaces/{namespace}/worker-deployments/{deployment_name}/set-current-version:\x01*Z\\"W/api/v1/namespaces/{namespace}/worker-deployments/{deployment_name}/set-current-version:\x01*\x8a\x9d\xcc\x1b\x34\n\x14temporal-resource-id\x12\x1c\x64\x65ployment:{deployment_name}\x12\xe7\x02\n\x18\x44\x65scribeWorkerDeployment\x12@.temporal.api.workflowservice.v1.DescribeWorkerDeploymentRequest\x1a\x41.temporal.api.workflowservice.v1.DescribeWorkerDeploymentResponse"\xc5\x01\x82\xd3\xe4\x93\x02\x85\x01\x12.temporal.api.workflowservice.v1.DeleteWorkerDeploymentRequest\x1a?.temporal.api.workflowservice.v1.DeleteWorkerDeploymentResponse"\xc5\x01\x82\xd3\xe4\x93\x02\x85\x01*.temporal.api.workflowservice.v1.ListWorkerDeploymentsResponse"g\x82\xd3\xe4\x93\x02\x61\x12*/namespaces/{namespace}/worker-deploymentsZ3\x12\x31/api/v1/namespaces/{namespace}/worker-deployments\x12\xae\x02\n\x16\x43reateWorkerDeployment\x12>.temporal.api.workflowservice.v1.CreateWorkerDeploymentRequest\x1a?.temporal.api.workflowservice.v1.CreateWorkerDeploymentResponse"\x92\x01\x82\xd3\xe4\x93\x02\x8b\x01".temporal.api.workflowservice.v1.DescribeBatchOperationRequest\x1a?.temporal.api.workflowservice.v1.DescribeBatchOperationResponse"\xa0\x01\x82\xd3\xe4\x93\x02o\x12\x31/namespaces/{namespace}/batch-operations/{job_id}Z:\x12\x38/api/v1/namespaces/{namespace}/batch-operations/{job_id}\x8a\x9d\xcc\x1b&\n\x14temporal-resource-id\x12\x0e\x62\x61tch:{job_id}\x12\xf5\x01\n\x13ListBatchOperations\x12;.temporal.api.workflowservice.v1.ListBatchOperationsRequest\x1a<.temporal.api.workflowservice.v1.ListBatchOperationsResponse"c\x82\xd3\xe4\x93\x02]\x12(/namespaces/{namespace}/batch-operationsZ1\x12//api/v1/namespaces/{namespace}/batch-operations\x12\xc4\x01\n\x12PollNexusTaskQueue\x12:.temporal.api.workflowservice.v1.PollNexusTaskQueueRequest\x1a;.temporal.api.workflowservice.v1.PollNexusTaskQueueResponse"5\x8a\x9d\xcc\x1b\x30\n\x14temporal-resource-id\x12\x18poller:{poller_group_id}\x12\xd9\x01\n\x19RespondNexusTaskCompleted\x12\x41.temporal.api.workflowservice.v1.RespondNexusTaskCompletedRequest\x1a\x42.temporal.api.workflowservice.v1.RespondNexusTaskCompletedResponse"5\x8a\x9d\xcc\x1b\x30\n\x14temporal-resource-id\x12\x18poller:{poller_group_id}\x12\xd0\x01\n\x16RespondNexusTaskFailed\x12>.temporal.api.workflowservice.v1.RespondNexusTaskFailedRequest\x1a?.temporal.api.workflowservice.v1.RespondNexusTaskFailedResponse"5\x8a\x9d\xcc\x1b\x30\n\x14temporal-resource-id\x12\x18poller:{poller_group_id}\x12\xe8\x02\n\x15UpdateActivityOptions\x12=.temporal.api.workflowservice.v1.UpdateActivityOptionsRequest\x1a>.temporal.api.workflowservice.v1.UpdateActivityOptionsResponse"\xcf\x01\x82\xd3\xe4\x93\x02\x8b\x01".temporal.api.workflowservice.v1.RecordWorkerHeartbeatResponse"\x95\x01\x82\xd3\xe4\x93\x02\x65")/namespaces/{namespace}/workers/heartbeat:\x01*Z5"0/api/v1/namespaces/{namespace}/workers/heartbeat:\x01*\x8a\x9d\xcc\x1b%\n\x14temporal-resource-id\x12\r{resource_id}\x12\xcb\x01\n\x0bListWorkers\x12\x33.temporal.api.workflowservice.v1.ListWorkersRequest\x1a\x34.temporal.api.workflowservice.v1.ListWorkersResponse"Q\x82\xd3\xe4\x93\x02K\x12\x1f/namespaces/{namespace}/workersZ(\x12&/api/v1/namespaces/{namespace}/workers\x12\xd8\x01\n\x0c\x43ountWorkers\x12\x34.temporal.api.workflowservice.v1.CountWorkersRequest\x1a\x35.temporal.api.workflowservice.v1.CountWorkersResponse"[\x82\xd3\xe4\x93\x02U\x12$/namespaces/{namespace}/worker-countZ-\x12+/api/v1/namespaces/{namespace}/worker-count\x12\xe2\x02\n\x15UpdateTaskQueueConfig\x12=.temporal.api.workflowservice.v1.UpdateTaskQueueConfigRequest\x1a>.temporal.api.workflowservice.v1.UpdateTaskQueueConfigResponse"\xc9\x01\x82\xd3\xe4\x93\x02\x8f\x01">/namespaces/{namespace}/task-queues/{task_queue}/update-config:\x01*ZJ"E/api/v1/namespaces/{namespace}/task-queues/{task_queue}/update-config:\x01*\x8a\x9d\xcc\x1b.\n\x14temporal-resource-id\x12\x16taskqueue:{task_queue}\x12\xa8\x02\n\x11\x46\x65tchWorkerConfig\x12\x39.temporal.api.workflowservice.v1.FetchWorkerConfigRequest\x1a:.temporal.api.workflowservice.v1.FetchWorkerConfigResponse"\x9b\x01\x82\xd3\xe4\x93\x02k",/namespaces/{namespace}/workers/fetch-config:\x01*Z8"3/api/v1/namespaces/{namespace}/workers/fetch-config:\x01*\x8a\x9d\xcc\x1b%\n\x14temporal-resource-id\x12\r{resource_id}\x12\xad\x02\n\x12UpdateWorkerConfig\x12:.temporal.api.workflowservice.v1.UpdateWorkerConfigRequest\x1a;.temporal.api.workflowservice.v1.UpdateWorkerConfigResponse"\x9d\x01\x82\xd3\xe4\x93\x02m"-/namespaces/{namespace}/workers/update-config:\x01*Z9"4/api/v1/namespaces/{namespace}/workers/update-config:\x01*\x8a\x9d\xcc\x1b%\n\x14temporal-resource-id\x12\r{resource_id}\x12\xcd\x02\n\x0e\x44\x65scribeWorker\x12\x36.temporal.api.workflowservice.v1.DescribeWorkerRequest\x1a\x37.temporal.api.workflowservice.v1.DescribeWorkerResponse"\xc9\x01\x82\xd3\xe4\x93\x02\x89\x01\x12>/namespaces/{namespace}/workers/describe/{worker_instance_key}ZG\x12\x45/api/v1/namespaces/{namespace}/workers/describe/{worker_instance_key}\x8a\x9d\xcc\x1b\x34\n\x14temporal-resource-id\x12\x1cworker:{worker_instance_key}\x12\xd2\x02\n\x16PauseWorkflowExecution\x12>.temporal.api.workflowservice.v1.PauseWorkflowExecutionRequest\x1a?.temporal.api.workflowservice.v1.PauseWorkflowExecutionResponse"\xb6\x01\x82\xd3\xe4\x93\x02}"5/namespaces/{namespace}/workflows/{workflow_id}/pause:\x01*ZA"/api/v1/namespaces/{namespace}/workflows/{workflow_id}/unpause:\x01*\x8a\x9d\xcc\x1b.\n\x14temporal-resource-id\x12\x16workflow:{workflow_id}\x12\xc8\x02\n\x16StartActivityExecution\x12>.temporal.api.workflowservice.v1.StartActivityExecutionRequest\x1a?.temporal.api.workflowservice.v1.StartActivityExecutionResponse"\xac\x01\x82\xd3\xe4\x93\x02s"0/namespaces/{namespace}/activities/{activity_id}:\x01*Z<"7/api/v1/namespaces/{namespace}/activities/{activity_id}:\x01*\x8a\x9d\xcc\x1b.\n\x14temporal-resource-id\x12\x16\x61\x63tivity:{activity_id}\x12\xb6\x02\n\x1cStartNexusOperationExecution\x12\x44.temporal.api.workflowservice.v1.StartNexusOperationExecutionRequest\x1a\x45.temporal.api.workflowservice.v1.StartNexusOperationExecutionResponse"\x88\x01\x82\xd3\xe4\x93\x02\x81\x01"7/namespaces/{namespace}/nexus-operations/{operation_id}:\x01*ZC">/api/v1/namespaces/{namespace}/nexus-operations/{operation_id}:\x01*\x12\xcb\x02\n\x19\x44\x65scribeActivityExecution\x12\x41.temporal.api.workflowservice.v1.DescribeActivityExecutionRequest\x1a\x42.temporal.api.workflowservice.v1.DescribeActivityExecutionResponse"\xa6\x01\x82\xd3\xe4\x93\x02m\x12\x30/namespaces/{namespace}/activities/{activity_id}Z9\x12\x37/api/v1/namespaces/{namespace}/activities/{activity_id}\x8a\x9d\xcc\x1b.\n\x14temporal-resource-id\x12\x16\x61\x63tivity:{activity_id}\x12\xb8\x02\n\x1f\x44\x65scribeNexusOperationExecution\x12G.temporal.api.workflowservice.v1.DescribeNexusOperationExecutionRequest\x1aH.temporal.api.workflowservice.v1.DescribeNexusOperationExecutionResponse"\x81\x01\x82\xd3\xe4\x93\x02{\x12\x37/namespaces/{namespace}/nexus-operations/{operation_id}Z@\x12>/api/v1/namespaces/{namespace}/nexus-operations/{operation_id}\x12\xcf\x02\n\x15PollActivityExecution\x12=.temporal.api.workflowservice.v1.PollActivityExecutionRequest\x1a>.temporal.api.workflowservice.v1.PollActivityExecutionResponse"\xb6\x01\x82\xd3\xe4\x93\x02}\x12\x38/namespaces/{namespace}/activities/{activity_id}/outcomeZA\x12?/api/v1/namespaces/{namespace}/activities/{activity_id}/outcome\x8a\x9d\xcc\x1b.\n\x14temporal-resource-id\x12\x16\x61\x63tivity:{activity_id}\x12\xb7\x02\n\x1bPollNexusOperationExecution\x12\x43.temporal.api.workflowservice.v1.PollNexusOperationExecutionRequest\x1a\x44.temporal.api.workflowservice.v1.PollNexusOperationExecutionResponse"\x8c\x01\x82\xd3\xe4\x93\x02\x85\x01\x12.temporal.api.workflowservice.v1.ListActivityExecutionsRequest\x1a?.temporal.api.workflowservice.v1.ListActivityExecutionsResponse"W\x82\xd3\xe4\x93\x02Q\x12"/namespaces/{namespace}/activitiesZ+\x12)/api/v1/namespaces/{namespace}/activities\x12\x90\x02\n\x1cListNexusOperationExecutions\x12\x44.temporal.api.workflowservice.v1.ListNexusOperationExecutionsRequest\x1a\x45.temporal.api.workflowservice.v1.ListNexusOperationExecutionsResponse"c\x82\xd3\xe4\x93\x02]\x12(/namespaces/{namespace}/nexus-operationsZ1\x12//api/v1/namespaces/{namespace}/nexus-operations\x12\xfd\x01\n\x17\x43ountActivityExecutions\x12?.temporal.api.workflowservice.v1.CountActivityExecutionsRequest\x1a@.temporal.api.workflowservice.v1.CountActivityExecutionsResponse"_\x82\xd3\xe4\x93\x02Y\x12&/namespaces/{namespace}/activity-countZ/\x12-/api/v1/namespaces/{namespace}/activity-count\x12\x9d\x02\n\x1d\x43ountNexusOperationExecutions\x12\x45.temporal.api.workflowservice.v1.CountNexusOperationExecutionsRequest\x1a\x46.temporal.api.workflowservice.v1.CountNexusOperationExecutionsResponse"m\x82\xd3\xe4\x93\x02g\x12-/namespaces/{namespace}/nexus-operation-countZ6\x12\x34/api/v1/namespaces/{namespace}/nexus-operation-count\x12\xef\x02\n\x1eRequestCancelActivityExecution\x12\x46.temporal.api.workflowservice.v1.RequestCancelActivityExecutionRequest\x1aG.temporal.api.workflowservice.v1.RequestCancelActivityExecutionResponse"\xbb\x01\x82\xd3\xe4\x93\x02\x81\x01"7/namespaces/{namespace}/activities/{activity_id}/cancel:\x01*ZC">/api/v1/namespaces/{namespace}/activities/{activity_id}/cancel:\x01*\x8a\x9d\xcc\x1b.\n\x14temporal-resource-id\x12\x16\x61\x63tivity:{activity_id}\x12\xdc\x02\n$RequestCancelNexusOperationExecution\x12L.temporal.api.workflowservice.v1.RequestCancelNexusOperationExecutionRequest\x1aM.temporal.api.workflowservice.v1.RequestCancelNexusOperationExecutionResponse"\x96\x01\x82\xd3\xe4\x93\x02\x8f\x01">/namespaces/{namespace}/nexus-operations/{operation_id}/cancel:\x01*ZJ"E/api/v1/namespaces/{namespace}/nexus-operations/{operation_id}/cancel:\x01*\x12\xe9\x02\n\x1aTerminateActivityExecution\x12\x42.temporal.api.workflowservice.v1.TerminateActivityExecutionRequest\x1a\x43.temporal.api.workflowservice.v1.TerminateActivityExecutionResponse"\xc1\x01\x82\xd3\xe4\x93\x02\x87\x01":/namespaces/{namespace}/activities/{activity_id}/terminate:\x01*ZF"A/api/v1/namespaces/{namespace}/activities/{activity_id}/terminate:\x01*\x8a\x9d\xcc\x1b.\n\x14temporal-resource-id\x12\x16\x61\x63tivity:{activity_id}\x12\x9e\x01\n\x17\x44\x65leteActivityExecution\x12?.temporal.api.workflowservice.v1.DeleteActivityExecutionRequest\x1a@.temporal.api.workflowservice.v1.DeleteActivityExecutionResponse"\x00\x12\xfd\x03\n\x16PauseActivityExecution\x12>.temporal.api.workflowservice.v1.PauseActivityExecutionRequest\x1a?.temporal.api.workflowservice.v1.PauseActivityExecutionResponse"\xe1\x02\x82\xd3\xe4\x93\x02\xb0\x02"6/namespaces/{namespace}/activities/{activity_id}/pause:\x01*ZB"=/api/v1/namespaces/{namespace}/activities/{activity_id}/pause:\x01*ZS"N/namespaces/{namespace}/workflows/{workflow_id}/activities/{activity_id}/pause:\x01*ZZ"U/api/v1/namespaces/{namespace}/workflows/{workflow_id}/activities/{activity_id}/pause:\x01*\x8a\x9d\xcc\x1b%\n\x14temporal-resource-id\x12\r{resource_id}\x12\xfd\x03\n\x16ResetActivityExecution\x12>.temporal.api.workflowservice.v1.ResetActivityExecutionRequest\x1a?.temporal.api.workflowservice.v1.ResetActivityExecutionResponse"\xe1\x02\x82\xd3\xe4\x93\x02\xb0\x02"6/namespaces/{namespace}/activities/{activity_id}/reset:\x01*ZB"=/api/v1/namespaces/{namespace}/activities/{activity_id}/reset:\x01*ZS"N/namespaces/{namespace}/workflows/{workflow_id}/activities/{activity_id}/reset:\x01*ZZ"U/api/v1/namespaces/{namespace}/workflows/{workflow_id}/activities/{activity_id}/reset:\x01*\x8a\x9d\xcc\x1b%\n\x14temporal-resource-id\x12\r{resource_id}\x12\x8b\x04\n\x18UnpauseActivityExecution\x12@.temporal.api.workflowservice.v1.UnpauseActivityExecutionRequest\x1a\x41.temporal.api.workflowservice.v1.UnpauseActivityExecutionResponse"\xe9\x02\x82\xd3\xe4\x93\x02\xb8\x02"8/namespaces/{namespace}/activities/{activity_id}/unpause:\x01*ZD"?/api/v1/namespaces/{namespace}/activities/{activity_id}/unpause:\x01*ZU"P/namespaces/{namespace}/workflows/{workflow_id}/activities/{activity_id}/unpause:\x01*Z\\"W/api/v1/namespaces/{namespace}/workflows/{workflow_id}/activities/{activity_id}/unpause:\x01*\x8a\x9d\xcc\x1b%\n\x14temporal-resource-id\x12\r{resource_id}\x12\xb9\x04\n\x1eUpdateActivityExecutionOptions\x12\x46.temporal.api.workflowservice.v1.UpdateActivityExecutionOptionsRequest\x1aG.temporal.api.workflowservice.v1.UpdateActivityExecutionOptionsResponse"\x85\x03\x82\xd3\xe4\x93\x02\xd4\x02"?/namespaces/{namespace}/activities/{activity_id}/update-options:\x01*ZK"F/api/v1/namespaces/{namespace}/activities/{activity_id}/update-options:\x01*Z\\"W/namespaces/{namespace}/workflows/{workflow_id}/activities/{activity_id}/update-options:\x01*Zc"^/api/v1/namespaces/{namespace}/workflows/{workflow_id}/activities/{activity_id}/update-options:\x01*\x8a\x9d\xcc\x1b%\n\x14temporal-resource-id\x12\r{resource_id}\x12\xd6\x02\n TerminateNexusOperationExecution\x12H.temporal.api.workflowservice.v1.TerminateNexusOperationExecutionRequest\x1aI.temporal.api.workflowservice.v1.TerminateNexusOperationExecutionResponse"\x9c\x01\x82\xd3\xe4\x93\x02\x95\x01"A/namespaces/{namespace}/nexus-operations/{operation_id}/terminate:\x01*ZM"H/api/v1/namespaces/{namespace}/nexus-operations/{operation_id}/terminate:\x01*\x12\xb0\x01\n\x1d\x44\x65leteNexusOperationExecution\x12\x45.temporal.api.workflowservice.v1.DeleteNexusOperationExecutionRequest\x1a\x46.temporal.api.workflowservice.v1.DeleteNexusOperationExecutionResponse"\x00\x12\xc1\x03\n!PollWorkflowExecutionTimeSkipping\x12I.temporal.api.workflowservice.v1.PollWorkflowExecutionTimeSkippingRequest\x1aJ.temporal.api.workflowservice.v1.PollWorkflowExecutionTimeSkippingResponse"\x84\x02\x82\xd3\xe4\x93\x02\xb7\x01\x12U/namespaces/{namespace}/workflows/{workflow_execution.workflow_id}/time-skipping/pollZ^\x12\\/api/v1/namespaces/{namespace}/workflows/{workflow_execution.workflow_id}/time-skipping/poll\x8a\x9d\xcc\x1b\x41\n\x14temporal-resource-id\x12)workflow:{workflow_execution.workflow_id}B\xb6\x01\n"io.temporal.api.workflowservice.v1B\x0cServiceProtoP\x01Z5go.temporal.io/api/workflowservice/v1;workflowservice\xaa\x02!Temporalio.Api.WorkflowService.V1\xea\x02$Temporalio::Api::WorkflowService::V1b\x06proto3' ) @@ -48,73 +54,103 @@ _WORKFLOWSERVICE.methods_by_name["StartWorkflowExecution"]._options = None _WORKFLOWSERVICE.methods_by_name[ "StartWorkflowExecution" - ]._serialized_options = b'\202\323\344\223\002q"//namespaces/{namespace}/workflows/{workflow_id}:\001*Z;"6/api/v1/namespaces/{namespace}/workflows/{workflow_id}:\001*' + ]._serialized_options = b'\202\323\344\223\002q"//namespaces/{namespace}/workflows/{workflow_id}:\001*Z;"6/api/v1/namespaces/{namespace}/workflows/{workflow_id}:\001*\212\235\314\033.\n\024temporal-resource-id\022\026workflow:{workflow_id}' _WORKFLOWSERVICE.methods_by_name["ExecuteMultiOperation"]._options = None _WORKFLOWSERVICE.methods_by_name[ "ExecuteMultiOperation" - ]._serialized_options = b'\202\323\344\223\002\205\001"9/namespaces/{namespace}/workflows/execute-multi-operation:\001*ZE"@/api/v1/namespaces/{namespace}/workflows/execute-multi-operation:\001*' + ]._serialized_options = ( + b"\212\235\314\033%\n\024temporal-resource-id\022\r{resource_id}" + ) _WORKFLOWSERVICE.methods_by_name["GetWorkflowExecutionHistory"]._options = None _WORKFLOWSERVICE.methods_by_name[ "GetWorkflowExecutionHistory" - ]._serialized_options = b"\202\323\344\223\002\217\001\022A/namespaces/{namespace}/workflows/{execution.workflow_id}/historyZJ\022H/api/v1/namespaces/{namespace}/workflows/{execution.workflow_id}/history" + ]._serialized_options = b"\202\323\344\223\002\217\001\022A/namespaces/{namespace}/workflows/{execution.workflow_id}/historyZJ\022H/api/v1/namespaces/{namespace}/workflows/{execution.workflow_id}/history\212\235\314\0338\n\024temporal-resource-id\022 workflow:{execution.workflow_id}" _WORKFLOWSERVICE.methods_by_name[ "GetWorkflowExecutionHistoryReverse" ]._options = None _WORKFLOWSERVICE.methods_by_name[ "GetWorkflowExecutionHistoryReverse" - ]._serialized_options = b"\202\323\344\223\002\237\001\022I/namespaces/{namespace}/workflows/{execution.workflow_id}/history-reverseZR\022P/api/v1/namespaces/{namespace}/workflows/{execution.workflow_id}/history-reverse" + ]._serialized_options = b"\202\323\344\223\002\237\001\022I/namespaces/{namespace}/workflows/{execution.workflow_id}/history-reverseZR\022P/api/v1/namespaces/{namespace}/workflows/{execution.workflow_id}/history-reverse\212\235\314\0338\n\024temporal-resource-id\022 workflow:{execution.workflow_id}" + _WORKFLOWSERVICE.methods_by_name["PollWorkflowTaskQueue"]._options = None + _WORKFLOWSERVICE.methods_by_name[ + "PollWorkflowTaskQueue" + ]._serialized_options = ( + b"\212\235\314\0330\n\024temporal-resource-id\022\030poller:{poller_group_id}" + ) + _WORKFLOWSERVICE.methods_by_name["RespondWorkflowTaskCompleted"]._options = None + _WORKFLOWSERVICE.methods_by_name[ + "RespondWorkflowTaskCompleted" + ]._serialized_options = ( + b"\212\235\314\033%\n\024temporal-resource-id\022\r{resource_id}" + ) + _WORKFLOWSERVICE.methods_by_name["RespondWorkflowTaskFailed"]._options = None + _WORKFLOWSERVICE.methods_by_name[ + "RespondWorkflowTaskFailed" + ]._serialized_options = ( + b"\212\235\314\033%\n\024temporal-resource-id\022\r{resource_id}" + ) + _WORKFLOWSERVICE.methods_by_name["PollActivityTaskQueue"]._options = None + _WORKFLOWSERVICE.methods_by_name[ + "PollActivityTaskQueue" + ]._serialized_options = ( + b"\212\235\314\0330\n\024temporal-resource-id\022\030poller:{poller_group_id}" + ) _WORKFLOWSERVICE.methods_by_name["RecordActivityTaskHeartbeat"]._options = None _WORKFLOWSERVICE.methods_by_name[ "RecordActivityTaskHeartbeat" - ]._serialized_options = b'\202\323\344\223\002k",/namespaces/{namespace}/activities/heartbeat:\001*Z8"3/api/v1/namespaces/{namespace}/activities/heartbeat:\001*' + ]._serialized_options = b'\202\323\344\223\002g"*/namespaces/{namespace}/activity-heartbeat:\001*Z6"1/api/v1/namespaces/{namespace}/activity-heartbeat:\001*\212\235\314\033%\n\024temporal-resource-id\022\r{resource_id}' _WORKFLOWSERVICE.methods_by_name["RecordActivityTaskHeartbeatById"]._options = None _WORKFLOWSERVICE.methods_by_name[ "RecordActivityTaskHeartbeatById" - ]._serialized_options = b'\202\323\344\223\002w"2/namespaces/{namespace}/activities/heartbeat-by-id:\001*Z>"9/api/v1/namespaces/{namespace}/activities/heartbeat-by-id:\001*' + ]._serialized_options = b'\202\323\344\223\002\300\002":/namespaces/{namespace}/activities/{activity_id}/heartbeat:\001*ZF"A/api/v1/namespaces/{namespace}/activities/{activity_id}/heartbeat:\001*ZW"R/namespaces/{namespace}/workflows/{workflow_id}/activities/{activity_id}/heartbeat:\001*Z^"Y/api/v1/namespaces/{namespace}/workflows/{workflow_id}/activities/{activity_id}/heartbeat:\001*\212\235\314\033%\n\024temporal-resource-id\022\r{resource_id}' _WORKFLOWSERVICE.methods_by_name["RespondActivityTaskCompleted"]._options = None _WORKFLOWSERVICE.methods_by_name[ "RespondActivityTaskCompleted" - ]._serialized_options = b'\202\323\344\223\002i"+/namespaces/{namespace}/activities/complete:\001*Z7"2/api/v1/namespaces/{namespace}/activities/complete:\001*' + ]._serialized_options = b'\202\323\344\223\002e")/namespaces/{namespace}/activity-complete:\001*Z5"0/api/v1/namespaces/{namespace}/activity-complete:\001*\212\235\314\033%\n\024temporal-resource-id\022\r{resource_id}' _WORKFLOWSERVICE.methods_by_name["RespondActivityTaskCompletedById"]._options = None _WORKFLOWSERVICE.methods_by_name[ "RespondActivityTaskCompletedById" - ]._serialized_options = b'\202\323\344\223\002u"1/namespaces/{namespace}/activities/complete-by-id:\001*Z="8/api/v1/namespaces/{namespace}/activities/complete-by-id:\001*' + ]._serialized_options = b'\202\323\344\223\002\274\002"9/namespaces/{namespace}/activities/{activity_id}/complete:\001*ZE"@/api/v1/namespaces/{namespace}/activities/{activity_id}/complete:\001*ZV"Q/namespaces/{namespace}/workflows/{workflow_id}/activities/{activity_id}/complete:\001*Z]"X/api/v1/namespaces/{namespace}/workflows/{workflow_id}/activities/{activity_id}/complete:\001*\212\235\314\033%\n\024temporal-resource-id\022\r{resource_id}' _WORKFLOWSERVICE.methods_by_name["RespondActivityTaskFailed"]._options = None _WORKFLOWSERVICE.methods_by_name[ "RespondActivityTaskFailed" - ]._serialized_options = b'\202\323\344\223\002a"\'/namespaces/{namespace}/activities/fail:\001*Z3"./api/v1/namespaces/{namespace}/activities/fail:\001*' + ]._serialized_options = b'\202\323\344\223\002]"%/namespaces/{namespace}/activity-fail:\001*Z1",/api/v1/namespaces/{namespace}/activity-fail:\001*\212\235\314\033%\n\024temporal-resource-id\022\r{resource_id}' _WORKFLOWSERVICE.methods_by_name["RespondActivityTaskFailedById"]._options = None _WORKFLOWSERVICE.methods_by_name[ "RespondActivityTaskFailedById" - ]._serialized_options = b'\202\323\344\223\002m"-/namespaces/{namespace}/activities/fail-by-id:\001*Z9"4/api/v1/namespaces/{namespace}/activities/fail-by-id:\001*' + ]._serialized_options = b'\202\323\344\223\002\254\002"5/namespaces/{namespace}/activities/{activity_id}/fail:\001*ZA"\022\022/namespaces/{namespace}/schedules/{schedule_id}/matching-timesZG\022E/api/v1/namespaces/{namespace}/schedules/{schedule_id}/matching-times" + ]._serialized_options = b"\202\323\344\223\002\211\001\022>/namespaces/{namespace}/schedules/{schedule_id}/matching-timesZG\022E/api/v1/namespaces/{namespace}/schedules/{schedule_id}/matching-times\212\235\314\033.\n\024temporal-resource-id\022\026schedule:{schedule_id}" _WORKFLOWSERVICE.methods_by_name["DeleteSchedule"]._options = None _WORKFLOWSERVICE.methods_by_name[ "DeleteSchedule" - ]._serialized_options = b"\202\323\344\223\002k*//namespaces/{namespace}/schedules/{schedule_id}Z8*6/api/v1/namespaces/{namespace}/schedules/{schedule_id}" + ]._serialized_options = b"\202\323\344\223\002k*//namespaces/{namespace}/schedules/{schedule_id}Z8*6/api/v1/namespaces/{namespace}/schedules/{schedule_id}\212\235\314\033.\n\024temporal-resource-id\022\026schedule:{schedule_id}" _WORKFLOWSERVICE.methods_by_name["ListSchedules"]._options = None _WORKFLOWSERVICE.methods_by_name[ "ListSchedules" ]._serialized_options = b"\202\323\344\223\002O\022!/namespaces/{namespace}/schedulesZ*\022(/api/v1/namespaces/{namespace}/schedules" + _WORKFLOWSERVICE.methods_by_name["CountSchedules"]._options = None + _WORKFLOWSERVICE.methods_by_name[ + "CountSchedules" + ]._serialized_options = b"\202\323\344\223\002Y\022&/namespaces/{namespace}/schedule-countZ/\022-/api/v1/namespaces/{namespace}/schedule-count" _WORKFLOWSERVICE.methods_by_name["GetWorkerBuildIdCompatibility"]._options = None _WORKFLOWSERVICE.methods_by_name[ "GetWorkerBuildIdCompatibility" @@ -198,7 +252,7 @@ _WORKFLOWSERVICE.methods_by_name["DescribeWorkerDeploymentVersion"]._options = None _WORKFLOWSERVICE.methods_by_name[ "DescribeWorkerDeploymentVersion" - ]._serialized_options = b"\202\323\344\223\002\367\001\022u/namespaces/{namespace}/worker-deployment-versions/{deployment_version.deployment_name}/{deployment_version.build_id}Z~\022|/api/v1/namespaces/{namespace}/worker-deployment-versions/{deployment_version.deployment_name}/{deployment_version.build_id}" + ]._serialized_options = b"\202\323\344\223\002\367\001\022u/namespaces/{namespace}/worker-deployment-versions/{deployment_version.deployment_name}/{deployment_version.build_id}Z~\022|/api/v1/namespaces/{namespace}/worker-deployment-versions/{deployment_version.deployment_name}/{deployment_version.build_id}\212\235\314\033G\n\024temporal-resource-id\022/deployment:{deployment_version.deployment_name}" _WORKFLOWSERVICE.methods_by_name["ListDeployments"]._options = None _WORKFLOWSERVICE.methods_by_name[ "ListDeployments" @@ -220,75 +274,121 @@ ]._options = None _WORKFLOWSERVICE.methods_by_name[ "SetWorkerDeploymentCurrentVersion" - ]._serialized_options = b'\202\323\344\223\002\263\001"P/namespaces/{namespace}/worker-deployments/{deployment_name}/set-current-version:\001*Z\\"W/api/v1/namespaces/{namespace}/worker-deployments/{deployment_name}/set-current-version:\001*' + ]._serialized_options = b'\202\323\344\223\002\263\001"P/namespaces/{namespace}/worker-deployments/{deployment_name}/set-current-version:\001*Z\\"W/api/v1/namespaces/{namespace}/worker-deployments/{deployment_name}/set-current-version:\001*\212\235\314\0334\n\024temporal-resource-id\022\034deployment:{deployment_name}' _WORKFLOWSERVICE.methods_by_name["DescribeWorkerDeployment"]._options = None _WORKFLOWSERVICE.methods_by_name[ "DescribeWorkerDeployment" - ]._serialized_options = b"\202\323\344\223\002\205\001\022/namespaces/{namespace}/task-queues/{task_queue}/update-config:\001*ZJ"E/api/v1/namespaces/{namespace}/task-queues/{task_queue}/update-config:\001*' + ]._serialized_options = b'\202\323\344\223\002\217\001">/namespaces/{namespace}/task-queues/{task_queue}/update-config:\001*ZJ"E/api/v1/namespaces/{namespace}/task-queues/{task_queue}/update-config:\001*\212\235\314\033.\n\024temporal-resource-id\022\026taskqueue:{task_queue}' _WORKFLOWSERVICE.methods_by_name["FetchWorkerConfig"]._options = None _WORKFLOWSERVICE.methods_by_name[ "FetchWorkerConfig" - ]._serialized_options = b'\202\323\344\223\002k",/namespaces/{namespace}/workers/fetch-config:\001*Z8"3/api/v1/namespaces/{namespace}/workers/fetch-config:\001*' + ]._serialized_options = b'\202\323\344\223\002k",/namespaces/{namespace}/workers/fetch-config:\001*Z8"3/api/v1/namespaces/{namespace}/workers/fetch-config:\001*\212\235\314\033%\n\024temporal-resource-id\022\r{resource_id}' _WORKFLOWSERVICE.methods_by_name["UpdateWorkerConfig"]._options = None _WORKFLOWSERVICE.methods_by_name[ "UpdateWorkerConfig" - ]._serialized_options = b'\202\323\344\223\002m"-/namespaces/{namespace}/workers/update-config:\001*Z9"4/api/v1/namespaces/{namespace}/workers/update-config:\001*' - _WORKFLOWSERVICE._serialized_start = 170 - _WORKFLOWSERVICE._serialized_end = 24377 + ]._serialized_options = b'\202\323\344\223\002m"-/namespaces/{namespace}/workers/update-config:\001*Z9"4/api/v1/namespaces/{namespace}/workers/update-config:\001*\212\235\314\033%\n\024temporal-resource-id\022\r{resource_id}' + _WORKFLOWSERVICE.methods_by_name["DescribeWorker"]._options = None + _WORKFLOWSERVICE.methods_by_name[ + "DescribeWorker" + ]._serialized_options = b"\202\323\344\223\002\211\001\022>/namespaces/{namespace}/workers/describe/{worker_instance_key}ZG\022E/api/v1/namespaces/{namespace}/workers/describe/{worker_instance_key}\212\235\314\0334\n\024temporal-resource-id\022\034worker:{worker_instance_key}" + _WORKFLOWSERVICE.methods_by_name["PauseWorkflowExecution"]._options = None + _WORKFLOWSERVICE.methods_by_name[ + "PauseWorkflowExecution" + ]._serialized_options = b'\202\323\344\223\002}"5/namespaces/{namespace}/workflows/{workflow_id}/pause:\001*ZA"/api/v1/namespaces/{namespace}/workflows/{workflow_id}/unpause:\001*\212\235\314\033.\n\024temporal-resource-id\022\026workflow:{workflow_id}' + _WORKFLOWSERVICE.methods_by_name["StartActivityExecution"]._options = None + _WORKFLOWSERVICE.methods_by_name[ + "StartActivityExecution" + ]._serialized_options = b'\202\323\344\223\002s"0/namespaces/{namespace}/activities/{activity_id}:\001*Z<"7/api/v1/namespaces/{namespace}/activities/{activity_id}:\001*\212\235\314\033.\n\024temporal-resource-id\022\026activity:{activity_id}' + _WORKFLOWSERVICE.methods_by_name["StartNexusOperationExecution"]._options = None + _WORKFLOWSERVICE.methods_by_name[ + "StartNexusOperationExecution" + ]._serialized_options = b'\202\323\344\223\002\201\001"7/namespaces/{namespace}/nexus-operations/{operation_id}:\001*ZC">/api/v1/namespaces/{namespace}/nexus-operations/{operation_id}:\001*' + _WORKFLOWSERVICE.methods_by_name["DescribeActivityExecution"]._options = None + _WORKFLOWSERVICE.methods_by_name[ + "DescribeActivityExecution" + ]._serialized_options = b"\202\323\344\223\002m\0220/namespaces/{namespace}/activities/{activity_id}Z9\0227/api/v1/namespaces/{namespace}/activities/{activity_id}\212\235\314\033.\n\024temporal-resource-id\022\026activity:{activity_id}" + _WORKFLOWSERVICE.methods_by_name["DescribeNexusOperationExecution"]._options = None + _WORKFLOWSERVICE.methods_by_name[ + "DescribeNexusOperationExecution" + ]._serialized_options = b"\202\323\344\223\002{\0227/namespaces/{namespace}/nexus-operations/{operation_id}Z@\022>/api/v1/namespaces/{namespace}/nexus-operations/{operation_id}" + _WORKFLOWSERVICE.methods_by_name["PollActivityExecution"]._options = None + _WORKFLOWSERVICE.methods_by_name[ + "PollActivityExecution" + ]._serialized_options = b"\202\323\344\223\002}\0228/namespaces/{namespace}/activities/{activity_id}/outcomeZA\022?/api/v1/namespaces/{namespace}/activities/{activity_id}/outcome\212\235\314\033.\n\024temporal-resource-id\022\026activity:{activity_id}" + _WORKFLOWSERVICE.methods_by_name["PollNexusOperationExecution"]._options = None + _WORKFLOWSERVICE.methods_by_name[ + "PollNexusOperationExecution" + ]._serialized_options = b"\202\323\344\223\002\205\001\022/api/v1/namespaces/{namespace}/activities/{activity_id}/cancel:\001*\212\235\314\033.\n\024temporal-resource-id\022\026activity:{activity_id}' + _WORKFLOWSERVICE.methods_by_name[ + "RequestCancelNexusOperationExecution" + ]._options = None + _WORKFLOWSERVICE.methods_by_name[ + "RequestCancelNexusOperationExecution" + ]._serialized_options = b'\202\323\344\223\002\217\001">/namespaces/{namespace}/nexus-operations/{operation_id}/cancel:\001*ZJ"E/api/v1/namespaces/{namespace}/nexus-operations/{operation_id}/cancel:\001*' + _WORKFLOWSERVICE.methods_by_name["TerminateActivityExecution"]._options = None + _WORKFLOWSERVICE.methods_by_name[ + "TerminateActivityExecution" + ]._serialized_options = b'\202\323\344\223\002\207\001":/namespaces/{namespace}/activities/{activity_id}/terminate:\001*ZF"A/api/v1/namespaces/{namespace}/activities/{activity_id}/terminate:\001*\212\235\314\033.\n\024temporal-resource-id\022\026activity:{activity_id}' + _WORKFLOWSERVICE.methods_by_name["PauseActivityExecution"]._options = None + _WORKFLOWSERVICE.methods_by_name[ + "PauseActivityExecution" + ]._serialized_options = b'\202\323\344\223\002\260\002"6/namespaces/{namespace}/activities/{activity_id}/pause:\001*ZB"=/api/v1/namespaces/{namespace}/activities/{activity_id}/pause:\001*ZS"N/namespaces/{namespace}/workflows/{workflow_id}/activities/{activity_id}/pause:\001*ZZ"U/api/v1/namespaces/{namespace}/workflows/{workflow_id}/activities/{activity_id}/pause:\001*\212\235\314\033%\n\024temporal-resource-id\022\r{resource_id}' + _WORKFLOWSERVICE.methods_by_name["ResetActivityExecution"]._options = None + _WORKFLOWSERVICE.methods_by_name[ + "ResetActivityExecution" + ]._serialized_options = b'\202\323\344\223\002\260\002"6/namespaces/{namespace}/activities/{activity_id}/reset:\001*ZB"=/api/v1/namespaces/{namespace}/activities/{activity_id}/reset:\001*ZS"N/namespaces/{namespace}/workflows/{workflow_id}/activities/{activity_id}/reset:\001*ZZ"U/api/v1/namespaces/{namespace}/workflows/{workflow_id}/activities/{activity_id}/reset:\001*\212\235\314\033%\n\024temporal-resource-id\022\r{resource_id}' + _WORKFLOWSERVICE.methods_by_name["UnpauseActivityExecution"]._options = None + _WORKFLOWSERVICE.methods_by_name[ + "UnpauseActivityExecution" + ]._serialized_options = b'\202\323\344\223\002\270\002"8/namespaces/{namespace}/activities/{activity_id}/unpause:\001*ZD"?/api/v1/namespaces/{namespace}/activities/{activity_id}/unpause:\001*ZU"P/namespaces/{namespace}/workflows/{workflow_id}/activities/{activity_id}/unpause:\001*Z\\"W/api/v1/namespaces/{namespace}/workflows/{workflow_id}/activities/{activity_id}/unpause:\001*\212\235\314\033%\n\024temporal-resource-id\022\r{resource_id}' + _WORKFLOWSERVICE.methods_by_name["UpdateActivityExecutionOptions"]._options = None + _WORKFLOWSERVICE.methods_by_name[ + "UpdateActivityExecutionOptions" + ]._serialized_options = b'\202\323\344\223\002\324\002"?/namespaces/{namespace}/activities/{activity_id}/update-options:\001*ZK"F/api/v1/namespaces/{namespace}/activities/{activity_id}/update-options:\001*Z\\"W/namespaces/{namespace}/workflows/{workflow_id}/activities/{activity_id}/update-options:\001*Zc"^/api/v1/namespaces/{namespace}/workflows/{workflow_id}/activities/{activity_id}/update-options:\001*\212\235\314\033%\n\024temporal-resource-id\022\r{resource_id}' + _WORKFLOWSERVICE.methods_by_name["TerminateNexusOperationExecution"]._options = None + _WORKFLOWSERVICE.methods_by_name[ + "TerminateNexusOperationExecution" + ]._serialized_options = b'\202\323\344\223\002\225\001"A/namespaces/{namespace}/nexus-operations/{operation_id}/terminate:\001*ZM"H/api/v1/namespaces/{namespace}/nexus-operations/{operation_id}/terminate:\001*' + _WORKFLOWSERVICE.methods_by_name[ + "PollWorkflowExecutionTimeSkipping" + ]._options = None + _WORKFLOWSERVICE.methods_by_name[ + "PollWorkflowExecutionTimeSkipping" + ]._serialized_options = b"\202\323\344\223\002\267\001\022U/namespaces/{namespace}/workflows/{workflow_execution.workflow_id}/time-skipping/pollZ^\022\\/api/v1/namespaces/{namespace}/workflows/{workflow_execution.workflow_id}/time-skipping/poll\212\235\314\033A\n\024temporal-resource-id\022)workflow:{workflow_execution.workflow_id}" + _WORKFLOWSERVICE._serialized_start = 250 + _WORKFLOWSERVICE._serialized_end = 39510 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/api/workflowservice/v1/service_pb2_grpc.py b/temporalio/api/workflowservice/v1/service_pb2_grpc.py index 770781e6d..486c6a394 100644 --- a/temporalio/api/workflowservice/v1/service_pb2_grpc.py +++ b/temporalio/api/workflowservice/v1/service_pb2_grpc.py @@ -278,6 +278,11 @@ def __init__(self, channel): request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ListSchedulesRequest.SerializeToString, response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ListSchedulesResponse.FromString, ) + self.CountSchedules = channel.unary_unary( + "/temporal.api.workflowservice.v1.WorkflowService/CountSchedules", + request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CountSchedulesRequest.SerializeToString, + response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CountSchedulesResponse.FromString, + ) self.UpdateWorkerBuildIdCompatibility = channel.unary_unary( "/temporal.api.workflowservice.v1.WorkflowService/UpdateWorkerBuildIdCompatibility", request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UpdateWorkerBuildIdCompatibilityRequest.SerializeToString, @@ -363,11 +368,36 @@ def __init__(self, channel): request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ListWorkerDeploymentsRequest.SerializeToString, response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ListWorkerDeploymentsResponse.FromString, ) + self.CreateWorkerDeployment = channel.unary_unary( + "/temporal.api.workflowservice.v1.WorkflowService/CreateWorkerDeployment", + request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CreateWorkerDeploymentRequest.SerializeToString, + response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CreateWorkerDeploymentResponse.FromString, + ) + self.CreateWorkerDeploymentVersion = channel.unary_unary( + "/temporal.api.workflowservice.v1.WorkflowService/CreateWorkerDeploymentVersion", + request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CreateWorkerDeploymentVersionRequest.SerializeToString, + response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CreateWorkerDeploymentVersionResponse.FromString, + ) + self.UpdateWorkerDeploymentVersionComputeConfig = channel.unary_unary( + "/temporal.api.workflowservice.v1.WorkflowService/UpdateWorkerDeploymentVersionComputeConfig", + request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UpdateWorkerDeploymentVersionComputeConfigRequest.SerializeToString, + response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UpdateWorkerDeploymentVersionComputeConfigResponse.FromString, + ) + self.ValidateWorkerDeploymentVersionComputeConfig = channel.unary_unary( + "/temporal.api.workflowservice.v1.WorkflowService/ValidateWorkerDeploymentVersionComputeConfig", + request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ValidateWorkerDeploymentVersionComputeConfigRequest.SerializeToString, + response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ValidateWorkerDeploymentVersionComputeConfigResponse.FromString, + ) self.UpdateWorkerDeploymentVersionMetadata = channel.unary_unary( "/temporal.api.workflowservice.v1.WorkflowService/UpdateWorkerDeploymentVersionMetadata", request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UpdateWorkerDeploymentVersionMetadataRequest.SerializeToString, response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UpdateWorkerDeploymentVersionMetadataResponse.FromString, ) + self.SetWorkerDeploymentManager = channel.unary_unary( + "/temporal.api.workflowservice.v1.WorkflowService/SetWorkerDeploymentManager", + request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.SetWorkerDeploymentManagerRequest.SerializeToString, + response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.SetWorkerDeploymentManagerResponse.FromString, + ) self.UpdateWorkflowExecution = channel.unary_unary( "/temporal.api.workflowservice.v1.WorkflowService/UpdateWorkflowExecution", request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UpdateWorkflowExecutionRequest.SerializeToString, @@ -473,6 +503,11 @@ def __init__(self, channel): request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ListWorkersRequest.SerializeToString, response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ListWorkersResponse.FromString, ) + self.CountWorkers = channel.unary_unary( + "/temporal.api.workflowservice.v1.WorkflowService/CountWorkers", + request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CountWorkersRequest.SerializeToString, + response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CountWorkersResponse.FromString, + ) self.UpdateTaskQueueConfig = channel.unary_unary( "/temporal.api.workflowservice.v1.WorkflowService/UpdateTaskQueueConfig", request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UpdateTaskQueueConfigRequest.SerializeToString, @@ -488,6 +523,126 @@ def __init__(self, channel): request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UpdateWorkerConfigRequest.SerializeToString, response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UpdateWorkerConfigResponse.FromString, ) + self.DescribeWorker = channel.unary_unary( + "/temporal.api.workflowservice.v1.WorkflowService/DescribeWorker", + request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.DescribeWorkerRequest.SerializeToString, + response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.DescribeWorkerResponse.FromString, + ) + self.PauseWorkflowExecution = channel.unary_unary( + "/temporal.api.workflowservice.v1.WorkflowService/PauseWorkflowExecution", + request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.PauseWorkflowExecutionRequest.SerializeToString, + response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.PauseWorkflowExecutionResponse.FromString, + ) + self.UnpauseWorkflowExecution = channel.unary_unary( + "/temporal.api.workflowservice.v1.WorkflowService/UnpauseWorkflowExecution", + request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UnpauseWorkflowExecutionRequest.SerializeToString, + response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UnpauseWorkflowExecutionResponse.FromString, + ) + self.StartActivityExecution = channel.unary_unary( + "/temporal.api.workflowservice.v1.WorkflowService/StartActivityExecution", + request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.StartActivityExecutionRequest.SerializeToString, + response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.StartActivityExecutionResponse.FromString, + ) + self.StartNexusOperationExecution = channel.unary_unary( + "/temporal.api.workflowservice.v1.WorkflowService/StartNexusOperationExecution", + request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.StartNexusOperationExecutionRequest.SerializeToString, + response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.StartNexusOperationExecutionResponse.FromString, + ) + self.DescribeActivityExecution = channel.unary_unary( + "/temporal.api.workflowservice.v1.WorkflowService/DescribeActivityExecution", + request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.DescribeActivityExecutionRequest.SerializeToString, + response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.DescribeActivityExecutionResponse.FromString, + ) + self.DescribeNexusOperationExecution = channel.unary_unary( + "/temporal.api.workflowservice.v1.WorkflowService/DescribeNexusOperationExecution", + request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.DescribeNexusOperationExecutionRequest.SerializeToString, + response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.DescribeNexusOperationExecutionResponse.FromString, + ) + self.PollActivityExecution = channel.unary_unary( + "/temporal.api.workflowservice.v1.WorkflowService/PollActivityExecution", + request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.PollActivityExecutionRequest.SerializeToString, + response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.PollActivityExecutionResponse.FromString, + ) + self.PollNexusOperationExecution = channel.unary_unary( + "/temporal.api.workflowservice.v1.WorkflowService/PollNexusOperationExecution", + request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.PollNexusOperationExecutionRequest.SerializeToString, + response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.PollNexusOperationExecutionResponse.FromString, + ) + self.ListActivityExecutions = channel.unary_unary( + "/temporal.api.workflowservice.v1.WorkflowService/ListActivityExecutions", + request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ListActivityExecutionsRequest.SerializeToString, + response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ListActivityExecutionsResponse.FromString, + ) + self.ListNexusOperationExecutions = channel.unary_unary( + "/temporal.api.workflowservice.v1.WorkflowService/ListNexusOperationExecutions", + request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ListNexusOperationExecutionsRequest.SerializeToString, + response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ListNexusOperationExecutionsResponse.FromString, + ) + self.CountActivityExecutions = channel.unary_unary( + "/temporal.api.workflowservice.v1.WorkflowService/CountActivityExecutions", + request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CountActivityExecutionsRequest.SerializeToString, + response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CountActivityExecutionsResponse.FromString, + ) + self.CountNexusOperationExecutions = channel.unary_unary( + "/temporal.api.workflowservice.v1.WorkflowService/CountNexusOperationExecutions", + request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CountNexusOperationExecutionsRequest.SerializeToString, + response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CountNexusOperationExecutionsResponse.FromString, + ) + self.RequestCancelActivityExecution = channel.unary_unary( + "/temporal.api.workflowservice.v1.WorkflowService/RequestCancelActivityExecution", + request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.RequestCancelActivityExecutionRequest.SerializeToString, + response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.RequestCancelActivityExecutionResponse.FromString, + ) + self.RequestCancelNexusOperationExecution = channel.unary_unary( + "/temporal.api.workflowservice.v1.WorkflowService/RequestCancelNexusOperationExecution", + request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.RequestCancelNexusOperationExecutionRequest.SerializeToString, + response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.RequestCancelNexusOperationExecutionResponse.FromString, + ) + self.TerminateActivityExecution = channel.unary_unary( + "/temporal.api.workflowservice.v1.WorkflowService/TerminateActivityExecution", + request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.TerminateActivityExecutionRequest.SerializeToString, + response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.TerminateActivityExecutionResponse.FromString, + ) + self.DeleteActivityExecution = channel.unary_unary( + "/temporal.api.workflowservice.v1.WorkflowService/DeleteActivityExecution", + request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.DeleteActivityExecutionRequest.SerializeToString, + response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.DeleteActivityExecutionResponse.FromString, + ) + self.PauseActivityExecution = channel.unary_unary( + "/temporal.api.workflowservice.v1.WorkflowService/PauseActivityExecution", + request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.PauseActivityExecutionRequest.SerializeToString, + response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.PauseActivityExecutionResponse.FromString, + ) + self.ResetActivityExecution = channel.unary_unary( + "/temporal.api.workflowservice.v1.WorkflowService/ResetActivityExecution", + request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ResetActivityExecutionRequest.SerializeToString, + response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ResetActivityExecutionResponse.FromString, + ) + self.UnpauseActivityExecution = channel.unary_unary( + "/temporal.api.workflowservice.v1.WorkflowService/UnpauseActivityExecution", + request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UnpauseActivityExecutionRequest.SerializeToString, + response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UnpauseActivityExecutionResponse.FromString, + ) + self.UpdateActivityExecutionOptions = channel.unary_unary( + "/temporal.api.workflowservice.v1.WorkflowService/UpdateActivityExecutionOptions", + request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UpdateActivityExecutionOptionsRequest.SerializeToString, + response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UpdateActivityExecutionOptionsResponse.FromString, + ) + self.TerminateNexusOperationExecution = channel.unary_unary( + "/temporal.api.workflowservice.v1.WorkflowService/TerminateNexusOperationExecution", + request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.TerminateNexusOperationExecutionRequest.SerializeToString, + response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.TerminateNexusOperationExecutionResponse.FromString, + ) + self.DeleteNexusOperationExecution = channel.unary_unary( + "/temporal.api.workflowservice.v1.WorkflowService/DeleteNexusOperationExecution", + request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.DeleteNexusOperationExecutionRequest.SerializeToString, + response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.DeleteNexusOperationExecutionResponse.FromString, + ) + self.PollWorkflowExecutionTimeSkipping = channel.unary_unary( + "/temporal.api.workflowservice.v1.WorkflowService/PollWorkflowExecutionTimeSkipping", + request_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.PollWorkflowExecutionTimeSkippingRequest.SerializeToString, + response_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.PollWorkflowExecutionTimeSkippingResponse.FromString, + ) class WorkflowServiceServicer(object): @@ -570,7 +725,8 @@ def ExecuteMultiOperation(self, request, context): Upon failure, it returns `MultiOperationExecutionFailure` where the status code equals the status code of the *first* operation that failed to be started. - NOTE: Experimental API. + (-- api-linter: core::0127::http-annotation=disabled + aip.dev/not-precedent: To be exposed over HTTP in the future. --) """ context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details("Method not implemented!") @@ -665,10 +821,17 @@ def PollActivityTaskQueue(self, request, context): def RecordActivityTaskHeartbeat(self, request, context): """RecordActivityTaskHeartbeat is optionally called by workers while they execute activities. - If worker fails to heartbeat within the `heartbeat_timeout` interval for the activity task, - then it will be marked as timed out and an `ACTIVITY_TASK_TIMED_OUT` event will be written to - the workflow history. Calling `RecordActivityTaskHeartbeat` will fail with `NotFound` in - such situations, in that event, the SDK should request cancellation of the activity. + If a worker fails to heartbeat within the `heartbeat_timeout` interval for the activity task, + then the current attempt times out. Depending on RetryPolicy, this may trigger a retry or + time out the activity. + + For workflow activities, an `ACTIVITY_TASK_TIMED_OUT` event will be written to the workflow + history. Calling `RecordActivityTaskHeartbeat` will fail with `NotFound` in such situations, + in that event, the SDK should request cancellation of the activity. + + The request may contain response `details` which will be persisted by the server and may be + used by the activity to checkpoint progress. The `cancel_requested` field in the response + indicates whether cancellation has been requested for the activity. """ context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details("Method not implemented!") @@ -689,7 +852,7 @@ def RespondActivityTaskCompleted(self, request, context): """RespondActivityTaskCompleted is called by workers when they successfully complete an activity task. - This results in a new `ACTIVITY_TASK_COMPLETED` event being written to the workflow history + For workflow activities, this results in a new `ACTIVITY_TASK_COMPLETED` event being written to the workflow history and a new workflow task created for the workflow. Fails with `NotFound` if the task token is no longer valid due to activity timeout, already being completed, or never having existed. """ @@ -698,7 +861,7 @@ def RespondActivityTaskCompleted(self, request, context): raise NotImplementedError("Method not implemented!") def RespondActivityTaskCompletedById(self, request, context): - """See `RecordActivityTaskCompleted`. This version allows clients to record completions by + """See `RespondActivityTaskCompleted`. This version allows clients to record completions by namespace/workflow id/activity id instead of task token. (-- api-linter: core::0136::prepositions=disabled @@ -733,7 +896,7 @@ def RespondActivityTaskFailedById(self, request, context): def RespondActivityTaskCanceled(self, request, context): """RespondActivityTaskFailed is called by workers when processing an activity task fails. - This results in a new `ACTIVITY_TASK_CANCELED` event being written to the workflow history + For workflow activities, this results in a new `ACTIVITY_TASK_CANCELED` event being written to the workflow history and a new workflow task created for the workflow. Fails with `NotFound` if the task token is no longer valid due to activity timeout, already being completed, or never having existed. """ @@ -742,7 +905,7 @@ def RespondActivityTaskCanceled(self, request, context): raise NotImplementedError("Method not implemented!") def RespondActivityTaskCanceledById(self, request, context): - """See `RecordActivityTaskCanceled`. This version allows clients to record failures by + """See `RespondActivityTaskCanceled`. This version allows clients to record failures by namespace/workflow id/activity id instead of task token. (-- api-linter: core::0136::prepositions=disabled @@ -795,8 +958,9 @@ def SignalWithStartWorkflowExecution(self, request, context): def ResetWorkflowExecution(self, request, context): """ResetWorkflowExecution will reset an existing workflow execution to a specified `WORKFLOW_TASK_COMPLETED` event (exclusive). It will immediately terminate the current - execution instance. - TODO: Does exclusive here mean *just* the completed event, or also WFT started? Otherwise the task is doomed to time out? + execution instance. "Exclusive" means the identified completed event itself is not replayed + in the reset history; the preceding `WORKFLOW_TASK_STARTED` event remains and will be marked as failed + immediately, and a new workflow task will be scheduled to retry it. """ context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details("Method not implemented!") @@ -857,7 +1021,8 @@ def ListArchivedWorkflowExecutions(self, request, context): raise NotImplementedError("Method not implemented!") def ScanWorkflowExecutions(self, request, context): - """ScanWorkflowExecutions is a visibility API to list large amount of workflow executions in a specific namespace without order. + """ScanWorkflowExecutions _was_ a visibility API to list large amount of workflow executions in a specific namespace without order. + It has since been deprecated in favor of `ListWorkflowExecutions` and rewritten to use `ListWorkflowExecutions` internally. Deprecated: Replaced with `ListWorkflowExecutions`. (-- api-linter: core::0127::http-annotation=disabled @@ -1020,8 +1185,15 @@ def ListSchedules(self, request, context): context.set_details("Method not implemented!") raise NotImplementedError("Method not implemented!") + def CountSchedules(self, request, context): + """CountSchedules is a visibility API to count schedules in a specific namespace.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + def UpdateWorkerBuildIdCompatibility(self, request, context): """Deprecated. Use `UpdateWorkerVersioningRules`. + Will be removed in server version v1.32.0. Allows users to specify sets of worker build id versions on a per task queue basis. Versions are ordered, and may be either compatible with some extant version, or a new incompatible @@ -1045,6 +1217,7 @@ def UpdateWorkerBuildIdCompatibility(self, request, context): def GetWorkerBuildIdCompatibility(self, request, context): """Deprecated. Use `GetWorkerVersioningRules`. + Will be removed in server version v1.32.0. Fetches the worker build id versioning sets for a task queue. """ context.set_code(grpc.StatusCode.UNIMPLEMENTED) @@ -1073,7 +1246,7 @@ def UpdateWorkerVersioningRules(self, request, context): the target Build ID of a redirect rule is able to process event histories made by the source Build ID by using [Patching](https://docs.temporal.io/workflows#patching) or other means. - WARNING: Worker Versioning is not yet stable and the API and behavior may change incompatibly. + Will be removed in server version v1.32.0. (-- api-linter: core::0127::http-annotation=disabled aip.dev/not-precedent: We do yet expose versioning API to HTTP. --) """ @@ -1083,7 +1256,7 @@ def UpdateWorkerVersioningRules(self, request, context): def GetWorkerVersioningRules(self, request, context): """Fetches the Build ID assignment and redirect rules for a Task Queue. - WARNING: Worker Versioning is not yet stable and the API and behavior may change incompatibly. + Will be removed in server version v1.32.0. """ context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details("Method not implemented!") @@ -1091,6 +1264,7 @@ def GetWorkerVersioningRules(self, request, context): def GetWorkerTaskReachability(self, request, context): """Deprecated. Use `DescribeTaskQueue`. + Will be removed in server version v1.32.0. Fetches task reachability to determine whether a worker may be retired. The request may specify task queues to query for or let the server fetch all task queues mapped to the given @@ -1225,6 +1399,42 @@ def ListWorkerDeployments(self, request, context): context.set_details("Method not implemented!") raise NotImplementedError("Method not implemented!") + def CreateWorkerDeployment(self, request, context): + """Creates a new Worker Deployment. + + Experimental. This API might significantly change or be removed in a + future release. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def CreateWorkerDeploymentVersion(self, request, context): + """Creates a new Worker Deployment Version. + + Experimental. This API might significantly change or be removed in a + future release. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def UpdateWorkerDeploymentVersionComputeConfig(self, request, context): + """Updates the compute config attached to a Worker Deployment Version. + Experimental. This API might significantly change or be removed in a future release. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def ValidateWorkerDeploymentVersionComputeConfig(self, request, context): + """Validates the compute config without attaching it to a Worker Deployment Version. + Experimental. This API might significantly change or be removed in a future release. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + def UpdateWorkerDeploymentVersionMetadata(self, request, context): """Updates the user-given metadata attached to a Worker Deployment Version. Experimental. This API might significantly change or be removed in a future release. @@ -1233,6 +1443,14 @@ def UpdateWorkerDeploymentVersionMetadata(self, request, context): context.set_details("Method not implemented!") raise NotImplementedError("Method not implemented!") + def SetWorkerDeploymentManager(self, request, context): + """Set/unset the ManagerIdentity of a Worker Deployment. + Experimental. This API might significantly change or be removed in a future release. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + def UpdateWorkflowExecution(self, request, context): """Invokes the specified Update function on user Workflow code.""" context.set_code(grpc.StatusCode.UNIMPLEMENTED) @@ -1306,6 +1524,8 @@ def RespondNexusTaskFailed(self, request, context): def UpdateActivityOptions(self, request, context): """UpdateActivityOptions is called by the client to update the options of an activity by its ID or type. If there are multiple pending activities of the provided type - all of them will be updated. + This API will be deprecated soon and replaced with a newer UpdateActivityExecutionOptions that is better named and + structured to work well for standalone activities. """ context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details("Method not implemented!") @@ -1334,6 +1554,8 @@ def PauseActivity(self, request, context): - The activity should respond to the cancellation accordingly. Returns a `NotFound` error if there is no pending activity with the provided ID or type + This API will be deprecated soon and replaced with a newer PauseActivityExecution that is better named and + structured to work well for standalone activities. """ context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details("Method not implemented!") @@ -1353,6 +1575,8 @@ def UnpauseActivity(self, request, context): 'reset_heartbeat': the activity heartbeat timer and heartbeats will be reset. Returns a `NotFound` error if there is no pending activity with the provided ID or type + This API will be deprecated soon and replaced with a newer UnpauseActivityExecution that is better named and + structured to work well for standalone activities. """ context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details("Method not implemented!") @@ -1376,6 +1600,8 @@ def ResetActivity(self, request, context): 'keep_paused': if the activity is paused, it will remain paused. Returns a `NotFound` error if there is no pending activity with the provided ID or type. + This API will be deprecated soon and replaced with a newer ResetActivityExecution that is better named and + structured to work well for standalone activities. """ context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details("Method not implemented!") @@ -1434,6 +1660,12 @@ def ListWorkers(self, request, context): context.set_details("Method not implemented!") raise NotImplementedError("Method not implemented!") + def CountWorkers(self, request, context): + """CountWorkers counts the number of workers in a specific namespace.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + def UpdateTaskQueueConfig(self, request, context): """Updates task queue configuration. For the overall queue rate limit: the rate limit set by this api overrides the worker-set rate limit, @@ -1459,6 +1691,252 @@ def UpdateWorkerConfig(self, request, context): context.set_details("Method not implemented!") raise NotImplementedError("Method not implemented!") + def DescribeWorker(self, request, context): + """DescribeWorker returns information about the specified worker.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def PauseWorkflowExecution(self, request, context): + """Note: This is an experimental API and the behavior may change in a future release. + PauseWorkflowExecution pauses the workflow execution specified in the request. Pausing a workflow execution results in + - The workflow execution status changes to `PAUSED` and a new WORKFLOW_EXECUTION_PAUSED event is added to the history + - No new workflow tasks or activity tasks are dispatched. + - Any workflow task currently executing on the worker will be allowed to complete. + - Any activity task currently executing will be paused. + - All server-side events will continue to be processed by the server. + - Queries & Updates on a paused workflow will be rejected. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def UnpauseWorkflowExecution(self, request, context): + """Note: This is an experimental API and the behavior may change in a future release. + UnpauseWorkflowExecution unpauses a previously paused workflow execution specified in the request. + Unpausing a workflow execution results in + - The workflow execution status changes to `RUNNING` and a new WORKFLOW_EXECUTION_UNPAUSED event is added to the history + - Workflow tasks and activity tasks are resumed. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def StartActivityExecution(self, request, context): + """StartActivityExecution starts a new activity execution. + + Returns an `ActivityExecutionAlreadyStarted` error if an instance already exists with same activity ID in this namespace + unless permitted by the specified ID conflict policy. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def StartNexusOperationExecution(self, request, context): + """StartNexusOperationExecution starts a new Nexus operation. + + Returns a `NexusOperationExecutionAlreadyStarted` error if an instance already exists with same operation ID in this + namespace unless permitted by the specified ID conflict policy. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def DescribeActivityExecution(self, request, context): + """DescribeActivityExecution returns information about an activity execution. + It can be used to: + - Get current activity info without waiting + - Long-poll for next state change and return new activity info + Response can optionally include activity input or outcome (if the activity has completed). + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def DescribeNexusOperationExecution(self, request, context): + """DescribeNexusOperationExecution returns information about a Nexus operation. + Supported use cases include: + - Get current operation info without waiting + - Long-poll for next state change and return new operation info + Response can optionally include operation input or outcome (if the operation has completed). + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def PollActivityExecution(self, request, context): + """PollActivityExecution long-polls for an activity execution to complete and returns the + outcome (result or failure). + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def PollNexusOperationExecution(self, request, context): + """PollNexusOperationExecution long-polls for a Nexus operation for a given wait stage to complete and returns + the outcome (result or failure). + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def ListActivityExecutions(self, request, context): + """ListActivityExecutions is a visibility API to list activity executions in a specific namespace.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def ListNexusOperationExecutions(self, request, context): + """ListNexusOperationExecutions is a visibility API to list Nexus operations in a specific namespace.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def CountActivityExecutions(self, request, context): + """CountActivityExecutions is a visibility API to count activity executions in a specific namespace.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def CountNexusOperationExecutions(self, request, context): + """CountNexusOperationExecutions is a visibility API to count Nexus operations in a specific namespace.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def RequestCancelActivityExecution(self, request, context): + """RequestCancelActivityExecution requests cancellation of an activity execution. + + Cancellation is cooperative: this call records the request, but the activity must detect and + acknowledge it for the activity to reach CANCELED status. The cancellation signal is + delivered via `cancel_requested` in the heartbeat response; SDKs surface this via + language-idiomatic mechanisms (context cancellation, exceptions, abort signals). + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def RequestCancelNexusOperationExecution(self, request, context): + """RequestCancelNexusOperationExecution requests cancellation of a Nexus operation. + + Requesting to cancel an operation does not automatically transition the operation to canceled status. + The operation will only transition to canceled status if it supports cancellation and the handler + processes the cancellation request. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def TerminateActivityExecution(self, request, context): + """TerminateActivityExecution terminates an existing activity execution immediately. + + Termination does not reach the worker and the activity code cannot react to it. A terminated activity may have a + running attempt. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def DeleteActivityExecution(self, request, context): + """DeleteActivityExecution asynchronously deletes a specific activity execution (when + ActivityExecution.run_id is provided) or the latest activity execution (when + ActivityExecution.run_id is not provided). If the activity Execution is running, it will be + terminated before deletion. + + (-- api-linter: core::0127::http-annotation=disabled + aip.dev/not-precedent: Activity deletion not exposed to HTTP, users should use cancel or terminate. --) + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def PauseActivityExecution(self, request, context): + """PauseActivityExecution pauses the execution of an activity specified by its ID. + This API can be used to target a workflow activity or a standalone activity + + Pausing an activity means: + - If the activity is currently waiting for a retry or is running and subsequently fails, + it will not be rescheduled until it is unpaused. + - If the activity is already paused, calling this method will have no effect. + - If the activity is running and finishes successfully, the activity will be completed. + - If the activity is running and finishes with failure: + * if there is no retry left - the activity will be completed. + * if there are more retries left - the activity will be paused. + For long-running activities: + - activities in paused state will send a cancellation with "activity_paused" set to 'true' in response to 'RecordActivityTaskHeartbeat'. + + Returns a `NotFound` error if there is no pending activity with the provided ID + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def ResetActivityExecution(self, request, context): + """ResetActivityExecution resets the execution of an activity specified by its ID. + This API can be used to target a workflow activity or a standalone activity. + + Resetting an activity means: + * number of attempts will be reset to 0. + * activity timeouts will be reset. + * if the activity is waiting for retry, and it is not paused or 'keep_paused' is not provided: + it will be scheduled immediately (* see 'jitter' flag) + + Returns a `NotFound` error if there is no pending activity with the provided ID or type. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def UnpauseActivityExecution(self, request, context): + """UnpauseActivityExecution unpauses the execution of an activity specified by its ID. + This API can be used to target a workflow activity or a standalone activity. + + If activity is not paused, this call will have no effect. + If the activity was paused while waiting for retry, it will be scheduled immediately (* see 'jitter' flag). + Once the activity is unpaused, all timeout timers will be regenerated. + + Returns a `NotFound` error if there is no pending activity with the provided ID + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def UpdateActivityExecutionOptions(self, request, context): + """UpdateActivityExecutionOptions is called by the client to update the options of an activity by its ID. + This API can be used to target a workflow activity or a standalone activity. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def TerminateNexusOperationExecution(self, request, context): + """TerminateNexusOperationExecution terminates an existing Nexus operation immediately. + + Termination happens immediately and the operation handler cannot react to it. A terminated operation will have + its outcome set to a failure with a termination reason. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def DeleteNexusOperationExecution(self, request, context): + """DeleteNexusOperationExecution asynchronously deletes a specific Nexus operation run (when + run_id is provided) or the latest run (when run_id is not provided). If the operation + is running, it will be terminated before deletion. + + (-- api-linter: core::0127::http-annotation=disabled + aip.dev/not-precedent: Nexus operation deletion not exposed to HTTP, users should use cancel or terminate. --) + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def PollWorkflowExecutionTimeSkipping(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + def add_WorkflowServiceServicer_to_server(servicer, server): rpc_method_handlers = { @@ -1712,6 +2190,11 @@ def add_WorkflowServiceServicer_to_server(servicer, server): request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ListSchedulesRequest.FromString, response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ListSchedulesResponse.SerializeToString, ), + "CountSchedules": grpc.unary_unary_rpc_method_handler( + servicer.CountSchedules, + request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CountSchedulesRequest.FromString, + response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CountSchedulesResponse.SerializeToString, + ), "UpdateWorkerBuildIdCompatibility": grpc.unary_unary_rpc_method_handler( servicer.UpdateWorkerBuildIdCompatibility, request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UpdateWorkerBuildIdCompatibilityRequest.FromString, @@ -1797,11 +2280,36 @@ def add_WorkflowServiceServicer_to_server(servicer, server): request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ListWorkerDeploymentsRequest.FromString, response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ListWorkerDeploymentsResponse.SerializeToString, ), + "CreateWorkerDeployment": grpc.unary_unary_rpc_method_handler( + servicer.CreateWorkerDeployment, + request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CreateWorkerDeploymentRequest.FromString, + response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CreateWorkerDeploymentResponse.SerializeToString, + ), + "CreateWorkerDeploymentVersion": grpc.unary_unary_rpc_method_handler( + servicer.CreateWorkerDeploymentVersion, + request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CreateWorkerDeploymentVersionRequest.FromString, + response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CreateWorkerDeploymentVersionResponse.SerializeToString, + ), + "UpdateWorkerDeploymentVersionComputeConfig": grpc.unary_unary_rpc_method_handler( + servicer.UpdateWorkerDeploymentVersionComputeConfig, + request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UpdateWorkerDeploymentVersionComputeConfigRequest.FromString, + response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UpdateWorkerDeploymentVersionComputeConfigResponse.SerializeToString, + ), + "ValidateWorkerDeploymentVersionComputeConfig": grpc.unary_unary_rpc_method_handler( + servicer.ValidateWorkerDeploymentVersionComputeConfig, + request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ValidateWorkerDeploymentVersionComputeConfigRequest.FromString, + response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ValidateWorkerDeploymentVersionComputeConfigResponse.SerializeToString, + ), "UpdateWorkerDeploymentVersionMetadata": grpc.unary_unary_rpc_method_handler( servicer.UpdateWorkerDeploymentVersionMetadata, request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UpdateWorkerDeploymentVersionMetadataRequest.FromString, response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UpdateWorkerDeploymentVersionMetadataResponse.SerializeToString, ), + "SetWorkerDeploymentManager": grpc.unary_unary_rpc_method_handler( + servicer.SetWorkerDeploymentManager, + request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.SetWorkerDeploymentManagerRequest.FromString, + response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.SetWorkerDeploymentManagerResponse.SerializeToString, + ), "UpdateWorkflowExecution": grpc.unary_unary_rpc_method_handler( servicer.UpdateWorkflowExecution, request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UpdateWorkflowExecutionRequest.FromString, @@ -1907,6 +2415,11 @@ def add_WorkflowServiceServicer_to_server(servicer, server): request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ListWorkersRequest.FromString, response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ListWorkersResponse.SerializeToString, ), + "CountWorkers": grpc.unary_unary_rpc_method_handler( + servicer.CountWorkers, + request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CountWorkersRequest.FromString, + response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CountWorkersResponse.SerializeToString, + ), "UpdateTaskQueueConfig": grpc.unary_unary_rpc_method_handler( servicer.UpdateTaskQueueConfig, request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UpdateTaskQueueConfigRequest.FromString, @@ -1922,6 +2435,126 @@ def add_WorkflowServiceServicer_to_server(servicer, server): request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UpdateWorkerConfigRequest.FromString, response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UpdateWorkerConfigResponse.SerializeToString, ), + "DescribeWorker": grpc.unary_unary_rpc_method_handler( + servicer.DescribeWorker, + request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.DescribeWorkerRequest.FromString, + response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.DescribeWorkerResponse.SerializeToString, + ), + "PauseWorkflowExecution": grpc.unary_unary_rpc_method_handler( + servicer.PauseWorkflowExecution, + request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.PauseWorkflowExecutionRequest.FromString, + response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.PauseWorkflowExecutionResponse.SerializeToString, + ), + "UnpauseWorkflowExecution": grpc.unary_unary_rpc_method_handler( + servicer.UnpauseWorkflowExecution, + request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UnpauseWorkflowExecutionRequest.FromString, + response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UnpauseWorkflowExecutionResponse.SerializeToString, + ), + "StartActivityExecution": grpc.unary_unary_rpc_method_handler( + servicer.StartActivityExecution, + request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.StartActivityExecutionRequest.FromString, + response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.StartActivityExecutionResponse.SerializeToString, + ), + "StartNexusOperationExecution": grpc.unary_unary_rpc_method_handler( + servicer.StartNexusOperationExecution, + request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.StartNexusOperationExecutionRequest.FromString, + response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.StartNexusOperationExecutionResponse.SerializeToString, + ), + "DescribeActivityExecution": grpc.unary_unary_rpc_method_handler( + servicer.DescribeActivityExecution, + request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.DescribeActivityExecutionRequest.FromString, + response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.DescribeActivityExecutionResponse.SerializeToString, + ), + "DescribeNexusOperationExecution": grpc.unary_unary_rpc_method_handler( + servicer.DescribeNexusOperationExecution, + request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.DescribeNexusOperationExecutionRequest.FromString, + response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.DescribeNexusOperationExecutionResponse.SerializeToString, + ), + "PollActivityExecution": grpc.unary_unary_rpc_method_handler( + servicer.PollActivityExecution, + request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.PollActivityExecutionRequest.FromString, + response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.PollActivityExecutionResponse.SerializeToString, + ), + "PollNexusOperationExecution": grpc.unary_unary_rpc_method_handler( + servicer.PollNexusOperationExecution, + request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.PollNexusOperationExecutionRequest.FromString, + response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.PollNexusOperationExecutionResponse.SerializeToString, + ), + "ListActivityExecutions": grpc.unary_unary_rpc_method_handler( + servicer.ListActivityExecutions, + request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ListActivityExecutionsRequest.FromString, + response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ListActivityExecutionsResponse.SerializeToString, + ), + "ListNexusOperationExecutions": grpc.unary_unary_rpc_method_handler( + servicer.ListNexusOperationExecutions, + request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ListNexusOperationExecutionsRequest.FromString, + response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ListNexusOperationExecutionsResponse.SerializeToString, + ), + "CountActivityExecutions": grpc.unary_unary_rpc_method_handler( + servicer.CountActivityExecutions, + request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CountActivityExecutionsRequest.FromString, + response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CountActivityExecutionsResponse.SerializeToString, + ), + "CountNexusOperationExecutions": grpc.unary_unary_rpc_method_handler( + servicer.CountNexusOperationExecutions, + request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CountNexusOperationExecutionsRequest.FromString, + response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CountNexusOperationExecutionsResponse.SerializeToString, + ), + "RequestCancelActivityExecution": grpc.unary_unary_rpc_method_handler( + servicer.RequestCancelActivityExecution, + request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.RequestCancelActivityExecutionRequest.FromString, + response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.RequestCancelActivityExecutionResponse.SerializeToString, + ), + "RequestCancelNexusOperationExecution": grpc.unary_unary_rpc_method_handler( + servicer.RequestCancelNexusOperationExecution, + request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.RequestCancelNexusOperationExecutionRequest.FromString, + response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.RequestCancelNexusOperationExecutionResponse.SerializeToString, + ), + "TerminateActivityExecution": grpc.unary_unary_rpc_method_handler( + servicer.TerminateActivityExecution, + request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.TerminateActivityExecutionRequest.FromString, + response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.TerminateActivityExecutionResponse.SerializeToString, + ), + "DeleteActivityExecution": grpc.unary_unary_rpc_method_handler( + servicer.DeleteActivityExecution, + request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.DeleteActivityExecutionRequest.FromString, + response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.DeleteActivityExecutionResponse.SerializeToString, + ), + "PauseActivityExecution": grpc.unary_unary_rpc_method_handler( + servicer.PauseActivityExecution, + request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.PauseActivityExecutionRequest.FromString, + response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.PauseActivityExecutionResponse.SerializeToString, + ), + "ResetActivityExecution": grpc.unary_unary_rpc_method_handler( + servicer.ResetActivityExecution, + request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ResetActivityExecutionRequest.FromString, + response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ResetActivityExecutionResponse.SerializeToString, + ), + "UnpauseActivityExecution": grpc.unary_unary_rpc_method_handler( + servicer.UnpauseActivityExecution, + request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UnpauseActivityExecutionRequest.FromString, + response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UnpauseActivityExecutionResponse.SerializeToString, + ), + "UpdateActivityExecutionOptions": grpc.unary_unary_rpc_method_handler( + servicer.UpdateActivityExecutionOptions, + request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UpdateActivityExecutionOptionsRequest.FromString, + response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UpdateActivityExecutionOptionsResponse.SerializeToString, + ), + "TerminateNexusOperationExecution": grpc.unary_unary_rpc_method_handler( + servicer.TerminateNexusOperationExecution, + request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.TerminateNexusOperationExecutionRequest.FromString, + response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.TerminateNexusOperationExecutionResponse.SerializeToString, + ), + "DeleteNexusOperationExecution": grpc.unary_unary_rpc_method_handler( + servicer.DeleteNexusOperationExecution, + request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.DeleteNexusOperationExecutionRequest.FromString, + response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.DeleteNexusOperationExecutionResponse.SerializeToString, + ), + "PollWorkflowExecutionTimeSkipping": grpc.unary_unary_rpc_method_handler( + servicer.PollWorkflowExecutionTimeSkipping, + request_deserializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.PollWorkflowExecutionTimeSkippingRequest.FromString, + response_serializer=temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.PollWorkflowExecutionTimeSkippingResponse.SerializeToString, + ), } generic_handler = grpc.method_handlers_generic_handler( "temporal.api.workflowservice.v1.WorkflowService", rpc_method_handlers @@ -3394,6 +4027,35 @@ def ListSchedules( metadata, ) + @staticmethod + def CountSchedules( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/temporal.api.workflowservice.v1.WorkflowService/CountSchedules", + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CountSchedulesRequest.SerializeToString, + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CountSchedulesResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + @staticmethod def UpdateWorkerBuildIdCompatibility( request, @@ -3888,7 +4550,123 @@ def ListWorkerDeployments( ) @staticmethod - def UpdateWorkerDeploymentVersionMetadata( + def CreateWorkerDeployment( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/temporal.api.workflowservice.v1.WorkflowService/CreateWorkerDeployment", + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CreateWorkerDeploymentRequest.SerializeToString, + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CreateWorkerDeploymentResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + + @staticmethod + def CreateWorkerDeploymentVersion( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/temporal.api.workflowservice.v1.WorkflowService/CreateWorkerDeploymentVersion", + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CreateWorkerDeploymentVersionRequest.SerializeToString, + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CreateWorkerDeploymentVersionResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + + @staticmethod + def UpdateWorkerDeploymentVersionComputeConfig( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/temporal.api.workflowservice.v1.WorkflowService/UpdateWorkerDeploymentVersionComputeConfig", + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UpdateWorkerDeploymentVersionComputeConfigRequest.SerializeToString, + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UpdateWorkerDeploymentVersionComputeConfigResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + + @staticmethod + def ValidateWorkerDeploymentVersionComputeConfig( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/temporal.api.workflowservice.v1.WorkflowService/ValidateWorkerDeploymentVersionComputeConfig", + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ValidateWorkerDeploymentVersionComputeConfigRequest.SerializeToString, + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ValidateWorkerDeploymentVersionComputeConfigResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + + @staticmethod + def UpdateWorkerDeploymentVersionMetadata( request, target, options=(), @@ -3916,6 +4694,35 @@ def UpdateWorkerDeploymentVersionMetadata( metadata, ) + @staticmethod + def SetWorkerDeploymentManager( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/temporal.api.workflowservice.v1.WorkflowService/SetWorkerDeploymentManager", + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.SetWorkerDeploymentManagerRequest.SerializeToString, + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.SetWorkerDeploymentManagerResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + @staticmethod def UpdateWorkflowExecution( request, @@ -4525,6 +5332,35 @@ def ListWorkers( metadata, ) + @staticmethod + def CountWorkers( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/temporal.api.workflowservice.v1.WorkflowService/CountWorkers", + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CountWorkersRequest.SerializeToString, + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CountWorkersResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + @staticmethod def UpdateTaskQueueConfig( request, @@ -4611,3 +5447,699 @@ def UpdateWorkerConfig( timeout, metadata, ) + + @staticmethod + def DescribeWorker( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/temporal.api.workflowservice.v1.WorkflowService/DescribeWorker", + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.DescribeWorkerRequest.SerializeToString, + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.DescribeWorkerResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + + @staticmethod + def PauseWorkflowExecution( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/temporal.api.workflowservice.v1.WorkflowService/PauseWorkflowExecution", + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.PauseWorkflowExecutionRequest.SerializeToString, + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.PauseWorkflowExecutionResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + + @staticmethod + def UnpauseWorkflowExecution( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/temporal.api.workflowservice.v1.WorkflowService/UnpauseWorkflowExecution", + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UnpauseWorkflowExecutionRequest.SerializeToString, + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UnpauseWorkflowExecutionResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + + @staticmethod + def StartActivityExecution( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/temporal.api.workflowservice.v1.WorkflowService/StartActivityExecution", + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.StartActivityExecutionRequest.SerializeToString, + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.StartActivityExecutionResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + + @staticmethod + def StartNexusOperationExecution( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/temporal.api.workflowservice.v1.WorkflowService/StartNexusOperationExecution", + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.StartNexusOperationExecutionRequest.SerializeToString, + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.StartNexusOperationExecutionResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + + @staticmethod + def DescribeActivityExecution( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/temporal.api.workflowservice.v1.WorkflowService/DescribeActivityExecution", + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.DescribeActivityExecutionRequest.SerializeToString, + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.DescribeActivityExecutionResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + + @staticmethod + def DescribeNexusOperationExecution( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/temporal.api.workflowservice.v1.WorkflowService/DescribeNexusOperationExecution", + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.DescribeNexusOperationExecutionRequest.SerializeToString, + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.DescribeNexusOperationExecutionResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + + @staticmethod + def PollActivityExecution( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/temporal.api.workflowservice.v1.WorkflowService/PollActivityExecution", + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.PollActivityExecutionRequest.SerializeToString, + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.PollActivityExecutionResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + + @staticmethod + def PollNexusOperationExecution( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/temporal.api.workflowservice.v1.WorkflowService/PollNexusOperationExecution", + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.PollNexusOperationExecutionRequest.SerializeToString, + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.PollNexusOperationExecutionResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + + @staticmethod + def ListActivityExecutions( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/temporal.api.workflowservice.v1.WorkflowService/ListActivityExecutions", + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ListActivityExecutionsRequest.SerializeToString, + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ListActivityExecutionsResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + + @staticmethod + def ListNexusOperationExecutions( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/temporal.api.workflowservice.v1.WorkflowService/ListNexusOperationExecutions", + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ListNexusOperationExecutionsRequest.SerializeToString, + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ListNexusOperationExecutionsResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + + @staticmethod + def CountActivityExecutions( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/temporal.api.workflowservice.v1.WorkflowService/CountActivityExecutions", + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CountActivityExecutionsRequest.SerializeToString, + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CountActivityExecutionsResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + + @staticmethod + def CountNexusOperationExecutions( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/temporal.api.workflowservice.v1.WorkflowService/CountNexusOperationExecutions", + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CountNexusOperationExecutionsRequest.SerializeToString, + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.CountNexusOperationExecutionsResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + + @staticmethod + def RequestCancelActivityExecution( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/temporal.api.workflowservice.v1.WorkflowService/RequestCancelActivityExecution", + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.RequestCancelActivityExecutionRequest.SerializeToString, + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.RequestCancelActivityExecutionResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + + @staticmethod + def RequestCancelNexusOperationExecution( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/temporal.api.workflowservice.v1.WorkflowService/RequestCancelNexusOperationExecution", + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.RequestCancelNexusOperationExecutionRequest.SerializeToString, + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.RequestCancelNexusOperationExecutionResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + + @staticmethod + def TerminateActivityExecution( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/temporal.api.workflowservice.v1.WorkflowService/TerminateActivityExecution", + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.TerminateActivityExecutionRequest.SerializeToString, + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.TerminateActivityExecutionResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + + @staticmethod + def DeleteActivityExecution( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/temporal.api.workflowservice.v1.WorkflowService/DeleteActivityExecution", + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.DeleteActivityExecutionRequest.SerializeToString, + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.DeleteActivityExecutionResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + + @staticmethod + def PauseActivityExecution( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/temporal.api.workflowservice.v1.WorkflowService/PauseActivityExecution", + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.PauseActivityExecutionRequest.SerializeToString, + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.PauseActivityExecutionResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + + @staticmethod + def ResetActivityExecution( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/temporal.api.workflowservice.v1.WorkflowService/ResetActivityExecution", + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ResetActivityExecutionRequest.SerializeToString, + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.ResetActivityExecutionResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + + @staticmethod + def UnpauseActivityExecution( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/temporal.api.workflowservice.v1.WorkflowService/UnpauseActivityExecution", + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UnpauseActivityExecutionRequest.SerializeToString, + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UnpauseActivityExecutionResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + + @staticmethod + def UpdateActivityExecutionOptions( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/temporal.api.workflowservice.v1.WorkflowService/UpdateActivityExecutionOptions", + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UpdateActivityExecutionOptionsRequest.SerializeToString, + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.UpdateActivityExecutionOptionsResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + + @staticmethod + def TerminateNexusOperationExecution( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/temporal.api.workflowservice.v1.WorkflowService/TerminateNexusOperationExecution", + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.TerminateNexusOperationExecutionRequest.SerializeToString, + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.TerminateNexusOperationExecutionResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + + @staticmethod + def DeleteNexusOperationExecution( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/temporal.api.workflowservice.v1.WorkflowService/DeleteNexusOperationExecution", + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.DeleteNexusOperationExecutionRequest.SerializeToString, + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.DeleteNexusOperationExecutionResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + + @staticmethod + def PollWorkflowExecutionTimeSkipping( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/temporal.api.workflowservice.v1.WorkflowService/PollWorkflowExecutionTimeSkipping", + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.PollWorkflowExecutionTimeSkippingRequest.SerializeToString, + temporal_dot_api_dot_workflowservice_dot_v1_dot_request__response__pb2.PollWorkflowExecutionTimeSkippingResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) diff --git a/temporalio/api/workflowservice/v1/service_pb2_grpc.pyi b/temporalio/api/workflowservice/v1/service_pb2_grpc.pyi index 6da9c7db3..d25f044b4 100644 --- a/temporalio/api/workflowservice/v1/service_pb2_grpc.pyi +++ b/temporalio/api/workflowservice/v1/service_pb2_grpc.pyi @@ -87,7 +87,8 @@ class WorkflowServiceStub: Upon failure, it returns `MultiOperationExecutionFailure` where the status code equals the status code of the *first* operation that failed to be started. - NOTE: Experimental API. + (-- api-linter: core::0127::http-annotation=disabled + aip.dev/not-precedent: To be exposed over HTTP in the future. --) """ GetWorkflowExecutionHistory: grpc.UnaryUnaryMultiCallable[ temporalio.api.workflowservice.v1.request_response_pb2.GetWorkflowExecutionHistoryRequest, @@ -100,8 +101,8 @@ class WorkflowServiceStub: temporalio.api.workflowservice.v1.request_response_pb2.GetWorkflowExecutionHistoryReverseRequest, temporalio.api.workflowservice.v1.request_response_pb2.GetWorkflowExecutionHistoryReverseResponse, ] - """GetWorkflowExecutionHistoryReverse returns the history of specified workflow execution in reverse - order (starting from last event). Fails with`NotFound` if the specified workflow execution is + """GetWorkflowExecutionHistoryReverse returns the history of specified workflow execution in reverse + order (starting from last event). Fails with`NotFound` if the specified workflow execution is unknown to the service. """ PollWorkflowTaskQueue: grpc.UnaryUnaryMultiCallable[ @@ -175,10 +176,17 @@ class WorkflowServiceStub: ] """RecordActivityTaskHeartbeat is optionally called by workers while they execute activities. - If worker fails to heartbeat within the `heartbeat_timeout` interval for the activity task, - then it will be marked as timed out and an `ACTIVITY_TASK_TIMED_OUT` event will be written to - the workflow history. Calling `RecordActivityTaskHeartbeat` will fail with `NotFound` in - such situations, in that event, the SDK should request cancellation of the activity. + If a worker fails to heartbeat within the `heartbeat_timeout` interval for the activity task, + then the current attempt times out. Depending on RetryPolicy, this may trigger a retry or + time out the activity. + + For workflow activities, an `ACTIVITY_TASK_TIMED_OUT` event will be written to the workflow + history. Calling `RecordActivityTaskHeartbeat` will fail with `NotFound` in such situations, + in that event, the SDK should request cancellation of the activity. + + The request may contain response `details` which will be persisted by the server and may be + used by the activity to checkpoint progress. The `cancel_requested` field in the response + indicates whether cancellation has been requested for the activity. """ RecordActivityTaskHeartbeatById: grpc.UnaryUnaryMultiCallable[ temporalio.api.workflowservice.v1.request_response_pb2.RecordActivityTaskHeartbeatByIdRequest, @@ -197,7 +205,7 @@ class WorkflowServiceStub: """RespondActivityTaskCompleted is called by workers when they successfully complete an activity task. - This results in a new `ACTIVITY_TASK_COMPLETED` event being written to the workflow history + For workflow activities, this results in a new `ACTIVITY_TASK_COMPLETED` event being written to the workflow history and a new workflow task created for the workflow. Fails with `NotFound` if the task token is no longer valid due to activity timeout, already being completed, or never having existed. """ @@ -205,7 +213,7 @@ class WorkflowServiceStub: temporalio.api.workflowservice.v1.request_response_pb2.RespondActivityTaskCompletedByIdRequest, temporalio.api.workflowservice.v1.request_response_pb2.RespondActivityTaskCompletedByIdResponse, ] - """See `RecordActivityTaskCompleted`. This version allows clients to record completions by + """See `RespondActivityTaskCompleted`. This version allows clients to record completions by namespace/workflow id/activity id instead of task token. (-- api-linter: core::0136::prepositions=disabled @@ -237,7 +245,7 @@ class WorkflowServiceStub: ] """RespondActivityTaskFailed is called by workers when processing an activity task fails. - This results in a new `ACTIVITY_TASK_CANCELED` event being written to the workflow history + For workflow activities, this results in a new `ACTIVITY_TASK_CANCELED` event being written to the workflow history and a new workflow task created for the workflow. Fails with `NotFound` if the task token is no longer valid due to activity timeout, already being completed, or never having existed. """ @@ -245,7 +253,7 @@ class WorkflowServiceStub: temporalio.api.workflowservice.v1.request_response_pb2.RespondActivityTaskCanceledByIdRequest, temporalio.api.workflowservice.v1.request_response_pb2.RespondActivityTaskCanceledByIdResponse, ] - """See `RecordActivityTaskCanceled`. This version allows clients to record failures by + """See `RespondActivityTaskCanceled`. This version allows clients to record failures by namespace/workflow id/activity id instead of task token. (-- api-linter: core::0136::prepositions=disabled @@ -294,8 +302,9 @@ class WorkflowServiceStub: ] """ResetWorkflowExecution will reset an existing workflow execution to a specified `WORKFLOW_TASK_COMPLETED` event (exclusive). It will immediately terminate the current - execution instance. - TODO: Does exclusive here mean *just* the completed event, or also WFT started? Otherwise the task is doomed to time out? + execution instance. "Exclusive" means the identified completed event itself is not replayed + in the reset history; the preceding `WORKFLOW_TASK_STARTED` event remains and will be marked as failed + immediately, and a new workflow task will be scheduled to retry it. """ TerminateWorkflowExecution: grpc.UnaryUnaryMultiCallable[ temporalio.api.workflowservice.v1.request_response_pb2.TerminateWorkflowExecutionRequest, @@ -349,7 +358,8 @@ class WorkflowServiceStub: temporalio.api.workflowservice.v1.request_response_pb2.ScanWorkflowExecutionsRequest, temporalio.api.workflowservice.v1.request_response_pb2.ScanWorkflowExecutionsResponse, ] - """ScanWorkflowExecutions is a visibility API to list large amount of workflow executions in a specific namespace without order. + """ScanWorkflowExecutions _was_ a visibility API to list large amount of workflow executions in a specific namespace without order. + It has since been deprecated in favor of `ListWorkflowExecutions` and rewritten to use `ListWorkflowExecutions` internally. Deprecated: Replaced with `ListWorkflowExecutions`. (-- api-linter: core::0127::http-annotation=disabled @@ -490,11 +500,17 @@ class WorkflowServiceStub: temporalio.api.workflowservice.v1.request_response_pb2.ListSchedulesResponse, ] """List all schedules in a namespace.""" + CountSchedules: grpc.UnaryUnaryMultiCallable[ + temporalio.api.workflowservice.v1.request_response_pb2.CountSchedulesRequest, + temporalio.api.workflowservice.v1.request_response_pb2.CountSchedulesResponse, + ] + """CountSchedules is a visibility API to count schedules in a specific namespace.""" UpdateWorkerBuildIdCompatibility: grpc.UnaryUnaryMultiCallable[ temporalio.api.workflowservice.v1.request_response_pb2.UpdateWorkerBuildIdCompatibilityRequest, temporalio.api.workflowservice.v1.request_response_pb2.UpdateWorkerBuildIdCompatibilityResponse, ] """Deprecated. Use `UpdateWorkerVersioningRules`. + Will be removed in server version v1.32.0. Allows users to specify sets of worker build id versions on a per task queue basis. Versions are ordered, and may be either compatible with some extant version, or a new incompatible @@ -502,7 +518,7 @@ class WorkflowServiceStub: members are compatible with one another. A single build id may be mapped to multiple task queues using this API for cases where a single process hosts - multiple workers. + multiple workers. To query which workers can be retired, use the `GetWorkerTaskReachability` API. @@ -517,6 +533,7 @@ class WorkflowServiceStub: temporalio.api.workflowservice.v1.request_response_pb2.GetWorkerBuildIdCompatibilityResponse, ] """Deprecated. Use `GetWorkerVersioningRules`. + Will be removed in server version v1.32.0. Fetches the worker build id versioning sets for a task queue. """ UpdateWorkerVersioningRules: grpc.UnaryUnaryMultiCallable[ @@ -544,7 +561,7 @@ class WorkflowServiceStub: the target Build ID of a redirect rule is able to process event histories made by the source Build ID by using [Patching](https://docs.temporal.io/workflows#patching) or other means. - WARNING: Worker Versioning is not yet stable and the API and behavior may change incompatibly. + Will be removed in server version v1.32.0. (-- api-linter: core::0127::http-annotation=disabled aip.dev/not-precedent: We do yet expose versioning API to HTTP. --) """ @@ -553,13 +570,14 @@ class WorkflowServiceStub: temporalio.api.workflowservice.v1.request_response_pb2.GetWorkerVersioningRulesResponse, ] """Fetches the Build ID assignment and redirect rules for a Task Queue. - WARNING: Worker Versioning is not yet stable and the API and behavior may change incompatibly. + Will be removed in server version v1.32.0. """ GetWorkerTaskReachability: grpc.UnaryUnaryMultiCallable[ temporalio.api.workflowservice.v1.request_response_pb2.GetWorkerTaskReachabilityRequest, temporalio.api.workflowservice.v1.request_response_pb2.GetWorkerTaskReachabilityResponse, ] """Deprecated. Use `DescribeTaskQueue`. + Will be removed in server version v1.32.0. Fetches task reachability to determine whether a worker may be retired. The request may specify task queues to query for or let the server fetch all task queues mapped to the given @@ -678,6 +696,38 @@ class WorkflowServiceStub: """Lists all Worker Deployments that are tracked in the Namespace. Experimental. This API might significantly change or be removed in a future release. """ + CreateWorkerDeployment: grpc.UnaryUnaryMultiCallable[ + temporalio.api.workflowservice.v1.request_response_pb2.CreateWorkerDeploymentRequest, + temporalio.api.workflowservice.v1.request_response_pb2.CreateWorkerDeploymentResponse, + ] + """Creates a new Worker Deployment. + + Experimental. This API might significantly change or be removed in a + future release. + """ + CreateWorkerDeploymentVersion: grpc.UnaryUnaryMultiCallable[ + temporalio.api.workflowservice.v1.request_response_pb2.CreateWorkerDeploymentVersionRequest, + temporalio.api.workflowservice.v1.request_response_pb2.CreateWorkerDeploymentVersionResponse, + ] + """Creates a new Worker Deployment Version. + + Experimental. This API might significantly change or be removed in a + future release. + """ + UpdateWorkerDeploymentVersionComputeConfig: grpc.UnaryUnaryMultiCallable[ + temporalio.api.workflowservice.v1.request_response_pb2.UpdateWorkerDeploymentVersionComputeConfigRequest, + temporalio.api.workflowservice.v1.request_response_pb2.UpdateWorkerDeploymentVersionComputeConfigResponse, + ] + """Updates the compute config attached to a Worker Deployment Version. + Experimental. This API might significantly change or be removed in a future release. + """ + ValidateWorkerDeploymentVersionComputeConfig: grpc.UnaryUnaryMultiCallable[ + temporalio.api.workflowservice.v1.request_response_pb2.ValidateWorkerDeploymentVersionComputeConfigRequest, + temporalio.api.workflowservice.v1.request_response_pb2.ValidateWorkerDeploymentVersionComputeConfigResponse, + ] + """Validates the compute config without attaching it to a Worker Deployment Version. + Experimental. This API might significantly change or be removed in a future release. + """ UpdateWorkerDeploymentVersionMetadata: grpc.UnaryUnaryMultiCallable[ temporalio.api.workflowservice.v1.request_response_pb2.UpdateWorkerDeploymentVersionMetadataRequest, temporalio.api.workflowservice.v1.request_response_pb2.UpdateWorkerDeploymentVersionMetadataResponse, @@ -685,6 +735,13 @@ class WorkflowServiceStub: """Updates the user-given metadata attached to a Worker Deployment Version. Experimental. This API might significantly change or be removed in a future release. """ + SetWorkerDeploymentManager: grpc.UnaryUnaryMultiCallable[ + temporalio.api.workflowservice.v1.request_response_pb2.SetWorkerDeploymentManagerRequest, + temporalio.api.workflowservice.v1.request_response_pb2.SetWorkerDeploymentManagerResponse, + ] + """Set/unset the ManagerIdentity of a Worker Deployment. + Experimental. This API might significantly change or be removed in a future release. + """ UpdateWorkflowExecution: grpc.UnaryUnaryMultiCallable[ temporalio.api.workflowservice.v1.request_response_pb2.UpdateWorkflowExecutionRequest, temporalio.api.workflowservice.v1.request_response_pb2.UpdateWorkflowExecutionResponse, @@ -752,6 +809,8 @@ class WorkflowServiceStub: ] """UpdateActivityOptions is called by the client to update the options of an activity by its ID or type. If there are multiple pending activities of the provided type - all of them will be updated. + This API will be deprecated soon and replaced with a newer UpdateActivityExecutionOptions that is better named and + structured to work well for standalone activities. """ UpdateWorkflowExecutionOptions: grpc.UnaryUnaryMultiCallable[ temporalio.api.workflowservice.v1.request_response_pb2.UpdateWorkflowExecutionOptionsRequest, @@ -778,6 +837,8 @@ class WorkflowServiceStub: - The activity should respond to the cancellation accordingly. Returns a `NotFound` error if there is no pending activity with the provided ID or type + This API will be deprecated soon and replaced with a newer PauseActivityExecution that is better named and + structured to work well for standalone activities. """ UnpauseActivity: grpc.UnaryUnaryMultiCallable[ temporalio.api.workflowservice.v1.request_response_pb2.UnpauseActivityRequest, @@ -796,6 +857,8 @@ class WorkflowServiceStub: 'reset_heartbeat': the activity heartbeat timer and heartbeats will be reset. Returns a `NotFound` error if there is no pending activity with the provided ID or type + This API will be deprecated soon and replaced with a newer UnpauseActivityExecution that is better named and + structured to work well for standalone activities. """ ResetActivity: grpc.UnaryUnaryMultiCallable[ temporalio.api.workflowservice.v1.request_response_pb2.ResetActivityRequest, @@ -818,6 +881,8 @@ class WorkflowServiceStub: 'keep_paused': if the activity is paused, it will remain paused. Returns a `NotFound` error if there is no pending activity with the provided ID or type. + This API will be deprecated soon and replaced with a newer ResetActivityExecution that is better named and + structured to work well for standalone activities. """ CreateWorkflowRule: grpc.UnaryUnaryMultiCallable[ temporalio.api.workflowservice.v1.request_response_pb2.CreateWorkflowRuleRequest, @@ -865,6 +930,11 @@ class WorkflowServiceStub: temporalio.api.workflowservice.v1.request_response_pb2.ListWorkersResponse, ] """ListWorkers is a visibility API to list worker status information in a specific namespace.""" + CountWorkers: grpc.UnaryUnaryMultiCallable[ + temporalio.api.workflowservice.v1.request_response_pb2.CountWorkersRequest, + temporalio.api.workflowservice.v1.request_response_pb2.CountWorkersResponse, + ] + """CountWorkers counts the number of workers in a specific namespace.""" UpdateTaskQueueConfig: grpc.UnaryUnaryMultiCallable[ temporalio.api.workflowservice.v1.request_response_pb2.UpdateTaskQueueConfigRequest, temporalio.api.workflowservice.v1.request_response_pb2.UpdateTaskQueueConfigResponse, @@ -887,6 +957,227 @@ class WorkflowServiceStub: Can be used to partially update the worker configuration. Can be used to update the configuration of multiple workers. """ + DescribeWorker: grpc.UnaryUnaryMultiCallable[ + temporalio.api.workflowservice.v1.request_response_pb2.DescribeWorkerRequest, + temporalio.api.workflowservice.v1.request_response_pb2.DescribeWorkerResponse, + ] + """DescribeWorker returns information about the specified worker.""" + PauseWorkflowExecution: grpc.UnaryUnaryMultiCallable[ + temporalio.api.workflowservice.v1.request_response_pb2.PauseWorkflowExecutionRequest, + temporalio.api.workflowservice.v1.request_response_pb2.PauseWorkflowExecutionResponse, + ] + """Note: This is an experimental API and the behavior may change in a future release. + PauseWorkflowExecution pauses the workflow execution specified in the request. Pausing a workflow execution results in + - The workflow execution status changes to `PAUSED` and a new WORKFLOW_EXECUTION_PAUSED event is added to the history + - No new workflow tasks or activity tasks are dispatched. + - Any workflow task currently executing on the worker will be allowed to complete. + - Any activity task currently executing will be paused. + - All server-side events will continue to be processed by the server. + - Queries & Updates on a paused workflow will be rejected. + """ + UnpauseWorkflowExecution: grpc.UnaryUnaryMultiCallable[ + temporalio.api.workflowservice.v1.request_response_pb2.UnpauseWorkflowExecutionRequest, + temporalio.api.workflowservice.v1.request_response_pb2.UnpauseWorkflowExecutionResponse, + ] + """Note: This is an experimental API and the behavior may change in a future release. + UnpauseWorkflowExecution unpauses a previously paused workflow execution specified in the request. + Unpausing a workflow execution results in + - The workflow execution status changes to `RUNNING` and a new WORKFLOW_EXECUTION_UNPAUSED event is added to the history + - Workflow tasks and activity tasks are resumed. + """ + StartActivityExecution: grpc.UnaryUnaryMultiCallable[ + temporalio.api.workflowservice.v1.request_response_pb2.StartActivityExecutionRequest, + temporalio.api.workflowservice.v1.request_response_pb2.StartActivityExecutionResponse, + ] + """StartActivityExecution starts a new activity execution. + + Returns an `ActivityExecutionAlreadyStarted` error if an instance already exists with same activity ID in this namespace + unless permitted by the specified ID conflict policy. + """ + StartNexusOperationExecution: grpc.UnaryUnaryMultiCallable[ + temporalio.api.workflowservice.v1.request_response_pb2.StartNexusOperationExecutionRequest, + temporalio.api.workflowservice.v1.request_response_pb2.StartNexusOperationExecutionResponse, + ] + """StartNexusOperationExecution starts a new Nexus operation. + + Returns a `NexusOperationExecutionAlreadyStarted` error if an instance already exists with same operation ID in this + namespace unless permitted by the specified ID conflict policy. + """ + DescribeActivityExecution: grpc.UnaryUnaryMultiCallable[ + temporalio.api.workflowservice.v1.request_response_pb2.DescribeActivityExecutionRequest, + temporalio.api.workflowservice.v1.request_response_pb2.DescribeActivityExecutionResponse, + ] + """DescribeActivityExecution returns information about an activity execution. + It can be used to: + - Get current activity info without waiting + - Long-poll for next state change and return new activity info + Response can optionally include activity input or outcome (if the activity has completed). + """ + DescribeNexusOperationExecution: grpc.UnaryUnaryMultiCallable[ + temporalio.api.workflowservice.v1.request_response_pb2.DescribeNexusOperationExecutionRequest, + temporalio.api.workflowservice.v1.request_response_pb2.DescribeNexusOperationExecutionResponse, + ] + """DescribeNexusOperationExecution returns information about a Nexus operation. + Supported use cases include: + - Get current operation info without waiting + - Long-poll for next state change and return new operation info + Response can optionally include operation input or outcome (if the operation has completed). + """ + PollActivityExecution: grpc.UnaryUnaryMultiCallable[ + temporalio.api.workflowservice.v1.request_response_pb2.PollActivityExecutionRequest, + temporalio.api.workflowservice.v1.request_response_pb2.PollActivityExecutionResponse, + ] + """PollActivityExecution long-polls for an activity execution to complete and returns the + outcome (result or failure). + """ + PollNexusOperationExecution: grpc.UnaryUnaryMultiCallable[ + temporalio.api.workflowservice.v1.request_response_pb2.PollNexusOperationExecutionRequest, + temporalio.api.workflowservice.v1.request_response_pb2.PollNexusOperationExecutionResponse, + ] + """PollNexusOperationExecution long-polls for a Nexus operation for a given wait stage to complete and returns + the outcome (result or failure). + """ + ListActivityExecutions: grpc.UnaryUnaryMultiCallable[ + temporalio.api.workflowservice.v1.request_response_pb2.ListActivityExecutionsRequest, + temporalio.api.workflowservice.v1.request_response_pb2.ListActivityExecutionsResponse, + ] + """ListActivityExecutions is a visibility API to list activity executions in a specific namespace.""" + ListNexusOperationExecutions: grpc.UnaryUnaryMultiCallable[ + temporalio.api.workflowservice.v1.request_response_pb2.ListNexusOperationExecutionsRequest, + temporalio.api.workflowservice.v1.request_response_pb2.ListNexusOperationExecutionsResponse, + ] + """ListNexusOperationExecutions is a visibility API to list Nexus operations in a specific namespace.""" + CountActivityExecutions: grpc.UnaryUnaryMultiCallable[ + temporalio.api.workflowservice.v1.request_response_pb2.CountActivityExecutionsRequest, + temporalio.api.workflowservice.v1.request_response_pb2.CountActivityExecutionsResponse, + ] + """CountActivityExecutions is a visibility API to count activity executions in a specific namespace.""" + CountNexusOperationExecutions: grpc.UnaryUnaryMultiCallable[ + temporalio.api.workflowservice.v1.request_response_pb2.CountNexusOperationExecutionsRequest, + temporalio.api.workflowservice.v1.request_response_pb2.CountNexusOperationExecutionsResponse, + ] + """CountNexusOperationExecutions is a visibility API to count Nexus operations in a specific namespace.""" + RequestCancelActivityExecution: grpc.UnaryUnaryMultiCallable[ + temporalio.api.workflowservice.v1.request_response_pb2.RequestCancelActivityExecutionRequest, + temporalio.api.workflowservice.v1.request_response_pb2.RequestCancelActivityExecutionResponse, + ] + """RequestCancelActivityExecution requests cancellation of an activity execution. + + Cancellation is cooperative: this call records the request, but the activity must detect and + acknowledge it for the activity to reach CANCELED status. The cancellation signal is + delivered via `cancel_requested` in the heartbeat response; SDKs surface this via + language-idiomatic mechanisms (context cancellation, exceptions, abort signals). + """ + RequestCancelNexusOperationExecution: grpc.UnaryUnaryMultiCallable[ + temporalio.api.workflowservice.v1.request_response_pb2.RequestCancelNexusOperationExecutionRequest, + temporalio.api.workflowservice.v1.request_response_pb2.RequestCancelNexusOperationExecutionResponse, + ] + """RequestCancelNexusOperationExecution requests cancellation of a Nexus operation. + + Requesting to cancel an operation does not automatically transition the operation to canceled status. + The operation will only transition to canceled status if it supports cancellation and the handler + processes the cancellation request. + """ + TerminateActivityExecution: grpc.UnaryUnaryMultiCallable[ + temporalio.api.workflowservice.v1.request_response_pb2.TerminateActivityExecutionRequest, + temporalio.api.workflowservice.v1.request_response_pb2.TerminateActivityExecutionResponse, + ] + """TerminateActivityExecution terminates an existing activity execution immediately. + + Termination does not reach the worker and the activity code cannot react to it. A terminated activity may have a + running attempt. + """ + DeleteActivityExecution: grpc.UnaryUnaryMultiCallable[ + temporalio.api.workflowservice.v1.request_response_pb2.DeleteActivityExecutionRequest, + temporalio.api.workflowservice.v1.request_response_pb2.DeleteActivityExecutionResponse, + ] + """DeleteActivityExecution asynchronously deletes a specific activity execution (when + ActivityExecution.run_id is provided) or the latest activity execution (when + ActivityExecution.run_id is not provided). If the activity Execution is running, it will be + terminated before deletion. + + (-- api-linter: core::0127::http-annotation=disabled + aip.dev/not-precedent: Activity deletion not exposed to HTTP, users should use cancel or terminate. --) + """ + PauseActivityExecution: grpc.UnaryUnaryMultiCallable[ + temporalio.api.workflowservice.v1.request_response_pb2.PauseActivityExecutionRequest, + temporalio.api.workflowservice.v1.request_response_pb2.PauseActivityExecutionResponse, + ] + """PauseActivityExecution pauses the execution of an activity specified by its ID. + This API can be used to target a workflow activity or a standalone activity + + Pausing an activity means: + - If the activity is currently waiting for a retry or is running and subsequently fails, + it will not be rescheduled until it is unpaused. + - If the activity is already paused, calling this method will have no effect. + - If the activity is running and finishes successfully, the activity will be completed. + - If the activity is running and finishes with failure: + * if there is no retry left - the activity will be completed. + * if there are more retries left - the activity will be paused. + For long-running activities: + - activities in paused state will send a cancellation with "activity_paused" set to 'true' in response to 'RecordActivityTaskHeartbeat'. + + Returns a `NotFound` error if there is no pending activity with the provided ID + """ + ResetActivityExecution: grpc.UnaryUnaryMultiCallable[ + temporalio.api.workflowservice.v1.request_response_pb2.ResetActivityExecutionRequest, + temporalio.api.workflowservice.v1.request_response_pb2.ResetActivityExecutionResponse, + ] + """ResetActivityExecution resets the execution of an activity specified by its ID. + This API can be used to target a workflow activity or a standalone activity. + + Resetting an activity means: + * number of attempts will be reset to 0. + * activity timeouts will be reset. + * if the activity is waiting for retry, and it is not paused or 'keep_paused' is not provided: + it will be scheduled immediately (* see 'jitter' flag) + + Returns a `NotFound` error if there is no pending activity with the provided ID or type. + """ + UnpauseActivityExecution: grpc.UnaryUnaryMultiCallable[ + temporalio.api.workflowservice.v1.request_response_pb2.UnpauseActivityExecutionRequest, + temporalio.api.workflowservice.v1.request_response_pb2.UnpauseActivityExecutionResponse, + ] + """UnpauseActivityExecution unpauses the execution of an activity specified by its ID. + This API can be used to target a workflow activity or a standalone activity. + + If activity is not paused, this call will have no effect. + If the activity was paused while waiting for retry, it will be scheduled immediately (* see 'jitter' flag). + Once the activity is unpaused, all timeout timers will be regenerated. + + Returns a `NotFound` error if there is no pending activity with the provided ID + """ + UpdateActivityExecutionOptions: grpc.UnaryUnaryMultiCallable[ + temporalio.api.workflowservice.v1.request_response_pb2.UpdateActivityExecutionOptionsRequest, + temporalio.api.workflowservice.v1.request_response_pb2.UpdateActivityExecutionOptionsResponse, + ] + """UpdateActivityExecutionOptions is called by the client to update the options of an activity by its ID. + This API can be used to target a workflow activity or a standalone activity. + """ + TerminateNexusOperationExecution: grpc.UnaryUnaryMultiCallable[ + temporalio.api.workflowservice.v1.request_response_pb2.TerminateNexusOperationExecutionRequest, + temporalio.api.workflowservice.v1.request_response_pb2.TerminateNexusOperationExecutionResponse, + ] + """TerminateNexusOperationExecution terminates an existing Nexus operation immediately. + + Termination happens immediately and the operation handler cannot react to it. A terminated operation will have + its outcome set to a failure with a termination reason. + """ + DeleteNexusOperationExecution: grpc.UnaryUnaryMultiCallable[ + temporalio.api.workflowservice.v1.request_response_pb2.DeleteNexusOperationExecutionRequest, + temporalio.api.workflowservice.v1.request_response_pb2.DeleteNexusOperationExecutionResponse, + ] + """DeleteNexusOperationExecution asynchronously deletes a specific Nexus operation run (when + run_id is provided) or the latest run (when run_id is not provided). If the operation + is running, it will be terminated before deletion. + + (-- api-linter: core::0127::http-annotation=disabled + aip.dev/not-precedent: Nexus operation deletion not exposed to HTTP, users should use cancel or terminate. --) + """ + PollWorkflowExecutionTimeSkipping: grpc.UnaryUnaryMultiCallable[ + temporalio.api.workflowservice.v1.request_response_pb2.PollWorkflowExecutionTimeSkippingRequest, + temporalio.api.workflowservice.v1.request_response_pb2.PollWorkflowExecutionTimeSkippingResponse, + ] class WorkflowServiceServicer(metaclass=abc.ABCMeta): """WorkflowService API defines how Temporal SDKs and other clients interact with the Temporal server @@ -983,7 +1274,8 @@ class WorkflowServiceServicer(metaclass=abc.ABCMeta): Upon failure, it returns `MultiOperationExecutionFailure` where the status code equals the status code of the *first* operation that failed to be started. - NOTE: Experimental API. + (-- api-linter: core::0127::http-annotation=disabled + aip.dev/not-precedent: To be exposed over HTTP in the future. --) """ @abc.abstractmethod def GetWorkflowExecutionHistory( @@ -1085,10 +1377,17 @@ class WorkflowServiceServicer(metaclass=abc.ABCMeta): ) -> temporalio.api.workflowservice.v1.request_response_pb2.RecordActivityTaskHeartbeatResponse: """RecordActivityTaskHeartbeat is optionally called by workers while they execute activities. - If worker fails to heartbeat within the `heartbeat_timeout` interval for the activity task, - then it will be marked as timed out and an `ACTIVITY_TASK_TIMED_OUT` event will be written to - the workflow history. Calling `RecordActivityTaskHeartbeat` will fail with `NotFound` in - such situations, in that event, the SDK should request cancellation of the activity. + If a worker fails to heartbeat within the `heartbeat_timeout` interval for the activity task, + then the current attempt times out. Depending on RetryPolicy, this may trigger a retry or + time out the activity. + + For workflow activities, an `ACTIVITY_TASK_TIMED_OUT` event will be written to the workflow + history. Calling `RecordActivityTaskHeartbeat` will fail with `NotFound` in such situations, + in that event, the SDK should request cancellation of the activity. + + The request may contain response `details` which will be persisted by the server and may be + used by the activity to checkpoint progress. The `cancel_requested` field in the response + indicates whether cancellation has been requested for the activity. """ @abc.abstractmethod def RecordActivityTaskHeartbeatById( @@ -1111,7 +1410,7 @@ class WorkflowServiceServicer(metaclass=abc.ABCMeta): """RespondActivityTaskCompleted is called by workers when they successfully complete an activity task. - This results in a new `ACTIVITY_TASK_COMPLETED` event being written to the workflow history + For workflow activities, this results in a new `ACTIVITY_TASK_COMPLETED` event being written to the workflow history and a new workflow task created for the workflow. Fails with `NotFound` if the task token is no longer valid due to activity timeout, already being completed, or never having existed. """ @@ -1121,7 +1420,7 @@ class WorkflowServiceServicer(metaclass=abc.ABCMeta): request: temporalio.api.workflowservice.v1.request_response_pb2.RespondActivityTaskCompletedByIdRequest, context: grpc.ServicerContext, ) -> temporalio.api.workflowservice.v1.request_response_pb2.RespondActivityTaskCompletedByIdResponse: - """See `RecordActivityTaskCompleted`. This version allows clients to record completions by + """See `RespondActivityTaskCompleted`. This version allows clients to record completions by namespace/workflow id/activity id instead of task token. (-- api-linter: core::0136::prepositions=disabled @@ -1159,7 +1458,7 @@ class WorkflowServiceServicer(metaclass=abc.ABCMeta): ) -> temporalio.api.workflowservice.v1.request_response_pb2.RespondActivityTaskCanceledResponse: """RespondActivityTaskFailed is called by workers when processing an activity task fails. - This results in a new `ACTIVITY_TASK_CANCELED` event being written to the workflow history + For workflow activities, this results in a new `ACTIVITY_TASK_CANCELED` event being written to the workflow history and a new workflow task created for the workflow. Fails with `NotFound` if the task token is no longer valid due to activity timeout, already being completed, or never having existed. """ @@ -1169,7 +1468,7 @@ class WorkflowServiceServicer(metaclass=abc.ABCMeta): request: temporalio.api.workflowservice.v1.request_response_pb2.RespondActivityTaskCanceledByIdRequest, context: grpc.ServicerContext, ) -> temporalio.api.workflowservice.v1.request_response_pb2.RespondActivityTaskCanceledByIdResponse: - """See `RecordActivityTaskCanceled`. This version allows clients to record failures by + """See `RespondActivityTaskCanceled`. This version allows clients to record failures by namespace/workflow id/activity id instead of task token. (-- api-linter: core::0136::prepositions=disabled @@ -1226,8 +1525,9 @@ class WorkflowServiceServicer(metaclass=abc.ABCMeta): ) -> temporalio.api.workflowservice.v1.request_response_pb2.ResetWorkflowExecutionResponse: """ResetWorkflowExecution will reset an existing workflow execution to a specified `WORKFLOW_TASK_COMPLETED` event (exclusive). It will immediately terminate the current - execution instance. - TODO: Does exclusive here mean *just* the completed event, or also WFT started? Otherwise the task is doomed to time out? + execution instance. "Exclusive" means the identified completed event itself is not replayed + in the reset history; the preceding `WORKFLOW_TASK_STARTED` event remains and will be marked as failed + immediately, and a new workflow task will be scheduled to retry it. """ @abc.abstractmethod def TerminateWorkflowExecution( @@ -1295,7 +1595,8 @@ class WorkflowServiceServicer(metaclass=abc.ABCMeta): request: temporalio.api.workflowservice.v1.request_response_pb2.ScanWorkflowExecutionsRequest, context: grpc.ServicerContext, ) -> temporalio.api.workflowservice.v1.request_response_pb2.ScanWorkflowExecutionsResponse: - """ScanWorkflowExecutions is a visibility API to list large amount of workflow executions in a specific namespace without order. + """ScanWorkflowExecutions _was_ a visibility API to list large amount of workflow executions in a specific namespace without order. + It has since been deprecated in favor of `ListWorkflowExecutions` and rewritten to use `ListWorkflowExecutions` internally. Deprecated: Replaced with `ListWorkflowExecutions`. (-- api-linter: core::0127::http-annotation=disabled @@ -1477,12 +1778,20 @@ class WorkflowServiceServicer(metaclass=abc.ABCMeta): ) -> temporalio.api.workflowservice.v1.request_response_pb2.ListSchedulesResponse: """List all schedules in a namespace.""" @abc.abstractmethod + def CountSchedules( + self, + request: temporalio.api.workflowservice.v1.request_response_pb2.CountSchedulesRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.CountSchedulesResponse: + """CountSchedules is a visibility API to count schedules in a specific namespace.""" + @abc.abstractmethod def UpdateWorkerBuildIdCompatibility( self, request: temporalio.api.workflowservice.v1.request_response_pb2.UpdateWorkerBuildIdCompatibilityRequest, context: grpc.ServicerContext, ) -> temporalio.api.workflowservice.v1.request_response_pb2.UpdateWorkerBuildIdCompatibilityResponse: """Deprecated. Use `UpdateWorkerVersioningRules`. + Will be removed in server version v1.32.0. Allows users to specify sets of worker build id versions on a per task queue basis. Versions are ordered, and may be either compatible with some extant version, or a new incompatible @@ -1507,6 +1816,7 @@ class WorkflowServiceServicer(metaclass=abc.ABCMeta): context: grpc.ServicerContext, ) -> temporalio.api.workflowservice.v1.request_response_pb2.GetWorkerBuildIdCompatibilityResponse: """Deprecated. Use `GetWorkerVersioningRules`. + Will be removed in server version v1.32.0. Fetches the worker build id versioning sets for a task queue. """ @abc.abstractmethod @@ -1536,7 +1846,7 @@ class WorkflowServiceServicer(metaclass=abc.ABCMeta): the target Build ID of a redirect rule is able to process event histories made by the source Build ID by using [Patching](https://docs.temporal.io/workflows#patching) or other means. - WARNING: Worker Versioning is not yet stable and the API and behavior may change incompatibly. + Will be removed in server version v1.32.0. (-- api-linter: core::0127::http-annotation=disabled aip.dev/not-precedent: We do yet expose versioning API to HTTP. --) """ @@ -1547,7 +1857,7 @@ class WorkflowServiceServicer(metaclass=abc.ABCMeta): context: grpc.ServicerContext, ) -> temporalio.api.workflowservice.v1.request_response_pb2.GetWorkerVersioningRulesResponse: """Fetches the Build ID assignment and redirect rules for a Task Queue. - WARNING: Worker Versioning is not yet stable and the API and behavior may change incompatibly. + Will be removed in server version v1.32.0. """ @abc.abstractmethod def GetWorkerTaskReachability( @@ -1556,6 +1866,7 @@ class WorkflowServiceServicer(metaclass=abc.ABCMeta): context: grpc.ServicerContext, ) -> temporalio.api.workflowservice.v1.request_response_pb2.GetWorkerTaskReachabilityResponse: """Deprecated. Use `DescribeTaskQueue`. + Will be removed in server version v1.32.0. Fetches task reachability to determine whether a worker may be retired. The request may specify task queues to query for or let the server fetch all task queues mapped to the given @@ -1699,6 +2010,46 @@ class WorkflowServiceServicer(metaclass=abc.ABCMeta): Experimental. This API might significantly change or be removed in a future release. """ @abc.abstractmethod + def CreateWorkerDeployment( + self, + request: temporalio.api.workflowservice.v1.request_response_pb2.CreateWorkerDeploymentRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.CreateWorkerDeploymentResponse: + """Creates a new Worker Deployment. + + Experimental. This API might significantly change or be removed in a + future release. + """ + @abc.abstractmethod + def CreateWorkerDeploymentVersion( + self, + request: temporalio.api.workflowservice.v1.request_response_pb2.CreateWorkerDeploymentVersionRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.CreateWorkerDeploymentVersionResponse: + """Creates a new Worker Deployment Version. + + Experimental. This API might significantly change or be removed in a + future release. + """ + @abc.abstractmethod + def UpdateWorkerDeploymentVersionComputeConfig( + self, + request: temporalio.api.workflowservice.v1.request_response_pb2.UpdateWorkerDeploymentVersionComputeConfigRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.UpdateWorkerDeploymentVersionComputeConfigResponse: + """Updates the compute config attached to a Worker Deployment Version. + Experimental. This API might significantly change or be removed in a future release. + """ + @abc.abstractmethod + def ValidateWorkerDeploymentVersionComputeConfig( + self, + request: temporalio.api.workflowservice.v1.request_response_pb2.ValidateWorkerDeploymentVersionComputeConfigRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.ValidateWorkerDeploymentVersionComputeConfigResponse: + """Validates the compute config without attaching it to a Worker Deployment Version. + Experimental. This API might significantly change or be removed in a future release. + """ + @abc.abstractmethod def UpdateWorkerDeploymentVersionMetadata( self, request: temporalio.api.workflowservice.v1.request_response_pb2.UpdateWorkerDeploymentVersionMetadataRequest, @@ -1708,6 +2059,15 @@ class WorkflowServiceServicer(metaclass=abc.ABCMeta): Experimental. This API might significantly change or be removed in a future release. """ @abc.abstractmethod + def SetWorkerDeploymentManager( + self, + request: temporalio.api.workflowservice.v1.request_response_pb2.SetWorkerDeploymentManagerRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.SetWorkerDeploymentManagerResponse: + """Set/unset the ManagerIdentity of a Worker Deployment. + Experimental. This API might significantly change or be removed in a future release. + """ + @abc.abstractmethod def UpdateWorkflowExecution( self, request: temporalio.api.workflowservice.v1.request_response_pb2.UpdateWorkflowExecutionRequest, @@ -1794,6 +2154,8 @@ class WorkflowServiceServicer(metaclass=abc.ABCMeta): ) -> temporalio.api.workflowservice.v1.request_response_pb2.UpdateActivityOptionsResponse: """UpdateActivityOptions is called by the client to update the options of an activity by its ID or type. If there are multiple pending activities of the provided type - all of them will be updated. + This API will be deprecated soon and replaced with a newer UpdateActivityExecutionOptions that is better named and + structured to work well for standalone activities. """ @abc.abstractmethod def UpdateWorkflowExecutionOptions( @@ -1824,6 +2186,8 @@ class WorkflowServiceServicer(metaclass=abc.ABCMeta): - The activity should respond to the cancellation accordingly. Returns a `NotFound` error if there is no pending activity with the provided ID or type + This API will be deprecated soon and replaced with a newer PauseActivityExecution that is better named and + structured to work well for standalone activities. """ @abc.abstractmethod def UnpauseActivity( @@ -1844,6 +2208,8 @@ class WorkflowServiceServicer(metaclass=abc.ABCMeta): 'reset_heartbeat': the activity heartbeat timer and heartbeats will be reset. Returns a `NotFound` error if there is no pending activity with the provided ID or type + This API will be deprecated soon and replaced with a newer UnpauseActivityExecution that is better named and + structured to work well for standalone activities. """ @abc.abstractmethod def ResetActivity( @@ -1868,6 +2234,8 @@ class WorkflowServiceServicer(metaclass=abc.ABCMeta): 'keep_paused': if the activity is paused, it will remain paused. Returns a `NotFound` error if there is no pending activity with the provided ID or type. + This API will be deprecated soon and replaced with a newer ResetActivityExecution that is better named and + structured to work well for standalone activities. """ @abc.abstractmethod def CreateWorkflowRule( @@ -1932,6 +2300,13 @@ class WorkflowServiceServicer(metaclass=abc.ABCMeta): ) -> temporalio.api.workflowservice.v1.request_response_pb2.ListWorkersResponse: """ListWorkers is a visibility API to list worker status information in a specific namespace.""" @abc.abstractmethod + def CountWorkers( + self, + request: temporalio.api.workflowservice.v1.request_response_pb2.CountWorkersRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.CountWorkersResponse: + """CountWorkers counts the number of workers in a specific namespace.""" + @abc.abstractmethod def UpdateTaskQueueConfig( self, request: temporalio.api.workflowservice.v1.request_response_pb2.UpdateTaskQueueConfigRequest, @@ -1961,6 +2336,275 @@ class WorkflowServiceServicer(metaclass=abc.ABCMeta): Can be used to partially update the worker configuration. Can be used to update the configuration of multiple workers. """ + @abc.abstractmethod + def DescribeWorker( + self, + request: temporalio.api.workflowservice.v1.request_response_pb2.DescribeWorkerRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.DescribeWorkerResponse: + """DescribeWorker returns information about the specified worker.""" + @abc.abstractmethod + def PauseWorkflowExecution( + self, + request: temporalio.api.workflowservice.v1.request_response_pb2.PauseWorkflowExecutionRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.PauseWorkflowExecutionResponse: + """Note: This is an experimental API and the behavior may change in a future release. + PauseWorkflowExecution pauses the workflow execution specified in the request. Pausing a workflow execution results in + - The workflow execution status changes to `PAUSED` and a new WORKFLOW_EXECUTION_PAUSED event is added to the history + - No new workflow tasks or activity tasks are dispatched. + - Any workflow task currently executing on the worker will be allowed to complete. + - Any activity task currently executing will be paused. + - All server-side events will continue to be processed by the server. + - Queries & Updates on a paused workflow will be rejected. + """ + @abc.abstractmethod + def UnpauseWorkflowExecution( + self, + request: temporalio.api.workflowservice.v1.request_response_pb2.UnpauseWorkflowExecutionRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.UnpauseWorkflowExecutionResponse: + """Note: This is an experimental API and the behavior may change in a future release. + UnpauseWorkflowExecution unpauses a previously paused workflow execution specified in the request. + Unpausing a workflow execution results in + - The workflow execution status changes to `RUNNING` and a new WORKFLOW_EXECUTION_UNPAUSED event is added to the history + - Workflow tasks and activity tasks are resumed. + """ + @abc.abstractmethod + def StartActivityExecution( + self, + request: temporalio.api.workflowservice.v1.request_response_pb2.StartActivityExecutionRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.StartActivityExecutionResponse: + """StartActivityExecution starts a new activity execution. + + Returns an `ActivityExecutionAlreadyStarted` error if an instance already exists with same activity ID in this namespace + unless permitted by the specified ID conflict policy. + """ + @abc.abstractmethod + def StartNexusOperationExecution( + self, + request: temporalio.api.workflowservice.v1.request_response_pb2.StartNexusOperationExecutionRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.StartNexusOperationExecutionResponse: + """StartNexusOperationExecution starts a new Nexus operation. + + Returns a `NexusOperationExecutionAlreadyStarted` error if an instance already exists with same operation ID in this + namespace unless permitted by the specified ID conflict policy. + """ + @abc.abstractmethod + def DescribeActivityExecution( + self, + request: temporalio.api.workflowservice.v1.request_response_pb2.DescribeActivityExecutionRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.DescribeActivityExecutionResponse: + """DescribeActivityExecution returns information about an activity execution. + It can be used to: + - Get current activity info without waiting + - Long-poll for next state change and return new activity info + Response can optionally include activity input or outcome (if the activity has completed). + """ + @abc.abstractmethod + def DescribeNexusOperationExecution( + self, + request: temporalio.api.workflowservice.v1.request_response_pb2.DescribeNexusOperationExecutionRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.DescribeNexusOperationExecutionResponse: + """DescribeNexusOperationExecution returns information about a Nexus operation. + Supported use cases include: + - Get current operation info without waiting + - Long-poll for next state change and return new operation info + Response can optionally include operation input or outcome (if the operation has completed). + """ + @abc.abstractmethod + def PollActivityExecution( + self, + request: temporalio.api.workflowservice.v1.request_response_pb2.PollActivityExecutionRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.PollActivityExecutionResponse: + """PollActivityExecution long-polls for an activity execution to complete and returns the + outcome (result or failure). + """ + @abc.abstractmethod + def PollNexusOperationExecution( + self, + request: temporalio.api.workflowservice.v1.request_response_pb2.PollNexusOperationExecutionRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.PollNexusOperationExecutionResponse: + """PollNexusOperationExecution long-polls for a Nexus operation for a given wait stage to complete and returns + the outcome (result or failure). + """ + @abc.abstractmethod + def ListActivityExecutions( + self, + request: temporalio.api.workflowservice.v1.request_response_pb2.ListActivityExecutionsRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.ListActivityExecutionsResponse: + """ListActivityExecutions is a visibility API to list activity executions in a specific namespace.""" + @abc.abstractmethod + def ListNexusOperationExecutions( + self, + request: temporalio.api.workflowservice.v1.request_response_pb2.ListNexusOperationExecutionsRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.ListNexusOperationExecutionsResponse: + """ListNexusOperationExecutions is a visibility API to list Nexus operations in a specific namespace.""" + @abc.abstractmethod + def CountActivityExecutions( + self, + request: temporalio.api.workflowservice.v1.request_response_pb2.CountActivityExecutionsRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.CountActivityExecutionsResponse: + """CountActivityExecutions is a visibility API to count activity executions in a specific namespace.""" + @abc.abstractmethod + def CountNexusOperationExecutions( + self, + request: temporalio.api.workflowservice.v1.request_response_pb2.CountNexusOperationExecutionsRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.CountNexusOperationExecutionsResponse: + """CountNexusOperationExecutions is a visibility API to count Nexus operations in a specific namespace.""" + @abc.abstractmethod + def RequestCancelActivityExecution( + self, + request: temporalio.api.workflowservice.v1.request_response_pb2.RequestCancelActivityExecutionRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.RequestCancelActivityExecutionResponse: + """RequestCancelActivityExecution requests cancellation of an activity execution. + + Cancellation is cooperative: this call records the request, but the activity must detect and + acknowledge it for the activity to reach CANCELED status. The cancellation signal is + delivered via `cancel_requested` in the heartbeat response; SDKs surface this via + language-idiomatic mechanisms (context cancellation, exceptions, abort signals). + """ + @abc.abstractmethod + def RequestCancelNexusOperationExecution( + self, + request: temporalio.api.workflowservice.v1.request_response_pb2.RequestCancelNexusOperationExecutionRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.RequestCancelNexusOperationExecutionResponse: + """RequestCancelNexusOperationExecution requests cancellation of a Nexus operation. + + Requesting to cancel an operation does not automatically transition the operation to canceled status. + The operation will only transition to canceled status if it supports cancellation and the handler + processes the cancellation request. + """ + @abc.abstractmethod + def TerminateActivityExecution( + self, + request: temporalio.api.workflowservice.v1.request_response_pb2.TerminateActivityExecutionRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.TerminateActivityExecutionResponse: + """TerminateActivityExecution terminates an existing activity execution immediately. + + Termination does not reach the worker and the activity code cannot react to it. A terminated activity may have a + running attempt. + """ + @abc.abstractmethod + def DeleteActivityExecution( + self, + request: temporalio.api.workflowservice.v1.request_response_pb2.DeleteActivityExecutionRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.DeleteActivityExecutionResponse: + """DeleteActivityExecution asynchronously deletes a specific activity execution (when + ActivityExecution.run_id is provided) or the latest activity execution (when + ActivityExecution.run_id is not provided). If the activity Execution is running, it will be + terminated before deletion. + + (-- api-linter: core::0127::http-annotation=disabled + aip.dev/not-precedent: Activity deletion not exposed to HTTP, users should use cancel or terminate. --) + """ + @abc.abstractmethod + def PauseActivityExecution( + self, + request: temporalio.api.workflowservice.v1.request_response_pb2.PauseActivityExecutionRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.PauseActivityExecutionResponse: + """PauseActivityExecution pauses the execution of an activity specified by its ID. + This API can be used to target a workflow activity or a standalone activity + + Pausing an activity means: + - If the activity is currently waiting for a retry or is running and subsequently fails, + it will not be rescheduled until it is unpaused. + - If the activity is already paused, calling this method will have no effect. + - If the activity is running and finishes successfully, the activity will be completed. + - If the activity is running and finishes with failure: + * if there is no retry left - the activity will be completed. + * if there are more retries left - the activity will be paused. + For long-running activities: + - activities in paused state will send a cancellation with "activity_paused" set to 'true' in response to 'RecordActivityTaskHeartbeat'. + + Returns a `NotFound` error if there is no pending activity with the provided ID + """ + @abc.abstractmethod + def ResetActivityExecution( + self, + request: temporalio.api.workflowservice.v1.request_response_pb2.ResetActivityExecutionRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.ResetActivityExecutionResponse: + """ResetActivityExecution resets the execution of an activity specified by its ID. + This API can be used to target a workflow activity or a standalone activity. + + Resetting an activity means: + * number of attempts will be reset to 0. + * activity timeouts will be reset. + * if the activity is waiting for retry, and it is not paused or 'keep_paused' is not provided: + it will be scheduled immediately (* see 'jitter' flag) + + Returns a `NotFound` error if there is no pending activity with the provided ID or type. + """ + @abc.abstractmethod + def UnpauseActivityExecution( + self, + request: temporalio.api.workflowservice.v1.request_response_pb2.UnpauseActivityExecutionRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.UnpauseActivityExecutionResponse: + """UnpauseActivityExecution unpauses the execution of an activity specified by its ID. + This API can be used to target a workflow activity or a standalone activity. + + If activity is not paused, this call will have no effect. + If the activity was paused while waiting for retry, it will be scheduled immediately (* see 'jitter' flag). + Once the activity is unpaused, all timeout timers will be regenerated. + + Returns a `NotFound` error if there is no pending activity with the provided ID + """ + @abc.abstractmethod + def UpdateActivityExecutionOptions( + self, + request: temporalio.api.workflowservice.v1.request_response_pb2.UpdateActivityExecutionOptionsRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.UpdateActivityExecutionOptionsResponse: + """UpdateActivityExecutionOptions is called by the client to update the options of an activity by its ID. + This API can be used to target a workflow activity or a standalone activity. + """ + @abc.abstractmethod + def TerminateNexusOperationExecution( + self, + request: temporalio.api.workflowservice.v1.request_response_pb2.TerminateNexusOperationExecutionRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.TerminateNexusOperationExecutionResponse: + """TerminateNexusOperationExecution terminates an existing Nexus operation immediately. + + Termination happens immediately and the operation handler cannot react to it. A terminated operation will have + its outcome set to a failure with a termination reason. + """ + @abc.abstractmethod + def DeleteNexusOperationExecution( + self, + request: temporalio.api.workflowservice.v1.request_response_pb2.DeleteNexusOperationExecutionRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.DeleteNexusOperationExecutionResponse: + """DeleteNexusOperationExecution asynchronously deletes a specific Nexus operation run (when + run_id is provided) or the latest run (when run_id is not provided). If the operation + is running, it will be terminated before deletion. + + (-- api-linter: core::0127::http-annotation=disabled + aip.dev/not-precedent: Nexus operation deletion not exposed to HTTP, users should use cancel or terminate. --) + """ + @abc.abstractmethod + def PollWorkflowExecutionTimeSkipping( + self, + request: temporalio.api.workflowservice.v1.request_response_pb2.PollWorkflowExecutionTimeSkippingRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.PollWorkflowExecutionTimeSkippingResponse: ... def add_WorkflowServiceServicer_to_server( servicer: WorkflowServiceServicer, server: grpc.Server diff --git a/temporalio/bridge/Cargo.lock b/temporalio/bridge/Cargo.lock index 47b78dfb4..97a247eb4 100644 --- a/temporalio/bridge/Cargo.lock +++ b/temporalio/bridge/Cargo.lock @@ -2,37 +2,17 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -45,34 +25,25 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anyhow" -version = "1.0.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" - -[[package]] -name = "arbitrary" -version = "1.4.2" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -dependencies = [ - "derive_arbitrary", -] +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -83,15 +54,15 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "axum" -version = "0.8.4" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core", "bytes", @@ -105,8 +76,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rustversion", - "serde", + "serde_core", "sync_wrapper", "tower", "tower-layer", @@ -115,9 +85,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.2" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", @@ -126,36 +96,18 @@ dependencies = [ "http-body-util", "mime", "pin-project-lite", - "rustversion", "sync_wrapper", "tower-layer", "tower-service", ] [[package]] -name = "backoff" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" -dependencies = [ - "getrandom 0.2.16", - "instant", - "rand 0.8.5", -] - -[[package]] -name = "backtrace" -version = "0.3.75" +name = "backon" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", + "fastrand", ] [[package]] @@ -166,46 +118,63 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.9.2" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bon" +version = "3.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" +checksum = "a602c73c7b0148ec6d12af6fd5cc7a46e2eacc8878271a999abac56eed12f561" +dependencies = [ + "bon-macros", + "rustversion", +] [[package]] -name = "block-buffer" -version = "0.10.4" +name = "bon-macros" +version = "3.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +checksum = "6dee98b0db6a962de883bf5d20362dee4d7ca0d12fe39a7c6c73c844e1cd7c1f" dependencies = [ - "generic-array", + "darling", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.119", ] [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytes" -version = "1.10.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "bzip2" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bea8dcd42434048e4f7a304411d9273a411f647446c1234a65ce0554923f4cff" +checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" dependencies = [ "libbz2-rs-sys", ] [[package]] name = "cc" -version = "1.2.33" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -213,41 +182,49 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] -name = "cfg_aliases" -version = "0.2.1" +name = "chacha20" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core 0.10.1", +] [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "num-traits", "serde", ] [[package]] -name = "cipher" -version = "0.4.4" +name = "combine" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ - "crypto-common", - "inout", + "bytes", + "memchr", ] [[package]] -name = "constant_time_eq" -version = "0.3.1" +name = "convert_case" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] [[package]] name = "core-foundation" @@ -267,9 +244,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.17" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" dependencies = [ "libc", ] @@ -285,43 +262,24 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-queue" -version = "0.3.12" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crypto-common" -version = "0.1.6" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "darling" -version = "0.20.11" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ "darling_core", "darling_macro", @@ -329,132 +287,51 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.11" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.119", ] [[package]] name = "darling_macro" -version = "0.20.11" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn", -] - -[[package]] -name = "dashmap" -version = "6.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - -[[package]] -name = "deflate64" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" - -[[package]] -name = "deranged" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "derive_arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "derive_builder" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" -dependencies = [ - "derive_builder_macro", -] - -[[package]] -name = "derive_builder_core" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "derive_builder_macro" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" -dependencies = [ - "derive_builder_core", - "syn", + "syn 2.0.119", ] [[package]] name = "derive_more" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ + "convert_case", "proc-macro2", "quote", - "syn", + "rustc_version", + "syn 2.0.119", "unicode-xid", ] -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", -] - [[package]] name = "dirs" version = "6.0.0" @@ -473,18 +350,18 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -493,30 +370,36 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "either" -version = "1.15.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" [[package]] name = "enum-iterator" -version = "2.1.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c280b9e6b3ae19e152d8e31cf47f18389781e119d4013a2a2bb0180e5facc635" +checksum = "a4549325971814bda7a44061bf3fe7e487d447cba01e4220a4b454d630d7a016" dependencies = [ "enum-iterator-derive", ] [[package]] name = "enum-iterator-derive" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b" +checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -528,7 +411,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -539,42 +422,47 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.6" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" dependencies = [ "serde", + "serde_core", "typeid", ] [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "fastrand" -version = "2.3.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "filetime" -version = "0.2.25" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" dependencies = [ "cfg-if", "libc", - "libredox", - "windows-sys 0.59.0", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "fixedbitset" version = "0.5.7" @@ -583,13 +471,13 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.1.2" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", - "libz-rs-sys", "miniz_oxide", + "zlib-rs", ] [[package]] @@ -604,26 +492,35 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] [[package]] name = "fragile" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" +checksum = "8878864ba14bb86e818a412bfd6f18f9eabd4ec0f008a28e8f7eb61db532fcf9" +dependencies = [ + "futures-core", +] [[package]] name = "futures" -version = "0.3.31" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" dependencies = [ "futures-channel", "futures-core", @@ -636,9 +533,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" dependencies = [ "futures-core", "futures-sink", @@ -646,15 +543,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" dependencies = [ "futures-core", "futures-task", @@ -663,19 +560,19 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -691,27 +588,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-timer" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ "futures-channel", "futures-core", @@ -721,91 +612,59 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "gethostname" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc257fdb4038301ce4b9cd1b3b51704509692bb3ff716a410cbd07925d9dae55" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ "rustix", - "windows-targets 0.52.6", + "windows-link", ] [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", - "js-sys", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "wasm-bindgen", + "wasi", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", - "js-sys", "libc", - "r-efi", - "wasi 0.14.2+wasi-0.2.4", - "wasm-bindgen", + "r-efi 5.3.0", + "wasip2", ] [[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - -[[package]] -name = "governor" -version = "0.10.1" +name = "getrandom" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444405bbb1a762387aa22dd569429533b54a1d8759d35d3b64cb39b0293eaa19" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", - "dashmap", - "futures-sink", - "futures-timer", - "futures-util", - "getrandom 0.3.3", - "hashbrown 0.15.5", - "nonzero_ext", - "parking_lot", - "portable-atomic", - "quanta", - "rand 0.9.2", - "smallvec", - "spinning_top", - "web-time", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", ] [[package]] name = "h2" -version = "0.4.12" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" dependencies = [ "atomic-waker", "bytes", @@ -822,19 +681,22 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.5" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.2.0", ] [[package]] @@ -843,31 +705,21 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - [[package]] name = "http" -version = "1.3.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", - "fnv", "itoa", ] [[package]] name = "http-body" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", "http", @@ -875,9 +727,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" dependencies = [ "bytes", "futures-core", @@ -900,9 +752,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.7.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" dependencies = [ "atomic-waker", "bytes", @@ -915,7 +767,6 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -923,16 +774,14 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", "hyper-util", "rustls", - "rustls-native-certs", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", @@ -953,14 +802,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.16" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64", "bytes", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -969,7 +817,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.0", + "socket2", "tokio", "tower-service", "tracing", @@ -977,12 +825,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -990,9 +839,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1003,11 +852,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -1018,42 +866,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -1069,9 +913,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -1080,9 +924,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -1090,106 +934,111 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.10.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.17.1", ] [[package]] -name = "indoc" -version = "2.0.6" +name = "inventory" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] [[package]] -name = "inout" -version = "0.1.4" +name = "ipnet" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] -name = "instant" -version = "0.1.13" +name = "itertools" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ - "cfg-if", + "either", ] [[package]] -name = "inventory" -version = "0.3.20" +name = "itoa" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab08d7cd2c5897f2c949e5383ea7c7db03fb19130ffcfbf7eda795137ae3cb83" -dependencies = [ - "rustversion", -] +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] -name = "io-uring" -version = "0.7.9" +name = "jni" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" dependencies = [ - "bitflags", "cfg-if", - "libc", -] - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror", + "walkdir", + "windows-link", +] [[package]] -name = "iri-string" -version = "0.7.8" +name = "jni-macros" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" dependencies = [ - "memchr", - "serde", + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", ] [[package]] -name = "itertools" -version = "0.14.0" +name = "jni-sys" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" dependencies = [ - "either", + "jni-sys-macros", ] [[package]] -name = "itoa" -version = "1.0.15" +name = "jni-sys-macros" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.4.3", "libc", ] [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -1201,99 +1050,61 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libbz2-rs-sys" -version = "0.2.2" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" +checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" [[package]] name = "libc" -version = "0.2.175" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" - -[[package]] -name = "liblzma" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10bf66f4598dc77ff96677c8e763655494f00ff9c1cf79e2eb5bb07bc31f807d" -dependencies = [ - "liblzma-sys", -] - -[[package]] -name = "liblzma-sys" -version = "0.4.4" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01b9596486f6d60c3bbe644c0e1be1aa6ccc472ad630fe8927b456973d7cb736" -dependencies = [ - "cc", - "libc", - "pkg-config", -] +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libredox" -version = "0.1.9" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" dependencies = [ - "bitflags", "libc", - "redox_syscall", -] - -[[package]] -name = "libz-rs-sys" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" -dependencies = [ - "zlib-rs", ] [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.27" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "lru" -version = "0.16.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ea4e65087ff52f3862caff188d489f1fab49a0cb09e01b2e3f1a617b10aaed" +checksum = "0b6180140927ee907000b0aa540091f6ea512ead4447c92b8fc35bc72788a5a6" dependencies = [ - "hashbrown 0.15.5", + "hashbrown 0.17.1", ] -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - [[package]] name = "matchers" version = "0.2.0" @@ -1311,18 +1122,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "memchr" -version = "2.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" - -[[package]] -name = "memoffset" -version = "0.9.1" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "mime" @@ -1337,24 +1139,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] name = "mio" -version = "1.0.4" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] name = "mockall" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2" +checksum = "f58d964098a5f9c6b63d0798e5372fd04708193510a7af313c22e9f29b7b620b" dependencies = [ "cfg-if", "downcast", @@ -1366,14 +1169,14 @@ dependencies = [ [[package]] name = "mockall_derive" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898" +checksum = "ca41ce716dda6a9be188b385aa78ee5260fc25cd3802cb2a8afdc6afbe6b6dbf" dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1382,36 +1185,24 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" -[[package]] -name = "nonzero_ext" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" - [[package]] name = "ntapi" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" dependencies = [ "winapi", ] [[package]] name = "nu-ansi-term" -version = "0.50.1" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - [[package]] name = "num-traits" version = "0.2.19" @@ -1423,63 +1214,53 @@ dependencies = [ [[package]] name = "objc2-core-foundation" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags", ] [[package]] name = "objc2-io-kit" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71c1c64d6120e51cd86033f67176b1cb66780c2efe34dec55176f77befd93c0a" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" dependencies = [ "libc", "objc2-core-foundation", ] -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "openssl-probe" -version = "0.1.6" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "opentelemetry" -version = "0.30.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf416e4cb72756655126f7dd7bb0af49c674f4c1b9903e80c009e0c37e552e6" +checksum = "b0142c63252a9e054e68a4c61a5778f7b14f576274d593f8ce883d191a099682" dependencies = [ "futures-core", "futures-sink", "js-sys", "pin-project-lite", - "thiserror 2.0.15", - "tracing", + "thiserror", ] [[package]] name = "opentelemetry-http" -version = "0.30.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f6639e842a97dbea8886e3439710ae463120091e2e064518ba8e716e6ac36d" +checksum = "5683015d09e2df236ef005b17f6f196f0d5f6313c4fa43a7b6a53b52776e4331" dependencies = [ "async-trait", "bytes", @@ -1490,9 +1271,9 @@ dependencies = [ [[package]] name = "opentelemetry-otlp" -version = "0.30.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbee664a43e07615731afc539ca60c6d9f1a9425e25ca09c57bc36c87c55852b" +checksum = "9966929966d17620d7c316c643ba62631826e10021409357772d5eea84f62c35" dependencies = [ "http", "opentelemetry", @@ -1501,38 +1282,39 @@ dependencies = [ "opentelemetry_sdk", "prost", "reqwest", - "thiserror 2.0.15", + "thiserror", "tokio", "tonic", - "tracing", + "tonic-types", ] [[package]] name = "opentelemetry-proto" -version = "0.30.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e046fd7660710fe5a05e8748e70d9058dc15c94ba914e7c4faa7c728f0e8ddc" +checksum = "56d658ba1faf63f7b9c492cfbe6e0ec365440a16132d3270c1065f7b33f1b638" dependencies = [ "opentelemetry", "opentelemetry_sdk", "prost", "tonic", + "tonic-prost", ] [[package]] name = "opentelemetry_sdk" -version = "0.30.0" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f644aa9e5e31d11896e024305d7e3c98a88884d9f8919dbf37a9991bc47a4b" +checksum = "9b59f80e1ac4d5ff7a2db8fb6c80badb7f0f3f858211fba08dd9aaec750894f9" dependencies = [ "futures-channel", "futures-executor", "futures-util", "opentelemetry", "percent-encoding", - "rand 0.9.2", - "serde_json", - "thiserror 2.0.15", + "portable-atomic", + "rand 0.9.5", + "thiserror", "tokio", "tokio-stream", ] @@ -1545,9 +1327,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -1555,40 +1337,53 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link", +] + +[[package]] +name = "pbjson" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8edd1efdd8ab23ba9cb9ace3d9987a72663d5d7c9f74fa00b51d6213645cf6c" +dependencies = [ + "base64", + "serde", ] [[package]] -name = "pbkdf2" -version = "0.12.2" +name = "pbjson-build" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +checksum = "2ed4d5c6ae95e08ac768883c8401cf0e8deb4e6e1d6a4e1fd3d2ec4f0ec63200" dependencies = [ - "digest", - "hmac", + "heck", + "itertools", + "prost", + "prost-types", ] [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "petgraph" -version = "0.7.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ "fixedbitset", + "hashbrown 0.15.5", "indexmap", ] @@ -1603,78 +1398,60 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" [[package]] name = "portable-atomic-util" -version = "0.2.4" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" -version = "0.1.2" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppmd-rust" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c834641d8ad1b348c9ee86dec3b9840d805acd5f24daa5f90c788951a52ff59b" - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1686,9 +1463,9 @@ dependencies = [ [[package]] name = "predicates" -version = "3.1.3" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" dependencies = [ "anstyle", "predicates-core", @@ -1696,15 +1473,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" [[package]] name = "predicates-tree" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" dependencies = [ "predicates-core", "termtree", @@ -1717,14 +1494,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.119", ] [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -1740,15 +1517,14 @@ dependencies = [ "lazy_static", "memchr", "parking_lot", - "protobuf", - "thiserror 2.0.15", + "thiserror", ] [[package]] name = "prost" -version = "0.13.5" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" dependencies = [ "bytes", "prost-derive", @@ -1756,51 +1532,52 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.13.5" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" +checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" dependencies = [ "heck", "itertools", "log", "multimap", - "once_cell", "petgraph", "prettyplease", "prost", "prost-types", + "pulldown-cmark", + "pulldown-cmark-to-cmark", "regex", - "syn", + "syn 2.0.119", "tempfile", ] [[package]] name = "prost-derive" -version = "0.13.5" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", "itertools", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "prost-types" -version = "0.13.5" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" dependencies = [ "prost", ] [[package]] name = "prost-wkt" -version = "0.6.1" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497e1e938f0c09ef9cabe1d49437b4016e03e8f82fbbe5d1c62a9b61b9decae1" +checksum = "cd3de5e9c9e84fcb5efa204b8e283d23e615a8bc8c777bf1d6622bb01dc61445" dependencies = [ "chrono", "inventory", @@ -1813,9 +1590,9 @@ dependencies = [ [[package]] name = "prost-wkt-build" -version = "0.6.1" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07b8bf115b70a7aa5af1fd5d6e9418492e9ccb6e4785e858c938e28d132a884b" +checksum = "fe500dc80e757a75e1e8fb7290e448d62dfba3105ece1d058579cb00b58151cd" dependencies = [ "heck", "prost", @@ -1826,9 +1603,9 @@ dependencies = [ [[package]] name = "prost-wkt-types" -version = "0.6.1" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8cdde6df0a98311c839392ca2f2f0bcecd545f86a62b4e3c6a49c336e970fe5" +checksum = "13807eaa7e15833d06e899008371926201cdcd11d74b6d490f49130cdb3f415e" dependencies = [ "chrono", "prost", @@ -1843,50 +1620,49 @@ dependencies = [ ] [[package]] -name = "protobuf" -version = "3.7.2" +name = "pulldown-cmark" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" +checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" dependencies = [ - "once_cell", - "protobuf-support", - "thiserror 1.0.69", + "bitflags", + "memchr", + "unicase", ] [[package]] -name = "protobuf-support" -version = "3.7.2" +name = "pulldown-cmark-to-cmark" +version = "22.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" +checksum = "50793def1b900256624a709439404384204a5dc3a6ec580281bfaac35e882e90" dependencies = [ - "thiserror 1.0.69", + "pulldown-cmark", ] [[package]] name = "pyo3" -version = "0.25.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8970a78afe0628a3e3430376fc5fd76b6b45c4d43360ffd6cdd40bdde72b682a" +checksum = "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c" dependencies = [ "anyhow", - "indoc", + "inventory", "libc", - "memoffset", "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", - "unindent", ] [[package]] name = "pyo3-async-runtimes" -version = "0.25.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d73cc6b1b7d8b3cef02101d37390dbdfe7e450dfea14921cae80a9534ba59ef2" +checksum = "b3ef68daa7316a3fac65e5e18b2203f010346de1c1c53456811a2624673ab046" dependencies = [ - "futures", + "futures-channel", + "futures-util", "once_cell", "pin-project-lite", "pyo3", @@ -1895,19 +1671,18 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.25.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458eb0c55e7ece017adeba38f2248ff3ac615e53660d7c71a238d7d2a01c7598" +checksum = "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078" dependencies = [ - "once_cell", "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.25.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7114fe5457c61b276ab77c5055f206295b812608083644a5c5b2640c3102565c" +checksum = "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b" dependencies = [ "libc", "pyo3-build-config", @@ -1915,114 +1690,43 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.25.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8725c0a622b374d6cb051d11a0983786448f7785336139c3c94f5aa6bef7e50" +checksum = "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771" dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "pyo3-macros-backend" -version = "0.25.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4109984c22491085343c05b0dbc54ddc405c3cf7b4374fc533f5c3313a572ccc" +checksum = "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362" dependencies = [ "heck", "proc-macro2", - "pyo3-build-config", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "pythonize" -version = "0.25.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597907139a488b22573158793aa7539df36ae863eba300c75f3a0d65fc475e27" +checksum = "6ec376e1216e0c929a74964ce2020012a1a39f32d80e78aa688721219ea7fb89" dependencies = [ "pyo3", "serde", ] -[[package]] -name = "quanta" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" -dependencies = [ - "crossbeam-utils", - "libc", - "once_cell", - "raw-cpuid", - "wasi 0.11.1+wasi-snapshot-preview1", - "web-sys", - "winapi", -] - -[[package]] -name = "quinn" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2 0.5.10", - "thiserror 2.0.15", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" -dependencies = [ - "bytes", - "getrandom 0.3.3", - "lru-slab", - "rand 0.9.2", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.15", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2 0.5.10", - "tracing", - "windows-sys 0.59.0", -] - [[package]] name = "quote" -version = "1.0.40" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -2034,34 +1738,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] -name = "rand" -version = "0.8.5" +name = "r-efi" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.9.2" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_chacha", + "rand_core 0.9.5", ] [[package]] -name = "rand_chacha" -version = "0.3.1" +name = "rand" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", ] [[package]] @@ -2071,41 +1771,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] name = "rand_core" -version = "0.6.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.3.4", ] [[package]] name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.3", -] - -[[package]] -name = "raw-cpuid" -version = "11.5.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" -dependencies = [ - "bitflags", -] +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] @@ -2116,16 +1804,16 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "libredox", - "thiserror 2.0.15", + "thiserror", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -2135,9 +1823,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" dependencies = [ "aho-corasick", "memchr", @@ -2146,15 +1834,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "reqwest" -version = "0.12.23" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64", "bytes", @@ -2171,10 +1859,9 @@ dependencies = [ "log", "percent-encoding", "pin-project-lite", - "quinn", "rustls", - "rustls-native-certs", "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", "serde_urlencoded", @@ -2200,7 +1887,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -2208,9 +1895,9 @@ dependencies = [ [[package]] name = "ringbuf" -version = "0.4.8" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe47b720588c8702e34b5979cb3271a8b1842c7cb6f57408efa70c779363488c" +checksum = "a158e09ede21a14b172ca6cdd6208386c6ae2cb6acef58d774368ef8c450dfa7" dependencies = [ "crossbeam-utils", "portable-atomic", @@ -2218,58 +1905,32 @@ dependencies = [ ] [[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - -[[package]] -name = "rustc-hash" -version = "2.1.1" +name = "rustc_version" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustfsm" -version = "0.1.0" -dependencies = [ - "rustfsm_procmacro", - "rustfsm_trait", -] - -[[package]] -name = "rustfsm_procmacro" -version = "0.1.0" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "derive_more", - "proc-macro2", - "quote", - "rustfsm_trait", - "syn", + "semver", ] -[[package]] -name = "rustfsm_trait" -version = "0.1.0" - [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.31" +version = "0.23.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" dependencies = [ "log", "once_cell", @@ -2282,9 +1943,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.1" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -2294,19 +1955,45 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ - "web-time", "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "ring", "rustls-pki-types", @@ -2315,23 +2002,32 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2342,9 +2038,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "3.3.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags", "core-foundation", @@ -2355,53 +2051,70 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", ] +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" -version = "1.0.219" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ + "serde_core", "serde_derive", ] [[package]] -name = "serde_derive" -version = "1.0.219" +name = "serde_core" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ - "proc-macro2", + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] name = "serde_json" -version = "1.0.143" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] name = "serde_spanned" -version = "1.0.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -2416,17 +2129,6 @@ dependencies = [ "serde", ] -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "sharded-slab" version = "0.1.7" @@ -2438,86 +2140,84 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "siphasher" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "slotmap" -version = "1.0.7" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" dependencies = [ "version_check", ] [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "spinning_top" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" -dependencies = [ - "lock_api", + "windows-sys 0.61.2", ] [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "strsim" @@ -2533,9 +2233,20 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.106" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" dependencies = [ "proc-macro2", "quote", @@ -2559,14 +2270,14 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "sysinfo" -version = "0.37.0" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07cec4dc2d2e357ca1e610cfb07de2fa7a10fc3e9fe89f72545f3d244ea87753" +checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f" dependencies = [ "libc", "memchr", @@ -2578,9 +2289,9 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.44" +version = "0.4.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" dependencies = [ "filetime", "libc", @@ -2589,34 +2300,57 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.13.2" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "tempfile" -version = "3.20.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] -name = "temporal-client" +name = "temporal-sdk-bridge" version = "0.1.0" dependencies = [ "anyhow", "async-trait", - "backoff", + "futures", + "prost", + "pyo3", + "pyo3-async-runtimes", + "pythonize", + "temporalio-client", + "temporalio-common", + "temporalio-sdk-core", + "tokio", + "tokio-rustls", + "tokio-stream", + "tonic", + "tracing", + "url", +] + +[[package]] +name = "temporalio-client" +version = "0.6.0" +dependencies = [ + "anyhow", + "async-trait", + "backon", "base64", + "bon", "bytes", - "derive_builder", "derive_more", + "dyn-clone", "futures-retry", "futures-util", "http", @@ -2624,11 +2358,12 @@ dependencies = [ "hyper", "hyper-util", "parking_lot", - "slotmap", - "temporal-sdk-core-api", - "temporal-sdk-core-protos", - "thiserror 2.0.15", + "rand 0.10.2", + "serde_json", + "temporalio-common", + "thiserror", "tokio", + "tokio-rustls", "tonic", "tower", "tracing", @@ -2637,126 +2372,147 @@ dependencies = [ ] [[package]] -name = "temporal-sdk-bridge" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "futures", - "prost", - "pyo3", - "pyo3-async-runtimes", - "pythonize", - "temporal-client", - "temporal-sdk-core", - "temporal-sdk-core-api", - "temporal-sdk-core-protos", - "tokio", - "tokio-stream", - "tonic", - "tracing", - "url", -] - -[[package]] -name = "temporal-sdk-core" -version = "0.1.0" +name = "temporalio-common" +version = "0.6.0" dependencies = [ "anyhow", "async-trait", - "crossbeam-channel", - "crossbeam-queue", - "crossbeam-utils", - "dashmap", - "derive_builder", + "bon", + "crc32fast", "derive_more", - "enum-iterator", - "enum_dispatch", - "flate2", + "dirs", + "erased-serde", + "futures", "futures-channel", - "futures-util", - "gethostname", - "governor", "http-body-util", "hyper", "hyper-util", - "itertools", - "lru", - "mockall", "opentelemetry", "opentelemetry-otlp", "opentelemetry_sdk", "parking_lot", - "pid", - "pin-project", "prometheus", "prost", - "prost-wkt-types", - "rand 0.9.2", + "prost-types", "reqwest", "ringbuf", - "rustfsm", + "rustls", "serde", "serde_json", - "siphasher", - "slotmap", - "sysinfo", - "tar", - "temporal-client", - "temporal-sdk-core-api", - "temporal-sdk-core-protos", - "thiserror 2.0.15", + "temporalio-common-wasm", + "temporalio-protos", + "thiserror", "tokio", - "tokio-stream", - "tokio-util", + "toml", "tonic", "tracing", + "tracing-core", "tracing-subscriber", "url", "uuid", - "zip", ] [[package]] -name = "temporal-sdk-core-api" -version = "0.1.0" +name = "temporalio-common-wasm" +version = "0.6.0" dependencies = [ + "anyhow", "async-trait", - "derive_builder", + "bon", + "chrono", + "crc32fast", "derive_more", - "dirs", - "opentelemetry", + "erased-serde", + "futures", + "parking_lot", "prost", + "prost-wkt-types", "serde", "serde_json", - "temporal-sdk-core-protos", - "thiserror 2.0.15", - "toml", - "tonic", + "temporalio-protos", + "thiserror", "tracing", "tracing-core", + "tracing-subscriber", "url", ] [[package]] -name = "temporal-sdk-core-protos" -version = "0.1.0" +name = "temporalio-macros" +version = "0.6.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "temporalio-protos" +version = "0.6.0" dependencies = [ "anyhow", "base64", "derive_more", + "http", + "pbjson", + "pbjson-build", "prost", - "prost-build", - "prost-wkt", - "prost-wkt-build", + "prost-types", "prost-wkt-types", - "rand 0.9.2", "serde", "serde_json", - "thiserror 2.0.15", + "thiserror", "tonic", - "tonic-build", + "tonic-prost", + "tonic-prost-build", +] + +[[package]] +name = "temporalio-sdk-core" +version = "0.6.0" +dependencies = [ + "anyhow", + "async-trait", + "backon", + "bon", + "crossbeam-channel", + "crossbeam-utils", + "derive_more", + "enum-iterator", + "enum_dispatch", + "flate2", + "futures", + "futures-util", + "gethostname", + "itertools", + "lru", + "mockall", + "opentelemetry-otlp", + "parking_lot", + "pid", + "pin-project", + "prost", + "prost-wkt-types", + "rand 0.10.2", + "reqwest", + "serde", + "serde_json", + "siphasher", + "slotmap", + "sysinfo", + "tar", + "temporalio-client", + "temporalio-common", + "temporalio-macros", + "thiserror", + "tokio", + "tokio-stream", + "tokio-util", + "tonic", + "tracing", + "url", "uuid", + "zip", ] [[package]] @@ -2767,133 +2523,76 @@ checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d76d3f064b981389ecb4b6b7f45a0bf9fdac1d5b9204c7bd6714fecc302850" -dependencies = [ - "thiserror-impl 2.0.15", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "proc-macro2", - "quote", - "syn", + "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.15" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d29feb33e986b6ea906bd9c3559a856983f92371b3eaa5e83782a351623de0" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] name = "thread_local" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" dependencies = [ "cfg-if", ] -[[package]] -name = "time" -version = "0.3.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde", - "time-core", -] - -[[package]] -name = "time-core" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" - [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", ] -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokio" -version = "1.47.1" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "slab", - "socket2 0.6.0", + "socket2", "tokio-macros", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -2901,9 +2600,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" dependencies = [ "futures-core", "pin-project-lite", @@ -2912,25 +2611,26 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", "futures-sink", + "libc", "pin-project-lite", "tokio", ] [[package]] name = "toml" -version = "0.9.5" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" +checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" dependencies = [ "indexmap", - "serde", + "serde_core", "serde_spanned", "toml_datetime", "toml_parser", @@ -2940,38 +2640,39 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ - "serde", + "serde_core", ] [[package]] name = "toml_parser" -version = "1.0.2" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.0.2" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tonic" -version = "0.13.1" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e581ba15a835f4d9ea06c55ab1bd4dce26fc53752c69a04aac00703bfb49ba9" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" dependencies = [ "async-trait", "axum", "base64", "bytes", + "flate2", "h2", "http", "http-body", @@ -2981,9 +2682,9 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", - "prost", "rustls-native-certs", - "socket2 0.5.10", + "socket2", + "sync_wrapper", "tokio", "tokio-rustls", "tokio-stream", @@ -2995,23 +2696,59 @@ dependencies = [ [[package]] name = "tonic-build" -version = "0.13.1" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac6f67be712d12f0b41328db3137e0d0757645d8904b4cb7d51cd9c2279e847" +checksum = "c68f61875ac5293cf72e6c8cf0158086428c82c37229e98c840878f1706b0322" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tonic-prost" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" +dependencies = [ + "bytes", + "prost", + "tonic", +] + +[[package]] +name = "tonic-prost-build" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "654e5643eff75d7f8c99197ce1440ed19a3474eada74c12bbac488b2cafdae27" dependencies = [ "prettyplease", "proc-macro2", "prost-build", "prost-types", "quote", - "syn", + "syn 2.0.119", + "tempfile", + "tonic-build", +] + +[[package]] +name = "tonic-types" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab1b02061f83d519bba3caa167f88f261ef05720ab8ebc954ade70de3348e8" +dependencies = [ + "prost", + "prost-types", + "tonic", ] [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -3028,20 +2765,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "bitflags", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -3058,9 +2795,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -3069,20 +2806,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -3090,9 +2827,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.20" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -3112,22 +2849,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] -name = "typeid" -version = "1.0.3" +name = "typed-path" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" [[package]] -name = "typenum" -version = "1.18.0" +name = "typeid" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typetag" -version = "0.2.20" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f22b40dd7bfe8c14230cf9702081366421890435b2d625fa92b4acc4c3de6f" +checksum = "c90e86058a30d42a1a928dfb4b49bb33c98c3a2b4909492e6b0881cd94798ec2" dependencies = [ "erased-serde", "inventory", @@ -3138,32 +2875,38 @@ dependencies = [ [[package]] name = "typetag-impl" -version = "0.2.20" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35f5380909ffc31b4de4f4bdf96b877175a016aa2ca98cee39fcfd8c4d53d952" +checksum = "f153acc4e99a5f2a5aefa09fb078be54e26271b2813f6041200b224c098d8328" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] -name = "unicode-xid" -version = "0.2.6" +name = "unicode-segmentation" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] -name = "unindent" -version = "0.2.4" +name = "unicode-xid" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "untrusted" @@ -3173,13 +2916,14 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -3190,13 +2934,11 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.18.0" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ - "getrandom 0.3.3", - "js-sys", - "wasm-bindgen", + "getrandom 0.4.3", ] [[package]] @@ -3211,6 +2953,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -3227,58 +2979,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ - "cfg-if", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3286,31 +3022,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn", - "wasm-bindgen-backend", + "syn 2.0.119", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-streams" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" dependencies = [ "futures-util", "js-sys", @@ -3321,22 +3057,21 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] -name = "web-time" -version = "1.1.0" +name = "webpki-root-certs" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" dependencies = [ - "js-sys", - "wasm-bindgen", + "rustls-pki-types", ] [[package]] @@ -3355,6 +3090,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -3363,31 +3107,30 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.61.3" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ "windows-collections", "windows-core", "windows-future", - "windows-link", "windows-numerics", ] [[package]] name = "windows-collections" -version = "0.2.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ "windows-core", ] [[package]] name = "windows-core" -version = "0.61.2" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", @@ -3398,9 +3141,9 @@ dependencies = [ [[package]] name = "windows-future" -version = "0.2.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" dependencies = [ "windows-core", "windows-link", @@ -3409,37 +3152,37 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "windows-link" -version = "0.1.3" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-numerics" -version = "0.2.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" dependencies = [ "windows-core", "windows-link", @@ -3447,18 +3190,18 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.3.4" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" -version = "0.4.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link", ] @@ -3469,25 +3212,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-targets 0.53.3", + "windows-link", ] [[package]] @@ -3496,38 +3230,21 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] name = "windows-threading" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" dependencies = [ "windows-link", ] @@ -3538,122 +3255,71 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" - [[package]] name = "winnow" -version = "0.7.12" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags", -] +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "xattr" -version = "1.5.1" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", "rustix", @@ -3661,11 +3327,10 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -3673,82 +3338,68 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -3757,9 +3408,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -3768,53 +3419,48 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "zip" -version = "4.6.1" +version = "8.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" dependencies = [ - "aes", - "arbitrary", "bzip2", - "constant_time_eq", "crc32fast", - "deflate64", "flate2", - "getrandom 0.3.3", - "hmac", "indexmap", - "liblzma", "memchr", - "pbkdf2", - "ppmd-rust", - "sha1", - "time", - "zeroize", + "typed-path", "zopfli", "zstd", ] [[package]] name = "zlib-rs" -version = "0.5.2" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b142a20ec14a91d5bc708c1dc21b080c550113d8aa77afa29635673a65dd02c5" + +[[package]] +name = "zmij" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zopfli" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" dependencies = [ "bumpalo", "crc32fast", @@ -3842,9 +3488,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.15+zstd.1.5.7" +version = "2.0.16+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" dependencies = [ "cc", "pkg-config", diff --git a/temporalio/bridge/Cargo.toml b/temporalio/bridge/Cargo.toml index 70a3c5820..4cf6a02fe 100644 --- a/temporalio/bridge/Cargo.toml +++ b/temporalio/bridge/Cargo.toml @@ -19,25 +19,28 @@ crate-type = ["cdylib"] anyhow = "1.0" async-trait = "0.1" futures = "0.3" -prost = "0.13" -pyo3 = { version = "0.25", features = [ +prost = "0.14" +pyo3 = { version = "0.29", features = [ "extension-module", - "abi3-py39", + "abi3-py310", "anyhow", + "multiple-pymethods", ] } -pyo3-async-runtimes = { version = "0.25", features = ["tokio-runtime"] } -pythonize = "0.25" -temporal-client = { version = "0.1.0", path = "./sdk-core/client" } -temporal-sdk-core = { version = "0.1.0", path = "./sdk-core/core", features = [ +pyo3-async-runtimes = { version = "0.29", features = ["tokio-runtime"] } +pythonize = "0.29" +temporalio-client = { version = "0.6", path = "./sdk-core/crates/client" } +temporalio-common = { version = "0.6", path = "./sdk-core/crates/common", features = [ + "envconfig", "otel" +]} +temporalio-sdk-core = { version = "0.6", path = "./sdk-core/crates/sdk-core", features = [ "ephemeral-server", ] } -temporal-sdk-core-api = { version = "0.1.0", path = "./sdk-core/core-api", features = [ - "envconfig", -] } -temporal-sdk-core-protos = { version = "0.1.0", path = "./sdk-core/sdk-core-protos" } tokio = "1.26" +# Matches the sdk-core client crate's rustls stack; used to build the custom +# server certificate verifier for ClientTlsConfig.verification_server_name. +tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } tokio-stream = "0.1" -tonic = "0.13" +tonic = "0.14" tracing = "0.1" url = "2.2" diff --git a/temporalio/bridge/_visitor.py b/temporalio/bridge/_visitor.py new file mode 100644 index 000000000..ef1e21dbd --- /dev/null +++ b/temporalio/bridge/_visitor.py @@ -0,0 +1,623 @@ +from __future__ import annotations + +# This file is generated by gen_payload_visitor.py. Changes should be made there. +from typing import Any + +import temporalio.nexus.system +from temporalio.api.common.v1.message_pb2 import Payload +from temporalio.bridge._visitor_functions import ( + BoundedVisitorFunctions, + PayloadSequence, + VisitorFunctions, +) + + +class PayloadVisitor: + """A visitor for payloads. + Applies a function to every payload in a tree of messages. + """ + + def __init__( + self, + *, + skip_search_attributes: bool = False, + skip_headers: bool = False, + concurrency_limit: int = 1, + ): + """Creates a new payload visitor. + + Args: + skip_search_attributes: If True, search attributes are not visited. + skip_headers: If True, headers are not visited. + concurrency_limit: Maximum number of payload visits that may run + concurrently during a single call to visit(). Defaults to 1 + (sequential). + """ + if concurrency_limit < 1: + raise ValueError("concurrency_limit must be positive") + self.skip_search_attributes = skip_search_attributes + self.skip_headers = skip_headers + self._concurrency_limit = concurrency_limit + + async def visit(self, fs: VisitorFunctions, root: Any) -> None: + """Visits the given root message with the given function.""" + method_name = "_visit_" + root.DESCRIPTOR.full_name.replace(".", "_") + method = getattr(self, method_name, None) + if method is None: + raise ValueError(f"Unknown root message type: {root.DESCRIPTOR.full_name}") + if self._concurrency_limit == 1: + await method(fs, root) + return + + bounded = BoundedVisitorFunctions(fs, self._concurrency_limit) + try: + await method(bounded, root) + finally: + await bounded.drain() + + async def _visit_nexus_operation_input_payload( + self, + fs: VisitorFunctions, + payload: Payload, + ) -> None: + await self._visit_temporal_api_common_v1_Payload(fs, payload) + + async def _visit_temporal_api_common_v1_Payload( + self, fs: VisitorFunctions, payload: Payload + ) -> None: + new_payload = await temporalio.nexus.system.maybe_visit_payload( + payload, + fs, + self.skip_search_attributes, + ) + if new_payload is None: + await fs.visit_payload(payload) + return + + if new_payload is not payload: + payload.CopyFrom(new_payload) + await fs.visit_system_nexus_envelope(payload) + + async def _visit_temporal_api_common_v1_Payloads( + self, fs: VisitorFunctions, o: Any + ): + await fs.visit_payloads(o.payloads) + + async def _visit_payload_container(self, fs: VisitorFunctions, o: PayloadSequence): + await fs.visit_payloads(o) + + async def _visit_temporal_api_failure_v1_ApplicationFailureInfo( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("details"): + await self._visit_temporal_api_common_v1_Payloads(fs, o.details) + + async def _visit_temporal_api_failure_v1_TimeoutFailureInfo( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("last_heartbeat_details"): + await self._visit_temporal_api_common_v1_Payloads( + fs, o.last_heartbeat_details + ) + + async def _visit_temporal_api_failure_v1_CanceledFailureInfo( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("details"): + await self._visit_temporal_api_common_v1_Payloads(fs, o.details) + + async def _visit_temporal_api_failure_v1_ResetWorkflowFailureInfo( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("last_heartbeat_details"): + await self._visit_temporal_api_common_v1_Payloads( + fs, o.last_heartbeat_details + ) + + async def _visit_temporal_api_failure_v1_Failure( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("encoded_attributes"): + await self._visit_temporal_api_common_v1_Payload(fs, o.encoded_attributes) + if o.HasField("cause"): + await self._visit_temporal_api_failure_v1_Failure(fs, o.cause) + if o.HasField("application_failure_info"): + await self._visit_temporal_api_failure_v1_ApplicationFailureInfo( + fs, o.application_failure_info + ) + elif o.HasField("timeout_failure_info"): + await self._visit_temporal_api_failure_v1_TimeoutFailureInfo( + fs, o.timeout_failure_info + ) + elif o.HasField("canceled_failure_info"): + await self._visit_temporal_api_failure_v1_CanceledFailureInfo( + fs, o.canceled_failure_info + ) + elif o.HasField("reset_workflow_failure_info"): + await self._visit_temporal_api_failure_v1_ResetWorkflowFailureInfo( + fs, o.reset_workflow_failure_info + ) + + async def _visit_temporal_api_common_v1_Memo(self, fs: VisitorFunctions, o: Any): + for v in o.fields.values(): + await self._visit_temporal_api_common_v1_Payload(fs, v) + + async def _visit_temporal_api_common_v1_SearchAttributes( + self, fs: VisitorFunctions, o: Any + ): + if self.skip_search_attributes: + return + for v in o.indexed_fields.values(): + await self._visit_temporal_api_common_v1_Payload(fs, v) + + async def _visit_coresdk_workflow_activation_InitializeWorkflow( + self, fs: VisitorFunctions, o: Any + ): + await self._visit_payload_container(fs, o.arguments) + if not self.skip_headers: + for v in o.headers.values(): + await self._visit_temporal_api_common_v1_Payload(fs, v) + if o.HasField("continued_failure"): + await self._visit_temporal_api_failure_v1_Failure(fs, o.continued_failure) + if o.HasField("last_completion_result"): + await self._visit_temporal_api_common_v1_Payloads( + fs, o.last_completion_result + ) + if o.HasField("memo"): + await self._visit_temporal_api_common_v1_Memo(fs, o.memo) + if o.HasField("search_attributes"): + await self._visit_temporal_api_common_v1_SearchAttributes( + fs, o.search_attributes + ) + + async def _visit_coresdk_workflow_activation_QueryWorkflow( + self, fs: VisitorFunctions, o: Any + ): + await self._visit_payload_container(fs, o.arguments) + if not self.skip_headers: + for v in o.headers.values(): + await self._visit_temporal_api_common_v1_Payload(fs, v) + + async def _visit_coresdk_workflow_activation_SignalWorkflow( + self, fs: VisitorFunctions, o: Any + ): + await self._visit_payload_container(fs, o.input) + if not self.skip_headers: + for v in o.headers.values(): + await self._visit_temporal_api_common_v1_Payload(fs, v) + + async def _visit_coresdk_activity_result_Success( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("result"): + await self._visit_temporal_api_common_v1_Payload(fs, o.result) + + async def _visit_coresdk_activity_result_Failure( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("failure"): + await self._visit_temporal_api_failure_v1_Failure(fs, o.failure) + + async def _visit_coresdk_activity_result_Cancellation( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("failure"): + await self._visit_temporal_api_failure_v1_Failure(fs, o.failure) + + async def _visit_coresdk_activity_result_ActivityResolution( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("completed"): + await self._visit_coresdk_activity_result_Success(fs, o.completed) + elif o.HasField("failed"): + await self._visit_coresdk_activity_result_Failure(fs, o.failed) + elif o.HasField("cancelled"): + await self._visit_coresdk_activity_result_Cancellation(fs, o.cancelled) + + async def _visit_coresdk_workflow_activation_ResolveActivity( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("result"): + await self._visit_coresdk_activity_result_ActivityResolution(fs, o.result) + + async def _visit_coresdk_workflow_activation_ResolveChildWorkflowExecutionStartCancelled( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("failure"): + await self._visit_temporal_api_failure_v1_Failure(fs, o.failure) + + async def _visit_coresdk_workflow_activation_ResolveChildWorkflowExecutionStart( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("cancelled"): + await self._visit_coresdk_workflow_activation_ResolveChildWorkflowExecutionStartCancelled( + fs, o.cancelled + ) + + async def _visit_coresdk_child_workflow_Success(self, fs: VisitorFunctions, o: Any): + if o.HasField("result"): + await self._visit_temporal_api_common_v1_Payload(fs, o.result) + + async def _visit_coresdk_child_workflow_Failure(self, fs: VisitorFunctions, o: Any): + if o.HasField("failure"): + await self._visit_temporal_api_failure_v1_Failure(fs, o.failure) + + async def _visit_coresdk_child_workflow_Cancellation( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("failure"): + await self._visit_temporal_api_failure_v1_Failure(fs, o.failure) + + async def _visit_coresdk_child_workflow_ChildWorkflowResult( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("completed"): + await self._visit_coresdk_child_workflow_Success(fs, o.completed) + elif o.HasField("failed"): + await self._visit_coresdk_child_workflow_Failure(fs, o.failed) + elif o.HasField("cancelled"): + await self._visit_coresdk_child_workflow_Cancellation(fs, o.cancelled) + + async def _visit_coresdk_workflow_activation_ResolveChildWorkflowExecution( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("result"): + await self._visit_coresdk_child_workflow_ChildWorkflowResult(fs, o.result) + + async def _visit_coresdk_workflow_activation_ResolveSignalExternalWorkflow( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("failure"): + await self._visit_temporal_api_failure_v1_Failure(fs, o.failure) + + async def _visit_coresdk_workflow_activation_ResolveRequestCancelExternalWorkflow( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("failure"): + await self._visit_temporal_api_failure_v1_Failure(fs, o.failure) + + async def _visit_coresdk_workflow_activation_DoUpdate( + self, fs: VisitorFunctions, o: Any + ): + await self._visit_payload_container(fs, o.input) + if not self.skip_headers: + for v in o.headers.values(): + await self._visit_temporal_api_common_v1_Payload(fs, v) + + async def _visit_coresdk_workflow_activation_ResolveNexusOperationStart( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("failed"): + await self._visit_temporal_api_failure_v1_Failure(fs, o.failed) + + async def _visit_coresdk_nexus_NexusOperationResult( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("completed"): + await self._visit_temporal_api_common_v1_Payload(fs, o.completed) + elif o.HasField("failed"): + await self._visit_temporal_api_failure_v1_Failure(fs, o.failed) + elif o.HasField("cancelled"): + await self._visit_temporal_api_failure_v1_Failure(fs, o.cancelled) + elif o.HasField("timed_out"): + await self._visit_temporal_api_failure_v1_Failure(fs, o.timed_out) + + async def _visit_coresdk_workflow_activation_ResolveNexusOperation( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("result"): + await self._visit_coresdk_nexus_NexusOperationResult(fs, o.result) + + async def _visit_coresdk_workflow_activation_WorkflowActivationJob( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("initialize_workflow"): + await self._visit_coresdk_workflow_activation_InitializeWorkflow( + fs, o.initialize_workflow + ) + elif o.HasField("query_workflow"): + await self._visit_coresdk_workflow_activation_QueryWorkflow( + fs, o.query_workflow + ) + elif o.HasField("signal_workflow"): + await self._visit_coresdk_workflow_activation_SignalWorkflow( + fs, o.signal_workflow + ) + elif o.HasField("resolve_activity"): + await self._visit_coresdk_workflow_activation_ResolveActivity( + fs, o.resolve_activity + ) + elif o.HasField("resolve_child_workflow_execution_start"): + await self._visit_coresdk_workflow_activation_ResolveChildWorkflowExecutionStart( + fs, o.resolve_child_workflow_execution_start + ) + elif o.HasField("resolve_child_workflow_execution"): + await self._visit_coresdk_workflow_activation_ResolveChildWorkflowExecution( + fs, o.resolve_child_workflow_execution + ) + elif o.HasField("resolve_signal_external_workflow"): + await self._visit_coresdk_workflow_activation_ResolveSignalExternalWorkflow( + fs, o.resolve_signal_external_workflow + ) + elif o.HasField("resolve_request_cancel_external_workflow"): + await self._visit_coresdk_workflow_activation_ResolveRequestCancelExternalWorkflow( + fs, o.resolve_request_cancel_external_workflow + ) + elif o.HasField("do_update"): + await self._visit_coresdk_workflow_activation_DoUpdate(fs, o.do_update) + elif o.HasField("resolve_nexus_operation_start"): + await self._visit_coresdk_workflow_activation_ResolveNexusOperationStart( + fs, o.resolve_nexus_operation_start + ) + elif o.HasField("resolve_nexus_operation"): + await self._visit_coresdk_workflow_activation_ResolveNexusOperation( + fs, o.resolve_nexus_operation + ) + + async def _visit_coresdk_workflow_activation_WorkflowActivation( + self, fs: VisitorFunctions, o: Any + ): + for v in o.jobs: + await self._visit_coresdk_workflow_activation_WorkflowActivationJob(fs, v) + + async def _visit_temporal_api_sdk_v1_UserMetadata( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("summary"): + await self._visit_temporal_api_common_v1_Payload(fs, o.summary) + if o.HasField("details"): + await self._visit_temporal_api_common_v1_Payload(fs, o.details) + + async def _visit_coresdk_workflow_commands_ScheduleActivity( + self, fs: VisitorFunctions, o: Any + ): + if not self.skip_headers: + for v in o.headers.values(): + await self._visit_temporal_api_common_v1_Payload(fs, v) + await self._visit_payload_container(fs, o.arguments) + + async def _visit_coresdk_workflow_commands_QuerySuccess( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("response"): + await self._visit_temporal_api_common_v1_Payload(fs, o.response) + + async def _visit_coresdk_workflow_commands_QueryResult( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("succeeded"): + await self._visit_coresdk_workflow_commands_QuerySuccess(fs, o.succeeded) + elif o.HasField("failed"): + await self._visit_temporal_api_failure_v1_Failure(fs, o.failed) + + async def _visit_coresdk_workflow_commands_CompleteWorkflowExecution( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("result"): + await self._visit_temporal_api_common_v1_Payload(fs, o.result) + + async def _visit_coresdk_workflow_commands_FailWorkflowExecution( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("failure"): + await self._visit_temporal_api_failure_v1_Failure(fs, o.failure) + + async def _visit_coresdk_workflow_commands_ContinueAsNewWorkflowExecution( + self, fs: VisitorFunctions, o: Any + ): + await self._visit_payload_container(fs, o.arguments) + for v in o.memo.values(): + await self._visit_temporal_api_common_v1_Payload(fs, v) + if not self.skip_headers: + for v in o.headers.values(): + await self._visit_temporal_api_common_v1_Payload(fs, v) + if o.HasField("search_attributes"): + await self._visit_temporal_api_common_v1_SearchAttributes( + fs, o.search_attributes + ) + + async def _visit_coresdk_workflow_commands_StartChildWorkflowExecution( + self, fs: VisitorFunctions, o: Any + ): + await self._visit_payload_container(fs, o.input) + if not self.skip_headers: + for v in o.headers.values(): + await self._visit_temporal_api_common_v1_Payload(fs, v) + for v in o.memo.values(): + await self._visit_temporal_api_common_v1_Payload(fs, v) + if o.HasField("search_attributes"): + await self._visit_temporal_api_common_v1_SearchAttributes( + fs, o.search_attributes + ) + + async def _visit_coresdk_workflow_commands_SignalExternalWorkflowExecution( + self, fs: VisitorFunctions, o: Any + ): + await self._visit_payload_container(fs, o.args) + if not self.skip_headers: + for v in o.headers.values(): + await self._visit_temporal_api_common_v1_Payload(fs, v) + + async def _visit_coresdk_workflow_commands_ScheduleLocalActivity( + self, fs: VisitorFunctions, o: Any + ): + if not self.skip_headers: + for v in o.headers.values(): + await self._visit_temporal_api_common_v1_Payload(fs, v) + await self._visit_payload_container(fs, o.arguments) + + async def _visit_coresdk_workflow_commands_UpsertWorkflowSearchAttributes( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("search_attributes"): + await self._visit_temporal_api_common_v1_SearchAttributes( + fs, o.search_attributes + ) + + async def _visit_coresdk_workflow_commands_ModifyWorkflowProperties( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("upserted_memo"): + await self._visit_temporal_api_common_v1_Memo(fs, o.upserted_memo) + + async def _visit_coresdk_workflow_commands_UpdateResponse( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("rejected"): + await self._visit_temporal_api_failure_v1_Failure(fs, o.rejected) + elif o.HasField("completed"): + await self._visit_temporal_api_common_v1_Payload(fs, o.completed) + + async def _visit_coresdk_workflow_commands_ScheduleNexusOperation( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("input"): + await self._visit_nexus_operation_input_payload(fs, o.input) + + async def _visit_coresdk_workflow_commands_WorkflowCommand( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("user_metadata"): + await self._visit_temporal_api_sdk_v1_UserMetadata(fs, o.user_metadata) + if o.HasField("schedule_activity"): + await self._visit_coresdk_workflow_commands_ScheduleActivity( + fs, o.schedule_activity + ) + elif o.HasField("respond_to_query"): + await self._visit_coresdk_workflow_commands_QueryResult( + fs, o.respond_to_query + ) + elif o.HasField("complete_workflow_execution"): + await self._visit_coresdk_workflow_commands_CompleteWorkflowExecution( + fs, o.complete_workflow_execution + ) + elif o.HasField("fail_workflow_execution"): + await self._visit_coresdk_workflow_commands_FailWorkflowExecution( + fs, o.fail_workflow_execution + ) + elif o.HasField("continue_as_new_workflow_execution"): + await self._visit_coresdk_workflow_commands_ContinueAsNewWorkflowExecution( + fs, o.continue_as_new_workflow_execution + ) + elif o.HasField("start_child_workflow_execution"): + await self._visit_coresdk_workflow_commands_StartChildWorkflowExecution( + fs, o.start_child_workflow_execution + ) + elif o.HasField("signal_external_workflow_execution"): + await self._visit_coresdk_workflow_commands_SignalExternalWorkflowExecution( + fs, o.signal_external_workflow_execution + ) + elif o.HasField("schedule_local_activity"): + await self._visit_coresdk_workflow_commands_ScheduleLocalActivity( + fs, o.schedule_local_activity + ) + elif o.HasField("upsert_workflow_search_attributes"): + await self._visit_coresdk_workflow_commands_UpsertWorkflowSearchAttributes( + fs, o.upsert_workflow_search_attributes + ) + elif o.HasField("modify_workflow_properties"): + await self._visit_coresdk_workflow_commands_ModifyWorkflowProperties( + fs, o.modify_workflow_properties + ) + elif o.HasField("update_response"): + await self._visit_coresdk_workflow_commands_UpdateResponse( + fs, o.update_response + ) + elif o.HasField("schedule_nexus_operation"): + await self._visit_coresdk_workflow_commands_ScheduleNexusOperation( + fs, o.schedule_nexus_operation + ) + + async def _visit_coresdk_workflow_completion_Success( + self, fs: VisitorFunctions, o: Any + ): + for v in o.commands: + await self._visit_coresdk_workflow_commands_WorkflowCommand(fs, v) + + async def _visit_coresdk_workflow_completion_Failure( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("failure"): + await self._visit_temporal_api_failure_v1_Failure(fs, o.failure) + + async def _visit_coresdk_workflow_completion_WorkflowActivationCompletion( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("successful"): + await self._visit_coresdk_workflow_completion_Success(fs, o.successful) + elif o.HasField("failed"): + await self._visit_coresdk_workflow_completion_Failure(fs, o.failed) + + async def _visit_temporal_api_nexus_v1_StartOperationResponse_Sync( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("payload"): + await self._visit_temporal_api_common_v1_Payload(fs, o.payload) + + async def _visit_temporal_api_nexus_v1_Failure(self, fs: VisitorFunctions, o: Any): + if o.HasField("cause"): + await self._visit_temporal_api_nexus_v1_Failure(fs, o.cause) + + async def _visit_temporal_api_nexus_v1_UnsuccessfulOperationError( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("failure"): + await self._visit_temporal_api_nexus_v1_Failure(fs, o.failure) + + async def _visit_temporal_api_nexus_v1_StartOperationResponse( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("sync_success"): + await self._visit_temporal_api_nexus_v1_StartOperationResponse_Sync( + fs, o.sync_success + ) + elif o.HasField("operation_error"): + await self._visit_temporal_api_nexus_v1_UnsuccessfulOperationError( + fs, o.operation_error + ) + elif o.HasField("failure"): + await self._visit_temporal_api_failure_v1_Failure(fs, o.failure) + + async def _visit_temporal_api_nexus_v1_Response(self, fs: VisitorFunctions, o: Any): + if o.HasField("start_operation"): + await self._visit_temporal_api_nexus_v1_StartOperationResponse( + fs, o.start_operation + ) + + async def _visit_temporal_api_nexus_v1_HandlerError( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("failure"): + await self._visit_temporal_api_nexus_v1_Failure(fs, o.failure) + + async def _visit_coresdk_nexus_NexusTaskCompletion( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("completed"): + await self._visit_temporal_api_nexus_v1_Response(fs, o.completed) + elif o.HasField("error"): + await self._visit_temporal_api_nexus_v1_HandlerError(fs, o.error) + elif o.HasField("failure"): + await self._visit_temporal_api_failure_v1_Failure(fs, o.failure) + + async def _visit_temporal_api_common_v1_Header(self, fs: VisitorFunctions, o: Any): + for v in o.fields.values(): + await self._visit_temporal_api_common_v1_Payload(fs, v) + + async def _visit_temporal_api_workflowservice_v1_SignalWithStartWorkflowExecutionRequest( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("input"): + await self._visit_temporal_api_common_v1_Payloads(fs, o.input) + if o.HasField("signal_input"): + await self._visit_temporal_api_common_v1_Payloads(fs, o.signal_input) + if o.HasField("memo"): + await self._visit_temporal_api_common_v1_Memo(fs, o.memo) + if o.HasField("search_attributes"): + await self._visit_temporal_api_common_v1_SearchAttributes( + fs, o.search_attributes + ) + if o.HasField("header"): + await self._visit_temporal_api_common_v1_Header(fs, o.header) + if o.HasField("user_metadata"): + await self._visit_temporal_api_sdk_v1_UserMetadata(fs, o.user_metadata) diff --git a/temporalio/bridge/_visitor_functions.py b/temporalio/bridge/_visitor_functions.py new file mode 100644 index 000000000..da8c67ae2 --- /dev/null +++ b/temporalio/bridge/_visitor_functions.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +import asyncio +from abc import ABC, abstractmethod + +from google.protobuf.internal.containers import RepeatedCompositeFieldContainer + +from temporalio.api.common.v1.message_pb2 import Payload + +PayloadSequence = list[Payload] | RepeatedCompositeFieldContainer[Payload] + + +class VisitorFunctions(ABC): + """Functions invoked by generated payload visitors.""" + + @abstractmethod + async def visit_payload(self, payload: Payload) -> None: + """Visit a single payload.""" + ... + + @abstractmethod + async def visit_payloads(self, payloads: PayloadSequence) -> None: + """Visit a sequence of payloads together.""" + ... + + async def visit_system_nexus_envelope(self, _payload: Payload) -> None: + """Visit a recognized system Nexus envelope payload.""" + return None + + def checkpoint(self) -> int | None: + """Return a marker for visits scheduled after this point, if supported.""" + return None + + async def drain_since(self, _checkpoint: int) -> None: + """Wait for visits scheduled after ``checkpoint`` to finish.""" + return None + + +class BoundedVisitorFunctions(VisitorFunctions): + """Wraps VisitorFunctions to cap concurrent payload visits via a semaphore. + + After the full traversal, call drain() to await all in-flight tasks. + """ + + def __init__(self, inner: VisitorFunctions, concurrency_limit: int) -> None: + """Create a bounded wrapper around the given visitor functions.""" + self._inner = inner + self._sem = asyncio.Semaphore(concurrency_limit) + self._tasks: list[asyncio.Task[None]] = [] + + async def visit_payload(self, payload: Payload) -> None: + """Visit a single payload once capacity is available.""" + await self._sem.acquire() + + async def _run() -> None: + try: + await self._inner.visit_payload(payload) + finally: + self._sem.release() + + self._tasks.append(asyncio.create_task(_run())) + + async def visit_payloads(self, payloads: PayloadSequence) -> None: + """Visit a sequence of payloads once capacity is available.""" + await self._sem.acquire() + + async def _run() -> None: + try: + await self._inner.visit_payloads(payloads) + finally: + self._sem.release() + + self._tasks.append(asyncio.create_task(_run())) + + async def visit_system_nexus_envelope(self, payload: Payload) -> None: + """Visit a system Nexus envelope payload once capacity is available.""" + await self._sem.acquire() + + async def _run() -> None: + try: + await self._inner.visit_system_nexus_envelope(payload) + finally: + self._sem.release() + + self._tasks.append(asyncio.create_task(_run())) + + def checkpoint(self) -> int: + """Return a marker for tasks scheduled after this point.""" + return len(self._tasks) + + async def drain_since(self, checkpoint: int) -> None: + """Wait for tasks scheduled after ``checkpoint`` to finish. + + This lets system-envelope traversal finish mutating its decoded value + before that value is serialized again, without waiting for unrelated + visits that were already in progress. + """ + await self._drain_tasks(self._tasks[checkpoint:]) + + async def drain(self) -> None: + """Wait for all in-flight background tasks to complete. + + On cancellation or error, cancels all remaining tasks and awaits + them so their finally blocks run before this coroutine returns. + """ + await self._drain_tasks(self._tasks) + + async def _drain_tasks(self, tasks: list[asyncio.Task[None]]) -> None: + """Wait for the given tasks, cancelling all tasks if one fails.""" + if not tasks: + return + try: + await asyncio.gather(*tasks) + except BaseException: + for task in self._tasks: + task.cancel() + await asyncio.gather(*self._tasks, return_exceptions=True) + raise diff --git a/temporalio/bridge/client.py b/temporalio/bridge/client.py index ddcee4445..213443f29 100644 --- a/temporalio/bridge/client.py +++ b/temporalio/bridge/client.py @@ -5,25 +5,29 @@ from __future__ import annotations +from collections.abc import Mapping from dataclasses import dataclass from datetime import timedelta -from typing import Mapping, Optional, Tuple, Type, TypeVar +from typing import TypeVar import google.protobuf.message import temporalio.bridge.runtime import temporalio.bridge.temporal_sdk_bridge -from temporalio.bridge.temporal_sdk_bridge import RPCError +from temporalio.bridge.temporal_sdk_bridge import ( + RPCError, # type:ignore[reportUnusedImport] +) @dataclass class ClientTlsConfig: """Python representation of the Rust struct for configuring TLS.""" - server_root_ca_cert: Optional[bytes] - domain: Optional[str] - client_cert: Optional[bytes] - client_private_key: Optional[bytes] + server_root_ca_cert: bytes | None + domain: str | None + client_cert: bytes | None + client_private_key: bytes | None + verification_server_name: str | None @dataclass @@ -34,7 +38,7 @@ class ClientRetryConfig: randomization_factor: float multiplier: float max_interval_millis: int - max_elapsed_time_millis: Optional[int] + max_elapsed_time_millis: int | None max_retries: int @@ -51,7 +55,16 @@ class ClientHttpConnectProxyConfig: """Python representation of the Rust struct for configuring HTTP proxy.""" target_host: str - basic_auth: Optional[Tuple[str, str]] + basic_auth: tuple[str, str] | None + + +@dataclass +class ClientDnsLoadBalancingConfig: + """Python representation of the Rust struct for configuring DNS load + balancing. + """ + + resolution_interval_millis: int @dataclass @@ -59,15 +72,19 @@ class ClientConfig: """Python representation of the Rust struct for configuring the client.""" target_url: str - metadata: Mapping[str, str] - api_key: Optional[str] + metadata: Mapping[str, str | bytes] + api_key: str | None identity: str - tls_config: Optional[ClientTlsConfig] - retry_config: Optional[ClientRetryConfig] - keep_alive_config: Optional[ClientKeepAliveConfig] + tls_config: ClientTlsConfig | None + retry_config: ClientRetryConfig | None + keep_alive_config: ClientKeepAliveConfig | None client_name: str client_version: str - http_connect_proxy_config: Optional[ClientHttpConnectProxyConfig] + http_connect_proxy_config: ClientHttpConnectProxyConfig | None + dns_load_balancing_config: ClientDnsLoadBalancingConfig | None + grpc_compression: str + payloads_warn_size: int + memo_warn_size: int @dataclass @@ -77,8 +94,8 @@ class RpcCall: rpc: str req: bytes retry: bool - metadata: Mapping[str, str] - timeout_millis: Optional[int] + metadata: Mapping[str, str | bytes] + timeout_millis: int | None ProtoMessage = TypeVar("ProtoMessage", bound=google.protobuf.message.Message) @@ -108,11 +125,11 @@ def __init__( self._runtime = runtime self._ref = ref - def update_metadata(self, metadata: Mapping[str, str]) -> None: + def update_metadata(self, metadata: Mapping[str, str | bytes]) -> None: """Update underlying metadata on Core client.""" self._ref.update_metadata(metadata) - def update_api_key(self, api_key: Optional[str]) -> None: + def update_api_key(self, api_key: str | None) -> None: """Update underlying API key on Core client.""" self._ref.update_api_key(api_key) @@ -122,10 +139,10 @@ async def call( service: str, rpc: str, req: google.protobuf.message.Message, - resp_type: Type[ProtoMessage], + resp_type: type[ProtoMessage], retry: bool, - metadata: Mapping[str, str], - timeout: Optional[timedelta], + metadata: Mapping[str, str | bytes], + timeout: timedelta | None, ) -> ProtoMessage: """Make RPC call using SDK Core.""" # Prepare call diff --git a/temporalio/bridge/metric.py b/temporalio/bridge/metric.py index 399fe5cc5..4b8d7453d 100644 --- a/temporalio/bridge/metric.py +++ b/temporalio/bridge/metric.py @@ -5,7 +5,7 @@ from __future__ import annotations -from typing import Mapping, Optional, Union +from collections.abc import Mapping import temporalio.bridge.runtime import temporalio.bridge.temporal_sdk_bridge @@ -15,7 +15,7 @@ class MetricMeter: """Metric meter using SDK Core.""" @staticmethod - def create(runtime: temporalio.bridge.runtime.Runtime) -> Optional[MetricMeter]: + def create(runtime: temporalio.bridge.runtime.Runtime) -> MetricMeter | None: """Create optional metric meter.""" ref = temporalio.bridge.temporal_sdk_bridge.new_metric_meter(runtime._ref) if not ref: @@ -42,8 +42,8 @@ def __init__( self, meter: MetricMeter, name: str, - description: Optional[str], - unit: Optional[str], + description: str | None, + unit: str | None, ) -> None: """Initialize counter metric.""" self._ref = meter._ref.new_counter(name, description, unit) @@ -62,8 +62,8 @@ def __init__( self, meter: MetricMeter, name: str, - description: Optional[str], - unit: Optional[str], + description: str | None, + unit: str | None, ) -> None: """Initialize histogram.""" self._ref = meter._ref.new_histogram(name, description, unit) @@ -82,8 +82,8 @@ def __init__( self, meter: MetricMeter, name: str, - description: Optional[str], - unit: Optional[str], + description: str | None, + unit: str | None, ) -> None: """Initialize histogram.""" self._ref = meter._ref.new_histogram_float(name, description, unit) @@ -102,8 +102,8 @@ def __init__( self, meter: MetricMeter, name: str, - description: Optional[str], - unit: Optional[str], + description: str | None, + unit: str | None, ) -> None: """Initialize histogram.""" self._ref = meter._ref.new_histogram_duration(name, description, unit) @@ -122,8 +122,8 @@ def __init__( self, meter: MetricMeter, name: str, - description: Optional[str], - unit: Optional[str], + description: str | None, + unit: str | None, ) -> None: """Initialize gauge.""" self._ref = meter._ref.new_gauge(name, description, unit) @@ -142,8 +142,8 @@ def __init__( self, meter: MetricMeter, name: str, - description: Optional[str], - unit: Optional[str], + description: str | None, + unit: str | None, ) -> None: """Initialize gauge.""" self._ref = meter._ref.new_gauge_float(name, description, unit) @@ -168,7 +168,7 @@ def __init__( self._ref = ref def with_additional_attributes( - self, new_attrs: Mapping[str, Union[str, int, float, bool]] + self, new_attrs: Mapping[str, str | int | float | bool] ) -> MetricAttributes: """Create new attributes with new attributes appended.""" return MetricAttributes( diff --git a/temporalio/bridge/proto/__init__.py b/temporalio/bridge/proto/__init__.py index d4e90a2fc..a48e10be7 100644 --- a/temporalio/bridge/proto/__init__.py +++ b/temporalio/bridge/proto/__init__.py @@ -3,6 +3,7 @@ ActivitySlotInfo, ActivityTaskCompletion, LocalActivitySlotInfo, + NamespaceInfo, NexusSlotInfo, WorkflowSlotInfo, ) @@ -12,6 +13,7 @@ "ActivitySlotInfo", "ActivityTaskCompletion", "LocalActivitySlotInfo", + "NamespaceInfo", "NexusSlotInfo", "WorkflowSlotInfo", ] diff --git a/temporalio/bridge/proto/activity_task/activity_task_pb2.py b/temporalio/bridge/proto/activity_task/activity_task_pb2.py index abb166222..0e09839dc 100644 --- a/temporalio/bridge/proto/activity_task/activity_task_pb2.py +++ b/temporalio/bridge/proto/activity_task/activity_task_pb2.py @@ -26,7 +26,7 @@ ) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n3temporal/sdk/core/activity_task/activity_task.proto\x12\x15\x63oresdk.activity_task\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\x1a%temporal/sdk/core/common/common.proto"\x8d\x01\n\x0c\x41\x63tivityTask\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12-\n\x05start\x18\x03 \x01(\x0b\x32\x1c.coresdk.activity_task.StartH\x00\x12/\n\x06\x63\x61ncel\x18\x04 \x01(\x0b\x32\x1d.coresdk.activity_task.CancelH\x00\x42\t\n\x07variant"\xa1\x07\n\x05Start\x12\x1a\n\x12workflow_namespace\x18\x01 \x01(\t\x12\x15\n\rworkflow_type\x18\x02 \x01(\t\x12\x45\n\x12workflow_execution\x18\x03 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x13\n\x0b\x61\x63tivity_id\x18\x04 \x01(\t\x12\x15\n\ractivity_type\x18\x05 \x01(\t\x12\x45\n\rheader_fields\x18\x06 \x03(\x0b\x32..coresdk.activity_task.Start.HeaderFieldsEntry\x12.\n\x05input\x18\x07 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12:\n\x11heartbeat_details\x18\x08 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x32\n\x0escheduled_time\x18\t \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x42\n\x1e\x63urrent_attempt_scheduled_time\x18\n \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x30\n\x0cstarted_time\x18\x0b \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0f\n\x07\x61ttempt\x18\x0c \x01(\r\x12<\n\x19schedule_to_close_timeout\x18\r \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\x0e \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x34\n\x11heartbeat_timeout\x18\x0f \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x0cretry_policy\x18\x10 \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x32\n\x08priority\x18\x12 \x01(\x0b\x32 .temporal.api.common.v1.Priority\x12\x10\n\x08is_local\x18\x11 \x01(\x08\x1aT\n\x11HeaderFieldsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\x8a\x01\n\x06\x43\x61ncel\x12;\n\x06reason\x18\x01 \x01(\x0e\x32+.coresdk.activity_task.ActivityCancelReason\x12\x43\n\x07\x64\x65tails\x18\x02 \x01(\x0b\x32\x32.coresdk.activity_task.ActivityCancellationDetails"\xa0\x01\n\x1b\x41\x63tivityCancellationDetails\x12\x14\n\x0cis_not_found\x18\x01 \x01(\x08\x12\x14\n\x0cis_cancelled\x18\x02 \x01(\x08\x12\x11\n\tis_paused\x18\x03 \x01(\x08\x12\x14\n\x0cis_timed_out\x18\x04 \x01(\x08\x12\x1a\n\x12is_worker_shutdown\x18\x05 \x01(\x08\x12\x10\n\x08is_reset\x18\x06 \x01(\x08*o\n\x14\x41\x63tivityCancelReason\x12\r\n\tNOT_FOUND\x10\x00\x12\r\n\tCANCELLED\x10\x01\x12\r\n\tTIMED_OUT\x10\x02\x12\x13\n\x0fWORKER_SHUTDOWN\x10\x03\x12\n\n\x06PAUSED\x10\x04\x12\t\n\x05RESET\x10\x05\x42\x32\xea\x02/Temporalio::Internal::Bridge::Api::ActivityTaskb\x06proto3' + b'\n3temporal/sdk/core/activity_task/activity_task.proto\x12\x15\x63oresdk.activity_task\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\x1a%temporal/sdk/core/common/common.proto"\x8d\x01\n\x0c\x41\x63tivityTask\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12-\n\x05start\x18\x03 \x01(\x0b\x32\x1c.coresdk.activity_task.StartH\x00\x12/\n\x06\x63\x61ncel\x18\x04 \x01(\x0b\x32\x1d.coresdk.activity_task.CancelH\x00\x42\t\n\x07variant"\xb1\x07\n\x05Start\x12\x1a\n\x12workflow_namespace\x18\x01 \x01(\t\x12\x15\n\rworkflow_type\x18\x02 \x01(\t\x12\x45\n\x12workflow_execution\x18\x03 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x13\n\x0b\x61\x63tivity_id\x18\x04 \x01(\t\x12\x15\n\ractivity_type\x18\x05 \x01(\t\x12\x45\n\rheader_fields\x18\x06 \x03(\x0b\x32..coresdk.activity_task.Start.HeaderFieldsEntry\x12.\n\x05input\x18\x07 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12:\n\x11heartbeat_details\x18\x08 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x32\n\x0escheduled_time\x18\t \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x42\n\x1e\x63urrent_attempt_scheduled_time\x18\n \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x30\n\x0cstarted_time\x18\x0b \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0f\n\x07\x61ttempt\x18\x0c \x01(\r\x12<\n\x19schedule_to_close_timeout\x18\r \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\x0e \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x34\n\x11heartbeat_timeout\x18\x0f \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x0cretry_policy\x18\x10 \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x32\n\x08priority\x18\x12 \x01(\x0b\x32 .temporal.api.common.v1.Priority\x12\x10\n\x08is_local\x18\x11 \x01(\x08\x12\x0e\n\x06run_id\x18\x13 \x01(\t\x1aT\n\x11HeaderFieldsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\x8a\x01\n\x06\x43\x61ncel\x12;\n\x06reason\x18\x01 \x01(\x0e\x32+.coresdk.activity_task.ActivityCancelReason\x12\x43\n\x07\x64\x65tails\x18\x02 \x01(\x0b\x32\x32.coresdk.activity_task.ActivityCancellationDetails"\xa0\x01\n\x1b\x41\x63tivityCancellationDetails\x12\x14\n\x0cis_not_found\x18\x01 \x01(\x08\x12\x14\n\x0cis_cancelled\x18\x02 \x01(\x08\x12\x11\n\tis_paused\x18\x03 \x01(\x08\x12\x14\n\x0cis_timed_out\x18\x04 \x01(\x08\x12\x1a\n\x12is_worker_shutdown\x18\x05 \x01(\x08\x12\x10\n\x08is_reset\x18\x06 \x01(\x08*o\n\x14\x41\x63tivityCancelReason\x12\r\n\tNOT_FOUND\x10\x00\x12\r\n\tCANCELLED\x10\x01\x12\r\n\tTIMED_OUT\x10\x02\x12\x13\n\x0fWORKER_SHUTDOWN\x10\x03\x12\n\n\x06PAUSED\x10\x04\x12\t\n\x05RESET\x10\x05\x42\x32\xea\x02/Temporalio::Internal::Bridge::Api::ActivityTaskb\x06proto3' ) _ACTIVITYCANCELREASON = DESCRIPTOR.enum_types_by_name["ActivityCancelReason"] @@ -107,16 +107,16 @@ ) _START_HEADERFIELDSENTRY._options = None _START_HEADERFIELDSENTRY._serialized_options = b"8\001" - _ACTIVITYCANCELREASON._serialized_start = 1600 - _ACTIVITYCANCELREASON._serialized_end = 1711 + _ACTIVITYCANCELREASON._serialized_start = 1616 + _ACTIVITYCANCELREASON._serialized_end = 1727 _ACTIVITYTASK._serialized_start = 221 _ACTIVITYTASK._serialized_end = 362 _START._serialized_start = 365 - _START._serialized_end = 1294 - _START_HEADERFIELDSENTRY._serialized_start = 1210 - _START_HEADERFIELDSENTRY._serialized_end = 1294 - _CANCEL._serialized_start = 1297 - _CANCEL._serialized_end = 1435 - _ACTIVITYCANCELLATIONDETAILS._serialized_start = 1438 - _ACTIVITYCANCELLATIONDETAILS._serialized_end = 1598 + _START._serialized_end = 1310 + _START_HEADERFIELDSENTRY._serialized_start = 1226 + _START_HEADERFIELDSENTRY._serialized_end = 1310 + _CANCEL._serialized_start = 1313 + _CANCEL._serialized_end = 1451 + _ACTIVITYCANCELLATIONDETAILS._serialized_start = 1454 + _ACTIVITYCANCELLATIONDETAILS._serialized_end = 1614 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/bridge/proto/activity_task/activity_task_pb2.pyi b/temporalio/bridge/proto/activity_task/activity_task_pb2.pyi index 6d977759c..1a2f8c7d2 100644 --- a/temporalio/bridge/proto/activity_task/activity_task_pb2.pyi +++ b/temporalio/bridge/proto/activity_task/activity_task_pb2.pyi @@ -159,6 +159,7 @@ class Start(google.protobuf.message.Message): RETRY_POLICY_FIELD_NUMBER: builtins.int PRIORITY_FIELD_NUMBER: builtins.int IS_LOCAL_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int workflow_namespace: builtins.str """The namespace the workflow lives in""" workflow_type: builtins.str @@ -224,6 +225,8 @@ class Start(google.protobuf.message.Message): """Set to true if this is a local activity. Note that heartbeating does not apply to local activities. """ + run_id: builtins.str + """Run ID of this activity execution. Only set for standalone activities.""" def __init__( self, *, @@ -254,6 +257,7 @@ class Start(google.protobuf.message.Message): retry_policy: temporalio.api.common.v1.message_pb2.RetryPolicy | None = ..., priority: temporalio.api.common.v1.message_pb2.Priority | None = ..., is_local: builtins.bool = ..., + run_id: builtins.str = ..., ) -> None: ... def HasField( self, @@ -303,6 +307,8 @@ class Start(google.protobuf.message.Message): b"priority", "retry_policy", b"retry_policy", + "run_id", + b"run_id", "schedule_to_close_timeout", b"schedule_to_close_timeout", "scheduled_time", diff --git a/temporalio/bridge/proto/common/__init__.py b/temporalio/bridge/proto/common/__init__.py index 5622fffb8..a8506090d 100644 --- a/temporalio/bridge/proto/common/__init__.py +++ b/temporalio/bridge/proto/common/__init__.py @@ -1,10 +1,12 @@ from .common_pb2 import ( + ExternalStorageMetrics, NamespacedWorkflowExecution, VersioningIntent, WorkerDeploymentVersion, ) __all__ = [ + "ExternalStorageMetrics", "NamespacedWorkflowExecution", "VersioningIntent", "WorkerDeploymentVersion", diff --git a/temporalio/bridge/proto/common/common_pb2.py b/temporalio/bridge/proto/common/common_pb2.py index c56456fce..481cf216d 100644 --- a/temporalio/bridge/proto/common/common_pb2.py +++ b/temporalio/bridge/proto/common/common_pb2.py @@ -18,7 +18,7 @@ from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2 DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n%temporal/sdk/core/common/common.proto\x12\x0e\x63oresdk.common\x1a\x1egoogle/protobuf/duration.proto"U\n\x1bNamespacedWorkflowExecution\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\t"D\n\x17WorkerDeploymentVersion\x12\x17\n\x0f\x64\x65ployment_name\x18\x01 \x01(\t\x12\x10\n\x08\x62uild_id\x18\x02 \x01(\t*@\n\x10VersioningIntent\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x0e\n\nCOMPATIBLE\x10\x01\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x02\x42,\xea\x02)Temporalio::Internal::Bridge::Api::Commonb\x06proto3' + b'\n%temporal/sdk/core/common/common.proto\x12\x0e\x63oresdk.common\x1a\x1egoogle/protobuf/duration.proto"U\n\x1bNamespacedWorkflowExecution\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\t"D\n\x17WorkerDeploymentVersion\x12\x17\n\x0f\x64\x65ployment_name\x18\x01 \x01(\t\x12\x10\n\x08\x62uild_id\x18\x02 \x01(\t"\x92\x01\n\x16\x45xternalStorageMetrics\x12\x15\n\rpayload_count\x18\x01 \x01(\x04\x12\x18\n\x10total_size_bytes\x18\x02 \x01(\x04\x12\x31\n\x0etotal_duration\x18\x03 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x14\n\x0c\x64river_names\x18\x04 \x03(\t*@\n\x10VersioningIntent\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x0e\n\nCOMPATIBLE\x10\x01\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x02\x42,\xea\x02)Temporalio::Internal::Bridge::Api::Commonb\x06proto3' ) _VERSIONINGINTENT = DESCRIPTOR.enum_types_by_name["VersioningIntent"] @@ -32,6 +32,7 @@ "NamespacedWorkflowExecution" ] _WORKERDEPLOYMENTVERSION = DESCRIPTOR.message_types_by_name["WorkerDeploymentVersion"] +_EXTERNALSTORAGEMETRICS = DESCRIPTOR.message_types_by_name["ExternalStorageMetrics"] NamespacedWorkflowExecution = _reflection.GeneratedProtocolMessageType( "NamespacedWorkflowExecution", (_message.Message,), @@ -54,15 +55,28 @@ ) _sym_db.RegisterMessage(WorkerDeploymentVersion) +ExternalStorageMetrics = _reflection.GeneratedProtocolMessageType( + "ExternalStorageMetrics", + (_message.Message,), + { + "DESCRIPTOR": _EXTERNALSTORAGEMETRICS, + "__module__": "temporal.sdk.core.common.common_pb2", + # @@protoc_insertion_point(class_scope:coresdk.common.ExternalStorageMetrics) + }, +) +_sym_db.RegisterMessage(ExternalStorageMetrics) + if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = ( b"\352\002)Temporalio::Internal::Bridge::Api::Common" ) - _VERSIONINGINTENT._serialized_start = 246 - _VERSIONINGINTENT._serialized_end = 310 + _VERSIONINGINTENT._serialized_start = 395 + _VERSIONINGINTENT._serialized_end = 459 _NAMESPACEDWORKFLOWEXECUTION._serialized_start = 89 _NAMESPACEDWORKFLOWEXECUTION._serialized_end = 174 _WORKERDEPLOYMENTVERSION._serialized_start = 176 _WORKERDEPLOYMENTVERSION._serialized_end = 244 + _EXTERNALSTORAGEMETRICS._serialized_start = 247 + _EXTERNALSTORAGEMETRICS._serialized_end = 393 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/bridge/proto/common/common_pb2.pyi b/temporalio/bridge/proto/common/common_pb2.pyi index 739a129e1..8862fa036 100644 --- a/temporalio/bridge/proto/common/common_pb2.pyi +++ b/temporalio/bridge/proto/common/common_pb2.pyi @@ -4,10 +4,13 @@ isort:skip_file """ import builtins +import collections.abc import sys import typing import google.protobuf.descriptor +import google.protobuf.duration_pb2 +import google.protobuf.internal.containers import google.protobuf.internal.enum_type_wrapper import google.protobuf.message @@ -121,3 +124,53 @@ class WorkerDeploymentVersion(google.protobuf.message.Message): ) -> None: ... global___WorkerDeploymentVersion = WorkerDeploymentVersion + +class ExternalStorageMetrics(google.protobuf.message.Message): + """Metrics for a set of external payload storage operations (all uploads and downloads) + performed while processing a task, so core can emit unified logging and metrics. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + PAYLOAD_COUNT_FIELD_NUMBER: builtins.int + TOTAL_SIZE_BYTES_FIELD_NUMBER: builtins.int + TOTAL_DURATION_FIELD_NUMBER: builtins.int + DRIVER_NAMES_FIELD_NUMBER: builtins.int + payload_count: builtins.int + """Number of payloads stored or retrieved externally.""" + total_size_bytes: builtins.int + """Total size in bytes of the externally stored or retrieved payloads.""" + @property + def total_duration(self) -> google.protobuf.duration_pb2.Duration: + """Wall-clock time spent on the external storage operations.""" + @property + def driver_names( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """Names of the drivers that participated in the operations.""" + def __init__( + self, + *, + payload_count: builtins.int = ..., + total_size_bytes: builtins.int = ..., + total_duration: google.protobuf.duration_pb2.Duration | None = ..., + driver_names: collections.abc.Iterable[builtins.str] | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["total_duration", b"total_duration"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "driver_names", + b"driver_names", + "payload_count", + b"payload_count", + "total_duration", + b"total_duration", + "total_size_bytes", + b"total_size_bytes", + ], + ) -> None: ... + +global___ExternalStorageMetrics = ExternalStorageMetrics diff --git a/temporalio/bridge/proto/core_interface_pb2.py b/temporalio/bridge/proto/core_interface_pb2.py index 7531a37a5..19f330433 100644 --- a/temporalio/bridge/proto/core_interface_pb2.py +++ b/temporalio/bridge/proto/core_interface_pb2.py @@ -44,7 +44,7 @@ ) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n&temporal/sdk/core/core_interface.proto\x12\x07\x63oresdk\x1a\x1egoogle/protobuf/duration.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\x1a\x37temporal/sdk/core/activity_result/activity_result.proto\x1a\x33temporal/sdk/core/activity_task/activity_task.proto\x1a%temporal/sdk/core/common/common.proto\x1a\x33temporal/sdk/core/external_data/external_data.proto\x1a?temporal/sdk/core/workflow_activation/workflow_activation.proto\x1a;temporal/sdk/core/workflow_commands/workflow_commands.proto\x1a?temporal/sdk/core/workflow_completion/workflow_completion.proto"Y\n\x11\x41\x63tivityHeartbeat\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12\x30\n\x07\x64\x65tails\x18\x02 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload"n\n\x16\x41\x63tivityTaskCompletion\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12@\n\x06result\x18\x02 \x01(\x0b\x32\x30.coresdk.activity_result.ActivityExecutionResult"<\n\x10WorkflowSlotInfo\x12\x15\n\rworkflow_type\x18\x01 \x01(\t\x12\x11\n\tis_sticky\x18\x02 \x01(\x08")\n\x10\x41\x63tivitySlotInfo\x12\x15\n\ractivity_type\x18\x01 \x01(\t".\n\x15LocalActivitySlotInfo\x12\x15\n\ractivity_type\x18\x01 \x01(\t"3\n\rNexusSlotInfo\x12\x0f\n\x07service\x18\x01 \x01(\t\x12\x11\n\toperation\x18\x02 \x01(\tB3\xea\x02\x30Temporalio::Internal::Bridge::Api::CoreInterfaceb\x06proto3' + b'\n&temporal/sdk/core/core_interface.proto\x12\x07\x63oresdk\x1a\x1egoogle/protobuf/duration.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\x1a\x37temporal/sdk/core/activity_result/activity_result.proto\x1a\x33temporal/sdk/core/activity_task/activity_task.proto\x1a%temporal/sdk/core/common/common.proto\x1a\x33temporal/sdk/core/external_data/external_data.proto\x1a?temporal/sdk/core/workflow_activation/workflow_activation.proto\x1a;temporal/sdk/core/workflow_commands/workflow_commands.proto\x1a?temporal/sdk/core/workflow_completion/workflow_completion.proto"Y\n\x11\x41\x63tivityHeartbeat\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12\x30\n\x07\x64\x65tails\x18\x02 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload"n\n\x16\x41\x63tivityTaskCompletion\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12@\n\x06result\x18\x02 \x01(\x0b\x32\x30.coresdk.activity_result.ActivityExecutionResult"<\n\x10WorkflowSlotInfo\x12\x15\n\rworkflow_type\x18\x01 \x01(\t\x12\x11\n\tis_sticky\x18\x02 \x01(\x08")\n\x10\x41\x63tivitySlotInfo\x12\x15\n\ractivity_type\x18\x01 \x01(\t".\n\x15LocalActivitySlotInfo\x12\x15\n\ractivity_type\x18\x01 \x01(\t"3\n\rNexusSlotInfo\x12\x0f\n\x07service\x18\x01 \x01(\t\x12\x11\n\toperation\x18\x02 \x01(\t"\x86\x01\n\rNamespaceInfo\x12-\n\x06limits\x18\x01 \x01(\x0b\x32\x1d.coresdk.NamespaceInfo.Limits\x1a\x46\n\x06Limits\x12\x1d\n\x15\x62lob_size_limit_error\x18\x01 \x01(\x03\x12\x1d\n\x15memo_size_limit_error\x18\x02 \x01(\x03\x42\x33\xea\x02\x30Temporalio::Internal::Bridge::Api::CoreInterfaceb\x06proto3' ) @@ -54,6 +54,8 @@ _ACTIVITYSLOTINFO = DESCRIPTOR.message_types_by_name["ActivitySlotInfo"] _LOCALACTIVITYSLOTINFO = DESCRIPTOR.message_types_by_name["LocalActivitySlotInfo"] _NEXUSSLOTINFO = DESCRIPTOR.message_types_by_name["NexusSlotInfo"] +_NAMESPACEINFO = DESCRIPTOR.message_types_by_name["NamespaceInfo"] +_NAMESPACEINFO_LIMITS = _NAMESPACEINFO.nested_types_by_name["Limits"] ActivityHeartbeat = _reflection.GeneratedProtocolMessageType( "ActivityHeartbeat", (_message.Message,), @@ -120,6 +122,27 @@ ) _sym_db.RegisterMessage(NexusSlotInfo) +NamespaceInfo = _reflection.GeneratedProtocolMessageType( + "NamespaceInfo", + (_message.Message,), + { + "Limits": _reflection.GeneratedProtocolMessageType( + "Limits", + (_message.Message,), + { + "DESCRIPTOR": _NAMESPACEINFO_LIMITS, + "__module__": "temporal.sdk.core.core_interface_pb2", + # @@protoc_insertion_point(class_scope:coresdk.NamespaceInfo.Limits) + }, + ), + "DESCRIPTOR": _NAMESPACEINFO, + "__module__": "temporal.sdk.core.core_interface_pb2", + # @@protoc_insertion_point(class_scope:coresdk.NamespaceInfo) + }, +) +_sym_db.RegisterMessage(NamespaceInfo) +_sym_db.RegisterMessage(NamespaceInfo.Limits) + if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = ( @@ -137,4 +160,8 @@ _LOCALACTIVITYSLOTINFO._serialized_end = 930 _NEXUSSLOTINFO._serialized_start = 932 _NEXUSSLOTINFO._serialized_end = 983 + _NAMESPACEINFO._serialized_start = 986 + _NAMESPACEINFO._serialized_end = 1120 + _NAMESPACEINFO_LIMITS._serialized_start = 1050 + _NAMESPACEINFO_LIMITS._serialized_end = 1120 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/bridge/proto/core_interface_pb2.pyi b/temporalio/bridge/proto/core_interface_pb2.pyi index 020359cf0..46fa4eccd 100644 --- a/temporalio/bridge/proto/core_interface_pb2.pyi +++ b/temporalio/bridge/proto/core_interface_pb2.pyi @@ -165,3 +165,54 @@ class NexusSlotInfo(google.protobuf.message.Message): ) -> None: ... global___NexusSlotInfo = NexusSlotInfo + +class NamespaceInfo(google.protobuf.message.Message): + """Info about a namespace""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class Limits(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + BLOB_SIZE_LIMIT_ERROR_FIELD_NUMBER: builtins.int + MEMO_SIZE_LIMIT_ERROR_FIELD_NUMBER: builtins.int + blob_size_limit_error: builtins.int + """Maximum size in bytes for payload fields in workflow history events + (e.g., workflow/activity inputs and results, failure details, signal payloads). + When exceeded, the server will reject the operation with an error. + """ + memo_size_limit_error: builtins.int + """Maximum total memo size in bytes per workflow execution.""" + def __init__( + self, + *, + blob_size_limit_error: builtins.int = ..., + memo_size_limit_error: builtins.int = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "blob_size_limit_error", + b"blob_size_limit_error", + "memo_size_limit_error", + b"memo_size_limit_error", + ], + ) -> None: ... + + LIMITS_FIELD_NUMBER: builtins.int + @property + def limits(self) -> global___NamespaceInfo.Limits: + """Namespace configured limits""" + def __init__( + self, + *, + limits: global___NamespaceInfo.Limits | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["limits", b"limits"] + ) -> builtins.bool: ... + def ClearField( + self, field_name: typing_extensions.Literal["limits", b"limits"] + ) -> None: ... + +global___NamespaceInfo = NamespaceInfo diff --git a/temporalio/bridge/proto/nexus/nexus_pb2.py b/temporalio/bridge/proto/nexus/nexus_pb2.py index 4dc3bea86..d932c3571 100644 --- a/temporalio/bridge/proto/nexus/nexus_pb2.py +++ b/temporalio/bridge/proto/nexus/nexus_pb2.py @@ -15,6 +15,8 @@ _sym_db = _symbol_database.Default() +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 + from temporalio.api.common.v1 import ( message_pb2 as temporal_dot_api_dot_common_dot_v1_dot_message__pb2, ) @@ -32,7 +34,7 @@ ) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n#temporal/sdk/core/nexus/nexus.proto\x12\rcoresdk.nexus\x1a$temporal/api/common/v1/message.proto\x1a%temporal/api/failure/v1/message.proto\x1a#temporal/api/nexus/v1/message.proto\x1a\x36temporal/api/workflowservice/v1/request_response.proto\x1a%temporal/sdk/core/common/common.proto"\xf8\x01\n\x14NexusOperationResult\x12\x34\n\tcompleted\x18\x01 \x01(\x0b\x32\x1f.temporal.api.common.v1.PayloadH\x00\x12\x32\n\x06\x66\x61iled\x18\x02 \x01(\x0b\x32 .temporal.api.failure.v1.FailureH\x00\x12\x35\n\tcancelled\x18\x03 \x01(\x0b\x32 .temporal.api.failure.v1.FailureH\x00\x12\x35\n\ttimed_out\x18\x04 \x01(\x0b\x32 .temporal.api.failure.v1.FailureH\x00\x42\x08\n\x06status"\xb5\x01\n\x13NexusTaskCompletion\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12\x34\n\tcompleted\x18\x02 \x01(\x0b\x32\x1f.temporal.api.nexus.v1.ResponseH\x00\x12\x34\n\x05\x65rror\x18\x03 \x01(\x0b\x32#.temporal.api.nexus.v1.HandlerErrorH\x00\x12\x14\n\nack_cancel\x18\x04 \x01(\x08H\x00\x42\x08\n\x06status"\x9a\x01\n\tNexusTask\x12K\n\x04task\x18\x01 \x01(\x0b\x32;.temporal.api.workflowservice.v1.PollNexusTaskQueueResponseH\x00\x12\x35\n\x0b\x63\x61ncel_task\x18\x02 \x01(\x0b\x32\x1e.coresdk.nexus.CancelNexusTaskH\x00\x42\t\n\x07variant"[\n\x0f\x43\x61ncelNexusTask\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12\x34\n\x06reason\x18\x02 \x01(\x0e\x32$.coresdk.nexus.NexusTaskCancelReason*;\n\x15NexusTaskCancelReason\x12\r\n\tTIMED_OUT\x10\x00\x12\x13\n\x0fWORKER_SHUTDOWN\x10\x01*\x7f\n\x1eNexusOperationCancellationType\x12\x1f\n\x1bWAIT_CANCELLATION_COMPLETED\x10\x00\x12\x0b\n\x07\x41\x42\x41NDON\x10\x01\x12\x0e\n\nTRY_CANCEL\x10\x02\x12\x1f\n\x1bWAIT_CANCELLATION_REQUESTED\x10\x03\x42+\xea\x02(Temporalio::Internal::Bridge::Api::Nexusb\x06proto3' + b'\n#temporal/sdk/core/nexus/nexus.proto\x12\rcoresdk.nexus\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\x1a%temporal/api/failure/v1/message.proto\x1a#temporal/api/nexus/v1/message.proto\x1a\x36temporal/api/workflowservice/v1/request_response.proto\x1a%temporal/sdk/core/common/common.proto"\xf8\x01\n\x14NexusOperationResult\x12\x34\n\tcompleted\x18\x01 \x01(\x0b\x32\x1f.temporal.api.common.v1.PayloadH\x00\x12\x32\n\x06\x66\x61iled\x18\x02 \x01(\x0b\x32 .temporal.api.failure.v1.FailureH\x00\x12\x35\n\tcancelled\x18\x03 \x01(\x0b\x32 .temporal.api.failure.v1.FailureH\x00\x12\x35\n\ttimed_out\x18\x04 \x01(\x0b\x32 .temporal.api.failure.v1.FailureH\x00\x42\x08\n\x06status"\xee\x01\n\x13NexusTaskCompletion\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12\x34\n\tcompleted\x18\x02 \x01(\x0b\x32\x1f.temporal.api.nexus.v1.ResponseH\x00\x12\x38\n\x05\x65rror\x18\x03 \x01(\x0b\x32#.temporal.api.nexus.v1.HandlerErrorB\x02\x18\x01H\x00\x12\x14\n\nack_cancel\x18\x04 \x01(\x08H\x00\x12\x33\n\x07\x66\x61ilure\x18\x05 \x01(\x0b\x32 .temporal.api.failure.v1.FailureH\x00\x42\x08\n\x06status"\xe2\x01\n\tNexusTask\x12K\n\x04task\x18\x01 \x01(\x0b\x32;.temporal.api.workflowservice.v1.PollNexusTaskQueueResponseH\x00\x12\x35\n\x0b\x63\x61ncel_task\x18\x02 \x01(\x0b\x32\x1e.coresdk.nexus.CancelNexusTaskH\x00\x12\x34\n\x10request_deadline\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x10\n\x08\x65ndpoint\x18\x04 \x01(\tB\t\n\x07variant"[\n\x0f\x43\x61ncelNexusTask\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12\x34\n\x06reason\x18\x02 \x01(\x0e\x32$.coresdk.nexus.NexusTaskCancelReason*;\n\x15NexusTaskCancelReason\x12\r\n\tTIMED_OUT\x10\x00\x12\x13\n\x0fWORKER_SHUTDOWN\x10\x01*\x7f\n\x1eNexusOperationCancellationType\x12\x1f\n\x1bWAIT_CANCELLATION_COMPLETED\x10\x00\x12\x0b\n\x07\x41\x42\x41NDON\x10\x01\x12\x0e\n\nTRY_CANCEL\x10\x02\x12\x1f\n\x1bWAIT_CANCELLATION_REQUESTED\x10\x03\x42+\xea\x02(Temporalio::Internal::Bridge::Api::Nexusb\x06proto3' ) _NEXUSTASKCANCELREASON = DESCRIPTOR.enum_types_by_name["NexusTaskCancelReason"] @@ -104,16 +106,18 @@ DESCRIPTOR._serialized_options = ( b"\352\002(Temporalio::Internal::Bridge::Api::Nexus" ) - _NEXUSTASKCANCELREASON._serialized_start = 948 - _NEXUSTASKCANCELREASON._serialized_end = 1007 - _NEXUSOPERATIONCANCELLATIONTYPE._serialized_start = 1009 - _NEXUSOPERATIONCANCELLATIONTYPE._serialized_end = 1136 - _NEXUSOPERATIONRESULT._serialized_start = 264 - _NEXUSOPERATIONRESULT._serialized_end = 512 - _NEXUSTASKCOMPLETION._serialized_start = 515 - _NEXUSTASKCOMPLETION._serialized_end = 696 - _NEXUSTASK._serialized_start = 699 - _NEXUSTASK._serialized_end = 853 - _CANCELNEXUSTASK._serialized_start = 855 - _CANCELNEXUSTASK._serialized_end = 946 + _NEXUSTASKCOMPLETION.fields_by_name["error"]._options = None + _NEXUSTASKCOMPLETION.fields_by_name["error"]._serialized_options = b"\030\001" + _NEXUSTASKCANCELREASON._serialized_start = 1110 + _NEXUSTASKCANCELREASON._serialized_end = 1169 + _NEXUSOPERATIONCANCELLATIONTYPE._serialized_start = 1171 + _NEXUSOPERATIONCANCELLATIONTYPE._serialized_end = 1298 + _NEXUSOPERATIONRESULT._serialized_start = 297 + _NEXUSOPERATIONRESULT._serialized_end = 545 + _NEXUSTASKCOMPLETION._serialized_start = 548 + _NEXUSTASKCOMPLETION._serialized_end = 786 + _NEXUSTASK._serialized_start = 789 + _NEXUSTASK._serialized_end = 1015 + _CANCELNEXUSTASK._serialized_start = 1017 + _CANCELNEXUSTASK._serialized_end = 1108 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/bridge/proto/nexus/nexus_pb2.pyi b/temporalio/bridge/proto/nexus/nexus_pb2.pyi index 8dfe261b7..3cfafac26 100644 --- a/temporalio/bridge/proto/nexus/nexus_pb2.pyi +++ b/temporalio/bridge/proto/nexus/nexus_pb2.pyi @@ -10,6 +10,7 @@ import typing import google.protobuf.descriptor import google.protobuf.internal.enum_type_wrapper import google.protobuf.message +import google.protobuf.timestamp_pb2 import temporalio.api.common.v1.message_pb2 import temporalio.api.failure.v1.message_pb2 @@ -164,6 +165,7 @@ class NexusTaskCompletion(google.protobuf.message.Message): COMPLETED_FIELD_NUMBER: builtins.int ERROR_FIELD_NUMBER: builtins.int ACK_CANCEL_FIELD_NUMBER: builtins.int + FAILURE_FIELD_NUMBER: builtins.int task_token: builtins.bytes """The unique identifier for this task provided in the poll response""" @property @@ -173,13 +175,16 @@ class NexusTaskCompletion(google.protobuf.message.Message): """ @property def error(self) -> temporalio.api.nexus.v1.message_pb2.HandlerError: - """The handler could not complete the request for some reason.""" + """The handler could not complete the request for some reason. Deprecated, use failure.""" ack_cancel: builtins.bool """The lang SDK acknowledges that it is responding to a `CancelNexusTask` and thus the response is irrelevant. This is not the only way to respond to a cancel, the other variants can still be used, but this variant should be used when the handler was aborted by cancellation. """ + @property + def failure(self) -> temporalio.api.failure.v1.message_pb2.Failure: + """The handler could not complete the request for some reason.""" def __init__( self, *, @@ -187,6 +192,7 @@ class NexusTaskCompletion(google.protobuf.message.Message): completed: temporalio.api.nexus.v1.message_pb2.Response | None = ..., error: temporalio.api.nexus.v1.message_pb2.HandlerError | None = ..., ack_cancel: builtins.bool = ..., + failure: temporalio.api.failure.v1.message_pb2.Failure | None = ..., ) -> None: ... def HasField( self, @@ -197,6 +203,8 @@ class NexusTaskCompletion(google.protobuf.message.Message): b"completed", "error", b"error", + "failure", + b"failure", "status", b"status", ], @@ -210,6 +218,8 @@ class NexusTaskCompletion(google.protobuf.message.Message): b"completed", "error", b"error", + "failure", + b"failure", "status", b"status", "task_token", @@ -218,7 +228,9 @@ class NexusTaskCompletion(google.protobuf.message.Message): ) -> None: ... def WhichOneof( self, oneof_group: typing_extensions.Literal["status", b"status"] - ) -> typing_extensions.Literal["completed", "error", "ack_cancel"] | None: ... + ) -> ( + typing_extensions.Literal["completed", "error", "ack_cancel", "failure"] | None + ): ... global___NexusTaskCompletion = NexusTaskCompletion @@ -227,6 +239,8 @@ class NexusTask(google.protobuf.message.Message): TASK_FIELD_NUMBER: builtins.int CANCEL_TASK_FIELD_NUMBER: builtins.int + REQUEST_DEADLINE_FIELD_NUMBER: builtins.int + ENDPOINT_FIELD_NUMBER: builtins.int @property def task( self, @@ -246,23 +260,51 @@ class NexusTask(google.protobuf.message.Message): EX: Core knows the nexus operation has timed out, and it does not make sense for the user's operation handler to continue doing work. """ + @property + def request_deadline(self) -> google.protobuf.timestamp_pb2.Timestamp: + """The deadline for this request, parsed from the "Request-Timeout" header. + Only set when variant is `task` and the header was present with a valid value. + Represented as an absolute timestamp. + """ + endpoint: builtins.str + """The endpoint this request was addressed to. Extracted from the request for convenient access. + Only set when variant is `task`. + """ def __init__( self, *, task: temporalio.api.workflowservice.v1.request_response_pb2.PollNexusTaskQueueResponse | None = ..., cancel_task: global___CancelNexusTask | None = ..., + request_deadline: google.protobuf.timestamp_pb2.Timestamp | None = ..., + endpoint: builtins.str = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal[ - "cancel_task", b"cancel_task", "task", b"task", "variant", b"variant" + "cancel_task", + b"cancel_task", + "request_deadline", + b"request_deadline", + "task", + b"task", + "variant", + b"variant", ], ) -> builtins.bool: ... def ClearField( self, field_name: typing_extensions.Literal[ - "cancel_task", b"cancel_task", "task", b"task", "variant", b"variant" + "cancel_task", + b"cancel_task", + "endpoint", + b"endpoint", + "request_deadline", + b"request_deadline", + "task", + b"task", + "variant", + b"variant", ], ) -> None: ... def WhichOneof( diff --git a/temporalio/bridge/proto/workflow_activation/workflow_activation_pb2.py b/temporalio/bridge/proto/workflow_activation/workflow_activation_pb2.py index cadb177da..dbece6b3e 100644 --- a/temporalio/bridge/proto/workflow_activation/workflow_activation_pb2.py +++ b/temporalio/bridge/proto/workflow_activation/workflow_activation_pb2.py @@ -44,7 +44,7 @@ ) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n?temporal/sdk/core/workflow_activation/workflow_activation.proto\x12\x1b\x63oresdk.workflow_activation\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a%temporal/api/failure/v1/message.proto\x1a$temporal/api/update/v1/message.proto\x1a$temporal/api/common/v1/message.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a\x37temporal/sdk/core/activity_result/activity_result.proto\x1a\x35temporal/sdk/core/child_workflow/child_workflow.proto\x1a%temporal/sdk/core/common/common.proto\x1a#temporal/sdk/core/nexus/nexus.proto"\xfa\x02\n\x12WorkflowActivation\x12\x0e\n\x06run_id\x18\x01 \x01(\t\x12-\n\ttimestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x14\n\x0cis_replaying\x18\x03 \x01(\x08\x12\x16\n\x0ehistory_length\x18\x04 \x01(\r\x12@\n\x04jobs\x18\x05 \x03(\x0b\x32\x32.coresdk.workflow_activation.WorkflowActivationJob\x12 \n\x18\x61vailable_internal_flags\x18\x06 \x03(\r\x12\x1a\n\x12history_size_bytes\x18\x07 \x01(\x04\x12!\n\x19\x63ontinue_as_new_suggested\x18\x08 \x01(\x08\x12T\n#deployment_version_for_current_task\x18\t \x01(\x0b\x32\'.coresdk.common.WorkerDeploymentVersion"\xe0\n\n\x15WorkflowActivationJob\x12N\n\x13initialize_workflow\x18\x01 \x01(\x0b\x32/.coresdk.workflow_activation.InitializeWorkflowH\x00\x12<\n\nfire_timer\x18\x02 \x01(\x0b\x32&.coresdk.workflow_activation.FireTimerH\x00\x12K\n\x12update_random_seed\x18\x04 \x01(\x0b\x32-.coresdk.workflow_activation.UpdateRandomSeedH\x00\x12\x44\n\x0equery_workflow\x18\x05 \x01(\x0b\x32*.coresdk.workflow_activation.QueryWorkflowH\x00\x12\x46\n\x0f\x63\x61ncel_workflow\x18\x06 \x01(\x0b\x32+.coresdk.workflow_activation.CancelWorkflowH\x00\x12\x46\n\x0fsignal_workflow\x18\x07 \x01(\x0b\x32+.coresdk.workflow_activation.SignalWorkflowH\x00\x12H\n\x10resolve_activity\x18\x08 \x01(\x0b\x32,.coresdk.workflow_activation.ResolveActivityH\x00\x12G\n\x10notify_has_patch\x18\t \x01(\x0b\x32+.coresdk.workflow_activation.NotifyHasPatchH\x00\x12q\n&resolve_child_workflow_execution_start\x18\n \x01(\x0b\x32?.coresdk.workflow_activation.ResolveChildWorkflowExecutionStartH\x00\x12\x66\n resolve_child_workflow_execution\x18\x0b \x01(\x0b\x32:.coresdk.workflow_activation.ResolveChildWorkflowExecutionH\x00\x12\x66\n resolve_signal_external_workflow\x18\x0c \x01(\x0b\x32:.coresdk.workflow_activation.ResolveSignalExternalWorkflowH\x00\x12u\n(resolve_request_cancel_external_workflow\x18\r \x01(\x0b\x32\x41.coresdk.workflow_activation.ResolveRequestCancelExternalWorkflowH\x00\x12:\n\tdo_update\x18\x0e \x01(\x0b\x32%.coresdk.workflow_activation.DoUpdateH\x00\x12`\n\x1dresolve_nexus_operation_start\x18\x0f \x01(\x0b\x32\x37.coresdk.workflow_activation.ResolveNexusOperationStartH\x00\x12U\n\x17resolve_nexus_operation\x18\x10 \x01(\x0b\x32\x32.coresdk.workflow_activation.ResolveNexusOperationH\x00\x12I\n\x11remove_from_cache\x18\x32 \x01(\x0b\x32,.coresdk.workflow_activation.RemoveFromCacheH\x00\x42\t\n\x07variant"\xd9\n\n\x12InitializeWorkflow\x12\x15\n\rworkflow_type\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x32\n\targuments\x18\x03 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x17\n\x0frandomness_seed\x18\x04 \x01(\x04\x12M\n\x07headers\x18\x05 \x03(\x0b\x32<.coresdk.workflow_activation.InitializeWorkflow.HeadersEntry\x12\x10\n\x08identity\x18\x06 \x01(\t\x12I\n\x14parent_workflow_info\x18\x07 \x01(\x0b\x32+.coresdk.common.NamespacedWorkflowExecution\x12=\n\x1aworkflow_execution_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x37\n\x14workflow_run_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.Duration\x12\'\n\x1f\x63ontinued_from_execution_run_id\x18\x0b \x01(\t\x12J\n\x13\x63ontinued_initiator\x18\x0c \x01(\x0e\x32-.temporal.api.enums.v1.ContinueAsNewInitiator\x12;\n\x11\x63ontinued_failure\x18\r \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12@\n\x16last_completion_result\x18\x0e \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x1e\n\x16\x66irst_execution_run_id\x18\x0f \x01(\t\x12\x39\n\x0cretry_policy\x18\x10 \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x0f\n\x07\x61ttempt\x18\x11 \x01(\x05\x12\x15\n\rcron_schedule\x18\x12 \x01(\t\x12\x46\n"workflow_execution_expiration_time\x18\x13 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x45\n"cron_schedule_to_schedule_interval\x18\x14 \x01(\x0b\x32\x19.google.protobuf.Duration\x12*\n\x04memo\x18\x15 \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo\x12\x43\n\x11search_attributes\x18\x16 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12.\n\nstart_time\x18\x17 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12@\n\rroot_workflow\x18\x18 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x32\n\x08priority\x18\x19 \x01(\x0b\x32 .temporal.api.common.v1.Priority\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\x18\n\tFireTimer\x12\x0b\n\x03seq\x18\x01 \x01(\r"m\n\x0fResolveActivity\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12;\n\x06result\x18\x02 \x01(\x0b\x32+.coresdk.activity_result.ActivityResolution\x12\x10\n\x08is_local\x18\x03 \x01(\x08"\xd1\x02\n"ResolveChildWorkflowExecutionStart\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12[\n\tsucceeded\x18\x02 \x01(\x0b\x32\x46.coresdk.workflow_activation.ResolveChildWorkflowExecutionStartSuccessH\x00\x12X\n\x06\x66\x61iled\x18\x03 \x01(\x0b\x32\x46.coresdk.workflow_activation.ResolveChildWorkflowExecutionStartFailureH\x00\x12]\n\tcancelled\x18\x04 \x01(\x0b\x32H.coresdk.workflow_activation.ResolveChildWorkflowExecutionStartCancelledH\x00\x42\x08\n\x06status";\n)ResolveChildWorkflowExecutionStartSuccess\x12\x0e\n\x06run_id\x18\x01 \x01(\t"\xa6\x01\n)ResolveChildWorkflowExecutionStartFailure\x12\x13\n\x0bworkflow_id\x18\x01 \x01(\t\x12\x15\n\rworkflow_type\x18\x02 \x01(\t\x12M\n\x05\x63\x61use\x18\x03 \x01(\x0e\x32>.coresdk.child_workflow.StartChildWorkflowExecutionFailedCause"`\n+ResolveChildWorkflowExecutionStartCancelled\x12\x31\n\x07\x66\x61ilure\x18\x01 \x01(\x0b\x32 .temporal.api.failure.v1.Failure"i\n\x1dResolveChildWorkflowExecution\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12;\n\x06result\x18\x02 \x01(\x0b\x32+.coresdk.child_workflow.ChildWorkflowResult"+\n\x10UpdateRandomSeed\x12\x17\n\x0frandomness_seed\x18\x01 \x01(\x04"\x84\x02\n\rQueryWorkflow\x12\x10\n\x08query_id\x18\x01 \x01(\t\x12\x12\n\nquery_type\x18\x02 \x01(\t\x12\x32\n\targuments\x18\x03 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12H\n\x07headers\x18\x05 \x03(\x0b\x32\x37.coresdk.workflow_activation.QueryWorkflow.HeadersEntry\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01" \n\x0e\x43\x61ncelWorkflow\x12\x0e\n\x06reason\x18\x01 \x01(\t"\x83\x02\n\x0eSignalWorkflow\x12\x13\n\x0bsignal_name\x18\x01 \x01(\t\x12.\n\x05input\x18\x02 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x10\n\x08identity\x18\x03 \x01(\t\x12I\n\x07headers\x18\x05 \x03(\x0b\x32\x38.coresdk.workflow_activation.SignalWorkflow.HeadersEntry\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01""\n\x0eNotifyHasPatch\x12\x10\n\x08patch_id\x18\x01 \x01(\t"_\n\x1dResolveSignalExternalWorkflow\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x31\n\x07\x66\x61ilure\x18\x02 \x01(\x0b\x32 .temporal.api.failure.v1.Failure"f\n$ResolveRequestCancelExternalWorkflow\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x31\n\x07\x66\x61ilure\x18\x02 \x01(\x0b\x32 .temporal.api.failure.v1.Failure"\xcb\x02\n\x08\x44oUpdate\x12\n\n\x02id\x18\x01 \x01(\t\x12\x1c\n\x14protocol_instance_id\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12.\n\x05input\x18\x04 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x43\n\x07headers\x18\x05 \x03(\x0b\x32\x32.coresdk.workflow_activation.DoUpdate.HeadersEntry\x12*\n\x04meta\x18\x06 \x01(\x0b\x32\x1c.temporal.api.update.v1.Meta\x12\x15\n\rrun_validator\x18\x07 \x01(\x08\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\x9a\x01\n\x1aResolveNexusOperationStart\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x19\n\x0foperation_token\x18\x02 \x01(\tH\x00\x12\x16\n\x0cstarted_sync\x18\x03 \x01(\x08H\x00\x12\x32\n\x06\x66\x61iled\x18\x04 \x01(\x0b\x32 .temporal.api.failure.v1.FailureH\x00\x42\x08\n\x06status"Y\n\x15ResolveNexusOperation\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x33\n\x06result\x18\x02 \x01(\x0b\x32#.coresdk.nexus.NexusOperationResult"\xe0\x02\n\x0fRemoveFromCache\x12\x0f\n\x07message\x18\x01 \x01(\t\x12K\n\x06reason\x18\x02 \x01(\x0e\x32;.coresdk.workflow_activation.RemoveFromCache.EvictionReason"\xee\x01\n\x0e\x45victionReason\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x0e\n\nCACHE_FULL\x10\x01\x12\x0e\n\nCACHE_MISS\x10\x02\x12\x12\n\x0eNONDETERMINISM\x10\x03\x12\r\n\tLANG_FAIL\x10\x04\x12\x12\n\x0eLANG_REQUESTED\x10\x05\x12\x12\n\x0eTASK_NOT_FOUND\x10\x06\x12\x15\n\x11UNHANDLED_COMMAND\x10\x07\x12\t\n\x05\x46\x41TAL\x10\x08\x12\x1f\n\x1bPAGINATION_OR_HISTORY_FETCH\x10\t\x12\x1d\n\x19WORKFLOW_EXECUTION_ENDING\x10\nB8\xea\x02\x35Temporalio::Internal::Bridge::Api::WorkflowActivationb\x06proto3' + b'\n?temporal/sdk/core/workflow_activation/workflow_activation.proto\x12\x1b\x63oresdk.workflow_activation\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a%temporal/api/failure/v1/message.proto\x1a$temporal/api/update/v1/message.proto\x1a$temporal/api/common/v1/message.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a\x37temporal/sdk/core/activity_result/activity_result.proto\x1a\x35temporal/sdk/core/child_workflow/child_workflow.proto\x1a%temporal/sdk/core/common/common.proto\x1a#temporal/sdk/core/nexus/nexus.proto"\xa2\x04\n\x12WorkflowActivation\x12\x0e\n\x06run_id\x18\x01 \x01(\t\x12-\n\ttimestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x14\n\x0cis_replaying\x18\x03 \x01(\x08\x12\x16\n\x0ehistory_length\x18\x04 \x01(\r\x12@\n\x04jobs\x18\x05 \x03(\x0b\x32\x32.coresdk.workflow_activation.WorkflowActivationJob\x12 \n\x18\x61vailable_internal_flags\x18\x06 \x03(\r\x12\x1a\n\x12history_size_bytes\x18\x07 \x01(\x04\x12!\n\x19\x63ontinue_as_new_suggested\x18\x08 \x01(\x08\x12T\n#deployment_version_for_current_task\x18\t \x01(\x0b\x32\'.coresdk.common.WorkerDeploymentVersion\x12\x18\n\x10last_sdk_version\x18\n \x01(\t\x12Z\n\x1fsuggest_continue_as_new_reasons\x18\x0b \x03(\x0e\x32\x31.temporal.api.enums.v1.SuggestContinueAsNewReason\x12\x30\n(target_worker_deployment_version_changed\x18\x0c \x01(\x08"\xe0\n\n\x15WorkflowActivationJob\x12N\n\x13initialize_workflow\x18\x01 \x01(\x0b\x32/.coresdk.workflow_activation.InitializeWorkflowH\x00\x12<\n\nfire_timer\x18\x02 \x01(\x0b\x32&.coresdk.workflow_activation.FireTimerH\x00\x12K\n\x12update_random_seed\x18\x04 \x01(\x0b\x32-.coresdk.workflow_activation.UpdateRandomSeedH\x00\x12\x44\n\x0equery_workflow\x18\x05 \x01(\x0b\x32*.coresdk.workflow_activation.QueryWorkflowH\x00\x12\x46\n\x0f\x63\x61ncel_workflow\x18\x06 \x01(\x0b\x32+.coresdk.workflow_activation.CancelWorkflowH\x00\x12\x46\n\x0fsignal_workflow\x18\x07 \x01(\x0b\x32+.coresdk.workflow_activation.SignalWorkflowH\x00\x12H\n\x10resolve_activity\x18\x08 \x01(\x0b\x32,.coresdk.workflow_activation.ResolveActivityH\x00\x12G\n\x10notify_has_patch\x18\t \x01(\x0b\x32+.coresdk.workflow_activation.NotifyHasPatchH\x00\x12q\n&resolve_child_workflow_execution_start\x18\n \x01(\x0b\x32?.coresdk.workflow_activation.ResolveChildWorkflowExecutionStartH\x00\x12\x66\n resolve_child_workflow_execution\x18\x0b \x01(\x0b\x32:.coresdk.workflow_activation.ResolveChildWorkflowExecutionH\x00\x12\x66\n resolve_signal_external_workflow\x18\x0c \x01(\x0b\x32:.coresdk.workflow_activation.ResolveSignalExternalWorkflowH\x00\x12u\n(resolve_request_cancel_external_workflow\x18\r \x01(\x0b\x32\x41.coresdk.workflow_activation.ResolveRequestCancelExternalWorkflowH\x00\x12:\n\tdo_update\x18\x0e \x01(\x0b\x32%.coresdk.workflow_activation.DoUpdateH\x00\x12`\n\x1dresolve_nexus_operation_start\x18\x0f \x01(\x0b\x32\x37.coresdk.workflow_activation.ResolveNexusOperationStartH\x00\x12U\n\x17resolve_nexus_operation\x18\x10 \x01(\x0b\x32\x32.coresdk.workflow_activation.ResolveNexusOperationH\x00\x12I\n\x11remove_from_cache\x18\x32 \x01(\x0b\x32,.coresdk.workflow_activation.RemoveFromCacheH\x00\x42\t\n\x07variant"\xd9\n\n\x12InitializeWorkflow\x12\x15\n\rworkflow_type\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x32\n\targuments\x18\x03 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x17\n\x0frandomness_seed\x18\x04 \x01(\x04\x12M\n\x07headers\x18\x05 \x03(\x0b\x32<.coresdk.workflow_activation.InitializeWorkflow.HeadersEntry\x12\x10\n\x08identity\x18\x06 \x01(\t\x12I\n\x14parent_workflow_info\x18\x07 \x01(\x0b\x32+.coresdk.common.NamespacedWorkflowExecution\x12=\n\x1aworkflow_execution_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x37\n\x14workflow_run_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.Duration\x12\'\n\x1f\x63ontinued_from_execution_run_id\x18\x0b \x01(\t\x12J\n\x13\x63ontinued_initiator\x18\x0c \x01(\x0e\x32-.temporal.api.enums.v1.ContinueAsNewInitiator\x12;\n\x11\x63ontinued_failure\x18\r \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12@\n\x16last_completion_result\x18\x0e \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x1e\n\x16\x66irst_execution_run_id\x18\x0f \x01(\t\x12\x39\n\x0cretry_policy\x18\x10 \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x0f\n\x07\x61ttempt\x18\x11 \x01(\x05\x12\x15\n\rcron_schedule\x18\x12 \x01(\t\x12\x46\n"workflow_execution_expiration_time\x18\x13 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x45\n"cron_schedule_to_schedule_interval\x18\x14 \x01(\x0b\x32\x19.google.protobuf.Duration\x12*\n\x04memo\x18\x15 \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo\x12\x43\n\x11search_attributes\x18\x16 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12.\n\nstart_time\x18\x17 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12@\n\rroot_workflow\x18\x18 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x32\n\x08priority\x18\x19 \x01(\x0b\x32 .temporal.api.common.v1.Priority\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\x18\n\tFireTimer\x12\x0b\n\x03seq\x18\x01 \x01(\r"m\n\x0fResolveActivity\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12;\n\x06result\x18\x02 \x01(\x0b\x32+.coresdk.activity_result.ActivityResolution\x12\x10\n\x08is_local\x18\x03 \x01(\x08"\xd1\x02\n"ResolveChildWorkflowExecutionStart\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12[\n\tsucceeded\x18\x02 \x01(\x0b\x32\x46.coresdk.workflow_activation.ResolveChildWorkflowExecutionStartSuccessH\x00\x12X\n\x06\x66\x61iled\x18\x03 \x01(\x0b\x32\x46.coresdk.workflow_activation.ResolveChildWorkflowExecutionStartFailureH\x00\x12]\n\tcancelled\x18\x04 \x01(\x0b\x32H.coresdk.workflow_activation.ResolveChildWorkflowExecutionStartCancelledH\x00\x42\x08\n\x06status";\n)ResolveChildWorkflowExecutionStartSuccess\x12\x0e\n\x06run_id\x18\x01 \x01(\t"\xa6\x01\n)ResolveChildWorkflowExecutionStartFailure\x12\x13\n\x0bworkflow_id\x18\x01 \x01(\t\x12\x15\n\rworkflow_type\x18\x02 \x01(\t\x12M\n\x05\x63\x61use\x18\x03 \x01(\x0e\x32>.coresdk.child_workflow.StartChildWorkflowExecutionFailedCause"`\n+ResolveChildWorkflowExecutionStartCancelled\x12\x31\n\x07\x66\x61ilure\x18\x01 \x01(\x0b\x32 .temporal.api.failure.v1.Failure"i\n\x1dResolveChildWorkflowExecution\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12;\n\x06result\x18\x02 \x01(\x0b\x32+.coresdk.child_workflow.ChildWorkflowResult"+\n\x10UpdateRandomSeed\x12\x17\n\x0frandomness_seed\x18\x01 \x01(\x04"\x84\x02\n\rQueryWorkflow\x12\x10\n\x08query_id\x18\x01 \x01(\t\x12\x12\n\nquery_type\x18\x02 \x01(\t\x12\x32\n\targuments\x18\x03 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12H\n\x07headers\x18\x05 \x03(\x0b\x32\x37.coresdk.workflow_activation.QueryWorkflow.HeadersEntry\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01" \n\x0e\x43\x61ncelWorkflow\x12\x0e\n\x06reason\x18\x01 \x01(\t"\x83\x02\n\x0eSignalWorkflow\x12\x13\n\x0bsignal_name\x18\x01 \x01(\t\x12.\n\x05input\x18\x02 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x10\n\x08identity\x18\x03 \x01(\t\x12I\n\x07headers\x18\x05 \x03(\x0b\x32\x38.coresdk.workflow_activation.SignalWorkflow.HeadersEntry\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01""\n\x0eNotifyHasPatch\x12\x10\n\x08patch_id\x18\x01 \x01(\t"_\n\x1dResolveSignalExternalWorkflow\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x31\n\x07\x66\x61ilure\x18\x02 \x01(\x0b\x32 .temporal.api.failure.v1.Failure"f\n$ResolveRequestCancelExternalWorkflow\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x31\n\x07\x66\x61ilure\x18\x02 \x01(\x0b\x32 .temporal.api.failure.v1.Failure"\xcb\x02\n\x08\x44oUpdate\x12\n\n\x02id\x18\x01 \x01(\t\x12\x1c\n\x14protocol_instance_id\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12.\n\x05input\x18\x04 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x43\n\x07headers\x18\x05 \x03(\x0b\x32\x32.coresdk.workflow_activation.DoUpdate.HeadersEntry\x12*\n\x04meta\x18\x06 \x01(\x0b\x32\x1c.temporal.api.update.v1.Meta\x12\x15\n\rrun_validator\x18\x07 \x01(\x08\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\x9a\x01\n\x1aResolveNexusOperationStart\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x19\n\x0foperation_token\x18\x02 \x01(\tH\x00\x12\x16\n\x0cstarted_sync\x18\x03 \x01(\x08H\x00\x12\x32\n\x06\x66\x61iled\x18\x04 \x01(\x0b\x32 .temporal.api.failure.v1.FailureH\x00\x42\x08\n\x06status"Y\n\x15ResolveNexusOperation\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x33\n\x06result\x18\x02 \x01(\x0b\x32#.coresdk.nexus.NexusOperationResult"\xe0\x02\n\x0fRemoveFromCache\x12\x0f\n\x07message\x18\x01 \x01(\t\x12K\n\x06reason\x18\x02 \x01(\x0e\x32;.coresdk.workflow_activation.RemoveFromCache.EvictionReason"\xee\x01\n\x0e\x45victionReason\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x0e\n\nCACHE_FULL\x10\x01\x12\x0e\n\nCACHE_MISS\x10\x02\x12\x12\n\x0eNONDETERMINISM\x10\x03\x12\r\n\tLANG_FAIL\x10\x04\x12\x12\n\x0eLANG_REQUESTED\x10\x05\x12\x12\n\x0eTASK_NOT_FOUND\x10\x06\x12\x15\n\x11UNHANDLED_COMMAND\x10\x07\x12\t\n\x05\x46\x41TAL\x10\x08\x12\x1f\n\x1bPAGINATION_OR_HISTORY_FETCH\x10\t\x12\x1d\n\x19WORKFLOW_EXECUTION_ENDING\x10\nB8\xea\x02\x35Temporalio::Internal::Bridge::Api::WorkflowActivationb\x06proto3' ) @@ -377,55 +377,55 @@ _DOUPDATE_HEADERSENTRY._options = None _DOUPDATE_HEADERSENTRY._serialized_options = b"8\001" _WORKFLOWACTIVATION._serialized_start = 532 - _WORKFLOWACTIVATION._serialized_end = 910 - _WORKFLOWACTIVATIONJOB._serialized_start = 913 - _WORKFLOWACTIVATIONJOB._serialized_end = 2289 - _INITIALIZEWORKFLOW._serialized_start = 2292 - _INITIALIZEWORKFLOW._serialized_end = 3661 - _INITIALIZEWORKFLOW_HEADERSENTRY._serialized_start = 3582 - _INITIALIZEWORKFLOW_HEADERSENTRY._serialized_end = 3661 - _FIRETIMER._serialized_start = 3663 - _FIRETIMER._serialized_end = 3687 - _RESOLVEACTIVITY._serialized_start = 3689 - _RESOLVEACTIVITY._serialized_end = 3798 - _RESOLVECHILDWORKFLOWEXECUTIONSTART._serialized_start = 3801 - _RESOLVECHILDWORKFLOWEXECUTIONSTART._serialized_end = 4138 - _RESOLVECHILDWORKFLOWEXECUTIONSTARTSUCCESS._serialized_start = 4140 - _RESOLVECHILDWORKFLOWEXECUTIONSTARTSUCCESS._serialized_end = 4199 - _RESOLVECHILDWORKFLOWEXECUTIONSTARTFAILURE._serialized_start = 4202 - _RESOLVECHILDWORKFLOWEXECUTIONSTARTFAILURE._serialized_end = 4368 - _RESOLVECHILDWORKFLOWEXECUTIONSTARTCANCELLED._serialized_start = 4370 - _RESOLVECHILDWORKFLOWEXECUTIONSTARTCANCELLED._serialized_end = 4466 - _RESOLVECHILDWORKFLOWEXECUTION._serialized_start = 4468 - _RESOLVECHILDWORKFLOWEXECUTION._serialized_end = 4573 - _UPDATERANDOMSEED._serialized_start = 4575 - _UPDATERANDOMSEED._serialized_end = 4618 - _QUERYWORKFLOW._serialized_start = 4621 - _QUERYWORKFLOW._serialized_end = 4881 - _QUERYWORKFLOW_HEADERSENTRY._serialized_start = 3582 - _QUERYWORKFLOW_HEADERSENTRY._serialized_end = 3661 - _CANCELWORKFLOW._serialized_start = 4883 - _CANCELWORKFLOW._serialized_end = 4915 - _SIGNALWORKFLOW._serialized_start = 4918 - _SIGNALWORKFLOW._serialized_end = 5177 - _SIGNALWORKFLOW_HEADERSENTRY._serialized_start = 3582 - _SIGNALWORKFLOW_HEADERSENTRY._serialized_end = 3661 - _NOTIFYHASPATCH._serialized_start = 5179 - _NOTIFYHASPATCH._serialized_end = 5213 - _RESOLVESIGNALEXTERNALWORKFLOW._serialized_start = 5215 - _RESOLVESIGNALEXTERNALWORKFLOW._serialized_end = 5310 - _RESOLVEREQUESTCANCELEXTERNALWORKFLOW._serialized_start = 5312 - _RESOLVEREQUESTCANCELEXTERNALWORKFLOW._serialized_end = 5414 - _DOUPDATE._serialized_start = 5417 - _DOUPDATE._serialized_end = 5748 - _DOUPDATE_HEADERSENTRY._serialized_start = 3582 - _DOUPDATE_HEADERSENTRY._serialized_end = 3661 - _RESOLVENEXUSOPERATIONSTART._serialized_start = 5751 - _RESOLVENEXUSOPERATIONSTART._serialized_end = 5905 - _RESOLVENEXUSOPERATION._serialized_start = 5907 - _RESOLVENEXUSOPERATION._serialized_end = 5996 - _REMOVEFROMCACHE._serialized_start = 5999 - _REMOVEFROMCACHE._serialized_end = 6351 - _REMOVEFROMCACHE_EVICTIONREASON._serialized_start = 6113 - _REMOVEFROMCACHE_EVICTIONREASON._serialized_end = 6351 + _WORKFLOWACTIVATION._serialized_end = 1078 + _WORKFLOWACTIVATIONJOB._serialized_start = 1081 + _WORKFLOWACTIVATIONJOB._serialized_end = 2457 + _INITIALIZEWORKFLOW._serialized_start = 2460 + _INITIALIZEWORKFLOW._serialized_end = 3829 + _INITIALIZEWORKFLOW_HEADERSENTRY._serialized_start = 3750 + _INITIALIZEWORKFLOW_HEADERSENTRY._serialized_end = 3829 + _FIRETIMER._serialized_start = 3831 + _FIRETIMER._serialized_end = 3855 + _RESOLVEACTIVITY._serialized_start = 3857 + _RESOLVEACTIVITY._serialized_end = 3966 + _RESOLVECHILDWORKFLOWEXECUTIONSTART._serialized_start = 3969 + _RESOLVECHILDWORKFLOWEXECUTIONSTART._serialized_end = 4306 + _RESOLVECHILDWORKFLOWEXECUTIONSTARTSUCCESS._serialized_start = 4308 + _RESOLVECHILDWORKFLOWEXECUTIONSTARTSUCCESS._serialized_end = 4367 + _RESOLVECHILDWORKFLOWEXECUTIONSTARTFAILURE._serialized_start = 4370 + _RESOLVECHILDWORKFLOWEXECUTIONSTARTFAILURE._serialized_end = 4536 + _RESOLVECHILDWORKFLOWEXECUTIONSTARTCANCELLED._serialized_start = 4538 + _RESOLVECHILDWORKFLOWEXECUTIONSTARTCANCELLED._serialized_end = 4634 + _RESOLVECHILDWORKFLOWEXECUTION._serialized_start = 4636 + _RESOLVECHILDWORKFLOWEXECUTION._serialized_end = 4741 + _UPDATERANDOMSEED._serialized_start = 4743 + _UPDATERANDOMSEED._serialized_end = 4786 + _QUERYWORKFLOW._serialized_start = 4789 + _QUERYWORKFLOW._serialized_end = 5049 + _QUERYWORKFLOW_HEADERSENTRY._serialized_start = 3750 + _QUERYWORKFLOW_HEADERSENTRY._serialized_end = 3829 + _CANCELWORKFLOW._serialized_start = 5051 + _CANCELWORKFLOW._serialized_end = 5083 + _SIGNALWORKFLOW._serialized_start = 5086 + _SIGNALWORKFLOW._serialized_end = 5345 + _SIGNALWORKFLOW_HEADERSENTRY._serialized_start = 3750 + _SIGNALWORKFLOW_HEADERSENTRY._serialized_end = 3829 + _NOTIFYHASPATCH._serialized_start = 5347 + _NOTIFYHASPATCH._serialized_end = 5381 + _RESOLVESIGNALEXTERNALWORKFLOW._serialized_start = 5383 + _RESOLVESIGNALEXTERNALWORKFLOW._serialized_end = 5478 + _RESOLVEREQUESTCANCELEXTERNALWORKFLOW._serialized_start = 5480 + _RESOLVEREQUESTCANCELEXTERNALWORKFLOW._serialized_end = 5582 + _DOUPDATE._serialized_start = 5585 + _DOUPDATE._serialized_end = 5916 + _DOUPDATE_HEADERSENTRY._serialized_start = 3750 + _DOUPDATE_HEADERSENTRY._serialized_end = 3829 + _RESOLVENEXUSOPERATIONSTART._serialized_start = 5919 + _RESOLVENEXUSOPERATIONSTART._serialized_end = 6073 + _RESOLVENEXUSOPERATION._serialized_start = 6075 + _RESOLVENEXUSOPERATION._serialized_end = 6164 + _REMOVEFROMCACHE._serialized_start = 6167 + _REMOVEFROMCACHE._serialized_end = 6519 + _REMOVEFROMCACHE_EVICTIONREASON._serialized_start = 6281 + _REMOVEFROMCACHE_EVICTIONREASON._serialized_end = 6519 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/bridge/proto/workflow_activation/workflow_activation_pb2.pyi b/temporalio/bridge/proto/workflow_activation/workflow_activation_pb2.pyi index 497382b74..a4162e7a4 100644 --- a/temporalio/bridge/proto/workflow_activation/workflow_activation_pb2.pyi +++ b/temporalio/bridge/proto/workflow_activation/workflow_activation_pb2.pyi @@ -92,6 +92,9 @@ class WorkflowActivation(google.protobuf.message.Message): HISTORY_SIZE_BYTES_FIELD_NUMBER: builtins.int CONTINUE_AS_NEW_SUGGESTED_FIELD_NUMBER: builtins.int DEPLOYMENT_VERSION_FOR_CURRENT_TASK_FIELD_NUMBER: builtins.int + LAST_SDK_VERSION_FIELD_NUMBER: builtins.int + SUGGEST_CONTINUE_AS_NEW_REASONS_FIELD_NUMBER: builtins.int + TARGET_WORKER_DEPLOYMENT_VERSION_CHANGED_FIELD_NUMBER: builtins.int run_id: builtins.str """The id of the currently active run of the workflow. Also used as a cache key. There may only ever be one active workflow task (and hence activation) of a run at one time. @@ -136,6 +139,23 @@ class WorkflowActivation(google.protobuf.message.Message): build id, if this worker was using the deprecated Build ID-only feature(s). """ + last_sdk_version: builtins.str + """The last seen SDK version from the most recent WFT completed event""" + @property + def suggest_continue_as_new_reasons( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[ + temporalio.api.enums.v1.workflow_pb2.SuggestContinueAsNewReason.ValueType + ]: + """Experimental. Optionally decide the versioning behavior that the first task of the new run should use. + For example, choose to AutoUpgrade on continue-as-new instead of inheriting the pinned version + of the previous run. + """ + target_worker_deployment_version_changed: builtins.bool + """True if Workflow's Target Worker Deployment Version is different from its Pinned Version and + the workflow is Pinned. + Experimental. + """ def __init__( self, *, @@ -149,6 +169,12 @@ class WorkflowActivation(google.protobuf.message.Message): continue_as_new_suggested: builtins.bool = ..., deployment_version_for_current_task: temporalio.bridge.proto.common.common_pb2.WorkerDeploymentVersion | None = ..., + last_sdk_version: builtins.str = ..., + suggest_continue_as_new_reasons: collections.abc.Iterable[ + temporalio.api.enums.v1.workflow_pb2.SuggestContinueAsNewReason.ValueType + ] + | None = ..., + target_worker_deployment_version_changed: builtins.bool = ..., ) -> None: ... def HasField( self, @@ -176,8 +202,14 @@ class WorkflowActivation(google.protobuf.message.Message): b"is_replaying", "jobs", b"jobs", + "last_sdk_version", + b"last_sdk_version", "run_id", b"run_id", + "suggest_continue_as_new_reasons", + b"suggest_continue_as_new_reasons", + "target_worker_deployment_version_changed", + b"target_worker_deployment_version_changed", "timestamp", b"timestamp", ], diff --git a/temporalio/bridge/proto/workflow_commands/workflow_commands_pb2.py b/temporalio/bridge/proto/workflow_commands/workflow_commands_pb2.py index 5181a3801..a82430744 100644 --- a/temporalio/bridge/proto/workflow_commands/workflow_commands_pb2.py +++ b/temporalio/bridge/proto/workflow_commands/workflow_commands_pb2.py @@ -42,7 +42,7 @@ ) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n;temporal/sdk/core/workflow_commands/workflow_commands.proto\x12\x19\x63oresdk.workflow_commands\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a$temporal/api/common/v1/message.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a%temporal/api/failure/v1/message.proto\x1a\'temporal/api/sdk/v1/user_metadata.proto\x1a\x35temporal/sdk/core/child_workflow/child_workflow.proto\x1a#temporal/sdk/core/nexus/nexus.proto\x1a%temporal/sdk/core/common/common.proto"\xe5\x0f\n\x0fWorkflowCommand\x12\x38\n\ruser_metadata\x18\x64 \x01(\x0b\x32!.temporal.api.sdk.v1.UserMetadata\x12<\n\x0bstart_timer\x18\x01 \x01(\x0b\x32%.coresdk.workflow_commands.StartTimerH\x00\x12H\n\x11schedule_activity\x18\x02 \x01(\x0b\x32+.coresdk.workflow_commands.ScheduleActivityH\x00\x12\x42\n\x10respond_to_query\x18\x03 \x01(\x0b\x32&.coresdk.workflow_commands.QueryResultH\x00\x12S\n\x17request_cancel_activity\x18\x04 \x01(\x0b\x32\x30.coresdk.workflow_commands.RequestCancelActivityH\x00\x12>\n\x0c\x63\x61ncel_timer\x18\x05 \x01(\x0b\x32&.coresdk.workflow_commands.CancelTimerH\x00\x12[\n\x1b\x63omplete_workflow_execution\x18\x06 \x01(\x0b\x32\x34.coresdk.workflow_commands.CompleteWorkflowExecutionH\x00\x12S\n\x17\x66\x61il_workflow_execution\x18\x07 \x01(\x0b\x32\x30.coresdk.workflow_commands.FailWorkflowExecutionH\x00\x12g\n"continue_as_new_workflow_execution\x18\x08 \x01(\x0b\x32\x39.coresdk.workflow_commands.ContinueAsNewWorkflowExecutionH\x00\x12W\n\x19\x63\x61ncel_workflow_execution\x18\t \x01(\x0b\x32\x32.coresdk.workflow_commands.CancelWorkflowExecutionH\x00\x12\x45\n\x10set_patch_marker\x18\n \x01(\x0b\x32).coresdk.workflow_commands.SetPatchMarkerH\x00\x12`\n\x1estart_child_workflow_execution\x18\x0b \x01(\x0b\x32\x36.coresdk.workflow_commands.StartChildWorkflowExecutionH\x00\x12\x62\n\x1f\x63\x61ncel_child_workflow_execution\x18\x0c \x01(\x0b\x32\x37.coresdk.workflow_commands.CancelChildWorkflowExecutionH\x00\x12w\n*request_cancel_external_workflow_execution\x18\r \x01(\x0b\x32\x41.coresdk.workflow_commands.RequestCancelExternalWorkflowExecutionH\x00\x12h\n"signal_external_workflow_execution\x18\x0e \x01(\x0b\x32:.coresdk.workflow_commands.SignalExternalWorkflowExecutionH\x00\x12Q\n\x16\x63\x61ncel_signal_workflow\x18\x0f \x01(\x0b\x32/.coresdk.workflow_commands.CancelSignalWorkflowH\x00\x12S\n\x17schedule_local_activity\x18\x10 \x01(\x0b\x32\x30.coresdk.workflow_commands.ScheduleLocalActivityH\x00\x12^\n\x1drequest_cancel_local_activity\x18\x11 \x01(\x0b\x32\x35.coresdk.workflow_commands.RequestCancelLocalActivityH\x00\x12\x66\n!upsert_workflow_search_attributes\x18\x12 \x01(\x0b\x32\x39.coresdk.workflow_commands.UpsertWorkflowSearchAttributesH\x00\x12Y\n\x1amodify_workflow_properties\x18\x13 \x01(\x0b\x32\x33.coresdk.workflow_commands.ModifyWorkflowPropertiesH\x00\x12\x44\n\x0fupdate_response\x18\x14 \x01(\x0b\x32).coresdk.workflow_commands.UpdateResponseH\x00\x12U\n\x18schedule_nexus_operation\x18\x15 \x01(\x0b\x32\x31.coresdk.workflow_commands.ScheduleNexusOperationH\x00\x12`\n\x1erequest_cancel_nexus_operation\x18\x16 \x01(\x0b\x32\x36.coresdk.workflow_commands.RequestCancelNexusOperationH\x00\x42\t\n\x07variant"S\n\nStartTimer\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x38\n\x15start_to_fire_timeout\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration"\x1a\n\x0b\x43\x61ncelTimer\x12\x0b\n\x03seq\x18\x01 \x01(\r"\xb8\x06\n\x10ScheduleActivity\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x13\n\x0b\x61\x63tivity_id\x18\x02 \x01(\t\x12\x15\n\ractivity_type\x18\x03 \x01(\t\x12\x12\n\ntask_queue\x18\x05 \x01(\t\x12I\n\x07headers\x18\x06 \x03(\x0b\x32\x38.coresdk.workflow_commands.ScheduleActivity.HeadersEntry\x12\x32\n\targuments\x18\x07 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12<\n\x19schedule_to_close_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12<\n\x19schedule_to_start_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x34\n\x11heartbeat_timeout\x18\x0b \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x0cretry_policy\x18\x0c \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12N\n\x11\x63\x61ncellation_type\x18\r \x01(\x0e\x32\x33.coresdk.workflow_commands.ActivityCancellationType\x12\x1e\n\x16\x64o_not_eagerly_execute\x18\x0e \x01(\x08\x12;\n\x11versioning_intent\x18\x0f \x01(\x0e\x32 .coresdk.common.VersioningIntent\x12\x32\n\x08priority\x18\x10 \x01(\x0b\x32 .temporal.api.common.v1.Priority\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\xee\x05\n\x15ScheduleLocalActivity\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x13\n\x0b\x61\x63tivity_id\x18\x02 \x01(\t\x12\x15\n\ractivity_type\x18\x03 \x01(\t\x12\x0f\n\x07\x61ttempt\x18\x04 \x01(\r\x12:\n\x16original_schedule_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12N\n\x07headers\x18\x06 \x03(\x0b\x32=.coresdk.workflow_commands.ScheduleLocalActivity.HeadersEntry\x12\x32\n\targuments\x18\x07 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12<\n\x19schedule_to_close_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12<\n\x19schedule_to_start_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x0cretry_policy\x18\x0b \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x38\n\x15local_retry_threshold\x18\x0c \x01(\x0b\x32\x19.google.protobuf.Duration\x12N\n\x11\x63\x61ncellation_type\x18\r \x01(\x0e\x32\x33.coresdk.workflow_commands.ActivityCancellationType\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"$\n\x15RequestCancelActivity\x12\x0b\n\x03seq\x18\x01 \x01(\r")\n\x1aRequestCancelLocalActivity\x12\x0b\n\x03seq\x18\x01 \x01(\r"\x9c\x01\n\x0bQueryResult\x12\x10\n\x08query_id\x18\x01 \x01(\t\x12<\n\tsucceeded\x18\x02 \x01(\x0b\x32\'.coresdk.workflow_commands.QuerySuccessH\x00\x12\x32\n\x06\x66\x61iled\x18\x03 \x01(\x0b\x32 .temporal.api.failure.v1.FailureH\x00\x42\t\n\x07variant"A\n\x0cQuerySuccess\x12\x31\n\x08response\x18\x01 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload"L\n\x19\x43ompleteWorkflowExecution\x12/\n\x06result\x18\x01 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload"J\n\x15\x46\x61ilWorkflowExecution\x12\x31\n\x07\x66\x61ilure\x18\x01 \x01(\x0b\x32 .temporal.api.failure.v1.Failure"\xfb\x06\n\x1e\x43ontinueAsNewWorkflowExecution\x12\x15\n\rworkflow_type\x18\x01 \x01(\t\x12\x12\n\ntask_queue\x18\x02 \x01(\t\x12\x32\n\targuments\x18\x03 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x37\n\x14workflow_run_timeout\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\x05 \x01(\x0b\x32\x19.google.protobuf.Duration\x12Q\n\x04memo\x18\x06 \x03(\x0b\x32\x43.coresdk.workflow_commands.ContinueAsNewWorkflowExecution.MemoEntry\x12W\n\x07headers\x18\x07 \x03(\x0b\x32\x46.coresdk.workflow_commands.ContinueAsNewWorkflowExecution.HeadersEntry\x12j\n\x11search_attributes\x18\x08 \x03(\x0b\x32O.coresdk.workflow_commands.ContinueAsNewWorkflowExecution.SearchAttributesEntry\x12\x39\n\x0cretry_policy\x18\t \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12;\n\x11versioning_intent\x18\n \x01(\x0e\x32 .coresdk.common.VersioningIntent\x1aL\n\tMemoEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01\x1aX\n\x15SearchAttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\x19\n\x17\x43\x61ncelWorkflowExecution"6\n\x0eSetPatchMarker\x12\x10\n\x08patch_id\x18\x01 \x01(\t\x12\x12\n\ndeprecated\x18\x02 \x01(\x08"\x94\n\n\x1bStartChildWorkflowExecution\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x11\n\tnamespace\x18\x02 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x03 \x01(\t\x12\x15\n\rworkflow_type\x18\x04 \x01(\t\x12\x12\n\ntask_queue\x18\x05 \x01(\t\x12.\n\x05input\x18\x06 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12=\n\x1aworkflow_execution_timeout\x18\x07 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x37\n\x14workflow_run_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x46\n\x13parent_close_policy\x18\n \x01(\x0e\x32).coresdk.child_workflow.ParentClosePolicy\x12N\n\x18workflow_id_reuse_policy\x18\x0c \x01(\x0e\x32,.temporal.api.enums.v1.WorkflowIdReusePolicy\x12\x39\n\x0cretry_policy\x18\r \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x15\n\rcron_schedule\x18\x0e \x01(\t\x12T\n\x07headers\x18\x0f \x03(\x0b\x32\x43.coresdk.workflow_commands.StartChildWorkflowExecution.HeadersEntry\x12N\n\x04memo\x18\x10 \x03(\x0b\x32@.coresdk.workflow_commands.StartChildWorkflowExecution.MemoEntry\x12g\n\x11search_attributes\x18\x11 \x03(\x0b\x32L.coresdk.workflow_commands.StartChildWorkflowExecution.SearchAttributesEntry\x12P\n\x11\x63\x61ncellation_type\x18\x12 \x01(\x0e\x32\x35.coresdk.child_workflow.ChildWorkflowCancellationType\x12;\n\x11versioning_intent\x18\x13 \x01(\x0e\x32 .coresdk.common.VersioningIntent\x12\x32\n\x08priority\x18\x14 \x01(\x0b\x32 .temporal.api.common.v1.Priority\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01\x1aL\n\tMemoEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01\x1aX\n\x15SearchAttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"J\n\x1c\x43\x61ncelChildWorkflowExecution\x12\x1a\n\x12\x63hild_workflow_seq\x18\x01 \x01(\r\x12\x0e\n\x06reason\x18\x02 \x01(\t"\x8e\x01\n&RequestCancelExternalWorkflowExecution\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12G\n\x12workflow_execution\x18\x02 \x01(\x0b\x32+.coresdk.common.NamespacedWorkflowExecution\x12\x0e\n\x06reason\x18\x03 \x01(\t"\x8f\x03\n\x1fSignalExternalWorkflowExecution\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12I\n\x12workflow_execution\x18\x02 \x01(\x0b\x32+.coresdk.common.NamespacedWorkflowExecutionH\x00\x12\x1b\n\x11\x63hild_workflow_id\x18\x03 \x01(\tH\x00\x12\x13\n\x0bsignal_name\x18\x04 \x01(\t\x12-\n\x04\x61rgs\x18\x05 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12X\n\x07headers\x18\x06 \x03(\x0b\x32G.coresdk.workflow_commands.SignalExternalWorkflowExecution.HeadersEntry\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01\x42\x08\n\x06target"#\n\x14\x43\x61ncelSignalWorkflow\x12\x0b\n\x03seq\x18\x01 \x01(\r"\xe6\x01\n\x1eUpsertWorkflowSearchAttributes\x12j\n\x11search_attributes\x18\x01 \x03(\x0b\x32O.coresdk.workflow_commands.UpsertWorkflowSearchAttributes.SearchAttributesEntry\x1aX\n\x15SearchAttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"O\n\x18ModifyWorkflowProperties\x12\x33\n\rupserted_memo\x18\x01 \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo"\xd2\x01\n\x0eUpdateResponse\x12\x1c\n\x14protocol_instance_id\x18\x01 \x01(\t\x12*\n\x08\x61\x63\x63\x65pted\x18\x02 \x01(\x0b\x32\x16.google.protobuf.EmptyH\x00\x12\x34\n\x08rejected\x18\x03 \x01(\x0b\x32 .temporal.api.failure.v1.FailureH\x00\x12\x34\n\tcompleted\x18\x04 \x01(\x0b\x32\x1f.temporal.api.common.v1.PayloadH\x00\x42\n\n\x08response"\xa1\x03\n\x16ScheduleNexusOperation\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x10\n\x08\x65ndpoint\x18\x02 \x01(\t\x12\x0f\n\x07service\x18\x03 \x01(\t\x12\x11\n\toperation\x18\x04 \x01(\t\x12.\n\x05input\x18\x05 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12<\n\x19schedule_to_close_timeout\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12X\n\x0cnexus_header\x18\x07 \x03(\x0b\x32\x42.coresdk.workflow_commands.ScheduleNexusOperation.NexusHeaderEntry\x12H\n\x11\x63\x61ncellation_type\x18\x08 \x01(\x0e\x32-.coresdk.nexus.NexusOperationCancellationType\x1a\x32\n\x10NexusHeaderEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"*\n\x1bRequestCancelNexusOperation\x12\x0b\n\x03seq\x18\x01 \x01(\r*X\n\x18\x41\x63tivityCancellationType\x12\x0e\n\nTRY_CANCEL\x10\x00\x12\x1f\n\x1bWAIT_CANCELLATION_COMPLETED\x10\x01\x12\x0b\n\x07\x41\x42\x41NDON\x10\x02\x42\x36\xea\x02\x33Temporalio::Internal::Bridge::Api::WorkflowCommandsb\x06proto3' + b'\n;temporal/sdk/core/workflow_commands/workflow_commands.proto\x12\x19\x63oresdk.workflow_commands\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a$temporal/api/common/v1/message.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a%temporal/api/failure/v1/message.proto\x1a\'temporal/api/sdk/v1/user_metadata.proto\x1a\x35temporal/sdk/core/child_workflow/child_workflow.proto\x1a#temporal/sdk/core/nexus/nexus.proto\x1a%temporal/sdk/core/common/common.proto"\xe5\x0f\n\x0fWorkflowCommand\x12\x38\n\ruser_metadata\x18\x64 \x01(\x0b\x32!.temporal.api.sdk.v1.UserMetadata\x12<\n\x0bstart_timer\x18\x01 \x01(\x0b\x32%.coresdk.workflow_commands.StartTimerH\x00\x12H\n\x11schedule_activity\x18\x02 \x01(\x0b\x32+.coresdk.workflow_commands.ScheduleActivityH\x00\x12\x42\n\x10respond_to_query\x18\x03 \x01(\x0b\x32&.coresdk.workflow_commands.QueryResultH\x00\x12S\n\x17request_cancel_activity\x18\x04 \x01(\x0b\x32\x30.coresdk.workflow_commands.RequestCancelActivityH\x00\x12>\n\x0c\x63\x61ncel_timer\x18\x05 \x01(\x0b\x32&.coresdk.workflow_commands.CancelTimerH\x00\x12[\n\x1b\x63omplete_workflow_execution\x18\x06 \x01(\x0b\x32\x34.coresdk.workflow_commands.CompleteWorkflowExecutionH\x00\x12S\n\x17\x66\x61il_workflow_execution\x18\x07 \x01(\x0b\x32\x30.coresdk.workflow_commands.FailWorkflowExecutionH\x00\x12g\n"continue_as_new_workflow_execution\x18\x08 \x01(\x0b\x32\x39.coresdk.workflow_commands.ContinueAsNewWorkflowExecutionH\x00\x12W\n\x19\x63\x61ncel_workflow_execution\x18\t \x01(\x0b\x32\x32.coresdk.workflow_commands.CancelWorkflowExecutionH\x00\x12\x45\n\x10set_patch_marker\x18\n \x01(\x0b\x32).coresdk.workflow_commands.SetPatchMarkerH\x00\x12`\n\x1estart_child_workflow_execution\x18\x0b \x01(\x0b\x32\x36.coresdk.workflow_commands.StartChildWorkflowExecutionH\x00\x12\x62\n\x1f\x63\x61ncel_child_workflow_execution\x18\x0c \x01(\x0b\x32\x37.coresdk.workflow_commands.CancelChildWorkflowExecutionH\x00\x12w\n*request_cancel_external_workflow_execution\x18\r \x01(\x0b\x32\x41.coresdk.workflow_commands.RequestCancelExternalWorkflowExecutionH\x00\x12h\n"signal_external_workflow_execution\x18\x0e \x01(\x0b\x32:.coresdk.workflow_commands.SignalExternalWorkflowExecutionH\x00\x12Q\n\x16\x63\x61ncel_signal_workflow\x18\x0f \x01(\x0b\x32/.coresdk.workflow_commands.CancelSignalWorkflowH\x00\x12S\n\x17schedule_local_activity\x18\x10 \x01(\x0b\x32\x30.coresdk.workflow_commands.ScheduleLocalActivityH\x00\x12^\n\x1drequest_cancel_local_activity\x18\x11 \x01(\x0b\x32\x35.coresdk.workflow_commands.RequestCancelLocalActivityH\x00\x12\x66\n!upsert_workflow_search_attributes\x18\x12 \x01(\x0b\x32\x39.coresdk.workflow_commands.UpsertWorkflowSearchAttributesH\x00\x12Y\n\x1amodify_workflow_properties\x18\x13 \x01(\x0b\x32\x33.coresdk.workflow_commands.ModifyWorkflowPropertiesH\x00\x12\x44\n\x0fupdate_response\x18\x14 \x01(\x0b\x32).coresdk.workflow_commands.UpdateResponseH\x00\x12U\n\x18schedule_nexus_operation\x18\x15 \x01(\x0b\x32\x31.coresdk.workflow_commands.ScheduleNexusOperationH\x00\x12`\n\x1erequest_cancel_nexus_operation\x18\x16 \x01(\x0b\x32\x36.coresdk.workflow_commands.RequestCancelNexusOperationH\x00\x42\t\n\x07variant"S\n\nStartTimer\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x38\n\x15start_to_fire_timeout\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration"\x1a\n\x0b\x43\x61ncelTimer\x12\x0b\n\x03seq\x18\x01 \x01(\r"\xb8\x06\n\x10ScheduleActivity\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x13\n\x0b\x61\x63tivity_id\x18\x02 \x01(\t\x12\x15\n\ractivity_type\x18\x03 \x01(\t\x12\x12\n\ntask_queue\x18\x05 \x01(\t\x12I\n\x07headers\x18\x06 \x03(\x0b\x32\x38.coresdk.workflow_commands.ScheduleActivity.HeadersEntry\x12\x32\n\targuments\x18\x07 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12<\n\x19schedule_to_close_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12<\n\x19schedule_to_start_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x34\n\x11heartbeat_timeout\x18\x0b \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x0cretry_policy\x18\x0c \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12N\n\x11\x63\x61ncellation_type\x18\r \x01(\x0e\x32\x33.coresdk.workflow_commands.ActivityCancellationType\x12\x1e\n\x16\x64o_not_eagerly_execute\x18\x0e \x01(\x08\x12;\n\x11versioning_intent\x18\x0f \x01(\x0e\x32 .coresdk.common.VersioningIntent\x12\x32\n\x08priority\x18\x10 \x01(\x0b\x32 .temporal.api.common.v1.Priority\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\xee\x05\n\x15ScheduleLocalActivity\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x13\n\x0b\x61\x63tivity_id\x18\x02 \x01(\t\x12\x15\n\ractivity_type\x18\x03 \x01(\t\x12\x0f\n\x07\x61ttempt\x18\x04 \x01(\r\x12:\n\x16original_schedule_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12N\n\x07headers\x18\x06 \x03(\x0b\x32=.coresdk.workflow_commands.ScheduleLocalActivity.HeadersEntry\x12\x32\n\targuments\x18\x07 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12<\n\x19schedule_to_close_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12<\n\x19schedule_to_start_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x0cretry_policy\x18\x0b \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x38\n\x15local_retry_threshold\x18\x0c \x01(\x0b\x32\x19.google.protobuf.Duration\x12N\n\x11\x63\x61ncellation_type\x18\r \x01(\x0e\x32\x33.coresdk.workflow_commands.ActivityCancellationType\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"$\n\x15RequestCancelActivity\x12\x0b\n\x03seq\x18\x01 \x01(\r")\n\x1aRequestCancelLocalActivity\x12\x0b\n\x03seq\x18\x01 \x01(\r"\x9c\x01\n\x0bQueryResult\x12\x10\n\x08query_id\x18\x01 \x01(\t\x12<\n\tsucceeded\x18\x02 \x01(\x0b\x32\'.coresdk.workflow_commands.QuerySuccessH\x00\x12\x32\n\x06\x66\x61iled\x18\x03 \x01(\x0b\x32 .temporal.api.failure.v1.FailureH\x00\x42\t\n\x07variant"A\n\x0cQuerySuccess\x12\x31\n\x08response\x18\x01 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload"L\n\x19\x43ompleteWorkflowExecution\x12/\n\x06result\x18\x01 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload"J\n\x15\x46\x61ilWorkflowExecution\x12\x31\n\x07\x66\x61ilure\x18\x01 \x01(\x0b\x32 .temporal.api.failure.v1.Failure"\x92\x07\n\x1e\x43ontinueAsNewWorkflowExecution\x12\x15\n\rworkflow_type\x18\x01 \x01(\t\x12\x12\n\ntask_queue\x18\x02 \x01(\t\x12\x32\n\targuments\x18\x03 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x37\n\x14workflow_run_timeout\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\x05 \x01(\x0b\x32\x19.google.protobuf.Duration\x12Q\n\x04memo\x18\x06 \x03(\x0b\x32\x43.coresdk.workflow_commands.ContinueAsNewWorkflowExecution.MemoEntry\x12W\n\x07headers\x18\x07 \x03(\x0b\x32\x46.coresdk.workflow_commands.ContinueAsNewWorkflowExecution.HeadersEntry\x12\x43\n\x11search_attributes\x18\x08 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12\x39\n\x0cretry_policy\x18\t \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12;\n\x11versioning_intent\x18\n \x01(\x0e\x32 .coresdk.common.VersioningIntent\x12[\n\x1binitial_versioning_behavior\x18\x0b \x01(\x0e\x32\x36.temporal.api.enums.v1.ContinueAsNewVersioningBehavior\x12\x39\n\x16\x62\x61\x63koff_start_interval\x18\x0c \x01(\x0b\x32\x19.google.protobuf.Duration\x1aL\n\tMemoEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\x19\n\x17\x43\x61ncelWorkflowExecution"6\n\x0eSetPatchMarker\x12\x10\n\x08patch_id\x18\x01 \x01(\t\x12\x12\n\ndeprecated\x18\x02 \x01(\x08"\x96\t\n\x1bStartChildWorkflowExecution\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x11\n\tnamespace\x18\x02 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x03 \x01(\t\x12\x15\n\rworkflow_type\x18\x04 \x01(\t\x12\x12\n\ntask_queue\x18\x05 \x01(\t\x12.\n\x05input\x18\x06 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12=\n\x1aworkflow_execution_timeout\x18\x07 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x37\n\x14workflow_run_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x46\n\x13parent_close_policy\x18\n \x01(\x0e\x32).coresdk.child_workflow.ParentClosePolicy\x12N\n\x18workflow_id_reuse_policy\x18\x0c \x01(\x0e\x32,.temporal.api.enums.v1.WorkflowIdReusePolicy\x12\x39\n\x0cretry_policy\x18\r \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x15\n\rcron_schedule\x18\x0e \x01(\t\x12T\n\x07headers\x18\x0f \x03(\x0b\x32\x43.coresdk.workflow_commands.StartChildWorkflowExecution.HeadersEntry\x12N\n\x04memo\x18\x10 \x03(\x0b\x32@.coresdk.workflow_commands.StartChildWorkflowExecution.MemoEntry\x12\x43\n\x11search_attributes\x18\x11 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12P\n\x11\x63\x61ncellation_type\x18\x12 \x01(\x0e\x32\x35.coresdk.child_workflow.ChildWorkflowCancellationType\x12;\n\x11versioning_intent\x18\x13 \x01(\x0e\x32 .coresdk.common.VersioningIntent\x12\x32\n\x08priority\x18\x14 \x01(\x0b\x32 .temporal.api.common.v1.Priority\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01\x1aL\n\tMemoEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"J\n\x1c\x43\x61ncelChildWorkflowExecution\x12\x1a\n\x12\x63hild_workflow_seq\x18\x01 \x01(\r\x12\x0e\n\x06reason\x18\x02 \x01(\t"\x8e\x01\n&RequestCancelExternalWorkflowExecution\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12G\n\x12workflow_execution\x18\x02 \x01(\x0b\x32+.coresdk.common.NamespacedWorkflowExecution\x12\x0e\n\x06reason\x18\x03 \x01(\t"\x8f\x03\n\x1fSignalExternalWorkflowExecution\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12I\n\x12workflow_execution\x18\x02 \x01(\x0b\x32+.coresdk.common.NamespacedWorkflowExecutionH\x00\x12\x1b\n\x11\x63hild_workflow_id\x18\x03 \x01(\tH\x00\x12\x13\n\x0bsignal_name\x18\x04 \x01(\t\x12-\n\x04\x61rgs\x18\x05 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12X\n\x07headers\x18\x06 \x03(\x0b\x32G.coresdk.workflow_commands.SignalExternalWorkflowExecution.HeadersEntry\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01\x42\x08\n\x06target"#\n\x14\x43\x61ncelSignalWorkflow\x12\x0b\n\x03seq\x18\x01 \x01(\r"e\n\x1eUpsertWorkflowSearchAttributes\x12\x43\n\x11search_attributes\x18\x01 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes"O\n\x18ModifyWorkflowProperties\x12\x33\n\rupserted_memo\x18\x01 \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo"\xd2\x01\n\x0eUpdateResponse\x12\x1c\n\x14protocol_instance_id\x18\x01 \x01(\t\x12*\n\x08\x61\x63\x63\x65pted\x18\x02 \x01(\x0b\x32\x16.google.protobuf.EmptyH\x00\x12\x34\n\x08rejected\x18\x03 \x01(\x0b\x32 .temporal.api.failure.v1.FailureH\x00\x12\x34\n\tcompleted\x18\x04 \x01(\x0b\x32\x1f.temporal.api.common.v1.PayloadH\x00\x42\n\n\x08response"\x9a\x04\n\x16ScheduleNexusOperation\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x10\n\x08\x65ndpoint\x18\x02 \x01(\t\x12\x0f\n\x07service\x18\x03 \x01(\t\x12\x11\n\toperation\x18\x04 \x01(\t\x12.\n\x05input\x18\x05 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12<\n\x19schedule_to_close_timeout\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12X\n\x0cnexus_header\x18\x07 \x03(\x0b\x32\x42.coresdk.workflow_commands.ScheduleNexusOperation.NexusHeaderEntry\x12H\n\x11\x63\x61ncellation_type\x18\x08 \x01(\x0e\x32-.coresdk.nexus.NexusOperationCancellationType\x12<\n\x19schedule_to_start_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.Duration\x1a\x32\n\x10NexusHeaderEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"*\n\x1bRequestCancelNexusOperation\x12\x0b\n\x03seq\x18\x01 \x01(\r*X\n\x18\x41\x63tivityCancellationType\x12\x0e\n\nTRY_CANCEL\x10\x00\x12\x1f\n\x1bWAIT_CANCELLATION_COMPLETED\x10\x01\x12\x0b\n\x07\x41\x42\x41NDON\x10\x02\x42\x36\xea\x02\x33Temporalio::Internal::Bridge::Api::WorkflowCommandsb\x06proto3' ) _ACTIVITYCANCELLATIONTYPE = DESCRIPTOR.enum_types_by_name["ActivityCancellationType"] @@ -80,9 +80,6 @@ _CONTINUEASNEWWORKFLOWEXECUTION_HEADERSENTRY = ( _CONTINUEASNEWWORKFLOWEXECUTION.nested_types_by_name["HeadersEntry"] ) -_CONTINUEASNEWWORKFLOWEXECUTION_SEARCHATTRIBUTESENTRY = ( - _CONTINUEASNEWWORKFLOWEXECUTION.nested_types_by_name["SearchAttributesEntry"] -) _CANCELWORKFLOWEXECUTION = DESCRIPTOR.message_types_by_name["CancelWorkflowExecution"] _SETPATCHMARKER = DESCRIPTOR.message_types_by_name["SetPatchMarker"] _STARTCHILDWORKFLOWEXECUTION = DESCRIPTOR.message_types_by_name[ @@ -94,9 +91,6 @@ _STARTCHILDWORKFLOWEXECUTION_MEMOENTRY = ( _STARTCHILDWORKFLOWEXECUTION.nested_types_by_name["MemoEntry"] ) -_STARTCHILDWORKFLOWEXECUTION_SEARCHATTRIBUTESENTRY = ( - _STARTCHILDWORKFLOWEXECUTION.nested_types_by_name["SearchAttributesEntry"] -) _CANCELCHILDWORKFLOWEXECUTION = DESCRIPTOR.message_types_by_name[ "CancelChildWorkflowExecution" ] @@ -113,9 +107,6 @@ _UPSERTWORKFLOWSEARCHATTRIBUTES = DESCRIPTOR.message_types_by_name[ "UpsertWorkflowSearchAttributes" ] -_UPSERTWORKFLOWSEARCHATTRIBUTES_SEARCHATTRIBUTESENTRY = ( - _UPSERTWORKFLOWSEARCHATTRIBUTES.nested_types_by_name["SearchAttributesEntry"] -) _MODIFYWORKFLOWPROPERTIES = DESCRIPTOR.message_types_by_name["ModifyWorkflowProperties"] _UPDATERESPONSE = DESCRIPTOR.message_types_by_name["UpdateResponse"] _SCHEDULENEXUSOPERATION = DESCRIPTOR.message_types_by_name["ScheduleNexusOperation"] @@ -288,15 +279,6 @@ # @@protoc_insertion_point(class_scope:coresdk.workflow_commands.ContinueAsNewWorkflowExecution.HeadersEntry) }, ), - "SearchAttributesEntry": _reflection.GeneratedProtocolMessageType( - "SearchAttributesEntry", - (_message.Message,), - { - "DESCRIPTOR": _CONTINUEASNEWWORKFLOWEXECUTION_SEARCHATTRIBUTESENTRY, - "__module__": "temporal.sdk.core.workflow_commands.workflow_commands_pb2", - # @@protoc_insertion_point(class_scope:coresdk.workflow_commands.ContinueAsNewWorkflowExecution.SearchAttributesEntry) - }, - ), "DESCRIPTOR": _CONTINUEASNEWWORKFLOWEXECUTION, "__module__": "temporal.sdk.core.workflow_commands.workflow_commands_pb2", # @@protoc_insertion_point(class_scope:coresdk.workflow_commands.ContinueAsNewWorkflowExecution) @@ -305,7 +287,6 @@ _sym_db.RegisterMessage(ContinueAsNewWorkflowExecution) _sym_db.RegisterMessage(ContinueAsNewWorkflowExecution.MemoEntry) _sym_db.RegisterMessage(ContinueAsNewWorkflowExecution.HeadersEntry) -_sym_db.RegisterMessage(ContinueAsNewWorkflowExecution.SearchAttributesEntry) CancelWorkflowExecution = _reflection.GeneratedProtocolMessageType( "CancelWorkflowExecution", @@ -351,15 +332,6 @@ # @@protoc_insertion_point(class_scope:coresdk.workflow_commands.StartChildWorkflowExecution.MemoEntry) }, ), - "SearchAttributesEntry": _reflection.GeneratedProtocolMessageType( - "SearchAttributesEntry", - (_message.Message,), - { - "DESCRIPTOR": _STARTCHILDWORKFLOWEXECUTION_SEARCHATTRIBUTESENTRY, - "__module__": "temporal.sdk.core.workflow_commands.workflow_commands_pb2", - # @@protoc_insertion_point(class_scope:coresdk.workflow_commands.StartChildWorkflowExecution.SearchAttributesEntry) - }, - ), "DESCRIPTOR": _STARTCHILDWORKFLOWEXECUTION, "__module__": "temporal.sdk.core.workflow_commands.workflow_commands_pb2", # @@protoc_insertion_point(class_scope:coresdk.workflow_commands.StartChildWorkflowExecution) @@ -368,7 +340,6 @@ _sym_db.RegisterMessage(StartChildWorkflowExecution) _sym_db.RegisterMessage(StartChildWorkflowExecution.HeadersEntry) _sym_db.RegisterMessage(StartChildWorkflowExecution.MemoEntry) -_sym_db.RegisterMessage(StartChildWorkflowExecution.SearchAttributesEntry) CancelChildWorkflowExecution = _reflection.GeneratedProtocolMessageType( "CancelChildWorkflowExecution", @@ -428,22 +399,12 @@ "UpsertWorkflowSearchAttributes", (_message.Message,), { - "SearchAttributesEntry": _reflection.GeneratedProtocolMessageType( - "SearchAttributesEntry", - (_message.Message,), - { - "DESCRIPTOR": _UPSERTWORKFLOWSEARCHATTRIBUTES_SEARCHATTRIBUTESENTRY, - "__module__": "temporal.sdk.core.workflow_commands.workflow_commands_pb2", - # @@protoc_insertion_point(class_scope:coresdk.workflow_commands.UpsertWorkflowSearchAttributes.SearchAttributesEntry) - }, - ), "DESCRIPTOR": _UPSERTWORKFLOWSEARCHATTRIBUTES, "__module__": "temporal.sdk.core.workflow_commands.workflow_commands_pb2", # @@protoc_insertion_point(class_scope:coresdk.workflow_commands.UpsertWorkflowSearchAttributes) }, ) _sym_db.RegisterMessage(UpsertWorkflowSearchAttributes) -_sym_db.RegisterMessage(UpsertWorkflowSearchAttributes.SearchAttributesEntry) ModifyWorkflowProperties = _reflection.GeneratedProtocolMessageType( "ModifyWorkflowProperties", @@ -512,22 +473,16 @@ _CONTINUEASNEWWORKFLOWEXECUTION_MEMOENTRY._serialized_options = b"8\001" _CONTINUEASNEWWORKFLOWEXECUTION_HEADERSENTRY._options = None _CONTINUEASNEWWORKFLOWEXECUTION_HEADERSENTRY._serialized_options = b"8\001" - _CONTINUEASNEWWORKFLOWEXECUTION_SEARCHATTRIBUTESENTRY._options = None - _CONTINUEASNEWWORKFLOWEXECUTION_SEARCHATTRIBUTESENTRY._serialized_options = b"8\001" _STARTCHILDWORKFLOWEXECUTION_HEADERSENTRY._options = None _STARTCHILDWORKFLOWEXECUTION_HEADERSENTRY._serialized_options = b"8\001" _STARTCHILDWORKFLOWEXECUTION_MEMOENTRY._options = None _STARTCHILDWORKFLOWEXECUTION_MEMOENTRY._serialized_options = b"8\001" - _STARTCHILDWORKFLOWEXECUTION_SEARCHATTRIBUTESENTRY._options = None - _STARTCHILDWORKFLOWEXECUTION_SEARCHATTRIBUTESENTRY._serialized_options = b"8\001" _SIGNALEXTERNALWORKFLOWEXECUTION_HEADERSENTRY._options = None _SIGNALEXTERNALWORKFLOWEXECUTION_HEADERSENTRY._serialized_options = b"8\001" - _UPSERTWORKFLOWSEARCHATTRIBUTES_SEARCHATTRIBUTESENTRY._options = None - _UPSERTWORKFLOWSEARCHATTRIBUTES_SEARCHATTRIBUTESENTRY._serialized_options = b"8\001" _SCHEDULENEXUSOPERATION_NEXUSHEADERENTRY._options = None _SCHEDULENEXUSOPERATION_NEXUSHEADERENTRY._serialized_options = b"8\001" - _ACTIVITYCANCELLATIONTYPE._serialized_start = 8580 - _ACTIVITYCANCELLATIONTYPE._serialized_end = 8668 + _ACTIVITYCANCELLATIONTYPE._serialized_start = 8468 + _ACTIVITYCANCELLATIONTYPE._serialized_end = 8556 _WORKFLOWCOMMAND._serialized_start = 472 _WORKFLOWCOMMAND._serialized_end = 2493 _STARTTIMER._serialized_start = 2495 @@ -555,47 +510,41 @@ _FAILWORKFLOWEXECUTION._serialized_start = 4573 _FAILWORKFLOWEXECUTION._serialized_end = 4647 _CONTINUEASNEWWORKFLOWEXECUTION._serialized_start = 4650 - _CONTINUEASNEWWORKFLOWEXECUTION._serialized_end = 5541 - _CONTINUEASNEWWORKFLOWEXECUTION_MEMOENTRY._serialized_start = 5294 - _CONTINUEASNEWWORKFLOWEXECUTION_MEMOENTRY._serialized_end = 5370 + _CONTINUEASNEWWORKFLOWEXECUTION._serialized_end = 5564 + _CONTINUEASNEWWORKFLOWEXECUTION_MEMOENTRY._serialized_start = 5407 + _CONTINUEASNEWWORKFLOWEXECUTION_MEMOENTRY._serialized_end = 5483 _CONTINUEASNEWWORKFLOWEXECUTION_HEADERSENTRY._serialized_start = 3354 _CONTINUEASNEWWORKFLOWEXECUTION_HEADERSENTRY._serialized_end = 3433 - _CONTINUEASNEWWORKFLOWEXECUTION_SEARCHATTRIBUTESENTRY._serialized_start = 5453 - _CONTINUEASNEWWORKFLOWEXECUTION_SEARCHATTRIBUTESENTRY._serialized_end = 5541 - _CANCELWORKFLOWEXECUTION._serialized_start = 5543 - _CANCELWORKFLOWEXECUTION._serialized_end = 5568 - _SETPATCHMARKER._serialized_start = 5570 - _SETPATCHMARKER._serialized_end = 5624 - _STARTCHILDWORKFLOWEXECUTION._serialized_start = 5627 - _STARTCHILDWORKFLOWEXECUTION._serialized_end = 6927 + _CANCELWORKFLOWEXECUTION._serialized_start = 5566 + _CANCELWORKFLOWEXECUTION._serialized_end = 5591 + _SETPATCHMARKER._serialized_start = 5593 + _SETPATCHMARKER._serialized_end = 5647 + _STARTCHILDWORKFLOWEXECUTION._serialized_start = 5650 + _STARTCHILDWORKFLOWEXECUTION._serialized_end = 6824 _STARTCHILDWORKFLOWEXECUTION_HEADERSENTRY._serialized_start = 3354 _STARTCHILDWORKFLOWEXECUTION_HEADERSENTRY._serialized_end = 3433 - _STARTCHILDWORKFLOWEXECUTION_MEMOENTRY._serialized_start = 5294 - _STARTCHILDWORKFLOWEXECUTION_MEMOENTRY._serialized_end = 5370 - _STARTCHILDWORKFLOWEXECUTION_SEARCHATTRIBUTESENTRY._serialized_start = 5453 - _STARTCHILDWORKFLOWEXECUTION_SEARCHATTRIBUTESENTRY._serialized_end = 5541 - _CANCELCHILDWORKFLOWEXECUTION._serialized_start = 6929 - _CANCELCHILDWORKFLOWEXECUTION._serialized_end = 7003 - _REQUESTCANCELEXTERNALWORKFLOWEXECUTION._serialized_start = 7006 - _REQUESTCANCELEXTERNALWORKFLOWEXECUTION._serialized_end = 7148 - _SIGNALEXTERNALWORKFLOWEXECUTION._serialized_start = 7151 - _SIGNALEXTERNALWORKFLOWEXECUTION._serialized_end = 7550 + _STARTCHILDWORKFLOWEXECUTION_MEMOENTRY._serialized_start = 5407 + _STARTCHILDWORKFLOWEXECUTION_MEMOENTRY._serialized_end = 5483 + _CANCELCHILDWORKFLOWEXECUTION._serialized_start = 6826 + _CANCELCHILDWORKFLOWEXECUTION._serialized_end = 6900 + _REQUESTCANCELEXTERNALWORKFLOWEXECUTION._serialized_start = 6903 + _REQUESTCANCELEXTERNALWORKFLOWEXECUTION._serialized_end = 7045 + _SIGNALEXTERNALWORKFLOWEXECUTION._serialized_start = 7048 + _SIGNALEXTERNALWORKFLOWEXECUTION._serialized_end = 7447 _SIGNALEXTERNALWORKFLOWEXECUTION_HEADERSENTRY._serialized_start = 3354 _SIGNALEXTERNALWORKFLOWEXECUTION_HEADERSENTRY._serialized_end = 3433 - _CANCELSIGNALWORKFLOW._serialized_start = 7552 - _CANCELSIGNALWORKFLOW._serialized_end = 7587 - _UPSERTWORKFLOWSEARCHATTRIBUTES._serialized_start = 7590 - _UPSERTWORKFLOWSEARCHATTRIBUTES._serialized_end = 7820 - _UPSERTWORKFLOWSEARCHATTRIBUTES_SEARCHATTRIBUTESENTRY._serialized_start = 5453 - _UPSERTWORKFLOWSEARCHATTRIBUTES_SEARCHATTRIBUTESENTRY._serialized_end = 5541 - _MODIFYWORKFLOWPROPERTIES._serialized_start = 7822 - _MODIFYWORKFLOWPROPERTIES._serialized_end = 7901 - _UPDATERESPONSE._serialized_start = 7904 - _UPDATERESPONSE._serialized_end = 8114 - _SCHEDULENEXUSOPERATION._serialized_start = 8117 - _SCHEDULENEXUSOPERATION._serialized_end = 8534 - _SCHEDULENEXUSOPERATION_NEXUSHEADERENTRY._serialized_start = 8484 - _SCHEDULENEXUSOPERATION_NEXUSHEADERENTRY._serialized_end = 8534 - _REQUESTCANCELNEXUSOPERATION._serialized_start = 8536 - _REQUESTCANCELNEXUSOPERATION._serialized_end = 8578 + _CANCELSIGNALWORKFLOW._serialized_start = 7449 + _CANCELSIGNALWORKFLOW._serialized_end = 7484 + _UPSERTWORKFLOWSEARCHATTRIBUTES._serialized_start = 7486 + _UPSERTWORKFLOWSEARCHATTRIBUTES._serialized_end = 7587 + _MODIFYWORKFLOWPROPERTIES._serialized_start = 7589 + _MODIFYWORKFLOWPROPERTIES._serialized_end = 7668 + _UPDATERESPONSE._serialized_start = 7671 + _UPDATERESPONSE._serialized_end = 7881 + _SCHEDULENEXUSOPERATION._serialized_start = 7884 + _SCHEDULENEXUSOPERATION._serialized_end = 8422 + _SCHEDULENEXUSOPERATION_NEXUSHEADERENTRY._serialized_start = 8372 + _SCHEDULENEXUSOPERATION_NEXUSHEADERENTRY._serialized_end = 8422 + _REQUESTCANCELNEXUSOPERATION._serialized_start = 8424 + _REQUESTCANCELNEXUSOPERATION._serialized_end = 8466 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/bridge/proto/workflow_commands/workflow_commands_pb2.pyi b/temporalio/bridge/proto/workflow_commands/workflow_commands_pb2.pyi index 2143d89a5..3860809b9 100644 --- a/temporalio/bridge/proto/workflow_commands/workflow_commands_pb2.pyi +++ b/temporalio/bridge/proto/workflow_commands/workflow_commands_pb2.pyi @@ -931,28 +931,6 @@ class ContinueAsNewWorkflowExecution(google.protobuf.message.Message): field_name: typing_extensions.Literal["key", b"key", "value", b"value"], ) -> None: ... - class SearchAttributesEntry(google.protobuf.message.Message): - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - KEY_FIELD_NUMBER: builtins.int - VALUE_FIELD_NUMBER: builtins.int - key: builtins.str - @property - def value(self) -> temporalio.api.common.v1.message_pb2.Payload: ... - def __init__( - self, - *, - key: builtins.str = ..., - value: temporalio.api.common.v1.message_pb2.Payload | None = ..., - ) -> None: ... - def HasField( - self, field_name: typing_extensions.Literal["value", b"value"] - ) -> builtins.bool: ... - def ClearField( - self, - field_name: typing_extensions.Literal["key", b"key", "value", b"value"], - ) -> None: ... - WORKFLOW_TYPE_FIELD_NUMBER: builtins.int TASK_QUEUE_FIELD_NUMBER: builtins.int ARGUMENTS_FIELD_NUMBER: builtins.int @@ -963,6 +941,8 @@ class ContinueAsNewWorkflowExecution(google.protobuf.message.Message): SEARCH_ATTRIBUTES_FIELD_NUMBER: builtins.int RETRY_POLICY_FIELD_NUMBER: builtins.int VERSIONING_INTENT_FIELD_NUMBER: builtins.int + INITIAL_VERSIONING_BEHAVIOR_FIELD_NUMBER: builtins.int + BACKOFF_START_INTERVAL_FIELD_NUMBER: builtins.int workflow_type: builtins.str """The identifier the lang-specific sdk uses to execute workflow code""" task_queue: builtins.str @@ -1001,9 +981,7 @@ class ContinueAsNewWorkflowExecution(google.protobuf.message.Message): @property def search_attributes( self, - ) -> google.protobuf.internal.containers.MessageMap[ - builtins.str, temporalio.api.common.v1.message_pb2.Payload - ]: + ) -> temporalio.api.common.v1.message_pb2.SearchAttributes: """If set, the new workflow will have these search attributes. If unset, re-uses the current workflow's search attributes. """ @@ -1016,6 +994,16 @@ class ContinueAsNewWorkflowExecution(google.protobuf.message.Message): temporalio.bridge.proto.common.common_pb2.VersioningIntent.ValueType ) """Whether the continued workflow should run on a worker with a compatible build id or not.""" + initial_versioning_behavior: ( + temporalio.api.enums.v1.workflow_pb2.ContinueAsNewVersioningBehavior.ValueType + ) + """Experimental. Optionally decide the versioning behavior that the first task of the new run should use. + For example, choose to AutoUpgrade on continue-as-new instead of inheriting the pinned version + of the previous run. + """ + @property + def backoff_start_interval(self) -> google.protobuf.duration_pb2.Duration: + """Delay before the first workflow task of the continued run is scheduled.""" def __init__( self, *, @@ -1035,18 +1023,22 @@ class ContinueAsNewWorkflowExecution(google.protobuf.message.Message): builtins.str, temporalio.api.common.v1.message_pb2.Payload ] | None = ..., - search_attributes: collections.abc.Mapping[ - builtins.str, temporalio.api.common.v1.message_pb2.Payload - ] + search_attributes: temporalio.api.common.v1.message_pb2.SearchAttributes | None = ..., retry_policy: temporalio.api.common.v1.message_pb2.RetryPolicy | None = ..., versioning_intent: temporalio.bridge.proto.common.common_pb2.VersioningIntent.ValueType = ..., + initial_versioning_behavior: temporalio.api.enums.v1.workflow_pb2.ContinueAsNewVersioningBehavior.ValueType = ..., + backoff_start_interval: google.protobuf.duration_pb2.Duration | None = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal[ + "backoff_start_interval", + b"backoff_start_interval", "retry_policy", b"retry_policy", + "search_attributes", + b"search_attributes", "workflow_run_timeout", b"workflow_run_timeout", "workflow_task_timeout", @@ -1058,8 +1050,12 @@ class ContinueAsNewWorkflowExecution(google.protobuf.message.Message): field_name: typing_extensions.Literal[ "arguments", b"arguments", + "backoff_start_interval", + b"backoff_start_interval", "headers", b"headers", + "initial_versioning_behavior", + b"initial_versioning_behavior", "memo", b"memo", "retry_policy", @@ -1174,28 +1170,6 @@ class StartChildWorkflowExecution(google.protobuf.message.Message): field_name: typing_extensions.Literal["key", b"key", "value", b"value"], ) -> None: ... - class SearchAttributesEntry(google.protobuf.message.Message): - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - KEY_FIELD_NUMBER: builtins.int - VALUE_FIELD_NUMBER: builtins.int - key: builtins.str - @property - def value(self) -> temporalio.api.common.v1.message_pb2.Payload: ... - def __init__( - self, - *, - key: builtins.str = ..., - value: temporalio.api.common.v1.message_pb2.Payload | None = ..., - ) -> None: ... - def HasField( - self, field_name: typing_extensions.Literal["value", b"value"] - ) -> builtins.bool: ... - def ClearField( - self, - field_name: typing_extensions.Literal["key", b"key", "value", b"value"], - ) -> None: ... - SEQ_FIELD_NUMBER: builtins.int NAMESPACE_FIELD_NUMBER: builtins.int WORKFLOW_ID_FIELD_NUMBER: builtins.int @@ -1264,9 +1238,7 @@ class StartChildWorkflowExecution(google.protobuf.message.Message): @property def search_attributes( self, - ) -> google.protobuf.internal.containers.MessageMap[ - builtins.str, temporalio.api.common.v1.message_pb2.Payload - ]: + ) -> temporalio.api.common.v1.message_pb2.SearchAttributes: """Search attributes""" cancellation_type: temporalio.bridge.proto.child_workflow.child_workflow_pb2.ChildWorkflowCancellationType.ValueType """Defines behaviour of the underlying workflow when child workflow cancellation has been requested.""" @@ -1302,9 +1274,7 @@ class StartChildWorkflowExecution(google.protobuf.message.Message): builtins.str, temporalio.api.common.v1.message_pb2.Payload ] | None = ..., - search_attributes: collections.abc.Mapping[ - builtins.str, temporalio.api.common.v1.message_pb2.Payload - ] + search_attributes: temporalio.api.common.v1.message_pb2.SearchAttributes | None = ..., cancellation_type: temporalio.bridge.proto.child_workflow.child_workflow_pb2.ChildWorkflowCancellationType.ValueType = ..., versioning_intent: temporalio.bridge.proto.common.common_pb2.VersioningIntent.ValueType = ..., @@ -1317,6 +1287,8 @@ class StartChildWorkflowExecution(google.protobuf.message.Message): b"priority", "retry_policy", b"retry_policy", + "search_attributes", + b"search_attributes", "workflow_execution_timeout", b"workflow_execution_timeout", "workflow_run_timeout", @@ -1577,46 +1549,26 @@ global___CancelSignalWorkflow = CancelSignalWorkflow class UpsertWorkflowSearchAttributes(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor - class SearchAttributesEntry(google.protobuf.message.Message): - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - KEY_FIELD_NUMBER: builtins.int - VALUE_FIELD_NUMBER: builtins.int - key: builtins.str - @property - def value(self) -> temporalio.api.common.v1.message_pb2.Payload: ... - def __init__( - self, - *, - key: builtins.str = ..., - value: temporalio.api.common.v1.message_pb2.Payload | None = ..., - ) -> None: ... - def HasField( - self, field_name: typing_extensions.Literal["value", b"value"] - ) -> builtins.bool: ... - def ClearField( - self, - field_name: typing_extensions.Literal["key", b"key", "value", b"value"], - ) -> None: ... - SEARCH_ATTRIBUTES_FIELD_NUMBER: builtins.int @property def search_attributes( self, - ) -> google.protobuf.internal.containers.MessageMap[ - builtins.str, temporalio.api.common.v1.message_pb2.Payload - ]: - """SearchAttributes fields - equivalent to indexed_fields on api. Key = search index, Value = - value? + ) -> temporalio.api.common.v1.message_pb2.SearchAttributes: + """SearchAttributes to upsert. The indexed_fields map will be merged with existing search + attributes, with these values taking precedence. """ def __init__( self, *, - search_attributes: collections.abc.Mapping[ - builtins.str, temporalio.api.common.v1.message_pb2.Payload - ] + search_attributes: temporalio.api.common.v1.message_pb2.SearchAttributes | None = ..., ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "search_attributes", b"search_attributes" + ], + ) -> builtins.bool: ... def ClearField( self, field_name: typing_extensions.Literal[ @@ -1757,6 +1709,8 @@ class ScheduleNexusOperation(google.protobuf.message.Message): SCHEDULE_TO_CLOSE_TIMEOUT_FIELD_NUMBER: builtins.int NEXUS_HEADER_FIELD_NUMBER: builtins.int CANCELLATION_TYPE_FIELD_NUMBER: builtins.int + SCHEDULE_TO_START_TIMEOUT_FIELD_NUMBER: builtins.int + START_TO_CLOSE_TIMEOUT_FIELD_NUMBER: builtins.int seq: builtins.int """Lang's incremental sequence number, used as the operation identifier""" endpoint: builtins.str @@ -1793,6 +1747,23 @@ class ScheduleNexusOperation(google.protobuf.message.Message): temporalio.bridge.proto.nexus.nexus_pb2.NexusOperationCancellationType.ValueType ) """Defines behaviour of the underlying nexus operation when operation cancellation has been requested.""" + @property + def schedule_to_start_timeout(self) -> google.protobuf.duration_pb2.Duration: + """Schedule-to-start timeout for this operation. + Indicates how long the caller is willing to wait for the operation to be started (or completed if synchronous) + by the handler. If the operation is not started within this timeout, it will fail with + TIMEOUT_TYPE_SCHEDULE_TO_START. + If not set or zero, no schedule-to-start timeout is enforced. + """ + @property + def start_to_close_timeout(self) -> google.protobuf.duration_pb2.Duration: + """Start-to-close timeout for this operation. + Indicates how long the caller is willing to wait for an asynchronous operation to complete after it has been + started. If the operation does not complete within this timeout after starting, it will fail with + TIMEOUT_TYPE_START_TO_CLOSE. + Only applies to asynchronous operations. Synchronous operations ignore this timeout. + If not set or zero, no start-to-close timeout is enforced. + """ def __init__( self, *, @@ -1804,11 +1775,20 @@ class ScheduleNexusOperation(google.protobuf.message.Message): schedule_to_close_timeout: google.protobuf.duration_pb2.Duration | None = ..., nexus_header: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., cancellation_type: temporalio.bridge.proto.nexus.nexus_pb2.NexusOperationCancellationType.ValueType = ..., + schedule_to_start_timeout: google.protobuf.duration_pb2.Duration | None = ..., + start_to_close_timeout: google.protobuf.duration_pb2.Duration | None = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal[ - "input", b"input", "schedule_to_close_timeout", b"schedule_to_close_timeout" + "input", + b"input", + "schedule_to_close_timeout", + b"schedule_to_close_timeout", + "schedule_to_start_timeout", + b"schedule_to_start_timeout", + "start_to_close_timeout", + b"start_to_close_timeout", ], ) -> builtins.bool: ... def ClearField( @@ -1826,10 +1806,14 @@ class ScheduleNexusOperation(google.protobuf.message.Message): b"operation", "schedule_to_close_timeout", b"schedule_to_close_timeout", + "schedule_to_start_timeout", + b"schedule_to_start_timeout", "seq", b"seq", "service", b"service", + "start_to_close_timeout", + b"start_to_close_timeout", ], ) -> None: ... diff --git a/temporalio/bridge/proto/workflow_completion/workflow_completion_pb2.py b/temporalio/bridge/proto/workflow_completion/workflow_completion_pb2.py index ce26b220d..057b301e4 100644 --- a/temporalio/bridge/proto/workflow_completion/workflow_completion_pb2.py +++ b/temporalio/bridge/proto/workflow_completion/workflow_completion_pb2.py @@ -31,7 +31,7 @@ ) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n?temporal/sdk/core/workflow_completion/workflow_completion.proto\x12\x1b\x63oresdk.workflow_completion\x1a%temporal/api/failure/v1/message.proto\x1a(temporal/api/enums/v1/failed_cause.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a%temporal/sdk/core/common/common.proto\x1a;temporal/sdk/core/workflow_commands/workflow_commands.proto"\xac\x01\n\x1cWorkflowActivationCompletion\x12\x0e\n\x06run_id\x18\x01 \x01(\t\x12:\n\nsuccessful\x18\x02 \x01(\x0b\x32$.coresdk.workflow_completion.SuccessH\x00\x12\x36\n\x06\x66\x61iled\x18\x03 \x01(\x0b\x32$.coresdk.workflow_completion.FailureH\x00\x42\x08\n\x06status"\xac\x01\n\x07Success\x12<\n\x08\x63ommands\x18\x01 \x03(\x0b\x32*.coresdk.workflow_commands.WorkflowCommand\x12\x1b\n\x13used_internal_flags\x18\x06 \x03(\r\x12\x46\n\x13versioning_behavior\x18\x07 \x01(\x0e\x32).temporal.api.enums.v1.VersioningBehavior"\x81\x01\n\x07\x46\x61ilure\x12\x31\n\x07\x66\x61ilure\x18\x01 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x43\n\x0b\x66orce_cause\x18\x02 \x01(\x0e\x32..temporal.api.enums.v1.WorkflowTaskFailedCauseB8\xea\x02\x35Temporalio::Internal::Bridge::Api::WorkflowCompletionb\x06proto3' + b'\n?temporal/sdk/core/workflow_completion/workflow_completion.proto\x12\x1b\x63oresdk.workflow_completion\x1a%temporal/api/failure/v1/message.proto\x1a(temporal/api/enums/v1/failed_cause.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a%temporal/sdk/core/common/common.proto\x1a;temporal/sdk/core/workflow_commands/workflow_commands.proto"\xbe\x02\n\x1cWorkflowActivationCompletion\x12\x0e\n\x06run_id\x18\x01 \x01(\t\x12:\n\nsuccessful\x18\x02 \x01(\x0b\x32$.coresdk.workflow_completion.SuccessH\x00\x12\x36\n\x06\x66\x61iled\x18\x03 \x01(\x0b\x32$.coresdk.workflow_completion.FailureH\x00\x12H\n\x18payload_download_metrics\x18\x04 \x01(\x0b\x32&.coresdk.common.ExternalStorageMetrics\x12\x46\n\x16payload_upload_metrics\x18\x05 \x01(\x0b\x32&.coresdk.common.ExternalStorageMetricsB\x08\n\x06status"\xac\x01\n\x07Success\x12<\n\x08\x63ommands\x18\x01 \x03(\x0b\x32*.coresdk.workflow_commands.WorkflowCommand\x12\x1b\n\x13used_internal_flags\x18\x06 \x03(\r\x12\x46\n\x13versioning_behavior\x18\x07 \x01(\x0e\x32).temporal.api.enums.v1.VersioningBehavior"\x81\x01\n\x07\x46\x61ilure\x12\x31\n\x07\x66\x61ilure\x18\x01 \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12\x43\n\x0b\x66orce_cause\x18\x02 \x01(\x0e\x32..temporal.api.enums.v1.WorkflowTaskFailedCauseB8\xea\x02\x35Temporalio::Internal::Bridge::Api::WorkflowCompletionb\x06proto3' ) @@ -79,9 +79,9 @@ b"\352\0025Temporalio::Internal::Bridge::Api::WorkflowCompletion" ) _WORKFLOWACTIVATIONCOMPLETION._serialized_start = 316 - _WORKFLOWACTIVATIONCOMPLETION._serialized_end = 488 - _SUCCESS._serialized_start = 491 - _SUCCESS._serialized_end = 663 - _FAILURE._serialized_start = 666 - _FAILURE._serialized_end = 795 + _WORKFLOWACTIVATIONCOMPLETION._serialized_end = 634 + _SUCCESS._serialized_start = 637 + _SUCCESS._serialized_end = 809 + _FAILURE._serialized_start = 812 + _FAILURE._serialized_end = 941 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/bridge/proto/workflow_completion/workflow_completion_pb2.pyi b/temporalio/bridge/proto/workflow_completion/workflow_completion_pb2.pyi index 5b438f360..8e12736aa 100644 --- a/temporalio/bridge/proto/workflow_completion/workflow_completion_pb2.pyi +++ b/temporalio/bridge/proto/workflow_completion/workflow_completion_pb2.pyi @@ -14,6 +14,7 @@ import google.protobuf.message import temporalio.api.enums.v1.failed_cause_pb2 import temporalio.api.enums.v1.workflow_pb2 import temporalio.api.failure.v1.message_pb2 +import temporalio.bridge.proto.common.common_pb2 import temporalio.bridge.proto.workflow_commands.workflow_commands_pb2 if sys.version_info >= (3, 8): @@ -31,23 +32,52 @@ class WorkflowActivationCompletion(google.protobuf.message.Message): RUN_ID_FIELD_NUMBER: builtins.int SUCCESSFUL_FIELD_NUMBER: builtins.int FAILED_FIELD_NUMBER: builtins.int + PAYLOAD_DOWNLOAD_METRICS_FIELD_NUMBER: builtins.int + PAYLOAD_UPLOAD_METRICS_FIELD_NUMBER: builtins.int run_id: builtins.str """The run id from the workflow activation you are completing""" @property def successful(self) -> global___Success: ... @property def failed(self) -> global___Failure: ... + @property + def payload_download_metrics( + self, + ) -> temporalio.bridge.proto.common.common_pb2.ExternalStorageMetrics: + """Metrics for external payload storage downloads (retrievals) performed while processing + this activation. Only set when external storage retrieved payloads. + """ + @property + def payload_upload_metrics( + self, + ) -> temporalio.bridge.proto.common.common_pb2.ExternalStorageMetrics: + """Metrics for external payload storage uploads (stores) performed while processing this + activation. Only set when external storage stored payloads. + """ def __init__( self, *, run_id: builtins.str = ..., successful: global___Success | None = ..., failed: global___Failure | None = ..., + payload_download_metrics: temporalio.bridge.proto.common.common_pb2.ExternalStorageMetrics + | None = ..., + payload_upload_metrics: temporalio.bridge.proto.common.common_pb2.ExternalStorageMetrics + | None = ..., ) -> None: ... def HasField( self, field_name: typing_extensions.Literal[ - "failed", b"failed", "status", b"status", "successful", b"successful" + "failed", + b"failed", + "payload_download_metrics", + b"payload_download_metrics", + "payload_upload_metrics", + b"payload_upload_metrics", + "status", + b"status", + "successful", + b"successful", ], ) -> builtins.bool: ... def ClearField( @@ -55,6 +85,10 @@ class WorkflowActivationCompletion(google.protobuf.message.Message): field_name: typing_extensions.Literal[ "failed", b"failed", + "payload_download_metrics", + b"payload_download_metrics", + "payload_upload_metrics", + b"payload_upload_metrics", "run_id", b"run_id", "status", diff --git a/temporalio/bridge/runtime.py b/temporalio/bridge/runtime.py index afc79f0f5..e5987cb42 100644 --- a/temporalio/bridge/runtime.py +++ b/temporalio/bridge/runtime.py @@ -5,8 +5,9 @@ from __future__ import annotations +from collections.abc import Callable, Mapping, Sequence from dataclasses import dataclass -from typing import Any, Callable, Dict, Mapping, Optional, Sequence, Type +from typing import Any from typing_extensions import Protocol @@ -17,15 +18,15 @@ class Runtime: """Runtime for SDK Core.""" @staticmethod - def _raise_in_thread(thread_id: int, exc_type: Type[BaseException]) -> bool: + def _raise_in_thread(thread_id: int, exc_type: type[BaseException]) -> bool: """Internal helper for raising an exception in thread.""" return temporalio.bridge.temporal_sdk_bridge.raise_in_thread( thread_id, exc_type ) - def __init__(self, *, telemetry: TelemetryConfig) -> None: + def __init__(self, *, options: RuntimeOptions) -> None: """Create SDK Core runtime.""" - self._ref = temporalio.bridge.temporal_sdk_bridge.init_runtime(telemetry) + self._ref = temporalio.bridge.temporal_sdk_bridge.init_runtime(options) def retrieve_buffered_metrics(self, durations_as_seconds: bool) -> Sequence[Any]: """Get buffered metrics.""" @@ -45,19 +46,19 @@ class LoggingConfig: """Python representation of the Rust struct for logging config.""" filter: str - forward_to: Optional[Callable[[Sequence[BufferedLogEntry]], None]] + forward_to: Callable[[Sequence[BufferedLogEntry]], None] | None @dataclass(frozen=True) class MetricsConfig: """Python representation of the Rust struct for metrics config.""" - opentelemetry: Optional[OpenTelemetryConfig] - prometheus: Optional[PrometheusConfig] + opentelemetry: OpenTelemetryConfig | None + prometheus: PrometheusConfig | None buffered_with_size: int attach_service_name: bool - global_tags: Optional[Mapping[str, str]] - metric_prefix: Optional[str] + global_tags: Mapping[str, str] | None + metric_prefix: str | None @dataclass(frozen=True) @@ -66,10 +67,11 @@ class OpenTelemetryConfig: url: str headers: Mapping[str, str] - metric_periodicity_millis: Optional[int] + metric_periodicity_millis: int | None metric_temporality_delta: bool durations_as_seconds: bool http: bool + histogram_bucket_overrides: Mapping[str, Sequence[float]] | None = None @dataclass(frozen=True) @@ -80,15 +82,24 @@ class PrometheusConfig: counters_total_suffix: bool unit_suffix: bool durations_as_seconds: bool - histogram_bucket_overrides: Optional[Mapping[str, Sequence[float]]] = None + histogram_bucket_overrides: Mapping[str, Sequence[float]] | None = None @dataclass(frozen=True) class TelemetryConfig: """Python representation of the Rust struct for telemetry config.""" - logging: Optional[LoggingConfig] - metrics: Optional[MetricsConfig] + logging: LoggingConfig | None + metrics: MetricsConfig | None + + +@dataclass(frozen=True) +class RuntimeOptions: + """Python representation of the Rust struct for runtime options.""" + + telemetry: TelemetryConfig + worker_heartbeat_interval_millis: int | None = 60_000 # 60s + disable_environment_info: bool = False # WARNING: This must match Rust runtime::BufferedLogEntry @@ -116,7 +127,7 @@ def level(self) -> int: ... @property - def fields(self) -> Dict[str, Any]: + def fields(self) -> dict[str, Any]: """Additional log entry fields. Requesting this property performs a conversion from the internal representation to the Python representation on every request. Therefore diff --git a/temporalio/bridge/sdk-core b/temporalio/bridge/sdk-core index 4614dcb8f..8cf682b7a 160000 --- a/temporalio/bridge/sdk-core +++ b/temporalio/bridge/sdk-core @@ -1 +1 @@ -Subproject commit 4614dcb8f4ffd2cb244eb0a19d7485c896e3459e +Subproject commit 8cf682b7aec9aafbeb6e779872822f37a6f8c55c diff --git a/temporalio/bridge/services_generated.py b/temporalio/bridge/services_generated.py new file mode 100644 index 000000000..c9960fcb5 --- /dev/null +++ b/temporalio/bridge/services_generated.py @@ -0,0 +1,3990 @@ +# Generated file. DO NOT EDIT +"""Generated RPC calls for Temporal services.""" + +from __future__ import annotations + +from collections.abc import Mapping +from datetime import timedelta +from typing import TYPE_CHECKING + +import google.protobuf.empty_pb2 + +import temporalio.api.cloud.cloudservice.v1 +import temporalio.api.operatorservice.v1 +import temporalio.api.testservice.v1 +import temporalio.api.workflowservice.v1 +import temporalio.bridge.proto.health.v1 + +if TYPE_CHECKING: + from temporalio.service import ServiceClient + + +class WorkflowService: + """RPC calls for the WorkflowService.""" + + def __init__(self, client: ServiceClient): + """Initialize service with the provided ServiceClient.""" + self._client = client + self._service = "workflow" + + async def count_activity_executions( + self, + req: temporalio.api.workflowservice.v1.CountActivityExecutionsRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.CountActivityExecutionsResponse: + """Invokes the WorkflowService.count_activity_executions rpc method.""" + return await self._client._rpc_call( + rpc="count_activity_executions", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.CountActivityExecutionsResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def count_nexus_operation_executions( + self, + req: temporalio.api.workflowservice.v1.CountNexusOperationExecutionsRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.CountNexusOperationExecutionsResponse: + """Invokes the WorkflowService.count_nexus_operation_executions rpc method.""" + return await self._client._rpc_call( + rpc="count_nexus_operation_executions", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.CountNexusOperationExecutionsResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def count_schedules( + self, + req: temporalio.api.workflowservice.v1.CountSchedulesRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.CountSchedulesResponse: + """Invokes the WorkflowService.count_schedules rpc method.""" + return await self._client._rpc_call( + rpc="count_schedules", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.CountSchedulesResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def count_workers( + self, + req: temporalio.api.workflowservice.v1.CountWorkersRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.CountWorkersResponse: + """Invokes the WorkflowService.count_workers rpc method.""" + return await self._client._rpc_call( + rpc="count_workers", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.CountWorkersResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def count_workflow_executions( + self, + req: temporalio.api.workflowservice.v1.CountWorkflowExecutionsRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.CountWorkflowExecutionsResponse: + """Invokes the WorkflowService.count_workflow_executions rpc method.""" + return await self._client._rpc_call( + rpc="count_workflow_executions", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.CountWorkflowExecutionsResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def create_schedule( + self, + req: temporalio.api.workflowservice.v1.CreateScheduleRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.CreateScheduleResponse: + """Invokes the WorkflowService.create_schedule rpc method.""" + return await self._client._rpc_call( + rpc="create_schedule", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.CreateScheduleResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def create_worker_deployment( + self, + req: temporalio.api.workflowservice.v1.CreateWorkerDeploymentRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.CreateWorkerDeploymentResponse: + """Invokes the WorkflowService.create_worker_deployment rpc method.""" + return await self._client._rpc_call( + rpc="create_worker_deployment", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.CreateWorkerDeploymentResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def create_worker_deployment_version( + self, + req: temporalio.api.workflowservice.v1.CreateWorkerDeploymentVersionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.CreateWorkerDeploymentVersionResponse: + """Invokes the WorkflowService.create_worker_deployment_version rpc method.""" + return await self._client._rpc_call( + rpc="create_worker_deployment_version", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.CreateWorkerDeploymentVersionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def create_workflow_rule( + self, + req: temporalio.api.workflowservice.v1.CreateWorkflowRuleRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.CreateWorkflowRuleResponse: + """Invokes the WorkflowService.create_workflow_rule rpc method.""" + return await self._client._rpc_call( + rpc="create_workflow_rule", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.CreateWorkflowRuleResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def delete_activity_execution( + self, + req: temporalio.api.workflowservice.v1.DeleteActivityExecutionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.DeleteActivityExecutionResponse: + """Invokes the WorkflowService.delete_activity_execution rpc method.""" + return await self._client._rpc_call( + rpc="delete_activity_execution", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.DeleteActivityExecutionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def delete_nexus_operation_execution( + self, + req: temporalio.api.workflowservice.v1.DeleteNexusOperationExecutionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.DeleteNexusOperationExecutionResponse: + """Invokes the WorkflowService.delete_nexus_operation_execution rpc method.""" + return await self._client._rpc_call( + rpc="delete_nexus_operation_execution", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.DeleteNexusOperationExecutionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def delete_schedule( + self, + req: temporalio.api.workflowservice.v1.DeleteScheduleRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.DeleteScheduleResponse: + """Invokes the WorkflowService.delete_schedule rpc method.""" + return await self._client._rpc_call( + rpc="delete_schedule", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.DeleteScheduleResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def delete_worker_deployment( + self, + req: temporalio.api.workflowservice.v1.DeleteWorkerDeploymentRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.DeleteWorkerDeploymentResponse: + """Invokes the WorkflowService.delete_worker_deployment rpc method.""" + return await self._client._rpc_call( + rpc="delete_worker_deployment", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.DeleteWorkerDeploymentResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def delete_worker_deployment_version( + self, + req: temporalio.api.workflowservice.v1.DeleteWorkerDeploymentVersionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.DeleteWorkerDeploymentVersionResponse: + """Invokes the WorkflowService.delete_worker_deployment_version rpc method.""" + return await self._client._rpc_call( + rpc="delete_worker_deployment_version", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.DeleteWorkerDeploymentVersionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def delete_workflow_execution( + self, + req: temporalio.api.workflowservice.v1.DeleteWorkflowExecutionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.DeleteWorkflowExecutionResponse: + """Invokes the WorkflowService.delete_workflow_execution rpc method.""" + return await self._client._rpc_call( + rpc="delete_workflow_execution", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.DeleteWorkflowExecutionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def delete_workflow_rule( + self, + req: temporalio.api.workflowservice.v1.DeleteWorkflowRuleRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.DeleteWorkflowRuleResponse: + """Invokes the WorkflowService.delete_workflow_rule rpc method.""" + return await self._client._rpc_call( + rpc="delete_workflow_rule", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.DeleteWorkflowRuleResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def deprecate_namespace( + self, + req: temporalio.api.workflowservice.v1.DeprecateNamespaceRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.DeprecateNamespaceResponse: + """Invokes the WorkflowService.deprecate_namespace rpc method.""" + return await self._client._rpc_call( + rpc="deprecate_namespace", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.DeprecateNamespaceResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def describe_activity_execution( + self, + req: temporalio.api.workflowservice.v1.DescribeActivityExecutionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.DescribeActivityExecutionResponse: + """Invokes the WorkflowService.describe_activity_execution rpc method.""" + return await self._client._rpc_call( + rpc="describe_activity_execution", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.DescribeActivityExecutionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def describe_batch_operation( + self, + req: temporalio.api.workflowservice.v1.DescribeBatchOperationRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.DescribeBatchOperationResponse: + """Invokes the WorkflowService.describe_batch_operation rpc method.""" + return await self._client._rpc_call( + rpc="describe_batch_operation", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.DescribeBatchOperationResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def describe_deployment( + self, + req: temporalio.api.workflowservice.v1.DescribeDeploymentRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.DescribeDeploymentResponse: + """Invokes the WorkflowService.describe_deployment rpc method.""" + return await self._client._rpc_call( + rpc="describe_deployment", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.DescribeDeploymentResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def describe_namespace( + self, + req: temporalio.api.workflowservice.v1.DescribeNamespaceRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.DescribeNamespaceResponse: + """Invokes the WorkflowService.describe_namespace rpc method.""" + return await self._client._rpc_call( + rpc="describe_namespace", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.DescribeNamespaceResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def describe_nexus_operation_execution( + self, + req: temporalio.api.workflowservice.v1.DescribeNexusOperationExecutionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.DescribeNexusOperationExecutionResponse: + """Invokes the WorkflowService.describe_nexus_operation_execution rpc method.""" + return await self._client._rpc_call( + rpc="describe_nexus_operation_execution", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.DescribeNexusOperationExecutionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def describe_schedule( + self, + req: temporalio.api.workflowservice.v1.DescribeScheduleRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.DescribeScheduleResponse: + """Invokes the WorkflowService.describe_schedule rpc method.""" + return await self._client._rpc_call( + rpc="describe_schedule", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.DescribeScheduleResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def describe_task_queue( + self, + req: temporalio.api.workflowservice.v1.DescribeTaskQueueRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.DescribeTaskQueueResponse: + """Invokes the WorkflowService.describe_task_queue rpc method.""" + return await self._client._rpc_call( + rpc="describe_task_queue", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.DescribeTaskQueueResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def describe_worker( + self, + req: temporalio.api.workflowservice.v1.DescribeWorkerRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.DescribeWorkerResponse: + """Invokes the WorkflowService.describe_worker rpc method.""" + return await self._client._rpc_call( + rpc="describe_worker", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.DescribeWorkerResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def describe_worker_deployment( + self, + req: temporalio.api.workflowservice.v1.DescribeWorkerDeploymentRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.DescribeWorkerDeploymentResponse: + """Invokes the WorkflowService.describe_worker_deployment rpc method.""" + return await self._client._rpc_call( + rpc="describe_worker_deployment", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.DescribeWorkerDeploymentResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def describe_worker_deployment_version( + self, + req: temporalio.api.workflowservice.v1.DescribeWorkerDeploymentVersionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.DescribeWorkerDeploymentVersionResponse: + """Invokes the WorkflowService.describe_worker_deployment_version rpc method.""" + return await self._client._rpc_call( + rpc="describe_worker_deployment_version", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.DescribeWorkerDeploymentVersionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def describe_workflow_execution( + self, + req: temporalio.api.workflowservice.v1.DescribeWorkflowExecutionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.DescribeWorkflowExecutionResponse: + """Invokes the WorkflowService.describe_workflow_execution rpc method.""" + return await self._client._rpc_call( + rpc="describe_workflow_execution", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.DescribeWorkflowExecutionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def describe_workflow_rule( + self, + req: temporalio.api.workflowservice.v1.DescribeWorkflowRuleRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.DescribeWorkflowRuleResponse: + """Invokes the WorkflowService.describe_workflow_rule rpc method.""" + return await self._client._rpc_call( + rpc="describe_workflow_rule", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.DescribeWorkflowRuleResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def execute_multi_operation( + self, + req: temporalio.api.workflowservice.v1.ExecuteMultiOperationRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.ExecuteMultiOperationResponse: + """Invokes the WorkflowService.execute_multi_operation rpc method.""" + return await self._client._rpc_call( + rpc="execute_multi_operation", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.ExecuteMultiOperationResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def fetch_worker_config( + self, + req: temporalio.api.workflowservice.v1.FetchWorkerConfigRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.FetchWorkerConfigResponse: + """Invokes the WorkflowService.fetch_worker_config rpc method.""" + return await self._client._rpc_call( + rpc="fetch_worker_config", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.FetchWorkerConfigResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_cluster_info( + self, + req: temporalio.api.workflowservice.v1.GetClusterInfoRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.GetClusterInfoResponse: + """Invokes the WorkflowService.get_cluster_info rpc method.""" + return await self._client._rpc_call( + rpc="get_cluster_info", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.GetClusterInfoResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_current_deployment( + self, + req: temporalio.api.workflowservice.v1.GetCurrentDeploymentRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.GetCurrentDeploymentResponse: + """Invokes the WorkflowService.get_current_deployment rpc method.""" + return await self._client._rpc_call( + rpc="get_current_deployment", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.GetCurrentDeploymentResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_deployment_reachability( + self, + req: temporalio.api.workflowservice.v1.GetDeploymentReachabilityRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.GetDeploymentReachabilityResponse: + """Invokes the WorkflowService.get_deployment_reachability rpc method.""" + return await self._client._rpc_call( + rpc="get_deployment_reachability", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.GetDeploymentReachabilityResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_search_attributes( + self, + req: temporalio.api.workflowservice.v1.GetSearchAttributesRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.GetSearchAttributesResponse: + """Invokes the WorkflowService.get_search_attributes rpc method.""" + return await self._client._rpc_call( + rpc="get_search_attributes", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.GetSearchAttributesResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_system_info( + self, + req: temporalio.api.workflowservice.v1.GetSystemInfoRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.GetSystemInfoResponse: + """Invokes the WorkflowService.get_system_info rpc method.""" + return await self._client._rpc_call( + rpc="get_system_info", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.GetSystemInfoResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_worker_build_id_compatibility( + self, + req: temporalio.api.workflowservice.v1.GetWorkerBuildIdCompatibilityRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.GetWorkerBuildIdCompatibilityResponse: + """Invokes the WorkflowService.get_worker_build_id_compatibility rpc method.""" + return await self._client._rpc_call( + rpc="get_worker_build_id_compatibility", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.GetWorkerBuildIdCompatibilityResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_worker_task_reachability( + self, + req: temporalio.api.workflowservice.v1.GetWorkerTaskReachabilityRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.GetWorkerTaskReachabilityResponse: + """Invokes the WorkflowService.get_worker_task_reachability rpc method.""" + return await self._client._rpc_call( + rpc="get_worker_task_reachability", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.GetWorkerTaskReachabilityResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_worker_versioning_rules( + self, + req: temporalio.api.workflowservice.v1.GetWorkerVersioningRulesRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.GetWorkerVersioningRulesResponse: + """Invokes the WorkflowService.get_worker_versioning_rules rpc method.""" + return await self._client._rpc_call( + rpc="get_worker_versioning_rules", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.GetWorkerVersioningRulesResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_workflow_execution_history( + self, + req: temporalio.api.workflowservice.v1.GetWorkflowExecutionHistoryRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.GetWorkflowExecutionHistoryResponse: + """Invokes the WorkflowService.get_workflow_execution_history rpc method.""" + return await self._client._rpc_call( + rpc="get_workflow_execution_history", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.GetWorkflowExecutionHistoryResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_workflow_execution_history_reverse( + self, + req: temporalio.api.workflowservice.v1.GetWorkflowExecutionHistoryReverseRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.GetWorkflowExecutionHistoryReverseResponse: + """Invokes the WorkflowService.get_workflow_execution_history_reverse rpc method.""" + return await self._client._rpc_call( + rpc="get_workflow_execution_history_reverse", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.GetWorkflowExecutionHistoryReverseResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def list_activity_executions( + self, + req: temporalio.api.workflowservice.v1.ListActivityExecutionsRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.ListActivityExecutionsResponse: + """Invokes the WorkflowService.list_activity_executions rpc method.""" + return await self._client._rpc_call( + rpc="list_activity_executions", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.ListActivityExecutionsResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def list_archived_workflow_executions( + self, + req: temporalio.api.workflowservice.v1.ListArchivedWorkflowExecutionsRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.ListArchivedWorkflowExecutionsResponse: + """Invokes the WorkflowService.list_archived_workflow_executions rpc method.""" + return await self._client._rpc_call( + rpc="list_archived_workflow_executions", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.ListArchivedWorkflowExecutionsResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def list_batch_operations( + self, + req: temporalio.api.workflowservice.v1.ListBatchOperationsRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.ListBatchOperationsResponse: + """Invokes the WorkflowService.list_batch_operations rpc method.""" + return await self._client._rpc_call( + rpc="list_batch_operations", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.ListBatchOperationsResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def list_closed_workflow_executions( + self, + req: temporalio.api.workflowservice.v1.ListClosedWorkflowExecutionsRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.ListClosedWorkflowExecutionsResponse: + """Invokes the WorkflowService.list_closed_workflow_executions rpc method.""" + return await self._client._rpc_call( + rpc="list_closed_workflow_executions", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.ListClosedWorkflowExecutionsResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def list_deployments( + self, + req: temporalio.api.workflowservice.v1.ListDeploymentsRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.ListDeploymentsResponse: + """Invokes the WorkflowService.list_deployments rpc method.""" + return await self._client._rpc_call( + rpc="list_deployments", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.ListDeploymentsResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def list_namespaces( + self, + req: temporalio.api.workflowservice.v1.ListNamespacesRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.ListNamespacesResponse: + """Invokes the WorkflowService.list_namespaces rpc method.""" + return await self._client._rpc_call( + rpc="list_namespaces", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.ListNamespacesResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def list_nexus_operation_executions( + self, + req: temporalio.api.workflowservice.v1.ListNexusOperationExecutionsRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.ListNexusOperationExecutionsResponse: + """Invokes the WorkflowService.list_nexus_operation_executions rpc method.""" + return await self._client._rpc_call( + rpc="list_nexus_operation_executions", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.ListNexusOperationExecutionsResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def list_open_workflow_executions( + self, + req: temporalio.api.workflowservice.v1.ListOpenWorkflowExecutionsRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.ListOpenWorkflowExecutionsResponse: + """Invokes the WorkflowService.list_open_workflow_executions rpc method.""" + return await self._client._rpc_call( + rpc="list_open_workflow_executions", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.ListOpenWorkflowExecutionsResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def list_schedule_matching_times( + self, + req: temporalio.api.workflowservice.v1.ListScheduleMatchingTimesRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.ListScheduleMatchingTimesResponse: + """Invokes the WorkflowService.list_schedule_matching_times rpc method.""" + return await self._client._rpc_call( + rpc="list_schedule_matching_times", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.ListScheduleMatchingTimesResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def list_schedules( + self, + req: temporalio.api.workflowservice.v1.ListSchedulesRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.ListSchedulesResponse: + """Invokes the WorkflowService.list_schedules rpc method.""" + return await self._client._rpc_call( + rpc="list_schedules", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.ListSchedulesResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def list_task_queue_partitions( + self, + req: temporalio.api.workflowservice.v1.ListTaskQueuePartitionsRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.ListTaskQueuePartitionsResponse: + """Invokes the WorkflowService.list_task_queue_partitions rpc method.""" + return await self._client._rpc_call( + rpc="list_task_queue_partitions", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.ListTaskQueuePartitionsResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def list_worker_deployments( + self, + req: temporalio.api.workflowservice.v1.ListWorkerDeploymentsRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.ListWorkerDeploymentsResponse: + """Invokes the WorkflowService.list_worker_deployments rpc method.""" + return await self._client._rpc_call( + rpc="list_worker_deployments", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.ListWorkerDeploymentsResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def list_workers( + self, + req: temporalio.api.workflowservice.v1.ListWorkersRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.ListWorkersResponse: + """Invokes the WorkflowService.list_workers rpc method.""" + return await self._client._rpc_call( + rpc="list_workers", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.ListWorkersResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def list_workflow_executions( + self, + req: temporalio.api.workflowservice.v1.ListWorkflowExecutionsRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.ListWorkflowExecutionsResponse: + """Invokes the WorkflowService.list_workflow_executions rpc method.""" + return await self._client._rpc_call( + rpc="list_workflow_executions", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.ListWorkflowExecutionsResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def list_workflow_rules( + self, + req: temporalio.api.workflowservice.v1.ListWorkflowRulesRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.ListWorkflowRulesResponse: + """Invokes the WorkflowService.list_workflow_rules rpc method.""" + return await self._client._rpc_call( + rpc="list_workflow_rules", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.ListWorkflowRulesResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def patch_schedule( + self, + req: temporalio.api.workflowservice.v1.PatchScheduleRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.PatchScheduleResponse: + """Invokes the WorkflowService.patch_schedule rpc method.""" + return await self._client._rpc_call( + rpc="patch_schedule", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.PatchScheduleResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def pause_activity( + self, + req: temporalio.api.workflowservice.v1.PauseActivityRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.PauseActivityResponse: + """Invokes the WorkflowService.pause_activity rpc method.""" + return await self._client._rpc_call( + rpc="pause_activity", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.PauseActivityResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def pause_activity_execution( + self, + req: temporalio.api.workflowservice.v1.PauseActivityExecutionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.PauseActivityExecutionResponse: + """Invokes the WorkflowService.pause_activity_execution rpc method.""" + return await self._client._rpc_call( + rpc="pause_activity_execution", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.PauseActivityExecutionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def pause_workflow_execution( + self, + req: temporalio.api.workflowservice.v1.PauseWorkflowExecutionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.PauseWorkflowExecutionResponse: + """Invokes the WorkflowService.pause_workflow_execution rpc method.""" + return await self._client._rpc_call( + rpc="pause_workflow_execution", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.PauseWorkflowExecutionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def poll_activity_execution( + self, + req: temporalio.api.workflowservice.v1.PollActivityExecutionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.PollActivityExecutionResponse: + """Invokes the WorkflowService.poll_activity_execution rpc method.""" + return await self._client._rpc_call( + rpc="poll_activity_execution", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.PollActivityExecutionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def poll_activity_task_queue( + self, + req: temporalio.api.workflowservice.v1.PollActivityTaskQueueRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.PollActivityTaskQueueResponse: + """Invokes the WorkflowService.poll_activity_task_queue rpc method.""" + return await self._client._rpc_call( + rpc="poll_activity_task_queue", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.PollActivityTaskQueueResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def poll_nexus_operation_execution( + self, + req: temporalio.api.workflowservice.v1.PollNexusOperationExecutionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.PollNexusOperationExecutionResponse: + """Invokes the WorkflowService.poll_nexus_operation_execution rpc method.""" + return await self._client._rpc_call( + rpc="poll_nexus_operation_execution", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.PollNexusOperationExecutionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def poll_nexus_task_queue( + self, + req: temporalio.api.workflowservice.v1.PollNexusTaskQueueRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.PollNexusTaskQueueResponse: + """Invokes the WorkflowService.poll_nexus_task_queue rpc method.""" + return await self._client._rpc_call( + rpc="poll_nexus_task_queue", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.PollNexusTaskQueueResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def poll_workflow_execution_time_skipping( + self, + req: temporalio.api.workflowservice.v1.PollWorkflowExecutionTimeSkippingRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.PollWorkflowExecutionTimeSkippingResponse: + """Invokes the WorkflowService.poll_workflow_execution_time_skipping rpc method.""" + return await self._client._rpc_call( + rpc="poll_workflow_execution_time_skipping", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.PollWorkflowExecutionTimeSkippingResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def poll_workflow_execution_update( + self, + req: temporalio.api.workflowservice.v1.PollWorkflowExecutionUpdateRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.PollWorkflowExecutionUpdateResponse: + """Invokes the WorkflowService.poll_workflow_execution_update rpc method.""" + return await self._client._rpc_call( + rpc="poll_workflow_execution_update", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.PollWorkflowExecutionUpdateResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def poll_workflow_task_queue( + self, + req: temporalio.api.workflowservice.v1.PollWorkflowTaskQueueRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.PollWorkflowTaskQueueResponse: + """Invokes the WorkflowService.poll_workflow_task_queue rpc method.""" + return await self._client._rpc_call( + rpc="poll_workflow_task_queue", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.PollWorkflowTaskQueueResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def query_workflow( + self, + req: temporalio.api.workflowservice.v1.QueryWorkflowRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.QueryWorkflowResponse: + """Invokes the WorkflowService.query_workflow rpc method.""" + return await self._client._rpc_call( + rpc="query_workflow", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.QueryWorkflowResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def record_activity_task_heartbeat( + self, + req: temporalio.api.workflowservice.v1.RecordActivityTaskHeartbeatRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.RecordActivityTaskHeartbeatResponse: + """Invokes the WorkflowService.record_activity_task_heartbeat rpc method.""" + return await self._client._rpc_call( + rpc="record_activity_task_heartbeat", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.RecordActivityTaskHeartbeatResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def record_activity_task_heartbeat_by_id( + self, + req: temporalio.api.workflowservice.v1.RecordActivityTaskHeartbeatByIdRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.RecordActivityTaskHeartbeatByIdResponse: + """Invokes the WorkflowService.record_activity_task_heartbeat_by_id rpc method.""" + return await self._client._rpc_call( + rpc="record_activity_task_heartbeat_by_id", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.RecordActivityTaskHeartbeatByIdResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def record_worker_heartbeat( + self, + req: temporalio.api.workflowservice.v1.RecordWorkerHeartbeatRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.RecordWorkerHeartbeatResponse: + """Invokes the WorkflowService.record_worker_heartbeat rpc method.""" + return await self._client._rpc_call( + rpc="record_worker_heartbeat", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.RecordWorkerHeartbeatResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def register_namespace( + self, + req: temporalio.api.workflowservice.v1.RegisterNamespaceRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.RegisterNamespaceResponse: + """Invokes the WorkflowService.register_namespace rpc method.""" + return await self._client._rpc_call( + rpc="register_namespace", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.RegisterNamespaceResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def request_cancel_activity_execution( + self, + req: temporalio.api.workflowservice.v1.RequestCancelActivityExecutionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.RequestCancelActivityExecutionResponse: + """Invokes the WorkflowService.request_cancel_activity_execution rpc method.""" + return await self._client._rpc_call( + rpc="request_cancel_activity_execution", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.RequestCancelActivityExecutionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def request_cancel_nexus_operation_execution( + self, + req: temporalio.api.workflowservice.v1.RequestCancelNexusOperationExecutionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.RequestCancelNexusOperationExecutionResponse: + """Invokes the WorkflowService.request_cancel_nexus_operation_execution rpc method.""" + return await self._client._rpc_call( + rpc="request_cancel_nexus_operation_execution", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.RequestCancelNexusOperationExecutionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def request_cancel_workflow_execution( + self, + req: temporalio.api.workflowservice.v1.RequestCancelWorkflowExecutionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.RequestCancelWorkflowExecutionResponse: + """Invokes the WorkflowService.request_cancel_workflow_execution rpc method.""" + return await self._client._rpc_call( + rpc="request_cancel_workflow_execution", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.RequestCancelWorkflowExecutionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def reset_activity( + self, + req: temporalio.api.workflowservice.v1.ResetActivityRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.ResetActivityResponse: + """Invokes the WorkflowService.reset_activity rpc method.""" + return await self._client._rpc_call( + rpc="reset_activity", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.ResetActivityResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def reset_activity_execution( + self, + req: temporalio.api.workflowservice.v1.ResetActivityExecutionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.ResetActivityExecutionResponse: + """Invokes the WorkflowService.reset_activity_execution rpc method.""" + return await self._client._rpc_call( + rpc="reset_activity_execution", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.ResetActivityExecutionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def reset_sticky_task_queue( + self, + req: temporalio.api.workflowservice.v1.ResetStickyTaskQueueRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.ResetStickyTaskQueueResponse: + """Invokes the WorkflowService.reset_sticky_task_queue rpc method.""" + return await self._client._rpc_call( + rpc="reset_sticky_task_queue", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.ResetStickyTaskQueueResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def reset_workflow_execution( + self, + req: temporalio.api.workflowservice.v1.ResetWorkflowExecutionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.ResetWorkflowExecutionResponse: + """Invokes the WorkflowService.reset_workflow_execution rpc method.""" + return await self._client._rpc_call( + rpc="reset_workflow_execution", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.ResetWorkflowExecutionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def respond_activity_task_canceled( + self, + req: temporalio.api.workflowservice.v1.RespondActivityTaskCanceledRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.RespondActivityTaskCanceledResponse: + """Invokes the WorkflowService.respond_activity_task_canceled rpc method.""" + return await self._client._rpc_call( + rpc="respond_activity_task_canceled", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.RespondActivityTaskCanceledResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def respond_activity_task_canceled_by_id( + self, + req: temporalio.api.workflowservice.v1.RespondActivityTaskCanceledByIdRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.RespondActivityTaskCanceledByIdResponse: + """Invokes the WorkflowService.respond_activity_task_canceled_by_id rpc method.""" + return await self._client._rpc_call( + rpc="respond_activity_task_canceled_by_id", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.RespondActivityTaskCanceledByIdResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def respond_activity_task_completed( + self, + req: temporalio.api.workflowservice.v1.RespondActivityTaskCompletedRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.RespondActivityTaskCompletedResponse: + """Invokes the WorkflowService.respond_activity_task_completed rpc method.""" + return await self._client._rpc_call( + rpc="respond_activity_task_completed", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.RespondActivityTaskCompletedResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def respond_activity_task_completed_by_id( + self, + req: temporalio.api.workflowservice.v1.RespondActivityTaskCompletedByIdRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.RespondActivityTaskCompletedByIdResponse: + """Invokes the WorkflowService.respond_activity_task_completed_by_id rpc method.""" + return await self._client._rpc_call( + rpc="respond_activity_task_completed_by_id", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.RespondActivityTaskCompletedByIdResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def respond_activity_task_failed( + self, + req: temporalio.api.workflowservice.v1.RespondActivityTaskFailedRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.RespondActivityTaskFailedResponse: + """Invokes the WorkflowService.respond_activity_task_failed rpc method.""" + return await self._client._rpc_call( + rpc="respond_activity_task_failed", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.RespondActivityTaskFailedResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def respond_activity_task_failed_by_id( + self, + req: temporalio.api.workflowservice.v1.RespondActivityTaskFailedByIdRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.RespondActivityTaskFailedByIdResponse: + """Invokes the WorkflowService.respond_activity_task_failed_by_id rpc method.""" + return await self._client._rpc_call( + rpc="respond_activity_task_failed_by_id", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.RespondActivityTaskFailedByIdResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def respond_nexus_task_completed( + self, + req: temporalio.api.workflowservice.v1.RespondNexusTaskCompletedRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.RespondNexusTaskCompletedResponse: + """Invokes the WorkflowService.respond_nexus_task_completed rpc method.""" + return await self._client._rpc_call( + rpc="respond_nexus_task_completed", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.RespondNexusTaskCompletedResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def respond_nexus_task_failed( + self, + req: temporalio.api.workflowservice.v1.RespondNexusTaskFailedRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.RespondNexusTaskFailedResponse: + """Invokes the WorkflowService.respond_nexus_task_failed rpc method.""" + return await self._client._rpc_call( + rpc="respond_nexus_task_failed", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.RespondNexusTaskFailedResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def respond_query_task_completed( + self, + req: temporalio.api.workflowservice.v1.RespondQueryTaskCompletedRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.RespondQueryTaskCompletedResponse: + """Invokes the WorkflowService.respond_query_task_completed rpc method.""" + return await self._client._rpc_call( + rpc="respond_query_task_completed", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.RespondQueryTaskCompletedResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def respond_workflow_task_completed( + self, + req: temporalio.api.workflowservice.v1.RespondWorkflowTaskCompletedRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.RespondWorkflowTaskCompletedResponse: + """Invokes the WorkflowService.respond_workflow_task_completed rpc method.""" + return await self._client._rpc_call( + rpc="respond_workflow_task_completed", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.RespondWorkflowTaskCompletedResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def respond_workflow_task_failed( + self, + req: temporalio.api.workflowservice.v1.RespondWorkflowTaskFailedRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.RespondWorkflowTaskFailedResponse: + """Invokes the WorkflowService.respond_workflow_task_failed rpc method.""" + return await self._client._rpc_call( + rpc="respond_workflow_task_failed", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.RespondWorkflowTaskFailedResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def scan_workflow_executions( + self, + req: temporalio.api.workflowservice.v1.ScanWorkflowExecutionsRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.ScanWorkflowExecutionsResponse: + """Invokes the WorkflowService.scan_workflow_executions rpc method.""" + return await self._client._rpc_call( + rpc="scan_workflow_executions", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.ScanWorkflowExecutionsResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def set_current_deployment( + self, + req: temporalio.api.workflowservice.v1.SetCurrentDeploymentRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.SetCurrentDeploymentResponse: + """Invokes the WorkflowService.set_current_deployment rpc method.""" + return await self._client._rpc_call( + rpc="set_current_deployment", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.SetCurrentDeploymentResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def set_worker_deployment_current_version( + self, + req: temporalio.api.workflowservice.v1.SetWorkerDeploymentCurrentVersionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.SetWorkerDeploymentCurrentVersionResponse: + """Invokes the WorkflowService.set_worker_deployment_current_version rpc method.""" + return await self._client._rpc_call( + rpc="set_worker_deployment_current_version", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.SetWorkerDeploymentCurrentVersionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def set_worker_deployment_manager( + self, + req: temporalio.api.workflowservice.v1.SetWorkerDeploymentManagerRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.SetWorkerDeploymentManagerResponse: + """Invokes the WorkflowService.set_worker_deployment_manager rpc method.""" + return await self._client._rpc_call( + rpc="set_worker_deployment_manager", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.SetWorkerDeploymentManagerResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def set_worker_deployment_ramping_version( + self, + req: temporalio.api.workflowservice.v1.SetWorkerDeploymentRampingVersionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.SetWorkerDeploymentRampingVersionResponse: + """Invokes the WorkflowService.set_worker_deployment_ramping_version rpc method.""" + return await self._client._rpc_call( + rpc="set_worker_deployment_ramping_version", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.SetWorkerDeploymentRampingVersionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def shutdown_worker( + self, + req: temporalio.api.workflowservice.v1.ShutdownWorkerRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.ShutdownWorkerResponse: + """Invokes the WorkflowService.shutdown_worker rpc method.""" + return await self._client._rpc_call( + rpc="shutdown_worker", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.ShutdownWorkerResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def signal_with_start_workflow_execution( + self, + req: temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse: + """Invokes the WorkflowService.signal_with_start_workflow_execution rpc method.""" + return await self._client._rpc_call( + rpc="signal_with_start_workflow_execution", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def signal_workflow_execution( + self, + req: temporalio.api.workflowservice.v1.SignalWorkflowExecutionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.SignalWorkflowExecutionResponse: + """Invokes the WorkflowService.signal_workflow_execution rpc method.""" + return await self._client._rpc_call( + rpc="signal_workflow_execution", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.SignalWorkflowExecutionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def start_activity_execution( + self, + req: temporalio.api.workflowservice.v1.StartActivityExecutionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.StartActivityExecutionResponse: + """Invokes the WorkflowService.start_activity_execution rpc method.""" + return await self._client._rpc_call( + rpc="start_activity_execution", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.StartActivityExecutionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def start_batch_operation( + self, + req: temporalio.api.workflowservice.v1.StartBatchOperationRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.StartBatchOperationResponse: + """Invokes the WorkflowService.start_batch_operation rpc method.""" + return await self._client._rpc_call( + rpc="start_batch_operation", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.StartBatchOperationResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def start_nexus_operation_execution( + self, + req: temporalio.api.workflowservice.v1.StartNexusOperationExecutionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.StartNexusOperationExecutionResponse: + """Invokes the WorkflowService.start_nexus_operation_execution rpc method.""" + return await self._client._rpc_call( + rpc="start_nexus_operation_execution", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.StartNexusOperationExecutionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def start_workflow_execution( + self, + req: temporalio.api.workflowservice.v1.StartWorkflowExecutionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse: + """Invokes the WorkflowService.start_workflow_execution rpc method.""" + return await self._client._rpc_call( + rpc="start_workflow_execution", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def stop_batch_operation( + self, + req: temporalio.api.workflowservice.v1.StopBatchOperationRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.StopBatchOperationResponse: + """Invokes the WorkflowService.stop_batch_operation rpc method.""" + return await self._client._rpc_call( + rpc="stop_batch_operation", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.StopBatchOperationResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def terminate_activity_execution( + self, + req: temporalio.api.workflowservice.v1.TerminateActivityExecutionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.TerminateActivityExecutionResponse: + """Invokes the WorkflowService.terminate_activity_execution rpc method.""" + return await self._client._rpc_call( + rpc="terminate_activity_execution", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.TerminateActivityExecutionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def terminate_nexus_operation_execution( + self, + req: temporalio.api.workflowservice.v1.TerminateNexusOperationExecutionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.TerminateNexusOperationExecutionResponse: + """Invokes the WorkflowService.terminate_nexus_operation_execution rpc method.""" + return await self._client._rpc_call( + rpc="terminate_nexus_operation_execution", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.TerminateNexusOperationExecutionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def terminate_workflow_execution( + self, + req: temporalio.api.workflowservice.v1.TerminateWorkflowExecutionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.TerminateWorkflowExecutionResponse: + """Invokes the WorkflowService.terminate_workflow_execution rpc method.""" + return await self._client._rpc_call( + rpc="terminate_workflow_execution", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.TerminateWorkflowExecutionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def trigger_workflow_rule( + self, + req: temporalio.api.workflowservice.v1.TriggerWorkflowRuleRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.TriggerWorkflowRuleResponse: + """Invokes the WorkflowService.trigger_workflow_rule rpc method.""" + return await self._client._rpc_call( + rpc="trigger_workflow_rule", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.TriggerWorkflowRuleResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def unpause_activity( + self, + req: temporalio.api.workflowservice.v1.UnpauseActivityRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.UnpauseActivityResponse: + """Invokes the WorkflowService.unpause_activity rpc method.""" + return await self._client._rpc_call( + rpc="unpause_activity", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.UnpauseActivityResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def unpause_activity_execution( + self, + req: temporalio.api.workflowservice.v1.UnpauseActivityExecutionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.UnpauseActivityExecutionResponse: + """Invokes the WorkflowService.unpause_activity_execution rpc method.""" + return await self._client._rpc_call( + rpc="unpause_activity_execution", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.UnpauseActivityExecutionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def unpause_workflow_execution( + self, + req: temporalio.api.workflowservice.v1.UnpauseWorkflowExecutionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.UnpauseWorkflowExecutionResponse: + """Invokes the WorkflowService.unpause_workflow_execution rpc method.""" + return await self._client._rpc_call( + rpc="unpause_workflow_execution", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.UnpauseWorkflowExecutionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def update_activity_execution_options( + self, + req: temporalio.api.workflowservice.v1.UpdateActivityExecutionOptionsRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.UpdateActivityExecutionOptionsResponse: + """Invokes the WorkflowService.update_activity_execution_options rpc method.""" + return await self._client._rpc_call( + rpc="update_activity_execution_options", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.UpdateActivityExecutionOptionsResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def update_activity_options( + self, + req: temporalio.api.workflowservice.v1.UpdateActivityOptionsRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.UpdateActivityOptionsResponse: + """Invokes the WorkflowService.update_activity_options rpc method.""" + return await self._client._rpc_call( + rpc="update_activity_options", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.UpdateActivityOptionsResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def update_namespace( + self, + req: temporalio.api.workflowservice.v1.UpdateNamespaceRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.UpdateNamespaceResponse: + """Invokes the WorkflowService.update_namespace rpc method.""" + return await self._client._rpc_call( + rpc="update_namespace", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.UpdateNamespaceResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def update_schedule( + self, + req: temporalio.api.workflowservice.v1.UpdateScheduleRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.UpdateScheduleResponse: + """Invokes the WorkflowService.update_schedule rpc method.""" + return await self._client._rpc_call( + rpc="update_schedule", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.UpdateScheduleResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def update_task_queue_config( + self, + req: temporalio.api.workflowservice.v1.UpdateTaskQueueConfigRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.UpdateTaskQueueConfigResponse: + """Invokes the WorkflowService.update_task_queue_config rpc method.""" + return await self._client._rpc_call( + rpc="update_task_queue_config", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.UpdateTaskQueueConfigResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def update_worker_build_id_compatibility( + self, + req: temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityResponse: + """Invokes the WorkflowService.update_worker_build_id_compatibility rpc method.""" + return await self._client._rpc_call( + rpc="update_worker_build_id_compatibility", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def update_worker_config( + self, + req: temporalio.api.workflowservice.v1.UpdateWorkerConfigRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.UpdateWorkerConfigResponse: + """Invokes the WorkflowService.update_worker_config rpc method.""" + return await self._client._rpc_call( + rpc="update_worker_config", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.UpdateWorkerConfigResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def update_worker_deployment_version_compute_config( + self, + req: temporalio.api.workflowservice.v1.UpdateWorkerDeploymentVersionComputeConfigRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.UpdateWorkerDeploymentVersionComputeConfigResponse: + """Invokes the WorkflowService.update_worker_deployment_version_compute_config rpc method.""" + return await self._client._rpc_call( + rpc="update_worker_deployment_version_compute_config", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.UpdateWorkerDeploymentVersionComputeConfigResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def update_worker_deployment_version_metadata( + self, + req: temporalio.api.workflowservice.v1.UpdateWorkerDeploymentVersionMetadataRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> ( + temporalio.api.workflowservice.v1.UpdateWorkerDeploymentVersionMetadataResponse + ): + """Invokes the WorkflowService.update_worker_deployment_version_metadata rpc method.""" + return await self._client._rpc_call( + rpc="update_worker_deployment_version_metadata", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.UpdateWorkerDeploymentVersionMetadataResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def update_worker_versioning_rules( + self, + req: temporalio.api.workflowservice.v1.UpdateWorkerVersioningRulesRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.UpdateWorkerVersioningRulesResponse: + """Invokes the WorkflowService.update_worker_versioning_rules rpc method.""" + return await self._client._rpc_call( + rpc="update_worker_versioning_rules", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.UpdateWorkerVersioningRulesResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def update_workflow_execution( + self, + req: temporalio.api.workflowservice.v1.UpdateWorkflowExecutionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.UpdateWorkflowExecutionResponse: + """Invokes the WorkflowService.update_workflow_execution rpc method.""" + return await self._client._rpc_call( + rpc="update_workflow_execution", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.UpdateWorkflowExecutionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def update_workflow_execution_options( + self, + req: temporalio.api.workflowservice.v1.UpdateWorkflowExecutionOptionsRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.UpdateWorkflowExecutionOptionsResponse: + """Invokes the WorkflowService.update_workflow_execution_options rpc method.""" + return await self._client._rpc_call( + rpc="update_workflow_execution_options", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.UpdateWorkflowExecutionOptionsResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def validate_worker_deployment_version_compute_config( + self, + req: temporalio.api.workflowservice.v1.ValidateWorkerDeploymentVersionComputeConfigRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.ValidateWorkerDeploymentVersionComputeConfigResponse: + """Invokes the WorkflowService.validate_worker_deployment_version_compute_config rpc method.""" + return await self._client._rpc_call( + rpc="validate_worker_deployment_version_compute_config", + req=req, + service=self._service, + resp_type=temporalio.api.workflowservice.v1.ValidateWorkerDeploymentVersionComputeConfigResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + +class OperatorService: + """RPC calls for the OperatorService.""" + + def __init__(self, client: ServiceClient): + """Initialize service with the provided ServiceClient.""" + self._client = client + self._service = "operator" + + async def add_or_update_remote_cluster( + self, + req: temporalio.api.operatorservice.v1.AddOrUpdateRemoteClusterRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.operatorservice.v1.AddOrUpdateRemoteClusterResponse: + """Invokes the OperatorService.add_or_update_remote_cluster rpc method.""" + return await self._client._rpc_call( + rpc="add_or_update_remote_cluster", + req=req, + service=self._service, + resp_type=temporalio.api.operatorservice.v1.AddOrUpdateRemoteClusterResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def add_search_attributes( + self, + req: temporalio.api.operatorservice.v1.AddSearchAttributesRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.operatorservice.v1.AddSearchAttributesResponse: + """Invokes the OperatorService.add_search_attributes rpc method.""" + return await self._client._rpc_call( + rpc="add_search_attributes", + req=req, + service=self._service, + resp_type=temporalio.api.operatorservice.v1.AddSearchAttributesResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def create_nexus_endpoint( + self, + req: temporalio.api.operatorservice.v1.CreateNexusEndpointRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.operatorservice.v1.CreateNexusEndpointResponse: + """Invokes the OperatorService.create_nexus_endpoint rpc method.""" + return await self._client._rpc_call( + rpc="create_nexus_endpoint", + req=req, + service=self._service, + resp_type=temporalio.api.operatorservice.v1.CreateNexusEndpointResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def delete_namespace( + self, + req: temporalio.api.operatorservice.v1.DeleteNamespaceRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.operatorservice.v1.DeleteNamespaceResponse: + """Invokes the OperatorService.delete_namespace rpc method.""" + return await self._client._rpc_call( + rpc="delete_namespace", + req=req, + service=self._service, + resp_type=temporalio.api.operatorservice.v1.DeleteNamespaceResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def delete_nexus_endpoint( + self, + req: temporalio.api.operatorservice.v1.DeleteNexusEndpointRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.operatorservice.v1.DeleteNexusEndpointResponse: + """Invokes the OperatorService.delete_nexus_endpoint rpc method.""" + return await self._client._rpc_call( + rpc="delete_nexus_endpoint", + req=req, + service=self._service, + resp_type=temporalio.api.operatorservice.v1.DeleteNexusEndpointResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_nexus_endpoint( + self, + req: temporalio.api.operatorservice.v1.GetNexusEndpointRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.operatorservice.v1.GetNexusEndpointResponse: + """Invokes the OperatorService.get_nexus_endpoint rpc method.""" + return await self._client._rpc_call( + rpc="get_nexus_endpoint", + req=req, + service=self._service, + resp_type=temporalio.api.operatorservice.v1.GetNexusEndpointResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def list_clusters( + self, + req: temporalio.api.operatorservice.v1.ListClustersRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.operatorservice.v1.ListClustersResponse: + """Invokes the OperatorService.list_clusters rpc method.""" + return await self._client._rpc_call( + rpc="list_clusters", + req=req, + service=self._service, + resp_type=temporalio.api.operatorservice.v1.ListClustersResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def list_nexus_endpoints( + self, + req: temporalio.api.operatorservice.v1.ListNexusEndpointsRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.operatorservice.v1.ListNexusEndpointsResponse: + """Invokes the OperatorService.list_nexus_endpoints rpc method.""" + return await self._client._rpc_call( + rpc="list_nexus_endpoints", + req=req, + service=self._service, + resp_type=temporalio.api.operatorservice.v1.ListNexusEndpointsResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def list_search_attributes( + self, + req: temporalio.api.operatorservice.v1.ListSearchAttributesRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.operatorservice.v1.ListSearchAttributesResponse: + """Invokes the OperatorService.list_search_attributes rpc method.""" + return await self._client._rpc_call( + rpc="list_search_attributes", + req=req, + service=self._service, + resp_type=temporalio.api.operatorservice.v1.ListSearchAttributesResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def remove_remote_cluster( + self, + req: temporalio.api.operatorservice.v1.RemoveRemoteClusterRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.operatorservice.v1.RemoveRemoteClusterResponse: + """Invokes the OperatorService.remove_remote_cluster rpc method.""" + return await self._client._rpc_call( + rpc="remove_remote_cluster", + req=req, + service=self._service, + resp_type=temporalio.api.operatorservice.v1.RemoveRemoteClusterResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def remove_search_attributes( + self, + req: temporalio.api.operatorservice.v1.RemoveSearchAttributesRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.operatorservice.v1.RemoveSearchAttributesResponse: + """Invokes the OperatorService.remove_search_attributes rpc method.""" + return await self._client._rpc_call( + rpc="remove_search_attributes", + req=req, + service=self._service, + resp_type=temporalio.api.operatorservice.v1.RemoveSearchAttributesResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def update_nexus_endpoint( + self, + req: temporalio.api.operatorservice.v1.UpdateNexusEndpointRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.operatorservice.v1.UpdateNexusEndpointResponse: + """Invokes the OperatorService.update_nexus_endpoint rpc method.""" + return await self._client._rpc_call( + rpc="update_nexus_endpoint", + req=req, + service=self._service, + resp_type=temporalio.api.operatorservice.v1.UpdateNexusEndpointResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + +class CloudService: + """RPC calls for the CloudService.""" + + def __init__(self, client: ServiceClient): + """Initialize service with the provided ServiceClient.""" + self._client = client + self._service = "cloud" + + async def add_namespace_region( + self, + req: temporalio.api.cloud.cloudservice.v1.AddNamespaceRegionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.AddNamespaceRegionResponse: + """Invokes the CloudService.add_namespace_region rpc method.""" + return await self._client._rpc_call( + rpc="add_namespace_region", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.AddNamespaceRegionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def add_user_group_member( + self, + req: temporalio.api.cloud.cloudservice.v1.AddUserGroupMemberRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.AddUserGroupMemberResponse: + """Invokes the CloudService.add_user_group_member rpc method.""" + return await self._client._rpc_call( + rpc="add_user_group_member", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.AddUserGroupMemberResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def create_account_audit_log_sink( + self, + req: temporalio.api.cloud.cloudservice.v1.CreateAccountAuditLogSinkRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.CreateAccountAuditLogSinkResponse: + """Invokes the CloudService.create_account_audit_log_sink rpc method.""" + return await self._client._rpc_call( + rpc="create_account_audit_log_sink", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.CreateAccountAuditLogSinkResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def create_api_key( + self, + req: temporalio.api.cloud.cloudservice.v1.CreateApiKeyRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.CreateApiKeyResponse: + """Invokes the CloudService.create_api_key rpc method.""" + return await self._client._rpc_call( + rpc="create_api_key", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.CreateApiKeyResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def create_billing_report( + self, + req: temporalio.api.cloud.cloudservice.v1.CreateBillingReportRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.CreateBillingReportResponse: + """Invokes the CloudService.create_billing_report rpc method.""" + return await self._client._rpc_call( + rpc="create_billing_report", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.CreateBillingReportResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def create_connectivity_rule( + self, + req: temporalio.api.cloud.cloudservice.v1.CreateConnectivityRuleRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.CreateConnectivityRuleResponse: + """Invokes the CloudService.create_connectivity_rule rpc method.""" + return await self._client._rpc_call( + rpc="create_connectivity_rule", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.CreateConnectivityRuleResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def create_custom_role( + self, + req: temporalio.api.cloud.cloudservice.v1.CreateCustomRoleRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.CreateCustomRoleResponse: + """Invokes the CloudService.create_custom_role rpc method.""" + return await self._client._rpc_call( + rpc="create_custom_role", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.CreateCustomRoleResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def create_namespace( + self, + req: temporalio.api.cloud.cloudservice.v1.CreateNamespaceRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.CreateNamespaceResponse: + """Invokes the CloudService.create_namespace rpc method.""" + return await self._client._rpc_call( + rpc="create_namespace", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.CreateNamespaceResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def create_namespace_export_sink( + self, + req: temporalio.api.cloud.cloudservice.v1.CreateNamespaceExportSinkRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.CreateNamespaceExportSinkResponse: + """Invokes the CloudService.create_namespace_export_sink rpc method.""" + return await self._client._rpc_call( + rpc="create_namespace_export_sink", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.CreateNamespaceExportSinkResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def create_nexus_endpoint( + self, + req: temporalio.api.cloud.cloudservice.v1.CreateNexusEndpointRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.CreateNexusEndpointResponse: + """Invokes the CloudService.create_nexus_endpoint rpc method.""" + return await self._client._rpc_call( + rpc="create_nexus_endpoint", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.CreateNexusEndpointResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def create_service_account( + self, + req: temporalio.api.cloud.cloudservice.v1.CreateServiceAccountRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.CreateServiceAccountResponse: + """Invokes the CloudService.create_service_account rpc method.""" + return await self._client._rpc_call( + rpc="create_service_account", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.CreateServiceAccountResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def create_user( + self, + req: temporalio.api.cloud.cloudservice.v1.CreateUserRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.CreateUserResponse: + """Invokes the CloudService.create_user rpc method.""" + return await self._client._rpc_call( + rpc="create_user", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.CreateUserResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def create_user_group( + self, + req: temporalio.api.cloud.cloudservice.v1.CreateUserGroupRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.CreateUserGroupResponse: + """Invokes the CloudService.create_user_group rpc method.""" + return await self._client._rpc_call( + rpc="create_user_group", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.CreateUserGroupResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def delete_account_audit_log_sink( + self, + req: temporalio.api.cloud.cloudservice.v1.DeleteAccountAuditLogSinkRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.DeleteAccountAuditLogSinkResponse: + """Invokes the CloudService.delete_account_audit_log_sink rpc method.""" + return await self._client._rpc_call( + rpc="delete_account_audit_log_sink", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.DeleteAccountAuditLogSinkResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def delete_api_key( + self, + req: temporalio.api.cloud.cloudservice.v1.DeleteApiKeyRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.DeleteApiKeyResponse: + """Invokes the CloudService.delete_api_key rpc method.""" + return await self._client._rpc_call( + rpc="delete_api_key", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.DeleteApiKeyResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def delete_connectivity_rule( + self, + req: temporalio.api.cloud.cloudservice.v1.DeleteConnectivityRuleRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.DeleteConnectivityRuleResponse: + """Invokes the CloudService.delete_connectivity_rule rpc method.""" + return await self._client._rpc_call( + rpc="delete_connectivity_rule", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.DeleteConnectivityRuleResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def delete_custom_role( + self, + req: temporalio.api.cloud.cloudservice.v1.DeleteCustomRoleRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.DeleteCustomRoleResponse: + """Invokes the CloudService.delete_custom_role rpc method.""" + return await self._client._rpc_call( + rpc="delete_custom_role", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.DeleteCustomRoleResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def delete_namespace( + self, + req: temporalio.api.cloud.cloudservice.v1.DeleteNamespaceRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.DeleteNamespaceResponse: + """Invokes the CloudService.delete_namespace rpc method.""" + return await self._client._rpc_call( + rpc="delete_namespace", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.DeleteNamespaceResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def delete_namespace_export_sink( + self, + req: temporalio.api.cloud.cloudservice.v1.DeleteNamespaceExportSinkRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.DeleteNamespaceExportSinkResponse: + """Invokes the CloudService.delete_namespace_export_sink rpc method.""" + return await self._client._rpc_call( + rpc="delete_namespace_export_sink", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.DeleteNamespaceExportSinkResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def delete_namespace_region( + self, + req: temporalio.api.cloud.cloudservice.v1.DeleteNamespaceRegionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.DeleteNamespaceRegionResponse: + """Invokes the CloudService.delete_namespace_region rpc method.""" + return await self._client._rpc_call( + rpc="delete_namespace_region", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.DeleteNamespaceRegionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def delete_nexus_endpoint( + self, + req: temporalio.api.cloud.cloudservice.v1.DeleteNexusEndpointRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.DeleteNexusEndpointResponse: + """Invokes the CloudService.delete_nexus_endpoint rpc method.""" + return await self._client._rpc_call( + rpc="delete_nexus_endpoint", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.DeleteNexusEndpointResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def delete_service_account( + self, + req: temporalio.api.cloud.cloudservice.v1.DeleteServiceAccountRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.DeleteServiceAccountResponse: + """Invokes the CloudService.delete_service_account rpc method.""" + return await self._client._rpc_call( + rpc="delete_service_account", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.DeleteServiceAccountResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def delete_user( + self, + req: temporalio.api.cloud.cloudservice.v1.DeleteUserRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.DeleteUserResponse: + """Invokes the CloudService.delete_user rpc method.""" + return await self._client._rpc_call( + rpc="delete_user", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.DeleteUserResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def delete_user_group( + self, + req: temporalio.api.cloud.cloudservice.v1.DeleteUserGroupRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.DeleteUserGroupResponse: + """Invokes the CloudService.delete_user_group rpc method.""" + return await self._client._rpc_call( + rpc="delete_user_group", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.DeleteUserGroupResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def failover_namespace_region( + self, + req: temporalio.api.cloud.cloudservice.v1.FailoverNamespaceRegionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.FailoverNamespaceRegionResponse: + """Invokes the CloudService.failover_namespace_region rpc method.""" + return await self._client._rpc_call( + rpc="failover_namespace_region", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.FailoverNamespaceRegionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_account( + self, + req: temporalio.api.cloud.cloudservice.v1.GetAccountRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetAccountResponse: + """Invokes the CloudService.get_account rpc method.""" + return await self._client._rpc_call( + rpc="get_account", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetAccountResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_account_audit_log_sink( + self, + req: temporalio.api.cloud.cloudservice.v1.GetAccountAuditLogSinkRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetAccountAuditLogSinkResponse: + """Invokes the CloudService.get_account_audit_log_sink rpc method.""" + return await self._client._rpc_call( + rpc="get_account_audit_log_sink", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetAccountAuditLogSinkResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_account_audit_log_sinks( + self, + req: temporalio.api.cloud.cloudservice.v1.GetAccountAuditLogSinksRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetAccountAuditLogSinksResponse: + """Invokes the CloudService.get_account_audit_log_sinks rpc method.""" + return await self._client._rpc_call( + rpc="get_account_audit_log_sinks", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetAccountAuditLogSinksResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_api_key( + self, + req: temporalio.api.cloud.cloudservice.v1.GetApiKeyRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetApiKeyResponse: + """Invokes the CloudService.get_api_key rpc method.""" + return await self._client._rpc_call( + rpc="get_api_key", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetApiKeyResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_api_keys( + self, + req: temporalio.api.cloud.cloudservice.v1.GetApiKeysRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetApiKeysResponse: + """Invokes the CloudService.get_api_keys rpc method.""" + return await self._client._rpc_call( + rpc="get_api_keys", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetApiKeysResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_async_operation( + self, + req: temporalio.api.cloud.cloudservice.v1.GetAsyncOperationRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetAsyncOperationResponse: + """Invokes the CloudService.get_async_operation rpc method.""" + return await self._client._rpc_call( + rpc="get_async_operation", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetAsyncOperationResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_audit_logs( + self, + req: temporalio.api.cloud.cloudservice.v1.GetAuditLogsRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetAuditLogsResponse: + """Invokes the CloudService.get_audit_logs rpc method.""" + return await self._client._rpc_call( + rpc="get_audit_logs", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetAuditLogsResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_billing_report( + self, + req: temporalio.api.cloud.cloudservice.v1.GetBillingReportRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetBillingReportResponse: + """Invokes the CloudService.get_billing_report rpc method.""" + return await self._client._rpc_call( + rpc="get_billing_report", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetBillingReportResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_connectivity_rule( + self, + req: temporalio.api.cloud.cloudservice.v1.GetConnectivityRuleRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetConnectivityRuleResponse: + """Invokes the CloudService.get_connectivity_rule rpc method.""" + return await self._client._rpc_call( + rpc="get_connectivity_rule", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetConnectivityRuleResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_connectivity_rules( + self, + req: temporalio.api.cloud.cloudservice.v1.GetConnectivityRulesRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetConnectivityRulesResponse: + """Invokes the CloudService.get_connectivity_rules rpc method.""" + return await self._client._rpc_call( + rpc="get_connectivity_rules", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetConnectivityRulesResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_current_identity( + self, + req: temporalio.api.cloud.cloudservice.v1.GetCurrentIdentityRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetCurrentIdentityResponse: + """Invokes the CloudService.get_current_identity rpc method.""" + return await self._client._rpc_call( + rpc="get_current_identity", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetCurrentIdentityResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_custom_role( + self, + req: temporalio.api.cloud.cloudservice.v1.GetCustomRoleRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetCustomRoleResponse: + """Invokes the CloudService.get_custom_role rpc method.""" + return await self._client._rpc_call( + rpc="get_custom_role", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetCustomRoleResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_custom_roles( + self, + req: temporalio.api.cloud.cloudservice.v1.GetCustomRolesRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetCustomRolesResponse: + """Invokes the CloudService.get_custom_roles rpc method.""" + return await self._client._rpc_call( + rpc="get_custom_roles", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetCustomRolesResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_namespace( + self, + req: temporalio.api.cloud.cloudservice.v1.GetNamespaceRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetNamespaceResponse: + """Invokes the CloudService.get_namespace rpc method.""" + return await self._client._rpc_call( + rpc="get_namespace", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetNamespaceResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_namespace_capacity_info( + self, + req: temporalio.api.cloud.cloudservice.v1.GetNamespaceCapacityInfoRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetNamespaceCapacityInfoResponse: + """Invokes the CloudService.get_namespace_capacity_info rpc method.""" + return await self._client._rpc_call( + rpc="get_namespace_capacity_info", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetNamespaceCapacityInfoResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_namespace_export_sink( + self, + req: temporalio.api.cloud.cloudservice.v1.GetNamespaceExportSinkRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetNamespaceExportSinkResponse: + """Invokes the CloudService.get_namespace_export_sink rpc method.""" + return await self._client._rpc_call( + rpc="get_namespace_export_sink", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetNamespaceExportSinkResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_namespace_export_sinks( + self, + req: temporalio.api.cloud.cloudservice.v1.GetNamespaceExportSinksRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetNamespaceExportSinksResponse: + """Invokes the CloudService.get_namespace_export_sinks rpc method.""" + return await self._client._rpc_call( + rpc="get_namespace_export_sinks", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetNamespaceExportSinksResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_namespaces( + self, + req: temporalio.api.cloud.cloudservice.v1.GetNamespacesRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetNamespacesResponse: + """Invokes the CloudService.get_namespaces rpc method.""" + return await self._client._rpc_call( + rpc="get_namespaces", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetNamespacesResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_nexus_endpoint( + self, + req: temporalio.api.cloud.cloudservice.v1.GetNexusEndpointRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetNexusEndpointResponse: + """Invokes the CloudService.get_nexus_endpoint rpc method.""" + return await self._client._rpc_call( + rpc="get_nexus_endpoint", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetNexusEndpointResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_nexus_endpoints( + self, + req: temporalio.api.cloud.cloudservice.v1.GetNexusEndpointsRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetNexusEndpointsResponse: + """Invokes the CloudService.get_nexus_endpoints rpc method.""" + return await self._client._rpc_call( + rpc="get_nexus_endpoints", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetNexusEndpointsResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_region( + self, + req: temporalio.api.cloud.cloudservice.v1.GetRegionRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetRegionResponse: + """Invokes the CloudService.get_region rpc method.""" + return await self._client._rpc_call( + rpc="get_region", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetRegionResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_regions( + self, + req: temporalio.api.cloud.cloudservice.v1.GetRegionsRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetRegionsResponse: + """Invokes the CloudService.get_regions rpc method.""" + return await self._client._rpc_call( + rpc="get_regions", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetRegionsResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_service_account( + self, + req: temporalio.api.cloud.cloudservice.v1.GetServiceAccountRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetServiceAccountResponse: + """Invokes the CloudService.get_service_account rpc method.""" + return await self._client._rpc_call( + rpc="get_service_account", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetServiceAccountResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_service_account_namespace_assignments( + self, + req: temporalio.api.cloud.cloudservice.v1.GetServiceAccountNamespaceAssignmentsRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetServiceAccountNamespaceAssignmentsResponse: + """Invokes the CloudService.get_service_account_namespace_assignments rpc method.""" + return await self._client._rpc_call( + rpc="get_service_account_namespace_assignments", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetServiceAccountNamespaceAssignmentsResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_service_accounts( + self, + req: temporalio.api.cloud.cloudservice.v1.GetServiceAccountsRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetServiceAccountsResponse: + """Invokes the CloudService.get_service_accounts rpc method.""" + return await self._client._rpc_call( + rpc="get_service_accounts", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetServiceAccountsResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_usage( + self, + req: temporalio.api.cloud.cloudservice.v1.GetUsageRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetUsageResponse: + """Invokes the CloudService.get_usage rpc method.""" + return await self._client._rpc_call( + rpc="get_usage", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetUsageResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_user( + self, + req: temporalio.api.cloud.cloudservice.v1.GetUserRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetUserResponse: + """Invokes the CloudService.get_user rpc method.""" + return await self._client._rpc_call( + rpc="get_user", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetUserResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_user_group( + self, + req: temporalio.api.cloud.cloudservice.v1.GetUserGroupRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetUserGroupResponse: + """Invokes the CloudService.get_user_group rpc method.""" + return await self._client._rpc_call( + rpc="get_user_group", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetUserGroupResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_user_group_members( + self, + req: temporalio.api.cloud.cloudservice.v1.GetUserGroupMembersRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetUserGroupMembersResponse: + """Invokes the CloudService.get_user_group_members rpc method.""" + return await self._client._rpc_call( + rpc="get_user_group_members", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetUserGroupMembersResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_user_group_namespace_assignments( + self, + req: temporalio.api.cloud.cloudservice.v1.GetUserGroupNamespaceAssignmentsRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetUserGroupNamespaceAssignmentsResponse: + """Invokes the CloudService.get_user_group_namespace_assignments rpc method.""" + return await self._client._rpc_call( + rpc="get_user_group_namespace_assignments", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetUserGroupNamespaceAssignmentsResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_user_groups( + self, + req: temporalio.api.cloud.cloudservice.v1.GetUserGroupsRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetUserGroupsResponse: + """Invokes the CloudService.get_user_groups rpc method.""" + return await self._client._rpc_call( + rpc="get_user_groups", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetUserGroupsResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_user_namespace_assignments( + self, + req: temporalio.api.cloud.cloudservice.v1.GetUserNamespaceAssignmentsRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetUserNamespaceAssignmentsResponse: + """Invokes the CloudService.get_user_namespace_assignments rpc method.""" + return await self._client._rpc_call( + rpc="get_user_namespace_assignments", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetUserNamespaceAssignmentsResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def get_users( + self, + req: temporalio.api.cloud.cloudservice.v1.GetUsersRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.GetUsersResponse: + """Invokes the CloudService.get_users rpc method.""" + return await self._client._rpc_call( + rpc="get_users", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.GetUsersResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def remove_user_group_member( + self, + req: temporalio.api.cloud.cloudservice.v1.RemoveUserGroupMemberRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.RemoveUserGroupMemberResponse: + """Invokes the CloudService.remove_user_group_member rpc method.""" + return await self._client._rpc_call( + rpc="remove_user_group_member", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.RemoveUserGroupMemberResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def rename_custom_search_attribute( + self, + req: temporalio.api.cloud.cloudservice.v1.RenameCustomSearchAttributeRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.RenameCustomSearchAttributeResponse: + """Invokes the CloudService.rename_custom_search_attribute rpc method.""" + return await self._client._rpc_call( + rpc="rename_custom_search_attribute", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.RenameCustomSearchAttributeResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def set_service_account_namespace_access( + self, + req: temporalio.api.cloud.cloudservice.v1.SetServiceAccountNamespaceAccessRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.SetServiceAccountNamespaceAccessResponse: + """Invokes the CloudService.set_service_account_namespace_access rpc method.""" + return await self._client._rpc_call( + rpc="set_service_account_namespace_access", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.SetServiceAccountNamespaceAccessResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def set_user_group_namespace_access( + self, + req: temporalio.api.cloud.cloudservice.v1.SetUserGroupNamespaceAccessRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.SetUserGroupNamespaceAccessResponse: + """Invokes the CloudService.set_user_group_namespace_access rpc method.""" + return await self._client._rpc_call( + rpc="set_user_group_namespace_access", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.SetUserGroupNamespaceAccessResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def set_user_namespace_access( + self, + req: temporalio.api.cloud.cloudservice.v1.SetUserNamespaceAccessRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.SetUserNamespaceAccessResponse: + """Invokes the CloudService.set_user_namespace_access rpc method.""" + return await self._client._rpc_call( + rpc="set_user_namespace_access", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.SetUserNamespaceAccessResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def update_account( + self, + req: temporalio.api.cloud.cloudservice.v1.UpdateAccountRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.UpdateAccountResponse: + """Invokes the CloudService.update_account rpc method.""" + return await self._client._rpc_call( + rpc="update_account", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.UpdateAccountResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def update_account_audit_log_sink( + self, + req: temporalio.api.cloud.cloudservice.v1.UpdateAccountAuditLogSinkRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.UpdateAccountAuditLogSinkResponse: + """Invokes the CloudService.update_account_audit_log_sink rpc method.""" + return await self._client._rpc_call( + rpc="update_account_audit_log_sink", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.UpdateAccountAuditLogSinkResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def update_api_key( + self, + req: temporalio.api.cloud.cloudservice.v1.UpdateApiKeyRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.UpdateApiKeyResponse: + """Invokes the CloudService.update_api_key rpc method.""" + return await self._client._rpc_call( + rpc="update_api_key", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.UpdateApiKeyResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def update_custom_role( + self, + req: temporalio.api.cloud.cloudservice.v1.UpdateCustomRoleRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.UpdateCustomRoleResponse: + """Invokes the CloudService.update_custom_role rpc method.""" + return await self._client._rpc_call( + rpc="update_custom_role", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.UpdateCustomRoleResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def update_namespace( + self, + req: temporalio.api.cloud.cloudservice.v1.UpdateNamespaceRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.UpdateNamespaceResponse: + """Invokes the CloudService.update_namespace rpc method.""" + return await self._client._rpc_call( + rpc="update_namespace", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.UpdateNamespaceResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def update_namespace_export_sink( + self, + req: temporalio.api.cloud.cloudservice.v1.UpdateNamespaceExportSinkRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.UpdateNamespaceExportSinkResponse: + """Invokes the CloudService.update_namespace_export_sink rpc method.""" + return await self._client._rpc_call( + rpc="update_namespace_export_sink", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.UpdateNamespaceExportSinkResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def update_namespace_tags( + self, + req: temporalio.api.cloud.cloudservice.v1.UpdateNamespaceTagsRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.UpdateNamespaceTagsResponse: + """Invokes the CloudService.update_namespace_tags rpc method.""" + return await self._client._rpc_call( + rpc="update_namespace_tags", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.UpdateNamespaceTagsResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def update_nexus_endpoint( + self, + req: temporalio.api.cloud.cloudservice.v1.UpdateNexusEndpointRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.UpdateNexusEndpointResponse: + """Invokes the CloudService.update_nexus_endpoint rpc method.""" + return await self._client._rpc_call( + rpc="update_nexus_endpoint", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.UpdateNexusEndpointResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def update_service_account( + self, + req: temporalio.api.cloud.cloudservice.v1.UpdateServiceAccountRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.UpdateServiceAccountResponse: + """Invokes the CloudService.update_service_account rpc method.""" + return await self._client._rpc_call( + rpc="update_service_account", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.UpdateServiceAccountResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def update_user( + self, + req: temporalio.api.cloud.cloudservice.v1.UpdateUserRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.UpdateUserResponse: + """Invokes the CloudService.update_user rpc method.""" + return await self._client._rpc_call( + rpc="update_user", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.UpdateUserResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def update_user_group( + self, + req: temporalio.api.cloud.cloudservice.v1.UpdateUserGroupRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.UpdateUserGroupResponse: + """Invokes the CloudService.update_user_group rpc method.""" + return await self._client._rpc_call( + rpc="update_user_group", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.UpdateUserGroupResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def validate_account_audit_log_sink( + self, + req: temporalio.api.cloud.cloudservice.v1.ValidateAccountAuditLogSinkRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.ValidateAccountAuditLogSinkResponse: + """Invokes the CloudService.validate_account_audit_log_sink rpc method.""" + return await self._client._rpc_call( + rpc="validate_account_audit_log_sink", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.ValidateAccountAuditLogSinkResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def validate_namespace_export_sink( + self, + req: temporalio.api.cloud.cloudservice.v1.ValidateNamespaceExportSinkRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.cloud.cloudservice.v1.ValidateNamespaceExportSinkResponse: + """Invokes the CloudService.validate_namespace_export_sink rpc method.""" + return await self._client._rpc_call( + rpc="validate_namespace_export_sink", + req=req, + service=self._service, + resp_type=temporalio.api.cloud.cloudservice.v1.ValidateNamespaceExportSinkResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + +class TestService: + """RPC calls for the TestService.""" + + def __init__(self, client: ServiceClient): + """Initialize service with the provided ServiceClient.""" + self._client = client + self._service = "test" + + async def get_current_time( + self, + req: google.protobuf.empty_pb2.Empty, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.testservice.v1.GetCurrentTimeResponse: + """Invokes the TestService.get_current_time rpc method.""" + return await self._client._rpc_call( + rpc="get_current_time", + req=req, + service=self._service, + resp_type=temporalio.api.testservice.v1.GetCurrentTimeResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def lock_time_skipping( + self, + req: temporalio.api.testservice.v1.LockTimeSkippingRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.testservice.v1.LockTimeSkippingResponse: + """Invokes the TestService.lock_time_skipping rpc method.""" + return await self._client._rpc_call( + rpc="lock_time_skipping", + req=req, + service=self._service, + resp_type=temporalio.api.testservice.v1.LockTimeSkippingResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def sleep( + self, + req: temporalio.api.testservice.v1.SleepRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.testservice.v1.SleepResponse: + """Invokes the TestService.sleep rpc method.""" + return await self._client._rpc_call( + rpc="sleep", + req=req, + service=self._service, + resp_type=temporalio.api.testservice.v1.SleepResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def sleep_until( + self, + req: temporalio.api.testservice.v1.SleepUntilRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.testservice.v1.SleepResponse: + """Invokes the TestService.sleep_until rpc method.""" + return await self._client._rpc_call( + rpc="sleep_until", + req=req, + service=self._service, + resp_type=temporalio.api.testservice.v1.SleepResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def unlock_time_skipping( + self, + req: temporalio.api.testservice.v1.UnlockTimeSkippingRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.testservice.v1.UnlockTimeSkippingResponse: + """Invokes the TestService.unlock_time_skipping rpc method.""" + return await self._client._rpc_call( + rpc="unlock_time_skipping", + req=req, + service=self._service, + resp_type=temporalio.api.testservice.v1.UnlockTimeSkippingResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + async def unlock_time_skipping_with_sleep( + self, + req: temporalio.api.testservice.v1.SleepRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.testservice.v1.SleepResponse: + """Invokes the TestService.unlock_time_skipping_with_sleep rpc method.""" + return await self._client._rpc_call( + rpc="unlock_time_skipping_with_sleep", + req=req, + service=self._service, + resp_type=temporalio.api.testservice.v1.SleepResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) + + +class HealthService: + """RPC calls for the HealthService.""" + + def __init__(self, client: ServiceClient): + """Initialize service with the provided ServiceClient.""" + self._client = client + self._service = "health" + + async def check( + self, + req: temporalio.bridge.proto.health.v1.HealthCheckRequest, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.bridge.proto.health.v1.HealthCheckResponse: + """Invokes the HealthService.check rpc method.""" + return await self._client._rpc_call( + rpc="check", + req=req, + service=self._service, + resp_type=temporalio.bridge.proto.health.v1.HealthCheckResponse, + retry=retry, + metadata=metadata, + timeout=timeout, + ) diff --git a/temporalio/bridge/src/client.rs b/temporalio/bridge/src/client.rs index 2f4ab867e..aec5c0ef8 100644 --- a/temporalio/bridge/src/client.rs +++ b/temporalio/bridge/src/client.rs @@ -2,25 +2,35 @@ use pyo3::exceptions::{PyException, PyRuntimeError, PyValueError}; use pyo3::prelude::*; use std::collections::HashMap; use std::str::FromStr; +use std::sync::Arc; use std::time::Duration; -use temporal_client::{ - ClientKeepAliveConfig as CoreClientKeepAliveConfig, ClientOptions, ClientOptionsBuilder, - ConfiguredClient, HealthService, HttpConnectProxyOptions, RetryClient, RetryConfig, - TemporalServiceClientWithMetrics, TestService, TlsConfig, WorkflowService, +use temporalio_client::tonic::{ + self, + metadata::{AsciiMetadataKey, AsciiMetadataValue, BinaryMetadataKey, BinaryMetadataValue}, }; -use tonic::metadata::MetadataKey; +use temporalio_client::{ + ClientKeepAliveOptions as CoreClientKeepAliveConfig, Connection, ConnectionOptions, + DnsLoadBalancingOptions, GrpcCompression, HttpConnectProxyOptions, RetryOptions, +}; +use tokio_rustls::rustls::client::danger::{ + HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier, +}; +use tokio_rustls::rustls::client::WebPkiServerVerifier; +use tokio_rustls::rustls::crypto::CryptoProvider; +use tokio_rustls::rustls::pki_types::pem::PemObject; +use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime}; +use tokio_rustls::rustls::{self, DigitallySignedStruct, RootCertStore, SignatureScheme}; +use tracing::warn; use url::Url; use crate::runtime; pyo3::create_exception!(temporal_sdk_bridge, RPCError, PyException); -type Client = RetryClient>; - #[pyclass] pub struct ClientRef { - pub(crate) retry_client: Client, - runtime: runtime::Runtime, + pub(crate) connection: Connection, + pub(crate) runtime: runtime::Runtime, } #[derive(FromPyObject)] @@ -28,13 +38,17 @@ pub struct ClientConfig { target_url: String, client_name: String, client_version: String, - metadata: HashMap, + metadata: HashMap, api_key: Option, identity: String, tls_config: Option, retry_config: Option, keep_alive_config: Option, http_connect_proxy_config: Option, + dns_load_balancing_config: Option, + grpc_compression: String, + payloads_warn_size: u64, + memo_warn_size: u64, } #[derive(FromPyObject)] @@ -43,6 +57,7 @@ struct ClientTlsConfig { domain: Option, client_cert: Option>, client_private_key: Option>, + verification_server_name: Option, } #[derive(FromPyObject)] @@ -68,25 +83,43 @@ struct ClientHttpConnectProxyConfig { } #[derive(FromPyObject)] -struct RpcCall { - rpc: String, +struct ClientDnsLoadBalancingConfig { + pub resolution_interval_millis: u64, +} + +#[derive(FromPyObject)] +pub(crate) struct RpcCall { + pub(crate) rpc: String, req: Vec, - retry: bool, - metadata: HashMap, + pub(crate) retry: bool, + metadata: HashMap, timeout_millis: Option, } +#[derive(FromPyObject)] +enum RpcMetadataValue { + #[pyo3(transparent, annotation = "str")] + Str(String), + #[pyo3(transparent, annotation = "bytes")] + Bytes(Vec), +} + pub fn connect_client<'a>( py: Python<'a>, runtime_ref: &runtime::RuntimeRef, config: ClientConfig, ) -> PyResult> { - let opts: ClientOptions = config.try_into()?; + let metrics_meter = runtime_ref + .runtime + .core + .telemetry() + .get_temporal_metric_meter(); + let opts = config.into_connection_options(metrics_meter)?; + runtime_ref.runtime.assert_same_process("create client")?; let runtime = runtime_ref.runtime.clone(); runtime_ref.runtime.future_into_py(py, async move { Ok(ClientRef { - retry_client: opts - .connect_no_namespace(runtime.core.telemetry().get_temporal_metric_meter()) + connection: Connection::connect(opts) .await .map_err(|err| PyRuntimeError::new_err(format!("Failed client connect: {err}")))?, runtime, @@ -94,454 +127,73 @@ pub fn connect_client<'a>( }) } +#[macro_export] macro_rules! rpc_call { - ($retry_client:ident, $call:ident, $call_name:ident) => { + ($connection:ident, $call:ident, $trait:tt, $service_method:ident, $call_name:ident) => { if $call.retry { - rpc_resp($retry_client.$call_name(rpc_req($call)?).await) + rpc_resp($trait::$call_name(&mut $connection, rpc_req($call)?).await) } else { - rpc_resp($retry_client.into_inner().$call_name(rpc_req($call)?).await) - } - }; -} - -macro_rules! rpc_call_on_trait { - ($retry_client:ident, $call:ident, $trait:tt, $call_name:ident) => { - if $call.retry { - rpc_resp($trait::$call_name(&mut $retry_client, rpc_req($call)?).await) - } else { - rpc_resp($trait::$call_name(&mut $retry_client.into_inner(), rpc_req($call)?).await) + rpc_resp( + $connection + .$service_method() + .$call_name(rpc_req($call)?) + .await, + ) } }; } #[pymethods] impl ClientRef { - fn update_metadata(&self, headers: HashMap) { - self.retry_client.get_client().set_headers(headers); - } + fn update_metadata(&self, headers: HashMap) -> PyResult<()> { + let (ascii_headers, binary_headers) = partition_headers(headers); - fn update_api_key(&self, api_key: Option) { - self.retry_client.get_client().set_api_key(api_key); - } + self.connection + .set_headers(ascii_headers) + .map_err(|err| PyValueError::new_err(err.to_string()))?; + self.connection + .set_binary_headers(binary_headers) + .map_err(|err| PyValueError::new_err(err.to_string()))?; - fn call_workflow_service<'p>( - &self, - py: Python<'p>, - call: RpcCall, - ) -> PyResult> { - let mut retry_client = self.retry_client.clone(); - self.runtime.future_into_py(py, async move { - let bytes = match call.rpc.as_str() { - "count_workflow_executions" => { - rpc_call!(retry_client, call, count_workflow_executions) - } - "create_schedule" => { - rpc_call!(retry_client, call, create_schedule) - } - "create_workflow_rule" => { - rpc_call!(retry_client, call, create_workflow_rule) - } - "delete_schedule" => { - rpc_call!(retry_client, call, delete_schedule) - } - "delete_worker_deployment" => { - rpc_call!(retry_client, call, delete_worker_deployment) - } - "delete_worker_deployment_version" => { - rpc_call!(retry_client, call, delete_worker_deployment_version) - } - "delete_workflow_execution" => { - rpc_call!(retry_client, call, delete_workflow_execution) - } - "delete_workflow_rule" => { - rpc_call!(retry_client, call, delete_workflow_rule) - } - "describe_batch_operation" => { - rpc_call!(retry_client, call, describe_batch_operation) - } - "describe_deployment" => { - rpc_call!(retry_client, call, describe_deployment) - } - "deprecate_namespace" => rpc_call!(retry_client, call, deprecate_namespace), - "describe_namespace" => rpc_call!(retry_client, call, describe_namespace), - "describe_schedule" => rpc_call!(retry_client, call, describe_schedule), - "describe_task_queue" => rpc_call!(retry_client, call, describe_task_queue), - "describe_worker_deployment" => { - rpc_call!(retry_client, call, describe_worker_deployment) - } - "describe_worker_deployment_version" => { - rpc_call!(retry_client, call, describe_worker_deployment_version) - } - "describe_workflow_execution" => { - rpc_call!(retry_client, call, describe_workflow_execution) - } - "describe_workflow_rule" => { - rpc_call!(retry_client, call, describe_workflow_rule) - } - "execute_multi_operation" => rpc_call!(retry_client, call, execute_multi_operation), - "fetch_worker_config" => rpc_call!(retry_client, call, fetch_worker_config), - "get_cluster_info" => rpc_call!(retry_client, call, get_cluster_info), - "get_current_deployment" => rpc_call!(retry_client, call, get_current_deployment), - "get_deployment_reachability" => { - rpc_call!(retry_client, call, get_deployment_reachability) - } - "get_search_attributes" => { - rpc_call!(retry_client, call, get_search_attributes) - } - "get_system_info" => rpc_call!(retry_client, call, get_system_info), - "get_worker_build_id_compatibility" => { - rpc_call!(retry_client, call, get_worker_build_id_compatibility) - } - "get_worker_task_reachability" => { - rpc_call!(retry_client, call, get_worker_task_reachability) - } - "get_worker_versioning_rules" => { - rpc_call!(retry_client, call, get_worker_versioning_rules) - } - "get_workflow_execution_history" => { - rpc_call!(retry_client, call, get_workflow_execution_history) - } - "get_workflow_execution_history_reverse" => { - rpc_call!(retry_client, call, get_workflow_execution_history_reverse) - } - "list_archived_workflow_executions" => { - rpc_call!(retry_client, call, list_archived_workflow_executions) - } - "list_closed_workflow_executions" => { - rpc_call!(retry_client, call, list_closed_workflow_executions) - } - "list_deployments" => { - rpc_call!(retry_client, call, list_deployments) - } - "list_namespaces" => rpc_call!(retry_client, call, list_namespaces), - "list_open_workflow_executions" => { - rpc_call!(retry_client, call, list_open_workflow_executions) - } - "list_schedule_matching_times" => { - rpc_call!(retry_client, call, list_schedule_matching_times) - } - "list_schedules" => { - rpc_call!(retry_client, call, list_schedules) - } - "list_task_queue_partitions" => { - rpc_call!(retry_client, call, list_task_queue_partitions) - } - "list_worker_deployments" => { - rpc_call!(retry_client, call, list_worker_deployments) - } - "list_workflow_executions" => { - rpc_call!(retry_client, call, list_workflow_executions) - } - "list_workflow_rules" => { - rpc_call!(retry_client, call, list_workflow_rules) - } - "patch_schedule" => { - rpc_call!(retry_client, call, patch_schedule) - } - "pause_activity" => { - rpc_call!(retry_client, call, pause_activity) - } - "poll_activity_task_queue" => { - rpc_call!(retry_client, call, poll_activity_task_queue) - } - "poll_nexus_task_queue" => rpc_call!(retry_client, call, poll_nexus_task_queue), - "poll_workflow_execution_update" => { - rpc_call!(retry_client, call, poll_workflow_execution_update) - } - "poll_workflow_task_queue" => { - rpc_call!(retry_client, call, poll_workflow_task_queue) - } - "query_workflow" => rpc_call!(retry_client, call, query_workflow), - "record_activity_task_heartbeat" => { - rpc_call!(retry_client, call, record_activity_task_heartbeat) - } - "record_activity_task_heartbeat_by_id" => { - rpc_call!(retry_client, call, record_activity_task_heartbeat_by_id) - } - "register_namespace" => rpc_call!(retry_client, call, register_namespace), - "request_cancel_workflow_execution" => { - rpc_call!(retry_client, call, request_cancel_workflow_execution) - } - "reset_activity" => { - rpc_call!(retry_client, call, reset_activity) - } - "reset_sticky_task_queue" => { - rpc_call!(retry_client, call, reset_sticky_task_queue) - } - "reset_workflow_execution" => { - rpc_call!(retry_client, call, reset_workflow_execution) - } - "respond_activity_task_canceled" => { - rpc_call!(retry_client, call, respond_activity_task_canceled) - } - "respond_activity_task_canceled_by_id" => { - rpc_call!(retry_client, call, respond_activity_task_canceled_by_id) - } - "respond_activity_task_completed" => { - rpc_call!(retry_client, call, respond_activity_task_completed) - } - "respond_activity_task_completed_by_id" => { - rpc_call!(retry_client, call, respond_activity_task_completed_by_id) - } - "respond_activity_task_failed" => { - rpc_call!(retry_client, call, respond_activity_task_failed) - } - "respond_activity_task_failed_by_id" => { - rpc_call!(retry_client, call, respond_activity_task_failed_by_id) - } - "respond_nexus_task_completed" => { - rpc_call!(retry_client, call, respond_nexus_task_completed) - } - "respond_nexus_task_failed" => { - rpc_call!(retry_client, call, respond_nexus_task_failed) - } - "respond_query_task_completed" => { - rpc_call!(retry_client, call, respond_query_task_completed) - } - "respond_workflow_task_completed" => { - rpc_call!(retry_client, call, respond_workflow_task_completed) - } - "respond_workflow_task_failed" => { - rpc_call!(retry_client, call, respond_workflow_task_failed) - } - "scan_workflow_executions" => { - rpc_call!(retry_client, call, scan_workflow_executions) - } - "set_current_deployment" => { - rpc_call!(retry_client, call, set_current_deployment) - } - "set_worker_deployment_current_version" => { - rpc_call!(retry_client, call, set_worker_deployment_current_version) - } - "set_worker_deployment_ramping_version" => { - rpc_call!(retry_client, call, set_worker_deployment_ramping_version) - } - "shutdown_worker" => { - rpc_call!(retry_client, call, shutdown_worker) - } - "signal_with_start_workflow_execution" => { - rpc_call!(retry_client, call, signal_with_start_workflow_execution) - } - "signal_workflow_execution" => { - rpc_call!(retry_client, call, signal_workflow_execution) - } - "start_workflow_execution" => { - rpc_call!(retry_client, call, start_workflow_execution) - } - "terminate_workflow_execution" => { - rpc_call!(retry_client, call, terminate_workflow_execution) - } - "trigger_workflow_rule" => { - rpc_call!(retry_client, call, trigger_workflow_rule) - } - "unpause_activity" => { - rpc_call!(retry_client, call, unpause_activity) - } - "update_namespace" => { - rpc_call_on_trait!(retry_client, call, WorkflowService, update_namespace) - } - "update_schedule" => rpc_call!(retry_client, call, update_schedule), - "update_task_queue_config" => { - rpc_call!(retry_client, call, update_task_queue_config) - } - "update_worker_config" => rpc_call!(retry_client, call, update_worker_config), - "update_worker_deployment_version_metadata" => { - rpc_call!( - retry_client, - call, - update_worker_deployment_version_metadata - ) - } - "update_worker_build_id_compatibility" => { - rpc_call!(retry_client, call, update_worker_build_id_compatibility) - } - "update_worker_versioning_rules" => { - rpc_call!(retry_client, call, update_worker_versioning_rules) - } - "update_workflow_execution" => { - rpc_call!(retry_client, call, update_workflow_execution) - } - "update_workflow_execution_options" => { - rpc_call!(retry_client, call, update_workflow_execution_options) - } - _ => { - return Err(PyValueError::new_err(format!( - "Unknown RPC call {}", - call.rpc - ))) - } - }?; - Ok(bytes) - }) - } - - fn call_operator_service<'p>( - &self, - py: Python<'p>, - call: RpcCall, - ) -> PyResult> { - use temporal_client::OperatorService; - - let mut retry_client = self.retry_client.clone(); - self.runtime.future_into_py(py, async move { - let bytes = match call.rpc.as_str() { - "add_or_update_remote_cluster" => { - rpc_call!(retry_client, call, add_or_update_remote_cluster) - } - "add_search_attributes" => { - rpc_call!(retry_client, call, add_search_attributes) - } - "create_nexus_endpoint" => rpc_call!(retry_client, call, create_nexus_endpoint), - "delete_namespace" => { - rpc_call_on_trait!(retry_client, call, OperatorService, delete_namespace) - } - "delete_nexus_endpoint" => rpc_call!(retry_client, call, delete_nexus_endpoint), - "get_nexus_endpoint" => rpc_call!(retry_client, call, get_nexus_endpoint), - "list_clusters" => rpc_call!(retry_client, call, list_clusters), - "list_nexus_endpoints" => rpc_call!(retry_client, call, list_nexus_endpoints), - "list_search_attributes" => { - rpc_call!(retry_client, call, list_search_attributes) - } - "remove_remote_cluster" => { - rpc_call!(retry_client, call, remove_remote_cluster) - } - "remove_search_attributes" => { - rpc_call!(retry_client, call, remove_search_attributes) - } - "update_nexus_endpoint" => rpc_call!(retry_client, call, update_nexus_endpoint), - _ => { - return Err(PyValueError::new_err(format!( - "Unknown RPC call {}", - call.rpc - ))) - } - }?; - Ok(bytes) - }) - } - - fn call_cloud_service<'p>(&self, py: Python<'p>, call: RpcCall) -> PyResult> { - use temporal_client::CloudService; - - let mut retry_client = self.retry_client.clone(); - self.runtime.future_into_py(py, async move { - let bytes = match call.rpc.as_str() { - "add_namespace_region" => rpc_call!(retry_client, call, add_namespace_region), - "create_api_key" => rpc_call!(retry_client, call, create_api_key), - "create_connectivity_rule" => { - rpc_call!(retry_client, call, create_connectivity_rule) - } - "create_namespace" => rpc_call!(retry_client, call, create_namespace), - "create_service_account" => rpc_call!(retry_client, call, create_service_account), - "create_user_group" => rpc_call!(retry_client, call, create_user_group), - "create_user" => rpc_call!(retry_client, call, create_user), - "delete_api_key" => rpc_call!(retry_client, call, delete_api_key), - "delete_connectivity_rule" => { - rpc_call!(retry_client, call, delete_connectivity_rule) - } - "delete_namespace" => { - rpc_call_on_trait!(retry_client, call, CloudService, delete_namespace) - } - "delete_service_account" => rpc_call!(retry_client, call, delete_service_account), - "delete_user_group" => rpc_call!(retry_client, call, delete_user_group), - "delete_user" => rpc_call!(retry_client, call, delete_user), - "failover_namespace_region" => { - rpc_call!(retry_client, call, failover_namespace_region) - } - "get_api_key" => rpc_call!(retry_client, call, get_api_key), - "get_api_keys" => rpc_call!(retry_client, call, get_api_keys), - "get_async_operation" => rpc_call!(retry_client, call, get_async_operation), - "get_connectivity_rule" => rpc_call!(retry_client, call, get_connectivity_rule), - "get_connectivity_rules" => rpc_call!(retry_client, call, get_connectivity_rules), - "get_namespace" => rpc_call!(retry_client, call, get_namespace), - "get_namespaces" => rpc_call!(retry_client, call, get_namespaces), - "get_region" => rpc_call!(retry_client, call, get_region), - "get_regions" => rpc_call!(retry_client, call, get_regions), - "get_service_account" => rpc_call!(retry_client, call, get_service_account), - "get_service_accounts" => rpc_call!(retry_client, call, get_service_accounts), - "get_user_group" => rpc_call!(retry_client, call, get_user_group), - "get_user_groups" => rpc_call!(retry_client, call, get_user_groups), - "get_user" => rpc_call!(retry_client, call, get_user), - "get_users" => rpc_call!(retry_client, call, get_users), - "rename_custom_search_attribute" => { - rpc_call!(retry_client, call, rename_custom_search_attribute) - } - "set_user_group_namespace_access" => { - rpc_call!(retry_client, call, set_user_group_namespace_access) - } - "set_user_namespace_access" => { - rpc_call!(retry_client, call, set_user_namespace_access) - } - "update_api_key" => rpc_call!(retry_client, call, update_api_key), - "update_namespace" => { - rpc_call_on_trait!(retry_client, call, CloudService, update_namespace) - } - "update_namespace_tags" => rpc_call!(retry_client, call, update_namespace_tags), - "update_service_account" => rpc_call!(retry_client, call, update_service_account), - "update_user_group" => rpc_call!(retry_client, call, update_user_group), - "update_user" => rpc_call!(retry_client, call, update_user), - _ => { - return Err(PyValueError::new_err(format!( - "Unknown RPC call {}", - call.rpc - ))) - } - }?; - Ok(bytes) - }) - } - - fn call_test_service<'p>(&self, py: Python<'p>, call: RpcCall) -> PyResult> { - let mut retry_client = self.retry_client.clone(); - self.runtime.future_into_py(py, async move { - let bytes = match call.rpc.as_str() { - "get_current_time" => rpc_call!(retry_client, call, get_current_time), - "lock_time_skipping" => rpc_call!(retry_client, call, lock_time_skipping), - "sleep_until" => rpc_call!(retry_client, call, sleep_until), - "sleep" => rpc_call!(retry_client, call, sleep), - "unlock_time_skipping_with_sleep" => { - rpc_call!(retry_client, call, unlock_time_skipping_with_sleep) - } - "unlock_time_skipping" => rpc_call!(retry_client, call, unlock_time_skipping), - _ => { - return Err(PyValueError::new_err(format!( - "Unknown RPC call {}", - call.rpc - ))) - } - }?; - Ok(bytes) - }) + Ok(()) } - fn call_health_service<'p>(&self, py: Python<'p>, call: RpcCall) -> PyResult> { - let mut retry_client = self.retry_client.clone(); - self.runtime.future_into_py(py, async move { - let bytes = match call.rpc.as_str() { - "check" => rpc_call!(retry_client, call, check), - _ => { - return Err(PyValueError::new_err(format!( - "Unknown RPC call {}", - call.rpc - ))) - } - }?; - Ok(bytes) - }) + fn update_api_key(&self, api_key: Option) { + self.connection.set_api_key(api_key); } } -fn rpc_req(call: RpcCall) -> PyResult> { +pub(crate) fn rpc_req(call: RpcCall) -> PyResult> { let proto = P::decode(&*call.req) .map_err(|err| PyValueError::new_err(format!("Invalid proto: {err}")))?; let mut req = tonic::Request::new(proto); for (k, v) in call.metadata { - req.metadata_mut().insert( - MetadataKey::from_str(k.as_str()) - .map_err(|err| PyValueError::new_err(format!("Invalid metadata key: {err}")))?, - v.parse() - .map_err(|err| PyValueError::new_err(format!("Invalid metadata value: {err}")))?, - ); + if let Ok(binary_key) = BinaryMetadataKey::from_str(&k) { + let RpcMetadataValue::Bytes(bytes) = v else { + return Err(PyValueError::new_err(format!( + "Invalid metadata value for binary key {k}: expected bytes" + ))); + }; + + req.metadata_mut() + .insert_bin(binary_key, BinaryMetadataValue::from_bytes(&bytes)); + } else { + let ascii_key = AsciiMetadataKey::from_str(&k) + .map_err(|err| PyValueError::new_err(format!("Invalid metadata key: {err}")))?; + + let RpcMetadataValue::Str(string) = v else { + return Err(PyValueError::new_err(format!( + "Invalid metadata value for ASCII key {k}: expected str" + ))); + }; + + req.metadata_mut().insert( + ascii_key, + AsciiMetadataValue::from_str(&string).map_err(|err| { + PyValueError::new_err(format!("Invalid metadata value: {err}")) + })?, + ); + } } if let Some(timeout_millis) = call.timeout_millis { req.set_timeout(Duration::from_millis(timeout_millis)); @@ -549,7 +201,7 @@ fn rpc_req(call: RpcCall) -> PyResult(res: Result, tonic::Status>) -> PyResult> +pub(crate) fn rpc_resp

(res: Result, tonic::Status>) -> PyResult> where P: prost::Message, P: Default, @@ -557,7 +209,7 @@ where match res { Ok(resp) => Ok(resp.get_ref().encode_to_vec()), Err(err) => { - Python::with_gil(move |py| { + Python::attach(move |py| { // Create tuple of "status", "message", and optional "details" let code = err.code() as u32; let message = err.message().to_owned(); @@ -568,90 +220,251 @@ where } } -impl TryFrom for ClientOptions { - type Error = PyErr; +fn partition_headers( + headers: HashMap, +) -> (HashMap, HashMap>) { + let (ascii_enum_headers, binary_enum_headers): (HashMap<_, _>, HashMap<_, _>) = headers + .into_iter() + .partition(|(_, v)| matches!(v, RpcMetadataValue::Str(_))); - fn try_from(opts: ClientConfig) -> PyResult { - let mut gateway_opts = ClientOptionsBuilder::default(); - gateway_opts - .target_url( - Url::parse(&opts.target_url) - .map_err(|err| PyValueError::new_err(format!("invalid target URL: {err}")))?, - ) - .client_name(opts.client_name) - .client_version(opts.client_version) - .identity(opts.identity) - .retry_config( - opts.retry_config - .map_or(RetryConfig::default(), |c| c.into()), - ) - .keep_alive(opts.keep_alive_config.map(Into::into)) - .http_connect_proxy(opts.http_connect_proxy_config.map(Into::into)) - .headers(Some(opts.metadata)) - .api_key(opts.api_key); - // Builder does not allow us to set option here, so we have to make - // a conditional to even call it - if let Some(tls_config) = opts.tls_config { - gateway_opts.tls_cfg(tls_config.try_into()?); - } - gateway_opts - .build() - .map_err(|err| PyValueError::new_err(format!("Invalid client config: {err}"))) + let ascii_headers = ascii_enum_headers + .into_iter() + .map(|(k, v)| { + let RpcMetadataValue::Str(s) = v else { + unreachable!(); + }; + (k, s) + }) + .collect(); + let binary_headers = binary_enum_headers + .into_iter() + .map(|(k, v)| { + let RpcMetadataValue::Bytes(b) = v else { + unreachable!(); + }; + (k, b) + }) + .collect(); + + (ascii_headers, binary_headers) +} + +impl ClientConfig { + fn into_connection_options( + self, + metrics_meter: Option, + ) -> PyResult { + let (ascii_headers, binary_headers) = partition_headers(self.metadata); + let has_proxy = self.http_connect_proxy_config.is_some(); + // Core rejects DNS load balancing alongside an HTTP CONNECT proxy, so + // suppress DNS LB whenever a proxy is configured to keep the + // pre-existing behavior even if a caller leaves the default. + let dns_load_balancing = if has_proxy { + warn!("Disabling DNS load balancing because http_connect_proxy_config is set"); + None + } else { + self.dns_load_balancing_config.map(Into::into) + }; + let conn_opts = ConnectionOptions::new( + Url::parse(&self.target_url) + .map_err(|err| PyValueError::new_err(format!("invalid target URL: {err}")))?, + ) + .client_name(self.client_name) + .client_version(self.client_version) + .identity(self.identity) + .retry_options( + self.retry_config + .map_or(RetryOptions::default(), |c| c.into()), + ) + .keep_alive(self.keep_alive_config.map(Into::into)) + .maybe_http_connect_proxy(self.http_connect_proxy_config.map(Into::into)) + .dns_load_balancing(dns_load_balancing) + .grpc_compression(grpc_compression_from_str(&self.grpc_compression)?) + .payload_limits( + temporalio_client::PayloadLimitsOptions::builder() + .payloads_warn_size(self.payloads_warn_size) + .memo_warn_size(self.memo_warn_size) + .build(), + ) + .headers(ascii_headers) + .binary_headers(binary_headers) + .maybe_api_key(self.api_key) + .maybe_tls_options(if let Some(tls_config) = self.tls_config { + Some(tls_config.try_into()?) + } else { + None + }) + .maybe_metrics_meter(metrics_meter); + Ok(conn_opts.build()) } } -impl TryFrom for temporal_client::TlsConfig { +fn grpc_compression_from_str(value: &str) -> PyResult { + match value { + "none" => Ok(GrpcCompression::None), + "gzip" => Ok(GrpcCompression::Gzip), + _ => Err(PyValueError::new_err(format!( + "invalid grpc_compression: {value}" + ))), + } +} + +impl TryFrom for temporalio_client::TlsOptions { type Error = PyErr; fn try_from(conf: ClientTlsConfig) -> PyResult { - Ok(TlsConfig { - server_root_ca_cert: conf.server_root_ca_cert, - domain: conf.domain, - client_tls_config: match (conf.client_cert, conf.client_private_key) { - (None, None) => None, - (Some(client_cert), Some(client_private_key)) => { - Some(temporal_client::ClientTlsConfig { - client_cert, - client_private_key, - }) - } - _ => { - return Err(PyValueError::new_err( - "Must have both client cert and private key or neither", - )) - } - }, - }) + let mut server_root_ca_cert = conf.server_root_ca_cert; + let server_cert_verifier = match conf.verification_server_name { + None => None, + Some(name) => { + // The CA bundle is consumed by the verifier's own root store; a + // custom verifier cannot be combined with roots on the connection. + let ca_cert = server_root_ca_cert.take().ok_or_else(|| { + PyValueError::new_err( + "Must have server root CA cert when verification server name is set", + ) + })?; + Some(fixed_server_name_verifier(&name, &ca_cert)?) + } + }; + let client_tls_options = match (conf.client_cert, conf.client_private_key) { + (None, None) => None, + (Some(client_cert), Some(client_private_key)) => Some( + temporalio_client::ClientTlsOptions::builder() + .client_cert(client_cert) + .client_private_key(client_private_key) + .build(), + ), + _ => { + return Err(PyValueError::new_err( + "Must have both client cert and private key or neither", + )) + } + }; + Ok(temporalio_client::TlsOptions::builder() + .maybe_server_root_ca_cert(server_root_ca_cert) + .maybe_domain(conf.domain) + .maybe_client_tls_options(client_tls_options) + .maybe_server_cert_verifier(server_cert_verifier) + .build()) } } -impl From for RetryConfig { +/// Builds a standard WebPKI verifier over the given root CA bundle that +/// checks the certificate against `verification_server_name` rather than the +/// connection's server name, leaving SNI/`:authority` to follow the +/// connected host (or `domain` when set). +fn fixed_server_name_verifier( + verification_server_name: &str, + ca_cert_pem: &[u8], +) -> PyResult> { + let certs = CertificateDer::pem_slice_iter(ca_cert_pem) + .collect::, _>>() + .map_err(|err| { + PyValueError::new_err(format!("Invalid server root CA cert PEM: {err:?}")) + })?; + // Root loading and provider selection mirror tonic's default (no custom + // verifier) client path: unparsable certificates in the bundle are + // skipped, and the provider is the process default if one is installed, + // else ring, as with the connection's `tls-ring` feature. + let mut roots = RootCertStore::empty(); + roots.add_parsable_certificates(certs); + let provider = CryptoProvider::get_default() + .cloned() + .unwrap_or_else(|| Arc::new(rustls::crypto::ring::default_provider())); + let inner = WebPkiServerVerifier::builder_with_provider(roots.into(), provider) + .build() + .map_err(|err| { + PyValueError::new_err(format!("Failed building certificate verifier: {err}")) + })?; + let server_name = ServerName::try_from(verification_server_name.to_owned()) + .map_err(|err| PyValueError::new_err(format!("Invalid verification server name: {err}")))?; + Ok(Arc::new(FixedServerNameVerifier { inner, server_name })) +} + +/// Delegates to the standard WebPKI verifier, but verifies the certificate +/// against a fixed server name instead of the connection's server name. +#[derive(Debug)] +struct FixedServerNameVerifier { + inner: Arc, + server_name: ServerName<'static>, +} + +impl ServerCertVerifier for FixedServerNameVerifier { + fn verify_server_cert( + &self, + end_entity: &CertificateDer<'_>, + intermediates: &[CertificateDer<'_>], + _server_name: &ServerName<'_>, + ocsp_response: &[u8], + now: UnixTime, + ) -> Result { + self.inner.verify_server_cert( + end_entity, + intermediates, + &self.server_name, + ocsp_response, + now, + ) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + self.inner.verify_tls12_signature(message, cert, dss) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + self.inner.verify_tls13_signature(message, cert, dss) + } + + fn supported_verify_schemes(&self) -> Vec { + self.inner.supported_verify_schemes() + } +} + +impl From for RetryOptions { fn from(conf: ClientRetryConfig) -> Self { - RetryConfig { - initial_interval: Duration::from_millis(conf.initial_interval_millis), - randomization_factor: conf.randomization_factor, - multiplier: conf.multiplier, - max_interval: Duration::from_millis(conf.max_interval_millis), - max_elapsed_time: conf.max_elapsed_time_millis.map(Duration::from_millis), - max_retries: conf.max_retries, - } + RetryOptions::builder() + .initial_interval(Duration::from_millis(conf.initial_interval_millis)) + .randomization_factor(conf.randomization_factor) + .multiplier(conf.multiplier) + .max_interval(Duration::from_millis(conf.max_interval_millis)) + .max_elapsed_time(conf.max_elapsed_time_millis.map(Duration::from_millis)) + .max_retries(conf.max_retries) + .build() } } impl From for CoreClientKeepAliveConfig { fn from(conf: ClientKeepAliveConfig) -> Self { - CoreClientKeepAliveConfig { - interval: Duration::from_millis(conf.interval_millis), - timeout: Duration::from_millis(conf.timeout_millis), - } + CoreClientKeepAliveConfig::builder() + .interval(Duration::from_millis(conf.interval_millis)) + .timeout(Duration::from_millis(conf.timeout_millis)) + .build() } } impl From for HttpConnectProxyOptions { fn from(conf: ClientHttpConnectProxyConfig) -> Self { - HttpConnectProxyOptions { - target_addr: conf.target_host, - basic_auth: conf.basic_auth, - } + HttpConnectProxyOptions::new(conf.target_host) + .maybe_basic_auth(conf.basic_auth) + .build() + } +} + +impl From for DnsLoadBalancingOptions { + fn from(conf: ClientDnsLoadBalancingConfig) -> Self { + let mut opts = DnsLoadBalancingOptions::default(); + opts.resolution_interval = Duration::from_millis(conf.resolution_interval_millis); + opts } } diff --git a/temporalio/bridge/src/client_rpc_generated.rs b/temporalio/bridge/src/client_rpc_generated.rs new file mode 100644 index 000000000..ea00d4fd5 --- /dev/null +++ b/temporalio/bridge/src/client_rpc_generated.rs @@ -0,0 +1,1977 @@ +// Generated file. DO NOT EDIT + +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; + +use super::{ + client::{rpc_req, rpc_resp, ClientRef, RpcCall}, + rpc_call, +}; + +#[pymethods] +impl ClientRef { + fn call_workflow_service<'p>( + &self, + py: Python<'p>, + call: RpcCall, + ) -> PyResult> { + self.runtime.assert_same_process("use client")?; + use temporalio_client::grpc::WorkflowService; + let mut connection = self.connection.clone(); + self.runtime.future_into_py(py, async move { + let bytes = match call.rpc.as_str() { + "count_activity_executions" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + count_activity_executions + ) + } + "count_nexus_operation_executions" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + count_nexus_operation_executions + ) + } + "count_schedules" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + count_schedules + ) + } + "count_workers" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + count_workers + ) + } + "count_workflow_executions" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + count_workflow_executions + ) + } + "create_schedule" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + create_schedule + ) + } + "create_worker_deployment" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + create_worker_deployment + ) + } + "create_worker_deployment_version" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + create_worker_deployment_version + ) + } + "create_workflow_rule" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + create_workflow_rule + ) + } + "delete_activity_execution" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + delete_activity_execution + ) + } + "delete_nexus_operation_execution" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + delete_nexus_operation_execution + ) + } + "delete_schedule" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + delete_schedule + ) + } + "delete_worker_deployment" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + delete_worker_deployment + ) + } + "delete_worker_deployment_version" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + delete_worker_deployment_version + ) + } + "delete_workflow_execution" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + delete_workflow_execution + ) + } + "delete_workflow_rule" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + delete_workflow_rule + ) + } + "deprecate_namespace" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + deprecate_namespace + ) + } + "describe_activity_execution" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + describe_activity_execution + ) + } + "describe_batch_operation" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + describe_batch_operation + ) + } + "describe_deployment" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + describe_deployment + ) + } + "describe_namespace" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + describe_namespace + ) + } + "describe_nexus_operation_execution" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + describe_nexus_operation_execution + ) + } + "describe_schedule" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + describe_schedule + ) + } + "describe_task_queue" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + describe_task_queue + ) + } + "describe_worker" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + describe_worker + ) + } + "describe_worker_deployment" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + describe_worker_deployment + ) + } + "describe_worker_deployment_version" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + describe_worker_deployment_version + ) + } + "describe_workflow_execution" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + describe_workflow_execution + ) + } + "describe_workflow_rule" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + describe_workflow_rule + ) + } + "execute_multi_operation" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + execute_multi_operation + ) + } + "fetch_worker_config" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + fetch_worker_config + ) + } + "get_cluster_info" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + get_cluster_info + ) + } + "get_current_deployment" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + get_current_deployment + ) + } + "get_deployment_reachability" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + get_deployment_reachability + ) + } + "get_search_attributes" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + get_search_attributes + ) + } + "get_system_info" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + get_system_info + ) + } + "get_worker_build_id_compatibility" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + get_worker_build_id_compatibility + ) + } + "get_worker_task_reachability" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + get_worker_task_reachability + ) + } + "get_worker_versioning_rules" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + get_worker_versioning_rules + ) + } + "get_workflow_execution_history" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + get_workflow_execution_history + ) + } + "get_workflow_execution_history_reverse" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + get_workflow_execution_history_reverse + ) + } + "list_activity_executions" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + list_activity_executions + ) + } + "list_archived_workflow_executions" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + list_archived_workflow_executions + ) + } + "list_batch_operations" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + list_batch_operations + ) + } + "list_closed_workflow_executions" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + list_closed_workflow_executions + ) + } + "list_deployments" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + list_deployments + ) + } + "list_namespaces" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + list_namespaces + ) + } + "list_nexus_operation_executions" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + list_nexus_operation_executions + ) + } + "list_open_workflow_executions" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + list_open_workflow_executions + ) + } + "list_schedule_matching_times" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + list_schedule_matching_times + ) + } + "list_schedules" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + list_schedules + ) + } + "list_task_queue_partitions" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + list_task_queue_partitions + ) + } + "list_worker_deployments" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + list_worker_deployments + ) + } + "list_workers" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + list_workers + ) + } + "list_workflow_executions" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + list_workflow_executions + ) + } + "list_workflow_rules" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + list_workflow_rules + ) + } + "patch_schedule" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + patch_schedule + ) + } + "pause_activity" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + pause_activity + ) + } + "pause_activity_execution" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + pause_activity_execution + ) + } + "pause_workflow_execution" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + pause_workflow_execution + ) + } + "poll_activity_execution" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + poll_activity_execution + ) + } + "poll_activity_task_queue" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + poll_activity_task_queue + ) + } + "poll_nexus_operation_execution" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + poll_nexus_operation_execution + ) + } + "poll_nexus_task_queue" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + poll_nexus_task_queue + ) + } + "poll_workflow_execution_time_skipping" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + poll_workflow_execution_time_skipping + ) + } + "poll_workflow_execution_update" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + poll_workflow_execution_update + ) + } + "poll_workflow_task_queue" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + poll_workflow_task_queue + ) + } + "query_workflow" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + query_workflow + ) + } + "record_activity_task_heartbeat" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + record_activity_task_heartbeat + ) + } + "record_activity_task_heartbeat_by_id" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + record_activity_task_heartbeat_by_id + ) + } + "record_worker_heartbeat" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + record_worker_heartbeat + ) + } + "register_namespace" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + register_namespace + ) + } + "request_cancel_activity_execution" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + request_cancel_activity_execution + ) + } + "request_cancel_nexus_operation_execution" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + request_cancel_nexus_operation_execution + ) + } + "request_cancel_workflow_execution" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + request_cancel_workflow_execution + ) + } + "reset_activity" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + reset_activity + ) + } + "reset_activity_execution" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + reset_activity_execution + ) + } + "reset_sticky_task_queue" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + reset_sticky_task_queue + ) + } + "reset_workflow_execution" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + reset_workflow_execution + ) + } + "respond_activity_task_canceled" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + respond_activity_task_canceled + ) + } + "respond_activity_task_canceled_by_id" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + respond_activity_task_canceled_by_id + ) + } + "respond_activity_task_completed" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + respond_activity_task_completed + ) + } + "respond_activity_task_completed_by_id" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + respond_activity_task_completed_by_id + ) + } + "respond_activity_task_failed" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + respond_activity_task_failed + ) + } + "respond_activity_task_failed_by_id" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + respond_activity_task_failed_by_id + ) + } + "respond_nexus_task_completed" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + respond_nexus_task_completed + ) + } + "respond_nexus_task_failed" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + respond_nexus_task_failed + ) + } + "respond_query_task_completed" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + respond_query_task_completed + ) + } + "respond_workflow_task_completed" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + respond_workflow_task_completed + ) + } + "respond_workflow_task_failed" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + respond_workflow_task_failed + ) + } + "scan_workflow_executions" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + scan_workflow_executions + ) + } + "set_current_deployment" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + set_current_deployment + ) + } + "set_worker_deployment_current_version" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + set_worker_deployment_current_version + ) + } + "set_worker_deployment_manager" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + set_worker_deployment_manager + ) + } + "set_worker_deployment_ramping_version" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + set_worker_deployment_ramping_version + ) + } + "shutdown_worker" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + shutdown_worker + ) + } + "signal_with_start_workflow_execution" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + signal_with_start_workflow_execution + ) + } + "signal_workflow_execution" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + signal_workflow_execution + ) + } + "start_activity_execution" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + start_activity_execution + ) + } + "start_batch_operation" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + start_batch_operation + ) + } + "start_nexus_operation_execution" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + start_nexus_operation_execution + ) + } + "start_workflow_execution" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + start_workflow_execution + ) + } + "stop_batch_operation" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + stop_batch_operation + ) + } + "terminate_activity_execution" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + terminate_activity_execution + ) + } + "terminate_nexus_operation_execution" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + terminate_nexus_operation_execution + ) + } + "terminate_workflow_execution" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + terminate_workflow_execution + ) + } + "trigger_workflow_rule" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + trigger_workflow_rule + ) + } + "unpause_activity" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + unpause_activity + ) + } + "unpause_activity_execution" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + unpause_activity_execution + ) + } + "unpause_workflow_execution" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + unpause_workflow_execution + ) + } + "update_activity_execution_options" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + update_activity_execution_options + ) + } + "update_activity_options" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + update_activity_options + ) + } + "update_namespace" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + update_namespace + ) + } + "update_schedule" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + update_schedule + ) + } + "update_task_queue_config" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + update_task_queue_config + ) + } + "update_worker_build_id_compatibility" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + update_worker_build_id_compatibility + ) + } + "update_worker_config" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + update_worker_config + ) + } + "update_worker_deployment_version_compute_config" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + update_worker_deployment_version_compute_config + ) + } + "update_worker_deployment_version_metadata" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + update_worker_deployment_version_metadata + ) + } + "update_worker_versioning_rules" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + update_worker_versioning_rules + ) + } + "update_workflow_execution" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + update_workflow_execution + ) + } + "update_workflow_execution_options" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + update_workflow_execution_options + ) + } + "validate_worker_deployment_version_compute_config" => { + rpc_call!( + connection, + call, + WorkflowService, + workflow_service, + validate_worker_deployment_version_compute_config + ) + } + _ => { + return Err(PyValueError::new_err(format!( + "Unknown RPC call {}", + call.rpc + ))) + } + }?; + Ok(bytes) + }) + } + + fn call_operator_service<'p>( + &self, + py: Python<'p>, + call: RpcCall, + ) -> PyResult> { + self.runtime.assert_same_process("use client")?; + use temporalio_client::grpc::OperatorService; + let mut connection = self.connection.clone(); + self.runtime.future_into_py(py, async move { + let bytes = match call.rpc.as_str() { + "add_or_update_remote_cluster" => { + rpc_call!( + connection, + call, + OperatorService, + operator_service, + add_or_update_remote_cluster + ) + } + "add_search_attributes" => { + rpc_call!( + connection, + call, + OperatorService, + operator_service, + add_search_attributes + ) + } + "create_nexus_endpoint" => { + rpc_call!( + connection, + call, + OperatorService, + operator_service, + create_nexus_endpoint + ) + } + "delete_namespace" => { + rpc_call!( + connection, + call, + OperatorService, + operator_service, + delete_namespace + ) + } + "delete_nexus_endpoint" => { + rpc_call!( + connection, + call, + OperatorService, + operator_service, + delete_nexus_endpoint + ) + } + "get_nexus_endpoint" => { + rpc_call!( + connection, + call, + OperatorService, + operator_service, + get_nexus_endpoint + ) + } + "list_clusters" => { + rpc_call!( + connection, + call, + OperatorService, + operator_service, + list_clusters + ) + } + "list_nexus_endpoints" => { + rpc_call!( + connection, + call, + OperatorService, + operator_service, + list_nexus_endpoints + ) + } + "list_search_attributes" => { + rpc_call!( + connection, + call, + OperatorService, + operator_service, + list_search_attributes + ) + } + "remove_remote_cluster" => { + rpc_call!( + connection, + call, + OperatorService, + operator_service, + remove_remote_cluster + ) + } + "remove_search_attributes" => { + rpc_call!( + connection, + call, + OperatorService, + operator_service, + remove_search_attributes + ) + } + "update_nexus_endpoint" => { + rpc_call!( + connection, + call, + OperatorService, + operator_service, + update_nexus_endpoint + ) + } + _ => { + return Err(PyValueError::new_err(format!( + "Unknown RPC call {}", + call.rpc + ))) + } + }?; + Ok(bytes) + }) + } + + fn call_cloud_service<'p>(&self, py: Python<'p>, call: RpcCall) -> PyResult> { + self.runtime.assert_same_process("use client")?; + use temporalio_client::grpc::CloudService; + let mut connection = self.connection.clone(); + self.runtime.future_into_py(py, async move { + let bytes = match call.rpc.as_str() { + "add_namespace_region" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + add_namespace_region + ) + } + "add_user_group_member" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + add_user_group_member + ) + } + "create_account_audit_log_sink" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + create_account_audit_log_sink + ) + } + "create_api_key" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + create_api_key + ) + } + "create_billing_report" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + create_billing_report + ) + } + "create_connectivity_rule" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + create_connectivity_rule + ) + } + "create_custom_role" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + create_custom_role + ) + } + "create_namespace" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + create_namespace + ) + } + "create_namespace_export_sink" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + create_namespace_export_sink + ) + } + "create_nexus_endpoint" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + create_nexus_endpoint + ) + } + "create_service_account" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + create_service_account + ) + } + "create_user" => { + rpc_call!(connection, call, CloudService, cloud_service, create_user) + } + "create_user_group" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + create_user_group + ) + } + "delete_account_audit_log_sink" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + delete_account_audit_log_sink + ) + } + "delete_api_key" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + delete_api_key + ) + } + "delete_connectivity_rule" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + delete_connectivity_rule + ) + } + "delete_custom_role" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + delete_custom_role + ) + } + "delete_namespace" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + delete_namespace + ) + } + "delete_namespace_export_sink" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + delete_namespace_export_sink + ) + } + "delete_namespace_region" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + delete_namespace_region + ) + } + "delete_nexus_endpoint" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + delete_nexus_endpoint + ) + } + "delete_service_account" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + delete_service_account + ) + } + "delete_user" => { + rpc_call!(connection, call, CloudService, cloud_service, delete_user) + } + "delete_user_group" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + delete_user_group + ) + } + "failover_namespace_region" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + failover_namespace_region + ) + } + "get_account" => { + rpc_call!(connection, call, CloudService, cloud_service, get_account) + } + "get_account_audit_log_sink" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + get_account_audit_log_sink + ) + } + "get_account_audit_log_sinks" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + get_account_audit_log_sinks + ) + } + "get_api_key" => { + rpc_call!(connection, call, CloudService, cloud_service, get_api_key) + } + "get_api_keys" => { + rpc_call!(connection, call, CloudService, cloud_service, get_api_keys) + } + "get_async_operation" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + get_async_operation + ) + } + "get_audit_logs" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + get_audit_logs + ) + } + "get_billing_report" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + get_billing_report + ) + } + "get_connectivity_rule" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + get_connectivity_rule + ) + } + "get_connectivity_rules" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + get_connectivity_rules + ) + } + "get_current_identity" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + get_current_identity + ) + } + "get_custom_role" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + get_custom_role + ) + } + "get_custom_roles" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + get_custom_roles + ) + } + "get_namespace" => { + rpc_call!(connection, call, CloudService, cloud_service, get_namespace) + } + "get_namespace_capacity_info" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + get_namespace_capacity_info + ) + } + "get_namespace_export_sink" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + get_namespace_export_sink + ) + } + "get_namespace_export_sinks" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + get_namespace_export_sinks + ) + } + "get_namespaces" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + get_namespaces + ) + } + "get_nexus_endpoint" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + get_nexus_endpoint + ) + } + "get_nexus_endpoints" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + get_nexus_endpoints + ) + } + "get_region" => { + rpc_call!(connection, call, CloudService, cloud_service, get_region) + } + "get_regions" => { + rpc_call!(connection, call, CloudService, cloud_service, get_regions) + } + "get_service_account" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + get_service_account + ) + } + "get_service_account_namespace_assignments" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + get_service_account_namespace_assignments + ) + } + "get_service_accounts" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + get_service_accounts + ) + } + "get_usage" => { + rpc_call!(connection, call, CloudService, cloud_service, get_usage) + } + "get_user" => { + rpc_call!(connection, call, CloudService, cloud_service, get_user) + } + "get_user_group" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + get_user_group + ) + } + "get_user_group_members" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + get_user_group_members + ) + } + "get_user_group_namespace_assignments" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + get_user_group_namespace_assignments + ) + } + "get_user_groups" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + get_user_groups + ) + } + "get_user_namespace_assignments" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + get_user_namespace_assignments + ) + } + "get_users" => { + rpc_call!(connection, call, CloudService, cloud_service, get_users) + } + "remove_user_group_member" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + remove_user_group_member + ) + } + "rename_custom_search_attribute" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + rename_custom_search_attribute + ) + } + "set_service_account_namespace_access" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + set_service_account_namespace_access + ) + } + "set_user_group_namespace_access" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + set_user_group_namespace_access + ) + } + "set_user_namespace_access" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + set_user_namespace_access + ) + } + "update_account" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + update_account + ) + } + "update_account_audit_log_sink" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + update_account_audit_log_sink + ) + } + "update_api_key" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + update_api_key + ) + } + "update_custom_role" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + update_custom_role + ) + } + "update_namespace" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + update_namespace + ) + } + "update_namespace_export_sink" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + update_namespace_export_sink + ) + } + "update_namespace_tags" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + update_namespace_tags + ) + } + "update_nexus_endpoint" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + update_nexus_endpoint + ) + } + "update_service_account" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + update_service_account + ) + } + "update_user" => { + rpc_call!(connection, call, CloudService, cloud_service, update_user) + } + "update_user_group" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + update_user_group + ) + } + "validate_account_audit_log_sink" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + validate_account_audit_log_sink + ) + } + "validate_namespace_export_sink" => { + rpc_call!( + connection, + call, + CloudService, + cloud_service, + validate_namespace_export_sink + ) + } + _ => { + return Err(PyValueError::new_err(format!( + "Unknown RPC call {}", + call.rpc + ))) + } + }?; + Ok(bytes) + }) + } + + fn call_test_service<'p>(&self, py: Python<'p>, call: RpcCall) -> PyResult> { + self.runtime.assert_same_process("use client")?; + use temporalio_client::grpc::TestService; + let mut connection = self.connection.clone(); + self.runtime.future_into_py(py, async move { + let bytes = match call.rpc.as_str() { + "get_current_time" => { + rpc_call!( + connection, + call, + TestService, + test_service, + get_current_time + ) + } + "lock_time_skipping" => { + rpc_call!( + connection, + call, + TestService, + test_service, + lock_time_skipping + ) + } + "sleep" => { + rpc_call!(connection, call, TestService, test_service, sleep) + } + "sleep_until" => { + rpc_call!(connection, call, TestService, test_service, sleep_until) + } + "unlock_time_skipping" => { + rpc_call!( + connection, + call, + TestService, + test_service, + unlock_time_skipping + ) + } + "unlock_time_skipping_with_sleep" => { + rpc_call!( + connection, + call, + TestService, + test_service, + unlock_time_skipping_with_sleep + ) + } + _ => { + return Err(PyValueError::new_err(format!( + "Unknown RPC call {}", + call.rpc + ))) + } + }?; + Ok(bytes) + }) + } + + fn call_health_service<'p>(&self, py: Python<'p>, call: RpcCall) -> PyResult> { + self.runtime.assert_same_process("use client")?; + use temporalio_client::grpc::HealthService; + let mut connection = self.connection.clone(); + self.runtime.future_into_py(py, async move { + let bytes = match call.rpc.as_str() { + "check" => { + rpc_call!(connection, call, HealthService, health_service, check) + } + _ => { + return Err(PyValueError::new_err(format!( + "Unknown RPC call {}", + call.rpc + ))) + } + }?; + Ok(bytes) + }) + } +} diff --git a/temporalio/bridge/src/envconfig.rs b/temporalio/bridge/src/envconfig.rs index 1651b4469..7ce2f4664 100644 --- a/temporalio/bridge/src/envconfig.rs +++ b/temporalio/bridge/src/envconfig.rs @@ -4,17 +4,17 @@ use pyo3::{ types::{PyBytes, PyDict}, }; use std::collections::HashMap; -use temporal_sdk_core_api::envconfig::{ +use temporalio_common::envconfig::{ load_client_config as core_load_client_config, load_client_config_profile as core_load_client_config_profile, - ClientConfig as CoreClientConfig, ClientConfigCodec, ClientConfigProfile as CoreClientConfigProfile, - ClientConfigTLS as CoreClientConfigTLS, DataSource, LoadClientConfigOptions, - LoadClientConfigProfileOptions, + ClientConfig as CoreClientConfig, ClientConfigCodec, + ClientConfigProfile as CoreClientConfigProfile, ClientConfigTLS as CoreClientConfigTLS, + DataSource, LoadClientConfigOptions, LoadClientConfigProfileOptions, }; pyo3::create_exception!(temporal_sdk_bridge, ConfigError, PyRuntimeError); -fn data_source_to_dict(py: Python, ds: &DataSource) -> PyResult { +fn data_source_to_dict(py: Python, ds: &DataSource) -> PyResult> { let dict = PyDict::new(py); match ds { DataSource::Path(p) => dict.set_item("path", p)?, @@ -23,7 +23,7 @@ fn data_source_to_dict(py: Python, ds: &DataSource) -> PyResult { Ok(dict.into()) } -fn tls_to_dict(py: Python, tls: &CoreClientConfigTLS) -> PyResult { +fn tls_to_dict(py: Python, tls: &CoreClientConfigTLS) -> PyResult> { let dict = PyDict::new(py); dict.set_item("disabled", tls.disabled)?; if let Some(v) = &tls.client_cert { @@ -42,7 +42,7 @@ fn tls_to_dict(py: Python, tls: &CoreClientConfigTLS) -> PyResult { Ok(dict.into()) } -fn codec_to_dict(py: Python, codec: &ClientConfigCodec) -> PyResult { +fn codec_to_dict(py: Python, codec: &ClientConfigCodec) -> PyResult> { let dict = PyDict::new(py); if let Some(v) = &codec.endpoint { dict.set_item("endpoint", v)?; @@ -53,7 +53,7 @@ fn codec_to_dict(py: Python, codec: &ClientConfigCodec) -> PyResult { Ok(dict.into()) } -fn profile_to_dict(py: Python, profile: &CoreClientConfigProfile) -> PyResult { +fn profile_to_dict(py: Python, profile: &CoreClientConfigProfile) -> PyResult> { let dict = PyDict::new(py); if let Some(v) = &profile.address { dict.set_item("address", v)?; @@ -76,7 +76,7 @@ fn profile_to_dict(py: Python, profile: &CoreClientConfigProfile) -> PyResult PyResult { +fn core_config_to_dict(py: Python, core_config: &CoreClientConfig) -> PyResult> { let profiles_dict = PyDict::new(py); for (name, profile) in &core_config.profiles { let connect_dict = profile_to_dict(py, profile)?; @@ -89,19 +89,14 @@ fn load_client_config_inner( py: Python, config_source: Option, config_file_strict: bool, - disable_file: bool, env_vars: Option>, -) -> PyResult { - let core_config = if disable_file { - CoreClientConfig::default() - } else { - let options = LoadClientConfigOptions { - config_source, - config_file_strict, - }; - core_load_client_config(options, env_vars.as_ref()) - .map_err(|e| ConfigError::new_err(format!("{e}")))? - }; +) -> PyResult> { + let options = LoadClientConfigOptions::builder() + .maybe_config_source(config_source) + .config_file_strict(config_file_strict) + .build(); + let core_config = core_load_client_config(options, env_vars.as_ref()) + .map_err(|e| ConfigError::new_err(format!("{e}")))?; core_config_to_dict(py, &core_config) } @@ -114,14 +109,14 @@ fn load_client_connect_config_inner( disable_env: bool, config_file_strict: bool, env_vars: Option>, -) -> PyResult { - let options = LoadClientConfigProfileOptions { - config_source, - config_file_profile: profile, - config_file_strict, - disable_file, - disable_env, - }; +) -> PyResult> { + let options = LoadClientConfigProfileOptions::builder() + .maybe_config_source(config_source) + .maybe_config_file_profile(profile) + .config_file_strict(config_file_strict) + .disable_file(disable_file) + .disable_env(disable_env) + .build(); let profile = core_load_client_config_profile(options, env_vars.as_ref()) .map_err(|e| ConfigError::new_err(format!("{e}")))?; @@ -130,15 +125,14 @@ fn load_client_connect_config_inner( } #[pyfunction] -#[pyo3(signature = (path, data, disable_file, config_file_strict, env_vars = None))] +#[pyo3(signature = (path, data, config_file_strict, env_vars = None))] pub fn load_client_config( py: Python, path: Option, data: Option>, - disable_file: bool, config_file_strict: bool, env_vars: Option>, -) -> PyResult { +) -> PyResult> { let config_source = match (path, data) { (Some(p), None) => Some(DataSource::Path(p)), (None, Some(d)) => Some(DataSource::Data(d)), @@ -149,13 +143,7 @@ pub fn load_client_config( )) } }; - load_client_config_inner( - py, - config_source, - config_file_strict, - disable_file, - env_vars, - ) + load_client_config_inner(py, config_source, config_file_strict, env_vars) } #[pyfunction] @@ -170,7 +158,7 @@ pub fn load_client_connect_config( disable_env: bool, config_file_strict: bool, env_vars: Option>, -) -> PyResult { +) -> PyResult> { let config_source = match (path, data) { (Some(p), None) => Some(DataSource::Path(p)), (None, Some(d)) => Some(DataSource::Data(d)), @@ -190,4 +178,4 @@ pub fn load_client_connect_config( config_file_strict, env_vars, ) -} \ No newline at end of file +} diff --git a/temporalio/bridge/src/lib.rs b/temporalio/bridge/src/lib.rs index 0281e9210..ee157fb18 100644 --- a/temporalio/bridge/src/lib.rs +++ b/temporalio/bridge/src/lib.rs @@ -2,6 +2,7 @@ use pyo3::prelude::*; use pyo3::types::PyTuple; mod client; +mod client_rpc_generated; mod envconfig; mod metric; mod runtime; @@ -60,10 +61,7 @@ fn temporal_sdk_bridge(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { let envconfig_module = PyModule::new(py, "envconfig")?; envconfig_module.add("ConfigError", py.get_type::())?; envconfig_module.add_function(wrap_pyfunction!(envconfig::load_client_config, m)?)?; - envconfig_module.add_function(wrap_pyfunction!( - envconfig::load_client_connect_config, - m - )?)?; + envconfig_module.add_function(wrap_pyfunction!(envconfig::load_client_connect_config, m)?)?; m.add_submodule(&envconfig_module)?; Ok(()) @@ -84,8 +82,8 @@ fn new_metric_meter(runtime_ref: &runtime::RuntimeRef) -> Option PyResult { - runtime::init_runtime(telemetry_config) +fn init_runtime(options: runtime::RuntimeOptions) -> PyResult { + runtime::init_runtime(options) } #[pyfunction] diff --git a/temporalio/bridge/src/metric.rs b/temporalio/bridge/src/metric.rs index 3b24d5974..276933f6d 100644 --- a/temporalio/bridge/src/metric.rs +++ b/temporalio/bridge/src/metric.rs @@ -4,8 +4,10 @@ use std::{collections::HashMap, sync::Arc}; use pyo3::prelude::*; use pyo3::{exceptions::PyTypeError, types::PyDict}; -use temporal_sdk_core_api::telemetry::metrics::{ - self, BufferInstrumentRef, CustomMetricAttributes, MetricEvent, NewAttributes, +use temporalio_common::telemetry::metrics::{ + self, + core::{BufferInstrumentRef, CustomMetricAttributes, MetricEvent}, + NewAttributes, }; use crate::runtime; @@ -17,7 +19,7 @@ pub struct MetricMeterRef { default_attributes: MetricAttributesRef, } -#[pyclass] +#[pyclass(from_py_object)] #[derive(Clone)] pub struct MetricAttributesRef { attrs: metrics::MetricAttributes, @@ -61,7 +63,7 @@ pub fn new_metric_meter(runtime_ref: &runtime::RuntimeRef) -> Option, ) -> MetricHistogramDurationRef { MetricHistogramDurationRef { - histogram: self - .meter - .inner - .histogram_duration(build_metric_parameters( - name, - description, - unit, - )), + histogram: self.meter.histogram_duration(build_metric_parameters( + name, + description, + unit, + )), } } @@ -145,7 +137,6 @@ impl MetricMeterRef { MetricGaugeRef { gauge: self .meter - .inner .gauge(build_metric_parameters(name, description, unit)), } } @@ -159,7 +150,6 @@ impl MetricMeterRef { MetricGaugeFloatRef { gauge: self .meter - .inner .gauge_f64(build_metric_parameters(name, description, unit)), } } @@ -213,16 +203,11 @@ fn build_metric_parameters( description: Option, unit: Option, ) -> metrics::MetricParameters { - let mut build = metrics::MetricParametersBuilder::default(); - build.name(name); - if let Some(description) = description { - build.description(description); - } - if let Some(unit) = unit { - build.unit(unit); - } - // Should be nothing that would fail validation here - build.build().unwrap() + metrics::MetricParameters::builder() + .name(name) + .maybe_description(description) + .maybe_unit(unit) + .build() } #[pymethods] @@ -231,9 +216,9 @@ impl MetricAttributesRef { &self, py: Python, meter: &MetricMeterRef, - new_attrs: HashMap, + new_attrs: HashMap>, ) -> PyResult { - let attrs = meter.meter.inner.extend_attributes( + let attrs = meter.meter.extend_attributes( self.attrs.clone(), NewAttributes { attributes: new_attrs @@ -249,7 +234,7 @@ impl MetricAttributesRef { fn metric_key_value_from_py( py: Python, k: String, - obj: PyObject, + obj: Py, ) -> PyResult { let val = if let Ok(v) = obj.extract::(py) { metrics::MetricValue::String(v) @@ -283,6 +268,7 @@ pub enum BufferedMetricUpdateValue { U64(u64), U128(u128), F64(f64), + I64(i64), } // WARNING: This must match temporalio.runtime.BufferedMetric protocol @@ -343,7 +329,7 @@ fn convert_metric_event( description: Some(params.description) .filter(|s| !s.is_empty()) .map(|s| s.to_string()), - unit: if matches!(kind, metrics::MetricKind::HistogramDuration) + unit: if matches!(kind, metrics::core::MetricKind::HistogramDuration) && params.unit == "duration" { if durations_as_seconds { @@ -357,11 +343,13 @@ fn convert_metric_event( Some(params.unit.to_string()) }, kind: match kind { - metrics::MetricKind::Counter => 0, - metrics::MetricKind::Gauge | metrics::MetricKind::GaugeF64 => 1, - metrics::MetricKind::Histogram - | metrics::MetricKind::HistogramF64 - | metrics::MetricKind::HistogramDuration => 2, + metrics::core::MetricKind::Counter => 0, + metrics::core::MetricKind::Gauge + | metrics::core::MetricKind::GaugeF64 => 1, + metrics::core::MetricKind::Histogram + | metrics::core::MetricKind::HistogramF64 + | metrics::core::MetricKind::HistogramDuration => 2, + metrics::core::MetricKind::UpDownCounter => 3, }, }, ) @@ -416,16 +404,17 @@ fn convert_metric_event( } => Some(BufferedMetricUpdate { metric: instrument.get().clone().0.clone_ref(py), value: match update { - metrics::MetricUpdateVal::Duration(v) if durations_as_seconds => { + metrics::core::MetricUpdateVal::Duration(v) if durations_as_seconds => { BufferedMetricUpdateValue::F64(v.as_secs_f64()) } - metrics::MetricUpdateVal::Duration(v) => { + metrics::core::MetricUpdateVal::Duration(v) => { BufferedMetricUpdateValue::U128(v.as_millis()) } - metrics::MetricUpdateVal::Delta(v) => BufferedMetricUpdateValue::U64(v), - metrics::MetricUpdateVal::DeltaF64(v) => BufferedMetricUpdateValue::F64(v), - metrics::MetricUpdateVal::Value(v) => BufferedMetricUpdateValue::U64(v), - metrics::MetricUpdateVal::ValueF64(v) => BufferedMetricUpdateValue::F64(v), + metrics::core::MetricUpdateVal::Delta(v) => BufferedMetricUpdateValue::U64(v), + metrics::core::MetricUpdateVal::DeltaF64(v) => BufferedMetricUpdateValue::F64(v), + metrics::core::MetricUpdateVal::Value(v) => BufferedMetricUpdateValue::U64(v), + metrics::core::MetricUpdateVal::ValueF64(v) => BufferedMetricUpdateValue::F64(v), + metrics::core::MetricUpdateVal::SignedDelta(v) => BufferedMetricUpdateValue::I64(v), }, attributes: attributes .get() diff --git a/temporalio/bridge/src/runtime.rs b/temporalio/bridge/src/runtime.rs index 84ec2ca42..8a8e7b591 100644 --- a/temporalio/bridge/src/runtime.rs +++ b/temporalio/bridge/src/runtime.rs @@ -1,5 +1,5 @@ use futures::channel::mpsc::Receiver; -use pyo3::exceptions::{PyRuntimeError, PyValueError}; +use pyo3::exceptions::{PyAssertionError, PyRuntimeError, PyValueError}; use pyo3::prelude::*; use pythonize::pythonize; use std::collections::HashMap; @@ -9,16 +9,15 @@ use std::pin::Pin; use std::str::FromStr; use std::sync::Arc; use std::time::{Duration, UNIX_EPOCH}; -use temporal_sdk_core::telemetry::{ - build_otlp_metric_exporter, start_prometheus_metric_exporter, CoreLogStreamConsumer, - MetricsCallBuffer, -}; -use temporal_sdk_core::{CoreRuntime, TokioRuntimeBuilder}; -use temporal_sdk_core_api::telemetry::metrics::{CoreMeter, MetricCallBufferer}; -use temporal_sdk_core_api::telemetry::{ - CoreLog, Logger, MetricTemporality, OtelCollectorOptionsBuilder, OtlpProtocol, - PrometheusExporterOptionsBuilder, TelemetryOptionsBuilder, +use temporalio_common::telemetry::metrics::core::MetricCallBufferer; +use temporalio_common::telemetry::metrics::CoreMeter; +use temporalio_common::telemetry::{ + build_otlp_metric_exporter, start_prometheus_metric_exporter, CoreLog, CoreLogStreamConsumer, + Logger, MetricTemporality, OtelCollectorOptions, OtlpProtocol, PrometheusExporterOptions, + TelemetryOptions, }; +use temporalio_sdk_core::telemetry::MetricsCallBuffer; +use temporalio_sdk_core::{CoreRuntime, TokioRuntimeBuilder}; use tokio::task::JoinHandle; use tokio_stream::StreamExt; use tracing::Level; @@ -33,6 +32,7 @@ pub struct RuntimeRef { #[derive(Clone)] pub(crate) struct Runtime { + pub(crate) pid: u32, pub(crate) core: Arc, metrics_call_buffer: Option>>, log_forwarder_handle: Option>>, @@ -47,7 +47,7 @@ pub struct TelemetryConfig { #[derive(FromPyObject)] pub struct LoggingConfig { filter: String, - forward_to: Option, + forward_to: Option>, } #[pyclass] @@ -75,6 +75,7 @@ pub struct OpenTelemetryConfig { metric_temporality_delta: bool, durations_as_seconds: bool, http: bool, + histogram_bucket_overrides: Option>>, } #[derive(FromPyObject)] @@ -86,17 +87,30 @@ pub struct PrometheusConfig { histogram_bucket_overrides: Option>>, } +#[derive(FromPyObject)] +pub struct RuntimeOptions { + telemetry: TelemetryConfig, + worker_heartbeat_interval_millis: Option, + disable_environment_info: bool, +} + const FORWARD_LOG_BUFFER_SIZE: usize = 2048; const FORWARD_LOG_MAX_FREQ_MS: u64 = 10; -pub fn init_runtime(telemetry_config: TelemetryConfig) -> PyResult { +pub fn init_runtime(options: RuntimeOptions) -> PyResult { + let RuntimeOptions { + telemetry: TelemetryConfig { logging, metrics }, + worker_heartbeat_interval_millis, + disable_environment_info, + } = options; + // Have to build/start telemetry config pieces - let mut telemetry_build = TelemetryOptionsBuilder::default(); + let telemetry_build = TelemetryOptions::builder(); // Build logging config, capturing forwarding info to start later - let mut log_forwarding: Option<(Receiver, PyObject)> = None; - if let Some(logging_conf) = telemetry_config.logging { - telemetry_build.logging(if let Some(forward_to) = logging_conf.forward_to { + let mut log_forwarding: Option<(Receiver, Py)> = None; + let maybe_logging = if let Some(logging_conf) = logging { + Some(if let Some(forward_to) = logging_conf.forward_to { // Note, actual log forwarding is started later let (consumer, stream) = CoreLogStreamConsumer::new(FORWARD_LOG_BUFFER_SIZE); log_forwarding = Some((stream, forward_to)); @@ -108,31 +122,32 @@ pub fn init_runtime(telemetry_config: TelemetryConfig) -> PyResult { Logger::Console { filter: logging_conf.filter.to_string(), } - }); - } - - // Build metric config, but actual metrics instance is late-bound after - // CoreRuntime is created since it needs Tokio runtime - if let Some(metrics_conf) = telemetry_config.metrics.as_ref() { - telemetry_build.attach_service_name(metrics_conf.attach_service_name); - if let Some(prefix) = &metrics_conf.metric_prefix { - telemetry_build.metric_prefix(prefix.to_string()); - } - } + }) + } else { + None + }; + + let runtime_options = temporalio_sdk_core::RuntimeOptions::builder() + .telemetry_options( + telemetry_build + .maybe_logging(maybe_logging) + .maybe_attach_service_name(metrics.as_ref().map(|c| c.attach_service_name)) + .maybe_metric_prefix(metrics.as_ref().and_then(|c| c.metric_prefix.clone())) + .build(), + ) + .heartbeat_interval(worker_heartbeat_interval_millis.map(Duration::from_millis)) + .disable_environment_info(disable_environment_info) + .build() + .map_err(|err| PyValueError::new_err(format!("Invalid runtime options: {err}")))?; // Create core runtime which starts tokio multi-thread runtime - let mut core = CoreRuntime::new( - telemetry_build - .build() - .map_err(|err| PyValueError::new_err(format!("Invalid telemetry config: {err}")))?, - TokioRuntimeBuilder::default(), - ) - .map_err(|err| PyRuntimeError::new_err(format!("Failed initializing telemetry: {err}")))?; + let mut core = CoreRuntime::new(runtime_options, TokioRuntimeBuilder::default()) + .map_err(|err| PyRuntimeError::new_err(format!("Failed initializing runtime: {err}")))?; // We late-bind the metrics after core runtime is created since it needs // the Tokio handle let mut metrics_call_buffer: Option>> = None; - if let Some(metrics_conf) = telemetry_config.metrics { + if let Some(metrics_conf) = metrics { let _guard = core.tokio_handle().enter(); // If they want buffered, cannot have Prom/OTel and we make buffered if metrics_conf.buffered_with_size > 0 { @@ -166,13 +181,14 @@ pub fn init_runtime(telemetry_config: TelemetryConfig) -> PyResult { .collect::>(); // We silently swallow errors here because logging them could // cause a bad loop and we don't want to assume console presence - let _ = Python::with_gil(|py| callback.call1(py, (entries,))); + let _ = Python::attach(|py| callback.call1(py, (entries,))); } })) }); Ok(RuntimeRef { runtime: Runtime { + pid: std::process::id(), core: Arc::new(core), metrics_call_buffer, log_forwarder_handle, @@ -192,11 +208,23 @@ impl Runtime { pub fn future_into_py<'a, F, T>(&self, py: Python<'a>, fut: F) -> PyResult> where F: Future> + Send + 'static, - T: for<'py> IntoPyObject<'py>, + T: for<'py> IntoPyObject<'py> + Send + 'static, { let _guard = self.core.tokio_handle().enter(); pyo3_async_runtimes::generic::future_into_py::(py, fut) } + + pub(crate) fn assert_same_process(&self, action: &'static str) -> PyResult<()> { + let current_pid = std::process::id(); + if self.pid != current_pid { + Err(PyAssertionError::new_err(format!( + "Cannot {} across forks (original runtime PID is {}, current is {})", + action, self.pid, current_pid, + ))) + } else { + Ok(()) + } + } } impl Drop for Runtime { @@ -286,7 +314,7 @@ impl BufferedLogEntry { } #[getter] - fn fields(&self, py: Python<'_>) -> PyResult> { + fn fields(&self, py: Python<'_>) -> PyResult>> { self.core_log .fields .iter() @@ -310,37 +338,41 @@ impl TryFrom for Arc { } // Build OTel exporter - let mut build = OtelCollectorOptionsBuilder::default(); - build + let otel_options = OtelCollectorOptions::builder() .url( - Url::parse(&otel_conf.url).map_err(|err| { - PyValueError::new_err(format!("Invalid OTel URL: {err}")) - })?, + Url::parse(&otel_conf.url) + .map_err(|err| PyValueError::new_err(format!("Invalid OTel URL: {err}")))?, ) .headers(otel_conf.headers) - .use_seconds_for_durations(otel_conf.durations_as_seconds); - if let Some(period) = otel_conf.metric_periodicity_millis { - build.metric_periodicity(Duration::from_millis(period)); - } - if otel_conf.metric_temporality_delta { - build.metric_temporality(MetricTemporality::Delta); - } - if let Some(global_tags) = conf.global_tags { - build.global_tags(global_tags); - } - if otel_conf.http { - build.protocol(OtlpProtocol::Http); - } - let otel_options = build - .build() - .map_err(|err| PyValueError::new_err(format!("Invalid OTel config: {err}")))?; + .use_seconds_for_durations(otel_conf.durations_as_seconds) + .maybe_metric_periodicity( + otel_conf + .metric_periodicity_millis + .map(Duration::from_millis), + ) + .maybe_metric_temporality(if otel_conf.metric_temporality_delta { + Some(MetricTemporality::Delta) + } else { + None + }) + .maybe_global_tags(conf.global_tags) + .maybe_protocol(if otel_conf.http { + Some(OtlpProtocol::Http) + } else { + None + }) + .maybe_histogram_bucket_overrides(otel_conf.histogram_bucket_overrides.map( + |overrides| temporalio_common::telemetry::HistogramBucketOverrides { + overrides, + }, + )) + .build(); Ok(Arc::new(build_otlp_metric_exporter(otel_options).map_err( |err| PyValueError::new_err(format!("Failed building OTel exporter: {err}")), )?)) } else if let Some(prom_conf) = conf.prometheus { // Start prom exporter - let mut build = PrometheusExporterOptionsBuilder::default(); - build + let prom_options = PrometheusExporterOptions::builder() .socket_addr( SocketAddr::from_str(&prom_conf.bind_address).map_err(|err| { PyValueError::new_err(format!("Invalid Prometheus address: {err}")) @@ -348,18 +380,14 @@ impl TryFrom for Arc { ) .counters_total_suffix(prom_conf.counters_total_suffix) .unit_suffix(prom_conf.unit_suffix) - .use_seconds_for_durations(prom_conf.durations_as_seconds); - if let Some(global_tags) = conf.global_tags { - build.global_tags(global_tags); - } - if let Some(overrides) = prom_conf.histogram_bucket_overrides { - build.histogram_bucket_overrides( - temporal_sdk_core_api::telemetry::HistogramBucketOverrides { overrides }, - ); - } - let prom_options = build.build().map_err(|err| { - PyValueError::new_err(format!("Invalid Prometheus config: {err}")) - })?; + .use_seconds_for_durations(prom_conf.durations_as_seconds) + .maybe_global_tags(conf.global_tags) + .maybe_histogram_bucket_overrides(prom_conf.histogram_bucket_overrides.map( + |overrides| temporalio_common::telemetry::HistogramBucketOverrides { + overrides, + }, + )) + .build(); Ok(start_prometheus_metric_exporter(prom_options) .map_err(|err| { PyValueError::new_err(format!("Failed starting Prometheus exporter: {err}")) @@ -394,6 +422,13 @@ impl pyo3_async_runtimes::generic::Runtime for TokioRuntime { { tokio::runtime::Handle::current().spawn(fut) } + + fn spawn_blocking(f: F) -> Self::JoinHandle + where + F: FnOnce() + Send + 'static, + { + tokio::runtime::Handle::current().spawn_blocking(f) + } } impl pyo3_async_runtimes::generic::ContextExt for TokioRuntime { @@ -412,10 +447,7 @@ impl pyo3_async_runtimes::generic::ContextExt for TokioRuntime { fn get_task_locals() -> Option { TASK_LOCALS - .try_with(|c| { - c.get() - .map(|locals| Python::with_gil(|py| locals.clone_ref(py))) - }) + .try_with(|c| c.get().cloned()) .unwrap_or_default() } } diff --git a/temporalio/bridge/src/testing.rs b/temporalio/bridge/src/testing.rs index 04eea1286..33f2be74f 100644 --- a/temporalio/bridge/src/testing.rs +++ b/temporalio/bridge/src/testing.rs @@ -1,8 +1,8 @@ use std::time::Duration; -use pyo3::exceptions::{PyRuntimeError, PyValueError}; +use pyo3::exceptions::PyRuntimeError; use pyo3::prelude::*; -use temporal_sdk_core::ephemeral_server; +use temporalio_sdk_core::ephemeral_server; use crate::runtime; @@ -25,6 +25,7 @@ pub struct DevServerConfig { port: Option, database_filename: Option, ui: bool, + ui_port: Option, log_format: String, log_level: String, extra_args: Vec, @@ -47,7 +48,7 @@ pub fn start_dev_server<'a>( runtime_ref: &runtime::RuntimeRef, config: DevServerConfig, ) -> PyResult> { - let opts: ephemeral_server::TemporalDevServerConfig = config.try_into()?; + let opts: ephemeral_server::TemporalDevServerConfig = config.into(); let runtime = runtime_ref.runtime.clone(); runtime_ref.runtime.future_into_py(py, async move { Ok(EphemeralServerRef { @@ -64,7 +65,7 @@ pub fn start_test_server<'a>( runtime_ref: &runtime::RuntimeRef, config: TestServerConfig, ) -> PyResult> { - let opts: ephemeral_server::TestServerConfig = config.try_into()?; + let opts: ephemeral_server::TestServerConfig = config.into(); let runtime = runtime_ref.runtime.clone(); runtime_ref.runtime.future_into_py(py, async move { Ok(EphemeralServerRef { @@ -109,11 +110,9 @@ impl EphemeralServerRef { } } -impl TryFrom for ephemeral_server::TemporalDevServerConfig { - type Error = PyErr; - - fn try_from(conf: DevServerConfig) -> PyResult { - ephemeral_server::TemporalDevServerConfigBuilder::default() +impl From for ephemeral_server::TemporalDevServerConfig { + fn from(conf: DevServerConfig) -> Self { + ephemeral_server::TemporalDevServerConfig::builder() .exe(if let Some(existing_path) = conf.existing_path { ephemeral_server::EphemeralExe::ExistingPath(existing_path.to_owned()) } else { @@ -132,21 +131,19 @@ impl TryFrom for ephemeral_server::TemporalDevServerConfig { }) .namespace(conf.namespace) .ip(conf.ip) - .port(conf.port) - .db_filename(conf.database_filename) + .maybe_port(conf.port) + .maybe_db_filename(conf.database_filename) .ui(conf.ui) + .maybe_ui_port(conf.ui_port) .log((conf.log_format, conf.log_level)) .extra_args(conf.extra_args) .build() - .map_err(|err| PyValueError::new_err(format!("Invalid Temporalite config: {err}"))) } } -impl TryFrom for ephemeral_server::TestServerConfig { - type Error = PyErr; - - fn try_from(conf: TestServerConfig) -> PyResult { - ephemeral_server::TestServerConfigBuilder::default() +impl From for ephemeral_server::TestServerConfig { + fn from(conf: TestServerConfig) -> Self { + ephemeral_server::TestServerConfig::builder() .exe(if let Some(existing_path) = conf.existing_path { ephemeral_server::EphemeralExe::ExistingPath(existing_path.to_owned()) } else { @@ -163,9 +160,8 @@ impl TryFrom for ephemeral_server::TestServerConfig { ttl: conf.download_ttl_ms.map(Duration::from_millis), } }) - .port(conf.port) + .maybe_port(conf.port) .extra_args(conf.extra_args) .build() - .map_err(|err| PyValueError::new_err(format!("Invalid test server config: {err}"))) } } diff --git a/temporalio/bridge/src/worker.rs b/temporalio/bridge/src/worker.rs index ba68f880a..321dc6560 100644 --- a/temporalio/bridge/src/worker.rs +++ b/temporalio/bridge/src/worker.rs @@ -5,22 +5,22 @@ use prost::Message; use pyo3::exceptions::{PyException, PyRuntimeError, PyValueError}; use pyo3::prelude::*; use pyo3::types::{PyBytes, PyTuple}; -use std::collections::HashMap; use std::collections::HashSet; use std::marker::PhantomData; use std::sync::{Arc, OnceLock}; use std::time::Duration; -use temporal_sdk_core::api::errors::PollError; -use temporal_sdk_core::replay::{HistoryForReplay, ReplayWorkerInput}; -use temporal_sdk_core_api::errors::WorkflowErrorType; -use temporal_sdk_core_api::worker::{ - SlotInfo, SlotInfoTrait, SlotKind, SlotKindType, SlotMarkUsedContext, SlotReleaseContext, - SlotReservationContext, SlotSupplier as SlotSupplierTrait, SlotSupplierPermit, +use temporalio_common::protos::coresdk::workflow_completion::WorkflowActivationCompletion; +use temporalio_common::protos::coresdk::{ + nexus::NexusTaskCompletion, ActivityHeartbeat, ActivityTaskCompletion, +}; +use temporalio_common::protos::temporal::api::history::v1::History; +use temporalio_common::protos::temporal::api::worker::v1::{PluginInfo, StorageDriverInfo}; +use temporalio_sdk_core::replay::{HistoryForReplay, ReplayWorkerInput}; +use temporalio_sdk_core::{ + PollError, SlotInfo, SlotInfoTrait, SlotKind, SlotKindType, SlotMarkUsedContext, + SlotReleaseContext, SlotReservationContext, SlotSupplier as SlotSupplierTrait, + SlotSupplierPermit, WorkflowErrorType, }; -use temporal_sdk_core_api::Worker; -use temporal_sdk_core_protos::coresdk::workflow_completion::WorkflowActivationCompletion; -use temporal_sdk_core_protos::coresdk::{ActivityHeartbeat, ActivityTaskCompletion, nexus::NexusTaskCompletion}; -use temporal_sdk_core_protos::temporal::api::history::v1::History; use tokio::sync::mpsc::{channel, Sender}; use tokio_stream::wrappers::ReceiverStream; use tracing::error; @@ -32,7 +32,7 @@ pyo3::create_exception!(temporal_sdk_bridge, PollShutdownError, PyException); #[pyclass] pub struct WorkerRef { - worker: Option>, + worker: Option>, /// Set upon the call to `validate`, with the task locals for the event loop at that time, which /// is whatever event loop the user is running their worker in. This loop might be needed by /// other rust-created threads that want to run async python code. @@ -51,16 +51,20 @@ pub struct WorkerConfig { workflow_task_poller_behavior: PollerBehavior, nonsticky_to_sticky_poll_ratio: f32, activity_task_poller_behavior: PollerBehavior, - no_remote_activities: bool, + task_types: WorkerTaskTypes, sticky_queue_schedule_to_start_timeout_millis: u64, max_heartbeat_throttle_interval_millis: u64, default_heartbeat_throttle_interval_millis: u64, max_activities_per_second: Option, max_task_queue_activities_per_second: Option, + max_eager_activity_reservations_per_workflow_task: usize, graceful_shutdown_period_millis: u64, nondeterminism_as_workflow_fail: bool, nondeterminism_as_workflow_fail_for_types: HashSet, nexus_task_poller_behavior: PollerBehavior, + plugins: Vec, + storage_drivers: HashSet, + disable_payload_error_limit: bool, } #[derive(FromPyObject)] @@ -75,31 +79,29 @@ pub struct PollerBehaviorAutoscaling { pub initial: usize, } -/// Recreates [temporal_sdk_core_api::worker::PollerBehavior] +/// Recreates [temporalio_sdk_core::PollerBehavior] #[derive(FromPyObject)] pub enum PollerBehavior { SimpleMaximum(PollerBehaviorSimpleMaximum), Autoscaling(PollerBehaviorAutoscaling), } -impl From for temporal_sdk_core_api::worker::PollerBehavior { +impl From for temporalio_sdk_core::PollerBehavior { fn from(value: PollerBehavior) -> Self { match value { PollerBehavior::SimpleMaximum(simple) => { - temporal_sdk_core_api::worker::PollerBehavior::SimpleMaximum(simple.simple_maximum) - } - PollerBehavior::Autoscaling(auto) => { - temporal_sdk_core_api::worker::PollerBehavior::Autoscaling { - minimum: auto.minimum, - maximum: auto.maximum, - initial: auto.initial, - } + temporalio_sdk_core::PollerBehavior::SimpleMaximum(simple.simple_maximum) } + PollerBehavior::Autoscaling(auto) => temporalio_sdk_core::PollerBehavior::Autoscaling { + minimum: auto.minimum, + maximum: auto.maximum, + initial: auto.initial, + }, } } } -/// Recreates [temporal_sdk_core_api::worker::WorkerVersioningStrategy] +/// Recreates [temporalio_sdk_core::WorkerVersioningStrategy] #[derive(FromPyObject)] pub enum WorkerVersioningStrategy { None(WorkerVersioningNone), @@ -112,7 +114,7 @@ pub struct WorkerVersioningNone { pub build_id_no_versioning: String, } -/// Recreates [temporal_sdk_core_api::worker::WorkerDeploymentOptions] +/// Recreates [temporalio_common::worker::WorkerDeploymentOptions] #[derive(FromPyObject)] pub struct WorkerDeploymentOptions { pub version: WorkerDeploymentVersion, @@ -126,15 +128,15 @@ pub struct LegacyBuildIdBased { pub build_id_with_versioning: String, } -/// Recreates [temporal_sdk_core_api::worker::WorkerDeploymentVersion] +/// Recreates [temporalio_common::worker::WorkerDeploymentVersion] #[derive(FromPyObject, IntoPyObject, Clone)] pub struct WorkerDeploymentVersion { pub deployment_name: String, pub build_id: String, } -impl From for WorkerDeploymentVersion { - fn from(version: temporal_sdk_core_api::worker::WorkerDeploymentVersion) -> Self { +impl From for WorkerDeploymentVersion { + fn from(version: temporalio_common::worker::WorkerDeploymentVersion) -> Self { WorkerDeploymentVersion { deployment_name: version.deployment_name, build_id: version.build_id, @@ -147,6 +149,7 @@ pub struct TunerHolder { workflow_slot_supplier: SlotSupplier, activity_slot_supplier: SlotSupplier, local_activity_slot_supplier: SlotSupplier, + nexus_slot_supplier: SlotSupplier, } #[derive(FromPyObject)] @@ -170,6 +173,25 @@ pub struct ResourceBasedSlotSupplier { tuner_config: ResourceBasedTunerConfig, } +#[derive(FromPyObject)] +pub struct WorkerTaskTypes { + enable_workflows: bool, + enable_local_activities: bool, + enable_remote_activities: bool, + enable_nexus: bool, +} + +impl From for temporalio_common::worker::WorkerTaskTypes { + fn from(t: WorkerTaskTypes) -> Self { + Self { + enable_workflows: t.enable_workflows, + enable_local_activities: t.enable_local_activities, + enable_remote_activities: t.enable_remote_activities, + enable_nexus: t.enable_nexus, + } + } +} + #[pyclass] pub struct SlotReserveCtx { #[pyo3(get)] @@ -211,9 +233,9 @@ impl SlotReserveCtx { #[pyclass] pub struct SlotMarkUsedCtx { #[pyo3(get)] - slot_info: PyObject, + slot_info: Py, #[pyo3(get)] - permit: PyObject, + permit: Py, } // NOTE: this is dumb because we already have the generated proto code, we just can't use @@ -248,9 +270,9 @@ pub struct NexusSlotInfo { #[pyclass] pub struct SlotReleaseCtx { #[pyo3(get)] - slot_info: Option, + slot_info: Option>, #[pyo3(get)] - permit: PyObject, + permit: Py, } fn slot_info_to_py_obj<'py>(py: Python<'py>, info: SlotInfo) -> PyResult> { @@ -280,14 +302,14 @@ fn slot_info_to_py_obj<'py>(py: Python<'py>, info: SlotInfo) -> PyResult, + inner: Arc>, } struct CustomSlotSupplierOfType { - inner: Arc, + inner: Arc>, event_loop_task_locals: Arc>, _phantom: PhantomData, } @@ -295,7 +317,7 @@ struct CustomSlotSupplierOfType { #[pymethods] impl CustomSlotSupplier { #[new] - fn new(inner: PyObject) -> Self { + fn new(inner: Py) -> Self { CustomSlotSupplier { inner: Arc::new(inner), } @@ -309,23 +331,23 @@ impl CustomSlotSupplier { #[pyclass] struct CreatedTaskForSlotCallback { - stored_task: Arc>, + stored_task: Arc>>, } #[pymethods] impl CreatedTaskForSlotCallback { - fn __call__(&self, task: PyObject) -> PyResult<()> { + fn __call__(&self, task: Py) -> PyResult<()> { self.stored_task.set(task).expect("must only be set once"); Ok(()) } } struct TaskCanceller { - stored_task: Arc>, + stored_task: Arc>>, } impl TaskCanceller { - fn new(stored_task: Arc>) -> Self { + fn new(stored_task: Arc>>) -> Self { TaskCanceller { stored_task } } } @@ -333,7 +355,7 @@ impl TaskCanceller { impl Drop for TaskCanceller { fn drop(&mut self) { if let Some(task) = self.stored_task.get() { - Python::with_gil(|py| { + Python::attach(|py| { task.call_method0(py, "cancel") .expect("Failed to cancel task"); }); @@ -349,7 +371,7 @@ impl SlotSupplierTrait for CustomSlotSupplierOfType< loop { let stored_task = Arc::new(OnceLock::new()); let _task_canceller = TaskCanceller::new(stored_task.clone()); - let pypermit = match Python::with_gil(|py| { + let pypermit = match Python::attach(|py| { let py_obj = self.inner.bind(py); let called = py_obj.call_method1( "reserve_slot", @@ -384,7 +406,7 @@ impl SlotSupplierTrait for CustomSlotSupplierOfType< } fn try_reserve_slot(&self, ctx: &dyn SlotReservationContext) -> Option { - Python::with_gil(|py| { + Python::attach(|py| { let py_obj = self.inner.bind(py); let pa = py_obj.call_method1( "try_reserve_slot", @@ -405,10 +427,10 @@ impl SlotSupplierTrait for CustomSlotSupplierOfType< } fn mark_slot_used(&self, ctx: &dyn SlotMarkUsedContext) { - if let Err(e) = Python::with_gil(|py| { + if let Err(e) = Python::attach(|py| { let permit = ctx .permit() - .user_data::() + .user_data::>() .map(|o| o.clone_ref(py)) .unwrap_or_else(|| py.None()); let py_obj = self.inner.bind(py); @@ -426,10 +448,10 @@ impl SlotSupplierTrait for CustomSlotSupplierOfType< } fn release_slot(&self, ctx: &dyn SlotReleaseContext) { - if let Err(e) = Python::with_gil(|py| { + if let Err(e) = Python::attach(|py| { let permit = ctx .permit() - .user_data::() + .user_data::>() .map(|o| o.clone_ref(py)) .unwrap_or_else(|| py.None()); let py_obj = self.inner.bind(py); @@ -459,7 +481,7 @@ pub struct ResourceBasedTunerConfig { macro_rules! enter_sync { ($runtime:expr) => { if let Some(subscriber) = $runtime.core.telemetry().trace_subscriber() { - temporal_sdk_core::telemetry::set_trace_subscriber_for_current_thread(subscriber); + temporalio_common::telemetry::set_trace_subscriber_for_current_thread(subscriber); } let _guard = $runtime.core.tokio_handle().enter(); }; @@ -471,12 +493,13 @@ pub fn new_worker( config: WorkerConfig, ) -> PyResult { enter_sync!(runtime_ref.runtime); + runtime_ref.runtime.assert_same_process("create worker")?; let event_loop_task_locals = Arc::new(OnceLock::new()); let config = convert_worker_config(config, event_loop_task_locals.clone())?; - let worker = temporal_sdk_core::init_worker( + let worker = temporalio_sdk_core::init_worker( &runtime_ref.runtime.core, config, - client.retry_client.clone().into_inner(), + client.connection.clone(), ) .context("Failed creating worker")?; Ok(WorkerRef { @@ -492,14 +515,18 @@ pub fn new_replay_worker<'a>( config: WorkerConfig, ) -> PyResult> { enter_sync!(runtime_ref.runtime); + runtime_ref + .runtime + .assert_same_process("create replay worker")?; let event_loop_task_locals = Arc::new(OnceLock::new()); let config = convert_worker_config(config, event_loop_task_locals.clone())?; let (history_pusher, stream) = HistoryPusher::new(runtime_ref.runtime.clone()); let worker = WorkerRef { worker: Some(Arc::new( - temporal_sdk_core::init_replay_worker(ReplayWorkerInput::new(config, stream)).map_err( - |err| PyValueError::new_err(format!("Failed creating replay worker: {err}")), - )?, + temporalio_sdk_core::init_replay_worker(ReplayWorkerInput::new(config, stream)) + .map_err(|err| { + PyValueError::new_err(format!("Failed creating replay worker: {err}")) + })?, )), event_loop_task_locals: Default::default(), runtime: runtime_ref.runtime.clone(), @@ -515,7 +542,8 @@ pub fn new_replay_worker<'a>( #[pymethods] impl WorkerRef { - fn validate<'p>(&self, py: Python<'p>) -> PyResult> { + fn validate<'p>(&self, py: Python<'p>) -> PyResult> { + self.runtime.assert_same_process("use worker")?; let worker = self.worker.as_ref().unwrap().clone(); // Set custom slot supplier task locals so they can run futures. // Event loop is assumed to be running at this point. @@ -526,15 +554,20 @@ impl WorkerRef { .expect("must only be set once"); self.runtime.future_into_py(py, async move { - worker - .validate() - .await - .context("Worker validation failed") - .map_err(Into::into) + let bytes = match worker.validate().await { + Ok(info) => info.encode_to_vec(), + Err(err) => { + return Err(PyRuntimeError::new_err(format!( + "Worker validation failed: {err}" + ))) + } + }; + Ok(bytes) }) } fn poll_workflow_activation<'p>(&self, py: Python<'p>) -> PyResult> { + self.runtime.assert_same_process("use worker")?; let worker = self.worker.as_ref().unwrap().clone(); self.runtime.future_into_py(py, async move { let bytes = match worker.poll_workflow_activation().await { @@ -547,6 +580,7 @@ impl WorkerRef { } fn poll_activity_task<'p>(&self, py: Python<'p>) -> PyResult> { + self.runtime.assert_same_process("use worker")?; let worker = self.worker.as_ref().unwrap().clone(); self.runtime.future_into_py(py, async move { let bytes = match worker.poll_activity_task().await { @@ -559,6 +593,7 @@ impl WorkerRef { } fn poll_nexus_task<'p>(&self, py: Python<'p>) -> PyResult> { + self.runtime.assert_same_process("use worker")?; let worker = self.worker.as_ref().unwrap().clone(); self.runtime.future_into_py(py, async move { let bytes = match worker.poll_nexus_task().await { @@ -604,10 +639,11 @@ impl WorkerRef { }) } - fn complete_nexus_task<'p>(&self, + fn complete_nexus_task<'p>( + &self, py: Python<'p>, proto: &Bound<'_, PyBytes>, -) -> PyResult> { + ) -> PyResult> { let worker = self.worker.as_ref().unwrap().clone(); let completion = NexusTaskCompletion::decode(proto.as_bytes()) .map_err(|err| PyValueError::new_err(format!("Invalid proto: {err}")))?; @@ -624,10 +660,14 @@ impl WorkerRef { enter_sync!(self.runtime); let heartbeat = ActivityHeartbeat::decode(proto.as_bytes()) .map_err(|err| PyValueError::new_err(format!("Invalid proto: {err}")))?; - self.worker - .as_ref() - .unwrap() - .record_activity_heartbeat(heartbeat); + let worker = self.worker.as_ref().unwrap().clone(); + // Detach from the GIL during the core call. Core may block on internal + // locks whose holders can call back into Python (e.g. a custom slot + // supplier's mark_slot_used runs while core's outstanding-activity + // lock is held); holding the GIL here would deadlock the worker. + proto + .py() + .detach(move || worker.record_activity_heartbeat(heartbeat)); Ok(()) } @@ -640,14 +680,17 @@ impl WorkerRef { Ok(()) } - fn replace_client(&self, client: &client::ClientRef) { + fn replace_client(&self, client: &client::ClientRef) -> PyResult<()> { + enter_sync!(self.runtime); self.worker .as_ref() .expect("missing worker") - .replace_client(client.retry_client.clone().into_inner()); + .replace_client(client.connection.clone()) + .map_err(|err| PyValueError::new_err(format!("Failed replacing client: {err}"))) } fn initiate_shutdown(&self) -> PyResult<()> { + enter_sync!(self.runtime); let worker = self.worker.as_ref().unwrap().clone(); worker.initiate_shutdown(); Ok(()) @@ -672,20 +715,20 @@ impl WorkerRef { fn convert_worker_config( conf: WorkerConfig, task_locals: Arc>, -) -> PyResult { +) -> PyResult { let converted_tuner = convert_tuner_holder(conf.tuner, task_locals)?; let converted_versioning_strategy = convert_versioning_strategy(conf.versioning_strategy); - temporal_sdk_core::WorkerConfigBuilder::default() + temporalio_sdk_core::WorkerConfig::builder() .namespace(conf.namespace) .task_queue(conf.task_queue) .versioning_strategy(converted_versioning_strategy) - .client_identity_override(conf.identity_override) + .maybe_client_identity_override(conf.identity_override) .max_cached_workflows(conf.max_cached_workflows) - .workflow_task_poller_behavior(conf.workflow_task_poller_behavior) + .workflow_task_poller_behavior(conf.workflow_task_poller_behavior.into()) .tuner(Arc::new(converted_tuner)) .nonsticky_to_sticky_poll_ratio(conf.nonsticky_to_sticky_poll_ratio) - .activity_task_poller_behavior(conf.activity_task_poller_behavior) - .no_remote_activities(conf.no_remote_activities) + .activity_task_poller_behavior(conf.activity_task_poller_behavior.into()) + .task_types(conf.task_types.into()) .sticky_queue_schedule_to_start_timeout(Duration::from_millis( conf.sticky_queue_schedule_to_start_timeout_millis, )) @@ -695,8 +738,11 @@ fn convert_worker_config( .default_heartbeat_throttle_interval(Duration::from_millis( conf.default_heartbeat_throttle_interval_millis, )) - .max_worker_activities_per_second(conf.max_activities_per_second) - .max_task_queue_activities_per_second(conf.max_task_queue_activities_per_second) + .maybe_max_worker_activities_per_second(conf.max_activities_per_second) + .maybe_max_task_queue_activities_per_second(conf.max_task_queue_activities_per_second) + .max_eager_activity_reservations_per_workflow_task( + conf.max_eager_activity_reservations_per_workflow_task, + ) // Even though grace period is optional, if it is not set then the // auto-cancel-activity behavior of shutdown will not occur, so we // always set it even if 0. @@ -715,9 +761,25 @@ fn convert_worker_config( HashSet::from([WorkflowErrorType::Nondeterminism]), ) }) - .collect::>>(), + .collect(), + ) + .nexus_task_poller_behavior(conf.nexus_task_poller_behavior.into()) + .plugins( + conf.plugins + .into_iter() + .map(|name| PluginInfo { + name, + version: String::new(), + }) + .collect(), + ) + .storage_drivers( + conf.storage_drivers + .into_iter() + .map(|r#type| StorageDriverInfo { r#type }) + .collect::>(), ) - .nexus_task_poller_behavior(conf.nexus_task_poller_behavior) + .disable_payload_error_limit(conf.disable_payload_error_limit) .build() .map_err(|err| PyValueError::new_err(format!("Invalid worker config: {err}"))) } @@ -725,7 +787,7 @@ fn convert_worker_config( fn convert_tuner_holder( holder: TunerHolder, task_locals: Arc>, -) -> PyResult { +) -> PyResult { // Verify all resource-based options are the same if any are set let maybe_wf_resource_opts = if let SlotSupplier::ResourceBased(ref ss) = holder.workflow_slot_supplier { @@ -745,10 +807,17 @@ fn convert_tuner_holder( } else { None }; + let maybe_nexus_resource_opts = + if let SlotSupplier::ResourceBased(ref ss) = holder.nexus_slot_supplier { + Some(&ss.tuner_config) + } else { + None + }; let all_resource_opts = [ maybe_wf_resource_opts, maybe_act_resource_opts, maybe_local_act_resource_opts, + maybe_nexus_resource_opts, ]; let mut set_resource_opts = all_resource_opts.iter().flatten(); let first = set_resource_opts.next(); @@ -763,17 +832,15 @@ fn convert_tuner_holder( )); } - let mut options = temporal_sdk_core::TunerHolderOptionsBuilder::default(); - if let Some(first) = first { - options.resource_based_options( - temporal_sdk_core::ResourceBasedSlotsOptionsBuilder::default() - .target_mem_usage(first.target_memory_usage) - .target_cpu_usage(first.target_cpu_usage) - .build() - .expect("Building ResourceBasedSlotsOptions is infallible"), - ); - }; - options + Ok(temporalio_sdk_core::TunerHolderOptions::builder() + .maybe_resource_based_config(first.map(|first| { + temporalio_sdk_core::ResourceBasedTunerConfig::Options( + temporalio_sdk_core::ResourceBasedSlotsOptions::builder() + .target_mem_usage(first.target_memory_usage) + .target_cpu_usage(first.target_cpu_usage) + .build(), + ) + })) .workflow_slot_options(convert_slot_supplier( holder.workflow_slot_supplier, task_locals.clone(), @@ -784,9 +851,12 @@ fn convert_tuner_holder( )?) .local_activity_slot_options(convert_slot_supplier( holder.local_activity_slot_supplier, + task_locals.clone(), + )?) + .nexus_slot_options(convert_slot_supplier( + holder.nexus_slot_supplier, task_locals, - )?); - Ok(options + )?) .build() .map_err(|e| PyValueError::new_err(format!("Invalid tuner holder options: {e}")))? .build_tuner_holder() @@ -796,56 +866,62 @@ fn convert_tuner_holder( fn convert_slot_supplier( supplier: SlotSupplier, task_locals: Arc>, -) -> PyResult> { +) -> PyResult> { Ok(match supplier { - SlotSupplier::FixedSize(fs) => temporal_sdk_core::SlotSupplierOptions::FixedSize { + SlotSupplier::FixedSize(fs) => temporalio_sdk_core::SlotSupplierOptions::FixedSize { slots: fs.num_slots, }, - SlotSupplier::ResourceBased(ss) => temporal_sdk_core::SlotSupplierOptions::ResourceBased( - temporal_sdk_core::ResourceSlotOptions::new( + SlotSupplier::ResourceBased(ss) => temporalio_sdk_core::SlotSupplierOptions::ResourceBased( + temporalio_sdk_core::ResourceSlotOptions::new( ss.minimum_slots, ss.maximum_slots, Duration::from_millis(ss.ramp_throttle_ms), ), ), - SlotSupplier::Custom(cs) => temporal_sdk_core::SlotSupplierOptions::Custom(Arc::new( - CustomSlotSupplierOfType:: { + SlotSupplier::Custom(cs) => { + temporalio_sdk_core::SlotSupplierOptions::Custom(Arc::new(CustomSlotSupplierOfType::< + SK, + > { inner: cs.inner, event_loop_task_locals: task_locals, _phantom: PhantomData, - }, - )), + })) + } }) } fn convert_versioning_strategy( strategy: WorkerVersioningStrategy, -) -> temporal_sdk_core_api::worker::WorkerVersioningStrategy { +) -> temporalio_sdk_core::WorkerVersioningStrategy { match strategy { - WorkerVersioningStrategy::None(vn) => { - temporal_sdk_core_api::worker::WorkerVersioningStrategy::None { - build_id: vn.build_id_no_versioning, - } - } + WorkerVersioningStrategy::None(vn) => temporalio_sdk_core::WorkerVersioningStrategy::None { + build_id: vn.build_id_no_versioning, + }, WorkerVersioningStrategy::DeploymentBased(options) => { - temporal_sdk_core_api::worker::WorkerVersioningStrategy::WorkerDeploymentBased( - temporal_sdk_core_api::worker::WorkerDeploymentOptions { - version: temporal_sdk_core_api::worker::WorkerDeploymentVersion { + temporalio_sdk_core::WorkerVersioningStrategy::WorkerDeploymentBased( + temporalio_common::worker::WorkerDeploymentOptions::new( + temporalio_common::worker::WorkerDeploymentVersion { deployment_name: options.version.deployment_name, build_id: options.version.build_id, }, - use_worker_versioning: options.use_worker_versioning, - default_versioning_behavior: Some( - options - .default_versioning_behavior - .try_into() - .unwrap_or_default(), - ), - }, + ) + .use_worker_versioning(options.use_worker_versioning) + .maybe_default_versioning_behavior(if options.use_worker_versioning { + Some( + temporalio_common::protos::temporal::api::enums::v1::VersioningBehavior::try_from( + options.default_versioning_behavior, + ) + .unwrap_or_default() + .into(), + ) + } else { + None + }) + .build(), ) } WorkerVersioningStrategy::LegacyBuildIdBased(lb) => { - temporal_sdk_core_api::worker::WorkerVersioningStrategy::LegacyBuildIdBased { + temporalio_sdk_core::WorkerVersioningStrategy::LegacyBuildIdBased { build_id: lb.build_id_with_versioning, } } diff --git a/temporalio/bridge/testing.py b/temporalio/bridge/testing.py index 667ec13ea..e428a33b2 100644 --- a/temporalio/bridge/testing.py +++ b/temporalio/bridge/testing.py @@ -5,8 +5,8 @@ from __future__ import annotations +from collections.abc import Sequence from dataclasses import dataclass -from typing import Optional, Sequence import temporalio.bridge.runtime import temporalio.bridge.temporal_sdk_bridge @@ -16,17 +16,18 @@ class DevServerConfig: """Python representation of the Rust struct for configuring dev server.""" - existing_path: Optional[str] + existing_path: str | None sdk_name: str sdk_version: str download_version: str - download_dest_dir: Optional[str] - download_ttl_ms: Optional[int] + download_dest_dir: str | None + download_ttl_ms: int | None namespace: str ip: str - port: Optional[int] - database_filename: Optional[str] + port: int | None + database_filename: str | None ui: bool + ui_port: int | None log_format: str log_level: str extra_args: Sequence[str] @@ -36,13 +37,13 @@ class DevServerConfig: class TestServerConfig: """Python representation of the Rust struct for configuring test server.""" - existing_path: Optional[str] + existing_path: str | None sdk_name: str sdk_version: str download_version: str - download_dest_dir: Optional[str] - download_ttl_ms: Optional[int] - port: Optional[int] + download_dest_dir: str | None + download_ttl_ms: int | None + port: int | None extra_args: Sequence[str] diff --git a/temporalio/bridge/worker.py b/temporalio/bridge/worker.py index e4cb05eee..4b7f55d09 100644 --- a/temporalio/bridge/worker.py +++ b/temporalio/bridge/worker.py @@ -5,25 +5,12 @@ from __future__ import annotations +from collections.abc import Awaitable, Callable, Sequence from dataclasses import dataclass from typing import ( - TYPE_CHECKING, - Awaitable, - Callable, - List, - Mapping, - Optional, - Sequence, - Set, - Tuple, - Union, + TypeAlias, ) -import google.protobuf.internal.containers -from typing_extensions import TypeAlias - -import temporalio.api.common.v1 -import temporalio.api.history.v1 import temporalio.bridge.client import temporalio.bridge.proto import temporalio.bridge.proto.activity_task @@ -33,11 +20,16 @@ import temporalio.bridge.runtime import temporalio.bridge.temporal_sdk_bridge import temporalio.converter -import temporalio.exceptions +import temporalio.converter._extstore +from temporalio.api.common.v1.message_pb2 import Payload +from temporalio.bridge._visitor_functions import PayloadSequence, VisitorFunctions from temporalio.bridge.temporal_sdk_bridge import ( CustomSlotSupplier as BridgeCustomSlotSupplier, ) -from temporalio.bridge.temporal_sdk_bridge import PollShutdownError # type: ignore +from temporalio.bridge.temporal_sdk_bridge import ( + PollShutdownError, # type: ignore # noqa: F401 +) +from temporalio.worker._command_aware_visitor import CommandAwarePayloadVisitor @dataclass @@ -47,22 +39,27 @@ class WorkerConfig: namespace: str task_queue: str versioning_strategy: WorkerVersioningStrategy - identity_override: Optional[str] + identity_override: str | None max_cached_workflows: int tuner: TunerHolder workflow_task_poller_behavior: PollerBehavior nonsticky_to_sticky_poll_ratio: float activity_task_poller_behavior: PollerBehavior no_remote_activities: bool + task_types: WorkerTaskTypes sticky_queue_schedule_to_start_timeout_millis: int max_heartbeat_throttle_interval_millis: int default_heartbeat_throttle_interval_millis: int - max_activities_per_second: Optional[float] - max_task_queue_activities_per_second: Optional[float] + max_activities_per_second: float | None + max_task_queue_activities_per_second: float | None + max_eager_activity_reservations_per_workflow_task: int graceful_shutdown_period_millis: int nondeterminism_as_workflow_fail: bool - nondeterminism_as_workflow_fail_for_types: Set[str] + nondeterminism_as_workflow_fail_for_types: set[str] nexus_task_poller_behavior: PollerBehavior + plugins: Sequence[str] + storage_drivers: set[str] + disable_payload_error_limit: bool @dataclass @@ -81,10 +78,7 @@ class PollerBehaviorAutoscaling: initial: int -PollerBehavior: TypeAlias = Union[ - PollerBehaviorSimpleMaximum, - PollerBehaviorAutoscaling, -] +PollerBehavior: TypeAlias = PollerBehaviorSimpleMaximum | PollerBehaviorAutoscaling @dataclass @@ -119,11 +113,11 @@ class WorkerVersioningStrategyLegacyBuildIdBased: build_id_with_versioning: str -WorkerVersioningStrategy: TypeAlias = Union[ - WorkerVersioningStrategyNone, - WorkerDeploymentOptions, - WorkerVersioningStrategyLegacyBuildIdBased, -] +WorkerVersioningStrategy: TypeAlias = ( + WorkerVersioningStrategyNone + | WorkerDeploymentOptions + | WorkerVersioningStrategyLegacyBuildIdBased +) @dataclass @@ -151,11 +145,9 @@ class FixedSizeSlotSupplier: num_slots: int -SlotSupplier: TypeAlias = Union[ - FixedSizeSlotSupplier, - ResourceBasedSlotSupplier, - BridgeCustomSlotSupplier, -] +SlotSupplier: TypeAlias = ( + FixedSizeSlotSupplier | ResourceBasedSlotSupplier | BridgeCustomSlotSupplier +) @dataclass @@ -165,6 +157,17 @@ class TunerHolder: workflow_slot_supplier: SlotSupplier activity_slot_supplier: SlotSupplier local_activity_slot_supplier: SlotSupplier + nexus_slot_supplier: SlotSupplier + + +@dataclass +class WorkerTaskTypes: + """Python representation of the Rust struct for worker task types""" + + enable_workflows: bool + enable_local_activities: bool + enable_remote_activities: bool + enable_nexus: bool class Worker: @@ -183,7 +186,7 @@ def create(client: temporalio.bridge.client.Client, config: WorkerConfig) -> Wor def for_replay( runtime: temporalio.bridge.runtime.Runtime, config: WorkerConfig, - ) -> Tuple[Worker, temporalio.bridge.temporal_sdk_bridge.HistoryPusher]: + ) -> tuple[Worker, temporalio.bridge.temporal_sdk_bridge.HistoryPusher]: """Create a bridge replay worker.""" [ replay_worker, @@ -197,9 +200,13 @@ def __init__(self, ref: temporalio.bridge.temporal_sdk_bridge.WorkerRef) -> None """Create SDK core worker from a bridge worker.""" self._ref = ref - async def validate(self) -> None: + async def validate( + self, + ) -> temporalio.bridge.proto.NamespaceInfo: """Validate the bridge worker.""" - await self._ref.validate() + return temporalio.bridge.proto.NamespaceInfo.FromString( + await self._ref.validate() # type: ignore[reportOptionalMemberAccess] + ) async def poll_workflow_activation( self, @@ -207,7 +214,7 @@ async def poll_workflow_activation( """Poll for a workflow activation.""" return ( temporalio.bridge.proto.workflow_activation.WorkflowActivation.FromString( - await self._ref.poll_workflow_activation() + await self._ref.poll_workflow_activation() # type: ignore[reportOptionalMemberAccess] ) ) @@ -216,7 +223,7 @@ async def poll_activity_task( ) -> temporalio.bridge.proto.activity_task.ActivityTask: """Poll for an activity task.""" return temporalio.bridge.proto.activity_task.ActivityTask.FromString( - await self._ref.poll_activity_task() + await self._ref.poll_activity_task() # type: ignore[reportOptionalMemberAccess] ) async def poll_nexus_task( @@ -224,7 +231,7 @@ async def poll_nexus_task( ) -> temporalio.bridge.proto.nexus.NexusTask: """Poll for a nexus task.""" return temporalio.bridge.proto.nexus.NexusTask.FromString( - await self._ref.poll_nexus_task() + await self._ref.poll_nexus_task() # type: ignore[reportOptionalMemberAccess] ) async def complete_workflow_activation( @@ -232,37 +239,37 @@ async def complete_workflow_activation( comp: temporalio.bridge.proto.workflow_completion.WorkflowActivationCompletion, ) -> None: """Complete a workflow activation.""" - await self._ref.complete_workflow_activation(comp.SerializeToString()) + await self._ref.complete_workflow_activation(comp.SerializeToString()) # type: ignore[reportOptionalMemberAccess] async def complete_activity_task( self, comp: temporalio.bridge.proto.ActivityTaskCompletion ) -> None: """Complete an activity task.""" - await self._ref.complete_activity_task(comp.SerializeToString()) + await self._ref.complete_activity_task(comp.SerializeToString()) # type: ignore[reportOptionalMemberAccess] async def complete_nexus_task( self, comp: temporalio.bridge.proto.nexus.NexusTaskCompletion ) -> None: """Complete a nexus task.""" - await self._ref.complete_nexus_task(comp.SerializeToString()) + await self._ref.complete_nexus_task(comp.SerializeToString()) # type: ignore[reportOptionalMemberAccess] def record_activity_heartbeat( self, comp: temporalio.bridge.proto.ActivityHeartbeat ) -> None: """Record an activity heartbeat.""" - self._ref.record_activity_heartbeat(comp.SerializeToString()) + self._ref.record_activity_heartbeat(comp.SerializeToString()) # type: ignore[reportOptionalMemberAccess] def request_workflow_eviction(self, run_id: str) -> None: """Request a workflow be evicted.""" - self._ref.request_workflow_eviction(run_id) + self._ref.request_workflow_eviction(run_id) # type: ignore[reportOptionalMemberAccess] def replace_client(self, client: temporalio.bridge.client.Client) -> None: """Replace the worker client.""" - self._ref.replace_client(client._ref) + self._ref.replace_client(client._ref) # type: ignore[reportOptionalMemberAccess] def initiate_shutdown(self) -> None: """Start shutdown of the worker.""" - self._ref.initiate_shutdown() + self._ref.initiate_shutdown() # type: ignore[reportOptionalMemberAccess] async def finalize_shutdown(self) -> None: """Finalize the worker. @@ -272,257 +279,88 @@ async def finalize_shutdown(self) -> None: """ ref = self._ref self._ref = None - await ref.finalize_shutdown() + await ref.finalize_shutdown() # type: ignore[reportOptionalMemberAccess] -# See https://mypy.readthedocs.io/en/stable/runtime_troubles.html#using-classes-that-are-generic-in-stubs-but-not-at-runtime -if TYPE_CHECKING: - PayloadContainer: TypeAlias = ( - google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ - temporalio.api.common.v1.Payload - ] - ) -else: - PayloadContainer: TypeAlias = ( - google.protobuf.internal.containers.RepeatedCompositeFieldContainer - ) +class _Visitor(VisitorFunctions): + def __init__( + self, + f: Callable[[Sequence[Payload]], Awaitable[list[Payload]]], + ): + self._f = f + async def visit_payload(self, payload: Payload) -> None: + new_payload = (await self._f([payload]))[0] + if new_payload is not payload: + payload.CopyFrom(new_payload) -async def _apply_to_headers( - headers: Mapping[str, temporalio.api.common.v1.Payload], - cb: Callable[ - [Sequence[temporalio.api.common.v1.Payload]], - Awaitable[List[temporalio.api.common.v1.Payload]], - ], -) -> None: - """Apply API payload callback to headers.""" - for payload in headers.values(): - new_payload = (await cb([payload]))[0] - payload.CopyFrom(new_payload) - - -async def _decode_headers( - headers: Mapping[str, temporalio.api.common.v1.Payload], - codec: temporalio.converter.PayloadCodec, -) -> None: - """Decode headers with the given codec.""" - return await _apply_to_headers(headers, codec.decode) - - -async def _encode_headers( - headers: Mapping[str, temporalio.api.common.v1.Payload], - codec: temporalio.converter.PayloadCodec, -) -> None: - """Encode headers with the given codec.""" - return await _apply_to_headers(headers, codec.encode) - - -async def _apply_to_payloads( - payloads: PayloadContainer, - cb: Callable[ - [Sequence[temporalio.api.common.v1.Payload]], - Awaitable[List[temporalio.api.common.v1.Payload]], - ], -) -> None: - """Apply API payload callback to payloads.""" - if len(payloads) == 0: - return - new_payloads = await cb(payloads) - if new_payloads is payloads: - return - del payloads[:] - # TODO(cretz): Copy too expensive? - payloads.extend(new_payloads) - - -async def _apply_to_payload( - payload: temporalio.api.common.v1.Payload, - cb: Callable[ - [Sequence[temporalio.api.common.v1.Payload]], - Awaitable[List[temporalio.api.common.v1.Payload]], - ], -) -> None: - """Apply API payload callback to payload.""" - new_payload = (await cb([payload]))[0] - payload.CopyFrom(new_payload) - - -async def _decode_payloads( - payloads: PayloadContainer, - codec: temporalio.converter.PayloadCodec, -) -> None: - """Decode payloads with the given codec.""" - return await _apply_to_payloads(payloads, codec.decode) - - -async def _decode_payload( - payload: temporalio.api.common.v1.Payload, - codec: temporalio.converter.PayloadCodec, -) -> None: - """Decode a payload with the given codec.""" - return await _apply_to_payload(payload, codec.decode) - - -async def _encode_payloads( - payloads: PayloadContainer, - codec: temporalio.converter.PayloadCodec, -) -> None: - """Encode payloads with the given codec.""" - return await _apply_to_payloads(payloads, codec.encode) - - -async def _encode_payload( - payload: temporalio.api.common.v1.Payload, - codec: temporalio.converter.PayloadCodec, -) -> None: - """Decode a payload with the given codec.""" - return await _apply_to_payload(payload, codec.encode) + async def visit_payloads(self, payloads: PayloadSequence) -> None: + if len(payloads) == 0: + return + new_payloads = await self._f(payloads) + if new_payloads is payloads: + return + del payloads[:] + payloads.extend(new_payloads) async def decode_activation( - act: temporalio.bridge.proto.workflow_activation.WorkflowActivation, - codec: temporalio.converter.PayloadCodec, + activation: temporalio.bridge.proto.workflow_activation.WorkflowActivation, + data_converter: temporalio.converter.DataConverter, decode_headers: bool, -) -> None: - """Decode the given activation with the codec.""" - for job in act.jobs: - if job.HasField("query_workflow"): - await _decode_payloads(job.query_workflow.arguments, codec) - if decode_headers: - await _decode_headers(job.query_workflow.headers, codec) - elif job.HasField("resolve_activity"): - if job.resolve_activity.result.HasField("cancelled"): - await codec.decode_failure( - job.resolve_activity.result.cancelled.failure - ) - elif job.resolve_activity.result.HasField("completed"): - if job.resolve_activity.result.completed.HasField("result"): - await _decode_payload( - job.resolve_activity.result.completed.result, codec - ) - elif job.resolve_activity.result.HasField("failed"): - await codec.decode_failure(job.resolve_activity.result.failed.failure) - elif job.HasField("resolve_child_workflow_execution"): - if job.resolve_child_workflow_execution.result.HasField("cancelled"): - await codec.decode_failure( - job.resolve_child_workflow_execution.result.cancelled.failure - ) - elif job.resolve_child_workflow_execution.result.HasField( - "completed" - ) and job.resolve_child_workflow_execution.result.completed.HasField( - "result" - ): - await _decode_payload( - job.resolve_child_workflow_execution.result.completed.result, codec - ) - elif job.resolve_child_workflow_execution.result.HasField("failed"): - await codec.decode_failure( - job.resolve_child_workflow_execution.result.failed.failure - ) - elif job.HasField("resolve_child_workflow_execution_start"): - if job.resolve_child_workflow_execution_start.HasField("cancelled"): - await codec.decode_failure( - job.resolve_child_workflow_execution_start.cancelled.failure - ) - elif job.HasField("resolve_request_cancel_external_workflow"): - if job.resolve_request_cancel_external_workflow.HasField("failure"): - await codec.decode_failure( - job.resolve_request_cancel_external_workflow.failure - ) - elif job.HasField("resolve_signal_external_workflow"): - if job.resolve_signal_external_workflow.HasField("failure"): - await codec.decode_failure(job.resolve_signal_external_workflow.failure) - elif job.HasField("signal_workflow"): - await _decode_payloads(job.signal_workflow.input, codec) - if decode_headers: - await _decode_headers(job.signal_workflow.headers, codec) - elif job.HasField("initialize_workflow"): - await _decode_payloads(job.initialize_workflow.arguments, codec) - if decode_headers: - await _decode_headers(job.initialize_workflow.headers, codec) - if job.initialize_workflow.HasField("continued_failure"): - await codec.decode_failure(job.initialize_workflow.continued_failure) - for val in job.initialize_workflow.memo.fields.values(): - # This uses API payload not bridge payload - new_payload = (await codec.decode([val]))[0] - # Make a shallow copy, in case new_payload.metadata and val.metadata are - # references to the same memory, e.g. decode() returns its input unchanged. - new_metadata = dict(new_payload.metadata) - val.metadata.clear() - val.metadata.update(new_metadata) - val.data = new_payload.data - elif job.HasField("do_update"): - await _decode_payloads(job.do_update.input, codec) - if decode_headers: - await _decode_headers(job.do_update.headers, codec) + storage_concurrency_limit: int, +) -> temporalio.converter._extstore.StorageOperationMetrics: + """Decode all payloads in the activation. + + Returns: + Metrics from any external storage retrieval operations that occurred. + """ + metrics = temporalio.converter._extstore.StorageOperationMetrics() + with metrics.track(): + await CommandAwarePayloadVisitor( + skip_search_attributes=True, + skip_headers=not decode_headers, + concurrency_limit=storage_concurrency_limit, + ).visit( + _Visitor(data_converter._external_retrieve_payload_sequence), activation + ) + + await CommandAwarePayloadVisitor( + skip_search_attributes=True, + skip_headers=not decode_headers, + ).visit(_Visitor(data_converter._decode_payload_sequence), activation) + + return metrics async def encode_completion( - comp: temporalio.bridge.proto.workflow_completion.WorkflowActivationCompletion, - codec: temporalio.converter.PayloadCodec, + completion: temporalio.bridge.proto.workflow_completion.WorkflowActivationCompletion, + data_converter: temporalio.converter.DataConverter, encode_headers: bool, -) -> None: - """Recursively encode the given completion with the codec.""" - if comp.HasField("failed"): - await codec.encode_failure(comp.failed.failure) - elif comp.HasField("successful"): - for command in comp.successful.commands: - if command.HasField("complete_workflow_execution"): - if command.complete_workflow_execution.HasField("result"): - await _encode_payload( - command.complete_workflow_execution.result, codec - ) - elif command.HasField("continue_as_new_workflow_execution"): - await _encode_payloads( - command.continue_as_new_workflow_execution.arguments, codec - ) - if encode_headers: - await _encode_headers( - command.continue_as_new_workflow_execution.headers, codec - ) - for val in command.continue_as_new_workflow_execution.memo.values(): - await _encode_payload(val, codec) - elif command.HasField("fail_workflow_execution"): - await codec.encode_failure(command.fail_workflow_execution.failure) - elif command.HasField("respond_to_query"): - if command.respond_to_query.HasField("failed"): - await codec.encode_failure(command.respond_to_query.failed) - elif command.respond_to_query.HasField( - "succeeded" - ) and command.respond_to_query.succeeded.HasField("response"): - await _encode_payload( - command.respond_to_query.succeeded.response, codec - ) - elif command.HasField("schedule_activity"): - await _encode_payloads(command.schedule_activity.arguments, codec) - if encode_headers: - await _encode_headers(command.schedule_activity.headers, codec) - elif command.HasField("schedule_local_activity"): - await _encode_payloads(command.schedule_local_activity.arguments, codec) - if encode_headers: - await _encode_headers( - command.schedule_local_activity.headers, codec - ) - elif command.HasField("signal_external_workflow_execution"): - await _encode_payloads( - command.signal_external_workflow_execution.args, codec - ) - if encode_headers: - await _encode_headers( - command.signal_external_workflow_execution.headers, codec - ) - elif command.HasField("start_child_workflow_execution"): - await _encode_payloads( - command.start_child_workflow_execution.input, codec - ) - if encode_headers: - await _encode_headers( - command.start_child_workflow_execution.headers, codec - ) - for val in command.start_child_workflow_execution.memo.values(): - await _encode_payload(val, codec) - elif command.HasField("update_response"): - if command.update_response.HasField("completed"): - await _encode_payload(command.update_response.completed, codec) - elif command.update_response.HasField("rejected"): - await codec.encode_failure(command.update_response.rejected) + storage_concurrency_limit: int, +) -> temporalio.converter._extstore.StorageOperationMetrics: + """Encode all payloads in the completion. + + Returns: + Metrics from any external storage store operations that occurred. + """ + await CommandAwarePayloadVisitor( + skip_search_attributes=True, + skip_headers=not encode_headers, + ).visit( + _Visitor(data_converter._encode_payload_sequence), + completion, + ) + + metrics = temporalio.converter._extstore.StorageOperationMetrics() + with metrics.track(): + await CommandAwarePayloadVisitor( + skip_search_attributes=True, + skip_headers=not encode_headers, + concurrency_limit=storage_concurrency_limit, + ).visit( + _Visitor(data_converter._external_store_payload_sequence), + completion, + ) + + return metrics diff --git a/temporalio/client.py b/temporalio/client.py deleted file mode 100644 index abdfb1462..000000000 --- a/temporalio/client.py +++ /dev/null @@ -1,7461 +0,0 @@ -"""Client for accessing Temporal.""" - -from __future__ import annotations - -import abc -import asyncio -import copy -import dataclasses -import inspect -import json -import re -import uuid -import warnings -from abc import ABC, abstractmethod -from asyncio import Future -from dataclasses import dataclass -from datetime import datetime, timedelta, timezone -from enum import Enum, IntEnum -from typing import ( - Any, - AsyncIterator, - Awaitable, - Callable, - Dict, - FrozenSet, - Generic, - Iterable, - Mapping, - Optional, - Sequence, - Text, - Tuple, - Type, - Union, - cast, - overload, -) - -import google.protobuf.duration_pb2 -import google.protobuf.json_format -import google.protobuf.timestamp_pb2 -from google.protobuf.internal.containers import MessageMap -from typing_extensions import Concatenate, Required, TypedDict - -import temporalio.api.common.v1 -import temporalio.api.enums.v1 -import temporalio.api.errordetails.v1 -import temporalio.api.failure.v1 -import temporalio.api.history.v1 -import temporalio.api.schedule.v1 -import temporalio.api.sdk.v1 -import temporalio.api.taskqueue.v1 -import temporalio.api.update.v1 -import temporalio.api.workflow.v1 -import temporalio.api.workflowservice.v1 -import temporalio.common -import temporalio.converter -import temporalio.exceptions -import temporalio.nexus -import temporalio.nexus._operation_context -import temporalio.runtime -import temporalio.service -import temporalio.workflow -from temporalio.activity import ActivityCancellationDetails -from temporalio.service import ( - HttpConnectProxyConfig, - KeepAliveConfig, - RetryConfig, - RPCError, - RPCStatusCode, - TLSConfig, -) - -from .common import HeaderCodecBehavior -from .types import ( - AnyType, - LocalReturnType, - MethodAsyncNoParam, - MethodAsyncSingleParam, - MethodSyncOrAsyncNoParam, - MethodSyncOrAsyncSingleParam, - MultiParamSpec, - ParamType, - ReturnType, - SelfType, -) - - -class Client: - """Client for accessing Temporal. - - Most users will use :py:meth:`connect` to create a client. The - :py:attr:`service` property provides access to a raw gRPC client. To create - another client, like for a different namespace, :py:func:`Client` may be - directly instantiated with a :py:attr:`service` of another. - - Clients are not thread-safe and should only be used in the event loop they - are first connected in. If a client needs to be used from another thread - than where it was created, make sure the event loop where it was created is - captured, and then call :py:func:`asyncio.run_coroutine_threadsafe` with the - client call and that event loop. - - Clients do not work across forks since runtimes do not work across forks. - """ - - @staticmethod - async def connect( - target_host: str, - *, - namespace: str = "default", - api_key: Optional[str] = None, - data_converter: temporalio.converter.DataConverter = temporalio.converter.DataConverter.default, - plugins: Sequence[Plugin] = [], - interceptors: Sequence[Interceptor] = [], - default_workflow_query_reject_condition: Optional[ - temporalio.common.QueryRejectCondition - ] = None, - tls: Union[bool, TLSConfig] = False, - retry_config: Optional[RetryConfig] = None, - keep_alive_config: Optional[KeepAliveConfig] = KeepAliveConfig.default, - rpc_metadata: Mapping[str, str] = {}, - identity: Optional[str] = None, - lazy: bool = False, - runtime: Optional[temporalio.runtime.Runtime] = None, - http_connect_proxy_config: Optional[HttpConnectProxyConfig] = None, - header_codec_behavior: HeaderCodecBehavior = HeaderCodecBehavior.NO_CODEC, - ) -> Client: - """Connect to a Temporal server. - - Args: - target_host: ``host:port`` for the Temporal server. For local - development, this is often "localhost:7233". - namespace: Namespace to use for client calls. - api_key: API key for Temporal. This becomes the "Authorization" - HTTP header with "Bearer " prepended. This is only set if RPC - metadata doesn't already have an "authorization" key. - data_converter: Data converter to use for all data conversions - to/from payloads. - plugins: Set of plugins that are chained together to allow - intercepting and modifying client creation and service connection. - The earlier plugins wrap the later ones. - - Any plugins that also implement - :py:class:`temporalio.worker.Plugin` will be used as worker - plugins too so they should not be given when creating a - worker. - interceptors: Set of interceptors that are chained together to allow - intercepting of client calls. The earlier interceptors wrap the - later ones. - - Any interceptors that also implement - :py:class:`temporalio.worker.Interceptor` will be used as worker - interceptors too so they should not be given when creating a - worker. - default_workflow_query_reject_condition: The default rejection - condition for workflow queries if not set during query. See - :py:meth:`WorkflowHandle.query` for details on the rejection - condition. - tls: If false, the default, do not use TLS. If true, use system - default TLS configuration. If TLS configuration present, that - TLS configuration will be used. - retry_config: Retry configuration for direct service calls (when - opted in) or all high-level calls made by this client (which all - opt-in to retries by default). If unset, a default retry - configuration is used. - keep_alive_config: Keep-alive configuration for the client - connection. Default is to check every 30s and kill the - connection if a response doesn't come back in 15s. Can be set to - ``None`` to disable. - rpc_metadata: Headers to use for all calls to the server. Keys here - can be overriden by per-call RPC metadata keys. - identity: Identity for this client. If unset, a default is created - based on the version of the SDK. - lazy: If true, the client will not connect until the first call is - attempted or a worker is created with it. Lazy clients cannot be - used for workers. - runtime: The runtime for this client, or the default if unset. - http_connect_proxy_config: Configuration for HTTP CONNECT proxy. - header_codec_behavior: Encoding behavior for headers sent by the client. - """ - connect_config = temporalio.service.ConnectConfig( - target_host=target_host, - api_key=api_key, - tls=tls, - retry_config=retry_config, - keep_alive_config=keep_alive_config, - rpc_metadata=rpc_metadata, - identity=identity or "", - lazy=lazy, - runtime=runtime, - http_connect_proxy_config=http_connect_proxy_config, - ) - - root_plugin: Plugin = _RootPlugin() - for plugin in reversed(plugins): - plugin.init_client_plugin(root_plugin) - root_plugin = plugin - - service_client = await root_plugin.connect_service_client(connect_config) - - return Client( - service_client, - namespace=namespace, - data_converter=data_converter, - interceptors=interceptors, - default_workflow_query_reject_condition=default_workflow_query_reject_condition, - header_codec_behavior=header_codec_behavior, - plugins=plugins, - ) - - def __init__( - self, - service_client: temporalio.service.ServiceClient, - *, - namespace: str = "default", - data_converter: temporalio.converter.DataConverter = temporalio.converter.DataConverter.default, - plugins: Sequence[Plugin] = [], - interceptors: Sequence[Interceptor] = [], - default_workflow_query_reject_condition: Optional[ - temporalio.common.QueryRejectCondition - ] = None, - header_codec_behavior: HeaderCodecBehavior = HeaderCodecBehavior.NO_CODEC, - ): - """Create a Temporal client from a service client. - - See :py:meth:`connect` for details on the parameters. - """ - # Store the config for tracking - config = ClientConfig( - service_client=service_client, - namespace=namespace, - data_converter=data_converter, - interceptors=interceptors, - default_workflow_query_reject_condition=default_workflow_query_reject_condition, - header_codec_behavior=header_codec_behavior, - plugins=plugins, - ) - - root_plugin: Plugin = _RootPlugin() - for plugin in reversed(plugins): - plugin.init_client_plugin(root_plugin) - root_plugin = plugin - - self._init_from_config(root_plugin.configure_client(config)) - - def _init_from_config(self, config: ClientConfig): - self._config = config - - # Iterate over interceptors in reverse building the impl - self._impl: OutboundInterceptor = _ClientImpl(self) - for interceptor in reversed(list(self._config["interceptors"])): - self._impl = interceptor.intercept_client(self._impl) - - def config(self) -> ClientConfig: - """Config, as a dictionary, used to create this client. - - This makes a shallow copy of the config each call. - """ - config = self._config.copy() - config["interceptors"] = list(config["interceptors"]) - return config - - @property - def service_client(self) -> temporalio.service.ServiceClient: - """Raw gRPC service client.""" - return self._config["service_client"] - - @property - def workflow_service(self) -> temporalio.service.WorkflowService: - """Raw gRPC workflow service client.""" - return self._config["service_client"].workflow_service - - @property - def operator_service(self) -> temporalio.service.OperatorService: - """Raw gRPC operator service client.""" - return self._config["service_client"].operator_service - - @property - def test_service(self) -> temporalio.service.TestService: - """Raw gRPC test service client.""" - return self._config["service_client"].test_service - - @property - def namespace(self) -> str: - """Namespace used in calls by this client.""" - return self._config["namespace"] - - @property - def identity(self) -> str: - """Identity used in calls by this client.""" - return self._config["service_client"].config.identity - - @property - def data_converter(self) -> temporalio.converter.DataConverter: - """Data converter used by this client.""" - return self._config["data_converter"] - - @property - def rpc_metadata(self) -> Mapping[str, str]: - """Headers for every call made by this client. - - Do not use mutate this mapping. Rather, set this property with an - entirely new mapping to change the headers. - """ - return self.service_client.config.rpc_metadata - - @rpc_metadata.setter - def rpc_metadata(self, value: Mapping[str, str]) -> None: - """Update the headers for this client. - - Do not mutate this mapping after set. Rather, set an entirely new - mapping if changes are needed. - """ - # Update config and perform update - self.service_client.config.rpc_metadata = value - self.service_client.update_rpc_metadata(value) - - @property - def api_key(self) -> Optional[str]: - """API key for every call made by this client.""" - return self.service_client.config.api_key - - @api_key.setter - def api_key(self, value: Optional[str]) -> None: - """Update the API key for this client. - - This is only set if RPCmetadata doesn't already have an "authorization" - key. - """ - # Update config and perform update - self.service_client.config.api_key = value - self.service_client.update_api_key(value) - - # Overload for no-param workflow - @overload - async def start_workflow( - self, - workflow: MethodAsyncNoParam[SelfType, ReturnType], - *, - id: str, - task_queue: str, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.TypedSearchAttributes, - temporalio.common.SearchAttributes, - ] - ] = None, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - start_delay: Optional[timedelta] = None, - start_signal: Optional[str] = None, - start_signal_args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - request_eager_start: bool = False, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: Optional[temporalio.common.VersioningOverride] = None, - ) -> WorkflowHandle[SelfType, ReturnType]: ... - - # Overload for single-param workflow - @overload - async def start_workflow( - self, - workflow: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], - arg: ParamType, - *, - id: str, - task_queue: str, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.TypedSearchAttributes, - temporalio.common.SearchAttributes, - ] - ] = None, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - start_delay: Optional[timedelta] = None, - start_signal: Optional[str] = None, - start_signal_args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - request_eager_start: bool = False, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: Optional[temporalio.common.VersioningOverride] = None, - ) -> WorkflowHandle[SelfType, ReturnType]: ... - - # Overload for multi-param workflow - @overload - async def start_workflow( - self, - workflow: Callable[ - Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType] - ], - *, - args: Sequence[Any], - id: str, - task_queue: str, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.TypedSearchAttributes, - temporalio.common.SearchAttributes, - ] - ] = None, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - start_delay: Optional[timedelta] = None, - start_signal: Optional[str] = None, - start_signal_args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - request_eager_start: bool = False, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: Optional[temporalio.common.VersioningOverride] = None, - ) -> WorkflowHandle[SelfType, ReturnType]: ... - - # Overload for string-name workflow - @overload - async def start_workflow( - self, - workflow: str, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: str, - task_queue: str, - result_type: Optional[Type] = None, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.TypedSearchAttributes, - temporalio.common.SearchAttributes, - ] - ] = None, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - start_delay: Optional[timedelta] = None, - start_signal: Optional[str] = None, - start_signal_args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - request_eager_start: bool = False, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: Optional[temporalio.common.VersioningOverride] = None, - ) -> WorkflowHandle[Any, Any]: ... - - async def start_workflow( - self, - workflow: Union[str, Callable[..., Awaitable[Any]]], - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: str, - task_queue: str, - result_type: Optional[Type] = None, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.TypedSearchAttributes, - temporalio.common.SearchAttributes, - ] - ] = None, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - start_delay: Optional[timedelta] = None, - start_signal: Optional[str] = None, - start_signal_args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - request_eager_start: bool = False, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: Optional[temporalio.common.VersioningOverride] = None, - # The following options should not be considered part of the public API. They - # are deliberately not exposed in overloads, and are not subject to any - # backwards compatibility guarantees. - callbacks: Sequence[Callback] = [], - workflow_event_links: Sequence[ - temporalio.api.common.v1.Link.WorkflowEvent - ] = [], - request_id: Optional[str] = None, - stack_level: int = 2, - ) -> WorkflowHandle[Any, Any]: - """Start a workflow and return its handle. - - Args: - workflow: String name or class method decorated with - ``@workflow.run`` for the workflow to start. - arg: Single argument to the workflow. - args: Multiple arguments to the workflow. Cannot be set if arg is. - id: Unique identifier for the workflow execution. - task_queue: Task queue to run the workflow on. - result_type: For string workflows, this can set the specific result - type hint to deserialize into. - execution_timeout: Total workflow execution timeout including - retries and continue as new. - run_timeout: Timeout of a single workflow run. - task_timeout: Timeout of a single workflow task. - id_conflict_policy: Behavior when a workflow is currently running with the same ID. - Default is UNSPECIFIED, which effectively means fail the start attempt. - Set to USE_EXISTING for idempotent deduplication on workflow ID. - Cannot be set if ``id_reuse_policy`` is set to TERMINATE_IF_RUNNING. - id_reuse_policy: Behavior when a closed workflow with the same ID exists. - Default is ALLOW_DUPLICATE. - retry_policy: Retry policy for the workflow. - cron_schedule: See https://docs.temporal.io/docs/content/what-is-a-temporal-cron-job/ - memo: Memo for the workflow. - search_attributes: Search attributes for the workflow. The - dictionary form of this is deprecated, use - :py:class:`temporalio.common.TypedSearchAttributes`. - static_summary: A single-line fixed summary for this workflow execution that may appear - in the UI/CLI. This can be in single-line Temporal markdown format. - static_details: General fixed details for this workflow execution that may appear in - UI/CLI. This can be in Temporal markdown format and can span multiple lines. This is - a fixed value on the workflow that cannot be updated. For details that can be - updated, use :py:meth:`temporalio.workflow.get_current_details` within the workflow. - start_delay: Amount of time to wait before starting the workflow. - This does not work with ``cron_schedule``. - start_signal: If present, this signal is sent as signal-with-start - instead of traditional workflow start. - start_signal_args: Arguments for start_signal if start_signal - present. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - request_eager_start: Potentially reduce the latency to start this workflow by - encouraging the server to start it on a local worker running with - this same client. - priority: Priority of the workflow execution. - versioning_override: Overrides the versioning behavior for this workflow. - - Returns: - A workflow handle to the started workflow. - - Raises: - temporalio.exceptions.WorkflowAlreadyStartedError: Workflow has - already been started. - RPCError: Workflow could not be started for some other reason. - """ - temporalio.common._warn_on_deprecated_search_attributes( - search_attributes, stack_level=stack_level - ) - name, result_type_from_type_hint = ( - temporalio.workflow._Definition.get_name_and_result_type(workflow) - ) - return await self._impl.start_workflow( - StartWorkflowInput( - workflow=name, - args=temporalio.common._arg_or_args(arg, args), - id=id, - task_queue=task_queue, - execution_timeout=execution_timeout, - run_timeout=run_timeout, - task_timeout=task_timeout, - id_reuse_policy=id_reuse_policy, - id_conflict_policy=id_conflict_policy, - retry_policy=retry_policy, - cron_schedule=cron_schedule, - memo=memo, - search_attributes=search_attributes, - start_delay=start_delay, - versioning_override=versioning_override, - headers={}, - static_summary=static_summary, - static_details=static_details, - start_signal=start_signal, - start_signal_args=start_signal_args, - ret_type=result_type or result_type_from_type_hint, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - request_eager_start=request_eager_start, - priority=priority, - callbacks=callbacks, - workflow_event_links=workflow_event_links, - request_id=request_id, - ) - ) - - # Overload for no-param workflow - @overload - async def execute_workflow( - self, - workflow: MethodAsyncNoParam[SelfType, ReturnType], - *, - id: str, - task_queue: str, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.TypedSearchAttributes, - temporalio.common.SearchAttributes, - ] - ] = None, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - start_delay: Optional[timedelta] = None, - start_signal: Optional[str] = None, - start_signal_args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - request_eager_start: bool = False, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: Optional[temporalio.common.VersioningOverride] = None, - ) -> ReturnType: ... - - # Overload for single-param workflow - @overload - async def execute_workflow( - self, - workflow: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], - arg: ParamType, - *, - id: str, - task_queue: str, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.TypedSearchAttributes, - temporalio.common.SearchAttributes, - ] - ] = None, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - start_delay: Optional[timedelta] = None, - start_signal: Optional[str] = None, - start_signal_args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - request_eager_start: bool = False, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: Optional[temporalio.common.VersioningOverride] = None, - ) -> ReturnType: ... - - # Overload for multi-param workflow - @overload - async def execute_workflow( - self, - workflow: Callable[ - Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType] - ], - *, - args: Sequence[Any], - id: str, - task_queue: str, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.TypedSearchAttributes, - temporalio.common.SearchAttributes, - ] - ] = None, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - start_delay: Optional[timedelta] = None, - start_signal: Optional[str] = None, - start_signal_args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - request_eager_start: bool = False, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: Optional[temporalio.common.VersioningOverride] = None, - ) -> ReturnType: ... - - # Overload for string-name workflow - @overload - async def execute_workflow( - self, - workflow: str, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: str, - task_queue: str, - result_type: Optional[Type] = None, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.TypedSearchAttributes, - temporalio.common.SearchAttributes, - ] - ] = None, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - start_delay: Optional[timedelta] = None, - start_signal: Optional[str] = None, - start_signal_args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - request_eager_start: bool = False, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: Optional[temporalio.common.VersioningOverride] = None, - ) -> Any: ... - - async def execute_workflow( - self, - workflow: Union[str, Callable[..., Awaitable[Any]]], - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: str, - task_queue: str, - result_type: Optional[Type] = None, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.TypedSearchAttributes, - temporalio.common.SearchAttributes, - ] - ] = None, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - start_delay: Optional[timedelta] = None, - start_signal: Optional[str] = None, - start_signal_args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - request_eager_start: bool = False, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: Optional[temporalio.common.VersioningOverride] = None, - ) -> Any: - """Start a workflow and wait for completion. - - This is a shortcut for :py:meth:`start_workflow` + - :py:meth:`WorkflowHandle.result`. - """ - return await ( - # We have to tell MyPy to ignore errors here because we want to call - # the non-@overload form of this and MyPy does not support that - await self.start_workflow( # type: ignore - workflow, # type: ignore[arg-type] - arg, - args=args, - task_queue=task_queue, - result_type=result_type, - id=id, - execution_timeout=execution_timeout, - run_timeout=run_timeout, - task_timeout=task_timeout, - id_reuse_policy=id_reuse_policy, - id_conflict_policy=id_conflict_policy, - retry_policy=retry_policy, - cron_schedule=cron_schedule, - memo=memo, - search_attributes=search_attributes, - static_summary=static_summary, - static_details=static_details, - start_delay=start_delay, - start_signal=start_signal, - start_signal_args=start_signal_args, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - request_eager_start=request_eager_start, - priority=priority, - versioning_override=versioning_override, - stack_level=3, - ) - ).result() - - def get_workflow_handle( - self, - workflow_id: str, - *, - run_id: Optional[str] = None, - first_execution_run_id: Optional[str] = None, - result_type: Optional[Type] = None, - ) -> WorkflowHandle[Any, Any]: - """Get a workflow handle to an existing workflow by its ID. - - Args: - workflow_id: Workflow ID to get a handle to. - run_id: Run ID that will be used for all calls. - first_execution_run_id: First execution run ID used for cancellation - and termination. - result_type: The result type to deserialize into if known. - - Returns: - The workflow handle. - """ - return WorkflowHandle( - self, - workflow_id, - run_id=run_id, - result_run_id=run_id, - first_execution_run_id=first_execution_run_id, - result_type=result_type, - ) - - def get_workflow_handle_for( - self, - workflow: Union[ - MethodAsyncNoParam[SelfType, ReturnType], - MethodAsyncSingleParam[SelfType, Any, ReturnType], - ], - workflow_id: str, - *, - run_id: Optional[str] = None, - first_execution_run_id: Optional[str] = None, - ) -> WorkflowHandle[SelfType, ReturnType]: - """Get a typed workflow handle to an existing workflow by its ID. - - This is the same as :py:meth:`get_workflow_handle` but typed. - - Args: - workflow: The workflow run method to use for typing the handle. - workflow_id: Workflow ID to get a handle to. - run_id: Run ID that will be used for all calls. - first_execution_run_id: First execution run ID used for cancellation - and termination. - - Returns: - The workflow handle. - """ - defn = temporalio.workflow._Definition.must_from_run_fn(workflow) - return self.get_workflow_handle( - workflow_id, - run_id=run_id, - first_execution_run_id=first_execution_run_id, - result_type=defn.ret_type, - ) - - # Overload for no-param update - @overload - async def execute_update_with_start_workflow( - self, - update: temporalio.workflow.UpdateMethodMultiParam[[SelfType], LocalReturnType], - *, - start_workflow_operation: WithStartWorkflowOperation[SelfType, Any], - id: Optional[str] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> LocalReturnType: ... - - # Overload for single-param update - @overload - async def execute_update_with_start_workflow( - self, - update: temporalio.workflow.UpdateMethodMultiParam[ - [SelfType, ParamType], LocalReturnType - ], - arg: ParamType, - *, - start_workflow_operation: WithStartWorkflowOperation[SelfType, Any], - id: Optional[str] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> LocalReturnType: ... - - # Overload for multi-param update - @overload - async def execute_update_with_start_workflow( - self, - update: temporalio.workflow.UpdateMethodMultiParam[ - MultiParamSpec, LocalReturnType - ], - *, - args: MultiParamSpec.args, # pyright: ignore - start_workflow_operation: WithStartWorkflowOperation[SelfType, Any], - id: Optional[str] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> LocalReturnType: ... - - # Overload for string-name update - @overload - async def execute_update_with_start_workflow( - self, - update: str, - arg: Any = temporalio.common._arg_unset, - *, - start_workflow_operation: WithStartWorkflowOperation[Any, Any], - args: Sequence[Any] = [], - id: Optional[str] = None, - result_type: Optional[Type] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> Any: ... - - async def execute_update_with_start_workflow( - self, - update: Union[str, Callable], - arg: Any = temporalio.common._arg_unset, - *, - start_workflow_operation: WithStartWorkflowOperation[Any, Any], - args: Sequence[Any] = [], - id: Optional[str] = None, - result_type: Optional[Type] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> Any: - """Send an update-with-start request and wait for the update to complete. - - A WorkflowIDConflictPolicy must be set in the start_workflow_operation. If the - specified workflow execution is not running, a new workflow execution is started - and the update is sent in the first workflow task. Alternatively if the specified - workflow execution is running then, if the WorkflowIDConflictPolicy is - USE_EXISTING, the update is issued against the specified workflow, and if the - WorkflowIDConflictPolicy is FAIL, an error is returned. This call will block until - the update has completed, and return the update result. Note that this means that - the call will not return successfully until the update has been delivered to a - worker. - - Args: - update: Update function or name on the workflow. arg: Single argument to the - update. - args: Multiple arguments to the update. Cannot be set if arg is. - start_workflow_operation: a WithStartWorkflowOperation definining the - WorkflowIDConflictPolicy and how to start the workflow in the event that a - workflow is started. - id: ID of the update. If not set, the default is a new UUID. - result_type: For string updates, this can set the specific result - type hint to deserialize into. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - - Raises: - WorkflowUpdateFailedError: If the update failed. - WorkflowUpdateRPCTimeoutOrCancelledError: This update call timed out - or was cancelled. This doesn't mean the update itself was timed out or - cancelled. - - RPCError: There was some issue starting the workflow or sending the update to - the workflow. - """ - handle = await self._start_update_with_start( - update, - arg, - args=args, - start_workflow_operation=start_workflow_operation, - wait_for_stage=WorkflowUpdateStage.COMPLETED, - id=id, - result_type=result_type, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - return await handle.result() - - # Overload for no-param start update - @overload - async def start_update_with_start_workflow( - self, - update: temporalio.workflow.UpdateMethodMultiParam[[SelfType], LocalReturnType], - *, - start_workflow_operation: WithStartWorkflowOperation[SelfType, Any], - wait_for_stage: WorkflowUpdateStage, - id: Optional[str] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> WorkflowUpdateHandle[LocalReturnType]: ... - - # Overload for single-param start update - @overload - async def start_update_with_start_workflow( - self, - update: temporalio.workflow.UpdateMethodMultiParam[ - [SelfType, ParamType], LocalReturnType - ], - arg: ParamType, - *, - start_workflow_operation: WithStartWorkflowOperation[SelfType, Any], - wait_for_stage: WorkflowUpdateStage, - id: Optional[str] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> WorkflowUpdateHandle[LocalReturnType]: ... - - # Overload for multi-param start update - @overload - async def start_update_with_start_workflow( - self, - update: temporalio.workflow.UpdateMethodMultiParam[ - MultiParamSpec, LocalReturnType - ], - *, - args: MultiParamSpec.args, # pyright: ignore - start_workflow_operation: WithStartWorkflowOperation[SelfType, Any], - wait_for_stage: WorkflowUpdateStage, - id: Optional[str] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> WorkflowUpdateHandle[LocalReturnType]: ... - - # Overload for string-name start update - @overload - async def start_update_with_start_workflow( - self, - update: str, - arg: Any = temporalio.common._arg_unset, - *, - start_workflow_operation: WithStartWorkflowOperation[Any, Any], - wait_for_stage: WorkflowUpdateStage, - args: Sequence[Any] = [], - id: Optional[str] = None, - result_type: Optional[Type] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> WorkflowUpdateHandle[Any]: ... - - async def start_update_with_start_workflow( - self, - update: Union[str, Callable], - arg: Any = temporalio.common._arg_unset, - *, - start_workflow_operation: WithStartWorkflowOperation[Any, Any], - wait_for_stage: WorkflowUpdateStage, - args: Sequence[Any] = [], - id: Optional[str] = None, - result_type: Optional[Type] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> WorkflowUpdateHandle[Any]: - """Send an update-with-start request and wait for it to be accepted. - - A WorkflowIDConflictPolicy must be set in the start_workflow_operation. If the - specified workflow execution is not running, a new workflow execution is started - and the update is sent in the first workflow task. Alternatively if the specified - workflow execution is running then, if the WorkflowIDConflictPolicy is - USE_EXISTING, the update is issued against the specified workflow, and if the - WorkflowIDConflictPolicy is FAIL, an error is returned. This call will block until - the update has been accepted, and return a WorkflowUpdateHandle. Note that this - means that the call will not return successfully until the update has been - delivered to a worker. - - .. warning:: - This API is experimental - - Args: - update: Update function or name on the workflow. arg: Single argument to the - update. - args: Multiple arguments to the update. Cannot be set if arg is. - start_workflow_operation: a WithStartWorkflowOperation definining the - WorkflowIDConflictPolicy and how to start the workflow in the event that a - workflow is started. - wait_for_stage: Required stage to wait until returning: either ACCEPTED or - COMPLETED. ADMITTED is not currently supported. See - https://docs.temporal.io/workflows#update for more details. - id: ID of the update. If not set, the default is a new UUID. - result_type: For string updates, this can set the specific result - type hint to deserialize into. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - - Raises: - WorkflowUpdateFailedError: If the update failed. - WorkflowUpdateRPCTimeoutOrCancelledError: This update call timed out - or was cancelled. This doesn't mean the update itself was timed out or - cancelled. - - RPCError: There was some issue starting the workflow or sending the update to - the workflow. - """ - return await self._start_update_with_start( - update, - arg, - wait_for_stage=wait_for_stage, - args=args, - id=id, - result_type=result_type, - start_workflow_operation=start_workflow_operation, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - - async def _start_update_with_start( - self, - update: Union[str, Callable], - arg: Any = temporalio.common._arg_unset, - *, - wait_for_stage: WorkflowUpdateStage, - args: Sequence[Any] = [], - id: Optional[str] = None, - result_type: Optional[Type] = None, - start_workflow_operation: WithStartWorkflowOperation[SelfType, ReturnType], - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> WorkflowUpdateHandle[Any]: - if wait_for_stage == WorkflowUpdateStage.ADMITTED: - raise ValueError("ADMITTED wait stage not supported") - - if start_workflow_operation._used: - raise RuntimeError("WithStartWorkflowOperation cannot be reused") - start_workflow_operation._used = True - - update_name, result_type_from_type_hint = ( - temporalio.workflow._UpdateDefinition.get_name_and_result_type(update) - ) - - update_input = UpdateWithStartUpdateWorkflowInput( - update_id=id, - update=update_name, - args=temporalio.common._arg_or_args(arg, args), - headers={}, - ret_type=result_type or result_type_from_type_hint, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - wait_for_stage=wait_for_stage, - ) - - def on_start( - start_response: temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse, - ): - start_workflow_operation._workflow_handle.set_result( - WorkflowHandle( - self, - start_workflow_operation._start_workflow_input.id, - first_execution_run_id=start_response.run_id, - result_run_id=start_response.run_id, - result_type=start_workflow_operation._start_workflow_input.ret_type, - ) - ) - - def on_start_error( - error: BaseException, - ): - start_workflow_operation._workflow_handle.set_exception(error) - - input = StartWorkflowUpdateWithStartInput( - start_workflow_input=start_workflow_operation._start_workflow_input, - update_workflow_input=update_input, - _on_start=on_start, - _on_start_error=on_start_error, - ) - - return await self._impl.start_update_with_start_workflow(input) - - def list_workflows( - self, - query: Optional[str] = None, - *, - limit: Optional[int] = None, - page_size: int = 1000, - next_page_token: Optional[bytes] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> WorkflowExecutionAsyncIterator: - """List workflows. - - This does not make a request until the first iteration is attempted. - Therefore any errors will not occur until then. - - Args: - query: A Temporal visibility list filter. See Temporal documentation - concerning visibility list filters including behavior when left - unset. - limit: Maximum number of workflows to return. If unset, all - workflows are returned. Only applies if using the - returned :py:class:`WorkflowExecutionAsyncIterator`. - as an async iterator. - page_size: Maximum number of results for each page. - next_page_token: A previously obtained next page token if doing - pagination. Usually not needed as the iterator automatically - starts from the beginning. - rpc_metadata: Headers used on each RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for each RPC call. - - Returns: - An async iterator that can be used with ``async for``. - """ - return self._impl.list_workflows( - ListWorkflowsInput( - query=query, - page_size=page_size, - next_page_token=next_page_token, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - limit=limit, - ) - ) - - async def count_workflows( - self, - query: Optional[str] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> WorkflowExecutionCount: - """Count workflows. - - Args: - query: A Temporal visibility filter. See Temporal documentation - concerning visibility list filters. - rpc_metadata: Headers used on each RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for each RPC call. - - Returns: - Count of workflows. - """ - return await self._impl.count_workflows( - CountWorkflowsInput( - query=query, rpc_metadata=rpc_metadata, rpc_timeout=rpc_timeout - ) - ) - - @overload - def get_async_activity_handle( - self, *, workflow_id: str, run_id: Optional[str], activity_id: str - ) -> AsyncActivityHandle: - pass - - @overload - def get_async_activity_handle(self, *, task_token: bytes) -> AsyncActivityHandle: - pass - - def get_async_activity_handle( - self, - *, - workflow_id: Optional[str] = None, - run_id: Optional[str] = None, - activity_id: Optional[str] = None, - task_token: Optional[bytes] = None, - ) -> AsyncActivityHandle: - """Get an async activity handle. - - Either the workflow_id, run_id, and activity_id can be provided, or a - singular task_token can be provided. - - Args: - workflow_id: Workflow ID for the activity. Cannot be set if - task_token is set. - run_id: Run ID for the activity. Cannot be set if task_token is set. - activity_id: ID for the activity. Cannot be set if task_token is - set. - task_token: Task token for the activity. Cannot be set if any of the - id parameters are set. - - Returns: - A handle that can be used for completion or heartbeat. - """ - if task_token is not None: - if workflow_id is not None or run_id is not None or activity_id is not None: - raise ValueError("Task token cannot be present with other IDs") - return AsyncActivityHandle(self, task_token) - elif workflow_id is not None: - if activity_id is None: - raise ValueError( - "Workflow ID, run ID, and activity ID must all be given together" - ) - return AsyncActivityHandle( - self, - AsyncActivityIDReference( - workflow_id=workflow_id, run_id=run_id, activity_id=activity_id - ), - ) - raise ValueError("Task token or workflow/run/activity ID must be present") - - async def create_schedule( - self, - id: str, - schedule: Schedule, - *, - trigger_immediately: bool = False, - backfill: Sequence[ScheduleBackfill] = [], - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.TypedSearchAttributes, - temporalio.common.SearchAttributes, - ] - ] = None, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> ScheduleHandle: - """Create a schedule and return its handle. - - Args: - id: Unique identifier of the schedule. - schedule: Schedule to create. - trigger_immediately: If true, trigger one action immediately when - creating the schedule. - backfill: Set of time periods to take actions on as if that time - passed right now. - memo: Memo for the schedule. Memo for a scheduled workflow is part - of the schedule action. - search_attributes: Search attributes for the schedule. Search - attributes for a scheduled workflow are part of the scheduled - action. The dictionary form of this is DEPRECATED, use - :py:class:`temporalio.common.TypedSearchAttributes`. - static_summary: A single-line fixed summary for this workflow execution that may appear - in the UI/CLI. This can be in single-line Temporal markdown format. - static_details: General fixed details for this workflow execution that may appear in - UI/CLI. This can be in Temporal markdown format and can span multiple lines. This is - a fixed value on the workflow that cannot be updated. For details that can be - updated, use :py:meth:`temporalio.workflow.get_current_details` within the workflow. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - - Returns: - A handle to the created schedule. - - Raises: - ScheduleAlreadyRunningError: If a schedule with this ID is already - running. - """ - temporalio.common._warn_on_deprecated_search_attributes(search_attributes) - return await self._impl.create_schedule( - CreateScheduleInput( - id=id, - schedule=schedule, - trigger_immediately=trigger_immediately, - backfill=backfill, - memo=memo, - search_attributes=search_attributes, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - ) - - def get_schedule_handle(self, id: str) -> ScheduleHandle: - """Get a schedule handle for the given ID.""" - return ScheduleHandle(self, id) - - async def list_schedules( - self, - query: Optional[str] = None, - *, - page_size: int = 1000, - next_page_token: Optional[bytes] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> ScheduleAsyncIterator: - """List schedules. - - This does not make a request until the first iteration is attempted. - Therefore any errors will not occur until then. - - Note, this list is eventually consistent. Therefore if a schedule is - added or deleted, it may not be available in the list immediately. - - Args: - page_size: Maximum number of results for each page. - query: A Temporal visibility list filter. See Temporal documentation - concerning visibility list filters including behavior when left - unset. - next_page_token: A previously obtained next page token if doing - pagination. Usually not needed as the iterator automatically - starts from the beginning. - rpc_metadata: Headers used on each RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for each RPC call. - - Returns: - An async iterator that can be used with ``async for``. - """ - return self._impl.list_schedules( - ListSchedulesInput( - page_size=page_size, - next_page_token=next_page_token, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - query=query, - ) - ) - - async def update_worker_build_id_compatibility( - self, - task_queue: str, - operation: BuildIdOp, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> None: - """Used to add new Build IDs or otherwise update the relative compatibility of Build Ids as - defined on a specific task queue for the Worker Versioning feature. - - For more on this feature, see https://docs.temporal.io/workers#worker-versioning - - .. warning:: - This API is experimental - - Args: - task_queue: The task queue to target. - operation: The operation to perform. - rpc_metadata: Headers used on each RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for each RPC call. - """ - return await self._impl.update_worker_build_id_compatibility( - UpdateWorkerBuildIdCompatibilityInput( - task_queue, - operation, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - ) - - async def get_worker_build_id_compatibility( - self, - task_queue: str, - max_sets: Optional[int] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> WorkerBuildIdVersionSets: - """Get the Build ID compatibility sets for a specific task queue. - - For more on this feature, see https://docs.temporal.io/workers#worker-versioning - - .. warning:: - This API is experimental - - Args: - task_queue: The task queue to target. - max_sets: The maximum number of sets to return. If not specified, all sets will be - returned. - rpc_metadata: Headers used on each RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for each RPC call. - """ - return await self._impl.get_worker_build_id_compatibility( - GetWorkerBuildIdCompatibilityInput( - task_queue, - max_sets, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - ) - - async def get_worker_task_reachability( - self, - build_ids: Sequence[str], - task_queues: Sequence[str] = [], - reachability_type: Optional[TaskReachabilityType] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> WorkerTaskReachability: - """Determine if some Build IDs for certain Task Queues could have tasks dispatched to them. - - For more on this feature, see https://docs.temporal.io/workers#worker-versioning - - .. warning:: - This API is experimental - - Args: - build_ids: The Build IDs to query the reachability of. At least one must be specified. - task_queues: Task Queues to restrict the query to. If not specified, all Task Queues - will be searched. When requesting a large number of task queues or all task queues - associated with the given Build IDs in a namespace, all Task Queues will be listed - in the response but some of them may not contain reachability information due to a - server enforced limit. When reaching the limit, task queues that reachability - information could not be retrieved for will be marked with a ``NotFetched`` entry in - {@link BuildIdReachability.taskQueueReachability}. The caller may issue another call - to get the reachability for those task queues. - reachability_type: The kind of reachability this request is concerned with. - rpc_metadata: Headers used on each RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for each RPC call. - """ - return await self._impl.get_worker_task_reachability( - GetWorkerTaskReachabilityInput( - build_ids, - task_queues, - reachability_type, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - ) - - -class ClientConfig(TypedDict, total=False): - """TypedDict of config originally passed to :py:meth:`Client`.""" - - service_client: Required[temporalio.service.ServiceClient] - namespace: Required[str] - data_converter: Required[temporalio.converter.DataConverter] - interceptors: Required[Sequence[Interceptor]] - default_workflow_query_reject_condition: Required[ - Optional[temporalio.common.QueryRejectCondition] - ] - header_codec_behavior: Required[HeaderCodecBehavior] - plugins: Required[Sequence[Plugin]] - - -class WorkflowHistoryEventFilterType(IntEnum): - """Type of history events to get for a workflow. - - See :py:class:`temporalio.api.enums.v1.HistoryEventFilterType`. - """ - - ALL_EVENT = int( - temporalio.api.enums.v1.HistoryEventFilterType.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT - ) - CLOSE_EVENT = int( - temporalio.api.enums.v1.HistoryEventFilterType.HISTORY_EVENT_FILTER_TYPE_CLOSE_EVENT - ) - - -class WorkflowHandle(Generic[SelfType, ReturnType]): - """Handle for interacting with a workflow. - - This is usually created via :py:meth:`Client.get_workflow_handle` or - returned from :py:meth:`Client.start_workflow`. - """ - - def __init__( - self, - client: Client, - id: str, - *, - run_id: Optional[str] = None, - result_run_id: Optional[str] = None, - first_execution_run_id: Optional[str] = None, - result_type: Optional[Type] = None, - start_workflow_response: Optional[ - Union[ - temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse, - temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse, - ] - ] = None, - ) -> None: - """Create workflow handle.""" - self._client = client - self._id = id - self._run_id = run_id - self._result_run_id = result_run_id - self._first_execution_run_id = first_execution_run_id - self._result_type = result_type - self._start_workflow_response = start_workflow_response - self.__temporal_eagerly_started = False - - @property - def id(self) -> str: - """ID for the workflow.""" - return self._id - - @property - def run_id(self) -> Optional[str]: - """If present, run ID used to ensure that requested operations apply - to this exact run. - - This is only created via :py:meth:`Client.get_workflow_handle`. - :py:meth:`Client.start_workflow` will not set this value. - - This cannot be mutated. If a different run ID is needed, - :py:meth:`Client.get_workflow_handle` must be used instead. - """ - return self._run_id - - @property - def result_run_id(self) -> Optional[str]: - """Run ID used for :py:meth:`result` calls if present to ensure result - is for a workflow starting from this run. - - When this handle is created via :py:meth:`Client.get_workflow_handle`, - this is the same as run_id. When this handle is created via - :py:meth:`Client.start_workflow`, this value will be the resulting run - ID. - - This cannot be mutated. If a different run ID is needed, - :py:meth:`Client.get_workflow_handle` must be used instead. - """ - return self._result_run_id - - @property - def first_execution_run_id(self) -> Optional[str]: - """Run ID used to ensure requested operations apply to a workflow ID - started with this run ID. - - This can be set when using :py:meth:`Client.get_workflow_handle`. When - :py:meth:`Client.start_workflow` is called without a start signal, this - is set to the resulting run. - - This cannot be mutated. If a different first execution run ID is needed, - :py:meth:`Client.get_workflow_handle` must be used instead. - """ - return self._first_execution_run_id - - async def result( - self, - *, - follow_runs: bool = True, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> ReturnType: - """Wait for result of the workflow. - - This will use :py:attr:`result_run_id` if present to base the result on. - To use another run ID, a new handle must be created via - :py:meth:`Client.get_workflow_handle`. - - Args: - follow_runs: If true (default), workflow runs will be continually - fetched, until the most recent one is found. If false, return - the result from the first run targeted by the request if that run - ends in a result, otherwise raise an exception. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for each RPC call. Note, - this is the timeout for each history RPC call not this overall - function. - - Returns: - Result of the workflow after being converted by the data converter. - - Raises: - WorkflowFailureError: Workflow failed, was cancelled, was - terminated, or timed out. Use the - :py:attr:`WorkflowFailureError.cause` to see the underlying - reason. - Exception: Other possible failures during result fetching. - """ - # We have to maintain our own run ID because it can change if we follow - # executions - hist_run_id = self._result_run_id - while True: - async for event in self._fetch_history_events_for_run( - hist_run_id, - wait_new_event=True, - event_filter_type=WorkflowHistoryEventFilterType.CLOSE_EVENT, - skip_archival=True, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ): - if event.HasField("workflow_execution_completed_event_attributes"): - complete_attr = event.workflow_execution_completed_event_attributes - # Follow execution - if follow_runs and complete_attr.new_execution_run_id: - hist_run_id = complete_attr.new_execution_run_id - break - # Ignoring anything after the first response like TypeScript - type_hints = [self._result_type] if self._result_type else None - results = await self._client.data_converter.decode_wrapper( - complete_attr.result, - type_hints, - ) - if not results: - return cast(ReturnType, None) - elif len(results) > 1: - warnings.warn(f"Expected single result, got {len(results)}") - return cast(ReturnType, results[0]) - elif event.HasField("workflow_execution_failed_event_attributes"): - fail_attr = event.workflow_execution_failed_event_attributes - # Follow execution - if follow_runs and fail_attr.new_execution_run_id: - hist_run_id = fail_attr.new_execution_run_id - break - raise WorkflowFailureError( - cause=await self._client.data_converter.decode_failure( - fail_attr.failure - ), - ) - elif event.HasField("workflow_execution_canceled_event_attributes"): - cancel_attr = event.workflow_execution_canceled_event_attributes - raise WorkflowFailureError( - cause=temporalio.exceptions.CancelledError( - "Workflow cancelled", - *( - await self._client.data_converter.decode_wrapper( - cancel_attr.details - ) - ), - ) - ) - elif event.HasField("workflow_execution_terminated_event_attributes"): - term_attr = event.workflow_execution_terminated_event_attributes - raise WorkflowFailureError( - cause=temporalio.exceptions.TerminatedError( - term_attr.reason or "Workflow terminated", - *( - await self._client.data_converter.decode_wrapper( - term_attr.details - ) - ), - ), - ) - elif event.HasField("workflow_execution_timed_out_event_attributes"): - time_attr = event.workflow_execution_timed_out_event_attributes - # Follow execution - if follow_runs and time_attr.new_execution_run_id: - hist_run_id = time_attr.new_execution_run_id - break - raise WorkflowFailureError( - cause=temporalio.exceptions.TimeoutError( - "Workflow timed out", - type=temporalio.exceptions.TimeoutType.START_TO_CLOSE, - last_heartbeat_details=[], - ), - ) - elif event.HasField( - "workflow_execution_continued_as_new_event_attributes" - ): - cont_attr = ( - event.workflow_execution_continued_as_new_event_attributes - ) - if not cont_attr.new_execution_run_id: - raise RuntimeError( - "Unexpectedly missing new run ID from continue as new" - ) - # Follow execution - if follow_runs: - hist_run_id = cont_attr.new_execution_run_id - break - raise WorkflowContinuedAsNewError(cont_attr.new_execution_run_id) - # This is reached on break which means that there's a different run - # ID if we're following. If there's not, it's an error because no - # event was given (should never happen). - if hist_run_id is None: - raise RuntimeError("No completion event found") - - async def cancel( - self, - *, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> None: - """Cancel the workflow. - - This will issue a cancellation for :py:attr:`run_id` if present. This - call will make sure to use the run chain starting from - :py:attr:`first_execution_run_id` if present. To create handles with - these values, use :py:meth:`Client.get_workflow_handle`. - - .. warning:: - Handles created as a result of :py:meth:`Client.start_workflow` with - a start signal will cancel the latest workflow with the same - workflow ID even if it is unrelated to the started workflow. - - Args: - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - - Raises: - RPCError: Workflow could not be cancelled. - """ - await self._client._impl.cancel_workflow( - CancelWorkflowInput( - id=self._id, - run_id=self._run_id, - first_execution_run_id=self._first_execution_run_id, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - ) - - async def describe( - self, - *, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> WorkflowExecutionDescription: - """Get workflow details. - - This will get details for :py:attr:`run_id` if present. To use a - different run ID, create a new handle with via - :py:meth:`Client.get_workflow_handle`. - - .. warning:: - Handles created as a result of :py:meth:`Client.start_workflow` will - describe the latest workflow with the same workflow ID even if it is - unrelated to the started workflow. - - Args: - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - - Returns: - Workflow details. - - Raises: - RPCError: Workflow details could not be fetched. - """ - return await self._client._impl.describe_workflow( - DescribeWorkflowInput( - id=self._id, - run_id=self._run_id, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - ) - - async def fetch_history( - self, - *, - event_filter_type: WorkflowHistoryEventFilterType = WorkflowHistoryEventFilterType.ALL_EVENT, - skip_archival: bool = False, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> WorkflowHistory: - """Get workflow history. - - This is a shortcut for :py:meth:`fetch_history_events` that just fetches - all events. - """ - return WorkflowHistory( - workflow_id=self.id, - events=[ - v - async for v in self.fetch_history_events( - event_filter_type=event_filter_type, - skip_archival=skip_archival, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - ], - ) - - def fetch_history_events( - self, - *, - page_size: Optional[int] = None, - next_page_token: Optional[bytes] = None, - wait_new_event: bool = False, - event_filter_type: WorkflowHistoryEventFilterType = WorkflowHistoryEventFilterType.ALL_EVENT, - skip_archival: bool = False, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> WorkflowHistoryEventAsyncIterator: - """Get workflow history events as an async iterator. - - This does not make a request until the first iteration is attempted. - Therefore any errors will not occur until then. - - Args: - page_size: Maximum amount to fetch per request if any maximum. - next_page_token: A specific page token to fetch. - wait_new_event: Whether the event fetching request will wait for new - events or just return right away. - event_filter_type: Which events to obtain. - skip_archival: Whether to skip archival. - rpc_metadata: Headers used on each RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for each RPC call. - - Returns: - An async iterator that doesn't begin fetching until iterated on. - """ - return self._fetch_history_events_for_run( - self._run_id, - page_size=page_size, - next_page_token=next_page_token, - wait_new_event=wait_new_event, - event_filter_type=event_filter_type, - skip_archival=skip_archival, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - - def _fetch_history_events_for_run( - self, - run_id: Optional[str], - *, - page_size: Optional[int] = None, - next_page_token: Optional[bytes] = None, - wait_new_event: bool = False, - event_filter_type: WorkflowHistoryEventFilterType = WorkflowHistoryEventFilterType.ALL_EVENT, - skip_archival: bool = False, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> WorkflowHistoryEventAsyncIterator: - return self._client._impl.fetch_workflow_history_events( - FetchWorkflowHistoryEventsInput( - id=self._id, - run_id=run_id, - page_size=page_size, - next_page_token=next_page_token, - wait_new_event=wait_new_event, - event_filter_type=event_filter_type, - skip_archival=skip_archival, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - ) - - # Overload for no-param query - @overload - async def query( - self, - query: MethodSyncOrAsyncNoParam[SelfType, LocalReturnType], - *, - reject_condition: Optional[temporalio.common.QueryRejectCondition] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> LocalReturnType: ... - - # Overload for single-param query - @overload - async def query( - self, - query: MethodSyncOrAsyncSingleParam[SelfType, ParamType, LocalReturnType], - arg: ParamType, - *, - reject_condition: Optional[temporalio.common.QueryRejectCondition] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> LocalReturnType: ... - - # Overload for multi-param query - @overload - async def query( - self, - query: Callable[ - Concatenate[SelfType, MultiParamSpec], - Union[Awaitable[LocalReturnType], LocalReturnType], - ], - *, - args: Sequence[Any], - reject_condition: Optional[temporalio.common.QueryRejectCondition] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> LocalReturnType: ... - - # Overload for string-name query - @overload - async def query( - self, - query: str, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - result_type: Optional[Type] = None, - reject_condition: Optional[temporalio.common.QueryRejectCondition] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> Any: ... - - async def query( - self, - query: Union[str, Callable], - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - result_type: Optional[Type] = None, - reject_condition: Optional[temporalio.common.QueryRejectCondition] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> Any: - """Query the workflow. - - This will query for :py:attr:`run_id` if present. To use a different - run ID, create a new handle with - :py:meth:`Client.get_workflow_handle`. - - .. warning:: - Handles created as a result of :py:meth:`Client.start_workflow` will - query the latest workflow with the same workflow ID even if it is - unrelated to the started workflow. - - Args: - query: Query function or name on the workflow. - arg: Single argument to the query. - args: Multiple arguments to the query. Cannot be set if arg is. - result_type: For string queries, this can set the specific result - type hint to deserialize into. - reject_condition: Condition for rejecting the query. If unset/None, - defaults to the client's default (which is defaulted to None). - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - - Returns: - Result of the query. - - Raises: - WorkflowQueryRejectedError: A query reject condition was satisfied. - RPCError: Workflow details could not be fetched. - """ - query_name: str - ret_type = result_type - if callable(query): - defn = temporalio.workflow._QueryDefinition.from_fn(query) - if not defn: - raise RuntimeError( - f"Query definition not found on {query.__qualname__}, " - "is it decorated with @workflow.query?" - ) - elif not defn.name: - raise RuntimeError("Cannot invoke dynamic query definition") - # TODO(cretz): Check count/type of args at runtime? - query_name = defn.name - ret_type = defn.ret_type - else: - query_name = str(query) - - return await self._client._impl.query_workflow( - QueryWorkflowInput( - id=self._id, - run_id=self._run_id, - query=query_name, - args=temporalio.common._arg_or_args(arg, args), - reject_condition=reject_condition - or self._client._config["default_workflow_query_reject_condition"], - headers={}, - ret_type=ret_type, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - ) - - # Overload for no-param signal - @overload - async def signal( - self, - signal: MethodSyncOrAsyncNoParam[SelfType, None], - *, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> None: ... - - # Overload for single-param signal - @overload - async def signal( - self, - signal: MethodSyncOrAsyncSingleParam[SelfType, ParamType, None], - arg: ParamType, - *, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> None: ... - - # Overload for multi-param signal - @overload - async def signal( - self, - signal: Callable[ - Concatenate[SelfType, MultiParamSpec], Union[Awaitable[None], None] - ], - *, - args: Sequence[Any], - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> None: ... - - # Overload for string-name signal - @overload - async def signal( - self, - signal: str, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> None: ... - - async def signal( - self, - signal: Union[str, Callable], - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> None: - """Send a signal to the workflow. - - This will signal for :py:attr:`run_id` if present. To use a different - run ID, create a new handle with via - :py:meth:`Client.get_workflow_handle`. - - .. warning:: - Handles created as a result of :py:meth:`Client.start_workflow` will - signal the latest workflow with the same workflow ID even if it is - unrelated to the started workflow. - - Args: - signal: Signal function or name on the workflow. - arg: Single argument to the signal. - args: Multiple arguments to the signal. Cannot be set if arg is. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - - Raises: - RPCError: Workflow could not be signalled. - """ - await self._client._impl.signal_workflow( - SignalWorkflowInput( - id=self._id, - run_id=self._run_id, - signal=temporalio.workflow._SignalDefinition.must_name_from_fn_or_str( - signal - ), - args=temporalio.common._arg_or_args(arg, args), - headers={}, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - ) - - async def terminate( - self, - *args: Any, - reason: Optional[str] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> None: - """Terminate the workflow. - - This will issue a termination for :py:attr:`run_id` if present. This - call will make sure to use the run chain starting from - :py:attr:`first_execution_run_id` if present. To create handles with - these values, use :py:meth:`Client.get_workflow_handle`. - - .. warning:: - Handles created as a result of :py:meth:`Client.start_workflow` with - a start signal will terminate the latest workflow with the same - workflow ID even if it is unrelated to the started workflow. - - Args: - args: Details to store on the termination. - reason: Reason for the termination. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - - Raises: - RPCError: Workflow could not be terminated. - """ - await self._client._impl.terminate_workflow( - TerminateWorkflowInput( - id=self._id, - run_id=self._run_id, - args=args, - reason=reason, - first_execution_run_id=self._first_execution_run_id, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - ) - - # Overload for no-param update - @overload - async def execute_update( - self, - update: temporalio.workflow.UpdateMethodMultiParam[[SelfType], LocalReturnType], - *, - id: Optional[str] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> LocalReturnType: ... - - # Overload for single-param update - @overload - async def execute_update( - self, - update: temporalio.workflow.UpdateMethodMultiParam[ - [SelfType, ParamType], LocalReturnType - ], - arg: ParamType, - *, - id: Optional[str] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> LocalReturnType: ... - - # Overload for multi-param update - @overload - async def execute_update( - self, - update: temporalio.workflow.UpdateMethodMultiParam[ - MultiParamSpec, LocalReturnType - ], - *, - args: MultiParamSpec.args, # pyright: ignore - id: Optional[str] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> LocalReturnType: ... - - # Overload for string-name update - @overload - async def execute_update( - self, - update: str, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: Optional[str] = None, - result_type: Optional[Type] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> Any: ... - - async def execute_update( - self, - update: Union[str, Callable], - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: Optional[str] = None, - result_type: Optional[Type] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> Any: - """Send an update request to the workflow and wait for it to complete. - - This will target the workflow with :py:attr:`run_id` if present. To use a - different run ID, create a new handle with via :py:meth:`Client.get_workflow_handle`. - - Args: - update: Update function or name on the workflow. - arg: Single argument to the update. - args: Multiple arguments to the update. Cannot be set if arg is. - id: ID of the update. If not set, the default is a new UUID. - result_type: For string updates, this can set the specific result - type hint to deserialize into. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - - Raises: - WorkflowUpdateFailedError: If the update failed. - WorkflowUpdateRPCTimeoutOrCancelledError: This update call timed out - or was cancelled. This doesn't mean the update itself was timed - out or cancelled. - RPCError: There was some issue sending the update to the workflow. - """ - handle = await self._start_update( - update, - arg, - args=args, - wait_for_stage=WorkflowUpdateStage.COMPLETED, - id=id, - result_type=result_type, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - return await handle.result() - - # Overload for no-param start update - @overload - async def start_update( - self, - update: temporalio.workflow.UpdateMethodMultiParam[[SelfType], LocalReturnType], - *, - wait_for_stage: WorkflowUpdateStage, - id: Optional[str] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> WorkflowUpdateHandle[LocalReturnType]: ... - - # Overload for single-param start update - @overload - async def start_update( - self, - update: temporalio.workflow.UpdateMethodMultiParam[ - [SelfType, ParamType], LocalReturnType - ], - arg: ParamType, - *, - wait_for_stage: WorkflowUpdateStage, - id: Optional[str] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> WorkflowUpdateHandle[LocalReturnType]: ... - - # Overload for multi-param start update - @overload - async def start_update( - self, - update: temporalio.workflow.UpdateMethodMultiParam[ - MultiParamSpec, LocalReturnType - ], - *, - args: MultiParamSpec.args, # pyright: ignore - wait_for_stage: WorkflowUpdateStage, - id: Optional[str] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> WorkflowUpdateHandle[LocalReturnType]: ... - - # Overload for string-name start update - @overload - async def start_update( - self, - update: str, - arg: Any = temporalio.common._arg_unset, - *, - wait_for_stage: WorkflowUpdateStage, - args: Sequence[Any] = [], - id: Optional[str] = None, - result_type: Optional[Type] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> WorkflowUpdateHandle[Any]: ... - - async def start_update( - self, - update: Union[str, Callable], - arg: Any = temporalio.common._arg_unset, - *, - wait_for_stage: WorkflowUpdateStage, - args: Sequence[Any] = [], - id: Optional[str] = None, - result_type: Optional[Type] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> WorkflowUpdateHandle[Any]: - """Send an update request to the workflow and return a handle to it. - - This will target the workflow with :py:attr:`run_id` if present. To use a - different run ID, create a new handle with via :py:meth:`Client.get_workflow_handle`. - - Args: - update: Update function or name on the workflow. arg: Single argument to the - update. - wait_for_stage: Required stage to wait until returning: either ACCEPTED or - COMPLETED. ADMITTED is not currently supported. See - https://docs.temporal.io/workflows#update for more details. - args: Multiple arguments to the update. Cannot be set if arg is. - id: ID of the update. If not set, the default is a new UUID. - result_type: For string updates, this can set the specific result - type hint to deserialize into. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - - Raises: - WorkflowUpdateRPCTimeoutOrCancelledError: This update call timed out - or was cancelled. This doesn't mean the update itself was timed out or - cancelled. - RPCError: There was some issue sending the update to the workflow. - """ - return await self._start_update( - update, - arg, - wait_for_stage=wait_for_stage, - args=args, - id=id, - result_type=result_type, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - - async def _start_update( - self, - update: Union[str, Callable], - arg: Any = temporalio.common._arg_unset, - *, - wait_for_stage: WorkflowUpdateStage, - args: Sequence[Any] = [], - id: Optional[str] = None, - result_type: Optional[Type] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> WorkflowUpdateHandle[Any]: - if wait_for_stage == WorkflowUpdateStage.ADMITTED: - raise ValueError("ADMITTED wait stage not supported") - - update_name, result_type_from_type_hint = ( - temporalio.workflow._UpdateDefinition.get_name_and_result_type(update) - ) - - return await self._client._impl.start_workflow_update( - StartWorkflowUpdateInput( - id=self._id, - run_id=self._run_id, - first_execution_run_id=self.first_execution_run_id, - update_id=id, - update=update_name, - args=temporalio.common._arg_or_args(arg, args), - headers={}, - ret_type=result_type or result_type_from_type_hint, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - wait_for_stage=wait_for_stage, - ) - ) - - def get_update_handle( - self, - id: str, - *, - workflow_run_id: Optional[str] = None, - result_type: Optional[Type] = None, - ) -> WorkflowUpdateHandle[Any]: - """Get a handle for an update. The handle can be used to wait on the - update result. - - Users may prefer the more typesafe :py:meth:`get_update_handle_for` - which accepts an update definition. - - Args: - id: Update ID to get a handle to. - workflow_run_id: Run ID to tie the handle to. If this is not set, - the :py:attr:`run_id` will be used. - result_type: The result type to deserialize into if known. - - Returns: - The update handle. - """ - return WorkflowUpdateHandle( - self._client, - id, - self._id, - workflow_run_id=workflow_run_id or self._run_id, - result_type=result_type, - ) - - def get_update_handle_for( - self, - update: temporalio.workflow.UpdateMethodMultiParam[Any, LocalReturnType], - id: str, - *, - workflow_run_id: Optional[str] = None, - ) -> WorkflowUpdateHandle[LocalReturnType]: - """Get a typed handle for an update. The handle can be used to wait on - the update result. - - This is the same as :py:meth:`get_update_handle` but typed. - - Args: - update: The update method to use for typing the handle. - id: Update ID to get a handle to. - workflow_run_id: Run ID to tie the handle to. If this is not set, - the :py:attr:`run_id` will be used. - - Returns: - The update handle. - """ - return self.get_update_handle( - id, workflow_run_id=workflow_run_id, result_type=update._defn.ret_type - ) - - -class WithStartWorkflowOperation(Generic[SelfType, ReturnType]): - """Defines a start-workflow operation used by update-with-start requests. - - Update-With-Start allows you to send an update to a workflow, while starting the - workflow if necessary. - """ - - # Overload for no-param workflow, with_start - @overload - def __init__( - self, - workflow: MethodAsyncNoParam[SelfType, ReturnType], - *, - id: str, - task_queue: str, - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.TypedSearchAttributes, - temporalio.common.SearchAttributes, - ] - ] = None, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - start_delay: Optional[timedelta] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: Optional[temporalio.common.VersioningOverride] = None, - ) -> None: ... - - # Overload for single-param workflow, with_start - @overload - def __init__( - self, - workflow: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], - arg: ParamType, - *, - id: str, - task_queue: str, - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.TypedSearchAttributes, - temporalio.common.SearchAttributes, - ] - ] = None, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - start_delay: Optional[timedelta] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: Optional[temporalio.common.VersioningOverride] = None, - ) -> None: ... - - # Overload for multi-param workflow, with_start - @overload - def __init__( - self, - workflow: Callable[ - Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType] - ], - *, - args: Sequence[Any], - id: str, - task_queue: str, - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.TypedSearchAttributes, - temporalio.common.SearchAttributes, - ] - ] = None, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - start_delay: Optional[timedelta] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: Optional[temporalio.common.VersioningOverride] = None, - ) -> None: ... - - # Overload for string-name workflow, with_start - @overload - def __init__( - self, - workflow: str, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: str, - task_queue: str, - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy, - result_type: Optional[Type] = None, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.TypedSearchAttributes, - temporalio.common.SearchAttributes, - ] - ] = None, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - start_delay: Optional[timedelta] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: Optional[temporalio.common.VersioningOverride] = None, - ) -> None: ... - - def __init__( - self, - workflow: Union[str, Callable[..., Awaitable[Any]]], - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: str, - task_queue: str, - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy, - result_type: Optional[Type] = None, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.TypedSearchAttributes, - temporalio.common.SearchAttributes, - ] - ] = None, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - start_delay: Optional[timedelta] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: Optional[temporalio.common.VersioningOverride] = None, - stack_level: int = 2, - ) -> None: - """Create a WithStartWorkflowOperation. - - See :py:meth:`temporalio.client.Client.start_workflow` for documentation of the - arguments. - """ - temporalio.common._warn_on_deprecated_search_attributes( - search_attributes, stack_level=stack_level - ) - name, result_type_from_run_fn = ( - temporalio.workflow._Definition.get_name_and_result_type(workflow) - ) - if id_conflict_policy == temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED: - raise ValueError("WorkflowIDConflictPolicy is required") - - self._start_workflow_input = UpdateWithStartStartWorkflowInput( - workflow=name, - args=temporalio.common._arg_or_args(arg, args), - id=id, - task_queue=task_queue, - execution_timeout=execution_timeout, - run_timeout=run_timeout, - task_timeout=task_timeout, - id_reuse_policy=id_reuse_policy, - id_conflict_policy=id_conflict_policy, - retry_policy=retry_policy, - cron_schedule=cron_schedule, - memo=memo, - search_attributes=search_attributes, - static_summary=static_summary, - static_details=static_details, - start_delay=start_delay, - headers={}, - ret_type=result_type or result_type_from_run_fn, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - priority=priority, - versioning_override=versioning_override, - ) - self._workflow_handle: Future[WorkflowHandle[SelfType, ReturnType]] = Future() - self._used = False - - async def workflow_handle(self) -> WorkflowHandle[SelfType, ReturnType]: - """Wait until workflow is running and return a WorkflowHandle.""" - return await self._workflow_handle - - -@dataclass(frozen=True) -class AsyncActivityIDReference: - """Reference to an async activity by its qualified ID.""" - - workflow_id: str - run_id: Optional[str] - activity_id: str - - -class AsyncActivityHandle: - """Handle representing an external activity for completion and heartbeat.""" - - def __init__( - self, client: Client, id_or_token: Union[AsyncActivityIDReference, bytes] - ) -> None: - """Create an async activity handle.""" - self._client = client - self._id_or_token = id_or_token - - async def heartbeat( - self, - *details: Any, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> None: - """Record a heartbeat for the activity. - - Args: - details: Details of the heartbeat. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - """ - await self._client._impl.heartbeat_async_activity( - HeartbeatAsyncActivityInput( - id_or_token=self._id_or_token, - details=details, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ), - ) - - async def complete( - self, - result: Optional[Any] = temporalio.common._arg_unset, - *, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> None: - """Complete the activity. - - Args: - result: Result of the activity if any. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - """ - await self._client._impl.complete_async_activity( - CompleteAsyncActivityInput( - id_or_token=self._id_or_token, - result=result, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ), - ) - - async def fail( - self, - error: Exception, - *, - last_heartbeat_details: Sequence[Any] = [], - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> None: - """Fail the activity. - - Args: - error: Error for the activity. - last_heartbeat_details: Last heartbeat details for the activity. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - """ - await self._client._impl.fail_async_activity( - FailAsyncActivityInput( - id_or_token=self._id_or_token, - error=error, - last_heartbeat_details=last_heartbeat_details, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ), - ) - - async def report_cancellation( - self, - *details: Any, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> None: - """Report the activity as cancelled. - - Args: - details: Cancellation details. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - """ - await self._client._impl.report_cancellation_async_activity( - ReportCancellationAsyncActivityInput( - id_or_token=self._id_or_token, - details=details, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ), - ) - - -@dataclass -class WorkflowExecution: - """Info for a single workflow execution run.""" - - close_time: Optional[datetime] - """When the workflow was closed if closed.""" - - data_converter: temporalio.converter.DataConverter - """Data converter from when this description was created.""" - - execution_time: Optional[datetime] - """When this workflow run started or should start.""" - - history_length: int - """Number of events in the history.""" - - id: str - """ID for the workflow.""" - - parent_id: Optional[str] - """ID for the parent workflow if this was started as a child.""" - - parent_run_id: Optional[str] - """Run ID for the parent workflow if this was started as a child.""" - - root_id: Optional[str] - """ID for the root workflow.""" - - root_run_id: Optional[str] - """Run ID for the root workflow.""" - - raw_info: temporalio.api.workflow.v1.WorkflowExecutionInfo - """Underlying protobuf info.""" - - run_id: str - """Run ID for this workflow run.""" - - search_attributes: temporalio.common.SearchAttributes - """Current set of search attributes if any. - - .. deprecated:: - Use :py:attr:`typed_search_attributes` instead. - """ - - start_time: datetime - """When the workflow was created.""" - - status: Optional[WorkflowExecutionStatus] - """Status for the workflow.""" - - task_queue: str - """Task queue for the workflow.""" - - typed_search_attributes: temporalio.common.TypedSearchAttributes - """Current set of search attributes if any.""" - - workflow_type: str - """Type name for the workflow.""" - - @classmethod - def _from_raw_info( - cls, - info: temporalio.api.workflow.v1.WorkflowExecutionInfo, - converter: temporalio.converter.DataConverter, - **additional_fields: Any, - ) -> WorkflowExecution: - return cls( - close_time=info.close_time.ToDatetime().replace(tzinfo=timezone.utc) - if info.HasField("close_time") - else None, - data_converter=converter, - execution_time=info.execution_time.ToDatetime().replace(tzinfo=timezone.utc) - if info.HasField("execution_time") - else None, - history_length=info.history_length, - id=info.execution.workflow_id, - parent_id=info.parent_execution.workflow_id - if info.HasField("parent_execution") - else None, - parent_run_id=info.parent_execution.run_id - if info.HasField("parent_execution") - else None, - root_id=info.root_execution.workflow_id - if info.HasField("root_execution") - else None, - root_run_id=info.root_execution.run_id - if info.HasField("root_execution") - else None, - raw_info=info, - run_id=info.execution.run_id, - search_attributes=temporalio.converter.decode_search_attributes( - info.search_attributes - ), - start_time=info.start_time.ToDatetime().replace(tzinfo=timezone.utc), - status=WorkflowExecutionStatus(info.status) if info.status else None, - task_queue=info.task_queue, - typed_search_attributes=temporalio.converter.decode_typed_search_attributes( - info.search_attributes - ), - workflow_type=info.type.name, - **additional_fields, - ) - - async def memo(self) -> Mapping[str, Any]: - """Workflow's memo values, converted without type hints. - - Since type hints are not used, the default converted values will come - back. For example, if the memo was originally created with a dataclass, - the value will be a dict. To convert using proper type hints, use - :py:meth:`memo_value`. - - Returns: - Mapping of all memo keys and they values without type hints. - """ - return { - k: (await self.data_converter.decode([v]))[0] - for k, v in self.raw_info.memo.fields.items() - } - - @overload - async def memo_value( - self, key: str, default: Any = temporalio.common._arg_unset - ) -> Any: ... - - @overload - async def memo_value( - self, key: str, *, type_hint: Type[ParamType] - ) -> ParamType: ... - - @overload - async def memo_value( - self, key: str, default: AnyType, *, type_hint: Type[ParamType] - ) -> Union[AnyType, ParamType]: ... - - async def memo_value( - self, - key: str, - default: Any = temporalio.common._arg_unset, - *, - type_hint: Optional[Type] = None, - ) -> Any: - """Memo value for the given key, optional default, and optional type - hint. - - Args: - key: Key to get memo value for. - default: Default to use if key is not present. If unset, a - :py:class:`KeyError` is raised when the key does not exist. - type_hint: Type hint to use when converting. - - Returns: - Memo value, converted with the type hint if present. - - Raises: - KeyError: Key not present and default not set. - """ - payload = self.raw_info.memo.fields.get(key) - if not payload: - if default is temporalio.common._arg_unset: - raise KeyError(f"Memo does not have a value for key {key}") - return default - return ( - await self.data_converter.decode( - [payload], [type_hint] if type_hint else None - ) - )[0] - - -@dataclass -class WorkflowExecutionDescription(WorkflowExecution): - """Description for a single workflow execution run.""" - - raw_description: temporalio.api.workflowservice.v1.DescribeWorkflowExecutionResponse - """Underlying protobuf description.""" - - _static_summary: Optional[str] = None - _static_details: Optional[str] = None - _metadata_decoded: bool = False - - async def static_summary(self) -> Optional[str]: - """Gets the single-line fixed summary for this workflow execution that may appear in - UI/CLI. This can be in single-line Temporal markdown format. - """ - if not self._metadata_decoded: - await self._decode_metadata() - return self._static_summary - - async def static_details(self) -> Optional[str]: - """Gets the general fixed details for this workflow execution that may appear in UI/CLI. - This can be in Temporal markdown format and can span multiple lines. - """ - if not self._metadata_decoded: - await self._decode_metadata() - return self._static_details - - async def _decode_metadata(self) -> None: - """Internal method to decode metadata lazily.""" - self._static_summary, self._static_details = await _decode_user_metadata( - self.data_converter, self.raw_description.execution_config.user_metadata - ) - self._metadata_decoded = True - - @staticmethod - async def _from_raw_description( - description: temporalio.api.workflowservice.v1.DescribeWorkflowExecutionResponse, - converter: temporalio.converter.DataConverter, - ) -> WorkflowExecutionDescription: - return WorkflowExecutionDescription._from_raw_info( # type: ignore - description.workflow_execution_info, - converter, - raw_description=description, - ) - - -class WorkflowExecutionStatus(IntEnum): - """Status of a workflow execution. - - See :py:class:`temporalio.api.enums.v1.WorkflowExecutionStatus`. - """ - - RUNNING = int( - temporalio.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_RUNNING - ) - COMPLETED = int( - temporalio.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_COMPLETED - ) - FAILED = int( - temporalio.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_FAILED - ) - CANCELED = int( - temporalio.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_CANCELED - ) - TERMINATED = int( - temporalio.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_TERMINATED - ) - CONTINUED_AS_NEW = int( - temporalio.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW - ) - TIMED_OUT = int( - temporalio.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_TIMED_OUT - ) - - -@dataclass -class WorkflowExecutionCount: - """Representation of a count from a count workflows call.""" - - count: int - """Approximate number of workflows matching the original query. - - If the query had a group-by clause, this is simply the sum of all the counts - in py:attr:`groups`. - """ - - groups: Sequence[WorkflowExecutionCountAggregationGroup] - """Groups if the query had a group-by clause, or empty if not.""" - - @staticmethod - def _from_raw( - raw: temporalio.api.workflowservice.v1.CountWorkflowExecutionsResponse, - ) -> WorkflowExecutionCount: - return WorkflowExecutionCount( - count=raw.count, - groups=[ - WorkflowExecutionCountAggregationGroup._from_raw(g) for g in raw.groups - ], - ) - - -@dataclass -class WorkflowExecutionCountAggregationGroup: - """Aggregation group if the workflow count query had a group-by clause.""" - - count: int - """Approximate number of workflows matching the original query for this - group. - """ - - group_values: Sequence[temporalio.common.SearchAttributeValue] - """Search attribute values for this group.""" - - @staticmethod - def _from_raw( - raw: temporalio.api.workflowservice.v1.CountWorkflowExecutionsResponse.AggregationGroup, - ) -> WorkflowExecutionCountAggregationGroup: - return WorkflowExecutionCountAggregationGroup( - count=raw.count, - group_values=[ - temporalio.converter._decode_search_attribute_value(v) - for v in raw.group_values - ], - ) - - -class WorkflowExecutionAsyncIterator: - """Asynchronous iterator for :py:class:`WorkflowExecution` values. - - Most users should use ``async for`` on this iterator and not call any of the - methods within. To consume the workflows as histories, call - :py:meth:`map_histories`. - """ - - def __init__( - self, - client: Client, - input: ListWorkflowsInput, - ) -> None: - """Create an asynchronous iterator for the given input. - - Users should not create this directly, but rather use - :py:meth:`Client.list_workflows`. - """ - self._client = client - self._input = input - self._next_page_token = input.next_page_token - self._current_page: Optional[Sequence[WorkflowExecution]] = None - self._current_page_index = 0 - self._limit = input.limit - self._yielded = 0 - - @property - def current_page_index(self) -> int: - """Index of the entry in the current page that will be returned from - the next :py:meth:`__anext__` call. - """ - return self._current_page_index - - @property - def current_page(self) -> Optional[Sequence[WorkflowExecution]]: - """Current page, if it has been fetched yet.""" - return self._current_page - - @property - def next_page_token(self) -> Optional[bytes]: - """Token for the next page request if any.""" - return self._next_page_token - - async def fetch_next_page(self, *, page_size: Optional[int] = None) -> None: - """Fetch the next page if any. - - Args: - page_size: Override the page size this iterator was originally - created with. - """ - page_size = page_size or self._input.page_size - if self._limit is not None and self._limit - self._yielded < page_size: - page_size = self._limit - self._yielded - - resp = await self._client.workflow_service.list_workflow_executions( - temporalio.api.workflowservice.v1.ListWorkflowExecutionsRequest( - namespace=self._client.namespace, - page_size=page_size, - next_page_token=self._next_page_token or b"", - query=self._input.query or "", - ), - retry=True, - metadata=self._input.rpc_metadata, - timeout=self._input.rpc_timeout, - ) - self._current_page = [ - WorkflowExecution._from_raw_info(v, self._client.data_converter) - for v in resp.executions - ] - self._current_page_index = 0 - self._next_page_token = resp.next_page_token or None - - def __aiter__(self) -> WorkflowExecutionAsyncIterator: - """Return self as the iterator.""" - return self - - async def __anext__(self) -> WorkflowExecution: - """Get the next execution on this iterator, fetching next page if - necessary. - """ - if self._limit is not None and self._yielded >= self._limit: - raise StopAsyncIteration - while True: - # No page? fetch and continue - if self._current_page is None: - await self.fetch_next_page() - continue - # No more left in page? - if self._current_page_index >= len(self._current_page): - # If there is a next page token, try to get another page and try - # again - if self._next_page_token is not None: - await self.fetch_next_page() - continue - # No more pages means we're done - raise StopAsyncIteration - # Get current, increment page index, and return - ret = self._current_page[self._current_page_index] - self._current_page_index += 1 - self._yielded += 1 - return ret - - async def map_histories( - self, - *, - event_filter_type: WorkflowHistoryEventFilterType = WorkflowHistoryEventFilterType.ALL_EVENT, - skip_archival: bool = False, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> AsyncIterator[WorkflowHistory]: - """Create an async iterator consuming all workflows and calling - :py:meth:`WorkflowHandle.fetch_history` on each one. - - This is just a shortcut for ``fetch_history``, see that method for - parameter details. - """ - async for v in self: - yield await self._client.get_workflow_handle( - v.id, run_id=v.run_id - ).fetch_history( - event_filter_type=event_filter_type, - skip_archival=skip_archival, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - - -@dataclass(frozen=True) -class WorkflowHistory: - """A workflow's ID and immutable history.""" - - workflow_id: str - """ID of the workflow.""" - - events: Sequence[temporalio.api.history.v1.HistoryEvent] - """History events for the workflow.""" - - @property - def run_id(self) -> str: - """Run ID extracted from the first event.""" - if not self.events: - raise RuntimeError("No events") - if not self.events[0].HasField("workflow_execution_started_event_attributes"): - raise RuntimeError("First event is not workflow start") - return self.events[ - 0 - ].workflow_execution_started_event_attributes.original_execution_run_id - - @staticmethod - def from_json( - workflow_id: str, history: Union[str, Dict[str, Any]] - ) -> WorkflowHistory: - """Construct a WorkflowHistory from an ID and a json dump of history. - - This is built to work both with Temporal UI/tctl JSON as well as - :py:meth:`to_json` even though they are slightly different. - - Args: - workflow_id: The workflow's ID - history: A string or parsed-to-dict representation of workflow - history - - Returns: - Workflow history - """ - parsed = _history_from_json(history) - return WorkflowHistory(workflow_id, parsed.events) - - def to_json(self) -> str: - """Convert this history to JSON. - - Note, this does not include the workflow ID. - """ - return google.protobuf.json_format.MessageToJson( - temporalio.api.history.v1.History(events=self.events) - ) - - def to_json_dict(self) -> Dict[str, Any]: - """Convert this history to JSON-compatible dict. - - Note, this does not include the workflow ID. - """ - return google.protobuf.json_format.MessageToDict( - temporalio.api.history.v1.History(events=self.events) - ) - - -@dataclass -class WorkflowHistoryEventAsyncIterator: - """Asynchronous iterator for history events of a workflow. - - Most users should use ``async for`` on this iterator and not call any of the - methods within. - """ - - def __init__( - self, - client: Client, - input: FetchWorkflowHistoryEventsInput, - ) -> None: - """Create an asynchronous iterator for the given input. - - Users should not create this directly, but rather use - :py:meth:`WorkflowHandle.fetch_history_events`. - """ - self._client = client - self._input = input - self._next_page_token = input.next_page_token - self._current_page: Optional[ - Sequence[temporalio.api.history.v1.HistoryEvent] - ] = None - self._current_page_index = 0 - - @property - def current_page_index(self) -> int: - """Index of the entry in the current page that will be returned from - the next :py:meth:`__anext__` call. - """ - return self._current_page_index - - @property - def current_page( - self, - ) -> Optional[Sequence[temporalio.api.history.v1.HistoryEvent]]: - """Current page, if it has been fetched yet.""" - return self._current_page - - @property - def next_page_token(self) -> Optional[bytes]: - """Token for the next page request if any.""" - return self._next_page_token - - async def fetch_next_page(self, *, page_size: Optional[int] = None) -> None: - """Fetch the next page if any. - - Args: - page_size: Override the page size this iterator was originally - created with. - """ - resp = await self._client.workflow_service.get_workflow_execution_history( - temporalio.api.workflowservice.v1.GetWorkflowExecutionHistoryRequest( - namespace=self._client.namespace, - execution=temporalio.api.common.v1.WorkflowExecution( - workflow_id=self._input.id, - run_id=self._input.run_id or "", - ), - maximum_page_size=self._input.page_size or 0, - next_page_token=self._next_page_token or b"", - wait_new_event=self._input.wait_new_event, - history_event_filter_type=temporalio.api.enums.v1.HistoryEventFilterType.ValueType( - self._input.event_filter_type - ), - skip_archival=self._input.skip_archival, - ), - retry=True, - metadata=self._input.rpc_metadata, - timeout=self._input.rpc_timeout, - ) - # We don't support raw history - assert len(resp.raw_history) == 0 - self._current_page = list(resp.history.events) - self._current_page_index = 0 - self._next_page_token = resp.next_page_token or None - - def __aiter__(self) -> WorkflowHistoryEventAsyncIterator: - """Return self as the iterator.""" - return self - - async def __anext__(self) -> temporalio.api.history.v1.HistoryEvent: - """Get the next execution on this iterator, fetching next page if - necessary. - """ - while True: - # No page? fetch and continue - if self._current_page is None: - await self.fetch_next_page() - continue - # No more left in page? - if self._current_page_index >= len(self._current_page): - # If there is a next page token, try to get another page and try - # again - if self._next_page_token is not None: - await self.fetch_next_page() - continue - # No more pages means we're done - raise StopAsyncIteration - # Increment page index and return - ret = self._current_page[self._current_page_index] - self._current_page_index += 1 - return ret - - -class ScheduleHandle: - """Handle for interacting with a schedule. - - This is usually created via :py:meth:`Client.get_schedule_handle` or - returned from :py:meth:`Client.create_schedule`. - - Attributes: - id: ID of the schedule. - """ - - def __init__(self, client: Client, id: str) -> None: - """Create schedule handle.""" - self._client = client - self.id = id - - async def backfill( - self, - *backfill: ScheduleBackfill, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> None: - """Backfill the schedule by going through the specified time periods as - if they passed right now. - - Args: - backfill: Backfill periods. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - """ - if not backfill: - raise ValueError("At least one backfill required") - await self._client._impl.backfill_schedule( - BackfillScheduleInput( - id=self.id, - backfills=backfill, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ), - ) - - async def delete( - self, - *, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> None: - """Delete this schedule. - - Args: - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - """ - await self._client._impl.delete_schedule( - DeleteScheduleInput( - id=self.id, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ), - ) - - async def describe( - self, - *, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> ScheduleDescription: - """Fetch this schedule's description. - - Args: - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - """ - return await self._client._impl.describe_schedule( - DescribeScheduleInput( - id=self.id, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ), - ) - - async def pause( - self, - *, - note: Optional[str] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> None: - """Pause the schedule and set a note. - - Args: - note: Note to set on the schedule. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - """ - await self._client._impl.pause_schedule( - PauseScheduleInput( - id=self.id, - note=note, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ), - ) - - async def trigger( - self, - *, - overlap: Optional[ScheduleOverlapPolicy] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> None: - """Trigger an action on this schedule to happen immediately. - - Args: - overlap: If set, overrides the schedule's overlap policy. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - """ - await self._client._impl.trigger_schedule( - TriggerScheduleInput( - id=self.id, - overlap=overlap, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ), - ) - - async def unpause( - self, - *, - note: Optional[str] = None, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> None: - """Unpause the schedule and set a note. - - Args: - note: Note to set on the schedule. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - """ - await self._client._impl.unpause_schedule( - UnpauseScheduleInput( - id=self.id, - note=note, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ), - ) - - @overload - async def update( - self, - updater: Callable[[ScheduleUpdateInput], Optional[ScheduleUpdate]], - *, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> None: ... - - @overload - async def update( - self, - updater: Callable[[ScheduleUpdateInput], Awaitable[Optional[ScheduleUpdate]]], - *, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> None: ... - - async def update( - self, - updater: Callable[ - [ScheduleUpdateInput], - Union[Optional[ScheduleUpdate], Awaitable[Optional[ScheduleUpdate]]], - ], - *, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> None: - """Update a schedule using a callback to build the update from the - description. - - The callback may be invoked multiple times in a conflict-resolution - loop. - - Args: - updater: Callback that returns the update. It accepts a - :py:class:`ScheduleUpdateInput` and returns a - :py:class:`ScheduleUpdate`. If None is returned or an error - occurs, the update is not attempted. This may be called multiple - times. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. This is for every call made - within. - rpc_timeout: Optional RPC deadline to set for the RPC call. This is - for each call made within, not overall. - """ - await self._client._impl.update_schedule( - UpdateScheduleInput( - id=self.id, - updater=updater, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ), - ) - - -@dataclass -class ScheduleSpec: - """Specification of the times scheduled actions may occur. - - The times are the union of :py:attr:`calendars`, :py:attr:`intervals`, and - :py:attr:`cron_expressions` excluding anything in :py:attr:`skip`. - """ - - calendars: Sequence[ScheduleCalendarSpec] = dataclasses.field(default_factory=list) - """Calendar-based specification of times.""" - - intervals: Sequence[ScheduleIntervalSpec] = dataclasses.field(default_factory=list) - """Interval-based specification of times.""" - - cron_expressions: Sequence[str] = dataclasses.field(default_factory=list) - """Cron-based specification of times. - - This is provided for easy migration from legacy string-based cron - scheduling. New uses should use :py:attr:`calendars` instead. These - expressions will be translated to calendar-based specifications on the - server. - """ - - skip: Sequence[ScheduleCalendarSpec] = dataclasses.field(default_factory=list) - """Set of matching calendar times that will be skipped.""" - - start_at: Optional[datetime] = None - """Time before which any matching times will be skipped.""" - - end_at: Optional[datetime] = None - """Time after which any matching times will be skipped.""" - - jitter: Optional[timedelta] = None - """Jitter to apply each action. - - An action's scheduled time will be incremented by a random value between 0 - and this value if present (but not past the next schedule). - """ - - time_zone_name: Optional[str] = None - """IANA time zone name, for example ``US/Central``.""" - - @staticmethod - def _from_proto(spec: temporalio.api.schedule.v1.ScheduleSpec) -> ScheduleSpec: - return ScheduleSpec( - calendars=[ - ScheduleCalendarSpec._from_proto(c) for c in spec.structured_calendar - ], - intervals=[ScheduleIntervalSpec._from_proto(i) for i in spec.interval], - cron_expressions=spec.cron_string, - skip=[ - ScheduleCalendarSpec._from_proto(c) - for c in spec.exclude_structured_calendar - ], - start_at=spec.start_time.ToDatetime().replace(tzinfo=timezone.utc) - if spec.HasField("start_time") - else None, - end_at=spec.end_time.ToDatetime().replace(tzinfo=timezone.utc) - if spec.HasField("end_time") - else None, - jitter=spec.jitter.ToTimedelta() if spec.HasField("jitter") else None, - time_zone_name=spec.timezone_name or None, - ) - - def _to_proto(self) -> temporalio.api.schedule.v1.ScheduleSpec: - start_time: Optional[google.protobuf.timestamp_pb2.Timestamp] = None - if self.start_at: - start_time = google.protobuf.timestamp_pb2.Timestamp() - start_time.FromDatetime(self.start_at) - end_time: Optional[google.protobuf.timestamp_pb2.Timestamp] = None - if self.end_at: - end_time = google.protobuf.timestamp_pb2.Timestamp() - end_time.FromDatetime(self.end_at) - jitter: Optional[google.protobuf.duration_pb2.Duration] = None - if self.jitter: - jitter = google.protobuf.duration_pb2.Duration() - jitter.FromTimedelta(self.jitter) - return temporalio.api.schedule.v1.ScheduleSpec( - structured_calendar=[cal._to_proto() for cal in self.calendars], - cron_string=self.cron_expressions, - interval=[i._to_proto() for i in self.intervals], - exclude_structured_calendar=[cal._to_proto() for cal in self.skip], - start_time=start_time, - end_time=end_time, - jitter=jitter, - timezone_name=self.time_zone_name or "", - ) - - -@dataclass(frozen=True) -class ScheduleRange: - """Inclusive range for a schedule match value.""" - - start: int - """Inclusive start of the range.""" - - end: int = 0 - """Inclusive end of the range. - - If unset or less than start, defaults to start. - """ - - step: int = 0 - """ - Step to take between each value. - - Unset or 0 defaults as 1. - """ - - def __post_init__(self): - """Set field defaults.""" - # Class is frozen, so we must setattr bypassing dataclass setattr - if self.end < self.start: - object.__setattr__(self, "end", self.start) - if self.step == 0: - object.__setattr__(self, "step", 1) - - @staticmethod - def _from_protos( - ranges: Sequence[temporalio.api.schedule.v1.Range], - ) -> Sequence[ScheduleRange]: - return tuple(ScheduleRange._from_proto(r) for r in ranges) - - @staticmethod - def _from_proto(range: temporalio.api.schedule.v1.Range) -> ScheduleRange: - return ScheduleRange(start=range.start, end=range.end, step=range.step) - - @staticmethod - def _to_protos( - ranges: Sequence[ScheduleRange], - ) -> Sequence[temporalio.api.schedule.v1.Range]: - return tuple(r._to_proto() for r in ranges) - - def _to_proto(self) -> temporalio.api.schedule.v1.Range: - return temporalio.api.schedule.v1.Range( - start=self.start, end=self.end, step=self.step - ) - - -@dataclass -class ScheduleCalendarSpec: - """Specification relative to calendar time when to run an action. - - A timestamp matches if at least one range of each field matches except for - year. If year is missing, that means all years match. For all fields besides - year, at least one range must be present to match anything. - """ - - second: Sequence[ScheduleRange] = (ScheduleRange(0),) - """Second range to match, 0-59. Default matches 0.""" - - minute: Sequence[ScheduleRange] = (ScheduleRange(0),) - """Minute range to match, 0-59. Default matches 0.""" - - hour: Sequence[ScheduleRange] = (ScheduleRange(0),) - """Hour range to match, 0-23. Default matches 0.""" - - day_of_month: Sequence[ScheduleRange] = (ScheduleRange(1, 31),) - """Day of month range to match, 1-31. Default matches all days.""" - - month: Sequence[ScheduleRange] = (ScheduleRange(1, 12),) - """Month range to match, 1-12. Default matches all months.""" - - year: Sequence[ScheduleRange] = () - """Optional year range to match. Default of empty matches all years.""" - - day_of_week: Sequence[ScheduleRange] = (ScheduleRange(0, 6),) - """Day of week range to match, 0-6, 0 is Sunday. Default matches all - days.""" - - comment: Optional[str] = None - """Description of this schedule.""" - - @staticmethod - def _from_proto( - spec: temporalio.api.schedule.v1.StructuredCalendarSpec, - ) -> ScheduleCalendarSpec: - return ScheduleCalendarSpec( - second=ScheduleRange._from_protos(spec.second), - minute=ScheduleRange._from_protos(spec.minute), - hour=ScheduleRange._from_protos(spec.hour), - day_of_month=ScheduleRange._from_protos(spec.day_of_month), - month=ScheduleRange._from_protos(spec.month), - year=ScheduleRange._from_protos(spec.year), - day_of_week=ScheduleRange._from_protos(spec.day_of_week), - comment=spec.comment or None, - ) - - def _to_proto(self) -> temporalio.api.schedule.v1.StructuredCalendarSpec: - return temporalio.api.schedule.v1.StructuredCalendarSpec( - second=ScheduleRange._to_protos(self.second), - minute=ScheduleRange._to_protos(self.minute), - hour=ScheduleRange._to_protos(self.hour), - day_of_month=ScheduleRange._to_protos(self.day_of_month), - month=ScheduleRange._to_protos(self.month), - year=ScheduleRange._to_protos(self.year), - day_of_week=ScheduleRange._to_protos(self.day_of_week), - comment=self.comment or "", - ) - - -@dataclass -class ScheduleIntervalSpec: - """Specification for scheduling on an interval. - - Matches times expressed as epoch + (n * every) + offset. - """ - - every: timedelta - """Period to repeat the interval.""" - - offset: Optional[timedelta] = None - """Fixed offset added to each interval period.""" - - @staticmethod - def _from_proto( - spec: temporalio.api.schedule.v1.IntervalSpec, - ) -> ScheduleIntervalSpec: - return ScheduleIntervalSpec( - every=spec.interval.ToTimedelta(), - offset=spec.phase.ToTimedelta() if spec.HasField("phase") else None, - ) - - def _to_proto(self) -> temporalio.api.schedule.v1.IntervalSpec: - interval = google.protobuf.duration_pb2.Duration() - interval.FromTimedelta(self.every) - phase: Optional[google.protobuf.duration_pb2.Duration] = None - if self.offset: - phase = google.protobuf.duration_pb2.Duration() - phase.FromTimedelta(self.offset) - return temporalio.api.schedule.v1.IntervalSpec(interval=interval, phase=phase) - - -class ScheduleAction(ABC): - """Base class for an action a schedule can take. - - See :py:class:`ScheduleActionStartWorkflow` for the most commonly used - implementation. - """ - - @staticmethod - def _from_proto( - action: temporalio.api.schedule.v1.ScheduleAction, - ) -> ScheduleAction: - if action.HasField("start_workflow"): - return ScheduleActionStartWorkflow._from_proto(action.start_workflow) - else: - raise ValueError(f"Unsupported action: {action.WhichOneof('action')}") - - @abstractmethod - async def _to_proto( - self, client: Client - ) -> temporalio.api.schedule.v1.ScheduleAction: ... - - -@dataclass -class ScheduleActionStartWorkflow(ScheduleAction): - """Schedule action to start a workflow.""" - - workflow: str - args: Union[Sequence[Any], Sequence[temporalio.api.common.v1.Payload]] - id: str - task_queue: str - execution_timeout: Optional[timedelta] - run_timeout: Optional[timedelta] - task_timeout: Optional[timedelta] - retry_policy: Optional[temporalio.common.RetryPolicy] - memo: Optional[ - Union[Mapping[str, Any], Mapping[str, temporalio.api.common.v1.Payload]] - ] - typed_search_attributes: temporalio.common.TypedSearchAttributes - untyped_search_attributes: temporalio.common.SearchAttributes - """This is deprecated and is only present in case existing untyped - attributes already exist for update. This should never be used when - creating.""" - static_summary: Optional[Union[str, temporalio.api.common.v1.Payload]] - static_details: Optional[Union[str, temporalio.api.common.v1.Payload]] - priority: temporalio.common.Priority - - headers: Optional[Mapping[str, temporalio.api.common.v1.Payload]] - """ - Headers may still be encoded by the payload codec if present. - """ - _from_raw: bool = dataclasses.field(compare=False, init=False) - - @staticmethod - def _from_proto( # pyright: ignore - info: temporalio.api.workflow.v1.NewWorkflowExecutionInfo, # type: ignore[override] - ) -> ScheduleActionStartWorkflow: - return ScheduleActionStartWorkflow("", raw_info=info) - - # Overload for no-param workflow - @overload - def __init__( - self, - workflow: MethodAsyncNoParam[SelfType, ReturnType], - *, - id: str, - task_queue: str, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - memo: Optional[Mapping[str, Any]] = None, - typed_search_attributes: temporalio.common.TypedSearchAttributes = temporalio.common.TypedSearchAttributes.empty, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - ) -> None: ... - - # Overload for single-param workflow - @overload - def __init__( - self, - workflow: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], - arg: ParamType, - *, - id: str, - task_queue: str, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - memo: Optional[Mapping[str, Any]] = None, - typed_search_attributes: temporalio.common.TypedSearchAttributes = temporalio.common.TypedSearchAttributes.empty, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - ) -> None: ... - - # Overload for multi-param workflow - @overload - def __init__( - self, - workflow: Callable[ - Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType] - ], - *, - args: Sequence[Any], - id: str, - task_queue: str, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - memo: Optional[Mapping[str, Any]] = None, - typed_search_attributes: temporalio.common.TypedSearchAttributes = temporalio.common.TypedSearchAttributes.empty, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - ) -> None: ... - - # Overload for string-name workflow - @overload - def __init__( - self, - workflow: str, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: str, - task_queue: str, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - memo: Optional[Mapping[str, Any]] = None, - typed_search_attributes: temporalio.common.TypedSearchAttributes = temporalio.common.TypedSearchAttributes.empty, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - ) -> None: ... - - # Overload for raw info - @overload - def __init__( - self, - workflow: str, - *, - raw_info: temporalio.api.workflow.v1.NewWorkflowExecutionInfo, - ) -> None: ... - - def __init__( - self, - workflow: Union[str, Callable[..., Awaitable[Any]]], - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: Optional[str] = None, - task_queue: Optional[str] = None, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - memo: Optional[Mapping[str, Any]] = None, - typed_search_attributes: temporalio.common.TypedSearchAttributes = temporalio.common.TypedSearchAttributes.empty, - untyped_search_attributes: temporalio.common.SearchAttributes = {}, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - headers: Optional[Mapping[str, temporalio.api.common.v1.Payload]] = None, - raw_info: Optional[temporalio.api.workflow.v1.NewWorkflowExecutionInfo] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - ) -> None: - """Create a start-workflow action. - - See :py:meth:`Client.start_workflow` for details on these parameter - values. - """ - super().__init__() - if raw_info: - self._from_raw = True - # Ignore other fields - self.workflow = raw_info.workflow_type.name - self.args = raw_info.input.payloads if raw_info.input else [] - self.id = raw_info.workflow_id - self.task_queue = raw_info.task_queue.name - self.execution_timeout = ( - raw_info.workflow_execution_timeout.ToTimedelta() - if raw_info.HasField("workflow_execution_timeout") - else None - ) - self.run_timeout = ( - raw_info.workflow_run_timeout.ToTimedelta() - if raw_info.HasField("workflow_run_timeout") - else None - ) - self.task_timeout = ( - raw_info.workflow_task_timeout.ToTimedelta() - if raw_info.HasField("workflow_task_timeout") - else None - ) - self.retry_policy = ( - temporalio.common.RetryPolicy.from_proto(raw_info.retry_policy) - if raw_info.HasField("retry_policy") - else None - ) - self.memo = raw_info.memo.fields if raw_info.memo.fields else None - self.typed_search_attributes = ( - temporalio.converter.decode_typed_search_attributes( - raw_info.search_attributes - ) - ) - self.headers = raw_info.header.fields if raw_info.header.fields else None - # Also set the untyped attributes as the set of attributes from - # decode with the typed ones removed - self.untyped_search_attributes = ( - temporalio.converter.decode_search_attributes( - raw_info.search_attributes - ) - ) - for pair in self.typed_search_attributes: - if pair.key.name in self.untyped_search_attributes: - # We know this is mutable here - del self.untyped_search_attributes[pair.key.name] # type: ignore - self.static_summary = ( - raw_info.user_metadata.summary - if raw_info.HasField("user_metadata") and raw_info.user_metadata.summary - else None - ) - self.static_details = ( - raw_info.user_metadata.details - if raw_info.HasField("user_metadata") and raw_info.user_metadata.details - else None - ) - self.priority = ( - temporalio.common.Priority._from_proto(raw_info.priority) - if raw_info.HasField("priority") and raw_info.priority - else temporalio.common.Priority.default - ) - else: - self._from_raw = False - if not id: - raise ValueError("ID required") - if not task_queue: - raise ValueError("Task queue required") - # Use definition if callable - if callable(workflow): - defn = temporalio.workflow._Definition.must_from_run_fn(workflow) - if not defn.name: - raise ValueError("Cannot schedule dynamic workflow explicitly") - workflow = defn.name - elif not isinstance(workflow, str): - raise TypeError("Workflow must be a string or callable") - self.workflow = workflow - self.args = temporalio.common._arg_or_args(arg, args) - self.id = id - self.task_queue = task_queue - self.execution_timeout = execution_timeout - self.run_timeout = run_timeout - self.task_timeout = task_timeout - self.retry_policy = retry_policy - self.memo = memo - self.typed_search_attributes = typed_search_attributes - self.untyped_search_attributes = untyped_search_attributes - self.headers = headers # encode here - self.static_summary = static_summary - self.static_details = static_details - self.priority = priority - - async def _to_proto( - self, client: Client - ) -> temporalio.api.schedule.v1.ScheduleAction: - execution_timeout: Optional[google.protobuf.duration_pb2.Duration] = None - if self.execution_timeout: - execution_timeout = google.protobuf.duration_pb2.Duration() - execution_timeout.FromTimedelta(self.execution_timeout) - run_timeout: Optional[google.protobuf.duration_pb2.Duration] = None - if self.run_timeout: - run_timeout = google.protobuf.duration_pb2.Duration() - run_timeout.FromTimedelta(self.run_timeout) - task_timeout: Optional[google.protobuf.duration_pb2.Duration] = None - if self.task_timeout: - task_timeout = google.protobuf.duration_pb2.Duration() - task_timeout.FromTimedelta(self.task_timeout) - retry_policy: Optional[temporalio.api.common.v1.RetryPolicy] = None - if self.retry_policy: - retry_policy = temporalio.api.common.v1.RetryPolicy() - self.retry_policy.apply_to_proto(retry_policy) - priority: Optional[temporalio.api.common.v1.Priority] = None - if self.priority: - priority = self.priority._to_proto() - action = temporalio.api.schedule.v1.ScheduleAction( - start_workflow=temporalio.api.workflow.v1.NewWorkflowExecutionInfo( - workflow_id=self.id, - workflow_type=temporalio.api.common.v1.WorkflowType(name=self.workflow), - task_queue=temporalio.api.taskqueue.v1.TaskQueue(name=self.task_queue), - input=None - if not self.args - else temporalio.api.common.v1.Payloads( - payloads=[ - a - if isinstance(a, temporalio.api.common.v1.Payload) - else (await client.data_converter.encode([a]))[0] - for a in self.args - ] - ), - workflow_execution_timeout=execution_timeout, - workflow_run_timeout=run_timeout, - workflow_task_timeout=task_timeout, - retry_policy=retry_policy, - memo=None - if not self.memo - else temporalio.api.common.v1.Memo( - fields={ - k: v - if isinstance(v, temporalio.api.common.v1.Payload) - else (await client.data_converter.encode([v]))[0] - for k, v in self.memo.items() - }, - ), - user_metadata=await _encode_user_metadata( - client.data_converter, self.static_summary, self.static_details - ), - priority=priority, - ), - ) - # Add any untyped attributes that are not also in the typed set - untyped_not_in_typed = { - k: v - for k, v in self.untyped_search_attributes.items() - if k not in self.typed_search_attributes - } - if untyped_not_in_typed: - temporalio.converter.encode_search_attributes( - untyped_not_in_typed, action.start_workflow.search_attributes - ) - # TODO (dan): confirm whether this be `is not None` - if self.typed_search_attributes: - temporalio.converter.encode_search_attributes( - self.typed_search_attributes, action.start_workflow.search_attributes - ) - if self.headers: - await _apply_headers( - self.headers, - action.start_workflow.header.fields, - client.config()["header_codec_behavior"] == HeaderCodecBehavior.CODEC - and not self._from_raw, - client.data_converter.payload_codec, - ) - return action - - -class ScheduleOverlapPolicy(IntEnum): - """Controls what happens when a workflow would be started by a schedule but - one is already running. - """ - - SKIP = int( - temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_SKIP - ) - """Don't start anything. - - When the workflow completes, the next scheduled event after that time will - be considered. - """ - - BUFFER_ONE = int( - temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_BUFFER_ONE - ) - """Start the workflow again soon as the current one completes, but only - buffer one start in this way. - - If another start is supposed to happen when the workflow is running, and one - is already buffered, then only the first one will be started after the - running workflow finishes. - """ - - BUFFER_ALL = int( - temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_BUFFER_ALL - ) - """Buffer up any number of starts to all happen sequentially, immediately - after the running workflow completes.""" - - CANCEL_OTHER = int( - temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_CANCEL_OTHER - ) - """If there is another workflow running, cancel it, and start the new one - after the old one completes cancellation.""" - - TERMINATE_OTHER = int( - temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_TERMINATE_OTHER - ) - """If there is another workflow running, terminate it and start the new one - immediately.""" - - ALLOW_ALL = int( - temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_ALLOW_ALL - ) - """Start any number of concurrent workflows. - - Note that with this policy, last completion result and last failure will not - be available since workflows are not sequential.""" - - -@dataclass -class ScheduleBackfill: - """Time period and policy for actions taken as if the time passed right - now. - """ - - start_at: datetime - """Start of the range to evaluate the schedule in. - - This is exclusive - """ - end_at: datetime - overlap: Optional[ScheduleOverlapPolicy] = None - - def _to_proto(self) -> temporalio.api.schedule.v1.BackfillRequest: - start_time = google.protobuf.timestamp_pb2.Timestamp() - start_time.FromDatetime(self.start_at) - end_time = google.protobuf.timestamp_pb2.Timestamp() - end_time.FromDatetime(self.end_at) - overlap_policy = temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_UNSPECIFIED - if self.overlap: - overlap_policy = temporalio.api.enums.v1.ScheduleOverlapPolicy.ValueType( - self.overlap - ) - return temporalio.api.schedule.v1.BackfillRequest( - start_time=start_time, - end_time=end_time, - overlap_policy=overlap_policy, - ) - - -@dataclass -class SchedulePolicy: - """Policies of a schedule.""" - - overlap: ScheduleOverlapPolicy = dataclasses.field( - default_factory=lambda: ScheduleOverlapPolicy.SKIP - ) - """Controls what happens when an action is started while another is still - running.""" - - catchup_window: timedelta = timedelta(days=365) - """After a Temporal server is unavailable, amount of time in the past to - execute missed actions.""" - - pause_on_failure: bool = False - """Whether to pause the schedule if an action fails or times out. - - Note: For workflows, this only applies after all retries have been - exhausted. - """ - - @staticmethod - def _from_proto(pol: temporalio.api.schedule.v1.SchedulePolicies) -> SchedulePolicy: - return SchedulePolicy( - overlap=ScheduleOverlapPolicy(int(pol.overlap_policy)), - catchup_window=pol.catchup_window.ToTimedelta(), - pause_on_failure=pol.pause_on_failure, - ) - - def _to_proto(self) -> temporalio.api.schedule.v1.SchedulePolicies: - catchup_window = google.protobuf.duration_pb2.Duration() - catchup_window.FromTimedelta(self.catchup_window) - return temporalio.api.schedule.v1.SchedulePolicies( - overlap_policy=temporalio.api.enums.v1.ScheduleOverlapPolicy.ValueType( - self.overlap - ), - catchup_window=catchup_window, - pause_on_failure=self.pause_on_failure, - ) - - -@dataclass -class ScheduleState: - """State of a schedule.""" - - note: Optional[str] = None - """Human readable message for the schedule. - - The system may overwrite this value on certain conditions like - pause-on-failure. - """ - - paused: bool = False - """Whether the schedule is paused.""" - - # Cannot be set to True on create - limited_actions: bool = False - """ - If true, remaining actions will be decremented for each action taken. - - On schedule create, this must be set to true if :py:attr:`remaining_actions` - is non-zero and left false if :py:attr:`remaining_actions` is zero. - """ - - remaining_actions: int = 0 - """Actions remaining on this schedule. - - Once this number hits 0, no further actions are scheduled automatically. - """ - - @staticmethod - def _from_proto(state: temporalio.api.schedule.v1.ScheduleState) -> ScheduleState: - return ScheduleState( - note=state.notes or None, - paused=state.paused, - limited_actions=state.limited_actions, - remaining_actions=state.remaining_actions, - ) - - def _to_proto(self) -> temporalio.api.schedule.v1.ScheduleState: - return temporalio.api.schedule.v1.ScheduleState( - notes=self.note or "", - paused=self.paused, - limited_actions=self.limited_actions, - remaining_actions=self.remaining_actions, - ) - - -@dataclass -class Schedule: - """A schedule for periodically running an action.""" - - action: ScheduleAction - """Action taken when scheduled.""" - - spec: ScheduleSpec - """When the action is taken.""" - - policy: SchedulePolicy = dataclasses.field(default_factory=SchedulePolicy) - """Schedule policies.""" - - state: ScheduleState = dataclasses.field(default_factory=ScheduleState) - """State of the schedule.""" - - @staticmethod - def _from_proto(sched: temporalio.api.schedule.v1.Schedule) -> Schedule: - return Schedule( - action=ScheduleAction._from_proto(sched.action), - spec=ScheduleSpec._from_proto(sched.spec), - policy=SchedulePolicy._from_proto(sched.policies), - state=ScheduleState._from_proto(sched.state), - ) - - async def _to_proto(self, client: Client) -> temporalio.api.schedule.v1.Schedule: - catchup_window = google.protobuf.duration_pb2.Duration() - catchup_window.FromTimedelta(self.policy.catchup_window) - return temporalio.api.schedule.v1.Schedule( - spec=self.spec._to_proto(), - action=await self.action._to_proto(client), - policies=self.policy._to_proto(), - state=self.state._to_proto(), - ) - - -@dataclass -class ScheduleDescription: - """Description of a schedule.""" - - id: str - """ID of the schedule.""" - - schedule: Schedule - """Schedule details that can be mutated.""" - - info: ScheduleInfo - """Information about the schedule.""" - - typed_search_attributes: temporalio.common.TypedSearchAttributes - """Search attributes on the schedule.""" - - search_attributes: temporalio.common.SearchAttributes - """Search attributes on the schedule. - - .. deprecated:: - Use :py:attr:`typed_search_attributes` instead. - """ - - data_converter: temporalio.converter.DataConverter - """Data converter used for memo decoding.""" - - raw_description: temporalio.api.workflowservice.v1.DescribeScheduleResponse - """Raw description of the schedule.""" - - @staticmethod - def _from_proto( - id: str, - desc: temporalio.api.workflowservice.v1.DescribeScheduleResponse, - converter: temporalio.converter.DataConverter, - ) -> ScheduleDescription: - return ScheduleDescription( - id=id, - schedule=Schedule._from_proto(desc.schedule), - info=ScheduleInfo._from_proto(desc.info), - typed_search_attributes=temporalio.converter.decode_typed_search_attributes( - desc.search_attributes - ), - search_attributes=temporalio.converter.decode_search_attributes( - desc.search_attributes - ), - data_converter=converter, - raw_description=desc, - ) - - async def memo(self) -> Mapping[str, Any]: - """Schedule's memo values, converted without type hints. - - Since type hints are not used, the default converted values will come - back. For example, if the memo was originally created with a dataclass, - the value will be a dict. To convert using proper type hints, use - :py:meth:`memo_value`. - - Returns: - Mapping of all memo keys and they values without type hints. - """ - return { - k: (await self.data_converter.decode([v]))[0] - for k, v in self.raw_description.memo.fields.items() - } - - @overload - async def memo_value( - self, key: str, default: Any = temporalio.common._arg_unset - ) -> Any: ... - - @overload - async def memo_value( - self, key: str, *, type_hint: Type[ParamType] - ) -> ParamType: ... - - @overload - async def memo_value( - self, key: str, default: AnyType, *, type_hint: Type[ParamType] - ) -> Union[AnyType, ParamType]: ... - - async def memo_value( - self, - key: str, - default: Any = temporalio.common._arg_unset, - *, - type_hint: Optional[Type] = None, - ) -> Any: - """Memo value for the given key, optional default, and optional type - hint. - - Args: - key: Key to get memo value for. - default: Default to use if key is not present. If unset, a - :py:class:`KeyError` is raised when the key does not exist. - type_hint: Type hint to use when converting. - - Returns: - Memo value, converted with the type hint if present. - - Raises: - KeyError: Key not present and default not set. - """ - payload = self.raw_description.memo.fields.get(key) - if not payload: - if default is temporalio.common._arg_unset: - raise KeyError(f"Memo does not have a value for key {key}") - return default - return ( - await self.data_converter.decode( - [payload], [type_hint] if type_hint else None - ) - )[0] - - -@dataclass -class ScheduleInfo: - """Information about a schedule.""" - - num_actions: int - """Number of actions taken by this schedule.""" - - num_actions_missed_catchup_window: int - """Number of times an action was skipped due to missing the catchup - window.""" - - num_actions_skipped_overlap: int - """Number of actions skipped due to overlap.""" - - running_actions: Sequence[ScheduleActionExecution] - """Currently running actions.""" - - recent_actions: Sequence[ScheduleActionResult] - """10 most recent actions, oldest first.""" - - next_action_times: Sequence[datetime] - """Next 10 scheduled action times.""" - - created_at: datetime - """When the schedule was created.""" - - last_updated_at: Optional[datetime] - """When the schedule was last updated.""" - - @staticmethod - def _from_proto(info: temporalio.api.schedule.v1.ScheduleInfo) -> ScheduleInfo: - return ScheduleInfo( - num_actions=info.action_count, - num_actions_missed_catchup_window=info.missed_catchup_window, - num_actions_skipped_overlap=info.overlap_skipped, - running_actions=[ - ScheduleActionExecutionStartWorkflow._from_proto(r) - for r in info.running_workflows - ], - recent_actions=[ - ScheduleActionResult._from_proto(r) for r in info.recent_actions - ], - next_action_times=[ - f.ToDatetime().replace(tzinfo=timezone.utc) - for f in info.future_action_times - ], - created_at=info.create_time.ToDatetime().replace(tzinfo=timezone.utc), - last_updated_at=info.update_time.ToDatetime().replace(tzinfo=timezone.utc) - if info.HasField("update_time") - else None, - ) - - -class ScheduleActionExecution(ABC): - """Base class for an action execution.""" - - pass - - -@dataclass -class ScheduleActionExecutionStartWorkflow(ScheduleActionExecution): - """Execution of a scheduled workflow start.""" - - workflow_id: str - """Workflow ID.""" - - first_execution_run_id: str - """Workflow run ID.""" - - @staticmethod - def _from_proto( - exec: temporalio.api.common.v1.WorkflowExecution, - ) -> ScheduleActionExecutionStartWorkflow: - return ScheduleActionExecutionStartWorkflow( - workflow_id=exec.workflow_id, - first_execution_run_id=exec.run_id, - ) - - -@dataclass -class ScheduleActionResult: - """Information about when an action took place.""" - - scheduled_at: datetime - """Scheduled time of the action including jitter.""" - - started_at: datetime - """When the action actually started.""" - - action: ScheduleActionExecution - """Action that took place.""" - - @staticmethod - def _from_proto( - res: temporalio.api.schedule.v1.ScheduleActionResult, - ) -> ScheduleActionResult: - return ScheduleActionResult( - scheduled_at=res.schedule_time.ToDatetime().replace(tzinfo=timezone.utc), - started_at=res.actual_time.ToDatetime().replace(tzinfo=timezone.utc), - action=ScheduleActionExecutionStartWorkflow._from_proto( - res.start_workflow_result - ), - ) - - -@dataclass -class ScheduleUpdateInput: - """Parameter for an update callback for :py:meth:`ScheduleHandle.update`.""" - - description: ScheduleDescription - """Current description of the schedule.""" - - -@dataclass -class ScheduleUpdate: - """Result of an update callback for :py:meth:`ScheduleHandle.update`.""" - - schedule: Schedule - """Schedule to update.""" - - search_attributes: Optional[temporalio.common.TypedSearchAttributes] = None - """Search attributes to update.""" - - -@dataclass -class ScheduleListDescription: - """Description of a listed schedule.""" - - id: str - """ID of the schedule.""" - - schedule: Optional[ScheduleListSchedule] - """Schedule details that can be mutated. - - This may not be present in older Temporal servers without advanced - visibility. - """ - - info: Optional[ScheduleListInfo] - """Information about the schedule. - - This may not be present in older Temporal servers without advanced - visibility. - """ - - typed_search_attributes: temporalio.common.TypedSearchAttributes - """Search attributes on the schedule.""" - - search_attributes: temporalio.common.SearchAttributes - """Search attributes on the schedule. - - .. deprecated:: - Use :py:attr:`typed_search_attributes` instead. - """ - - data_converter: temporalio.converter.DataConverter - """Data converter used for memo decoding.""" - - raw_entry: temporalio.api.schedule.v1.ScheduleListEntry - """Raw description of the schedule.""" - - @staticmethod - def _from_proto( - entry: temporalio.api.schedule.v1.ScheduleListEntry, - converter: temporalio.converter.DataConverter, - ) -> ScheduleListDescription: - return ScheduleListDescription( - id=entry.schedule_id, - schedule=ScheduleListSchedule._from_proto(entry.info) - if entry.HasField("info") - else None, - info=ScheduleListInfo._from_proto(entry.info) - if entry.HasField("info") - else None, - typed_search_attributes=temporalio.converter.decode_typed_search_attributes( - entry.search_attributes - ), - search_attributes=temporalio.converter.decode_search_attributes( - entry.search_attributes - ), - data_converter=converter, - raw_entry=entry, - ) - - async def memo(self) -> Mapping[str, Any]: - """Schedule's memo values, converted without type hints. - - Since type hints are not used, the default converted values will come - back. For example, if the memo was originally created with a dataclass, - the value will be a dict. To convert using proper type hints, use - :py:meth:`memo_value`. - - Returns: - Mapping of all memo keys and they values without type hints. - """ - return { - k: (await self.data_converter.decode([v]))[0] - for k, v in self.raw_entry.memo.fields.items() - } - - @overload - async def memo_value( - self, key: str, default: Any = temporalio.common._arg_unset - ) -> Any: ... - - @overload - async def memo_value( - self, key: str, *, type_hint: Type[ParamType] - ) -> ParamType: ... - - @overload - async def memo_value( - self, key: str, default: AnyType, *, type_hint: Type[ParamType] - ) -> Union[AnyType, ParamType]: ... - - async def memo_value( - self, - key: str, - default: Any = temporalio.common._arg_unset, - *, - type_hint: Optional[Type] = None, - ) -> Any: - """Memo value for the given key, optional default, and optional type - hint. - - Args: - key: Key to get memo value for. - default: Default to use if key is not present. If unset, a - :py:class:`KeyError` is raised when the key does not exist. - type_hint: Type hint to use when converting. - - Returns: - Memo value, converted with the type hint if present. - - Raises: - KeyError: Key not present and default not set. - """ - payload = self.raw_entry.memo.fields.get(key) - if not payload: - if default is temporalio.common._arg_unset: - raise KeyError(f"Memo does not have a value for key {key}") - return default - return ( - await self.data_converter.decode( - [payload], [type_hint] if type_hint else None - ) - )[0] - - -@dataclass -class ScheduleListSchedule: - """Details for a listed schedule.""" - - action: ScheduleListAction - """Action taken when scheduled.""" - - spec: ScheduleSpec - """When the action is taken.""" - - state: ScheduleListState - """State of the schedule.""" - - @staticmethod - def _from_proto( - info: temporalio.api.schedule.v1.ScheduleListInfo, - ) -> ScheduleListSchedule: - # Only start workflow supported for now - if not info.HasField("workflow_type"): - raise ValueError("Unknown action on schedule") - return ScheduleListSchedule( - action=ScheduleListActionStartWorkflow(workflow=info.workflow_type.name), - spec=ScheduleSpec._from_proto(info.spec), - state=ScheduleListState._from_proto(info), - ) - - -class ScheduleListAction(ABC): - """Base class for an action a listed schedule can take.""" - - pass - - -@dataclass -class ScheduleListActionStartWorkflow(ScheduleListAction): - """Action to start a workflow on a listed schedule.""" - - workflow: str - """Workflow type name.""" - - -@dataclass -class ScheduleListInfo: - """Information about a listed schedule.""" - - recent_actions: Sequence[ScheduleActionResult] - """Most recent actions, oldest first. - - This may be a smaller amount than present on - :py:attr:`ScheduleDescription.info`. - """ - - next_action_times: Sequence[datetime] - """Next scheduled action times. - - This may be a smaller amount than present on - :py:attr:`ScheduleDescription.info`. - """ - - @staticmethod - def _from_proto( - info: temporalio.api.schedule.v1.ScheduleListInfo, - ) -> ScheduleListInfo: - return ScheduleListInfo( - recent_actions=[ - ScheduleActionResult._from_proto(r) for r in info.recent_actions - ], - next_action_times=[ - f.ToDatetime().replace(tzinfo=timezone.utc) - for f in info.future_action_times - ], - ) - - -@dataclass -class ScheduleListState: - """State of a listed schedule.""" - - note: Optional[str] - """Human readable message for the schedule. - - The system may overwrite this value on certain conditions like - pause-on-failure. - """ - - paused: bool - """Whether the schedule is paused.""" - - @staticmethod - def _from_proto( - info: temporalio.api.schedule.v1.ScheduleListInfo, - ) -> ScheduleListState: - return ScheduleListState( - note=info.notes or None, - paused=info.paused, - ) - - -class ScheduleAsyncIterator: - """Asynchronous iterator for :py:class:`ScheduleListDescription` values. - - Most users should use ``async for`` on this iterator and not call any of the - methods within. - """ - - def __init__( - self, - client: Client, - input: ListSchedulesInput, - ) -> None: - """Create an asynchronous iterator for the given input. - - Users should not create this directly, but rather use - :py:meth:`Client.list_schedules`. - """ - self._client = client - self._input = input - self._next_page_token = input.next_page_token - self._current_page: Optional[Sequence[ScheduleListDescription]] = None - self._current_page_index = 0 - - @property - def current_page_index(self) -> int: - """Index of the entry in the current page that will be returned from - the next :py:meth:`__anext__` call. - """ - return self._current_page_index - - @property - def current_page(self) -> Optional[Sequence[ScheduleListDescription]]: - """Current page, if it has been fetched yet.""" - return self._current_page - - @property - def next_page_token(self) -> Optional[bytes]: - """Token for the next page request if any.""" - return self._next_page_token - - async def fetch_next_page(self, *, page_size: Optional[int] = None) -> None: - """Fetch the next page if any. - - Args: - page_size: Override the page size this iterator was originally - created with. - """ - resp = await self._client.workflow_service.list_schedules( - temporalio.api.workflowservice.v1.ListSchedulesRequest( - namespace=self._client.namespace, - maximum_page_size=page_size or self._input.page_size, - next_page_token=self._next_page_token or b"", - query=self._input.query or "", - ), - retry=True, - metadata=self._input.rpc_metadata, - timeout=self._input.rpc_timeout, - ) - self._current_page = [ - ScheduleListDescription._from_proto(v, self._client.data_converter) - for v in resp.schedules - ] - self._current_page_index = 0 - self._next_page_token = resp.next_page_token or None - - def __aiter__(self) -> ScheduleAsyncIterator: - """Return self as the iterator.""" - return self - - async def __anext__(self) -> ScheduleListDescription: - """Get the next execution on this iterator, fetching next page if - necessary. - """ - while True: - # No page? fetch and continue - if self._current_page is None: - await self.fetch_next_page() - continue - # No more left in page? - if self._current_page_index >= len(self._current_page): - # If there is a next page token, try to get another page and try - # again - if self._next_page_token is not None: - await self.fetch_next_page() - continue - # No more pages means we're done - raise StopAsyncIteration - # Get current, increment page index, and return - ret = self._current_page[self._current_page_index] - self._current_page_index += 1 - return ret - - -class WorkflowUpdateHandle(Generic[LocalReturnType]): - """Handle for a workflow update execution request.""" - - def __init__( - self, - client: Client, - id: str, - workflow_id: str, - *, - workflow_run_id: Optional[str] = None, - result_type: Optional[Type] = None, - known_outcome: Optional[temporalio.api.update.v1.Outcome] = None, - ): - """Create a workflow update handle. - - Users should not create this directly, but rather use - :py:meth:`WorkflowHandle.start_update` or :py:meth:`WorkflowHandle.get_update_handle`. - """ - self._client = client - self._id = id - self._workflow_id = workflow_id - self._workflow_run_id = workflow_run_id - self._result_type = result_type - self._known_outcome = known_outcome - - @property - def id(self) -> str: - """ID of this Update request.""" - return self._id - - @property - def workflow_id(self) -> str: - """The ID of the Workflow targeted by this Update.""" - return self._workflow_id - - @property - def workflow_run_id(self) -> Optional[str]: - """If specified, the specific run of the Workflow targeted by this Update.""" - return self._workflow_run_id - - async def result( - self, - *, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> LocalReturnType: - """Wait for and return the result of the update. The result may already be known in which case no network call - is made. Otherwise the result will be polled for until it is returned. - - Args: - rpc_metadata: Headers used on the RPC call. Keys here override client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for each RPC call. Note: this is the timeout for each - RPC call while polling, not a timeout for the function as a whole. If an individual RPC times out, - it will be retried until the result is available. - - Raises: - WorkflowUpdateFailedError: If the update failed. - WorkflowUpdateRPCTimeoutOrCancelledError: This update call timed out - or was cancelled. This doesn't mean the update itself was timed - out or cancelled. - RPCError: Update result could not be fetched for some other reason. - """ - # Poll until outcome reached - await self._poll_until_outcome( - rpc_metadata=rpc_metadata, rpc_timeout=rpc_timeout - ) - - # Convert outcome to failure or value - assert self._known_outcome - if self._known_outcome.HasField("failure"): - raise WorkflowUpdateFailedError( - await self._client.data_converter.decode_failure( - self._known_outcome.failure - ), - ) - if not self._known_outcome.success.payloads: - return None # type: ignore - type_hints = [self._result_type] if self._result_type else None - results = await self._client.data_converter.decode( - self._known_outcome.success.payloads, type_hints - ) - if not results: - return None # type: ignore - elif len(results) > 1: - warnings.warn(f"Expected single update result, got {len(results)}") - return results[0] - - async def _poll_until_outcome( - self, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, - ) -> None: - if self._known_outcome: - return - req = temporalio.api.workflowservice.v1.PollWorkflowExecutionUpdateRequest( - namespace=self._client.namespace, - update_ref=temporalio.api.update.v1.UpdateRef( - workflow_execution=temporalio.api.common.v1.WorkflowExecution( - workflow_id=self.workflow_id, - run_id=self.workflow_run_id or "", - ), - update_id=self.id, - ), - identity=self._client.identity, - wait_policy=temporalio.api.update.v1.WaitPolicy( - lifecycle_stage=temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED - ), - ) - - # Continue polling as long as we have no outcome - while True: - try: - res = ( - await self._client.workflow_service.poll_workflow_execution_update( - req, - retry=True, - metadata=rpc_metadata, - timeout=rpc_timeout, - ) - ) - if res.HasField("outcome"): - self._known_outcome = res.outcome - return - except RPCError as err: - if ( - err.status == RPCStatusCode.DEADLINE_EXCEEDED - or err.status == RPCStatusCode.CANCELLED - ): - raise WorkflowUpdateRPCTimeoutOrCancelledError() from err - else: - raise - except asyncio.CancelledError as err: - raise WorkflowUpdateRPCTimeoutOrCancelledError() from err - - -class WorkflowUpdateStage(IntEnum): - """Stage to wait for workflow update to reach before returning from - ``start_update``. - """ - - ADMITTED = int( - temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED - ) - ACCEPTED = int( - temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED - ) - COMPLETED = int( - temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED - ) - - -class WorkflowFailureError(temporalio.exceptions.TemporalError): - """Error that occurs when a workflow is unsuccessful.""" - - def __init__(self, *, cause: BaseException) -> None: - """Create workflow failure error.""" - super().__init__("Workflow execution failed") - self.__cause__ = cause - - @property - def cause(self) -> BaseException: - """Cause of the workflow failure.""" - assert self.__cause__ - return self.__cause__ - - -class WorkflowContinuedAsNewError(temporalio.exceptions.TemporalError): - """Error that occurs when a workflow was continued as new.""" - - def __init__(self, new_execution_run_id: str) -> None: - """Create workflow continue as new error.""" - super().__init__("Workflow continued as new") - self._new_execution_run_id = new_execution_run_id - - @property - def new_execution_run_id(self) -> str: - """New execution run ID the workflow continued to""" - return self._new_execution_run_id - - -class WorkflowQueryRejectedError(temporalio.exceptions.TemporalError): - """Error that occurs when a query was rejected.""" - - def __init__(self, status: Optional[WorkflowExecutionStatus]) -> None: - """Create workflow query rejected error.""" - super().__init__(f"Query rejected, status: {status}") - self._status = status - - @property - def status(self) -> Optional[WorkflowExecutionStatus]: - """Get workflow execution status causing rejection.""" - return self._status - - -class WorkflowQueryFailedError(temporalio.exceptions.TemporalError): - """Error that occurs when a query fails.""" - - def __init__(self, message: str) -> None: - """Create workflow query failed error.""" - super().__init__(message) - self._message = message - - @property - def message(self) -> str: - """Get query failed message.""" - return self._message - - -class WorkflowUpdateFailedError(temporalio.exceptions.TemporalError): - """Error that occurs when an update fails.""" - - def __init__(self, cause: BaseException) -> None: - """Create workflow update failed error.""" - super().__init__("Workflow update failed") - self.__cause__ = cause - - @property - def cause(self) -> BaseException: - """Cause of the update failure.""" - assert self.__cause__ - return self.__cause__ - - -class RPCTimeoutOrCancelledError(temporalio.exceptions.TemporalError): - """Error that occurs on some client calls that timeout or get cancelled.""" - - pass - - -class WorkflowUpdateRPCTimeoutOrCancelledError(RPCTimeoutOrCancelledError): - """Error that occurs when update RPC call times out or is cancelled. - - Note, this is not related to any general concept of timing out or cancelling - a running update, this is only related to the client call itself. - """ - - def __init__(self) -> None: - """Create workflow update timeout or cancelled error.""" - super().__init__("Timeout or cancellation waiting for update") - - -class AsyncActivityCancelledError(temporalio.exceptions.TemporalError): - """Error that occurs when async activity attempted heartbeat but was cancelled.""" - - def __init__(self, details: Optional[ActivityCancellationDetails] = None) -> None: - """Create async activity cancelled error.""" - super().__init__("Activity cancelled") - self.details = details - - -class ScheduleAlreadyRunningError(temporalio.exceptions.TemporalError): - """Error when a schedule is already running.""" - - def __init__(self) -> None: - """Create schedule already running error.""" - super().__init__("Schedule already running") - - -@dataclass -class StartWorkflowInput: - """Input for :py:meth:`OutboundInterceptor.start_workflow`.""" - - workflow: str - args: Sequence[Any] - id: str - task_queue: str - execution_timeout: Optional[timedelta] - run_timeout: Optional[timedelta] - task_timeout: Optional[timedelta] - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy - retry_policy: Optional[temporalio.common.RetryPolicy] - cron_schedule: str - memo: Optional[Mapping[str, Any]] - search_attributes: Optional[ - Union[ - temporalio.common.SearchAttributes, temporalio.common.TypedSearchAttributes - ] - ] - start_delay: Optional[timedelta] - headers: Mapping[str, temporalio.api.common.v1.Payload] - start_signal: Optional[str] - start_signal_args: Sequence[Any] - static_summary: Optional[str] - static_details: Optional[str] - # Type may be absent - ret_type: Optional[Type] - rpc_metadata: Mapping[str, str] - rpc_timeout: Optional[timedelta] - request_eager_start: bool - priority: temporalio.common.Priority - # The following options are experimental and unstable. - callbacks: Sequence[Callback] - workflow_event_links: Sequence[temporalio.api.common.v1.Link.WorkflowEvent] - request_id: Optional[str] - versioning_override: Optional[temporalio.common.VersioningOverride] = None - - -@dataclass -class CancelWorkflowInput: - """Input for :py:meth:`OutboundInterceptor.cancel_workflow`.""" - - id: str - run_id: Optional[str] - first_execution_run_id: Optional[str] - rpc_metadata: Mapping[str, str] - rpc_timeout: Optional[timedelta] - - -@dataclass -class DescribeWorkflowInput: - """Input for :py:meth:`OutboundInterceptor.describe_workflow`.""" - - id: str - run_id: Optional[str] - rpc_metadata: Mapping[str, str] - rpc_timeout: Optional[timedelta] - - -@dataclass -class FetchWorkflowHistoryEventsInput: - """Input for :py:meth:`OutboundInterceptor.fetch_workflow_history_events`.""" - - id: str - run_id: Optional[str] - page_size: Optional[int] - next_page_token: Optional[bytes] - wait_new_event: bool - event_filter_type: WorkflowHistoryEventFilterType - skip_archival: bool - rpc_metadata: Mapping[str, str] - rpc_timeout: Optional[timedelta] - - -@dataclass -class ListWorkflowsInput: - """Input for :py:meth:`OutboundInterceptor.list_workflows`.""" - - query: Optional[str] - page_size: int - next_page_token: Optional[bytes] - rpc_metadata: Mapping[str, str] - rpc_timeout: Optional[timedelta] - limit: Optional[int] - - -@dataclass -class CountWorkflowsInput: - """Input for :py:meth:`OutboundInterceptor.count_workflows`.""" - - query: Optional[str] - rpc_metadata: Mapping[str, str] - rpc_timeout: Optional[timedelta] - - -@dataclass -class QueryWorkflowInput: - """Input for :py:meth:`OutboundInterceptor.query_workflow`.""" - - id: str - run_id: Optional[str] - query: str - args: Sequence[Any] - reject_condition: Optional[temporalio.common.QueryRejectCondition] - headers: Mapping[str, temporalio.api.common.v1.Payload] - # Type may be absent - ret_type: Optional[Type] - rpc_metadata: Mapping[str, str] - rpc_timeout: Optional[timedelta] - - -@dataclass -class SignalWorkflowInput: - """Input for :py:meth:`OutboundInterceptor.signal_workflow`.""" - - id: str - run_id: Optional[str] - signal: str - args: Sequence[Any] - headers: Mapping[str, temporalio.api.common.v1.Payload] - rpc_metadata: Mapping[str, str] - rpc_timeout: Optional[timedelta] - - -@dataclass -class TerminateWorkflowInput: - """Input for :py:meth:`OutboundInterceptor.terminate_workflow`.""" - - id: str - run_id: Optional[str] - first_execution_run_id: Optional[str] - args: Sequence[Any] - reason: Optional[str] - rpc_metadata: Mapping[str, str] - rpc_timeout: Optional[timedelta] - - -@dataclass -class StartWorkflowUpdateInput: - """Input for :py:meth:`OutboundInterceptor.start_workflow_update`.""" - - id: str - run_id: Optional[str] - first_execution_run_id: Optional[str] - update_id: Optional[str] - update: str - args: Sequence[Any] - wait_for_stage: WorkflowUpdateStage - headers: Mapping[str, temporalio.api.common.v1.Payload] - ret_type: Optional[Type] - rpc_metadata: Mapping[str, str] - rpc_timeout: Optional[timedelta] - - -@dataclass -class UpdateWithStartUpdateWorkflowInput: - """Update input for :py:meth:`OutboundInterceptor.start_update_with_start_workflow`.""" - - update_id: Optional[str] - update: str - args: Sequence[Any] - wait_for_stage: WorkflowUpdateStage - headers: Mapping[str, temporalio.api.common.v1.Payload] - ret_type: Optional[Type] - rpc_metadata: Mapping[str, str] - rpc_timeout: Optional[timedelta] - - -@dataclass -class UpdateWithStartStartWorkflowInput: - """StartWorkflow input for :py:meth:`OutboundInterceptor.start_update_with_start_workflow`.""" - - # Similar to StartWorkflowInput but without e.g. run_id, start_signal, - # start_signal_args, request_eager_start. - - workflow: str - args: Sequence[Any] - id: str - task_queue: str - execution_timeout: Optional[timedelta] - run_timeout: Optional[timedelta] - task_timeout: Optional[timedelta] - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy - retry_policy: Optional[temporalio.common.RetryPolicy] - cron_schedule: str - memo: Optional[Mapping[str, Any]] - search_attributes: Optional[ - Union[ - temporalio.common.SearchAttributes, temporalio.common.TypedSearchAttributes - ] - ] - start_delay: Optional[timedelta] - headers: Mapping[str, temporalio.api.common.v1.Payload] - static_summary: Optional[str] - static_details: Optional[str] - # Type may be absent - ret_type: Optional[Type] - rpc_metadata: Mapping[str, str] - rpc_timeout: Optional[timedelta] - priority: temporalio.common.Priority - versioning_override: Optional[temporalio.common.VersioningOverride] = None - - -@dataclass -class StartWorkflowUpdateWithStartInput: - """Input for :py:meth:`OutboundInterceptor.start_update_with_start_workflow`.""" - - start_workflow_input: UpdateWithStartStartWorkflowInput - update_workflow_input: UpdateWithStartUpdateWorkflowInput - _on_start: Callable[ - [temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse], None - ] - _on_start_error: Callable[[BaseException], None] - - -@dataclass -class HeartbeatAsyncActivityInput: - """Input for :py:meth:`OutboundInterceptor.heartbeat_async_activity`.""" - - id_or_token: Union[AsyncActivityIDReference, bytes] - details: Sequence[Any] - rpc_metadata: Mapping[str, str] - rpc_timeout: Optional[timedelta] - - -@dataclass -class CompleteAsyncActivityInput: - """Input for :py:meth:`OutboundInterceptor.complete_async_activity`.""" - - id_or_token: Union[AsyncActivityIDReference, bytes] - result: Optional[Any] - rpc_metadata: Mapping[str, str] - rpc_timeout: Optional[timedelta] - - -@dataclass -class FailAsyncActivityInput: - """Input for :py:meth:`OutboundInterceptor.fail_async_activity`.""" - - id_or_token: Union[AsyncActivityIDReference, bytes] - error: Exception - last_heartbeat_details: Sequence[Any] - rpc_metadata: Mapping[str, str] - rpc_timeout: Optional[timedelta] - - -@dataclass -class ReportCancellationAsyncActivityInput: - """Input for :py:meth:`OutboundInterceptor.report_cancellation_async_activity`.""" - - id_or_token: Union[AsyncActivityIDReference, bytes] - details: Sequence[Any] - rpc_metadata: Mapping[str, str] - rpc_timeout: Optional[timedelta] - - -@dataclass -class CreateScheduleInput: - """Input for :py:meth:`OutboundInterceptor.create_schedule`.""" - - id: str - schedule: Schedule - trigger_immediately: bool - backfill: Sequence[ScheduleBackfill] - memo: Optional[Mapping[str, Any]] - search_attributes: Optional[ - Union[ - temporalio.common.SearchAttributes, temporalio.common.TypedSearchAttributes - ] - ] - rpc_metadata: Mapping[str, str] - rpc_timeout: Optional[timedelta] - - -@dataclass -class ListSchedulesInput: - """Input for :py:meth:`OutboundInterceptor.list_schedules`.""" - - page_size: int - next_page_token: Optional[bytes] - rpc_metadata: Mapping[str, str] - rpc_timeout: Optional[timedelta] - query: Optional[str] = None - - -@dataclass -class BackfillScheduleInput: - """Input for :py:meth:`OutboundInterceptor.backfill_schedule`.""" - - id: str - backfills: Sequence[ScheduleBackfill] - rpc_metadata: Mapping[str, str] - rpc_timeout: Optional[timedelta] - - -@dataclass -class DeleteScheduleInput: - """Input for :py:meth:`OutboundInterceptor.delete_schedule`.""" - - id: str - rpc_metadata: Mapping[str, str] - rpc_timeout: Optional[timedelta] - - -@dataclass -class DescribeScheduleInput: - """Input for :py:meth:`OutboundInterceptor.describe_schedule`.""" - - id: str - rpc_metadata: Mapping[str, str] - rpc_timeout: Optional[timedelta] - - -@dataclass -class PauseScheduleInput: - """Input for :py:meth:`OutboundInterceptor.pause_schedule`.""" - - id: str - note: Optional[str] - rpc_metadata: Mapping[str, str] - rpc_timeout: Optional[timedelta] - - -@dataclass -class TriggerScheduleInput: - """Input for :py:meth:`OutboundInterceptor.trigger_schedule`.""" - - id: str - overlap: Optional[ScheduleOverlapPolicy] - rpc_metadata: Mapping[str, str] - rpc_timeout: Optional[timedelta] - - -@dataclass -class UnpauseScheduleInput: - """Input for :py:meth:`OutboundInterceptor.unpause_schedule`.""" - - id: str - note: Optional[str] - rpc_metadata: Mapping[str, str] - rpc_timeout: Optional[timedelta] - - -@dataclass -class UpdateScheduleInput: - """Input for :py:meth:`OutboundInterceptor.update_schedule`.""" - - id: str - updater: Callable[ - [ScheduleUpdateInput], - Union[Optional[ScheduleUpdate], Awaitable[Optional[ScheduleUpdate]]], - ] - rpc_metadata: Mapping[str, str] - rpc_timeout: Optional[timedelta] - - -@dataclass -class UpdateWorkerBuildIdCompatibilityInput: - """Input for :py:meth:`OutboundInterceptor.update_worker_build_id_compatibility`.""" - - task_queue: str - operation: BuildIdOp - rpc_metadata: Mapping[str, str] - rpc_timeout: Optional[timedelta] - - -@dataclass -class GetWorkerBuildIdCompatibilityInput: - """Input for :py:meth:`OutboundInterceptor.get_worker_build_id_compatibility`.""" - - task_queue: str - max_sets: Optional[int] - rpc_metadata: Mapping[str, str] - rpc_timeout: Optional[timedelta] - - -@dataclass -class GetWorkerTaskReachabilityInput: - """Input for :py:meth:`OutboundInterceptor.get_worker_task_reachability`.""" - - build_ids: Sequence[str] - task_queues: Sequence[str] - reachability: Optional[TaskReachabilityType] - rpc_metadata: Mapping[str, str] - rpc_timeout: Optional[timedelta] - - -@dataclass -class Interceptor: - """Interceptor for clients. - - This should be extended by any client interceptors. - """ - - def intercept_client(self, next: OutboundInterceptor) -> OutboundInterceptor: - """Method called for intercepting a client. - - Args: - next: The underlying outbound interceptor this interceptor should - delegate to. - - Returns: - The new interceptor that will be called for each client call. - """ - return next - - -class OutboundInterceptor: - """OutboundInterceptor for intercepting client calls. - - This should be extended by any client outbound interceptors. - """ - - def __init__(self, next: OutboundInterceptor) -> None: - """Create the outbound interceptor. - - Args: - next: The next interceptor in the chain. The default implementation - of all calls is to delegate to the next interceptor. - """ - self.next = next - - ### Workflow calls - - async def start_workflow( - self, input: StartWorkflowInput - ) -> WorkflowHandle[Any, Any]: - """Called for every :py:meth:`Client.start_workflow` call.""" - return await self.next.start_workflow(input) - - async def cancel_workflow(self, input: CancelWorkflowInput) -> None: - """Called for every :py:meth:`WorkflowHandle.cancel` call.""" - await self.next.cancel_workflow(input) - - async def describe_workflow( - self, input: DescribeWorkflowInput - ) -> WorkflowExecutionDescription: - """Called for every :py:meth:`WorkflowHandle.describe` call.""" - return await self.next.describe_workflow(input) - - def fetch_workflow_history_events( - self, input: FetchWorkflowHistoryEventsInput - ) -> WorkflowHistoryEventAsyncIterator: - """Called for every :py:meth:`WorkflowHandle.fetch_history_events` call.""" - return self.next.fetch_workflow_history_events(input) - - def list_workflows( - self, input: ListWorkflowsInput - ) -> WorkflowExecutionAsyncIterator: - """Called for every :py:meth:`Client.list_workflows` call.""" - return self.next.list_workflows(input) - - async def count_workflows( - self, input: CountWorkflowsInput - ) -> WorkflowExecutionCount: - """Called for every :py:meth:`Client.count_workflows` call.""" - return await self.next.count_workflows(input) - - async def query_workflow(self, input: QueryWorkflowInput) -> Any: - """Called for every :py:meth:`WorkflowHandle.query` call.""" - return await self.next.query_workflow(input) - - async def signal_workflow(self, input: SignalWorkflowInput) -> None: - """Called for every :py:meth:`WorkflowHandle.signal` call.""" - await self.next.signal_workflow(input) - - async def terminate_workflow(self, input: TerminateWorkflowInput) -> None: - """Called for every :py:meth:`WorkflowHandle.terminate` call.""" - await self.next.terminate_workflow(input) - - async def start_workflow_update( - self, input: StartWorkflowUpdateInput - ) -> WorkflowUpdateHandle[Any]: - """Called for every :py:meth:`WorkflowHandle.start_update` and :py:meth:`WorkflowHandle.execute_update` call.""" - return await self.next.start_workflow_update(input) - - async def start_update_with_start_workflow( - self, input: StartWorkflowUpdateWithStartInput - ) -> WorkflowUpdateHandle[Any]: - """Called for every :py:meth:`Client.start_update_with_start_workflow` and :py:meth:`Client.execute_update_with_start_workflow` call.""" - return await self.next.start_update_with_start_workflow(input) - - ### Async activity calls - - async def heartbeat_async_activity( - self, input: HeartbeatAsyncActivityInput - ) -> None: - """Called for every :py:meth:`AsyncActivityHandle.heartbeat` call.""" - await self.next.heartbeat_async_activity(input) - - async def complete_async_activity(self, input: CompleteAsyncActivityInput) -> None: - """Called for every :py:meth:`AsyncActivityHandle.complete` call.""" - await self.next.complete_async_activity(input) - - async def fail_async_activity(self, input: FailAsyncActivityInput) -> None: - """Called for every :py:meth:`AsyncActivityHandle.fail` call.""" - await self.next.fail_async_activity(input) - - async def report_cancellation_async_activity( - self, input: ReportCancellationAsyncActivityInput - ) -> None: - """Called for every :py:meth:`AsyncActivityHandle.report_cancellation` call.""" - await self.next.report_cancellation_async_activity(input) - - ### Schedule calls - - async def create_schedule(self, input: CreateScheduleInput) -> ScheduleHandle: - """Called for every :py:meth:`Client.create_schedule` call.""" - return await self.next.create_schedule(input) - - def list_schedules(self, input: ListSchedulesInput) -> ScheduleAsyncIterator: - """Called for every :py:meth:`Client.list_schedules` call.""" - return self.next.list_schedules(input) - - async def backfill_schedule(self, input: BackfillScheduleInput) -> None: - """Called for every :py:meth:`ScheduleHandle.backfill` call.""" - await self.next.backfill_schedule(input) - - async def delete_schedule(self, input: DeleteScheduleInput) -> None: - """Called for every :py:meth:`ScheduleHandle.delete` call.""" - await self.next.delete_schedule(input) - - async def describe_schedule( - self, input: DescribeScheduleInput - ) -> ScheduleDescription: - """Called for every :py:meth:`ScheduleHandle.describe` call.""" - return await self.next.describe_schedule(input) - - async def pause_schedule(self, input: PauseScheduleInput) -> None: - """Called for every :py:meth:`ScheduleHandle.pause` call.""" - await self.next.pause_schedule(input) - - async def trigger_schedule(self, input: TriggerScheduleInput) -> None: - """Called for every :py:meth:`ScheduleHandle.trigger` call.""" - await self.next.trigger_schedule(input) - - async def unpause_schedule(self, input: UnpauseScheduleInput) -> None: - """Called for every :py:meth:`ScheduleHandle.unpause` call.""" - await self.next.unpause_schedule(input) - - async def update_schedule(self, input: UpdateScheduleInput) -> None: - """Called for every :py:meth:`ScheduleHandle.update` call.""" - await self.next.update_schedule(input) - - async def update_worker_build_id_compatibility( - self, input: UpdateWorkerBuildIdCompatibilityInput - ) -> None: - """Called for every :py:meth:`Client.update_worker_build_id_compatibility` call.""" - await self.next.update_worker_build_id_compatibility(input) - - async def get_worker_build_id_compatibility( - self, input: GetWorkerBuildIdCompatibilityInput - ) -> WorkerBuildIdVersionSets: - """Called for every :py:meth:`Client.get_worker_build_id_compatibility` call.""" - return await self.next.get_worker_build_id_compatibility(input) - - async def get_worker_task_reachability( - self, input: GetWorkerTaskReachabilityInput - ) -> WorkerTaskReachability: - """Called for every :py:meth:`Client.get_worker_task_reachability` call.""" - return await self.next.get_worker_task_reachability(input) - - -class _ClientImpl(OutboundInterceptor): - def __init__(self, client: Client) -> None: # type: ignore - # We are intentionally not calling the base class's __init__ here - self._client = client - - ### Workflow calls - - async def start_workflow( - self, input: StartWorkflowInput - ) -> WorkflowHandle[Any, Any]: - req: Union[ - temporalio.api.workflowservice.v1.StartWorkflowExecutionRequest, - temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest, - ] - if input.start_signal is not None: - req = await self._build_signal_with_start_workflow_execution_request(input) - else: - req = await self._build_start_workflow_execution_request(input) - - resp: Union[ - temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse, - temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse, - ] - first_execution_run_id = None - eagerly_started = False - try: - if isinstance( - req, - temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest, - ): - resp = await self._client.workflow_service.signal_with_start_workflow_execution( - req, - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - else: - resp = await self._client.workflow_service.start_workflow_execution( - req, - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - first_execution_run_id = resp.run_id - eagerly_started = resp.HasField("eager_workflow_task") - except RPCError as err: - # If the status is ALREADY_EXISTS and the details can be extracted - # as already started, use a different exception - if err.status == RPCStatusCode.ALREADY_EXISTS and err.grpc_status.details: - details = temporalio.api.errordetails.v1.WorkflowExecutionAlreadyStartedFailure() - if err.grpc_status.details[0].Unpack(details): - raise temporalio.exceptions.WorkflowAlreadyStartedError( - input.id, input.workflow, run_id=details.run_id - ) - raise - handle: WorkflowHandle[Any, Any] = WorkflowHandle( - self._client, - req.workflow_id, - result_run_id=resp.run_id, - first_execution_run_id=first_execution_run_id, - result_type=input.ret_type, - start_workflow_response=resp, - ) - setattr(handle, "__temporal_eagerly_started", eagerly_started) - return handle - - async def _build_start_workflow_execution_request( - self, input: StartWorkflowInput - ) -> temporalio.api.workflowservice.v1.StartWorkflowExecutionRequest: - req = temporalio.api.workflowservice.v1.StartWorkflowExecutionRequest() - await self._populate_start_workflow_execution_request(req, input) - # _populate_start_workflow_execution_request is used for both StartWorkflowInput - # and UpdateWithStartStartWorkflowInput. UpdateWithStartStartWorkflowInput does - # not have the following two fields so they are handled here. - req.request_eager_execution = input.request_eager_start - if input.request_id: - req.request_id = input.request_id - - links = [ - temporalio.api.common.v1.Link(workflow_event=link) - for link in input.workflow_event_links - ] - req.completion_callbacks.extend( - temporalio.api.common.v1.Callback( - nexus=temporalio.api.common.v1.Callback.Nexus( - url=callback.url, - header=callback.headers, - ), - links=links, - ) - for callback in input.callbacks - ) - # Links are duplicated on request for compatibility with older server versions. - req.links.extend(links) - - if temporalio.nexus._operation_context._in_nexus_backing_workflow_start_context(): - req.on_conflict_options.attach_request_id = True - req.on_conflict_options.attach_completion_callbacks = True - req.on_conflict_options.attach_links = True - - return req - - async def _build_signal_with_start_workflow_execution_request( - self, input: StartWorkflowInput - ) -> temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest: - assert input.start_signal - req = temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest( - signal_name=input.start_signal - ) - if input.start_signal_args: - req.signal_input.payloads.extend( - await self._client.data_converter.encode(input.start_signal_args) - ) - await self._populate_start_workflow_execution_request(req, input) - return req - - async def _build_update_with_start_start_workflow_execution_request( - self, input: UpdateWithStartStartWorkflowInput - ) -> temporalio.api.workflowservice.v1.StartWorkflowExecutionRequest: - req = temporalio.api.workflowservice.v1.StartWorkflowExecutionRequest() - await self._populate_start_workflow_execution_request(req, input) - return req - - async def _populate_start_workflow_execution_request( - self, - req: Union[ - temporalio.api.workflowservice.v1.StartWorkflowExecutionRequest, - temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest, - ], - input: Union[StartWorkflowInput, UpdateWithStartStartWorkflowInput], - ) -> None: - req.namespace = self._client.namespace - req.workflow_id = input.id - req.workflow_type.name = input.workflow - req.task_queue.name = input.task_queue - if input.args: - req.input.payloads.extend( - await self._client.data_converter.encode(input.args) - ) - if input.execution_timeout is not None: - req.workflow_execution_timeout.FromTimedelta(input.execution_timeout) - if input.run_timeout is not None: - req.workflow_run_timeout.FromTimedelta(input.run_timeout) - if input.task_timeout is not None: - req.workflow_task_timeout.FromTimedelta(input.task_timeout) - req.identity = self._client.identity - req.request_id = str(uuid.uuid4()) - req.workflow_id_reuse_policy = cast( - "temporalio.api.enums.v1.WorkflowIdReusePolicy.ValueType", - int(input.id_reuse_policy), - ) - req.workflow_id_conflict_policy = cast( - "temporalio.api.enums.v1.WorkflowIdConflictPolicy.ValueType", - int(input.id_conflict_policy), - ) - - if input.retry_policy is not None: - input.retry_policy.apply_to_proto(req.retry_policy) - req.cron_schedule = input.cron_schedule - if input.memo is not None: - for k, v in input.memo.items(): - req.memo.fields[k].CopyFrom( - (await self._client.data_converter.encode([v]))[0] - ) - if input.search_attributes is not None: - temporalio.converter.encode_search_attributes( - input.search_attributes, req.search_attributes - ) - metadata = await _encode_user_metadata( - self._client.data_converter, input.static_summary, input.static_details - ) - if metadata is not None: - req.user_metadata.CopyFrom(metadata) - if input.start_delay is not None: - req.workflow_start_delay.FromTimedelta(input.start_delay) - if input.headers is not None: - await self._apply_headers(input.headers, req.header.fields) - if input.priority is not None: - req.priority.CopyFrom(input.priority._to_proto()) - if input.versioning_override is not None: - req.versioning_override.CopyFrom(input.versioning_override._to_proto()) - - async def cancel_workflow(self, input: CancelWorkflowInput) -> None: - await self._client.workflow_service.request_cancel_workflow_execution( - temporalio.api.workflowservice.v1.RequestCancelWorkflowExecutionRequest( - namespace=self._client.namespace, - workflow_execution=temporalio.api.common.v1.WorkflowExecution( - workflow_id=input.id, - run_id=input.run_id or "", - ), - identity=self._client.identity, - request_id=str(uuid.uuid4()), - first_execution_run_id=input.first_execution_run_id or "", - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - - async def describe_workflow( - self, input: DescribeWorkflowInput - ) -> WorkflowExecutionDescription: - return await WorkflowExecutionDescription._from_raw_description( - await self._client.workflow_service.describe_workflow_execution( - temporalio.api.workflowservice.v1.DescribeWorkflowExecutionRequest( - namespace=self._client.namespace, - execution=temporalio.api.common.v1.WorkflowExecution( - workflow_id=input.id, - run_id=input.run_id or "", - ), - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ), - self._client.data_converter, - ) - - def fetch_workflow_history_events( - self, input: FetchWorkflowHistoryEventsInput - ) -> WorkflowHistoryEventAsyncIterator: - return WorkflowHistoryEventAsyncIterator(self._client, input) - - def list_workflows( - self, input: ListWorkflowsInput - ) -> WorkflowExecutionAsyncIterator: - return WorkflowExecutionAsyncIterator(self._client, input) - - async def count_workflows( - self, input: CountWorkflowsInput - ) -> WorkflowExecutionCount: - return WorkflowExecutionCount._from_raw( - await self._client.workflow_service.count_workflow_executions( - temporalio.api.workflowservice.v1.CountWorkflowExecutionsRequest( - namespace=self._client.namespace, - query=input.query or "", - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - ) - - async def query_workflow(self, input: QueryWorkflowInput) -> Any: - req = temporalio.api.workflowservice.v1.QueryWorkflowRequest( - namespace=self._client.namespace, - execution=temporalio.api.common.v1.WorkflowExecution( - workflow_id=input.id, - run_id=input.run_id or "", - ), - ) - if input.reject_condition: - req.query_reject_condition = cast( - "temporalio.api.enums.v1.QueryRejectCondition.ValueType", - int(input.reject_condition), - ) - req.query.query_type = input.query - if input.args: - req.query.query_args.payloads.extend( - await self._client.data_converter.encode(input.args) - ) - if input.headers is not None: - await self._apply_headers(input.headers, req.query.header.fields) - try: - resp = await self._client.workflow_service.query_workflow( - req, retry=True, metadata=input.rpc_metadata, timeout=input.rpc_timeout - ) - except RPCError as err: - # If the status is INVALID_ARGUMENT, we can assume it's a query - # failed error - if err.status == RPCStatusCode.INVALID_ARGUMENT: - raise WorkflowQueryFailedError(err.message) - else: - raise - if resp.HasField("query_rejected"): - raise WorkflowQueryRejectedError( - WorkflowExecutionStatus(resp.query_rejected.status) - if resp.query_rejected.status - else None - ) - if not resp.query_result.payloads: - return None - type_hints = [input.ret_type] if input.ret_type else None - results = await self._client.data_converter.decode( - resp.query_result.payloads, type_hints - ) - if not results: - return None - elif len(results) > 1: - warnings.warn(f"Expected single query result, got {len(results)}") - return results[0] - - async def signal_workflow(self, input: SignalWorkflowInput) -> None: - req = temporalio.api.workflowservice.v1.SignalWorkflowExecutionRequest( - namespace=self._client.namespace, - workflow_execution=temporalio.api.common.v1.WorkflowExecution( - workflow_id=input.id, - run_id=input.run_id or "", - ), - signal_name=input.signal, - identity=self._client.identity, - request_id=str(uuid.uuid4()), - ) - if input.args: - req.input.payloads.extend( - await self._client.data_converter.encode(input.args) - ) - if input.headers is not None: - await self._apply_headers(input.headers, req.header.fields) - await self._client.workflow_service.signal_workflow_execution( - req, retry=True, metadata=input.rpc_metadata, timeout=input.rpc_timeout - ) - - async def terminate_workflow(self, input: TerminateWorkflowInput) -> None: - req = temporalio.api.workflowservice.v1.TerminateWorkflowExecutionRequest( - namespace=self._client.namespace, - workflow_execution=temporalio.api.common.v1.WorkflowExecution( - workflow_id=input.id, - run_id=input.run_id or "", - ), - reason=input.reason or "", - identity=self._client.identity, - first_execution_run_id=input.first_execution_run_id or "", - ) - if input.args: - req.details.payloads.extend( - await self._client.data_converter.encode(input.args) - ) - await self._client.workflow_service.terminate_workflow_execution( - req, retry=True, metadata=input.rpc_metadata, timeout=input.rpc_timeout - ) - - async def start_workflow_update( - self, input: StartWorkflowUpdateInput - ) -> WorkflowUpdateHandle[Any]: - workflow_id = input.id - req = await self._build_update_workflow_execution_request(input, workflow_id) - - # Repeatedly try to invoke UpdateWorkflowExecution until the update is durable. - resp: temporalio.api.workflowservice.v1.UpdateWorkflowExecutionResponse - while True: - try: - resp = await self._client.workflow_service.update_workflow_execution( - req, - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - except RPCError as err: - if ( - err.status == RPCStatusCode.DEADLINE_EXCEEDED - or err.status == RPCStatusCode.CANCELLED - ): - raise WorkflowUpdateRPCTimeoutOrCancelledError() from err - else: - raise - except asyncio.CancelledError as err: - raise WorkflowUpdateRPCTimeoutOrCancelledError() from err - if ( - resp.stage - >= temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED - ): - break - - # Build the handle. If the user's wait stage is COMPLETED, make sure we - # poll for result. - handle: WorkflowUpdateHandle[Any] = WorkflowUpdateHandle( - client=self._client, - id=req.request.meta.update_id, - workflow_id=workflow_id, - workflow_run_id=resp.update_ref.workflow_execution.run_id, - result_type=input.ret_type, - ) - if resp.HasField("outcome"): - handle._known_outcome = resp.outcome - if input.wait_for_stage == WorkflowUpdateStage.COMPLETED: - await handle._poll_until_outcome() - return handle - - async def _build_update_workflow_execution_request( - self, - input: Union[StartWorkflowUpdateInput, UpdateWithStartUpdateWorkflowInput], - workflow_id: str, - ) -> temporalio.api.workflowservice.v1.UpdateWorkflowExecutionRequest: - run_id, first_execution_run_id = ( - ( - input.run_id, - input.first_execution_run_id, - ) - if isinstance(input, StartWorkflowUpdateInput) - else (None, None) - ) - req = temporalio.api.workflowservice.v1.UpdateWorkflowExecutionRequest( - namespace=self._client.namespace, - workflow_execution=temporalio.api.common.v1.WorkflowExecution( - workflow_id=workflow_id, - run_id=run_id or "", - ), - first_execution_run_id=first_execution_run_id or "", - request=temporalio.api.update.v1.Request( - meta=temporalio.api.update.v1.Meta( - update_id=input.update_id or str(uuid.uuid4()), - identity=self._client.identity, - ), - input=temporalio.api.update.v1.Input( - name=input.update, - ), - ), - wait_policy=temporalio.api.update.v1.WaitPolicy( - lifecycle_stage=temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.ValueType( - input.wait_for_stage - ) - ), - ) - if input.args: - req.request.input.args.payloads.extend( - await self._client.data_converter.encode(input.args) - ) - if input.headers is not None: - await self._apply_headers(input.headers, req.request.input.header.fields) - return req - - async def start_update_with_start_workflow( - self, input: StartWorkflowUpdateWithStartInput - ) -> WorkflowUpdateHandle[Any]: - seen_start = False - - def on_start( - start_response: temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse, - ): - nonlocal seen_start - if not seen_start: - input._on_start(start_response) - seen_start = True - - err: Optional[BaseException] = None - - try: - return await self._start_workflow_update_with_start( - input.start_workflow_input, input.update_workflow_input, on_start - ) - except asyncio.CancelledError as _err: - err = _err - raise WorkflowUpdateRPCTimeoutOrCancelledError() from err - except RPCError as _err: - err = _err - if err.status in [ - RPCStatusCode.DEADLINE_EXCEEDED, - RPCStatusCode.CANCELLED, - ]: - raise WorkflowUpdateRPCTimeoutOrCancelledError() from err - else: - multiop_failure = ( - temporalio.api.errordetails.v1.MultiOperationExecutionFailure() - ) - if err.grpc_status.details and err.grpc_status.details[0].Unpack( - multiop_failure - ): - status = next( - ( - st - for st in multiop_failure.statuses - if ( - st.code != RPCStatusCode.OK - and not ( - st.details - and st.details[0].Is( - temporalio.api.failure.v1.MultiOperationExecutionAborted.DESCRIPTOR - ) - ) - ) - ), - None, - ) - if status and status.code in list(RPCStatusCode): - if ( - status.code == RPCStatusCode.ALREADY_EXISTS - and status.details - ): - details = temporalio.api.errordetails.v1.WorkflowExecutionAlreadyStartedFailure() - if status.details[0].Unpack(details): - err = temporalio.exceptions.WorkflowAlreadyStartedError( - input.start_workflow_input.id, - input.start_workflow_input.workflow, - run_id=details.run_id, - ) - else: - err = RPCError( - status.message, - RPCStatusCode(status.code), - err.raw_grpc_status, - ) - raise err - finally: - if err and not seen_start: - input._on_start_error(err) - - async def _start_workflow_update_with_start( - self, - start_input: UpdateWithStartStartWorkflowInput, - update_input: UpdateWithStartUpdateWorkflowInput, - on_start: Callable[ - [temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse], None - ], - ) -> WorkflowUpdateHandle[Any]: - start_req = ( - await self._build_update_with_start_start_workflow_execution_request( - start_input - ) - ) - update_req = await self._build_update_workflow_execution_request( - update_input, workflow_id=start_input.id - ) - multiop_req = temporalio.api.workflowservice.v1.ExecuteMultiOperationRequest( - namespace=self._client.namespace, - operations=[ - temporalio.api.workflowservice.v1.ExecuteMultiOperationRequest.Operation( - start_workflow=start_req - ), - temporalio.api.workflowservice.v1.ExecuteMultiOperationRequest.Operation( - update_workflow=update_req - ), - ], - ) - - # Repeatedly try to invoke ExecuteMultiOperation until the update is durable - while True: - multiop_response = ( - await self._client.workflow_service.execute_multi_operation(multiop_req) - ) - start_response = multiop_response.responses[0].start_workflow - update_response = multiop_response.responses[1].update_workflow - on_start(start_response) - known_outcome = ( - update_response.outcome if update_response.HasField("outcome") else None - ) - if ( - update_response.stage - >= temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED - ): - break - - handle: WorkflowUpdateHandle[Any] = WorkflowUpdateHandle( - client=self._client, - id=update_req.request.meta.update_id, - workflow_id=start_input.id, - workflow_run_id=start_response.run_id, - known_outcome=known_outcome, - result_type=update_input.ret_type, - ) - if update_input.wait_for_stage == WorkflowUpdateStage.COMPLETED: - await handle._poll_until_outcome() - - return handle - - ### Async activity calls - - async def heartbeat_async_activity( - self, input: HeartbeatAsyncActivityInput - ) -> None: - details = ( - None - if not input.details - else await self._client.data_converter.encode_wrapper(input.details) - ) - if isinstance(input.id_or_token, AsyncActivityIDReference): - resp_by_id = await self._client.workflow_service.record_activity_task_heartbeat_by_id( - temporalio.api.workflowservice.v1.RecordActivityTaskHeartbeatByIdRequest( - workflow_id=input.id_or_token.workflow_id, - run_id=input.id_or_token.run_id or "", - activity_id=input.id_or_token.activity_id, - namespace=self._client.namespace, - identity=self._client.identity, - details=details, - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - if ( - resp_by_id.cancel_requested - or resp_by_id.activity_paused - or resp_by_id.activity_reset - ): - raise AsyncActivityCancelledError( - details=ActivityCancellationDetails( - cancel_requested=resp_by_id.cancel_requested, - paused=resp_by_id.activity_paused, - reset=resp_by_id.activity_reset, - ) - ) - - else: - resp = await self._client.workflow_service.record_activity_task_heartbeat( - temporalio.api.workflowservice.v1.RecordActivityTaskHeartbeatRequest( - task_token=input.id_or_token, - namespace=self._client.namespace, - identity=self._client.identity, - details=details, - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - if resp.cancel_requested or resp.activity_paused: - raise AsyncActivityCancelledError( - details=ActivityCancellationDetails( - cancel_requested=resp.cancel_requested, - paused=resp.activity_paused, - reset=resp.activity_reset, - ) - ) - - async def complete_async_activity(self, input: CompleteAsyncActivityInput) -> None: - result = ( - None - if input.result is temporalio.common._arg_unset - else await self._client.data_converter.encode_wrapper([input.result]) - ) - if isinstance(input.id_or_token, AsyncActivityIDReference): - await self._client.workflow_service.respond_activity_task_completed_by_id( - temporalio.api.workflowservice.v1.RespondActivityTaskCompletedByIdRequest( - workflow_id=input.id_or_token.workflow_id, - run_id=input.id_or_token.run_id or "", - activity_id=input.id_or_token.activity_id, - namespace=self._client.namespace, - identity=self._client.identity, - result=result, - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - else: - await self._client.workflow_service.respond_activity_task_completed( - temporalio.api.workflowservice.v1.RespondActivityTaskCompletedRequest( - task_token=input.id_or_token, - namespace=self._client.namespace, - identity=self._client.identity, - result=result, - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - - async def fail_async_activity(self, input: FailAsyncActivityInput) -> None: - failure = temporalio.api.failure.v1.Failure() - await self._client.data_converter.encode_failure(input.error, failure) - last_heartbeat_details = ( - None - if not input.last_heartbeat_details - else await self._client.data_converter.encode_wrapper( - input.last_heartbeat_details - ) - ) - if isinstance(input.id_or_token, AsyncActivityIDReference): - await self._client.workflow_service.respond_activity_task_failed_by_id( - temporalio.api.workflowservice.v1.RespondActivityTaskFailedByIdRequest( - workflow_id=input.id_or_token.workflow_id, - run_id=input.id_or_token.run_id or "", - activity_id=input.id_or_token.activity_id, - namespace=self._client.namespace, - identity=self._client.identity, - failure=failure, - last_heartbeat_details=last_heartbeat_details, - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - else: - await self._client.workflow_service.respond_activity_task_failed( - temporalio.api.workflowservice.v1.RespondActivityTaskFailedRequest( - task_token=input.id_or_token, - namespace=self._client.namespace, - identity=self._client.identity, - failure=failure, - last_heartbeat_details=last_heartbeat_details, - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - - async def report_cancellation_async_activity( - self, input: ReportCancellationAsyncActivityInput - ) -> None: - details = ( - None - if not input.details - else await self._client.data_converter.encode_wrapper(input.details) - ) - if isinstance(input.id_or_token, AsyncActivityIDReference): - await self._client.workflow_service.respond_activity_task_canceled_by_id( - temporalio.api.workflowservice.v1.RespondActivityTaskCanceledByIdRequest( - workflow_id=input.id_or_token.workflow_id, - run_id=input.id_or_token.run_id or "", - activity_id=input.id_or_token.activity_id, - namespace=self._client.namespace, - identity=self._client.identity, - details=details, - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - else: - await self._client.workflow_service.respond_activity_task_canceled( - temporalio.api.workflowservice.v1.RespondActivityTaskCanceledRequest( - task_token=input.id_or_token, - namespace=self._client.namespace, - identity=self._client.identity, - details=details, - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - - ### Schedule calls - - async def create_schedule(self, input: CreateScheduleInput) -> ScheduleHandle: - # Limited actions must be false if remaining actions is 0 and must be - # true if remaining actions is non-zero - if ( - input.schedule.state.limited_actions - and not input.schedule.state.remaining_actions - ): - raise ValueError( - "Must set limited actions to false if there are no remaining actions set" - ) - if ( - not input.schedule.state.limited_actions - and input.schedule.state.remaining_actions - ): - raise ValueError( - "Must set limited actions to true if there are remaining actions set" - ) - - initial_patch: Optional[temporalio.api.schedule.v1.SchedulePatch] = None - if input.trigger_immediately or input.backfill: - initial_patch = temporalio.api.schedule.v1.SchedulePatch( - trigger_immediately=temporalio.api.schedule.v1.TriggerImmediatelyRequest( - overlap_policy=temporalio.api.enums.v1.ScheduleOverlapPolicy.ValueType( - input.schedule.policy.overlap - ), - ) - if input.trigger_immediately - else None, - backfill_request=[b._to_proto() for b in input.backfill] - if input.backfill - else None, - ) - try: - request = temporalio.api.workflowservice.v1.CreateScheduleRequest( - namespace=self._client.namespace, - schedule_id=input.id, - schedule=await input.schedule._to_proto(self._client), - initial_patch=initial_patch, - identity=self._client.identity, - request_id=str(uuid.uuid4()), - memo=None - if not input.memo - else temporalio.api.common.v1.Memo( - fields={ - k: (await self._client.data_converter.encode([v]))[0] - for k, v in input.memo.items() - }, - ), - ) - if input.search_attributes: - temporalio.converter.encode_search_attributes( - input.search_attributes, request.search_attributes - ) - await self._client.workflow_service.create_schedule( - request, - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - except RPCError as err: - already_started = ( - err.status == RPCStatusCode.ALREADY_EXISTS - and err.grpc_status.details - and err.grpc_status.details[0].Is( - temporalio.api.errordetails.v1.WorkflowExecutionAlreadyStartedFailure.DESCRIPTOR - ) - ) - if already_started: - raise ScheduleAlreadyRunningError() - raise - return ScheduleHandle(self._client, input.id) - - def list_schedules(self, input: ListSchedulesInput) -> ScheduleAsyncIterator: - return ScheduleAsyncIterator(self._client, input) - - async def backfill_schedule(self, input: BackfillScheduleInput) -> None: - await self._client.workflow_service.patch_schedule( - temporalio.api.workflowservice.v1.PatchScheduleRequest( - namespace=self._client.namespace, - schedule_id=input.id, - patch=temporalio.api.schedule.v1.SchedulePatch( - backfill_request=[b._to_proto() for b in input.backfills], - ), - identity=self._client.identity, - request_id=str(uuid.uuid4()), - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - - async def delete_schedule(self, input: DeleteScheduleInput) -> None: - await self._client.workflow_service.delete_schedule( - temporalio.api.workflowservice.v1.DeleteScheduleRequest( - namespace=self._client.namespace, - schedule_id=input.id, - identity=self._client.identity, - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - - async def describe_schedule( - self, input: DescribeScheduleInput - ) -> ScheduleDescription: - return ScheduleDescription._from_proto( - input.id, - await self._client.workflow_service.describe_schedule( - temporalio.api.workflowservice.v1.DescribeScheduleRequest( - namespace=self._client.namespace, - schedule_id=input.id, - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ), - self._client.data_converter, - ) - - async def pause_schedule(self, input: PauseScheduleInput) -> None: - await self._client.workflow_service.patch_schedule( - temporalio.api.workflowservice.v1.PatchScheduleRequest( - namespace=self._client.namespace, - schedule_id=input.id, - patch=temporalio.api.schedule.v1.SchedulePatch( - pause=input.note or "Paused via Python SDK", - ), - identity=self._client.identity, - request_id=str(uuid.uuid4()), - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - - async def trigger_schedule(self, input: TriggerScheduleInput) -> None: - overlap_policy = temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_UNSPECIFIED - if input.overlap: - overlap_policy = temporalio.api.enums.v1.ScheduleOverlapPolicy.ValueType( - input.overlap - ) - await self._client.workflow_service.patch_schedule( - temporalio.api.workflowservice.v1.PatchScheduleRequest( - namespace=self._client.namespace, - schedule_id=input.id, - patch=temporalio.api.schedule.v1.SchedulePatch( - trigger_immediately=temporalio.api.schedule.v1.TriggerImmediatelyRequest( - overlap_policy=overlap_policy, - ), - ), - identity=self._client.identity, - request_id=str(uuid.uuid4()), - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - - async def unpause_schedule(self, input: UnpauseScheduleInput) -> None: - await self._client.workflow_service.patch_schedule( - temporalio.api.workflowservice.v1.PatchScheduleRequest( - namespace=self._client.namespace, - schedule_id=input.id, - patch=temporalio.api.schedule.v1.SchedulePatch( - unpause=input.note or "Unpaused via Python SDK", - ), - identity=self._client.identity, - request_id=str(uuid.uuid4()), - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - - async def update_schedule(self, input: UpdateScheduleInput) -> None: - # TODO(cretz): This is supposed to be a retry-conflict loop, but we do - # not yet have a way to know update failure is due to conflict token - # mismatch - update = input.updater( - ScheduleUpdateInput( - description=ScheduleDescription._from_proto( - input.id, - await self._client.workflow_service.describe_schedule( - temporalio.api.workflowservice.v1.DescribeScheduleRequest( - namespace=self._client.namespace, - schedule_id=input.id, - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ), - self._client.data_converter, - ) - ) - ) - if inspect.iscoroutine(update): - update = await update - if not update: - return - assert isinstance(update, ScheduleUpdate) - request = temporalio.api.workflowservice.v1.UpdateScheduleRequest( - namespace=self._client.namespace, - schedule_id=input.id, - schedule=await update.schedule._to_proto(self._client), - identity=self._client.identity, - request_id=str(uuid.uuid4()), - ) - if update.search_attributes is not None: - request.search_attributes.indexed_fields.clear() # Ensure that we at least create an empty map - temporalio.converter.encode_search_attributes( - update.search_attributes, request.search_attributes - ) - await self._client.workflow_service.update_schedule( - request, - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - - async def update_worker_build_id_compatibility( - self, input: UpdateWorkerBuildIdCompatibilityInput - ) -> None: - req = input.operation._as_partial_proto() - req.namespace = self._client.namespace - req.task_queue = input.task_queue - await self._client.workflow_service.update_worker_build_id_compatibility( - req, retry=True, metadata=input.rpc_metadata, timeout=input.rpc_timeout - ) - - async def get_worker_build_id_compatibility( - self, input: GetWorkerBuildIdCompatibilityInput - ) -> WorkerBuildIdVersionSets: - req = temporalio.api.workflowservice.v1.GetWorkerBuildIdCompatibilityRequest( - namespace=self._client.namespace, - task_queue=input.task_queue, - max_sets=input.max_sets or 0, - ) - resp = await self._client.workflow_service.get_worker_build_id_compatibility( - req, retry=True, metadata=input.rpc_metadata, timeout=input.rpc_timeout - ) - return WorkerBuildIdVersionSets._from_proto(resp) - - async def get_worker_task_reachability( - self, input: GetWorkerTaskReachabilityInput - ) -> WorkerTaskReachability: - req = temporalio.api.workflowservice.v1.GetWorkerTaskReachabilityRequest( - namespace=self._client.namespace, - build_ids=input.build_ids, - task_queues=input.task_queues, - reachability=input.reachability._to_proto() - if input.reachability - else temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_UNSPECIFIED, - ) - resp = await self._client.workflow_service.get_worker_task_reachability( - req, retry=True, metadata=input.rpc_metadata, timeout=input.rpc_timeout - ) - return WorkerTaskReachability._from_proto(resp) - - async def _apply_headers( - self, - source: Optional[Mapping[str, temporalio.api.common.v1.Payload]], - dest: MessageMap[Text, temporalio.api.common.v1.Payload], - ) -> None: - await _apply_headers( - source, - dest, - self._client.config()["header_codec_behavior"] == HeaderCodecBehavior.CODEC, - self._client.data_converter.payload_codec, - ) - - -async def _apply_headers( - source: Optional[Mapping[str, temporalio.api.common.v1.Payload]], - dest: MessageMap[Text, temporalio.api.common.v1.Payload], - encode_headers: bool, - codec: Optional[temporalio.converter.PayloadCodec], -) -> None: - if source is None: - return - if encode_headers and codec is not None: - for payload in source.values(): - new_payload = (await codec.encode([payload]))[0] - payload.CopyFrom(new_payload) - temporalio.common._apply_headers(source, dest) - - -def _history_from_json( - history: Union[str, Dict[str, Any]], -) -> temporalio.api.history.v1.History: - if isinstance(history, str): - history = json.loads(history) - else: - # Copy the dict so we can mutate it - history = copy.deepcopy(history) - if not isinstance(history, dict): - raise ValueError("JSON history not a dictionary") - events = history.get("events") - if not isinstance(events, Iterable): - raise ValueError("History does not have iterable 'events'") - for event in events: - if not isinstance(event, dict): - raise ValueError("Event not a dictionary") - _fix_history_enum( - "CANCEL_EXTERNAL_WORKFLOW_EXECUTION_FAILED_CAUSE", - event, - "requestCancelExternalWorkflowExecutionFailedEventAttributes", - "cause", - ) - _fix_history_enum("CONTINUE_AS_NEW_INITIATOR", event, "*", "initiator") - _fix_history_enum("EVENT_TYPE", event, "eventType") - _fix_history_enum( - "PARENT_CLOSE_POLICY", - event, - "startChildWorkflowExecutionInitiatedEventAttributes", - "parentClosePolicy", - ) - _fix_history_enum("RETRY_STATE", event, "*", "retryState") - _fix_history_enum( - "SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_FAILED_CAUSE", - event, - "signalExternalWorkflowExecutionFailedEventAttributes", - "cause", - ) - _fix_history_enum( - "START_CHILD_WORKFLOW_EXECUTION_FAILED_CAUSE", - event, - "startChildWorkflowExecutionFailedEventAttributes", - "cause", - ) - _fix_history_enum("TASK_QUEUE_KIND", event, "*", "taskQueue", "kind") - _fix_history_enum( - "TIMEOUT_TYPE", - event, - "workflowTaskTimedOutEventAttributes", - "timeoutType", - ) - _fix_history_enum( - "WORKFLOW_ID_REUSE_POLICY", - event, - "startChildWorkflowExecutionInitiatedEventAttributes", - "workflowIdReusePolicy", - ) - _fix_history_enum( - "WORKFLOW_TASK_FAILED_CAUSE", - event, - "workflowTaskFailedEventAttributes", - "cause", - ) - _fix_history_failure(event, "*", "failure") - _fix_history_failure(event, "activityTaskStartedEventAttributes", "lastFailure") - _fix_history_failure( - event, "workflowExecutionStartedEventAttributes", "continuedFailure" - ) - return google.protobuf.json_format.ParseDict( - history, temporalio.api.history.v1.History(), ignore_unknown_fields=True - ) - - -_pascal_case_match = re.compile("([A-Z]+)") - - -def _fix_history_failure(parent: Dict[str, Any], *attrs: str) -> None: - _fix_history_enum( - "TIMEOUT_TYPE", parent, *attrs, "timeoutFailureInfo", "timeoutType" - ) - _fix_history_enum("RETRY_STATE", parent, *attrs, "*", "retryState") - # Recurse into causes. First collect all failure parents. - parents = [parent] - for attr in attrs: - new_parents = [] - for parent in parents: - if attr == "*": - for v in parent.values(): - if isinstance(v, dict): - new_parents.append(v) - else: - child = parent.get(attr) - if isinstance(child, dict): - new_parents.append(child) - if not new_parents: - return - parents = new_parents - # Fix each - for parent in parents: - _fix_history_failure(parent, "cause") - - -def _fix_history_enum(prefix: str, parent: Dict[str, Any], *attrs: str) -> None: - # If the attr is "*", we need to handle all dict children - if attrs[0] == "*": - for child in parent.values(): - if isinstance(child, dict): - _fix_history_enum(prefix, child, *attrs[1:]) - else: - child = parent.get(attrs[0]) - if isinstance(child, str) and len(attrs) == 1: - # We only fix it if it doesn't already have the prefix - if not parent[attrs[0]].startswith(prefix): - parent[attrs[0]] = ( - prefix + _pascal_case_match.sub(r"_\1", child).upper() - ) - elif isinstance(child, dict) and len(attrs) > 1: - _fix_history_enum(prefix, child, *attrs[1:]) - elif isinstance(child, list) and len(attrs) > 1: - for child_item in child: - if isinstance(child_item, dict): - _fix_history_enum(prefix, child_item, *attrs[1:]) - - -@dataclass(frozen=True) -class WorkerBuildIdVersionSets: - """Represents the sets of compatible Build ID versions associated with some Task Queue, as - fetched by :py:meth:`Client.get_worker_build_id_compatibility`. - """ - - version_sets: Sequence[BuildIdVersionSet] - """All version sets that were fetched for this task queue.""" - - def default_set(self) -> BuildIdVersionSet: - """Returns the default version set for this task queue.""" - return self.version_sets[-1] - - def default_build_id(self) -> str: - """Returns the default Build ID for this task queue.""" - return self.default_set().default() - - @staticmethod - def _from_proto( - resp: temporalio.api.workflowservice.v1.GetWorkerBuildIdCompatibilityResponse, - ) -> WorkerBuildIdVersionSets: - return WorkerBuildIdVersionSets( - version_sets=[ - BuildIdVersionSet(mvs.build_ids) for mvs in resp.major_version_sets - ] - ) - - -@dataclass(frozen=True) -class BuildIdVersionSet: - """A set of Build IDs which are compatible with each other.""" - - build_ids: Sequence[str] - """All Build IDs contained in the set.""" - - def default(self) -> str: - """Returns the default Build ID for this set.""" - return self.build_ids[-1] - - -class BuildIdOp(ABC): - """Base class for Build ID operations as used by - :py:meth:`Client.update_worker_build_id_compatibility`. - """ - - @abstractmethod - def _as_partial_proto( - self, - ) -> temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest: - """Returns a partial request with the operation populated. Caller must populate - non-operation fields. This is done b/c there's no good way to assign a non-primitive message - as the operation after initializing the request. - """ - ... - - -@dataclass(frozen=True) -class BuildIdOpAddNewDefault(BuildIdOp): - """Adds a new Build Id into a new set, which will be used as the default set for - the queue. This means all new workflows will start on this Build Id. - """ - - build_id: str - - def _as_partial_proto( - self, - ) -> temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest: - return ( - temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest( - add_new_build_id_in_new_default_set=self.build_id - ) - ) - - -@dataclass(frozen=True) -class BuildIdOpAddNewCompatible(BuildIdOp): - """Adds a new Build Id into an existing compatible set. The newly added ID becomes - the default for that compatible set, and thus new workflow tasks for workflows which have been - executing on workers in that set will now start on this new Build Id. - """ - - build_id: str - """The Build Id to add to the compatible set.""" - - existing_compatible_build_id: str - """A Build Id which must already be defined on the task queue, and is used to find the - compatible set to add the new id to. - """ - - promote_set: bool = False - """If set to true, the targeted set will also be promoted to become the overall default set for - the queue.""" - - def _as_partial_proto( - self, - ) -> temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest: - return temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest( - add_new_compatible_build_id=temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest.AddNewCompatibleVersion( - new_build_id=self.build_id, - existing_compatible_build_id=self.existing_compatible_build_id, - make_set_default=self.promote_set, - ) - ) - - -@dataclass(frozen=True) -class BuildIdOpPromoteSetByBuildId(BuildIdOp): - """Promotes a set of compatible Build Ids to become the current default set for the task queue. - Any Build Id in the set may be used to target it. - """ - - build_id: str - """A Build Id which must already be defined on the task queue, and is used to find the - compatible set to promote.""" - - def _as_partial_proto( - self, - ) -> temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest: - return ( - temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest( - promote_set_by_build_id=self.build_id - ) - ) - - -@dataclass(frozen=True) -class BuildIdOpPromoteBuildIdWithinSet(BuildIdOp): - """Promotes a Build Id within an existing set to become the default ID for that set.""" - - build_id: str - - def _as_partial_proto( - self, - ) -> temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest: - return ( - temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest( - promote_build_id_within_set=self.build_id - ) - ) - - -@dataclass(frozen=True) -class BuildIdOpMergeSets(BuildIdOp): - """Merges two sets into one set, thus declaring all the Build Ids in both as compatible with one - another. The default of the primary set is maintained as the merged set's overall default. - """ - - primary_build_id: str - """A Build Id which and is used to find the primary set to be merged.""" - - secondary_build_id: str - """A Build Id which and is used to find the secondary set to be merged.""" - - def _as_partial_proto( - self, - ) -> temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest: - return temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest( - merge_sets=temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest.MergeSets( - primary_set_build_id=self.primary_build_id, - secondary_set_build_id=self.secondary_build_id, - ) - ) - - -@dataclass(frozen=True) -class WorkerTaskReachability: - """Contains information about the reachability of some Build IDs""" - - build_id_reachability: Mapping[str, BuildIdReachability] - """Maps Build IDs to information about their reachability""" - - @staticmethod - def _from_proto( - resp: temporalio.api.workflowservice.v1.GetWorkerTaskReachabilityResponse, - ) -> WorkerTaskReachability: - mapping = dict() - for bid_reach in resp.build_id_reachability: - tq_mapping = dict() - unretrieved = set() - for tq_reach in bid_reach.task_queue_reachability: - if tq_reach.reachability == [ - temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_UNSPECIFIED - ]: - unretrieved.add(tq_reach.task_queue) - continue - tq_mapping[tq_reach.task_queue] = [ - TaskReachabilityType._from_proto(r) for r in tq_reach.reachability - ] - - mapping[bid_reach.build_id] = BuildIdReachability( - task_queue_reachability=tq_mapping, - unretrieved_task_queues=frozenset(unretrieved), - ) - - return WorkerTaskReachability(build_id_reachability=mapping) - - -@dataclass(frozen=True) -class BuildIdReachability: - """Contains information about the reachability of a specific Build ID""" - - task_queue_reachability: Mapping[str, Sequence[TaskReachabilityType]] - """Maps Task Queue names to the reachability status of the Build ID on that queue. If the value - is an empty list, the Build ID is not reachable on that queue. - """ - - unretrieved_task_queues: FrozenSet[str] - """If any Task Queues could not be retrieved because the server limits the number that can be - queried at once, they will be listed here. - """ - - -class TaskReachabilityType(Enum): - """Enumerates how a task might reach certain kinds of workflows""" - - NEW_WORKFLOWS = 1 - EXISTING_WORKFLOWS = 2 - OPEN_WORKFLOWS = 3 - CLOSED_WORKFLOWS = 4 - - @staticmethod - def _from_proto( - reachability: temporalio.api.enums.v1.TaskReachability.ValueType, - ) -> TaskReachabilityType: - if ( - reachability - == temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_NEW_WORKFLOWS - ): - return TaskReachabilityType.NEW_WORKFLOWS - elif ( - reachability - == temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_EXISTING_WORKFLOWS - ): - return TaskReachabilityType.EXISTING_WORKFLOWS - elif ( - reachability - == temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_OPEN_WORKFLOWS - ): - return TaskReachabilityType.OPEN_WORKFLOWS - elif ( - reachability - == temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_CLOSED_WORKFLOWS - ): - return TaskReachabilityType.CLOSED_WORKFLOWS - else: - raise ValueError(f"Cannot convert reachability type: {reachability}") - - def _to_proto(self) -> temporalio.api.enums.v1.TaskReachability.ValueType: - if self == TaskReachabilityType.NEW_WORKFLOWS: - return ( - temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_NEW_WORKFLOWS - ) - elif self == TaskReachabilityType.EXISTING_WORKFLOWS: - return temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_EXISTING_WORKFLOWS - elif self == TaskReachabilityType.OPEN_WORKFLOWS: - return temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_OPEN_WORKFLOWS - elif self == TaskReachabilityType.CLOSED_WORKFLOWS: - return temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_CLOSED_WORKFLOWS - else: - return ( - temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_UNSPECIFIED - ) - - -class CloudOperationsClient: - """Client for accessing Temporal Cloud Operations API. - - .. warning:: - This client and the API are experimental - - Most users will use :py:meth:`connect` to create a client. The - :py:attr:`cloud_service` property provides access to a raw gRPC cloud - service client. - - Clients are not thread-safe and should only be used in the event loop they - are first connected in. If a client needs to be used from another thread - than where it was created, make sure the event loop where it was created is - captured, and then call :py:func:`asyncio.run_coroutine_threadsafe` with the - client call and that event loop. - - Clients do not work across forks since runtimes do not work across forks. - """ - - @staticmethod - async def connect( - *, - api_key: Optional[str] = None, - version: Optional[str] = None, - target_host: str = "saas-api.tmprl.cloud:443", - tls: Union[bool, TLSConfig] = True, - retry_config: Optional[RetryConfig] = None, - keep_alive_config: Optional[KeepAliveConfig] = KeepAliveConfig.default, - rpc_metadata: Mapping[str, str] = {}, - identity: Optional[str] = None, - lazy: bool = False, - runtime: Optional[temporalio.runtime.Runtime] = None, - http_connect_proxy_config: Optional[HttpConnectProxyConfig] = None, - ) -> CloudOperationsClient: - """Connect to a Temporal Cloud Operations API. - - .. warning:: - This client and the API are experimental - - Args: - api_key: API key for Temporal. This becomes the "Authorization" - HTTP header with "Bearer " prepended. This is only set if RPC - metadata doesn't already have an "authorization" key. This is - essentially required for access to the cloud API. - version: Version header for safer mutations. May or may not be - required depending on cloud settings. - target_host: ``host:port`` for the Temporal server. The default is - to the common cloud endpoint. - tls: If true, the default, use system default TLS configuration. If - false, the default, do not use TLS. If TLS configuration - present, that TLS configuration will be used. The default is - usually required to access the API. - retry_config: Retry configuration for direct service calls (when - opted in) or all high-level calls made by this client (which all - opt-in to retries by default). If unset, a default retry - configuration is used. - keep_alive_config: Keep-alive configuration for the client - connection. Default is to check every 30s and kill the - connection if a response doesn't come back in 15s. Can be set to - ``None`` to disable. - rpc_metadata: Headers to use for all calls to the server. Keys here - can be overriden by per-call RPC metadata keys. - identity: Identity for this client. If unset, a default is created - based on the version of the SDK. - lazy: If true, the client will not connect until the first call is - attempted or a worker is created with it. Lazy clients cannot be - used for workers. - runtime: The runtime for this client, or the default if unset. - http_connect_proxy_config: Configuration for HTTP CONNECT proxy. - """ - # Add version if given - if version: - rpc_metadata = dict(rpc_metadata) - rpc_metadata["temporal-cloud-api-version"] = version - connect_config = temporalio.service.ConnectConfig( - target_host=target_host, - api_key=api_key, - tls=tls, - retry_config=retry_config, - keep_alive_config=keep_alive_config, - rpc_metadata=rpc_metadata, - identity=identity or "", - lazy=lazy, - runtime=runtime, - http_connect_proxy_config=http_connect_proxy_config, - ) - return CloudOperationsClient( - await temporalio.service.ServiceClient.connect(connect_config) - ) - - def __init__( - self, - service_client: temporalio.service.ServiceClient, - ): - """Create a Temporal Cloud Operations client from a service client. - - .. warning:: - This client and the API are experimental - - Args: - service_client: Existing service client to use. - """ - self._service_client = service_client - - @property - def service_client(self) -> temporalio.service.ServiceClient: - """Raw gRPC service client.""" - return self._service_client - - @property - def cloud_service(self) -> temporalio.service.CloudService: - """Raw gRPC cloud service client.""" - return self._service_client.cloud_service - - @property - def identity(self) -> str: - """Identity used in calls by this client.""" - return self._service_client.config.identity - - @property - def rpc_metadata(self) -> Mapping[str, str]: - """Headers for every call made by this client. - - Do not use mutate this mapping. Rather, set this property with an - entirely new mapping to change the headers. This may include the - ``temporal-cloud-api-version`` header if set. - """ - return self.service_client.config.rpc_metadata - - @rpc_metadata.setter - def rpc_metadata(self, value: Mapping[str, str]) -> None: - """Update the headers for this client. - - Do not mutate this mapping after set. Rather, set an entirely new - mapping if changes are needed. Currently this must be set with the - ``temporal-cloud-api-version`` header if it is needed. - """ - # Update config and perform update - self.service_client.config.rpc_metadata = value - self.service_client.update_rpc_metadata(value) - - @property - def api_key(self) -> Optional[str]: - """API key for every call made by this client.""" - return self.service_client.config.api_key - - @api_key.setter - def api_key(self, value: Optional[str]) -> None: - """Update the API key for this client. - - This is only set if RPCmetadata doesn't already have an "authorization" - key. - """ - # Update config and perform update - self.service_client.config.api_key = value - self.service_client.update_api_key(value) - - -# Intended to become a union of callback types -Callback = temporalio.nexus.NexusCallback - - -async def _encode_user_metadata( - converter: temporalio.converter.DataConverter, - summary: Optional[Union[str, temporalio.api.common.v1.Payload]], - details: Optional[Union[str, temporalio.api.common.v1.Payload]], -) -> Optional[temporalio.api.sdk.v1.UserMetadata]: - if summary is None and details is None: - return None - enc_summary = None - enc_details = None - if summary is not None: - if isinstance(summary, str): - enc_summary = (await converter.encode([summary]))[0] - else: - enc_summary = summary - if details is not None: - if isinstance(details, str): - enc_details = (await converter.encode([details]))[0] - else: - enc_details = details - return temporalio.api.sdk.v1.UserMetadata(summary=enc_summary, details=enc_details) - - -async def _decode_user_metadata( - converter: temporalio.converter.DataConverter, - metadata: Optional[temporalio.api.sdk.v1.UserMetadata], -) -> Tuple[Optional[str], Optional[str]]: - """Returns (summary, details)""" - if metadata is None: - return None, None - return ( - None - if not metadata.HasField("summary") - else (await converter.decode([metadata.summary]))[0], - None - if not metadata.HasField("details") - else (await converter.decode([metadata.details]))[0], - ) - - -class Plugin(abc.ABC): - """Base class for client plugins that can intercept and modify client behavior. - - Plugins allow customization of client creation and service connection processes - through a chain of responsibility pattern. Each plugin can modify the client - configuration or intercept service client connections. - - If the plugin is also a temporalio.worker.Plugin, it will additionally be propagated as a worker plugin. - You should likley not also provide it to the worker as that will result in the plugin being applied twice. - """ - - def name(self) -> str: - """Get the name of this plugin. Can be overridden if desired to provide a more appropriate name. - - Returns: - The fully qualified name of the plugin class (module.classname). - """ - return type(self).__module__ + "." + type(self).__qualname__ - - @abstractmethod - def init_client_plugin(self, next: Plugin) -> None: - """Initialize this plugin in the plugin chain. - - This method sets up the chain of responsibility pattern by providing a reference - to the next plugin in the chain. It is called during client creation to build - the plugin chain. Note, this may be called twice in the case of :py:meth:`connect`. - Implementations should store this reference and call the corresponding method - of the next plugin on method calls. - - Args: - next: The next plugin in the chain to delegate to. - """ - - @abstractmethod - def configure_client(self, config: ClientConfig) -> ClientConfig: - """Hook called when creating a client to allow modification of configuration. - - This method is called during client creation and allows plugins to modify - the client configuration before the client is fully initialized. Plugins - can add interceptors, modify connection parameters, or change other settings. - - Args: - config: The client configuration dictionary to potentially modify. - - Returns: - The modified client configuration. - """ - - @abstractmethod - async def connect_service_client( - self, config: temporalio.service.ConnectConfig - ) -> temporalio.service.ServiceClient: - """Hook called when connecting to the Temporal service. - - This method is called during service client connection and allows plugins - to intercept or modify the connection process. Plugins can modify connection - parameters, add authentication, or provide custom connection logic. - - Args: - config: The service connection configuration. - - Returns: - The connected service client. - """ - - -class _RootPlugin(Plugin): - def init_client_plugin(self, next: Plugin) -> None: - raise NotImplementedError() - - def configure_client(self, config: ClientConfig) -> ClientConfig: - return config - - async def connect_service_client( - self, config: temporalio.service.ConnectConfig - ) -> temporalio.service.ServiceClient: - return await temporalio.service.ServiceClient.connect(config) diff --git a/temporalio/client/__init__.py b/temporalio/client/__init__.py new file mode 100644 index 000000000..cdb34b860 --- /dev/null +++ b/temporalio/client/__init__.py @@ -0,0 +1,384 @@ +"""Client for accessing Temporal.""" + +from __future__ import annotations + +from temporalio.activity import ActivityCancellationDetails +from temporalio.converter import ( + ActivitySerializationContext, + DataConverter, + SerializationContext, + StorageDriverActivityInfo, + StorageDriverStoreContext, + StorageDriverWorkflowInfo, + WithSerializationContext, + WorkflowSerializationContext, +) +from temporalio.service import ( + ConnectConfig, + DnsLoadBalancingConfig, + GrpcCompression, + HttpConnectProxyConfig, + KeepAliveConfig, + PayloadLimitsConfig, + RetryConfig, + RPCError, + RPCStatusCode, + ServiceClient, + TLSConfig, +) + +from ..common import HeaderCodecBehavior +from ..types import ( + AnyType, + CallableAsyncNoParam, + CallableAsyncSingleParam, + CallableSyncNoParam, + CallableSyncSingleParam, + LocalReturnType, + MethodAsyncNoParam, + MethodAsyncSingleParam, + MethodSyncOrAsyncNoParam, + MethodSyncOrAsyncSingleParam, + MultiParamSpec, + ParamType, + ReturnType, + SelfType, +) +from ._activity import ( + ActivityExecution, + ActivityExecutionAsyncIterator, + ActivityExecutionCount, + ActivityExecutionCountAggregationGroup, + ActivityExecutionDescription, + ActivityExecutionStatus, + ActivityHandle, + AsyncActivityHandle, + AsyncActivityIDReference, + PendingActivityState, +) +from ._callback import ( + Callback, +) +from ._client import ( + Client, + ClientConfig, + ClientConnectConfig, +) +from ._cloud import ( + CloudOperationsClient, +) +from ._exceptions import ( + ActivityFailureError, + AsyncActivityCancelledError, + RPCTimeoutOrCancelledError, + ScheduleAlreadyRunningError, + WorkflowContinuedAsNewError, + WorkflowFailureError, + WorkflowQueryFailedError, + WorkflowQueryRejectedError, + WorkflowUpdateFailedError, + WorkflowUpdateRPCTimeoutOrCancelledError, +) +from ._helpers import ( + _apply_headers, + _decode_user_metadata, + _encode_user_metadata, + _fix_history_enum, + _fix_history_failure, + _history_from_json, + _pascal_case_match, +) +from ._impl import _ClientImpl +from ._interceptor import ( + BackfillScheduleInput, + CancelActivityInput, + CancelNexusOperationInput, + CancelWorkflowInput, + CompleteAsyncActivityInput, + CountActivitiesInput, + CountNexusOperationsInput, + CountWorkflowsInput, + CreateScheduleInput, + DeleteScheduleInput, + DescribeActivityInput, + DescribeNexusOperationInput, + DescribeScheduleInput, + DescribeWorkflowInput, + FailAsyncActivityInput, + FetchWorkflowHistoryEventsInput, + GetNexusOperationResultInput, + GetWorkerBuildIdCompatibilityInput, + GetWorkerTaskReachabilityInput, + HeartbeatAsyncActivityInput, + Interceptor, + ListActivitiesInput, + ListNexusOperationsInput, + ListSchedulesInput, + ListWorkflowsInput, + OutboundInterceptor, + PauseScheduleInput, + QueryWorkflowInput, + ReportCancellationAsyncActivityInput, + SignalWorkflowInput, + StartActivityInput, + StartNexusOperationInput, + StartWorkflowInput, + StartWorkflowUpdateInput, + StartWorkflowUpdateWithStartInput, + TerminateActivityInput, + TerminateNexusOperationInput, + TerminateWorkflowInput, + TriggerScheduleInput, + UnpauseScheduleInput, + UpdateScheduleInput, + UpdateWithStartStartWorkflowInput, + UpdateWithStartUpdateWorkflowInput, + UpdateWorkerBuildIdCompatibilityInput, +) +from ._nexus import ( + NexusClient, + NexusOperationExecution, + NexusOperationExecutionAsyncIterator, + NexusOperationExecutionCancellationInfo, + NexusOperationExecutionCount, + NexusOperationExecutionCountAggregationGroup, + NexusOperationExecutionDescription, + NexusOperationFailureError, + NexusOperationHandle, +) +from ._plugin import ( + Plugin, +) +from ._schedule import ( + Schedule, + ScheduleAction, + ScheduleActionExecution, + ScheduleActionExecutionStartWorkflow, + ScheduleActionResult, + ScheduleActionStartWorkflow, + ScheduleAsyncIterator, + ScheduleBackfill, + ScheduleCalendarSpec, + ScheduleDescription, + ScheduleHandle, + ScheduleInfo, + ScheduleIntervalSpec, + ScheduleListAction, + ScheduleListActionStartWorkflow, + ScheduleListDescription, + ScheduleListInfo, + ScheduleListSchedule, + ScheduleListState, + ScheduleOverlapPolicy, + SchedulePolicy, + ScheduleRange, + ScheduleSpec, + ScheduleState, + ScheduleUpdate, + ScheduleUpdateInput, +) +from ._worker_versioning import ( + BuildIdOp, + BuildIdOpAddNewCompatible, + BuildIdOpAddNewDefault, + BuildIdOpMergeSets, + BuildIdOpPromoteBuildIdWithinSet, + BuildIdOpPromoteSetByBuildId, + BuildIdReachability, + BuildIdVersionSet, + TaskReachabilityType, + WorkerBuildIdVersionSets, + WorkerTaskReachability, +) +from ._workflow import ( + WithStartWorkflowOperation, + WorkflowExecution, + WorkflowExecutionAsyncIterator, + WorkflowExecutionCount, + WorkflowExecutionCountAggregationGroup, + WorkflowExecutionDescription, + WorkflowExecutionStatus, + WorkflowHandle, + WorkflowHistory, + WorkflowHistoryEventAsyncIterator, + WorkflowHistoryEventFilterType, + WorkflowUpdateHandle, + WorkflowUpdateStage, +) + +__all__ = [ + "Client", + "ClientConnectConfig", + "ClientConfig", + "WorkflowHistoryEventFilterType", + "WorkflowHandle", + "WithStartWorkflowOperation", + "WorkflowExecution", + "WorkflowExecutionDescription", + "WorkflowExecutionStatus", + "WorkflowExecutionCount", + "WorkflowExecutionCountAggregationGroup", + "WorkflowExecutionAsyncIterator", + "WorkflowHistory", + "WorkflowHistoryEventAsyncIterator", + "WorkflowUpdateHandle", + "WorkflowUpdateStage", + "ActivityExecutionAsyncIterator", + "ActivityExecution", + "ActivityExecutionDescription", + "ActivityExecutionStatus", + "PendingActivityState", + "ActivityExecutionCount", + "ActivityExecutionCountAggregationGroup", + "AsyncActivityIDReference", + "AsyncActivityHandle", + "ActivityHandle", + "NexusClient", + "NexusOperationExecution", + "NexusOperationExecutionAsyncIterator", + "NexusOperationExecutionCancellationInfo", + "NexusOperationExecutionCount", + "NexusOperationExecutionCountAggregationGroup", + "NexusOperationExecutionDescription", + "NexusOperationHandle", + "ScheduleHandle", + "ScheduleSpec", + "ScheduleRange", + "ScheduleCalendarSpec", + "ScheduleIntervalSpec", + "ScheduleAction", + "ScheduleActionStartWorkflow", + "ScheduleOverlapPolicy", + "ScheduleBackfill", + "SchedulePolicy", + "ScheduleState", + "Schedule", + "ScheduleDescription", + "ScheduleInfo", + "ScheduleActionExecution", + "ScheduleActionExecutionStartWorkflow", + "ScheduleActionResult", + "ScheduleUpdateInput", + "ScheduleUpdate", + "ScheduleListDescription", + "ScheduleListSchedule", + "ScheduleListAction", + "ScheduleListActionStartWorkflow", + "ScheduleListInfo", + "ScheduleListState", + "ScheduleAsyncIterator", + "WorkflowFailureError", + "WorkflowContinuedAsNewError", + "WorkflowQueryRejectedError", + "WorkflowQueryFailedError", + "WorkflowUpdateFailedError", + "RPCTimeoutOrCancelledError", + "WorkflowUpdateRPCTimeoutOrCancelledError", + "ActivityFailureError", + "AsyncActivityCancelledError", + "NexusOperationFailureError", + "ScheduleAlreadyRunningError", + "StartWorkflowInput", + "CancelWorkflowInput", + "DescribeWorkflowInput", + "FetchWorkflowHistoryEventsInput", + "ListWorkflowsInput", + "CountWorkflowsInput", + "QueryWorkflowInput", + "SignalWorkflowInput", + "TerminateWorkflowInput", + "StartActivityInput", + "CancelActivityInput", + "TerminateActivityInput", + "DescribeActivityInput", + "ListActivitiesInput", + "CountActivitiesInput", + "StartNexusOperationInput", + "DescribeNexusOperationInput", + "GetNexusOperationResultInput", + "CancelNexusOperationInput", + "TerminateNexusOperationInput", + "ListNexusOperationsInput", + "CountNexusOperationsInput", + "StartWorkflowUpdateInput", + "UpdateWithStartUpdateWorkflowInput", + "UpdateWithStartStartWorkflowInput", + "StartWorkflowUpdateWithStartInput", + "HeartbeatAsyncActivityInput", + "CompleteAsyncActivityInput", + "FailAsyncActivityInput", + "ReportCancellationAsyncActivityInput", + "CreateScheduleInput", + "ListSchedulesInput", + "BackfillScheduleInput", + "DeleteScheduleInput", + "DescribeScheduleInput", + "PauseScheduleInput", + "TriggerScheduleInput", + "UnpauseScheduleInput", + "UpdateScheduleInput", + "UpdateWorkerBuildIdCompatibilityInput", + "GetWorkerBuildIdCompatibilityInput", + "GetWorkerTaskReachabilityInput", + "Interceptor", + "OutboundInterceptor", + "WorkerBuildIdVersionSets", + "BuildIdVersionSet", + "BuildIdOp", + "BuildIdOpAddNewDefault", + "BuildIdOpAddNewCompatible", + "BuildIdOpPromoteSetByBuildId", + "BuildIdOpPromoteBuildIdWithinSet", + "BuildIdOpMergeSets", + "WorkerTaskReachability", + "BuildIdReachability", + "TaskReachabilityType", + "CloudOperationsClient", + "Plugin", + "Callback", + "_ClientImpl", + "_apply_headers", + "_decode_user_metadata", + "_encode_user_metadata", + "_fix_history_enum", + "_fix_history_failure", + "_history_from_json", + "_pascal_case_match", + # Re-export Temporal-owned names that old temporalio/client.py imported at + # module scope so explicit imports from temporalio.client keep working. + "ActivityCancellationDetails", + "ActivitySerializationContext", + "DataConverter", + "SerializationContext", + "StorageDriverActivityInfo", + "StorageDriverStoreContext", + "StorageDriverWorkflowInfo", + "WithSerializationContext", + "WorkflowSerializationContext", + "ConnectConfig", + "DnsLoadBalancingConfig", + "GrpcCompression", + "HttpConnectProxyConfig", + "KeepAliveConfig", + "PayloadLimitsConfig", + "RetryConfig", + "RPCError", + "RPCStatusCode", + "ServiceClient", + "TLSConfig", + "HeaderCodecBehavior", + "AnyType", + "CallableAsyncNoParam", + "CallableAsyncSingleParam", + "CallableSyncNoParam", + "CallableSyncSingleParam", + "LocalReturnType", + "MethodAsyncNoParam", + "MethodAsyncSingleParam", + "MethodSyncOrAsyncNoParam", + "MethodSyncOrAsyncSingleParam", + "MultiParamSpec", + "ParamType", + "ReturnType", + "SelfType", +] diff --git a/temporalio/client/_activity.py b/temporalio/client/_activity.py new file mode 100644 index 000000000..138d09dc7 --- /dev/null +++ b/temporalio/client/_activity.py @@ -0,0 +1,918 @@ +"""Client support for accessing Temporal.""" + +from __future__ import annotations + +import asyncio +import functools +import warnings +from collections.abc import ( + Mapping, + Sequence, +) +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +from enum import IntEnum +from typing import ( + TYPE_CHECKING, + Any, + Generic, + cast, +) + +from typing_extensions import Self + +import temporalio.api.activity.v1 +import temporalio.api.common.v1 +import temporalio.api.enums.v1 +import temporalio.api.workflowservice.v1 +import temporalio.common +import temporalio.converter +import temporalio.converter._search_attributes +from temporalio.converter import ( + ActivitySerializationContext, + DataConverter, + SerializationContext, + WithSerializationContext, +) +from temporalio.service import ( + RPCError, + RPCStatusCode, +) + +from ..types import ( + ReturnType, +) +from ._exceptions import ActivityFailureError +from ._interceptor import ( + CancelActivityInput, + CompleteAsyncActivityInput, + DescribeActivityInput, + FailAsyncActivityInput, + HeartbeatAsyncActivityInput, + ReportCancellationAsyncActivityInput, + TerminateActivityInput, +) + +if TYPE_CHECKING: + from ._client import Client + from ._interceptor import ListActivitiesInput + + +class ActivityExecutionAsyncIterator: + """Asynchronous iterator for activity execution values. + + You should typically use ``async for`` on this iterator and not call any of its methods. + + .. warning:: + This API is experimental. + """ + + def __init__( + self, + client: Client, + input: ListActivitiesInput, + ) -> None: + """Create an asynchronous iterator for the given input. + + Users should not create this directly, but rather use + :py:meth:`Client.list_activities`. + """ + self._client = client + self._input = input + self._next_page_token = input.next_page_token + self._current_page: Sequence[ActivityExecution] | None = None + self._current_page_index = 0 + self._limit = input.limit + self._yielded = 0 + + @property + def current_page_index(self) -> int: + """Index of the entry in the current page that will be returned from + the next :py:meth:`__anext__` call. + """ + return self._current_page_index + + @property + def current_page(self) -> Sequence[ActivityExecution] | None: + """Current page, if it has been fetched yet.""" + return self._current_page + + @property + def next_page_token(self) -> bytes | None: + """Token for the next page request if any.""" + return self._next_page_token + + async def fetch_next_page(self, *, page_size: int | None = None) -> None: + """Fetch the next page of results. + + Args: + page_size: Override the page size this iterator was originally + created with. + """ + page_size = page_size or self._input.page_size + if self._limit is not None and self._limit - self._yielded < page_size: + page_size = self._limit - self._yielded + + resp = await self._client.workflow_service.list_activity_executions( + temporalio.api.workflowservice.v1.ListActivityExecutionsRequest( + namespace=self._client.namespace, + page_size=page_size, + next_page_token=self._next_page_token or b"", + query=self._input.query or "", + ), + retry=True, + metadata=self._input.rpc_metadata, + timeout=self._input.rpc_timeout, + ) + + self._current_page = [ + ActivityExecution._from_raw_info(v, self._client.namespace) + for v in resp.executions + ] + self._current_page_index = 0 + self._next_page_token = resp.next_page_token or None + + def __aiter__(self) -> ActivityExecutionAsyncIterator: + """Return self as the iterator.""" + return self + + async def __anext__(self) -> ActivityExecution: + """Get the next execution on this iterator, fetching next page if + necessary. + """ + if self._limit is not None and self._yielded >= self._limit: + raise StopAsyncIteration + while True: + # No page? fetch and continue + if self._current_page is None: + await self.fetch_next_page() + continue + # No more left in page? + if self._current_page_index >= len(self._current_page): + # If there is a next page token, try to get another page and try + # again + if self._next_page_token is not None: + await self.fetch_next_page() + continue + # No more pages means we're done + raise StopAsyncIteration + # Get current, increment page index, and return + ret = self._current_page[self._current_page_index] + self._current_page_index += 1 + self._yielded += 1 + return ret + + +@dataclass(frozen=True) +class ActivityExecution: + """Info for an activity execution not started by a workflow, from list response. + + .. warning:: + This API is experimental. + """ + + activity_id: str + """Activity ID.""" + + activity_run_id: str | None + """Run ID of the activity.""" + + activity_type: str + """Type name of the activity.""" + + close_time: datetime | None + """Time the activity reached a terminal status, if closed.""" + + execution_duration: timedelta | None + """Duration from scheduled to close time, only populated if closed.""" + + namespace: str + """Namespace of the activity (copied from calling client).""" + + raw_info: ( + temporalio.api.activity.v1.ActivityExecutionListInfo + | temporalio.api.activity.v1.ActivityExecutionInfo + ) + """Underlying protobuf info.""" + + scheduled_time: datetime + """Time the activity was originally scheduled.""" + + state_transition_count: int | None + """Number of state transitions, if available.""" + + status: ActivityExecutionStatus + """Current status of the activity.""" + + task_queue: str + """Task queue the activity was scheduled on.""" + + typed_search_attributes: temporalio.common.TypedSearchAttributes + """Current set of search attributes if any.""" + + @classmethod + def _from_raw_info( + cls, info: temporalio.api.activity.v1.ActivityExecutionListInfo, namespace: str + ) -> Self: + """Create from raw proto activity list info.""" + return cls( + activity_id=info.activity_id, + activity_run_id=info.run_id or None, + activity_type=( + info.activity_type.name if info.HasField("activity_type") else "" + ), + close_time=( + info.close_time.ToDatetime().replace(tzinfo=timezone.utc) + if info.HasField("close_time") + else None + ), + execution_duration=( + info.execution_duration.ToTimedelta() + if info.HasField("execution_duration") + else None + ), + namespace=namespace, + raw_info=info, + scheduled_time=( + info.schedule_time.ToDatetime().replace(tzinfo=timezone.utc) + if info.HasField("schedule_time") + else datetime.min + ), + state_transition_count=( + info.state_transition_count if info.state_transition_count else None + ), + status=( + ActivityExecutionStatus(info.status) + if info.status + else ActivityExecutionStatus.UNSPECIFIED + ), + task_queue=info.task_queue, + typed_search_attributes=temporalio.converter.decode_typed_search_attributes( + info.search_attributes + ), + ) + + +@dataclass(frozen=True) +class ActivityExecutionDescription(ActivityExecution): + """Detailed information about an activity execution not started by a workflow. + + .. warning:: + This API is experimental. + """ + + attempt: int + """Current attempt number.""" + + canceled_reason: str | None + """Reason for cancellation, if cancel was requested.""" + + current_retry_interval: timedelta | None + """Time until the next retry, if applicable.""" + + eager_execution_requested: bool + """Whether eager execution was requested for this activity.""" + + expiration_time: datetime + """Scheduled time plus schedule_to_close_timeout.""" + + last_attempt_complete_time: datetime | None + """Time when the last attempt completed.""" + + last_failure: Exception | None + """Failure from the last failed attempt, if any.""" + + last_heartbeat_time: datetime | None + """Time of the last heartbeat.""" + + last_started_time: datetime | None + """Time the last attempt was started.""" + + last_worker_identity: str + """Identity of the last worker that processed the activity.""" + + next_attempt_schedule_time: datetime | None + """Time when the next attempt will be scheduled.""" + + paused: bool + """Whether the activity is paused.""" + + raw_heartbeat_details: Sequence[temporalio.api.common.v1.Payload] + """Details from the last heartbeat.""" + + retry_policy: temporalio.common.RetryPolicy | None + """Retry policy for the activity.""" + + run_state: PendingActivityState | None + """More detailed breakdown if status is RUNNING.""" + + long_poll_token: bytes | None + """Token for follow-on long-poll requests. None if the activity is complete.""" + + raw_callbacks: Sequence[temporalio.api.activity.v1.CallbackInfo] + """Underlying protobuf callbacks""" + + @classmethod + async def _from_execution_info( + cls, + info: temporalio.api.activity.v1.ActivityExecutionInfo, + long_poll_token: bytes | None, + namespace: str, + data_converter: temporalio.converter.DataConverter, + callbacks: Sequence[temporalio.api.activity.v1.CallbackInfo], + ) -> Self: + """Create from raw proto activity execution info.""" + # Decode heartbeat details if present + decoded_heartbeat_details: Sequence[temporalio.api.common.v1.Payload] = ( + info.heartbeat_details.payloads + ) + if decoded_heartbeat_details and data_converter.payload_codec: + decoded_heartbeat_details = await data_converter.payload_codec.decode( + decoded_heartbeat_details + ) + + return cls( + activity_id=info.activity_id, + activity_run_id=info.run_id or None, + activity_type=( + info.activity_type.name if info.HasField("activity_type") else "" + ), + attempt=info.attempt, + canceled_reason=info.canceled_reason or None, + close_time=( + info.close_time.ToDatetime(tzinfo=timezone.utc) + if info.HasField("close_time") + else None + ), + current_retry_interval=( + info.current_retry_interval.ToTimedelta() + if info.HasField("current_retry_interval") + else None + ), + eager_execution_requested=getattr(info, "eager_execution_requested", False), + execution_duration=( + info.execution_duration.ToTimedelta() + if info.HasField("execution_duration") + else None + ), + expiration_time=( + info.expiration_time.ToDatetime(tzinfo=timezone.utc) + if info.HasField("expiration_time") + else datetime.min + ), + last_attempt_complete_time=( + info.last_attempt_complete_time.ToDatetime(tzinfo=timezone.utc) + if info.HasField("last_attempt_complete_time") + else None + ), + last_failure=( + cast( + Exception | None, + await data_converter.decode_failure(info.last_failure), + ) + if info.HasField("last_failure") + else None + ), + last_heartbeat_time=( + info.last_heartbeat_time.ToDatetime(tzinfo=timezone.utc) + if info.HasField("last_heartbeat_time") + else None + ), + last_started_time=( + info.last_started_time.ToDatetime(tzinfo=timezone.utc) + if info.HasField("last_started_time") + else None + ), + last_worker_identity=info.last_worker_identity, + long_poll_token=long_poll_token or None, + namespace=namespace, + next_attempt_schedule_time=( + info.next_attempt_schedule_time.ToDatetime(tzinfo=timezone.utc) + if info.HasField("next_attempt_schedule_time") + else None + ), + paused=getattr(info, "paused", False), + raw_heartbeat_details=decoded_heartbeat_details, + raw_info=info, + retry_policy=temporalio.common.RetryPolicy.from_proto(info.retry_policy) + if info.HasField("retry_policy") + else None, + run_state=( + PendingActivityState(info.run_state) if info.run_state else None + ), + scheduled_time=(info.schedule_time.ToDatetime(tzinfo=timezone.utc)), + state_transition_count=( + info.state_transition_count if info.state_transition_count else None + ), + status=( + ActivityExecutionStatus(info.status) + if info.status + else ActivityExecutionStatus.UNSPECIFIED + ), + task_queue=info.task_queue, + typed_search_attributes=temporalio.converter.decode_typed_search_attributes( + info.search_attributes + ), + raw_callbacks=callbacks, + ) + + +class ActivityExecutionStatus(IntEnum): + """Status of an activity execution. + + .. warning:: + This API is experimental. + + See :py:class:`temporalio.api.enums.v1.ActivityExecutionStatus`. + """ + + UNSPECIFIED = int( + temporalio.api.enums.v1.ActivityExecutionStatus.ACTIVITY_EXECUTION_STATUS_UNSPECIFIED + ) + RUNNING = int( + temporalio.api.enums.v1.ActivityExecutionStatus.ACTIVITY_EXECUTION_STATUS_RUNNING + ) + COMPLETED = int( + temporalio.api.enums.v1.ActivityExecutionStatus.ACTIVITY_EXECUTION_STATUS_COMPLETED + ) + FAILED = int( + temporalio.api.enums.v1.ActivityExecutionStatus.ACTIVITY_EXECUTION_STATUS_FAILED + ) + CANCELED = int( + temporalio.api.enums.v1.ActivityExecutionStatus.ACTIVITY_EXECUTION_STATUS_CANCELED + ) + TERMINATED = int( + temporalio.api.enums.v1.ActivityExecutionStatus.ACTIVITY_EXECUTION_STATUS_TERMINATED + ) + TIMED_OUT = int( + temporalio.api.enums.v1.ActivityExecutionStatus.ACTIVITY_EXECUTION_STATUS_TIMED_OUT + ) + + +class PendingActivityState(IntEnum): + """Detailed state of an activity execution that is in ACTIVITY_EXECUTION_STATUS_RUNNING. + + .. warning:: + This API is experimental. + + See :py:class:`temporalio.api.enums.v1.PendingActivityState`. + """ + + UNSPECIFIED = int( + temporalio.api.enums.v1.PendingActivityState.PENDING_ACTIVITY_STATE_UNSPECIFIED + ) + SCHEDULED = int( + temporalio.api.enums.v1.PendingActivityState.PENDING_ACTIVITY_STATE_SCHEDULED + ) + STARTED = int( + temporalio.api.enums.v1.PendingActivityState.PENDING_ACTIVITY_STATE_STARTED + ) + CANCEL_REQUESTED = int( + temporalio.api.enums.v1.PendingActivityState.PENDING_ACTIVITY_STATE_CANCEL_REQUESTED + ) + PAUSED = int( + temporalio.api.enums.v1.PendingActivityState.PENDING_ACTIVITY_STATE_PAUSED + ) + PAUSE_REQUESTED = int( + temporalio.api.enums.v1.PendingActivityState.PENDING_ACTIVITY_STATE_PAUSE_REQUESTED + ) + + +@dataclass(frozen=True) +class ActivityExecutionCount: + """Representation of a count from a count activities call. + + .. warning:: + This API is experimental. + """ + + count: int + """Total count matching the filter, if any.""" + + groups: Sequence[ActivityExecutionCountAggregationGroup] + """Aggregation groups if requested.""" + + @staticmethod + def _from_raw( + resp: temporalio.api.workflowservice.v1.CountActivityExecutionsResponse, + ) -> ActivityExecutionCount: + """Create from raw proto response.""" + return ActivityExecutionCount( + count=resp.count, + groups=[ + ActivityExecutionCountAggregationGroup._from_raw(g) for g in resp.groups + ], + ) + + +@dataclass(frozen=True) +class ActivityExecutionCountAggregationGroup: + """A single aggregation group from a count activities call. + + .. warning:: + This API is experimental. + """ + + count: int + """Count for this group.""" + + group_values: Sequence[temporalio.common.SearchAttributeValue] + """Values that define this group.""" + + @staticmethod + def _from_raw( + raw: temporalio.api.workflowservice.v1.CountActivityExecutionsResponse.AggregationGroup, + ) -> ActivityExecutionCountAggregationGroup: + return ActivityExecutionCountAggregationGroup( + count=raw.count, + group_values=[ + temporalio.converter._search_attributes._decode_search_attribute_value( + v + ) + for v in raw.group_values + ], + ) + + +@dataclass(frozen=True) +class AsyncActivityIDReference: + """Reference to an async activity by its qualified ID.""" + + workflow_id: str | None + run_id: str | None + activity_id: str + + +class AsyncActivityHandle(WithSerializationContext): + """Handle representing an external activity for completion and heartbeat.""" + + def __init__( + self, + client: Client, + id_or_token: AsyncActivityIDReference | bytes, + data_converter_override: DataConverter | None = None, + ) -> None: + """Create an async activity handle.""" + self._client = client + self._id_or_token = id_or_token + self._data_converter_override = data_converter_override + + async def heartbeat( + self, + *details: Any, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Record a heartbeat for the activity. + + Args: + details: Details of the heartbeat. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + """ + await self._client._impl.heartbeat_async_activity( + HeartbeatAsyncActivityInput( + id_or_token=self._id_or_token, + details=details, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + data_converter_override=self._data_converter_override, + ), + ) + + async def complete( + self, + result: Any | None = temporalio.common._arg_unset, + *, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Complete the activity. + + Args: + result: Result of the activity if any. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + """ + await self._client._impl.complete_async_activity( + CompleteAsyncActivityInput( + id_or_token=self._id_or_token, + result=result, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + data_converter_override=self._data_converter_override, + ), + ) + + async def fail( + self, + error: Exception, + *, + last_heartbeat_details: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Fail the activity. + + Args: + error: Error for the activity. + last_heartbeat_details: Last heartbeat details for the activity. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + """ + await self._client._impl.fail_async_activity( + FailAsyncActivityInput( + id_or_token=self._id_or_token, + error=error, + last_heartbeat_details=last_heartbeat_details, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + data_converter_override=self._data_converter_override, + ), + ) + + async def report_cancellation( + self, + *details: Any, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Report the activity as cancelled. + + Args: + details: Cancellation details. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + """ + await self._client._impl.report_cancellation_async_activity( + ReportCancellationAsyncActivityInput( + id_or_token=self._id_or_token, + details=details, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + data_converter_override=self._data_converter_override, + ), + ) + + def with_context(self, context: SerializationContext) -> Self: + """Create a new AsyncActivityHandle with a different serialization context. + + Payloads received by the activity will be decoded and deserialized using a data converter + with :py:class:`ActivitySerializationContext` set as context. If you are using a custom data + converter that makes use of this context then you can use this method to supply matching + context data to the data converter used to serialize and encode the outbound payloads. + """ + data_converter = self._client.data_converter.with_context(context) + if data_converter is self._client.data_converter: + return self + cls = type(self) + if cls.__init__ is not AsyncActivityHandle.__init__: + raise TypeError( + "If you have subclassed AsyncActivityHandle and overridden the __init__ method " + "then you must override with_context to return an instance of your class." + ) + return cls( + self._client, + self._id_or_token, + data_converter, + ) + + +class ActivityHandle(Generic[ReturnType]): + """Handle representing an activity execution not started by a workflow. + + .. warning:: + This API is experimental. + """ + + def __init__( + self, + client: Client, + id: str, + *, + run_id: str | None = None, + result_type: type | None = None, + ) -> None: + """Create activity handle.""" + self._client = client + self._id = id + self._run_id = run_id + self._result_type = result_type + self._known_outcome: ( + temporalio.api.activity.v1.ActivityExecutionOutcome | None + ) = None + + @functools.cached_property + def _data_converter(self) -> temporalio.converter.DataConverter: + return self._client.data_converter.with_context( + ActivitySerializationContext( + namespace=self._client.namespace, + activity_id=self._id, + activity_type=None, + activity_task_queue=None, + is_local=False, + workflow_id=None, + workflow_type=None, + ) + ) + + @property + def id(self) -> str: + """ID of the activity.""" + return self._id + + @property + def run_id(self) -> str | None: + """Run ID of the activity.""" + return self._run_id + + async def result( + self, + *, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: + """Wait for result of the activity. + + .. warning:: + This API is experimental. + + The result may already be known if this method has been called before, + in which case no network call is made. Otherwise the result will be + polled for until it is available. + + Args: + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for each RPC call. Note: + this is the timeout for each RPC call while polling, not a + timeout for the function as a whole. If an individual RPC + times out, it will be retried until the result is available. + + Returns: + The result of the activity. + + Raises: + ActivityFailureError: If the activity completed with a failure. + RPCError: Activity result could not be fetched for some reason. + """ + await self._poll_until_outcome( + rpc_metadata=rpc_metadata, rpc_timeout=rpc_timeout + ) + + # Convert outcome to failure or value + assert self._known_outcome + if self._known_outcome.HasField("failure"): + raise ActivityFailureError( + cause=await self._data_converter.decode_failure( + self._known_outcome.failure + ), + ) + if not self._known_outcome.result.payloads: + return None # type: ignore + type_hints = [self._result_type] if self._result_type else None + results = await self._data_converter.decode( + self._known_outcome.result.payloads, type_hints + ) + if not results: + return None # type: ignore + elif len(results) > 1: + warnings.warn(f"Expected single activity result, got {len(results)}") + return results[0] + + async def _poll_until_outcome( + self, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Poll for activity result until it's available.""" + if self._known_outcome: + return + + req = temporalio.api.workflowservice.v1.PollActivityExecutionRequest( + namespace=self._client.namespace, + activity_id=self._id, + run_id=self._run_id or "", + ) + + # Continue polling as long as we have no outcome + while True: + try: + res = await self._client.workflow_service.poll_activity_execution( + req, + retry=True, + metadata=rpc_metadata, + timeout=rpc_timeout, + ) + if res.HasField("outcome"): + self._known_outcome = res.outcome + return + except RPCError as err: + if err.status == RPCStatusCode.DEADLINE_EXCEEDED: + # Deadline exceeded is expected with long polling; retry + continue + elif err.status == RPCStatusCode.CANCELLED: + raise asyncio.CancelledError() from err + else: + raise + except asyncio.CancelledError: + raise + + async def cancel( + self, + *, + reason: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Request cancellation of the activity. + + .. warning:: + This API is experimental. + + Requesting cancellation of an activity does not automatically transition the activity to + canceled status. If the activity is heartbeating, a :py:class:`exceptions.CancelledError` + exception will be raised when receiving the heartbeat response; if the activity allows this + exception to bubble out, the activity will transition to canceled status. If the activity it + is not heartbeating, this method will have no effect on activity status. + + Args: + reason: Reason for the cancellation. Recorded and available via describe. + rpc_metadata: Headers used on the RPC call. + rpc_timeout: Optional RPC deadline to set for the RPC call. + """ + await self._client._impl.cancel_activity( + CancelActivityInput( + activity_id=self._id, + activity_run_id=self._run_id, + reason=reason, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) + + async def terminate( + self, + *, + reason: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Terminate the activity execution immediately. + + .. warning:: + This API is experimental. + + Termination does not reach the worker and the activity code cannot react to it. + A terminated activity may have a running attempt and will be requested to be + canceled by the server when it heartbeats. + + Args: + reason: Reason for the termination. + rpc_metadata: Headers used on the RPC call. + rpc_timeout: Optional RPC deadline to set for the RPC call. + """ + await self._client._impl.terminate_activity( + TerminateActivityInput( + activity_id=self._id, + activity_run_id=self._run_id, + reason=reason, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) + + async def describe( + self, + *, + long_poll_token: bytes | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityExecutionDescription: + """Describe the activity execution. + + .. warning:: + This API is experimental. + + Args: + long_poll_token: Token from a previous describe response. If provided, + the request will long-poll until the activity state changes. + rpc_metadata: Headers used on the RPC call. + rpc_timeout: Optional RPC deadline to set for the RPC call. + + Returns: + Activity execution description. + """ + return await self._client._impl.describe_activity( + DescribeActivityInput( + activity_id=self._id, + activity_run_id=self._run_id, + long_poll_token=long_poll_token, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) diff --git a/temporalio/client/_callback.py b/temporalio/client/_callback.py new file mode 100644 index 000000000..45aa89030 --- /dev/null +++ b/temporalio/client/_callback.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +import temporalio.nexus + +Callback = temporalio.nexus.NexusCallback diff --git a/temporalio/client/_client.py b/temporalio/client/_client.py new file mode 100644 index 000000000..5acdfe476 --- /dev/null +++ b/temporalio/client/_client.py @@ -0,0 +1,3072 @@ +"""Client support for accessing Temporal.""" + +from __future__ import annotations + +from collections.abc import ( + Awaitable, + Callable, + Mapping, + Sequence, +) +from datetime import timedelta +from typing import ( + TYPE_CHECKING, + Any, + Concatenate, + cast, + overload, +) + +import nexusrpc +from nexusrpc import OutputT +from typing_extensions import Required, Self, TypedDict + +import temporalio.activity +import temporalio.api.common.v1 +import temporalio.api.workflowservice.v1 +import temporalio.common +import temporalio.converter +import temporalio.runtime +import temporalio.service +import temporalio.workflow +from temporalio.service import ( + ConnectConfig, + DnsLoadBalancingConfig, + GrpcCompression, + HttpConnectProxyConfig, + KeepAliveConfig, + PayloadLimitsConfig, + RetryConfig, + ServiceClient, + TLSConfig, +) + +from ..common import HeaderCodecBehavior +from ..types import ( + CallableAsyncNoParam, + CallableAsyncSingleParam, + CallableSyncNoParam, + CallableSyncSingleParam, + LocalReturnType, + MethodAsyncNoParam, + MethodAsyncSingleParam, + MultiParamSpec, + NexusServiceType, + ParamType, + ReturnType, + SelfType, +) +from ._activity import ( + ActivityExecutionAsyncIterator, + ActivityExecutionCount, + ActivityHandle, + AsyncActivityHandle, + AsyncActivityIDReference, +) +from ._callback import Callback +from ._impl import _ClientImpl +from ._interceptor import ( + CountActivitiesInput, + CountNexusOperationsInput, + CountWorkflowsInput, + CreateScheduleInput, + GetWorkerBuildIdCompatibilityInput, + GetWorkerTaskReachabilityInput, + ListActivitiesInput, + ListNexusOperationsInput, + ListSchedulesInput, + ListWorkflowsInput, + OutboundInterceptor, + StartActivityInput, + StartWorkflowInput, + StartWorkflowUpdateWithStartInput, + UpdateWithStartUpdateWorkflowInput, + UpdateWorkerBuildIdCompatibilityInput, +) +from ._nexus import ( + NexusClient, + NexusOperationExecutionAsyncIterator, + NexusOperationExecutionCount, + NexusOperationHandle, + _NexusClient, +) +from ._schedule import ( + Schedule, + ScheduleAsyncIterator, + ScheduleBackfill, + ScheduleHandle, +) +from ._worker_versioning import ( + BuildIdOp, + TaskReachabilityType, + WorkerBuildIdVersionSets, + WorkerTaskReachability, +) +from ._workflow import ( + WithStartWorkflowOperation, + WorkflowExecutionAsyncIterator, + WorkflowExecutionCount, + WorkflowHandle, + WorkflowUpdateHandle, + WorkflowUpdateStage, +) + +if TYPE_CHECKING: + from ._interceptor import Interceptor + from ._plugin import Plugin + + +class Client: + """Client for accessing Temporal. + + Most users will use :py:meth:`connect` to create a client. The + :py:attr:`service` property provides access to a raw gRPC client. To create + another client, like for a different namespace, :py:func:`Client` may be + directly instantiated with a :py:attr:`service` of another. + + Clients are not thread-safe and should only be used in the event loop they + are first connected in. If a client needs to be used from another thread + than where it was created, make sure the event loop where it was created is + captured, and then call :py:func:`asyncio.run_coroutine_threadsafe` with the + client call and that event loop. + + Clients do not work across forks since runtimes do not work across forks. + """ + + @classmethod + async def connect( + cls, + target_host: str, + *, + namespace: str = "default", + api_key: str | None = None, + data_converter: temporalio.converter.DataConverter = temporalio.converter.DataConverter.default, + plugins: Sequence[Plugin] = [], + interceptors: Sequence[Interceptor] = [], + default_workflow_query_reject_condition: None + | (temporalio.common.QueryRejectCondition) = None, + tls: bool | TLSConfig | None = None, + retry_config: RetryConfig | None = None, + keep_alive_config: KeepAliveConfig | None = KeepAliveConfig.default, + rpc_metadata: Mapping[str, str | bytes] = {}, + identity: str | None = None, + lazy: bool = False, + runtime: temporalio.runtime.Runtime | None = None, + http_connect_proxy_config: HttpConnectProxyConfig | None = None, + dns_load_balancing_config: DnsLoadBalancingConfig | None = None, + grpc_compression: GrpcCompression = GrpcCompression.GZIP, + payload_limits: PayloadLimitsConfig = PayloadLimitsConfig(), + header_codec_behavior: HeaderCodecBehavior = HeaderCodecBehavior.NO_CODEC, + ) -> Self: + """Connect to a Temporal server. + + Args: + target_host: ``host:port`` for the Temporal server. For local + development, this is often "localhost:7233". + namespace: Namespace to use for client calls. + api_key: API key for Temporal. This becomes the "Authorization" + HTTP header with "Bearer " prepended. This is only set if RPC + metadata doesn't already have an "authorization" key. + data_converter: Data converter to use for all data conversions + to/from payloads. + plugins: Set of plugins that are chained together to allow + intercepting and modifying client creation and service connection. + The earlier plugins wrap the later ones. + + Any plugins that also implement + :py:class:`temporalio.worker.Plugin` will be used as worker + plugins too so they should not be given when creating a + worker. + interceptors: Set of interceptors that are chained together to allow + intercepting of client calls. The earlier interceptors wrap the + later ones. + + Any interceptors that also implement + :py:class:`temporalio.worker.Interceptor` will be used as worker + interceptors too so they should not be given when creating a + worker. + default_workflow_query_reject_condition: The default rejection + condition for workflow queries if not set during query. See + :py:meth:`WorkflowHandle.query` for details on the rejection + condition. + tls: If ``None``, the default, TLS will be enabled automatically + when ``api_key`` is provided, otherwise TLS is disabled. If + ``False``, do not use TLS. If ``True``, use system default TLS + configuration. If TLS configuration present, that TLS + configuration will be used. + retry_config: Retry configuration for direct service calls (when + opted in) or all high-level calls made by this client (which all + opt-in to retries by default). If unset, a default retry + configuration is used. + keep_alive_config: Keep-alive configuration for the client + connection. Default is to check every 30s and kill the + connection if a response doesn't come back in 15s. Can be set to + ``None`` to disable. + rpc_metadata: Headers to use for all calls to the server. Keys here + can be overriden by per-call RPC metadata keys. + identity: Identity for this client. If unset, a default is created + based on the version of the SDK. + lazy: If true, the client will not connect until the first call is + attempted or a worker is created with it. Lazy clients cannot be + used for workers. + runtime: The runtime for this client, or the default if unset. + http_connect_proxy_config: Configuration for HTTP CONNECT proxy. + dns_load_balancing_config: DNS load balancing configuration for the + client connection. Default is to re-resolve DNS every 30s. Can + be set to ``None`` to disable. Silently disabled when + ``http_connect_proxy_config`` is set, since the two are mutually + exclusive. + grpc_compression: Transport-level gRPC compression for the client + connection. Default is gzip. Set to + :py:attr:`GrpcCompression.NONE` to disable compression. + payload_limits: Warning thresholds for outbound payload/memo sizes. Over-threshold + fields are logged but still sent. Set a threshold to 0 to disable it. + header_codec_behavior: Encoding behavior for headers sent by the client. + """ + connect_config = temporalio.service.ConnectConfig( + target_host=target_host, + api_key=api_key, + tls=tls, + retry_config=retry_config, + keep_alive_config=keep_alive_config, + rpc_metadata=rpc_metadata, + identity=identity or "", + lazy=lazy, + runtime=runtime, + http_connect_proxy_config=http_connect_proxy_config, + dns_load_balancing_config=dns_load_balancing_config, + grpc_compression=grpc_compression, + payload_limits=payload_limits, + ) + + def make_lambda( + plugin: Plugin, next: Callable[[ConnectConfig], Awaitable[ServiceClient]] + ): + return lambda config: plugin.connect_service_client(config, next) + + next_function = ServiceClient.connect + for plugin in reversed(plugins): + next_function = make_lambda(plugin, next_function) + + service_client = await next_function(connect_config) + + return cls( + service_client, + namespace=namespace, + data_converter=data_converter, + interceptors=interceptors, + default_workflow_query_reject_condition=default_workflow_query_reject_condition, + header_codec_behavior=header_codec_behavior, + plugins=plugins, + ) + + def __init__( + self, + service_client: temporalio.service.ServiceClient, + *, + namespace: str = "default", + data_converter: temporalio.converter.DataConverter = temporalio.converter.DataConverter.default, + plugins: Sequence[Plugin] = [], + interceptors: Sequence[Interceptor] = [], + default_workflow_query_reject_condition: None + | (temporalio.common.QueryRejectCondition) = None, + header_codec_behavior: HeaderCodecBehavior = HeaderCodecBehavior.NO_CODEC, + ): + """Create a Temporal client from a service client. + + See :py:meth:`connect` for details on the parameters. + """ + # Store the config for tracking + config = ClientConfig( + service_client=service_client, + namespace=namespace, + data_converter=data_converter, + plugins=plugins, + interceptors=interceptors, + default_workflow_query_reject_condition=default_workflow_query_reject_condition, + header_codec_behavior=header_codec_behavior, + ) + self._initial_config = config.copy() + + for plugin in plugins: + config = plugin.configure_client(config) + + self._init_from_config(config) + + def _init_from_config(self, config: ClientConfig): + self._config = config + + # Iterate over interceptors in reverse building the impl + self._impl: OutboundInterceptor = _ClientImpl(self) + for interceptor in reversed(list(self._config["interceptors"])): + self._impl = interceptor.intercept_client(self._impl) + + def config(self, *, active_config: bool = False) -> ClientConfig: + """Config, as a dictionary, used to create this client. + + Args: + active_config: If true, return the modified configuration in use rather than the initial one + provided to the client. + + This makes a shallow copy of the config each call. + """ + config = self._config.copy() if active_config else self._initial_config.copy() + config["interceptors"] = list(config["interceptors"]) + return config + + @property + def service_client(self) -> temporalio.service.ServiceClient: + """Raw gRPC service client.""" + return self._config["service_client"] + + @property + def workflow_service(self) -> temporalio.service.WorkflowService: + """Raw gRPC workflow service client.""" + return self._config["service_client"].workflow_service + + @property + def operator_service(self) -> temporalio.service.OperatorService: + """Raw gRPC operator service client.""" + return self._config["service_client"].operator_service + + @property + def test_service(self) -> temporalio.service.TestService: + """Raw gRPC test service client.""" + return self._config["service_client"].test_service + + @property + def namespace(self) -> str: + """Namespace used in calls by this client.""" + return self._config["namespace"] + + @property + def identity(self) -> str: + """Identity used in calls by this client.""" + return self._config["service_client"].config.identity + + @property + def data_converter(self) -> temporalio.converter.DataConverter: + """Data converter used by this client.""" + return self._config["data_converter"] + + @property + def rpc_metadata(self) -> Mapping[str, str | bytes]: + """Headers for every call made by this client. + + Do not use mutate this mapping. Rather, set this property with an + entirely new mapping to change the headers. + """ + return self.service_client.config.rpc_metadata + + @rpc_metadata.setter + def rpc_metadata(self, value: Mapping[str, str | bytes]) -> None: + """Update the headers for this client. + + Do not mutate this mapping after set. Rather, set an entirely new + mapping if changes are needed. + + Raises: + TypeError: the key/value pair is not a valid gRPC ASCII or binary metadata. + All binary metadata must be supplied as bytes, and the key must end in '-bin'. + + .. warning:: + Attempting to set an invalid binary RPC metadata value may leave the client + in an inconsistent state (as well as raise a :py:class:`TypeError`). + """ + # Update config and perform update + # This may raise if the metadata is invalid: + self.service_client.update_rpc_metadata(value) + self.service_client.config.rpc_metadata = value + + @property + def api_key(self) -> str | None: + """API key for every call made by this client.""" + return self.service_client.config.api_key + + @api_key.setter + def api_key(self, value: str | None) -> None: + """Update the API key for this client. + + This is only set if RPCmetadata doesn't already have an "authorization" + key. + """ + # Update config and perform update + self.service_client.config.api_key = value + self.service_client.update_api_key(value) + + # Overload for no-param workflow + @overload + async def start_workflow( + self, + workflow: MethodAsyncNoParam[SelfType, ReturnType], + *, + id: str, + task_queue: str, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + request_eager_start: bool = False, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> WorkflowHandle[SelfType, ReturnType]: ... + + # Overload for single-param workflow + @overload + async def start_workflow( + self, + workflow: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], + arg: ParamType, + *, + id: str, + task_queue: str, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + request_eager_start: bool = False, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> WorkflowHandle[SelfType, ReturnType]: ... + + # Overload for multi-param workflow + @overload + async def start_workflow( + self, + workflow: Callable[ + Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType] + ], + *, + args: Sequence[Any], + id: str, + task_queue: str, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + request_eager_start: bool = False, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> WorkflowHandle[SelfType, ReturnType]: ... + + # Overload for string-name workflow + @overload + async def start_workflow( + self, + workflow: str, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + result_type: type | None = None, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + request_eager_start: bool = False, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> WorkflowHandle[Any, Any]: ... + + async def start_workflow( + self, + workflow: str | Callable[..., Awaitable[Any]], + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + result_type: type | None = None, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + request_eager_start: bool = False, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + # The following options should not be considered part of the public API. They + # are deliberately not exposed in overloads, and are not subject to any + # backwards compatibility guarantees. + callbacks: Sequence[Callback] = [], + links: Sequence[temporalio.api.common.v1.Link] = [], + request_id: str | None = None, + stack_level: int = 2, + ) -> WorkflowHandle[Any, Any]: + """Start a workflow and return its handle. + + Args: + workflow: String name or class method decorated with + ``@workflow.run`` for the workflow to start. + arg: Single argument to the workflow. + args: Multiple arguments to the workflow. Cannot be set if arg is. + id: Unique identifier for the workflow execution. + task_queue: Task queue to run the workflow on. + result_type: For string workflows, this can set the specific result + type hint to deserialize into. + execution_timeout: Total workflow execution timeout including + retries and continue as new. + run_timeout: Timeout of a single workflow run. + task_timeout: Timeout of a single workflow task. + id_conflict_policy: Behavior when a workflow is currently running with the same ID. + Default is UNSPECIFIED, which effectively means fail the start attempt. + Set to USE_EXISTING for idempotent deduplication on workflow ID. + Cannot be set if ``id_reuse_policy`` is set to TERMINATE_IF_RUNNING. + id_reuse_policy: Behavior when a closed workflow with the same ID exists. + Default is ALLOW_DUPLICATE. + retry_policy: Retry policy for the workflow. + cron_schedule: See https://docs.temporal.io/docs/content/what-is-a-temporal-cron-job/ + memo: Memo for the workflow. + search_attributes: Search attributes for the workflow. The + dictionary form of this is deprecated, use + :py:class:`temporalio.common.TypedSearchAttributes`. + static_summary: A single-line fixed summary for this workflow execution that may appear + in the UI/CLI. This can be in single-line Temporal markdown format. + static_details: General fixed details for this workflow execution that may appear in + UI/CLI. This can be in Temporal markdown format and can span multiple lines. This is + a fixed value on the workflow that cannot be updated. For details that can be + updated, use :py:meth:`temporalio.workflow.get_current_details` within the workflow. + start_delay: Amount of time to wait before starting the workflow. + This does not work with ``cron_schedule``. + start_signal: If present, this signal is sent as signal-with-start + instead of traditional workflow start. + start_signal_args: Arguments for start_signal if start_signal + present. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + request_eager_start: Potentially reduce the latency to start this workflow by + encouraging the server to start it on a local worker running with + this same client. + priority: Priority of the workflow execution. + versioning_override: Overrides the versioning behavior for this workflow. + + Returns: + A workflow handle to the started workflow. + + Raises: + temporalio.exceptions.WorkflowAlreadyStartedError: Workflow has + already been started. + RPCError: Workflow could not be started for some other reason. + """ + temporalio.common._warn_on_deprecated_search_attributes( + search_attributes, stack_level=stack_level + ) + name, result_type_from_type_hint = ( + temporalio.workflow._Definition.get_name_and_result_type(workflow) + ) + return await self._impl.start_workflow( + StartWorkflowInput( + workflow=name, + args=temporalio.common._arg_or_args(arg, args), + id=id, + task_queue=task_queue, + execution_timeout=execution_timeout, + run_timeout=run_timeout, + task_timeout=task_timeout, + id_reuse_policy=id_reuse_policy, + id_conflict_policy=id_conflict_policy, + retry_policy=retry_policy, + cron_schedule=cron_schedule, + memo=memo, + search_attributes=search_attributes, + start_delay=start_delay, + versioning_override=versioning_override, + headers={}, + static_summary=static_summary, + static_details=static_details, + start_signal=start_signal, + start_signal_args=start_signal_args, + ret_type=result_type or result_type_from_type_hint, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + request_eager_start=request_eager_start, + priority=priority, + callbacks=callbacks, + links=links, + request_id=request_id, + ) + ) + + # Overload for no-param workflow + @overload + async def execute_workflow( + self, + workflow: MethodAsyncNoParam[SelfType, ReturnType], + *, + id: str, + task_queue: str, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + request_eager_start: bool = False, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> ReturnType: ... + + # Overload for single-param workflow + @overload + async def execute_workflow( + self, + workflow: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], + arg: ParamType, + *, + id: str, + task_queue: str, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + request_eager_start: bool = False, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> ReturnType: ... + + # Overload for multi-param workflow + @overload + async def execute_workflow( + self, + workflow: Callable[ + Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType] + ], + *, + args: Sequence[Any], + id: str, + task_queue: str, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + request_eager_start: bool = False, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> ReturnType: ... + + # Overload for string-name workflow + @overload + async def execute_workflow( + self, + workflow: str, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + result_type: type | None = None, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + request_eager_start: bool = False, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> Any: ... + + async def execute_workflow( + self, + workflow: str | Callable[..., Awaitable[Any]], + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + result_type: type | None = None, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + request_eager_start: bool = False, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> Any: + """Start a workflow and wait for completion. + + This is a shortcut for :py:meth:`start_workflow` + + :py:meth:`WorkflowHandle.result`. + """ + return await ( + # We have to tell MyPy to ignore errors here because we want to call + # the non-@overload form of this and MyPy does not support that + await self.start_workflow( # type: ignore + workflow, # type: ignore[arg-type] + arg, + args=args, + task_queue=task_queue, + result_type=result_type, + id=id, + execution_timeout=execution_timeout, + run_timeout=run_timeout, + task_timeout=task_timeout, + id_reuse_policy=id_reuse_policy, + id_conflict_policy=id_conflict_policy, + retry_policy=retry_policy, + cron_schedule=cron_schedule, + memo=memo, + search_attributes=search_attributes, + static_summary=static_summary, + static_details=static_details, + start_delay=start_delay, + start_signal=start_signal, + start_signal_args=start_signal_args, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + request_eager_start=request_eager_start, + priority=priority, + versioning_override=versioning_override, + stack_level=3, + ) + ).result() + + def get_workflow_handle( + self, + workflow_id: str, + *, + run_id: str | None = None, + first_execution_run_id: str | None = None, + result_type: type | None = None, + ) -> WorkflowHandle[Any, Any]: + """Get a workflow handle to an existing workflow by its ID. + + Args: + workflow_id: Workflow ID to get a handle to. + run_id: Run ID that will be used for all calls. + first_execution_run_id: First execution run ID used for cancellation + and termination. + result_type: The result type to deserialize into if known. + + Returns: + The workflow handle. + """ + return WorkflowHandle( + self, + workflow_id, + run_id=run_id, + result_run_id=run_id, + first_execution_run_id=first_execution_run_id, + result_type=result_type, + ) + + def get_workflow_handle_for( + self, + workflow: ( + MethodAsyncNoParam[SelfType, ReturnType] + | MethodAsyncSingleParam[SelfType, Any, ReturnType] + ), + workflow_id: str, + *, + run_id: str | None = None, + first_execution_run_id: str | None = None, + ) -> WorkflowHandle[SelfType, ReturnType]: + """Get a typed workflow handle to an existing workflow by its ID. + + This is the same as :py:meth:`get_workflow_handle` but typed. + + Args: + workflow: The workflow run method to use for typing the handle. + workflow_id: Workflow ID to get a handle to. + run_id: Run ID that will be used for all calls. + first_execution_run_id: First execution run ID used for cancellation + and termination. + + Returns: + The workflow handle. + """ + defn = temporalio.workflow._Definition.must_from_run_fn(workflow) + return self.get_workflow_handle( + workflow_id, + run_id=run_id, + first_execution_run_id=first_execution_run_id, + result_type=defn.ret_type, + ) + + # Overload for no-param update + @overload + async def execute_update_with_start_workflow( + self, + update: temporalio.workflow.UpdateMethodMultiParam[[SelfType], LocalReturnType], + *, + start_workflow_operation: WithStartWorkflowOperation[SelfType, Any], + id: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> LocalReturnType: ... + + # Overload for single-param update + @overload + async def execute_update_with_start_workflow( + self, + update: temporalio.workflow.UpdateMethodMultiParam[ + [SelfType, ParamType], LocalReturnType + ], + arg: ParamType, + *, + start_workflow_operation: WithStartWorkflowOperation[SelfType, Any], + id: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> LocalReturnType: ... + + # Overload for multi-param update + @overload + async def execute_update_with_start_workflow( + self, + update: temporalio.workflow.UpdateMethodMultiParam[ + MultiParamSpec, LocalReturnType + ], + *, + args: MultiParamSpec.args, # type: ignore + start_workflow_operation: WithStartWorkflowOperation[SelfType, Any], + id: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> LocalReturnType: ... + + # Overload for string-name update + @overload + async def execute_update_with_start_workflow( + self, + update: str, + arg: Any = temporalio.common._arg_unset, + *, + start_workflow_operation: WithStartWorkflowOperation[Any, Any], + args: Sequence[Any] = [], + id: str | None = None, + result_type: type | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> Any: ... + + async def execute_update_with_start_workflow( + self, + update: str | Callable, + arg: Any = temporalio.common._arg_unset, + *, + start_workflow_operation: WithStartWorkflowOperation[Any, Any], + args: Sequence[Any] = [], + id: str | None = None, + result_type: type | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> Any: + """Send an update-with-start request and wait for the update to complete. + + A WorkflowIDConflictPolicy must be set in the start_workflow_operation. If the + specified workflow execution is not running, a new workflow execution is started + and the update is sent in the first workflow task. Alternatively if the specified + workflow execution is running then, if the WorkflowIDConflictPolicy is + USE_EXISTING, the update is issued against the specified workflow, and if the + WorkflowIDConflictPolicy is FAIL, an error is returned. This call will block until + the update has completed, and return the update result. Note that this means that + the call will not return successfully until the update has been delivered to a + worker. + + Args: + update: Update function or name on the workflow. arg: Single argument to the + update. + args: Multiple arguments to the update. Cannot be set if arg is. + start_workflow_operation: a WithStartWorkflowOperation definining the + WorkflowIDConflictPolicy and how to start the workflow in the event that a + workflow is started. + id: ID of the update. If not set, the default is a new UUID. + result_type: For string updates, this can set the specific result + type hint to deserialize into. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + + Raises: + WorkflowUpdateFailedError: If the update failed. + WorkflowUpdateRPCTimeoutOrCancelledError: This update call timed out + or was cancelled. This doesn't mean the update itself was timed out or + cancelled. + + RPCError: There was some issue starting the workflow or sending the update to + the workflow. + """ + handle = await self._start_update_with_start( + update, + arg, + args=args, + start_workflow_operation=start_workflow_operation, + wait_for_stage=WorkflowUpdateStage.COMPLETED, + id=id, + result_type=result_type, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + return await handle.result() + + # Overload for no-param start update + @overload + async def start_update_with_start_workflow( + self, + update: temporalio.workflow.UpdateMethodMultiParam[[SelfType], LocalReturnType], + *, + start_workflow_operation: WithStartWorkflowOperation[SelfType, Any], + wait_for_stage: WorkflowUpdateStage, + id: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowUpdateHandle[LocalReturnType]: ... + + # Overload for single-param start update + @overload + async def start_update_with_start_workflow( + self, + update: temporalio.workflow.UpdateMethodMultiParam[ + [SelfType, ParamType], LocalReturnType + ], + arg: ParamType, + *, + start_workflow_operation: WithStartWorkflowOperation[SelfType, Any], + wait_for_stage: WorkflowUpdateStage, + id: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowUpdateHandle[LocalReturnType]: ... + + # Overload for multi-param start update + @overload + async def start_update_with_start_workflow( + self, + update: temporalio.workflow.UpdateMethodMultiParam[ + MultiParamSpec, LocalReturnType + ], + *, + args: MultiParamSpec.args, # type: ignore + start_workflow_operation: WithStartWorkflowOperation[SelfType, Any], + wait_for_stage: WorkflowUpdateStage, + id: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowUpdateHandle[LocalReturnType]: ... + + # Overload for string-name start update + @overload + async def start_update_with_start_workflow( + self, + update: str, + arg: Any = temporalio.common._arg_unset, + *, + start_workflow_operation: WithStartWorkflowOperation[Any, Any], + wait_for_stage: WorkflowUpdateStage, + args: Sequence[Any] = [], + id: str | None = None, + result_type: type | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowUpdateHandle[Any]: ... + + async def start_update_with_start_workflow( + self, + update: str | Callable, + arg: Any = temporalio.common._arg_unset, + *, + start_workflow_operation: WithStartWorkflowOperation[Any, Any], + wait_for_stage: WorkflowUpdateStage, + args: Sequence[Any] = [], + id: str | None = None, + result_type: type | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowUpdateHandle[Any]: + """Send an update-with-start request and wait for it to be accepted. + + A WorkflowIDConflictPolicy must be set in the start_workflow_operation. If the + specified workflow execution is not running, a new workflow execution is started + and the update is sent in the first workflow task. Alternatively if the specified + workflow execution is running then, if the WorkflowIDConflictPolicy is + USE_EXISTING, the update is issued against the specified workflow, and if the + WorkflowIDConflictPolicy is FAIL, an error is returned. This call will block until + the update has been accepted, and return a WorkflowUpdateHandle. Note that this + means that the call will not return successfully until the update has been + delivered to a worker. + + Args: + update: Update function or name on the workflow. arg: Single argument to the + update. + args: Multiple arguments to the update. Cannot be set if arg is. + start_workflow_operation: a WithStartWorkflowOperation definining the + WorkflowIDConflictPolicy and how to start the workflow in the event that a + workflow is started. + wait_for_stage: Required stage to wait until returning: either ACCEPTED or + COMPLETED. ADMITTED is not currently supported. See + https://docs.temporal.io/workflows#update for more details. + id: ID of the update. If not set, the default is a new UUID. + result_type: For string updates, this can set the specific result + type hint to deserialize into. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + + Raises: + WorkflowUpdateFailedError: If the update failed. + WorkflowUpdateRPCTimeoutOrCancelledError: This update call timed out + or was cancelled. This doesn't mean the update itself was timed out or + cancelled. + + RPCError: There was some issue starting the workflow or sending the update to + the workflow. + """ + return await self._start_update_with_start( + update, + arg, + wait_for_stage=wait_for_stage, + args=args, + id=id, + result_type=result_type, + start_workflow_operation=start_workflow_operation, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + + async def _start_update_with_start( + self, + update: str | Callable, + arg: Any = temporalio.common._arg_unset, + *, + wait_for_stage: WorkflowUpdateStage, + args: Sequence[Any] = [], + id: str | None = None, + result_type: type | None = None, + start_workflow_operation: WithStartWorkflowOperation[SelfType, ReturnType], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowUpdateHandle[Any]: + if wait_for_stage == WorkflowUpdateStage.ADMITTED: + raise ValueError("ADMITTED wait stage not supported") + + if start_workflow_operation._used: + raise RuntimeError("WithStartWorkflowOperation cannot be reused") + start_workflow_operation._used = True + + update_name, result_type_from_type_hint = ( + temporalio.workflow._UpdateDefinition.get_name_and_result_type(update) + ) + + update_input = UpdateWithStartUpdateWorkflowInput( + update_id=id, + update=update_name, + args=temporalio.common._arg_or_args(arg, args), + headers={}, + ret_type=result_type or result_type_from_type_hint, + wait_for_stage=wait_for_stage, + ) + + def on_start( + start_response: temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse, + ): + start_workflow_operation._workflow_handle.set_result( + WorkflowHandle( + self, + start_workflow_operation._start_workflow_input.id, + first_execution_run_id=start_response.run_id, + result_run_id=start_response.run_id, + result_type=start_workflow_operation._start_workflow_input.ret_type, + ) + ) + + def on_start_error( + error: BaseException, + ): + start_workflow_operation._workflow_handle.set_exception(error) + + input = StartWorkflowUpdateWithStartInput( + start_workflow_input=start_workflow_operation._start_workflow_input, + update_workflow_input=update_input, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + _on_start=on_start, + _on_start_error=on_start_error, + ) + + return await self._impl.start_update_with_start_workflow(input) + + def list_workflows( + self, + query: str | None = None, + *, + limit: int | None = None, + page_size: int = 1000, + next_page_token: bytes | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowExecutionAsyncIterator: + """List workflows. + + This does not make a request until the first iteration is attempted. + Therefore any errors will not occur until then. + + Args: + query: A Temporal visibility list filter. See Temporal documentation + concerning visibility list filters including behavior when left + unset. + limit: Maximum number of workflows to return. If unset, all + workflows are returned. Only applies if using the + returned :py:class:`WorkflowExecutionAsyncIterator`. + as an async iterator. + page_size: Maximum number of results for each page. + next_page_token: A previously obtained next page token if doing + pagination. Usually not needed as the iterator automatically + starts from the beginning. + rpc_metadata: Headers used on each RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for each RPC call. + + Returns: + An async iterator that can be used with ``async for``. + """ + return self._impl.list_workflows( + ListWorkflowsInput( + query=query, + page_size=page_size, + next_page_token=next_page_token, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + limit=limit, + ) + ) + + async def count_workflows( + self, + query: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowExecutionCount: + """Count workflows. + + Args: + query: A Temporal visibility filter. See Temporal documentation + concerning visibility list filters. + rpc_metadata: Headers used on each RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for each RPC call. + + Returns: + Count of workflows. + """ + return await self._impl.count_workflows( + CountWorkflowsInput( + query=query, rpc_metadata=rpc_metadata, rpc_timeout=rpc_timeout + ) + ) + + # async no-param + @overload + async def start_activity( + self, + activity: CallableAsyncNoParam[ReturnType], + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + # sync no-param + @overload + async def start_activity( + self, + activity: CallableSyncNoParam[ReturnType], + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + # async single-param + @overload + async def start_activity( + self, + activity: CallableAsyncSingleParam[ParamType, ReturnType], + arg: ParamType, + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + # sync single-param + @overload + async def start_activity( + self, + activity: CallableSyncSingleParam[ParamType, ReturnType], + arg: ParamType, + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + # async multi-param + @overload + async def start_activity( + self, + activity: Callable[..., Awaitable[ReturnType]], + *, + args: Sequence[Any], + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + # sync multi-param + @overload + async def start_activity( + self, + activity: Callable[..., ReturnType], + *, + args: Sequence[Any], + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + # string name + @overload + async def start_activity( + self, + activity: str, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + result_type: type | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[Any]: ... + + async def start_activity( + self, + activity: ( + str | Callable[..., Awaitable[ReturnType]] | Callable[..., ReturnType] + ), + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + result_type: type | None = None, + # Either schedule_to_close_timeout or start_to_close_timeout must be present + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: + """Start an activity and return its handle. + + .. warning:: + This API is experimental. + + Args: + activity: String name or callable activity function to execute. + arg: Single argument to the activity. + args: Multiple arguments to the activity. Cannot be set if arg is. + id: Unique identifier for the activity. Required. + task_queue: Task queue to send the activity to. + result_type: For string name activities, optional type to deserialize result into. + schedule_to_close_timeout: Total time allowed for the activity from schedule to completion. + schedule_to_start_timeout: Time allowed for the activity to sit in the task queue. + start_to_close_timeout: Time allowed for a single execution attempt. + heartbeat_timeout: Time between heartbeats before the activity is considered failed. + id_reuse_policy: How to handle reusing activity IDs from closed activities. + Default is ALLOW_DUPLICATE. + id_conflict_policy: How to handle activity ID conflicts with running activities. + Default is FAIL. + retry_policy: Retry policy for the activity. + search_attributes: Search attributes for the activity. + summary: A single-line fixed summary for this activity that may appear + in the UI/CLI. This can be in single-line Temporal markdown format. + priority: Priority of the activity execution. + start_delay: Time to wait before dispatching the activity. + This delay is not applied to retry attempts. + rpc_metadata: Headers used on the RPC call. + rpc_timeout: Optional RPC deadline to set for the RPC call. + + Returns: + A handle to the started activity. + """ + name, result_type_from_type_annotation = ( + temporalio.activity._Definition.get_name_and_result_type(activity) + ) + return await self._impl.start_activity( + StartActivityInput( + activity_type=name, + args=temporalio.common._arg_or_args(arg, args), + id=id, + task_queue=task_queue, + result_type=result_type or result_type_from_type_annotation, + schedule_to_close_timeout=schedule_to_close_timeout, + schedule_to_start_timeout=schedule_to_start_timeout, + start_to_close_timeout=start_to_close_timeout, + heartbeat_timeout=heartbeat_timeout, + id_reuse_policy=id_reuse_policy, + id_conflict_policy=id_conflict_policy, + retry_policy=retry_policy, + search_attributes=search_attributes, + summary=summary, + start_delay=start_delay, + headers={}, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + priority=priority, + ) + ) + + # async no-param + @overload + async def execute_activity( + self, + activity: CallableAsyncNoParam[ReturnType], + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + # sync no-param + @overload + async def execute_activity( + self, + activity: CallableSyncNoParam[ReturnType], + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + # async single-param + @overload + async def execute_activity( + self, + activity: CallableAsyncSingleParam[ParamType, ReturnType], + arg: ParamType, + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + # sync single-param + @overload + async def execute_activity( + self, + activity: CallableSyncSingleParam[ParamType, ReturnType], + arg: ParamType, + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + # async multi-param + @overload + async def execute_activity( + self, + activity: Callable[..., Awaitable[ReturnType]], + *, + args: Sequence[Any], + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + # sync multi-param + @overload + async def execute_activity( + self, + activity: Callable[..., ReturnType], + *, + args: Sequence[Any], + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + # string name + @overload + async def execute_activity( + self, + activity: str, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + result_type: type | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> Any: ... + + async def execute_activity( + self, + activity: ( + str | Callable[..., Awaitable[ReturnType]] | Callable[..., ReturnType] + ), + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + result_type: type | None = None, + # Either schedule_to_close_timeout or start_to_close_timeout must be present + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: + """Start an activity, wait for it to complete, and return its result. + + .. warning:: + This API is experimental. + + This is a convenience method that combines :py:meth:`start_activity` and + :py:meth:`ActivityHandle.result`. + + Returns: + The result of the activity. + + Raises: + ActivityFailureError: If the activity completed with a failure. + """ + handle: ActivityHandle[ReturnType] = await self.start_activity( + cast(Any, activity), + arg, + args=args, + id=id, + task_queue=task_queue, + result_type=result_type, + schedule_to_close_timeout=schedule_to_close_timeout, + schedule_to_start_timeout=schedule_to_start_timeout, + start_to_close_timeout=start_to_close_timeout, + heartbeat_timeout=heartbeat_timeout, + id_reuse_policy=id_reuse_policy, + id_conflict_policy=id_conflict_policy, + retry_policy=retry_policy, + search_attributes=search_attributes, + summary=summary, + priority=priority, + start_delay=start_delay, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + return await handle.result() + + # async no-param + @overload + async def start_activity_class( + self, + activity: type[CallableAsyncNoParam[ReturnType]], + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + # sync no-param + @overload + async def start_activity_class( + self, + activity: type[CallableSyncNoParam[ReturnType]], + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + # async single-param + @overload + async def start_activity_class( + self, + activity: type[CallableAsyncSingleParam[ParamType, ReturnType]], + arg: ParamType, + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + # sync single-param + @overload + async def start_activity_class( + self, + activity: type[CallableSyncSingleParam[ParamType, ReturnType]], + arg: ParamType, + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + # async multi-param + @overload + async def start_activity_class( + self, + activity: type[Callable[..., Awaitable[ReturnType]]], # type: ignore[reportInvalidTypeForm] + *, + args: Sequence[Any], + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + # sync multi-param + @overload + async def start_activity_class( + self, + activity: type[Callable[..., ReturnType]], # type: ignore[reportInvalidTypeForm] + *, + args: Sequence[Any], + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + async def start_activity_class( + self, + activity: type[Callable], # type: ignore[reportInvalidTypeForm] + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + result_type: type | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[Any]: + """Start an activity from a callable class. + + .. warning:: + This API is experimental. + + See :py:meth:`start_activity` for parameter and return details. + """ + return await self.start_activity( + cast(Any, activity), + arg, + args=args, + id=id, + task_queue=task_queue, + result_type=result_type, + schedule_to_close_timeout=schedule_to_close_timeout, + schedule_to_start_timeout=schedule_to_start_timeout, + start_to_close_timeout=start_to_close_timeout, + heartbeat_timeout=heartbeat_timeout, + id_reuse_policy=id_reuse_policy, + id_conflict_policy=id_conflict_policy, + retry_policy=retry_policy, + search_attributes=search_attributes, + summary=summary, + priority=priority, + start_delay=start_delay, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + + # async no-param + @overload + async def execute_activity_class( + self, + activity: type[CallableAsyncNoParam[ReturnType]], + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + # sync no-param + @overload + async def execute_activity_class( + self, + activity: type[CallableSyncNoParam[ReturnType]], + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + # async single-param + @overload + async def execute_activity_class( + self, + activity: type[CallableAsyncSingleParam[ParamType, ReturnType]], + arg: ParamType, + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + # sync single-param + @overload + async def execute_activity_class( + self, + activity: type[CallableSyncSingleParam[ParamType, ReturnType]], + arg: ParamType, + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + # async multi-param + @overload + async def execute_activity_class( + self, + activity: type[Callable[..., Awaitable[ReturnType]]], # type: ignore[reportInvalidTypeForm] + *, + args: Sequence[Any], + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + # sync multi-param + @overload + async def execute_activity_class( + self, + activity: type[Callable[..., ReturnType]], # type: ignore[reportInvalidTypeForm] + *, + args: Sequence[Any], + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + async def execute_activity_class( + self, + activity: type[Callable], # type: ignore[reportInvalidTypeForm] + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + result_type: type | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> Any: + """Start an activity from a callable class and wait for completion. + + .. warning:: + This API is experimental. + + This is a shortcut for ``await`` :py:meth:`start_activity_class`. + """ + return await self.execute_activity( + cast(Any, activity), + arg, + args=args, + id=id, + task_queue=task_queue, + result_type=result_type, + schedule_to_close_timeout=schedule_to_close_timeout, + schedule_to_start_timeout=schedule_to_start_timeout, + start_to_close_timeout=start_to_close_timeout, + heartbeat_timeout=heartbeat_timeout, + id_reuse_policy=id_reuse_policy, + id_conflict_policy=id_conflict_policy, + retry_policy=retry_policy, + search_attributes=search_attributes, + summary=summary, + priority=priority, + start_delay=start_delay, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + + # async no-param + @overload + async def start_activity_method( + self, + activity: MethodAsyncNoParam[SelfType, ReturnType], + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + # async single-param + @overload + async def start_activity_method( + self, + activity: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], + arg: ParamType, + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + # async multi-param + @overload + async def start_activity_method( + self, + activity: Callable[ + Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType] + ], + *, + args: Sequence[Any], + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + # sync multi-param + @overload + async def start_activity_method( + self, + activity: Callable[Concatenate[SelfType, MultiParamSpec], ReturnType], + *, + args: Sequence[Any], + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + async def start_activity_method( + self, + activity: Callable, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + result_type: type | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[Any]: + """Start an activity from a method. + + .. warning:: + This API is experimental. + + See :py:meth:`start_activity` for parameter and return details. + """ + return await self.start_activity( + cast(Any, activity), + arg, + args=args, + id=id, + task_queue=task_queue, + result_type=result_type, + schedule_to_close_timeout=schedule_to_close_timeout, + schedule_to_start_timeout=schedule_to_start_timeout, + start_to_close_timeout=start_to_close_timeout, + heartbeat_timeout=heartbeat_timeout, + id_reuse_policy=id_reuse_policy, + id_conflict_policy=id_conflict_policy, + retry_policy=retry_policy, + search_attributes=search_attributes, + summary=summary, + priority=priority, + start_delay=start_delay, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + + # async no-param + @overload + async def execute_activity_method( + self, + activity: MethodAsyncNoParam[SelfType, ReturnType], + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + # async single-param + @overload + async def execute_activity_method( + self, + activity: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], + arg: ParamType, + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + # async multi-param + @overload + async def execute_activity_method( + self, + activity: Callable[ + Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType] + ], + *, + args: Sequence[Any], + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + # sync multi-param + @overload + async def execute_activity_method( + self, + activity: Callable[Concatenate[SelfType, MultiParamSpec], ReturnType], + *, + args: Sequence[Any], + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + async def execute_activity_method( + self, + activity: Callable, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + result_type: type | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> Any: + """Start an activity from a method and wait for completion. + + .. warning:: + This API is experimental. + + This is a shortcut for ``await`` :py:meth:`start_activity_method`. + """ + return await self.execute_activity( + cast(Any, activity), + arg, + args=args, + id=id, + task_queue=task_queue, + result_type=result_type, + schedule_to_close_timeout=schedule_to_close_timeout, + schedule_to_start_timeout=schedule_to_start_timeout, + start_to_close_timeout=start_to_close_timeout, + heartbeat_timeout=heartbeat_timeout, + id_reuse_policy=id_reuse_policy, + id_conflict_policy=id_conflict_policy, + retry_policy=retry_policy, + search_attributes=search_attributes, + summary=summary, + priority=priority, + start_delay=start_delay, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + + def list_activities( + self, + query: str, + *, + limit: int | None = None, + page_size: int = 1000, + next_page_token: bytes | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityExecutionAsyncIterator: + """List activities not started by a workflow. + + .. warning:: + This API is experimental. + + This does not make a request until the first iteration is attempted. + Therefore any errors will not occur until then. + + Args: + query: A Temporal visibility list filter for activities. Required. + limit: Maximum number of activities to return. If unset, all + activities are returned. Only applies if using the + returned :py:class:`ActivityExecutionAsyncIterator` + as an async iterator. + page_size: Maximum number of results for each page. + next_page_token: A previously obtained next page token if doing + pagination. Usually not needed as the iterator automatically + starts from the beginning. + rpc_metadata: Headers used on each RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for each RPC call. + + Returns: + An async iterator that can be used with ``async for``. + """ + return self._impl.list_activities( + ListActivitiesInput( + query=query, + page_size=page_size, + next_page_token=next_page_token, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + limit=limit, + ) + ) + + async def count_activities( + self, + query: str | None = None, + *, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityExecutionCount: + """Count activities not started by a workflow. + + .. warning:: + This API is experimental. + + Args: + query: A Temporal visibility filter for activities. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + + Returns: + Count of activities. + """ + return await self._impl.count_activities( + CountActivitiesInput( + query=query, rpc_metadata=rpc_metadata, rpc_timeout=rpc_timeout + ) + ) + + @overload + def get_activity_handle( + self, + activity_id: str, + *, + run_id: str | None = None, + ) -> ActivityHandle[Any]: ... + + @overload + def get_activity_handle( + self, + activity_id: str, + *, + run_id: str | None = None, + result_type: type[ReturnType], + ) -> ActivityHandle[ReturnType]: ... + + def get_activity_handle( + self, + activity_id: str, + *, + run_id: str | None = None, + result_type: type | None = None, + ) -> ActivityHandle[Any]: + """Get a handle to an existing activity, as the caller of that activity. + + The activity must not have been started by a workflow. + + .. warning:: + This API is experimental. + + To get a handle to an activity execution that you control for manual completion and + heartbeating, see :py:meth:`Client.get_async_activity_handle`. + + Args: + activity_id: The activity ID. + run_id: The activity run ID. If not provided, targets the latest run. + result_type: The result type to deserialize into. + + Returns: + A handle to the activity. + """ + return ActivityHandle( + self, + activity_id, + run_id=run_id, + result_type=result_type, + ) + + @overload + def get_async_activity_handle( + self, *, activity_id: str, run_id: str | None = None + ) -> AsyncActivityHandle: + pass + + @overload + def get_async_activity_handle( + self, *, workflow_id: str, run_id: str | None, activity_id: str + ) -> AsyncActivityHandle: + pass + + @overload + def get_async_activity_handle(self, *, task_token: bytes) -> AsyncActivityHandle: + pass + + def get_async_activity_handle( + self, + *, + workflow_id: str | None = None, + run_id: str | None = None, + activity_id: str | None = None, + task_token: bytes | None = None, + ) -> AsyncActivityHandle: + """Get a handle to an activity execution that you control, for manual + completion and heartbeating. + + To get a handle to an activity execution as the caller of that activity, + see :py:meth:`Client.get_activity_handle`. + + This function may be used to get a handle to an activity started by a + client, or an activity started by a workflow. + + To get a handle to an activity started by a workflow, use one of the + following two calls: + - Supply ``workflow_id``, ``run_id``, and ``activity_id`` + - Supply the activity ``task_token`` alone + + To get a handle to an activity not started by a workflow, supply + ``activity_id`` and ``run_id`` + + Args: + workflow_id: Workflow ID for the activity, or None if not a workflow + activity. Cannot be set if task_token is set. + run_id: Run ID for the activity or workflow. Cannot be set if + task_token is set. + activity_id: ID for the activity. Cannot be set if task_token is + set. + task_token: Task token for the activity. Cannot be set with other + fields. + + Returns: + A handle that can be used for completion or heartbeating. + """ + if task_token is not None: + if workflow_id is not None or run_id is not None or activity_id is not None: + raise ValueError("Task token cannot be present with other IDs") + return AsyncActivityHandle(self, task_token) + elif workflow_id is not None: + if activity_id is None: + raise ValueError( + "Workflow ID, run ID, and activity ID must all be given together" + ) + return AsyncActivityHandle( + self, + AsyncActivityIDReference( + workflow_id=workflow_id, run_id=run_id, activity_id=activity_id + ), + ) + elif activity_id is not None: + return AsyncActivityHandle( + self, + AsyncActivityIDReference( + activity_id=activity_id, + run_id=run_id, + workflow_id=None, + ), + ) + raise ValueError( + "Require task token, or workflow_id & run_id & activity_id, or activity_id & run_id" + ) + + async def create_schedule( + self, + id: str, + schedule: Schedule, + *, + trigger_immediately: bool = False, + backfill: Sequence[ScheduleBackfill] = [], + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ScheduleHandle: + """Create a schedule and return its handle. + + Args: + id: Unique identifier of the schedule. + schedule: Schedule to create. + trigger_immediately: If true, trigger one action immediately when + creating the schedule. + backfill: Set of time periods to take actions on as if that time + passed right now. + memo: Memo for the schedule. Memo for a scheduled workflow is part + of the schedule action. + search_attributes: Search attributes for the schedule. Search + attributes for a scheduled workflow are part of the scheduled + action. The dictionary form of this is DEPRECATED, use + :py:class:`temporalio.common.TypedSearchAttributes`. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + + Returns: + A handle to the created schedule. + + Raises: + ScheduleAlreadyRunningError: If a schedule with this ID is already + running. + """ + temporalio.common._warn_on_deprecated_search_attributes(search_attributes) + return await self._impl.create_schedule( + CreateScheduleInput( + id=id, + schedule=schedule, + trigger_immediately=trigger_immediately, + backfill=backfill, + memo=memo, + search_attributes=search_attributes, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) + + def get_schedule_handle(self, id: str) -> ScheduleHandle: + """Get a schedule handle for the given ID.""" + return ScheduleHandle(self, id) + + async def list_schedules( + self, + query: str | None = None, + *, + page_size: int = 1000, + next_page_token: bytes | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ScheduleAsyncIterator: + """List schedules. + + This does not make a request until the first iteration is attempted. + Therefore any errors will not occur until then. + + Note, this list is eventually consistent. Therefore if a schedule is + added or deleted, it may not be available in the list immediately. + + Args: + page_size: Maximum number of results for each page. + query: A Temporal visibility list filter. See Temporal documentation + concerning visibility list filters including behavior when left + unset. + next_page_token: A previously obtained next page token if doing + pagination. Usually not needed as the iterator automatically + starts from the beginning. + rpc_metadata: Headers used on each RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for each RPC call. + + Returns: + An async iterator that can be used with ``async for``. + """ + return self._impl.list_schedules( + ListSchedulesInput( + page_size=page_size, + next_page_token=next_page_token, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + query=query, + ) + ) + + async def update_worker_build_id_compatibility( + self, + task_queue: str, + operation: BuildIdOp, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Used to add new Build IDs or otherwise update the relative compatibility of Build Ids as + defined on a specific task queue for the Worker Versioning feature. + + For more on this feature, see https://docs.temporal.io/workers#worker-versioning + + .. deprecated:: + Legacy API, see the docs above for new usage + + Args: + task_queue: The task queue to target. + operation: The operation to perform. + rpc_metadata: Headers used on each RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for each RPC call. + """ + return await self._impl.update_worker_build_id_compatibility( + UpdateWorkerBuildIdCompatibilityInput( + task_queue, + operation, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) + + async def get_worker_build_id_compatibility( + self, + task_queue: str, + max_sets: int | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkerBuildIdVersionSets: + """Get the Build ID compatibility sets for a specific task queue. + + For more on this feature, see https://docs.temporal.io/workers#worker-versioning + + .. deprecated:: + Legacy API, see the docs above for new usage + + Args: + task_queue: The task queue to target. + max_sets: The maximum number of sets to return. If not specified, all sets will be + returned. + rpc_metadata: Headers used on each RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for each RPC call. + """ + return await self._impl.get_worker_build_id_compatibility( + GetWorkerBuildIdCompatibilityInput( + task_queue, + max_sets, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) + + async def get_worker_task_reachability( + self, + build_ids: Sequence[str], + task_queues: Sequence[str] = [], + reachability_type: TaskReachabilityType | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkerTaskReachability: + """Determine if some Build IDs for certain Task Queues could have tasks dispatched to them. + + For more on this feature, see https://docs.temporal.io/workers#worker-versioning + + .. deprecated:: + Legacy API, see the docs above for new usage + + Args: + build_ids: The Build IDs to query the reachability of. At least one must be specified. + task_queues: Task Queues to restrict the query to. If not specified, all Task Queues + will be searched. When requesting a large number of task queues or all task queues + associated with the given Build IDs in a namespace, all Task Queues will be listed + in the response but some of them may not contain reachability information due to a + server enforced limit. When reaching the limit, task queues that reachability + information could not be retrieved for will be marked with a ``NotFetched`` entry in + {@link BuildIdReachability.taskQueueReachability}. The caller may issue another call + to get the reachability for those task queues. + reachability_type: The kind of reachability this request is concerned with. + rpc_metadata: Headers used on each RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for each RPC call. + """ + return await self._impl.get_worker_task_reachability( + GetWorkerTaskReachabilityInput( + build_ids, + task_queues, + reachability_type, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) + + def create_nexus_client( + self, + service: type[NexusServiceType] | str, + endpoint: str, + ) -> NexusClient[NexusServiceType]: + """Create a client for starting standalone Nexus operations. + + .. warning:: + This API is experimental and unstable. + + Args: + service: The Nexus service type or service name string. + endpoint: Endpoint name, resolved to a URL via the cluster's + endpoint registry. + + Returns: + A Nexus client for the given service and endpoint. + """ + return _NexusClient(client=self, service=service, endpoint=endpoint) + + def list_nexus_operations( + self, + query: str, + *, + limit: int | None = None, + page_size: int = 1000, + next_page_token: bytes | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> NexusOperationExecutionAsyncIterator: + """List standalone Nexus operations. + + .. warning:: + This API is experimental and unstable. + + This does not make a request until the first iteration is attempted. + Therefore any errors will not occur until then. + + Args: + query: A Temporal visibility list filter for nexus operations. Required. + limit: Maximum number of operations to return. If unset, all + operations are returned. Only applies if using the + returned :py:class:`NexusOperationExecutionAsyncIterator` + as an async iterator. + page_size: Maximum number of results for each page. + next_page_token: A previously obtained next page token if doing + pagination. Usually not needed as the iterator automatically + starts from the beginning. + rpc_metadata: Headers used on each RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for each RPC call. + + Returns: + An async iterator that can be used with ``async for``. + """ + return self._impl.list_nexus_operations( + ListNexusOperationsInput( + query=query, + page_size=page_size, + next_page_token=next_page_token, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + limit=limit, + ) + ) + + async def count_nexus_operations( + self, + query: str | None = None, + *, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> NexusOperationExecutionCount: + """Count standalone Nexus operations. + + .. warning:: + This API is experimental and unstable. + + Args: + query: A Temporal visibility filter for nexus operations. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + + Returns: + Count of nexus operations. + """ + return await self._impl.count_nexus_operations( + CountNexusOperationsInput( + query=query, rpc_metadata=rpc_metadata, rpc_timeout=rpc_timeout + ) + ) + + @overload + def get_nexus_operation_handle( + self, + operation_id: str, + *, + run_id: str | None = None, + ) -> NexusOperationHandle[Any]: ... + + @overload + def get_nexus_operation_handle( + self, + operation_id: str, + *, + run_id: str | None = None, + result_type: type[ReturnType], + ) -> NexusOperationHandle[ReturnType]: ... + + @overload + def get_nexus_operation_handle( + self, + operation_id: str, + *, + operation: nexusrpc.Operation[Any, OutputT], + run_id: str | None = None, + ) -> NexusOperationHandle[OutputT]: ... + + def get_nexus_operation_handle( + self, + operation_id: str, + *, + operation: nexusrpc.Operation[Any, Any] | None = None, + run_id: str | None = None, + result_type: type | None = None, + ) -> NexusOperationHandle[Any]: + """Get a handle to an existing standalone Nexus operation. + + .. warning:: + This API is experimental and unstable. + + Args: + operation_id: The operation ID. + operation: A ``nexusrpc.Operation`` from which the result type + is extracted. If both ``operation`` and ``result_type`` are + provided, the ``result_type`` takes precedence. + run_id: The operation run ID. If not provided, targets the latest run. + result_type: The result type to deserialize into. + + Returns: + A handle to the operation. + """ + result_type = result_type or (operation.output_type if operation else None) + return NexusOperationHandle( + self, + operation_id, + run_id=run_id, + result_type=result_type, + ) + + +class ClientConnectConfig(TypedDict, total=False): + """TypedDict of keyword arguments for :py:meth:`Client.connect`.""" + + target_host: str + namespace: str + api_key: str | None + data_converter: temporalio.converter.DataConverter + plugins: Sequence[Plugin] + interceptors: Sequence[Interceptor] + default_workflow_query_reject_condition: ( + temporalio.common.QueryRejectCondition | None + ) + tls: bool | TLSConfig | None + retry_config: RetryConfig | None + keep_alive_config: KeepAliveConfig | None + rpc_metadata: Mapping[str, str | bytes] + identity: str | None + lazy: bool + runtime: temporalio.runtime.Runtime | None + http_connect_proxy_config: HttpConnectProxyConfig | None + dns_load_balancing_config: DnsLoadBalancingConfig | None + grpc_compression: GrpcCompression + payload_limits: PayloadLimitsConfig + header_codec_behavior: HeaderCodecBehavior + + +class ClientConfig(TypedDict, total=False): + """TypedDict of config originally passed to :py:meth:`Client`.""" + + service_client: Required[temporalio.service.ServiceClient] + namespace: Required[str] + data_converter: Required[temporalio.converter.DataConverter] + plugins: Required[Sequence[Plugin]] + interceptors: Required[Sequence[Interceptor]] + default_workflow_query_reject_condition: Required[ + temporalio.common.QueryRejectCondition | None + ] + header_codec_behavior: Required[HeaderCodecBehavior] diff --git a/temporalio/client/_cloud.py b/temporalio/client/_cloud.py new file mode 100644 index 000000000..5394f8ca9 --- /dev/null +++ b/temporalio/client/_cloud.py @@ -0,0 +1,187 @@ +"""Client support for accessing Temporal.""" + +from __future__ import annotations + +from collections.abc import ( + Mapping, +) + +import temporalio.runtime +import temporalio.service +from temporalio.service import ( + DnsLoadBalancingConfig, + GrpcCompression, + HttpConnectProxyConfig, + KeepAliveConfig, + RetryConfig, + TLSConfig, +) + + +class CloudOperationsClient: + """Client for accessing Temporal Cloud Operations API. + + .. warning:: + This client and the API are experimental + + Most users will use :py:meth:`connect` to create a client. The + :py:attr:`cloud_service` property provides access to a raw gRPC cloud + service client. + + Clients are not thread-safe and should only be used in the event loop they + are first connected in. If a client needs to be used from another thread + than where it was created, make sure the event loop where it was created is + captured, and then call :py:func:`asyncio.run_coroutine_threadsafe` with the + client call and that event loop. + + Clients do not work across forks since runtimes do not work across forks. + """ + + @staticmethod + async def connect( + *, + api_key: str | None = None, + version: str | None = None, + target_host: str = "saas-api.tmprl.cloud:443", + tls: bool | TLSConfig = True, + retry_config: RetryConfig | None = None, + keep_alive_config: KeepAliveConfig | None = KeepAliveConfig.default, + rpc_metadata: Mapping[str, str | bytes] = {}, + identity: str | None = None, + lazy: bool = False, + runtime: temporalio.runtime.Runtime | None = None, + http_connect_proxy_config: HttpConnectProxyConfig | None = None, + dns_load_balancing_config: DnsLoadBalancingConfig | None = None, + grpc_compression: GrpcCompression = GrpcCompression.GZIP, + ) -> CloudOperationsClient: + """Connect to a Temporal Cloud Operations API. + + .. warning:: + This client and the API are experimental + + Args: + api_key: API key for Temporal. This becomes the "Authorization" + HTTP header with "Bearer " prepended. This is only set if RPC + metadata doesn't already have an "authorization" key. This is + essentially required for access to the cloud API. + version: Version header for safer mutations. May or may not be + required depending on cloud settings. + target_host: ``host:port`` for the Temporal server. The default is + to the common cloud endpoint. + tls: If true, the default, use system default TLS configuration. If + false, the default, do not use TLS. If TLS configuration + present, that TLS configuration will be used. The default is + usually required to access the API. + retry_config: Retry configuration for direct service calls (when + opted in) or all high-level calls made by this client (which all + opt-in to retries by default). If unset, a default retry + configuration is used. + keep_alive_config: Keep-alive configuration for the client + connection. Default is to check every 30s and kill the + connection if a response doesn't come back in 15s. Can be set to + ``None`` to disable. + rpc_metadata: Headers to use for all calls to the server. Keys here + can be overriden by per-call RPC metadata keys. + identity: Identity for this client. If unset, a default is created + based on the version of the SDK. + lazy: If true, the client will not connect until the first call is + attempted or a worker is created with it. Lazy clients cannot be + used for workers. + runtime: The runtime for this client, or the default if unset. + http_connect_proxy_config: Configuration for HTTP CONNECT proxy. + dns_load_balancing_config: DNS load balancing configuration for the + client connection. Default is disabled. Silently disabled when + ``http_connect_proxy_config`` is set, since the two are mutually + exclusive. + grpc_compression: Transport-level gRPC compression for the client + connection. Default is gzip. Set to + :py:attr:`GrpcCompression.NONE` to disable compression. + """ + # Add version if given + if version: + rpc_metadata = dict(rpc_metadata) + rpc_metadata["temporal-cloud-api-version"] = version + connect_config = temporalio.service.ConnectConfig( + target_host=target_host, + api_key=api_key, + tls=tls, + retry_config=retry_config, + keep_alive_config=keep_alive_config, + rpc_metadata=rpc_metadata, + identity=identity or "", + lazy=lazy, + runtime=runtime, + http_connect_proxy_config=http_connect_proxy_config, + dns_load_balancing_config=dns_load_balancing_config, + grpc_compression=grpc_compression, + ) + return CloudOperationsClient( + await temporalio.service.ServiceClient.connect(connect_config) + ) + + def __init__( + self, + service_client: temporalio.service.ServiceClient, + ): + """Create a Temporal Cloud Operations client from a service client. + + .. warning:: + This client and the API are experimental + + Args: + service_client: Existing service client to use. + """ + self._service_client = service_client + + @property + def service_client(self) -> temporalio.service.ServiceClient: + """Raw gRPC service client.""" + return self._service_client + + @property + def cloud_service(self) -> temporalio.service.CloudService: + """Raw gRPC cloud service client.""" + return self._service_client.cloud_service + + @property + def identity(self) -> str: + """Identity used in calls by this client.""" + return self._service_client.config.identity + + @property + def rpc_metadata(self) -> Mapping[str, str | bytes]: + """Headers for every call made by this client. + + Do not use mutate this mapping. Rather, set this property with an + entirely new mapping to change the headers. This may include the + ``temporal-cloud-api-version`` header if set. + """ + return self.service_client.config.rpc_metadata + + @rpc_metadata.setter + def rpc_metadata(self, value: Mapping[str, str | bytes]) -> None: + """Update the headers for this client. + + Do not mutate this mapping after set. Rather, set an entirely new + mapping if changes are needed. Currently this must be set with the + ``temporal-cloud-api-version`` header if it is needed. + """ + # Update config and perform update + self.service_client.config.rpc_metadata = value + self.service_client.update_rpc_metadata(value) + + @property + def api_key(self) -> str | None: + """API key for every call made by this client.""" + return self.service_client.config.api_key + + @api_key.setter + def api_key(self, value: str | None) -> None: + """Update the API key for this client. + + This is only set if RPCmetadata doesn't already have an "authorization" + key. + """ + # Update config and perform update + self.service_client.config.api_key = value + self.service_client.update_api_key(value) diff --git a/temporalio/client/_exceptions.py b/temporalio/client/_exceptions.py new file mode 100644 index 000000000..fdcd263e6 --- /dev/null +++ b/temporalio/client/_exceptions.py @@ -0,0 +1,139 @@ +"""Client support for accessing Temporal.""" + +from __future__ import annotations + +from typing import ( + TYPE_CHECKING, +) + +import temporalio.exceptions +from temporalio.activity import ActivityCancellationDetails + +if TYPE_CHECKING: + from ._workflow import WorkflowExecutionStatus + + +class WorkflowFailureError(temporalio.exceptions.TemporalError): + """Error that occurs when a workflow is unsuccessful.""" + + def __init__(self, *, cause: BaseException) -> None: + """Create workflow failure error.""" + super().__init__("Workflow execution failed") + self.__cause__ = cause + + @property + def cause(self) -> BaseException: + """Cause of the workflow failure.""" + assert self.__cause__ + return self.__cause__ + + +class WorkflowContinuedAsNewError(temporalio.exceptions.TemporalError): + """Error that occurs when a workflow was continued as new.""" + + def __init__(self, new_execution_run_id: str) -> None: + """Create workflow continue as new error.""" + super().__init__("Workflow continued as new") + self._new_execution_run_id = new_execution_run_id + + @property + def new_execution_run_id(self) -> str: + """New execution run ID the workflow continued to""" + return self._new_execution_run_id + + +class WorkflowQueryRejectedError(temporalio.exceptions.TemporalError): + """Error that occurs when a query was rejected.""" + + def __init__(self, status: WorkflowExecutionStatus | None) -> None: + """Create workflow query rejected error.""" + super().__init__(f"Query rejected, status: {status}") + self._status = status + + @property + def status(self) -> WorkflowExecutionStatus | None: + """Get workflow execution status causing rejection.""" + return self._status + + +class WorkflowQueryFailedError(temporalio.exceptions.TemporalError): + """Error that occurs when a query fails.""" + + def __init__(self, message: str) -> None: + """Create workflow query failed error.""" + super().__init__(message) + self._message = message + + @property + def message(self) -> str: + """Get query failed message.""" + return self._message + + +class WorkflowUpdateFailedError(temporalio.exceptions.TemporalError): + """Error that occurs when an update fails.""" + + def __init__(self, cause: BaseException) -> None: + """Create workflow update failed error.""" + super().__init__("Workflow update failed") + self.__cause__ = cause + + @property + def cause(self) -> BaseException: + """Cause of the update failure.""" + assert self.__cause__ + return self.__cause__ + + +class RPCTimeoutOrCancelledError(temporalio.exceptions.TemporalError): + """Error that occurs on some client calls that timeout or get cancelled.""" + + pass + + +class WorkflowUpdateRPCTimeoutOrCancelledError(RPCTimeoutOrCancelledError): + """Error that occurs when update RPC call times out or is cancelled. + + Note, this is not related to any general concept of timing out or cancelling + a running update, this is only related to the client call itself. + """ + + def __init__(self) -> None: + """Create workflow update timeout or cancelled error.""" + super().__init__("Timeout or cancellation waiting for update") + + +class ActivityFailureError(temporalio.exceptions.TemporalError): + """Error that occurs when an activity is unsuccessful. + + .. warning:: + This API is experimental. + """ + + def __init__(self, *, cause: BaseException) -> None: + """Create activity failure error.""" + super().__init__("Activity execution failed") + self.__cause__ = cause + + @property + def cause(self) -> BaseException: + """Cause of the activity failure.""" + assert self.__cause__ + return self.__cause__ + + +class AsyncActivityCancelledError(temporalio.exceptions.TemporalError): + """Error that occurs when async activity attempted heartbeat but was cancelled.""" + + def __init__(self, details: ActivityCancellationDetails | None = None) -> None: + """Create async activity cancelled error.""" + super().__init__("Activity cancelled") + self.details = details + + +class ScheduleAlreadyRunningError(temporalio.exceptions.TemporalError): + """Error when a schedule is already running.""" + + def __init__(self) -> None: + """Create schedule already running error.""" + super().__init__("Schedule already running") diff --git a/temporalio/client/_helpers.py b/temporalio/client/_helpers.py new file mode 100644 index 000000000..08a584651 --- /dev/null +++ b/temporalio/client/_helpers.py @@ -0,0 +1,202 @@ +"""Client support for accessing Temporal.""" + +from __future__ import annotations + +import copy +import json +import re +from collections.abc import ( + Iterable, + Mapping, +) +from typing import ( + Any, +) + +import google.protobuf.json_format +from google.protobuf.internal.containers import MessageMap + +import temporalio.api.common.v1 +import temporalio.api.history.v1 +import temporalio.api.sdk.v1 +import temporalio.common +import temporalio.converter +from temporalio.converter import ( + DataConverter, +) + + +async def _apply_headers( # pyright: ignore[reportUnusedFunction] + source: Mapping[str, temporalio.api.common.v1.Payload] | None, + dest: MessageMap[str, temporalio.api.common.v1.Payload], + encode_headers: bool, + data_converter: DataConverter, +) -> None: + if source is None: + return + if encode_headers: + for payload in source.values(): + payload.CopyFrom(await data_converter._transform_outbound_payload(payload)) + temporalio.common._apply_headers(source, dest) + + +def _history_from_json( # pyright: ignore[reportUnusedFunction] + history: str | dict[str, Any], +) -> temporalio.api.history.v1.History: + if isinstance(history, str): + history = json.loads(history) + else: + # Copy the dict so we can mutate it + history = copy.deepcopy(history) + if not isinstance(history, dict): + raise ValueError("JSON history not a dictionary") + events = history.get("events") + if not isinstance(events, Iterable): + raise ValueError("History does not have iterable 'events'") + for event in events: + if not isinstance(event, dict): + raise ValueError("Event not a dictionary") + _fix_history_enum( + "CANCEL_EXTERNAL_WORKFLOW_EXECUTION_FAILED_CAUSE", + event, + "requestCancelExternalWorkflowExecutionFailedEventAttributes", + "cause", + ) + _fix_history_enum("CONTINUE_AS_NEW_INITIATOR", event, "*", "initiator") + _fix_history_enum("EVENT_TYPE", event, "eventType") + _fix_history_enum( + "PARENT_CLOSE_POLICY", + event, + "startChildWorkflowExecutionInitiatedEventAttributes", + "parentClosePolicy", + ) + _fix_history_enum("RETRY_STATE", event, "*", "retryState") + _fix_history_enum( + "SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_FAILED_CAUSE", + event, + "signalExternalWorkflowExecutionFailedEventAttributes", + "cause", + ) + _fix_history_enum( + "START_CHILD_WORKFLOW_EXECUTION_FAILED_CAUSE", + event, + "startChildWorkflowExecutionFailedEventAttributes", + "cause", + ) + _fix_history_enum("TASK_QUEUE_KIND", event, "*", "taskQueue", "kind") + _fix_history_enum( + "TIMEOUT_TYPE", + event, + "workflowTaskTimedOutEventAttributes", + "timeoutType", + ) + _fix_history_enum( + "WORKFLOW_ID_REUSE_POLICY", + event, + "startChildWorkflowExecutionInitiatedEventAttributes", + "workflowIdReusePolicy", + ) + _fix_history_enum( + "WORKFLOW_TASK_FAILED_CAUSE", + event, + "workflowTaskFailedEventAttributes", + "cause", + ) + _fix_history_failure(event, "*", "failure") + _fix_history_failure(event, "activityTaskStartedEventAttributes", "lastFailure") + _fix_history_failure( + event, "workflowExecutionStartedEventAttributes", "continuedFailure" + ) + return google.protobuf.json_format.ParseDict( + history, temporalio.api.history.v1.History(), ignore_unknown_fields=True + ) + + +def _fix_history_failure(parent: dict[str, Any], *attrs: str) -> None: + _fix_history_enum( + "TIMEOUT_TYPE", parent, *attrs, "timeoutFailureInfo", "timeoutType" + ) + _fix_history_enum("RETRY_STATE", parent, *attrs, "*", "retryState") + # Recurse into causes. First collect all failure parents. + parents = [parent] + for attr in attrs: + new_parents = [] + for parent in parents: + if attr == "*": + for v in parent.values(): + if isinstance(v, dict): + new_parents.append(v) + else: + child = parent.get(attr) + if isinstance(child, dict): + new_parents.append(child) + if not new_parents: + return + parents = new_parents + # Fix each + for parent in parents: + _fix_history_failure(parent, "cause") + + +_pascal_case_match = re.compile("([A-Z]+)") + + +def _fix_history_enum(prefix: str, parent: dict[str, Any], *attrs: str) -> None: + # If the attr is "*", we need to handle all dict children + if attrs[0] == "*": + for child in parent.values(): + if isinstance(child, dict): + _fix_history_enum(prefix, child, *attrs[1:]) + else: + child = parent.get(attrs[0]) + if isinstance(child, str) and len(attrs) == 1: + # We only fix it if it doesn't already have the prefix + if not parent[attrs[0]].startswith(prefix): + parent[attrs[0]] = ( + prefix + _pascal_case_match.sub(r"_\1", child).upper() + ) + elif isinstance(child, dict) and len(attrs) > 1: + _fix_history_enum(prefix, child, *attrs[1:]) + elif isinstance(child, list) and len(attrs) > 1: + for child_item in child: + if isinstance(child_item, dict): + _fix_history_enum(prefix, child_item, *attrs[1:]) + + +async def _encode_user_metadata( # pyright: ignore[reportUnusedFunction] + converter: temporalio.converter.DataConverter, + summary: str | temporalio.api.common.v1.Payload | None, + details: str | temporalio.api.common.v1.Payload | None, +) -> temporalio.api.sdk.v1.UserMetadata | None: + if summary is None and details is None: + return None + enc_summary = None + enc_details = None + if summary is not None: + if isinstance(summary, str): + enc_summary = (await converter.encode([summary]))[0] + else: + enc_summary = summary + if details is not None: + if isinstance(details, str): + enc_details = (await converter.encode([details]))[0] + else: + enc_details = details + return temporalio.api.sdk.v1.UserMetadata(summary=enc_summary, details=enc_details) + + +async def _decode_user_metadata( # pyright: ignore[reportUnusedFunction] + converter: temporalio.converter.DataConverter, + metadata: temporalio.api.sdk.v1.UserMetadata | None, +) -> tuple[str | None, str | None]: + """Returns (summary, details)""" + if metadata is None: + return None, None + return ( + None + if not metadata.HasField("summary") + else (await converter.decode([metadata.summary]))[0], + None + if not metadata.HasField("details") + else (await converter.decode([metadata.details]))[0], + ) diff --git a/temporalio/client/_impl.py b/temporalio/client/_impl.py new file mode 100644 index 000000000..9595351f2 --- /dev/null +++ b/temporalio/client/_impl.py @@ -0,0 +1,1711 @@ +"""Client support for accessing Temporal.""" + +from __future__ import annotations + +import asyncio +import inspect +import uuid +import warnings +from collections.abc import ( + Callable, + Mapping, +) +from datetime import timedelta +from typing import ( + TYPE_CHECKING, + Any, + cast, +) + +from google.protobuf.internal.containers import MessageMap + +import temporalio.api.common.v1 +import temporalio.api.enums.v1 +import temporalio.api.errordetails.v1 +import temporalio.api.failure.v1 +import temporalio.api.schedule.v1 +import temporalio.api.taskqueue.v1 +import temporalio.api.update.v1 +import temporalio.api.workflowservice.v1 +import temporalio.common +import temporalio.converter +import temporalio.exceptions +import temporalio.nexus +import temporalio.nexus._operation_context +from temporalio.activity import ActivityCancellationDetails +from temporalio.converter import ( + ActivitySerializationContext, + StorageDriverActivityInfo, + StorageDriverStoreContext, + StorageDriverWorkflowInfo, + WorkflowSerializationContext, +) +from temporalio.service import ( + RPCError, + RPCStatusCode, +) + +from ..common import HeaderCodecBehavior +from ._activity import ( + ActivityExecutionAsyncIterator, + ActivityExecutionCount, + ActivityExecutionDescription, + ActivityHandle, + AsyncActivityIDReference, +) +from ._exceptions import ( + AsyncActivityCancelledError, + ScheduleAlreadyRunningError, + WorkflowQueryFailedError, + WorkflowQueryRejectedError, + WorkflowUpdateRPCTimeoutOrCancelledError, +) +from ._helpers import _apply_headers, _encode_user_metadata +from ._interceptor import ( + BackfillScheduleInput, + CancelActivityInput, + CancelNexusOperationInput, + CancelWorkflowInput, + CompleteAsyncActivityInput, + CountActivitiesInput, + CountNexusOperationsInput, + CountWorkflowsInput, + CreateScheduleInput, + DeleteScheduleInput, + DescribeActivityInput, + DescribeNexusOperationInput, + DescribeScheduleInput, + DescribeWorkflowInput, + FailAsyncActivityInput, + FetchWorkflowHistoryEventsInput, + GetNexusOperationResultInput, + GetWorkerBuildIdCompatibilityInput, + GetWorkerTaskReachabilityInput, + HeartbeatAsyncActivityInput, + ListActivitiesInput, + ListNexusOperationsInput, + ListSchedulesInput, + ListWorkflowsInput, + OutboundInterceptor, + PauseScheduleInput, + QueryWorkflowInput, + ReportCancellationAsyncActivityInput, + SignalWorkflowInput, + StartActivityInput, + StartNexusOperationInput, + StartWorkflowInput, + StartWorkflowUpdateInput, + StartWorkflowUpdateWithStartInput, + TerminateActivityInput, + TerminateNexusOperationInput, + TerminateWorkflowInput, + TriggerScheduleInput, + UnpauseScheduleInput, + UpdateScheduleInput, + UpdateWithStartStartWorkflowInput, + UpdateWithStartUpdateWorkflowInput, + UpdateWorkerBuildIdCompatibilityInput, +) +from ._nexus import ( + NexusOperationExecutionAsyncIterator, + NexusOperationExecutionCount, + NexusOperationExecutionDescription, + NexusOperationFailureError, + NexusOperationHandle, +) +from ._schedule import ( + ScheduleAsyncIterator, + ScheduleDescription, + ScheduleHandle, + ScheduleUpdate, + ScheduleUpdateInput, +) +from ._worker_versioning import WorkerBuildIdVersionSets, WorkerTaskReachability +from ._workflow import ( + WorkflowExecutionAsyncIterator, + WorkflowExecutionCount, + WorkflowExecutionDescription, + WorkflowExecutionStatus, + WorkflowHandle, + WorkflowHistoryEventAsyncIterator, + WorkflowUpdateHandle, + WorkflowUpdateStage, +) + +if TYPE_CHECKING: + from ._client import Client + + +class _ClientImpl(OutboundInterceptor): # pyright: ignore[reportUnusedClass] + def __init__(self, client: Client) -> None: # type: ignore + # We are intentionally not calling the base class's __init__ here + self._client = client + + ### Workflow calls + + async def start_workflow( + self, input: StartWorkflowInput + ) -> WorkflowHandle[Any, Any]: + req: ( + temporalio.api.workflowservice.v1.StartWorkflowExecutionRequest + | temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest + ) + if input.start_signal is not None: + req = await self._build_signal_with_start_workflow_execution_request(input) + else: + req = await self._build_start_workflow_execution_request(input) + + resp: ( + temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse + | temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse + ) + first_execution_run_id = None + eagerly_started = False + try: + if isinstance( + req, + temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest, + ): + resp = await self._client.workflow_service.signal_with_start_workflow_execution( + req, + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + else: + resp = await self._client.workflow_service.start_workflow_execution( + req, + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + first_execution_run_id = resp.run_id + eagerly_started = resp.HasField("eager_workflow_task") + except RPCError as err: + # If the status is ALREADY_EXISTS and the details can be extracted + # as already started, use a different exception + if err.status == RPCStatusCode.ALREADY_EXISTS and err.grpc_status.details: + details = temporalio.api.errordetails.v1.WorkflowExecutionAlreadyStartedFailure() + if err.grpc_status.details[0].Unpack(details): + raise temporalio.exceptions.WorkflowAlreadyStartedError( + input.id, input.workflow, run_id=details.run_id + ) + raise + handle: WorkflowHandle[Any, Any] = WorkflowHandle( + self._client, + req.workflow_id, + result_run_id=resp.run_id, + first_execution_run_id=first_execution_run_id, + result_type=input.ret_type, + start_workflow_response=resp, + ) + setattr(handle, "__temporal_eagerly_started", eagerly_started) + nexus_ctx = temporalio.nexus._operation_context._try_start_operation_context() + if nexus_ctx is not None: + nexus_ctx._add_start_workflow_response_link(handle) + return handle + + async def _build_start_workflow_execution_request( + self, input: StartWorkflowInput + ) -> temporalio.api.workflowservice.v1.StartWorkflowExecutionRequest: + req = temporalio.api.workflowservice.v1.StartWorkflowExecutionRequest() + await self._populate_start_workflow_execution_request(req, input) + # _populate_start_workflow_execution_request is used for both StartWorkflowInput + # and UpdateWithStartStartWorkflowInput. UpdateWithStartStartWorkflowInput does + # not have the following two fields so they are handled here. + req.request_eager_execution = input.request_eager_start + if input.request_id: + req.request_id = input.request_id + + req.completion_callbacks.extend( + temporalio.api.common.v1.Callback( + nexus=temporalio.api.common.v1.Callback.Nexus( + url=callback.url, + header=callback.headers, + ), + links=input.links, + ) + for callback in input.callbacks + ) + # Links are duplicated on request for compatibility with older server versions. + req.links.extend(input.links) + + nexus_ctx = temporalio.nexus._operation_context._try_start_operation_context() + if nexus_ctx is not None: + # This start was issued from inside a Nexus operation handler. If the workflow ID + # conflict policy is WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING and a conflict is + # detected, attach this request's request ID, completion callbacks, and links to + # the existing run. The TemporalNexusClient and WorkflowRunOperationContext are + # responsible for setting the callbacks correctly, so it is safe to enable all + # on-conflict options whenever we are invoked from an operation handler. + req.on_conflict_options.attach_request_id = True + req.on_conflict_options.attach_completion_callbacks = True + req.on_conflict_options.attach_links = True + # The nexus-backing workflow already carries its inbound links via input.links + # (start_workflow forwards them as links=...). A plain start_workflow issued from + # inside a Nexus operation handler must forward the inbound Nexus task links + # explicitly so the started callee's WorkflowExecutionStarted event links back to + # the caller. + if not temporalio.nexus._operation_context._in_nexus_backing_start_context(): + req.links.extend(nexus_ctx._get_request_links()) + + return req + + async def _build_signal_with_start_workflow_execution_request( + self, input: StartWorkflowInput + ) -> temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest: + assert input.start_signal + data_converter = self._client.data_converter._with_contexts( + WorkflowSerializationContext( + namespace=self._client.namespace, + workflow_id=input.id, + ), + StorageDriverStoreContext( + target=StorageDriverWorkflowInfo( + id=input.id, type=input.workflow, namespace=self._client.namespace + ), + ), + ) + req = temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest( + signal_name=input.start_signal + ) + if input.start_signal_args: + req.signal_input.payloads.extend( + await data_converter.encode(input.start_signal_args) + ) + await self._populate_start_workflow_execution_request(req, input) + # If this signal-with-start is issued from inside a Nexus operation handler (but not the + # nexus-backing workflow), forward the inbound Nexus task links so both the callee's + # WorkflowExecutionStarted and WorkflowExecutionSignaled events link back to the caller. + if not temporalio.nexus._operation_context._in_nexus_backing_start_context(): + nexus_ctx = ( + temporalio.nexus._operation_context._try_start_operation_context() + ) + if nexus_ctx is not None: + req.links.extend(nexus_ctx._get_request_links()) + return req + + async def _build_update_with_start_start_workflow_execution_request( + self, input: UpdateWithStartStartWorkflowInput + ) -> temporalio.api.workflowservice.v1.StartWorkflowExecutionRequest: + req = temporalio.api.workflowservice.v1.StartWorkflowExecutionRequest() + await self._populate_start_workflow_execution_request(req, input) + return req + + async def _populate_start_workflow_execution_request( + self, + req: ( + temporalio.api.workflowservice.v1.StartWorkflowExecutionRequest + | temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest + ), + input: StartWorkflowInput | UpdateWithStartStartWorkflowInput, + ) -> None: + data_converter = self._client.data_converter._with_contexts( + WorkflowSerializationContext( + namespace=self._client.namespace, + workflow_id=input.id, + ), + StorageDriverStoreContext( + target=StorageDriverWorkflowInfo( + id=input.id, type=input.workflow, namespace=self._client.namespace + ), + ), + ) + req.namespace = self._client.namespace + req.workflow_id = input.id + req.workflow_type.name = input.workflow + req.task_queue.name = input.task_queue + if input.args: + req.input.payloads.extend(await data_converter.encode(input.args)) + if input.execution_timeout is not None: + req.workflow_execution_timeout.FromTimedelta(input.execution_timeout) + if input.run_timeout is not None: + req.workflow_run_timeout.FromTimedelta(input.run_timeout) + if input.task_timeout is not None: + req.workflow_task_timeout.FromTimedelta(input.task_timeout) + req.identity = self._client.identity + req.request_id = str(uuid.uuid4()) + req.workflow_id_reuse_policy = cast( + "temporalio.api.enums.v1.WorkflowIdReusePolicy.ValueType", + int(input.id_reuse_policy), + ) + req.workflow_id_conflict_policy = cast( + "temporalio.api.enums.v1.WorkflowIdConflictPolicy.ValueType", + int(input.id_conflict_policy), + ) + + if input.retry_policy is not None: + input.retry_policy.apply_to_proto(req.retry_policy) + req.cron_schedule = input.cron_schedule + if input.memo is not None: + await data_converter._encode_memo_existing(input.memo, req.memo) + if input.search_attributes is not None: + temporalio.converter.encode_search_attributes( + input.search_attributes, req.search_attributes + ) + metadata = await _encode_user_metadata( + data_converter, input.static_summary, input.static_details + ) + if metadata is not None: + req.user_metadata.CopyFrom(metadata) + if input.start_delay is not None: + req.workflow_start_delay.FromTimedelta(input.start_delay) + if input.headers is not None: # type:ignore[reportUnnecessaryComparison] + await self._apply_headers(input.headers, req.header.fields) + if input.priority is not None: # type:ignore[reportUnnecessaryComparison] + req.priority.CopyFrom(input.priority._to_proto()) + if input.versioning_override is not None: + req.versioning_override.CopyFrom(input.versioning_override._to_proto()) + + async def cancel_workflow(self, input: CancelWorkflowInput) -> None: + await self._client.workflow_service.request_cancel_workflow_execution( + temporalio.api.workflowservice.v1.RequestCancelWorkflowExecutionRequest( + namespace=self._client.namespace, + workflow_execution=temporalio.api.common.v1.WorkflowExecution( + workflow_id=input.id, + run_id=input.run_id or "", + ), + identity=self._client.identity, + request_id=str(uuid.uuid4()), + first_execution_run_id=input.first_execution_run_id or "", + reason=input.reason, + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + + async def describe_workflow( + self, input: DescribeWorkflowInput + ) -> WorkflowExecutionDescription: + return await WorkflowExecutionDescription._from_raw_description( + await self._client.workflow_service.describe_workflow_execution( + temporalio.api.workflowservice.v1.DescribeWorkflowExecutionRequest( + namespace=self._client.namespace, + execution=temporalio.api.common.v1.WorkflowExecution( + workflow_id=input.id, + run_id=input.run_id or "", + ), + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ), + namespace=self._client.namespace, + converter=self._client.data_converter.with_context( + WorkflowSerializationContext( + namespace=self._client.namespace, + workflow_id=input.id, + ) + ), + ) + + def fetch_workflow_history_events( + self, input: FetchWorkflowHistoryEventsInput + ) -> WorkflowHistoryEventAsyncIterator: + return WorkflowHistoryEventAsyncIterator(self._client, input) + + def list_workflows( + self, input: ListWorkflowsInput + ) -> WorkflowExecutionAsyncIterator: + return WorkflowExecutionAsyncIterator(self._client, input) + + async def count_workflows( + self, input: CountWorkflowsInput + ) -> WorkflowExecutionCount: + return WorkflowExecutionCount._from_raw( + await self._client.workflow_service.count_workflow_executions( + temporalio.api.workflowservice.v1.CountWorkflowExecutionsRequest( + namespace=self._client.namespace, + query=input.query or "", + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + ) + + async def query_workflow(self, input: QueryWorkflowInput) -> Any: + data_converter = self._client.data_converter._with_contexts( + WorkflowSerializationContext( + namespace=self._client.namespace, + workflow_id=input.id, + ), + StorageDriverStoreContext( + target=StorageDriverWorkflowInfo( + id=input.id, + run_id=input.run_id or None, + namespace=self._client.namespace, + ), + ), + ) + req = temporalio.api.workflowservice.v1.QueryWorkflowRequest( + namespace=self._client.namespace, + execution=temporalio.api.common.v1.WorkflowExecution( + workflow_id=input.id, + run_id=input.run_id or "", + ), + ) + if input.reject_condition: + req.query_reject_condition = cast( + "temporalio.api.enums.v1.QueryRejectCondition.ValueType", + int(input.reject_condition), + ) + req.query.query_type = input.query + if input.args: + req.query.query_args.payloads.extend( + await data_converter.encode(input.args) + ) + if input.headers is not None: # type:ignore[reportUnnecessaryComparison] + await self._apply_headers(input.headers, req.query.header.fields) + try: + resp = await self._client.workflow_service.query_workflow( + req, + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + except RPCError as err: + # If the status is INVALID_ARGUMENT, we can assume it's a query + # failed error + if err.status == RPCStatusCode.INVALID_ARGUMENT: + raise WorkflowQueryFailedError(err.message) + else: + raise + if resp.HasField("query_rejected"): + raise WorkflowQueryRejectedError( + WorkflowExecutionStatus(resp.query_rejected.status) + if resp.query_rejected.status + else None + ) + if not resp.query_result.payloads: + return None + type_hints = [input.ret_type] if input.ret_type else None + results = await data_converter.decode(resp.query_result.payloads, type_hints) + if not results: + return None + elif len(results) > 1: + warnings.warn(f"Expected single query result, got {len(results)}") + return results[0] + + async def signal_workflow(self, input: SignalWorkflowInput) -> None: + data_converter = self._client.data_converter._with_contexts( + WorkflowSerializationContext( + namespace=self._client.namespace, + workflow_id=input.id, + ), + StorageDriverStoreContext( + target=StorageDriverWorkflowInfo( + id=input.id, + run_id=input.run_id or None, + namespace=self._client.namespace, + ), + ), + ) + req = temporalio.api.workflowservice.v1.SignalWorkflowExecutionRequest( + namespace=self._client.namespace, + workflow_execution=temporalio.api.common.v1.WorkflowExecution( + workflow_id=input.id, + run_id=input.run_id or "", + ), + signal_name=input.signal, + identity=self._client.identity, + request_id=str(uuid.uuid4()), + ) + if input.args: + req.input.payloads.extend(await data_converter.encode(input.args)) + if input.headers is not None: # type:ignore[reportUnnecessaryComparison] + await self._apply_headers(input.headers, req.header.fields) + # If this signal is issued from inside a Nexus operation handler, forward the inbound + # Nexus task links so the WorkflowExecutionSignaled event links back to the caller. + nexus_ctx = temporalio.nexus._operation_context._try_start_operation_context() + if nexus_ctx is not None: + req.links.extend(nexus_ctx._get_request_links()) + resp = await self._client.workflow_service.signal_workflow_execution( + req, retry=True, metadata=input.rpc_metadata, timeout=input.rpc_timeout + ) + # Server >= 1.31 with EnableCHASMSignalBacklinks returns a response link pointing at the + # signal event; older servers leave it unset. Propagate when present. + if nexus_ctx is not None and resp.HasField("link"): + nexus_ctx._add_response_link(resp.link) + + async def terminate_workflow(self, input: TerminateWorkflowInput) -> None: + data_converter = self._client.data_converter._with_contexts( + WorkflowSerializationContext( + namespace=self._client.namespace, + workflow_id=input.id, + ), + StorageDriverStoreContext( + target=StorageDriverWorkflowInfo( + id=input.id, + run_id=input.run_id or None, + namespace=self._client.namespace, + ), + ), + ) + req = temporalio.api.workflowservice.v1.TerminateWorkflowExecutionRequest( + namespace=self._client.namespace, + workflow_execution=temporalio.api.common.v1.WorkflowExecution( + workflow_id=input.id, + run_id=input.run_id or "", + ), + reason=input.reason or "", + identity=self._client.identity, + first_execution_run_id=input.first_execution_run_id or "", + ) + if input.args: + req.details.payloads.extend(await data_converter.encode(input.args)) + await self._client.workflow_service.terminate_workflow_execution( + req, retry=True, metadata=input.rpc_metadata, timeout=input.rpc_timeout + ) + + async def start_activity(self, input: StartActivityInput) -> ActivityHandle[Any]: + """Start an activity and return a handle to it.""" + if not (input.start_to_close_timeout or input.schedule_to_close_timeout): + raise ValueError( + "Activity must have start_to_close_timeout or schedule_to_close_timeout" + ) + if input.start_delay is not None and input.start_delay < timedelta(0): + raise ValueError("start_delay must be non-negative") + req = await self._build_start_activity_execution_request(input) + + resp: temporalio.api.workflowservice.v1.StartActivityExecutionResponse + try: + resp = await self._client.workflow_service.start_activity_execution( + req, + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + except RPCError as err: + # If the status is ALREADY_EXISTS and the details can be extracted + # as already started, use a different exception + if err.status == RPCStatusCode.ALREADY_EXISTS and err.grpc_status.details: + details = temporalio.api.errordetails.v1.ActivityExecutionAlreadyStartedFailure() + if err.grpc_status.details[0].Unpack(details): + raise temporalio.exceptions.ActivityAlreadyStartedError( + input.id, input.activity_type, run_id=details.run_id + ) + raise + + # Apply StartActivity response elements to the current Nexus context. + # No-ops if called outside a Nexus context. + temporalio.nexus._operation_context._apply_start_activity_response_to_nexus_context( + input.id, resp + ) + + return ActivityHandle( + self._client, + input.id, + run_id=resp.run_id, + result_type=input.result_type, + ) + + async def _build_start_activity_execution_request( + self, input: StartActivityInput + ) -> temporalio.api.workflowservice.v1.StartActivityExecutionRequest: + """Build StartActivityExecutionRequest from input.""" + data_converter = self._client.data_converter._with_contexts( + ActivitySerializationContext( + namespace=self._client.namespace, + activity_id=input.id, + activity_type=input.activity_type, + activity_task_queue=input.task_queue, + is_local=False, + workflow_id=None, + workflow_type=None, + ), + StorageDriverStoreContext( + target=StorageDriverActivityInfo( + id=input.id, + type=input.activity_type, + namespace=self._client.namespace, + ), + ), + ) + + req = temporalio.api.workflowservice.v1.StartActivityExecutionRequest( + namespace=self._client.namespace, + identity=self._client.identity, + activity_id=input.id, + activity_type=temporalio.api.common.v1.ActivityType( + name=input.activity_type + ), + task_queue=temporalio.api.taskqueue.v1.TaskQueue(name=input.task_queue), + id_reuse_policy=cast( + "temporalio.api.enums.v1.ActivityIdReusePolicy.ValueType", + int(input.id_reuse_policy), + ), + id_conflict_policy=cast( + "temporalio.api.enums.v1.ActivityIdConflictPolicy.ValueType", + int(input.id_conflict_policy), + ), + ) + + if input.schedule_to_close_timeout is not None: + req.schedule_to_close_timeout.FromTimedelta(input.schedule_to_close_timeout) + if input.start_to_close_timeout is not None: + req.start_to_close_timeout.FromTimedelta(input.start_to_close_timeout) + if input.schedule_to_start_timeout is not None: + req.schedule_to_start_timeout.FromTimedelta(input.schedule_to_start_timeout) + if input.heartbeat_timeout is not None: + req.heartbeat_timeout.FromTimedelta(input.heartbeat_timeout) + if input.start_delay is not None: + req.start_delay.FromTimedelta(input.start_delay) + if input.retry_policy is not None: + input.retry_policy.apply_to_proto(req.retry_policy) + + # Set input payloads + if input.args: + req.input.payloads.extend(await data_converter.encode(input.args)) + + # Set search attributes + if input.search_attributes is not None: + temporalio.converter.encode_search_attributes( + input.search_attributes, req.search_attributes + ) + + # Set user metadata + metadata = await _encode_user_metadata(data_converter, input.summary, None) + if metadata is not None: + req.user_metadata.CopyFrom(metadata) + + # Set headers + if input.headers: + await self._apply_headers(input.headers, req.header.fields) + + # Set priority + req.priority.CopyFrom(input.priority._to_proto()) + + # Add request_id, links, and completion callbacks from the Nexus context + # if not in a Nexus context, this is a no-op + temporalio.nexus._operation_context._apply_nexus_context_to_start_activity_request( + req + ) + + return req + + async def cancel_activity(self, input: CancelActivityInput) -> None: + """Cancel an activity.""" + await self._client.workflow_service.request_cancel_activity_execution( + temporalio.api.workflowservice.v1.RequestCancelActivityExecutionRequest( + namespace=self._client.namespace, + activity_id=input.activity_id, + run_id=input.activity_run_id or "", + identity=self._client.identity, + request_id=str(uuid.uuid4()), + reason=input.reason or "", + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + + async def terminate_activity(self, input: TerminateActivityInput) -> None: + """Terminate an activity.""" + await self._client.workflow_service.terminate_activity_execution( + temporalio.api.workflowservice.v1.TerminateActivityExecutionRequest( + namespace=self._client.namespace, + activity_id=input.activity_id, + run_id=input.activity_run_id or "", + reason=input.reason or "", + identity=self._client.identity, + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + + async def describe_activity( + self, input: DescribeActivityInput + ) -> ActivityExecutionDescription: + """Describe an activity.""" + resp = await self._client.workflow_service.describe_activity_execution( + temporalio.api.workflowservice.v1.DescribeActivityExecutionRequest( + namespace=self._client.namespace, + activity_id=input.activity_id, + run_id=input.activity_run_id or "", + long_poll_token=input.long_poll_token or b"", + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + return await ActivityExecutionDescription._from_execution_info( + info=resp.info, + long_poll_token=resp.long_poll_token or None, + namespace=self._client.namespace, + data_converter=self._client.data_converter.with_context( + ActivitySerializationContext( + namespace=self._client.namespace, + activity_id=resp.info.activity_id, + activity_task_queue=resp.info.task_queue, + activity_type=resp.info.activity_type.name, + workflow_id=None, + workflow_type=None, + is_local=False, + ) + ), + callbacks=resp.callbacks, + ) + + def list_activities( + self, input: ListActivitiesInput + ) -> ActivityExecutionAsyncIterator: + return ActivityExecutionAsyncIterator(self._client, input) + + async def count_activities( + self, input: CountActivitiesInput + ) -> ActivityExecutionCount: + return ActivityExecutionCount._from_raw( + await self._client.workflow_service.count_activity_executions( + temporalio.api.workflowservice.v1.CountActivityExecutionsRequest( + namespace=self._client.namespace, + query=input.query or "", + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + ) + + async def start_workflow_update( + self, input: StartWorkflowUpdateInput + ) -> WorkflowUpdateHandle[Any]: + workflow_id = input.id + req = await self._build_update_workflow_execution_request(input, workflow_id) + + # Repeatedly try to invoke UpdateWorkflowExecution until the update is durable. + resp: temporalio.api.workflowservice.v1.UpdateWorkflowExecutionResponse + while True: + try: + resp = await self._client.workflow_service.update_workflow_execution( + req, + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + except RPCError as err: + if ( + err.status == RPCStatusCode.DEADLINE_EXCEEDED + or err.status == RPCStatusCode.CANCELLED + ): + raise WorkflowUpdateRPCTimeoutOrCancelledError() from err + else: + raise + except asyncio.CancelledError as err: + raise WorkflowUpdateRPCTimeoutOrCancelledError() from err + if ( + resp.stage + >= temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED + ): + break + + # Add response link if its a Nexus operation + nexus_ctx = temporalio.nexus._operation_context._try_start_operation_context() + if nexus_ctx is not None and resp.HasField("link"): + nexus_ctx._add_response_link(resp.link) + + # Build the handle. If the user's wait stage is COMPLETED, make sure we + # poll for result. + handle: WorkflowUpdateHandle[Any] = WorkflowUpdateHandle( + client=self._client, + id=req.request.meta.update_id, + workflow_id=workflow_id, + workflow_run_id=resp.update_ref.workflow_execution.run_id, + result_type=input.ret_type, + ) + if resp.HasField("outcome"): + handle._known_outcome = resp.outcome + if input.wait_for_stage == WorkflowUpdateStage.COMPLETED: + await handle._poll_until_outcome() + return handle + + async def _build_update_workflow_execution_request( + self, + input: StartWorkflowUpdateInput | UpdateWithStartUpdateWorkflowInput, + workflow_id: str, + ) -> temporalio.api.workflowservice.v1.UpdateWorkflowExecutionRequest: + data_converter = self._client.data_converter._with_contexts( + WorkflowSerializationContext( + namespace=self._client.namespace, + workflow_id=workflow_id, + ), + StorageDriverStoreContext( + target=StorageDriverWorkflowInfo( + id=workflow_id, + run_id=(input.run_id or None) + if isinstance(input, StartWorkflowUpdateInput) + else None, + namespace=self._client.namespace, + ), + ), + ) + run_id, first_execution_run_id = ( + ( + input.run_id, + input.first_execution_run_id, + ) + if isinstance(input, StartWorkflowUpdateInput) + else (None, None) + ) + req = temporalio.api.workflowservice.v1.UpdateWorkflowExecutionRequest( + namespace=self._client.namespace, + workflow_execution=temporalio.api.common.v1.WorkflowExecution( + workflow_id=workflow_id, + run_id=run_id or "", + ), + first_execution_run_id=first_execution_run_id or "", + request=temporalio.api.update.v1.Request( + meta=temporalio.api.update.v1.Meta( + update_id=input.update_id or str(uuid.uuid4()), + identity=self._client.identity, + ), + input=temporalio.api.update.v1.Input( + name=input.update, + ), + ), + wait_policy=temporalio.api.update.v1.WaitPolicy( + lifecycle_stage=temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.ValueType( + input.wait_for_stage + ) + ), + ) + # Only set Nexus fields for StartWorkflowUpdateInput, skip for UpdateWithStartUpdateWorkflowInput + if isinstance(input, StartWorkflowUpdateInput): + if input.request_id: + req.request.request_id = input.request_id + if input.links: + req.request.links.extend(input.links) + if input.callbacks: + req.request.completion_callbacks.extend( + temporalio.api.common.v1.Callback( + nexus=temporalio.api.common.v1.Callback.Nexus( + url=callback.url, + header=callback.headers, + ), + links=input.links or [], + ) + for callback in input.callbacks + ) + if input.args: + req.request.input.args.payloads.extend( + await data_converter.encode(input.args) + ) + if input.headers is not None: # type:ignore[reportUnnecessaryComparison] + await self._apply_headers(input.headers, req.request.input.header.fields) + return req + + async def start_update_with_start_workflow( + self, input: StartWorkflowUpdateWithStartInput + ) -> WorkflowUpdateHandle[Any]: + seen_start = False + + def on_start( + start_response: temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse, + ): + nonlocal seen_start + if not seen_start: + input._on_start(start_response) + seen_start = True + + err: BaseException | None = None + + try: + return await self._start_workflow_update_with_start( + input.start_workflow_input, + input.update_workflow_input, + input.rpc_metadata, + input.rpc_timeout, + on_start, + ) + except asyncio.CancelledError as _err: + err = _err + raise WorkflowUpdateRPCTimeoutOrCancelledError() from err + except RPCError as _err: + err = _err + if err.status in [ + RPCStatusCode.DEADLINE_EXCEEDED, + RPCStatusCode.CANCELLED, + ]: + raise WorkflowUpdateRPCTimeoutOrCancelledError() from err + else: + multiop_failure = ( + temporalio.api.errordetails.v1.MultiOperationExecutionFailure() + ) + if err.grpc_status.details and err.grpc_status.details[0].Unpack( + multiop_failure + ): + status = next( + ( + st + for st in multiop_failure.statuses + if ( + st.code != RPCStatusCode.OK + and not ( + st.details + and st.details[0].Is( + temporalio.api.failure.v1.MultiOperationExecutionAborted.DESCRIPTOR + ) + ) + ) + ), + None, + ) + if status and status.code in list(RPCStatusCode): + if ( + status.code == RPCStatusCode.ALREADY_EXISTS + and status.details + ): + details = temporalio.api.errordetails.v1.WorkflowExecutionAlreadyStartedFailure() + if status.details[0].Unpack(details): + err = temporalio.exceptions.WorkflowAlreadyStartedError( + input.start_workflow_input.id, + input.start_workflow_input.workflow, + run_id=details.run_id, + ) + else: + err = RPCError( + status.message, + RPCStatusCode(status.code), + err.raw_grpc_status, + ) + raise err + finally: + if err and not seen_start: + input._on_start_error(err) + + async def _start_workflow_update_with_start( + self, + start_input: UpdateWithStartStartWorkflowInput, + update_input: UpdateWithStartUpdateWorkflowInput, + rpc_metadata: Mapping[str, str | bytes], + rpc_timeout: timedelta | None, + on_start: Callable[ + [temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse], None + ], + ) -> WorkflowUpdateHandle[Any]: + start_req = ( + await self._build_update_with_start_start_workflow_execution_request( + start_input + ) + ) + update_req = await self._build_update_workflow_execution_request( + update_input, workflow_id=start_input.id + ) + multiop_req = temporalio.api.workflowservice.v1.ExecuteMultiOperationRequest( + namespace=self._client.namespace, + operations=[ + temporalio.api.workflowservice.v1.ExecuteMultiOperationRequest.Operation( + start_workflow=start_req + ), + temporalio.api.workflowservice.v1.ExecuteMultiOperationRequest.Operation( + update_workflow=update_req + ), + ], + ) + + # Repeatedly try to invoke ExecuteMultiOperation until the update is durable + while True: + multiop_response = ( + await self._client.workflow_service.execute_multi_operation( + multiop_req, + retry=True, + metadata=rpc_metadata, + timeout=rpc_timeout, + ) + ) + start_response = multiop_response.responses[0].start_workflow + update_response = multiop_response.responses[1].update_workflow + on_start(start_response) + known_outcome = ( + update_response.outcome if update_response.HasField("outcome") else None + ) + if ( + update_response.stage + >= temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED + ): + break + + handle: WorkflowUpdateHandle[Any] = WorkflowUpdateHandle( + client=self._client, + id=update_req.request.meta.update_id, + workflow_id=start_input.id, + workflow_run_id=start_response.run_id, + known_outcome=known_outcome, + result_type=update_input.ret_type, + ) + if update_input.wait_for_stage == WorkflowUpdateStage.COMPLETED: + await handle._poll_until_outcome() + + return handle + + ### Async activity calls + + def _get_async_activity_store_context( + self, id_or_token: AsyncActivityIDReference | bytes + ) -> StorageDriverStoreContext: + if isinstance(id_or_token, AsyncActivityIDReference): + if id_or_token.workflow_id: + return StorageDriverStoreContext( + target=StorageDriverWorkflowInfo( + id=id_or_token.workflow_id or None, + run_id=id_or_token.run_id or None, + namespace=self._client.namespace, + ), + ) + return StorageDriverStoreContext( + target=StorageDriverActivityInfo( + id=id_or_token.activity_id, + run_id=id_or_token.run_id or None, + namespace=self._client.namespace, + ), + ) + else: + return StorageDriverStoreContext(target=None) + + async def heartbeat_async_activity( + self, input: HeartbeatAsyncActivityInput + ) -> None: + data_converter = ( + input.data_converter_override or self._client.data_converter + )._with_store_context(self._get_async_activity_store_context(input.id_or_token)) + details = ( + None + if not input.details + else await data_converter.encode_wrapper(input.details) + ) + if isinstance(input.id_or_token, AsyncActivityIDReference): + resp_by_id = await self._client.workflow_service.record_activity_task_heartbeat_by_id( + temporalio.api.workflowservice.v1.RecordActivityTaskHeartbeatByIdRequest( + workflow_id=input.id_or_token.workflow_id or "", + run_id=input.id_or_token.run_id or "", + activity_id=input.id_or_token.activity_id, + namespace=self._client.namespace, + identity=self._client.identity, + details=details, + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + if ( + resp_by_id.cancel_requested + or resp_by_id.activity_paused + or resp_by_id.activity_reset + ): + raise AsyncActivityCancelledError( + details=ActivityCancellationDetails( + cancel_requested=resp_by_id.cancel_requested, + paused=resp_by_id.activity_paused, + reset=resp_by_id.activity_reset, + ) + ) + + else: + resp = await self._client.workflow_service.record_activity_task_heartbeat( + temporalio.api.workflowservice.v1.RecordActivityTaskHeartbeatRequest( + task_token=input.id_or_token, + namespace=self._client.namespace, + identity=self._client.identity, + details=details, + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + if resp.cancel_requested or resp.activity_paused: + raise AsyncActivityCancelledError( + details=ActivityCancellationDetails( + cancel_requested=resp.cancel_requested, + paused=resp.activity_paused, + reset=resp.activity_reset, + ) + ) + + async def complete_async_activity(self, input: CompleteAsyncActivityInput) -> None: + data_converter = ( + input.data_converter_override or self._client.data_converter + )._with_store_context(self._get_async_activity_store_context(input.id_or_token)) + result = ( + None + if input.result is temporalio.common._arg_unset + else await data_converter.encode_wrapper([input.result]) + ) + if isinstance(input.id_or_token, AsyncActivityIDReference): + await self._client.workflow_service.respond_activity_task_completed_by_id( + temporalio.api.workflowservice.v1.RespondActivityTaskCompletedByIdRequest( + workflow_id=input.id_or_token.workflow_id or "", + run_id=input.id_or_token.run_id or "", + activity_id=input.id_or_token.activity_id, + namespace=self._client.namespace, + identity=self._client.identity, + result=result, + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + else: + await self._client.workflow_service.respond_activity_task_completed( + temporalio.api.workflowservice.v1.RespondActivityTaskCompletedRequest( + task_token=input.id_or_token, + namespace=self._client.namespace, + identity=self._client.identity, + result=result, + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + + async def fail_async_activity(self, input: FailAsyncActivityInput) -> None: + data_converter = ( + input.data_converter_override or self._client.data_converter + )._with_store_context(self._get_async_activity_store_context(input.id_or_token)) + + failure = temporalio.api.failure.v1.Failure() + await data_converter.encode_failure(input.error, failure) + last_heartbeat_details = ( + await data_converter.encode_wrapper(input.last_heartbeat_details) + if input.last_heartbeat_details + else None + ) + if isinstance(input.id_or_token, AsyncActivityIDReference): + await self._client.workflow_service.respond_activity_task_failed_by_id( + temporalio.api.workflowservice.v1.RespondActivityTaskFailedByIdRequest( + workflow_id=input.id_or_token.workflow_id or "", + run_id=input.id_or_token.run_id or "", + activity_id=input.id_or_token.activity_id, + namespace=self._client.namespace, + identity=self._client.identity, + failure=failure, + last_heartbeat_details=last_heartbeat_details, + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + else: + await self._client.workflow_service.respond_activity_task_failed( + temporalio.api.workflowservice.v1.RespondActivityTaskFailedRequest( + task_token=input.id_or_token, + namespace=self._client.namespace, + identity=self._client.identity, + failure=failure, + last_heartbeat_details=last_heartbeat_details, + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + + async def report_cancellation_async_activity( + self, input: ReportCancellationAsyncActivityInput + ) -> None: + data_converter = ( + input.data_converter_override or self._client.data_converter + )._with_store_context(self._get_async_activity_store_context(input.id_or_token)) + details = ( + None + if not input.details + else await data_converter.encode_wrapper(input.details) + ) + if isinstance(input.id_or_token, AsyncActivityIDReference): + await self._client.workflow_service.respond_activity_task_canceled_by_id( + temporalio.api.workflowservice.v1.RespondActivityTaskCanceledByIdRequest( + workflow_id=input.id_or_token.workflow_id or "", + run_id=input.id_or_token.run_id or "", + activity_id=input.id_or_token.activity_id, + namespace=self._client.namespace, + identity=self._client.identity, + details=details, + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + else: + await self._client.workflow_service.respond_activity_task_canceled( + temporalio.api.workflowservice.v1.RespondActivityTaskCanceledRequest( + task_token=input.id_or_token, + namespace=self._client.namespace, + identity=self._client.identity, + details=details, + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + + ### Schedule calls + + async def create_schedule(self, input: CreateScheduleInput) -> ScheduleHandle: + # Limited actions must be false if remaining actions is 0 and must be + # true if remaining actions is non-zero + if ( + input.schedule.state.limited_actions + and not input.schedule.state.remaining_actions + ): + raise ValueError( + "Must set limited actions to false if there are no remaining actions set" + ) + if ( + not input.schedule.state.limited_actions + and input.schedule.state.remaining_actions + ): + raise ValueError( + "Must set limited actions to true if there are remaining actions set" + ) + + initial_patch: temporalio.api.schedule.v1.SchedulePatch | None = None + if input.trigger_immediately or input.backfill: + initial_patch = temporalio.api.schedule.v1.SchedulePatch( + trigger_immediately=temporalio.api.schedule.v1.TriggerImmediatelyRequest( + overlap_policy=temporalio.api.enums.v1.ScheduleOverlapPolicy.ValueType( + input.schedule.policy.overlap + ), + ) + if input.trigger_immediately + else None, + backfill_request=[b._to_proto() for b in input.backfill] + if input.backfill + else None, + ) + try: + request = temporalio.api.workflowservice.v1.CreateScheduleRequest( + namespace=self._client.namespace, + schedule_id=input.id, + schedule=await input.schedule._to_proto(self._client), + initial_patch=initial_patch, + identity=self._client.identity, + request_id=str(uuid.uuid4()), + memo=await self._client.data_converter._encode_memo(input.memo) + if input.memo + else None, + ) + if input.search_attributes: + temporalio.converter.encode_search_attributes( + input.search_attributes, request.search_attributes + ) + await self._client.workflow_service.create_schedule( + request, + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + except RPCError as err: + already_started = ( + err.status == RPCStatusCode.ALREADY_EXISTS + and err.grpc_status.details + and err.grpc_status.details[0].Is( + temporalio.api.errordetails.v1.WorkflowExecutionAlreadyStartedFailure.DESCRIPTOR + ) + ) + if already_started: + raise ScheduleAlreadyRunningError() + raise + return ScheduleHandle(self._client, input.id) + + def list_schedules(self, input: ListSchedulesInput) -> ScheduleAsyncIterator: + return ScheduleAsyncIterator(self._client, input) + + async def backfill_schedule(self, input: BackfillScheduleInput) -> None: + await self._client.workflow_service.patch_schedule( + temporalio.api.workflowservice.v1.PatchScheduleRequest( + namespace=self._client.namespace, + schedule_id=input.id, + patch=temporalio.api.schedule.v1.SchedulePatch( + backfill_request=[b._to_proto() for b in input.backfills], + ), + identity=self._client.identity, + request_id=str(uuid.uuid4()), + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + + async def delete_schedule(self, input: DeleteScheduleInput) -> None: + await self._client.workflow_service.delete_schedule( + temporalio.api.workflowservice.v1.DeleteScheduleRequest( + namespace=self._client.namespace, + schedule_id=input.id, + identity=self._client.identity, + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + + async def describe_schedule( + self, input: DescribeScheduleInput + ) -> ScheduleDescription: + return ScheduleDescription._from_proto( + input.id, + await self._client.workflow_service.describe_schedule( + temporalio.api.workflowservice.v1.DescribeScheduleRequest( + namespace=self._client.namespace, + schedule_id=input.id, + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ), + self._client.data_converter, + ) + + async def pause_schedule(self, input: PauseScheduleInput) -> None: + await self._client.workflow_service.patch_schedule( + temporalio.api.workflowservice.v1.PatchScheduleRequest( + namespace=self._client.namespace, + schedule_id=input.id, + patch=temporalio.api.schedule.v1.SchedulePatch( + pause=input.note or "Paused via Python SDK", + ), + identity=self._client.identity, + request_id=str(uuid.uuid4()), + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + + async def trigger_schedule(self, input: TriggerScheduleInput) -> None: + overlap_policy = temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_UNSPECIFIED + if input.overlap: + overlap_policy = temporalio.api.enums.v1.ScheduleOverlapPolicy.ValueType( + input.overlap + ) + await self._client.workflow_service.patch_schedule( + temporalio.api.workflowservice.v1.PatchScheduleRequest( + namespace=self._client.namespace, + schedule_id=input.id, + patch=temporalio.api.schedule.v1.SchedulePatch( + trigger_immediately=temporalio.api.schedule.v1.TriggerImmediatelyRequest( + overlap_policy=overlap_policy, + ), + ), + identity=self._client.identity, + request_id=str(uuid.uuid4()), + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + + async def unpause_schedule(self, input: UnpauseScheduleInput) -> None: + await self._client.workflow_service.patch_schedule( + temporalio.api.workflowservice.v1.PatchScheduleRequest( + namespace=self._client.namespace, + schedule_id=input.id, + patch=temporalio.api.schedule.v1.SchedulePatch( + unpause=input.note or "Unpaused via Python SDK", + ), + identity=self._client.identity, + request_id=str(uuid.uuid4()), + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + + async def update_schedule(self, input: UpdateScheduleInput) -> None: + # TODO(cretz): This is supposed to be a retry-conflict loop, but we do + # not yet have a way to know update failure is due to conflict token + # mismatch + update = input.updater( + ScheduleUpdateInput( + description=ScheduleDescription._from_proto( + input.id, + await self._client.workflow_service.describe_schedule( + temporalio.api.workflowservice.v1.DescribeScheduleRequest( + namespace=self._client.namespace, + schedule_id=input.id, + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ), + self._client.data_converter, + ) + ) + ) + if inspect.iscoroutine(update): + update = await update + if not update: + return + assert isinstance(update, ScheduleUpdate) + request = temporalio.api.workflowservice.v1.UpdateScheduleRequest( + namespace=self._client.namespace, + schedule_id=input.id, + schedule=await update.schedule._to_proto(self._client), + identity=self._client.identity, + request_id=str(uuid.uuid4()), + ) + if update.search_attributes is not None: + request.search_attributes.indexed_fields.clear() # Ensure that we at least create an empty map + temporalio.converter.encode_search_attributes( + update.search_attributes, request.search_attributes + ) + await self._client.workflow_service.update_schedule( + request, + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + + async def update_worker_build_id_compatibility( + self, input: UpdateWorkerBuildIdCompatibilityInput + ) -> None: + req = input.operation._as_partial_proto() + req.namespace = self._client.namespace + req.task_queue = input.task_queue + await self._client.workflow_service.update_worker_build_id_compatibility( + req, retry=True, metadata=input.rpc_metadata, timeout=input.rpc_timeout + ) + + async def get_worker_build_id_compatibility( + self, input: GetWorkerBuildIdCompatibilityInput + ) -> WorkerBuildIdVersionSets: + req = temporalio.api.workflowservice.v1.GetWorkerBuildIdCompatibilityRequest( + namespace=self._client.namespace, + task_queue=input.task_queue, + max_sets=input.max_sets or 0, + ) + resp = await self._client.workflow_service.get_worker_build_id_compatibility( + req, retry=True, metadata=input.rpc_metadata, timeout=input.rpc_timeout + ) + return WorkerBuildIdVersionSets._from_proto(resp) + + async def get_worker_task_reachability( + self, input: GetWorkerTaskReachabilityInput + ) -> WorkerTaskReachability: + req = temporalio.api.workflowservice.v1.GetWorkerTaskReachabilityRequest( + namespace=self._client.namespace, + build_ids=input.build_ids, + task_queues=input.task_queues, + reachability=input.reachability._to_proto() + if input.reachability + else temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_UNSPECIFIED, + ) + resp = await self._client.workflow_service.get_worker_task_reachability( + req, retry=True, metadata=input.rpc_metadata, timeout=input.rpc_timeout + ) + return WorkerTaskReachability._from_proto(resp) + + ### Nexus operation calls + + async def start_nexus_operation( + self, input: StartNexusOperationInput + ) -> NexusOperationHandle[Any]: + """Start a nexus operation and return a handle to it.""" + req = temporalio.api.workflowservice.v1.StartNexusOperationExecutionRequest( + namespace=self._client.namespace, + identity=self._client.identity, + request_id=str(uuid.uuid4()), + operation_id=input.id, + endpoint=input.endpoint, + service=input.service, + operation=input.operation, + id_reuse_policy=cast( + "temporalio.api.enums.v1.NexusOperationIdReusePolicy.ValueType", + int(input.id_reuse_policy), + ), + id_conflict_policy=cast( + "temporalio.api.enums.v1.NexusOperationIdConflictPolicy.ValueType", + int(input.id_conflict_policy), + ), + ) + + if input.schedule_to_close_timeout is not None: + req.schedule_to_close_timeout.FromTimedelta(input.schedule_to_close_timeout) + if input.schedule_to_start_timeout is not None: + req.schedule_to_start_timeout.FromTimedelta(input.schedule_to_start_timeout) + if input.start_to_close_timeout is not None: + req.start_to_close_timeout.FromTimedelta(input.start_to_close_timeout) + + # Set input payload + encoded = await self._client.data_converter.encode([input.arg]) + if encoded: + req.input.CopyFrom(encoded[0]) + + # Set search attributes + if input.search_attributes is not None: + temporalio.converter.encode_search_attributes( + input.search_attributes, req.search_attributes + ) + + # Set user metadata + metadata = await _encode_user_metadata( + self._client.data_converter, input.summary, None + ) + if metadata is not None: + req.user_metadata.CopyFrom(metadata) + + # Set nexus headers + if input.headers: + for k, v in input.headers.items(): + req.nexus_header[k] = v + + resp: temporalio.api.workflowservice.v1.StartNexusOperationExecutionResponse + try: + resp = await self._client.workflow_service.start_nexus_operation_execution( + req, + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + except RPCError as err: + if err.status == RPCStatusCode.ALREADY_EXISTS and err.grpc_status.details: + details = temporalio.api.errordetails.v1.NexusOperationExecutionAlreadyStartedFailure() + if err.grpc_status.details[0].Unpack(details): + raise temporalio.exceptions.NexusOperationAlreadyStartedError( + input.id, run_id=details.run_id + ) + raise + return NexusOperationHandle( + self._client, + input.id, + run_id=resp.run_id or None, + result_type=input.result_type, + endpoint=input.endpoint, + service=input.service, + ) + + async def describe_nexus_operation( + self, input: DescribeNexusOperationInput + ) -> NexusOperationExecutionDescription: + """Describe a nexus operation.""" + req = temporalio.api.workflowservice.v1.DescribeNexusOperationExecutionRequest( + namespace=self._client.namespace, + operation_id=input.operation_id, + run_id=input.run_id or "", + ) + resp = await self._client.workflow_service.describe_nexus_operation_execution( + req=req, + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + return await NexusOperationExecutionDescription._from_execution_info( + info=resp.info, + data_converter=self._client.data_converter, + ) + + async def get_nexus_operation_result( + self, input: GetNexusOperationResultInput + ) -> Any: + """Poll for nexus operation result until it's available.""" + req = temporalio.api.workflowservice.v1.PollNexusOperationExecutionRequest( + namespace=self._client.namespace, + operation_id=input.operation_id, + run_id=input.run_id or "", + wait_stage=temporalio.api.enums.v1.NexusOperationWaitStage.NEXUS_OPERATION_WAIT_STAGE_CLOSED, + ) + + # Continue polling as long as we have no outcome + while True: + try: + res = ( + await self._client.workflow_service.poll_nexus_operation_execution( + req, + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + ) + match res.WhichOneof("outcome"): + case "result": + type_hints = [input.result_type] if input.result_type else None + [result] = await self._client.data_converter.decode( + [res.result], type_hints + ) + return result + + case "failure": + raise NexusOperationFailureError( + cause=await self._client.data_converter.decode_failure( + res.failure + ) + ) + + case None: + # poll again + pass + except RPCError as err: + match err.status: + case RPCStatusCode.DEADLINE_EXCEEDED: + # Deadline exceeded is expected with long polling; retry + continue + case RPCStatusCode.CANCELLED: + raise asyncio.CancelledError() from err + case _: + raise + + async def cancel_nexus_operation(self, input: CancelNexusOperationInput) -> None: + """Cancel a nexus operation.""" + await self._client.workflow_service.request_cancel_nexus_operation_execution( + temporalio.api.workflowservice.v1.RequestCancelNexusOperationExecutionRequest( + namespace=self._client.namespace, + operation_id=input.operation_id, + run_id=input.run_id or "", + identity=self._client.identity, + request_id=str(uuid.uuid4()), + reason=input.reason or "", + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + + async def terminate_nexus_operation( + self, input: TerminateNexusOperationInput + ) -> None: + """Terminate a nexus operation.""" + await self._client.workflow_service.terminate_nexus_operation_execution( + temporalio.api.workflowservice.v1.TerminateNexusOperationExecutionRequest( + namespace=self._client.namespace, + operation_id=input.operation_id, + run_id=input.run_id or "", + reason=input.reason or "", + identity=self._client.identity, + request_id=str(uuid.uuid4()), + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + + def list_nexus_operations( + self, input: ListNexusOperationsInput + ) -> NexusOperationExecutionAsyncIterator: + return NexusOperationExecutionAsyncIterator(self._client, input) + + async def count_nexus_operations( + self, input: CountNexusOperationsInput + ) -> NexusOperationExecutionCount: + return NexusOperationExecutionCount._from_raw( + await self._client.workflow_service.count_nexus_operation_executions( + temporalio.api.workflowservice.v1.CountNexusOperationExecutionsRequest( + namespace=self._client.namespace, + query=input.query or "", + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + ) + + async def _apply_headers( + self, + source: Mapping[str, temporalio.api.common.v1.Payload] | None, + dest: MessageMap[str, temporalio.api.common.v1.Payload], + ) -> None: + await _apply_headers( + source, + dest, + self._client.config(active_config=True)["header_codec_behavior"] + == HeaderCodecBehavior.CODEC, + self._client.data_converter, + ) diff --git a/temporalio/client/_interceptor.py b/temporalio/client/_interceptor.py new file mode 100644 index 000000000..a6daedd45 --- /dev/null +++ b/temporalio/client/_interceptor.py @@ -0,0 +1,981 @@ +"""Client support for accessing Temporal.""" + +from __future__ import annotations + +from collections.abc import ( + Awaitable, + Callable, + Mapping, + Sequence, +) +from dataclasses import dataclass +from datetime import timedelta +from typing import ( + TYPE_CHECKING, + Any, +) + +import temporalio.api.common.v1 +import temporalio.api.workflowservice.v1 +import temporalio.common +from temporalio.converter import ( + DataConverter, +) + +from ._callback import Callback + +if TYPE_CHECKING: + from ._activity import ( + ActivityExecutionAsyncIterator, + ActivityExecutionCount, + ActivityExecutionDescription, + ActivityHandle, + AsyncActivityIDReference, + ) + from ._nexus import ( + NexusOperationExecutionAsyncIterator, + NexusOperationExecutionCount, + NexusOperationExecutionDescription, + NexusOperationHandle, + ) + from ._schedule import ( + Schedule, + ScheduleAsyncIterator, + ScheduleBackfill, + ScheduleDescription, + ScheduleHandle, + ScheduleOverlapPolicy, + ScheduleUpdate, + ScheduleUpdateInput, + ) + from ._worker_versioning import ( + BuildIdOp, + TaskReachabilityType, + WorkerBuildIdVersionSets, + WorkerTaskReachability, + ) + from ._workflow import ( + WorkflowExecutionAsyncIterator, + WorkflowExecutionCount, + WorkflowExecutionDescription, + WorkflowHandle, + WorkflowHistoryEventAsyncIterator, + WorkflowHistoryEventFilterType, + WorkflowUpdateHandle, + WorkflowUpdateStage, + ) + + +@dataclass +class StartWorkflowInput: + """Input for :py:meth:`OutboundInterceptor.start_workflow`.""" + + workflow: str + args: Sequence[Any] + id: str + task_queue: str + execution_timeout: timedelta | None + run_timeout: timedelta | None + task_timeout: timedelta | None + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy + retry_policy: temporalio.common.RetryPolicy | None + cron_schedule: str + memo: Mapping[str, Any] | None + search_attributes: None | ( + temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes + ) + start_delay: timedelta | None + headers: Mapping[str, temporalio.api.common.v1.Payload] + start_signal: str | None + start_signal_args: Sequence[Any] + static_summary: str | None + static_details: str | None + # Type may be absent + ret_type: type | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + request_eager_start: bool + priority: temporalio.common.Priority + # The following options are experimental and unstable. + callbacks: Sequence[Callback] + links: Sequence[temporalio.api.common.v1.Link] + request_id: str | None + versioning_override: temporalio.common.VersioningOverride | None = None + + +@dataclass +class CancelWorkflowInput: + """Input for :py:meth:`OutboundInterceptor.cancel_workflow`.""" + + id: str + run_id: str | None + first_execution_run_id: str | None + reason: str + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class DescribeWorkflowInput: + """Input for :py:meth:`OutboundInterceptor.describe_workflow`.""" + + id: str + run_id: str | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class FetchWorkflowHistoryEventsInput: + """Input for :py:meth:`OutboundInterceptor.fetch_workflow_history_events`.""" + + id: str + run_id: str | None + page_size: int | None + next_page_token: bytes | None + wait_new_event: bool + event_filter_type: WorkflowHistoryEventFilterType + skip_archival: bool + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class ListWorkflowsInput: + """Input for :py:meth:`OutboundInterceptor.list_workflows`.""" + + query: str | None + page_size: int + next_page_token: bytes | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + limit: int | None + + +@dataclass +class CountWorkflowsInput: + """Input for :py:meth:`OutboundInterceptor.count_workflows`.""" + + query: str | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class QueryWorkflowInput: + """Input for :py:meth:`OutboundInterceptor.query_workflow`.""" + + id: str + run_id: str | None + query: str + args: Sequence[Any] + reject_condition: temporalio.common.QueryRejectCondition | None + headers: Mapping[str, temporalio.api.common.v1.Payload] + # Type may be absent + ret_type: type | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class SignalWorkflowInput: + """Input for :py:meth:`OutboundInterceptor.signal_workflow`.""" + + id: str + run_id: str | None + signal: str + args: Sequence[Any] + headers: Mapping[str, temporalio.api.common.v1.Payload] + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class TerminateWorkflowInput: + """Input for :py:meth:`OutboundInterceptor.terminate_workflow`.""" + + id: str + run_id: str | None + first_execution_run_id: str | None + args: Sequence[Any] + reason: str | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class StartActivityInput: + """Input for :py:meth:`OutboundInterceptor.start_activity`. + + .. warning:: + This API is experimental. + """ + + activity_type: str + args: Sequence[Any] + id: str + task_queue: str + result_type: type | None + schedule_to_close_timeout: timedelta | None + start_to_close_timeout: timedelta | None + schedule_to_start_timeout: timedelta | None + heartbeat_timeout: timedelta | None + id_reuse_policy: temporalio.common.ActivityIDReusePolicy + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy + retry_policy: temporalio.common.RetryPolicy | None + priority: temporalio.common.Priority + search_attributes: temporalio.common.TypedSearchAttributes | None + summary: str | None + start_delay: timedelta | None + headers: Mapping[str, temporalio.api.common.v1.Payload] + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class CancelActivityInput: + """Input for :py:meth:`OutboundInterceptor.cancel_activity`. + + .. warning:: + This API is experimental. + """ + + activity_id: str + activity_run_id: str | None + reason: str | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class TerminateActivityInput: + """Input for :py:meth:`OutboundInterceptor.terminate_activity`. + + .. warning:: + This API is experimental. + """ + + activity_id: str + activity_run_id: str | None + reason: str | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class DescribeActivityInput: + """Input for :py:meth:`OutboundInterceptor.describe_activity`. + + .. warning:: + This API is experimental. + """ + + activity_id: str + activity_run_id: str | None + long_poll_token: bytes | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class ListActivitiesInput: + """Input for :py:meth:`OutboundInterceptor.list_activities`. + + .. warning:: + This API is experimental. + """ + + query: str | None + page_size: int + next_page_token: bytes | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + limit: int | None + + +@dataclass +class CountActivitiesInput: + """Input for :py:meth:`OutboundInterceptor.count_activities`. + + .. warning:: + This API is experimental. + """ + + query: str | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class StartWorkflowUpdateInput: + """Input for :py:meth:`OutboundInterceptor.start_workflow_update`.""" + + id: str + run_id: str | None + first_execution_run_id: str | None + update_id: str | None + update: str + args: Sequence[Any] + wait_for_stage: WorkflowUpdateStage + headers: Mapping[str, temporalio.api.common.v1.Payload] + ret_type: type | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + # The following options are for Workflow Updates exposed as Nexus Operations. Experimental and unstable + callbacks: Sequence[Callback] | None = None + links: Sequence[temporalio.api.common.v1.Link] | None = None + request_id: str | None = None + + +@dataclass +class UpdateWithStartUpdateWorkflowInput: + """Update input for :py:meth:`OutboundInterceptor.start_update_with_start_workflow`.""" + + update_id: str | None + update: str + args: Sequence[Any] + wait_for_stage: WorkflowUpdateStage + headers: Mapping[str, temporalio.api.common.v1.Payload] + ret_type: type | None + + +@dataclass +class UpdateWithStartStartWorkflowInput: + """StartWorkflow input for :py:meth:`OutboundInterceptor.start_update_with_start_workflow`.""" + + # Similar to StartWorkflowInput but without e.g. run_id, start_signal, + # start_signal_args, request_eager_start. + + workflow: str + args: Sequence[Any] + id: str + task_queue: str + execution_timeout: timedelta | None + run_timeout: timedelta | None + task_timeout: timedelta | None + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy + retry_policy: temporalio.common.RetryPolicy | None + cron_schedule: str + memo: Mapping[str, Any] | None + search_attributes: None | ( + temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes + ) + start_delay: timedelta | None + headers: Mapping[str, temporalio.api.common.v1.Payload] + static_summary: str | None + static_details: str | None + # Type may be absent + ret_type: type | None + priority: temporalio.common.Priority + versioning_override: temporalio.common.VersioningOverride | None = None + + +@dataclass +class StartWorkflowUpdateWithStartInput: + """Input for :py:meth:`OutboundInterceptor.start_update_with_start_workflow`. + + The ``rpc_metadata`` and ``rpc_timeout`` fields are authoritative for the + ``execute_multi_operation`` gRPC call. Interceptors that wish to set RPC + metadata should modify :py:attr:`rpc_metadata` on this object. + """ + + start_workflow_input: UpdateWithStartStartWorkflowInput + update_workflow_input: UpdateWithStartUpdateWorkflowInput + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + _on_start: Callable[ + [temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse], None + ] + _on_start_error: Callable[[BaseException], None] + + +@dataclass +class HeartbeatAsyncActivityInput: + """Input for :py:meth:`OutboundInterceptor.heartbeat_async_activity`.""" + + id_or_token: AsyncActivityIDReference | bytes + details: Sequence[Any] + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + data_converter_override: DataConverter | None = None + + +@dataclass +class CompleteAsyncActivityInput: + """Input for :py:meth:`OutboundInterceptor.complete_async_activity`.""" + + id_or_token: AsyncActivityIDReference | bytes + result: Any | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + data_converter_override: DataConverter | None = None + + +@dataclass +class FailAsyncActivityInput: + """Input for :py:meth:`OutboundInterceptor.fail_async_activity`.""" + + id_or_token: AsyncActivityIDReference | bytes + error: Exception + last_heartbeat_details: Sequence[Any] + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + data_converter_override: DataConverter | None = None + + +@dataclass +class ReportCancellationAsyncActivityInput: + """Input for :py:meth:`OutboundInterceptor.report_cancellation_async_activity`.""" + + id_or_token: AsyncActivityIDReference | bytes + details: Sequence[Any] + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + data_converter_override: DataConverter | None = None + + +@dataclass +class CreateScheduleInput: + """Input for :py:meth:`OutboundInterceptor.create_schedule`.""" + + id: str + schedule: Schedule + trigger_immediately: bool + backfill: Sequence[ScheduleBackfill] + memo: Mapping[str, Any] | None + search_attributes: None | ( + temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes + ) + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class ListSchedulesInput: + """Input for :py:meth:`OutboundInterceptor.list_schedules`.""" + + page_size: int + next_page_token: bytes | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + query: str | None = None + + +@dataclass +class BackfillScheduleInput: + """Input for :py:meth:`OutboundInterceptor.backfill_schedule`.""" + + id: str + backfills: Sequence[ScheduleBackfill] + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class DeleteScheduleInput: + """Input for :py:meth:`OutboundInterceptor.delete_schedule`.""" + + id: str + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class DescribeScheduleInput: + """Input for :py:meth:`OutboundInterceptor.describe_schedule`.""" + + id: str + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class PauseScheduleInput: + """Input for :py:meth:`OutboundInterceptor.pause_schedule`.""" + + id: str + note: str | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class TriggerScheduleInput: + """Input for :py:meth:`OutboundInterceptor.trigger_schedule`.""" + + id: str + overlap: ScheduleOverlapPolicy | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class UnpauseScheduleInput: + """Input for :py:meth:`OutboundInterceptor.unpause_schedule`.""" + + id: str + note: str | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class UpdateScheduleInput: + """Input for :py:meth:`OutboundInterceptor.update_schedule`.""" + + id: str + updater: Callable[ + [ScheduleUpdateInput], + ScheduleUpdate | None | Awaitable[ScheduleUpdate | None], + ] + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class UpdateWorkerBuildIdCompatibilityInput: + """Input for :py:meth:`OutboundInterceptor.update_worker_build_id_compatibility`.""" + + task_queue: str + operation: BuildIdOp + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class GetWorkerBuildIdCompatibilityInput: + """Input for :py:meth:`OutboundInterceptor.get_worker_build_id_compatibility`.""" + + task_queue: str + max_sets: int | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class GetWorkerTaskReachabilityInput: + """Input for :py:meth:`OutboundInterceptor.get_worker_task_reachability`.""" + + build_ids: Sequence[str] + task_queues: Sequence[str] + reachability: TaskReachabilityType | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class StartNexusOperationInput: + """Input for :py:meth:`OutboundInterceptor.start_nexus_operation`. + + .. warning:: + This API is experimental and unstable. + """ + + operation: str + arg: Any + id: str + endpoint: str + service: str + result_type: type | None + schedule_to_close_timeout: timedelta | None + schedule_to_start_timeout: timedelta | None + start_to_close_timeout: timedelta | None + id_reuse_policy: temporalio.common.NexusOperationIDReusePolicy + id_conflict_policy: temporalio.common.NexusOperationIDConflictPolicy + search_attributes: temporalio.common.TypedSearchAttributes | None + summary: str | None + headers: Mapping[str, str] + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class DescribeNexusOperationInput: + """Input for :py:meth:`OutboundInterceptor.describe_nexus_operation`. + + .. warning:: + This API is experimental and unstable. + """ + + operation_id: str + run_id: str | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class GetNexusOperationResultInput: + """Input for :py:meth:`OutboundInterceptor.get_nexus_operation_result`. + + .. warning:: + This API is experimental and unstable. + """ + + operation_id: str + run_id: str | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + result_type: type[Any] | None + + +@dataclass +class CancelNexusOperationInput: + """Input for :py:meth:`OutboundInterceptor.cancel_nexus_operation`. + + .. warning:: + This API is experimental and unstable. + """ + + operation_id: str + run_id: str | None + reason: str | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class TerminateNexusOperationInput: + """Input for :py:meth:`OutboundInterceptor.terminate_nexus_operation`. + + .. warning:: + This API is experimental and unstable. + """ + + operation_id: str + run_id: str | None + reason: str | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class ListNexusOperationsInput: + """Input for :py:meth:`OutboundInterceptor.list_nexus_operations`. + + .. warning:: + This API is experimental and unstable. + """ + + query: str | None + page_size: int + next_page_token: bytes | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + limit: int | None + + +@dataclass +class CountNexusOperationsInput: + """Input for :py:meth:`OutboundInterceptor.count_nexus_operations`. + + .. warning:: + This API is experimental and unstable. + """ + + query: str | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class Interceptor: + """Interceptor for clients. + + This should be extended by any client interceptors. + """ + + def intercept_client(self, next: OutboundInterceptor) -> OutboundInterceptor: + """Method called for intercepting a client. + + Args: + next: The underlying outbound interceptor this interceptor should + delegate to. + + Returns: + The new interceptor that will be called for each client call. + """ + return next + + +class OutboundInterceptor: + """OutboundInterceptor for intercepting client calls. + + This should be extended by any client outbound interceptors. + """ + + def __init__(self, next: OutboundInterceptor) -> None: + """Create the outbound interceptor. + + Args: + next: The next interceptor in the chain. The default implementation + of all calls is to delegate to the next interceptor. + """ + self.next = next + + ### Workflow calls + + async def start_workflow( + self, input: StartWorkflowInput + ) -> WorkflowHandle[Any, Any]: + """Called for every :py:meth:`Client.start_workflow` call.""" + return await self.next.start_workflow(input) + + async def cancel_workflow(self, input: CancelWorkflowInput) -> None: + """Called for every :py:meth:`WorkflowHandle.cancel` call.""" + await self.next.cancel_workflow(input) + + async def describe_workflow( + self, input: DescribeWorkflowInput + ) -> WorkflowExecutionDescription: + """Called for every :py:meth:`WorkflowHandle.describe` call.""" + return await self.next.describe_workflow(input) + + def fetch_workflow_history_events( + self, input: FetchWorkflowHistoryEventsInput + ) -> WorkflowHistoryEventAsyncIterator: + """Called for every :py:meth:`WorkflowHandle.fetch_history_events` call.""" + return self.next.fetch_workflow_history_events(input) + + def list_workflows( + self, input: ListWorkflowsInput + ) -> WorkflowExecutionAsyncIterator: + """Called for every :py:meth:`Client.list_workflows` call.""" + return self.next.list_workflows(input) + + async def count_workflows( + self, input: CountWorkflowsInput + ) -> WorkflowExecutionCount: + """Called for every :py:meth:`Client.count_workflows` call.""" + return await self.next.count_workflows(input) + + async def query_workflow(self, input: QueryWorkflowInput) -> Any: + """Called for every :py:meth:`WorkflowHandle.query` call.""" + return await self.next.query_workflow(input) + + async def signal_workflow(self, input: SignalWorkflowInput) -> None: + """Called for every :py:meth:`WorkflowHandle.signal` call.""" + await self.next.signal_workflow(input) + + async def terminate_workflow(self, input: TerminateWorkflowInput) -> None: + """Called for every :py:meth:`WorkflowHandle.terminate` call.""" + await self.next.terminate_workflow(input) + + ### Activity calls + + async def start_activity(self, input: StartActivityInput) -> ActivityHandle[Any]: + """Called for every :py:meth:`Client.start_activity` call. + + .. warning:: + This API is experimental. + """ + return await self.next.start_activity(input) + + async def cancel_activity(self, input: CancelActivityInput) -> None: + """Called for every :py:meth:`ActivityHandle.cancel` call. + + .. warning:: + This API is experimental. + """ + await self.next.cancel_activity(input) + + async def terminate_activity(self, input: TerminateActivityInput) -> None: + """Called for every :py:meth:`ActivityHandle.terminate` call. + + .. warning:: + This API is experimental. + """ + await self.next.terminate_activity(input) + + async def describe_activity( + self, input: DescribeActivityInput + ) -> ActivityExecutionDescription: + """Called for every :py:meth:`ActivityHandle.describe` call. + + .. warning:: + This API is experimental. + """ + return await self.next.describe_activity(input) + + def list_activities( + self, input: ListActivitiesInput + ) -> ActivityExecutionAsyncIterator: + """Called for every :py:meth:`Client.list_activities` call. + + .. warning:: + This API is experimental. + """ + return self.next.list_activities(input) + + async def count_activities( + self, input: CountActivitiesInput + ) -> ActivityExecutionCount: + """Called for every :py:meth:`Client.count_activities` call. + + .. warning:: + This API is experimental. + """ + return await self.next.count_activities(input) + + async def start_workflow_update( + self, input: StartWorkflowUpdateInput + ) -> WorkflowUpdateHandle[Any]: + """Called for every :py:meth:`WorkflowHandle.start_update` and :py:meth:`WorkflowHandle.execute_update` call.""" + return await self.next.start_workflow_update(input) + + async def start_update_with_start_workflow( + self, input: StartWorkflowUpdateWithStartInput + ) -> WorkflowUpdateHandle[Any]: + """Called for every :py:meth:`Client.start_update_with_start_workflow` and :py:meth:`Client.execute_update_with_start_workflow` call.""" + return await self.next.start_update_with_start_workflow(input) + + ### Async activity calls + + async def heartbeat_async_activity( + self, input: HeartbeatAsyncActivityInput + ) -> None: + """Called for every :py:meth:`AsyncActivityHandle.heartbeat` call.""" + await self.next.heartbeat_async_activity(input) + + async def complete_async_activity(self, input: CompleteAsyncActivityInput) -> None: + """Called for every :py:meth:`AsyncActivityHandle.complete` call.""" + await self.next.complete_async_activity(input) + + async def fail_async_activity(self, input: FailAsyncActivityInput) -> None: + """Called for every :py:meth:`AsyncActivityHandle.fail` call.""" + await self.next.fail_async_activity(input) + + async def report_cancellation_async_activity( + self, input: ReportCancellationAsyncActivityInput + ) -> None: + """Called for every :py:meth:`AsyncActivityHandle.report_cancellation` call.""" + await self.next.report_cancellation_async_activity(input) + + ### Schedule calls + + async def create_schedule(self, input: CreateScheduleInput) -> ScheduleHandle: + """Called for every :py:meth:`Client.create_schedule` call.""" + return await self.next.create_schedule(input) + + def list_schedules(self, input: ListSchedulesInput) -> ScheduleAsyncIterator: + """Called for every :py:meth:`Client.list_schedules` call.""" + return self.next.list_schedules(input) + + async def backfill_schedule(self, input: BackfillScheduleInput) -> None: + """Called for every :py:meth:`ScheduleHandle.backfill` call.""" + await self.next.backfill_schedule(input) + + async def delete_schedule(self, input: DeleteScheduleInput) -> None: + """Called for every :py:meth:`ScheduleHandle.delete` call.""" + await self.next.delete_schedule(input) + + async def describe_schedule( + self, input: DescribeScheduleInput + ) -> ScheduleDescription: + """Called for every :py:meth:`ScheduleHandle.describe` call.""" + return await self.next.describe_schedule(input) + + async def pause_schedule(self, input: PauseScheduleInput) -> None: + """Called for every :py:meth:`ScheduleHandle.pause` call.""" + await self.next.pause_schedule(input) + + async def trigger_schedule(self, input: TriggerScheduleInput) -> None: + """Called for every :py:meth:`ScheduleHandle.trigger` call.""" + await self.next.trigger_schedule(input) + + async def unpause_schedule(self, input: UnpauseScheduleInput) -> None: + """Called for every :py:meth:`ScheduleHandle.unpause` call.""" + await self.next.unpause_schedule(input) + + async def update_schedule(self, input: UpdateScheduleInput) -> None: + """Called for every :py:meth:`ScheduleHandle.update` call.""" + await self.next.update_schedule(input) + + async def update_worker_build_id_compatibility( + self, input: UpdateWorkerBuildIdCompatibilityInput + ) -> None: + """Called for every :py:meth:`Client.update_worker_build_id_compatibility` call.""" + await self.next.update_worker_build_id_compatibility(input) + + async def get_worker_build_id_compatibility( + self, input: GetWorkerBuildIdCompatibilityInput + ) -> WorkerBuildIdVersionSets: + """Called for every :py:meth:`Client.get_worker_build_id_compatibility` call.""" + return await self.next.get_worker_build_id_compatibility(input) + + async def get_worker_task_reachability( + self, input: GetWorkerTaskReachabilityInput + ) -> WorkerTaskReachability: + """Called for every :py:meth:`Client.get_worker_task_reachability` call.""" + return await self.next.get_worker_task_reachability(input) + + ### Nexus operation calls + + async def start_nexus_operation( + self, input: StartNexusOperationInput + ) -> NexusOperationHandle[Any]: + """Called for every :py:meth:`NexusClient.start_operation` call. + + .. warning:: + This API is experimental and unstable. + """ + return await self.next.start_nexus_operation(input) + + async def describe_nexus_operation( + self, input: DescribeNexusOperationInput + ) -> NexusOperationExecutionDescription: + """Called for every :py:meth:`NexusOperationHandle.describe` call. + + .. warning:: + This API is experimental and unstable. + """ + return await self.next.describe_nexus_operation(input) + + async def get_nexus_operation_result( + self, input: GetNexusOperationResultInput + ) -> Any: + """Called for every :py:meth:`NexusOperationHandle.result` call. + + .. warning:: + This API is experimental and unstable. + """ + return await self.next.get_nexus_operation_result(input) + + async def cancel_nexus_operation(self, input: CancelNexusOperationInput) -> None: + """Called for every :py:meth:`NexusOperationHandle.cancel` call. + + .. warning:: + This API is experimental and unstable. + """ + await self.next.cancel_nexus_operation(input) + + async def terminate_nexus_operation( + self, input: TerminateNexusOperationInput + ) -> None: + """Called for every :py:meth:`NexusOperationHandle.terminate` call. + + .. warning:: + This API is experimental and unstable. + """ + await self.next.terminate_nexus_operation(input) + + def list_nexus_operations( + self, input: ListNexusOperationsInput + ) -> NexusOperationExecutionAsyncIterator: + """Called for every :py:meth:`Client.list_nexus_operations` call. + + .. warning:: + This API is experimental and unstable. + """ + return self.next.list_nexus_operations(input) + + async def count_nexus_operations( + self, input: CountNexusOperationsInput + ) -> NexusOperationExecutionCount: + """Called for every :py:meth:`Client.count_nexus_operations` call. + + .. warning:: + This API is experimental and unstable. + """ + return await self.next.count_nexus_operations(input) diff --git a/temporalio/client/_nexus.py b/temporalio/client/_nexus.py new file mode 100644 index 000000000..7eea155a9 --- /dev/null +++ b/temporalio/client/_nexus.py @@ -0,0 +1,1337 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Awaitable, Callable, Mapping, Sequence +from dataclasses import dataclass, field +from datetime import datetime, timedelta, timezone +from typing import TYPE_CHECKING, Any, Generic, cast, overload + +import nexusrpc +from nexusrpc import InputT, OutputT +from typing_extensions import Self + +import temporalio.api.nexus.v1 +import temporalio.api.workflowservice.v1 +import temporalio.common +import temporalio.converter +import temporalio.converter._search_attributes +import temporalio.exceptions +import temporalio.nexus._util +from temporalio.types import NexusServiceType, ReturnType + +from ._helpers import _decode_user_metadata +from ._interceptor import ( + CancelNexusOperationInput, + DescribeNexusOperationInput, + GetNexusOperationResultInput, + ListNexusOperationsInput, + StartNexusOperationInput, + TerminateNexusOperationInput, +) + +if TYPE_CHECKING: + from ._client import Client + + +@dataclass +class NexusOperationExecutionCancellationInfo: + """Cancellation information for a Nexus Operation. + + .. warning:: + This API is experimental and unstable. + """ + + raw: temporalio.api.nexus.v1.NexusOperationExecutionCancellationInfo + """Underlying protobuf cancellation info.""" + + requested_time: datetime | None + """The time when cancellation was requested.""" + + state: temporalio.common.NexusOperationCancellationState + """The current state of the cancellation request.""" + + attempt: int + """The number of attempts made to deliver the cancel operation request.""" + + last_attempt_complete_time: datetime | None + """The time when the last attempt completed.""" + + next_attempt_schedule_time: datetime | None + """The time when the next attempt is scheduled.""" + + last_attempt_failure: BaseException | None + """The last attempt's failure, if any.""" + + blocked_reason: str + """Blocked reason provides additional information if the cancellation state is BLOCKED.""" + + reason: str + """The reason specified in the cancellation request.""" + + @classmethod + async def _from_cancellation_info( + cls, + info: temporalio.api.nexus.v1.NexusOperationExecutionCancellationInfo, + data_converter: temporalio.converter.DataConverter, + ) -> Self: + """Create from raw proto nexus operation cancellation info.""" + return cls( + raw=info, + requested_time=( + info.requested_time.ToDatetime(tzinfo=timezone.utc) + if info.HasField("requested_time") + else None + ), + state=( + temporalio.common.NexusOperationCancellationState(info.state) + if info.state + else temporalio.common.NexusOperationCancellationState.UNSPECIFIED + ), + attempt=info.attempt, + last_attempt_complete_time=( + info.last_attempt_complete_time.ToDatetime(tzinfo=timezone.utc) + if info.HasField("last_attempt_complete_time") + else None + ), + next_attempt_schedule_time=( + info.next_attempt_schedule_time.ToDatetime(tzinfo=timezone.utc) + if info.HasField("next_attempt_schedule_time") + else None + ), + last_attempt_failure=( + cast( + BaseException | None, + await data_converter.decode_failure(info.last_attempt_failure), + ) + if info.HasField("last_attempt_failure") + else None + ), + blocked_reason=info.blocked_reason, + reason=info.reason, + ) + + +@dataclass +class NexusOperationExecution: + """Info for a standalone Nexus operation execution, from list response. + + .. warning:: + This API is experimental and unstable. + """ + + operation_id: str + """Unique identifier of this operation.""" + + run_id: str + """Run ID of the standalone Nexus operation.""" + + endpoint: str + """Endpoint name.""" + + service: str + """Service name.""" + + operation: str + """Operation name.""" + + schedule_time: datetime | None + """Time the operation was originally scheduled.""" + + close_time: datetime | None + """Time the operation reached a terminal status, if closed.""" + + status: temporalio.common.NexusOperationExecutionStatus + """Current status of the operation.""" + + search_attributes: temporalio.common.TypedSearchAttributes + """Current set of search attributes if any.""" + + state_transition_count: int + """Number of state transitions.""" + + execution_duration: timedelta | None + """Duration from scheduled to close time, only populated if closed.""" + + raw_info: ( + temporalio.api.nexus.v1.NexusOperationExecutionListInfo + | temporalio.api.nexus.v1.NexusOperationExecutionInfo + ) + """Underlying protobuf info.""" + + @classmethod + def _from_raw_info( + cls, info: temporalio.api.nexus.v1.NexusOperationExecutionListInfo + ) -> Self: + """Create from raw proto nexus operation list info.""" + return cls( + operation_id=info.operation_id, + run_id=info.run_id, + endpoint=info.endpoint, + service=info.service, + operation=info.operation, + schedule_time=( + info.schedule_time.ToDatetime(tzinfo=timezone.utc) + if info.HasField("schedule_time") + else None + ), + close_time=( + info.close_time.ToDatetime(tzinfo=timezone.utc) + if info.HasField("close_time") + else None + ), + status=( + temporalio.common.NexusOperationExecutionStatus(info.status) + if info.status + else temporalio.common.NexusOperationExecutionStatus.UNSPECIFIED + ), + search_attributes=temporalio.converter.decode_typed_search_attributes( + info.search_attributes + ), + state_transition_count=info.state_transition_count, + execution_duration=( + info.execution_duration.ToTimedelta() + if info.HasField("execution_duration") + else None + ), + raw_info=info, + ) + + +@dataclass +class NexusOperationExecutionDescription(NexusOperationExecution): + """Detailed information about a standalone Nexus operation execution. + + .. warning:: + This API is experimental and unstable. + """ + + raw_description: temporalio.api.nexus.v1.NexusOperationExecutionInfo + """Underlying protobuf description info.""" + + state: temporalio.common.PendingNexusOperationExecutionState + """More detailed breakdown if status is :py:attr:`NexusOperationExecutionStatus.RUNNING`.""" + + schedule_to_close_timeout: timedelta | None + """Schedule-to-close timeout for this operation.""" + + schedule_to_start_timeout: timedelta | None + """Schedule-to-start timeout for this operation.""" + + start_to_close_timeout: timedelta | None + """Start-to-close timeout for this operation.""" + + attempt: int + """Current attempt number.""" + + expiration_time: datetime | None + """Scheduled time plus schedule_to_close_timeout.""" + + last_attempt_complete_time: datetime | None + """Time when the last attempt completed.""" + + next_attempt_schedule_time: datetime | None + """Time when the next attempt will be scheduled.""" + + last_attempt_failure: BaseException | None + """Failure from the last failed attempt, if any.""" + + blocked_reason: str | None + """Reason the operation is blocked, if any.""" + + request_id: str + """Server-generated request ID used as an idempotency token.""" + + operation_token: str | None + """Operation token is only set for asynchronous operations after a successful start_operation call.""" + + identity: str + """Identity of the client that started this operation.""" + + cancellation_info: NexusOperationExecutionCancellationInfo | None + """Cancellation info if cancellation was requested.""" + + _data_converter: temporalio.converter.DataConverter = field( + kw_only=True, compare=False, repr=False + ) + _static_summary: str | None = field( + kw_only=True, default=None, compare=False, repr=False + ) + _static_details: str | None = field( + kw_only=True, default=None, compare=False, repr=False + ) + _metadata_decoded: bool = field( + kw_only=True, default=False, compare=False, repr=False + ) + + async def static_summary(self) -> str | None: + """Gets the single-line fixed summary for this Nexus operation execution that may appear in + UI/CLI. This can be in single-line Temporal markdown format. + """ + if not self._metadata_decoded: + await self._decode_metadata() + return self._static_summary + + async def static_details(self) -> str | None: + """Gets the general fixed details for this Nexus operation execution that may appear in UI/CLI. + This can be in Temporal markdown format and can span multiple lines. + """ + if not self._metadata_decoded: + await self._decode_metadata() + return self._static_details + + async def _decode_metadata(self) -> None: + """Internal method to decode metadata lazily.""" + self._static_summary, self._static_details = await _decode_user_metadata( + self._data_converter, self.raw_description.user_metadata + ) + self._metadata_decoded = True + + @classmethod + async def _from_execution_info( + cls, + info: temporalio.api.nexus.v1.NexusOperationExecutionInfo, + data_converter: temporalio.converter.DataConverter, + ) -> Self: + """Create from raw proto nexus operation execution info.""" + return cls( + _data_converter=data_converter, + operation_id=info.operation_id, + run_id=info.run_id, + endpoint=info.endpoint, + service=info.service, + operation=info.operation, + schedule_time=( + info.schedule_time.ToDatetime(tzinfo=timezone.utc) + if info.HasField("schedule_time") + else None + ), + close_time=( + info.close_time.ToDatetime(tzinfo=timezone.utc) + if info.HasField("close_time") + else None + ), + status=( + temporalio.common.NexusOperationExecutionStatus(info.status) + if info.status + else temporalio.common.NexusOperationExecutionStatus.UNSPECIFIED + ), + search_attributes=temporalio.converter.decode_typed_search_attributes( + info.search_attributes + ), + state_transition_count=info.state_transition_count, + execution_duration=( + info.execution_duration.ToTimedelta() + if info.HasField("execution_duration") + else None + ), + raw_info=info, + raw_description=info, + state=( + temporalio.common.PendingNexusOperationExecutionState(info.state) + if info.state + else temporalio.common.PendingNexusOperationExecutionState.UNSPECIFIED + ), + schedule_to_close_timeout=( + info.schedule_to_close_timeout.ToTimedelta() + if info.HasField("schedule_to_close_timeout") + else None + ), + schedule_to_start_timeout=( + info.schedule_to_start_timeout.ToTimedelta() + if info.HasField("schedule_to_start_timeout") + else None + ), + start_to_close_timeout=( + info.start_to_close_timeout.ToTimedelta() + if info.HasField("start_to_close_timeout") + else None + ), + attempt=info.attempt, + expiration_time=( + info.expiration_time.ToDatetime(tzinfo=timezone.utc) + if info.HasField("expiration_time") + else None + ), + last_attempt_complete_time=( + info.last_attempt_complete_time.ToDatetime(tzinfo=timezone.utc) + if info.HasField("last_attempt_complete_time") + else None + ), + last_attempt_failure=( + cast( + BaseException | None, + await data_converter.decode_failure(info.last_attempt_failure), + ) + if info.HasField("last_attempt_failure") + else None + ), + next_attempt_schedule_time=( + info.next_attempt_schedule_time.ToDatetime(tzinfo=timezone.utc) + if info.HasField("next_attempt_schedule_time") + else None + ), + blocked_reason=info.blocked_reason if info.blocked_reason else None, + request_id=info.request_id, + operation_token=info.operation_token if info.operation_token else None, + identity=info.identity, + cancellation_info=( + await NexusOperationExecutionCancellationInfo._from_cancellation_info( + info.cancellation_info, data_converter + ) + if info.HasField("cancellation_info") + else None + ), + ) + + +@dataclass(frozen=True) +class NexusOperationExecutionCountAggregationGroup: + """A single aggregation group from a count nexus operations call. + + .. warning:: + This API is experimental and unstable. + """ + + count: int + """Count for this group.""" + + group_values: Sequence[temporalio.common.SearchAttributeValue] + """Values that define this group.""" + + @staticmethod + def _from_raw( + raw: temporalio.api.workflowservice.v1.CountNexusOperationExecutionsResponse.AggregationGroup, + ) -> NexusOperationExecutionCountAggregationGroup: + return NexusOperationExecutionCountAggregationGroup( + count=raw.count, + group_values=[ + temporalio.converter._search_attributes._decode_search_attribute_value( + v + ) + for v in raw.group_values + ], + ) + + +@dataclass +class NexusOperationExecutionCount: + """Representation of a count from a count nexus operations call. + + .. warning:: + This API is experimental and unstable. + """ + + count: int + """Approximate number of operations matching the original query. + + If the query had a group-by clause, this is simply the sum of all the counts + in :py:attr:`groups`. + """ + + groups: Sequence[NexusOperationExecutionCountAggregationGroup] + """Groups if the query had a group-by clause, or empty if not.""" + + @staticmethod + def _from_raw( + resp: temporalio.api.workflowservice.v1.CountNexusOperationExecutionsResponse, + ) -> NexusOperationExecutionCount: + """Create from raw proto response.""" + return NexusOperationExecutionCount( + count=resp.count, + groups=[ + NexusOperationExecutionCountAggregationGroup._from_raw(g) + for g in resp.groups + ], + ) + + +class NexusOperationFailureError(temporalio.exceptions.TemporalError): + """Error that occurs when a Nexus operation is unsuccessful. + + .. warning:: + This API is experimental and unstable. + """ + + def __init__(self, *, cause: BaseException) -> None: + """Create Nexus operation failure error.""" + super().__init__("Nexus operation execution failed") + self.__cause__ = cause + + @property + def cause(self) -> BaseException: + """Cause of the Nexus operation failure.""" + assert self.__cause__ + return self.__cause__ + + +class NexusClient(ABC, Generic[NexusServiceType]): + """Client for starting standalone Nexus operations. + + .. warning:: + This API is experimental and unstable. + + Use :py:meth:`Client.create_nexus_client` to create a client. + """ + + # Overload for nexusrpc.Operation + @overload + @abstractmethod + async def start_operation( + self, + operation: nexusrpc.Operation[InputT, OutputT], + arg: InputT, + *, + id: str, + id_reuse_policy: temporalio.common.NexusOperationIDReusePolicy = temporalio.common.NexusOperationIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.NexusOperationIDConflictPolicy = temporalio.common.NexusOperationIDConflictPolicy.FAIL, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + headers: Mapping[str, str] | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> NexusOperationHandle[OutputT]: ... + + # Overload for string operation name + @overload + @abstractmethod + async def start_operation( + self, + operation: str, + arg: Any, + *, + id: str, + id_reuse_policy: temporalio.common.NexusOperationIDReusePolicy = temporalio.common.NexusOperationIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.NexusOperationIDConflictPolicy = temporalio.common.NexusOperationIDConflictPolicy.FAIL, + result_type: type[OutputT] | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + headers: Mapping[str, str] | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> NexusOperationHandle[OutputT]: ... + + # Overload for workflow_run_operation methods + @overload + @abstractmethod + async def start_operation( + self, + operation: Callable[ + [NexusServiceType, temporalio.nexus.WorkflowRunOperationContext, InputT], + Awaitable[temporalio.nexus.WorkflowHandle[OutputT]], + ], + arg: InputT, + *, + id: str, + id_reuse_policy: temporalio.common.NexusOperationIDReusePolicy = temporalio.common.NexusOperationIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.NexusOperationIDConflictPolicy = temporalio.common.NexusOperationIDConflictPolicy.FAIL, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + headers: Mapping[str, str] | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> NexusOperationHandle[OutputT]: ... + + # Overload for sync_operation methods (async def) + @overload + @abstractmethod + async def start_operation( + self, + operation: Callable[ + [NexusServiceType, nexusrpc.handler.StartOperationContext, InputT], + Awaitable[OutputT], + ], + arg: InputT, + *, + id: str, + id_reuse_policy: temporalio.common.NexusOperationIDReusePolicy = temporalio.common.NexusOperationIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.NexusOperationIDConflictPolicy = temporalio.common.NexusOperationIDConflictPolicy.FAIL, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + headers: Mapping[str, str] | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> NexusOperationHandle[OutputT]: ... + + # Overload for sync_operation methods (def) + @overload + @abstractmethod + async def start_operation( + self, + operation: Callable[ + [NexusServiceType, nexusrpc.handler.StartOperationContext, InputT], + OutputT, + ], + arg: InputT, + *, + id: str, + id_reuse_policy: temporalio.common.NexusOperationIDReusePolicy = temporalio.common.NexusOperationIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.NexusOperationIDConflictPolicy = temporalio.common.NexusOperationIDConflictPolicy.FAIL, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + headers: Mapping[str, str] | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> NexusOperationHandle[OutputT]: ... + + # Overload for operation_handler + @overload + @abstractmethod + async def start_operation( + self, + operation: Callable[ + [NexusServiceType], nexusrpc.handler.OperationHandler[InputT, OutputT] + ], + arg: InputT, + *, + id: str, + id_reuse_policy: temporalio.common.NexusOperationIDReusePolicy = temporalio.common.NexusOperationIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.NexusOperationIDConflictPolicy = temporalio.common.NexusOperationIDConflictPolicy.FAIL, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + headers: Mapping[str, str] | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> NexusOperationHandle[OutputT]: ... + + # Overload for temporal_operation methods + @overload + @abstractmethod + async def start_operation( + self, + operation: Callable[ + [ + NexusServiceType, + temporalio.nexus.TemporalStartOperationContext, + temporalio.nexus.TemporalNexusClient, + InputT, + ], + Awaitable[temporalio.nexus.TemporalOperationResult[OutputT]], + ], + arg: InputT, + *, + id: str, + id_reuse_policy: temporalio.common.NexusOperationIDReusePolicy = temporalio.common.NexusOperationIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.NexusOperationIDConflictPolicy = temporalio.common.NexusOperationIDConflictPolicy.FAIL, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + headers: Mapping[str, str] | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> NexusOperationHandle[OutputT]: ... + + @abstractmethod + async def start_operation( + self, + operation: nexusrpc.Operation[Any, Any] | str | Callable[..., Any], + arg: Any, + *, + id: str, + id_reuse_policy: temporalio.common.NexusOperationIDReusePolicy = temporalio.common.NexusOperationIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.NexusOperationIDConflictPolicy = temporalio.common.NexusOperationIDConflictPolicy.FAIL, + result_type: type | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + headers: Mapping[str, str] | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> NexusOperationHandle[Any]: + """Start a Nexus operation and return a handle. + + .. warning:: + This API is experimental and unstable. + + Args: + operation: The operation to start. Can be a ``nexusrpc.Operation``, + a callable operation method, or a string name. + arg: Input argument for the operation. + id: Unique identifier for this operation. + id_reuse_policy: Policy for reusing operation IDs. + id_conflict_policy: Policy for handling ID conflicts. + result_type: For string operation names, this can set the specific + result type hint to deserialize into. + schedule_to_close_timeout: End-to-end timeout for the Nexus + operation. If unset, defaults to the maximum allowed by the + Temporal server. + schedule_to_start_timeout: Maximum time to wait for the operation + to be started (or completed, if synchronous) by the handler. If + unset, no schedule-to-start timeout is enforced. + start_to_close_timeout: Maximum time to wait for an asynchronous + operation to complete after it has been started. Only applies to + asynchronous operations and is ignored for synchronous + operations. If unset, no start-to-close timeout is enforced. + search_attributes: Search attributes for the operation. + summary: Summary for the operation. + headers: Headers to attach to the Nexus request. + rpc_metadata: Headers used on the RPC call. + rpc_timeout: Optional RPC deadline to set for the RPC call. + + Returns: + A handle to the started operation. + """ + ... + + # Overload for nexusrpc.Operation + @overload + @abstractmethod + async def execute_operation( + self, + operation: nexusrpc.Operation[InputT, OutputT], + arg: InputT, + *, + id: str, + id_reuse_policy: temporalio.common.NexusOperationIDReusePolicy = temporalio.common.NexusOperationIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.NexusOperationIDConflictPolicy = temporalio.common.NexusOperationIDConflictPolicy.FAIL, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + headers: Mapping[str, str] | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> OutputT: ... + + # Overload for string operation name + @overload + @abstractmethod + async def execute_operation( + self, + operation: str, + arg: Any, + *, + id: str, + id_reuse_policy: temporalio.common.NexusOperationIDReusePolicy = temporalio.common.NexusOperationIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.NexusOperationIDConflictPolicy = temporalio.common.NexusOperationIDConflictPolicy.FAIL, + result_type: type[OutputT] | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + headers: Mapping[str, str] | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> OutputT: ... + + # Overload for workflow_run_operation methods + @overload + @abstractmethod + async def execute_operation( + self, + operation: Callable[ + [NexusServiceType, temporalio.nexus.WorkflowRunOperationContext, InputT], + Awaitable[temporalio.nexus.WorkflowHandle[OutputT]], + ], + arg: InputT, + *, + id: str, + id_reuse_policy: temporalio.common.NexusOperationIDReusePolicy = temporalio.common.NexusOperationIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.NexusOperationIDConflictPolicy = temporalio.common.NexusOperationIDConflictPolicy.FAIL, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + headers: Mapping[str, str] | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> OutputT: ... + + # Overload for sync_operation methods (async def) + @overload + @abstractmethod + async def execute_operation( + self, + operation: Callable[ + [NexusServiceType, nexusrpc.handler.StartOperationContext, InputT], + Awaitable[OutputT], + ], + arg: InputT, + *, + id: str, + id_reuse_policy: temporalio.common.NexusOperationIDReusePolicy = temporalio.common.NexusOperationIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.NexusOperationIDConflictPolicy = temporalio.common.NexusOperationIDConflictPolicy.FAIL, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + headers: Mapping[str, str] | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> OutputT: ... + + # Overload for sync_operation methods (async def) + @overload + @abstractmethod + async def execute_operation( + self, + operation: Callable[ + [NexusServiceType, nexusrpc.handler.StartOperationContext, InputT], + OutputT, + ], + arg: InputT, + *, + id: str, + id_reuse_policy: temporalio.common.NexusOperationIDReusePolicy = temporalio.common.NexusOperationIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.NexusOperationIDConflictPolicy = temporalio.common.NexusOperationIDConflictPolicy.FAIL, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + headers: Mapping[str, str] | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> OutputT: ... + + # Overload for operation_handler + @overload + @abstractmethod + async def execute_operation( + self, + operation: Callable[ + [NexusServiceType], + nexusrpc.handler.OperationHandler[InputT, OutputT], + ], + arg: InputT, + *, + id: str, + id_reuse_policy: temporalio.common.NexusOperationIDReusePolicy = temporalio.common.NexusOperationIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.NexusOperationIDConflictPolicy = temporalio.common.NexusOperationIDConflictPolicy.FAIL, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + headers: Mapping[str, str] | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> OutputT: ... + + # Overload for temporal_operation methods + @overload + @abstractmethod + async def execute_operation( + self, + operation: Callable[ + [ + NexusServiceType, + temporalio.nexus.TemporalStartOperationContext, + temporalio.nexus.TemporalNexusClient, + InputT, + ], + Awaitable[temporalio.nexus.TemporalOperationResult[OutputT]], + ], + arg: InputT, + *, + id: str, + id_reuse_policy: temporalio.common.NexusOperationIDReusePolicy = temporalio.common.NexusOperationIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.NexusOperationIDConflictPolicy = temporalio.common.NexusOperationIDConflictPolicy.FAIL, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + headers: Mapping[str, str] | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> OutputT: ... + + @abstractmethod + async def execute_operation( + self, + operation: nexusrpc.Operation[Any, Any] | str | Callable[..., Any], + arg: Any, + *, + id: str, + id_reuse_policy: temporalio.common.NexusOperationIDReusePolicy = temporalio.common.NexusOperationIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.NexusOperationIDConflictPolicy = temporalio.common.NexusOperationIDConflictPolicy.FAIL, + result_type: type | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + headers: Mapping[str, str] | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> Any: + """Start a Nexus operation and wait for its result. + + .. warning:: + This API is experimental and unstable. + + This is a shortcut for ``await (await nexus_client.start_operation(...)).result()``. + + Args: + operation: The operation to execute. Can be a ``nexusrpc.Operation``, + a callable operation method, or a string name. + arg: Input argument for the operation. + id: Unique identifier for this operation. + id_reuse_policy: Policy for reusing operation IDs. + id_conflict_policy: Policy for handling ID conflicts. + result_type: For string operation names, this can set the specific + result type hint to deserialize into. + schedule_to_close_timeout: End-to-end timeout for the Nexus + operation. If unset, defaults to the maximum allowed by the + Temporal server. + schedule_to_start_timeout: Maximum time to wait for the operation + to be started (or completed, if synchronous) by the handler. If + unset, no schedule-to-start timeout is enforced. + start_to_close_timeout: Maximum time to wait for an asynchronous + operation to complete after it has been started. Only applies to + asynchronous operations and is ignored for synchronous + operations. If unset, no start-to-close timeout is enforced. + search_attributes: Search attributes for the operation. + summary: Summary for the operation. + headers: Headers to attach to the Nexus request. + rpc_metadata: Headers used on the RPC call. + rpc_timeout: Optional RPC deadline to set for the RPC call. + + Returns: + The result of the operation. + """ + ... + + +class _NexusClient(NexusClient[NexusServiceType]): # pyright: ignore[reportUnusedClass] + """Concrete implementation of NexusClient.""" + + def __init__( + self, + client: Client, + service: type[NexusServiceType] | str, + endpoint: str, + ) -> None: + self._client = client + if isinstance(service, str): + self._service_name = service + elif service_defn := nexusrpc.get_service_definition(service): + self._service_name = service_defn.name + else: + self._service_name = service.__name__ + self._endpoint = endpoint + + def _resolve_operation( + self, + operation: nexusrpc.Operation[Any, Any] | str | Callable[..., Any], + ) -> tuple[str, type | None]: + """Resolve an operation to its name and output type.""" + if isinstance(operation, str): + return operation, None + elif isinstance(operation, nexusrpc.Operation): + return operation.name, operation.output_type + elif callable(operation): + _, op = temporalio.nexus._util.get_operation_factory(operation) + if isinstance(op, nexusrpc.Operation): + return op.name, op.output_type + else: + raise ValueError( + f"Operation callable is not a Nexus operation: {operation}" + ) + else: + raise ValueError( # pyright: ignore[reportUnreachable] + f"Operation is not resolvable as a Nexus operation: {operation}" + ) + + async def start_operation( + self, + operation: nexusrpc.Operation[Any, Any] | str | Callable[..., Any], + arg: Any, + *, + id: str, + id_reuse_policy: temporalio.common.NexusOperationIDReusePolicy = temporalio.common.NexusOperationIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.NexusOperationIDConflictPolicy = temporalio.common.NexusOperationIDConflictPolicy.FAIL, + result_type: type | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + headers: Mapping[str, str] | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> NexusOperationHandle[Any]: + """Start a Nexus operation and return a handle. + + .. warning:: + This API is experimental and unstable. + """ + op_name, output_type = self._resolve_operation(operation) + final_result_type: type | None = ( + result_type if isinstance(operation, str) else output_type + ) + + return await self._client._impl.start_nexus_operation( + StartNexusOperationInput( + operation=op_name, + arg=arg, + id=id, + endpoint=self._endpoint, + service=self._service_name, + result_type=final_result_type, + schedule_to_close_timeout=schedule_to_close_timeout, + schedule_to_start_timeout=schedule_to_start_timeout, + start_to_close_timeout=start_to_close_timeout, + id_reuse_policy=id_reuse_policy, + id_conflict_policy=id_conflict_policy, + search_attributes=search_attributes, + summary=summary, + headers=dict(headers) if headers else {}, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) + + async def execute_operation( + self, + operation: nexusrpc.Operation[Any, Any] | str | Callable[..., Any], + arg: Any, + *, + id: str, + id_reuse_policy: temporalio.common.NexusOperationIDReusePolicy = temporalio.common.NexusOperationIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.NexusOperationIDConflictPolicy = temporalio.common.NexusOperationIDConflictPolicy.FAIL, + result_type: type | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + headers: Mapping[str, str] | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> Any: + """Start a Nexus operation and wait for its result. + + .. warning:: + This API is experimental and unstable. + """ + handle = await self.start_operation( + operation, + arg, + id=id, + id_reuse_policy=id_reuse_policy, + id_conflict_policy=id_conflict_policy, + result_type=result_type, + schedule_to_close_timeout=schedule_to_close_timeout, + schedule_to_start_timeout=schedule_to_start_timeout, + start_to_close_timeout=start_to_close_timeout, + search_attributes=search_attributes, + summary=summary, + headers=headers, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + return await handle.result() + + +class NexusOperationHandle(Generic[ReturnType]): + """Handle representing a standalone Nexus operation execution. + + .. warning:: + This API is experimental and unstable. + """ + + def __init__( + self, + client: Client, + operation_id: str, + *, + run_id: str | None = None, + result_type: type | None = None, + endpoint: str = "", + service: str = "", + ) -> None: + """Create nexus operation handle.""" + self._client = client + self._operation_id = operation_id + self._run_id = run_id + self._result_type = result_type + self._endpoint = endpoint + self._service = service + # the default value is `_arg_unset` because ReturnType could be None + self._known_outcome: ReturnType | NexusOperationFailureError | object = ( + temporalio.common._arg_unset + ) + + @property + def operation_id(self) -> str: + """ID of the operation.""" + return self._operation_id + + @property + def run_id(self) -> str | None: + """Run ID of the operation.""" + return self._run_id + + @property + def endpoint(self) -> str: + """Endpoint name.""" + return self._endpoint + + @property + def service(self) -> str: + """Service name.""" + return self._service + + async def result( + self, + *, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: + """Wait for result of the Nexus operation. + + .. warning:: + This API is experimental and unstable. + + The result may already be known if this method has been called before, + in which case no network call is made. Otherwise the result will be + polled for until it is available. + + Args: + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for each RPC call. Note: + this is the timeout for each RPC call while polling, not a + timeout for the function as a whole. If an individual RPC + times out, it will be retried until the result is available. + + Returns: + The result of the operation. + + Raises: + NexusOperationFailureError: If the operation completed with a failure. + RPCError: Operation result could not be fetched for some reason. + """ + if self._known_outcome is temporalio.common._arg_unset: + try: + self._known_outcome = ( + await self._client._impl.get_nexus_operation_result( + GetNexusOperationResultInput( + operation_id=self._operation_id, + run_id=self._run_id, + result_type=self._result_type, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) + ) + return cast(ReturnType, self._known_outcome) + except NexusOperationFailureError as failure: + self._known_outcome = failure + raise + elif isinstance(self._known_outcome, NexusOperationFailureError): + raise self._known_outcome + else: + return cast(ReturnType, self._known_outcome) + + async def describe( + self, + *, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> NexusOperationExecutionDescription: + """Describe the Nexus operation execution. + + .. warning:: + This API is experimental and unstable. + + Args: + rpc_metadata: Headers used on the RPC call. + rpc_timeout: Optional RPC deadline to set for the RPC call. + + Returns: + Nexus operation execution description. + """ + return await self._client._impl.describe_nexus_operation( + DescribeNexusOperationInput( + operation_id=self._operation_id, + run_id=self._run_id, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) + + async def cancel( + self, + *, + reason: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Request cancellation of the Nexus operation. + + .. warning:: + This API is experimental and unstable. + + Args: + reason: Reason for the cancellation. + rpc_metadata: Headers used on the RPC call. + rpc_timeout: Optional RPC deadline to set for the RPC call. + """ + await self._client._impl.cancel_nexus_operation( + CancelNexusOperationInput( + operation_id=self._operation_id, + run_id=self._run_id, + reason=reason, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) + + async def terminate( + self, + *, + reason: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Terminate the Nexus operation execution immediately. + + .. warning:: + This API is experimental and unstable. + + Args: + reason: Reason for the termination. + rpc_metadata: Headers used on the RPC call. + rpc_timeout: Optional RPC deadline to set for the RPC call. + """ + await self._client._impl.terminate_nexus_operation( + TerminateNexusOperationInput( + operation_id=self._operation_id, + run_id=self._run_id, + reason=reason, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) + + +class NexusOperationExecutionAsyncIterator: + """Asynchronous iterator for Nexus operation execution values. + + You should typically use ``async for`` on this iterator and not call any of its methods. + + .. warning:: + This API is experimental and unstable. + """ + + def __init__( + self, + client: Client, + input: ListNexusOperationsInput, + ) -> None: + """Create an asynchronous iterator for the given input. + + Users should not create this directly, but rather use + :py:meth:`Client.list_nexus_operations`. + """ + self._client = client + self._input = input + self._next_page_token = input.next_page_token + self._current_page: Sequence[NexusOperationExecution] | None = None + self._current_page_index = 0 + self._limit = input.limit + self._yielded = 0 + + @property + def current_page_index(self) -> int: + """Index of the entry in the current page that will be returned from + the next :py:meth:`__anext__` call. + """ + return self._current_page_index + + @property + def current_page(self) -> Sequence[NexusOperationExecution] | None: + """Current page, if it has been fetched yet.""" + return self._current_page + + @property + def next_page_token(self) -> bytes | None: + """Token for the next page request if any.""" + return self._next_page_token + + async def fetch_next_page(self, *, page_size: int | None = None) -> None: + """Fetch the next page of results. + + Args: + page_size: Override the page size this iterator was originally + created with. + """ + page_size = page_size or self._input.page_size + if self._limit is not None and self._limit - self._yielded < page_size: + page_size = self._limit - self._yielded + + resp = await self._client.workflow_service.list_nexus_operation_executions( + temporalio.api.workflowservice.v1.ListNexusOperationExecutionsRequest( + namespace=self._client.namespace, + page_size=page_size, + next_page_token=self._next_page_token or b"", + query=self._input.query or "", + ), + retry=True, + metadata=self._input.rpc_metadata, + timeout=self._input.rpc_timeout, + ) + + self._current_page = [ + NexusOperationExecution._from_raw_info(v) for v in resp.operations + ] + self._current_page_index = 0 + self._next_page_token = resp.next_page_token or None + + def __aiter__(self) -> NexusOperationExecutionAsyncIterator: + """Return self as the iterator.""" + return self + + async def __anext__(self) -> NexusOperationExecution: + """Get the next execution on this iterator, fetching next page if + necessary. + """ + if self._limit is not None and self._yielded >= self._limit: + raise StopAsyncIteration + while True: + # No page? fetch and continue + if self._current_page is None: + await self.fetch_next_page() + continue + # No more left in page? + if self._current_page_index >= len(self._current_page): + # If there is a next page token, try to get another page and try + # again + if self._next_page_token is not None: + await self.fetch_next_page() + continue + # No more pages means we're done + raise StopAsyncIteration + # Get current, increment page index, and return + ret = self._current_page[self._current_page_index] + self._current_page_index += 1 + self._yielded += 1 + return ret diff --git a/temporalio/client/_plugin.py b/temporalio/client/_plugin.py new file mode 100644 index 000000000..95b47e43d --- /dev/null +++ b/temporalio/client/_plugin.py @@ -0,0 +1,75 @@ +"""Client support for accessing Temporal.""" + +from __future__ import annotations + +import abc +from abc import abstractmethod +from collections.abc import ( + Awaitable, + Callable, +) +from typing import ( + TYPE_CHECKING, +) + +from temporalio.service import ( + ConnectConfig, + ServiceClient, +) + +if TYPE_CHECKING: + from ._client import ClientConfig + + +class Plugin(abc.ABC): + """Base class for client plugins that can intercept and modify client behavior. + + Plugins allow customization of client creation and service connection processes + through a chain of responsibility pattern. Each plugin can modify the client + configuration or intercept service client connections. + + If the plugin is also a temporalio.worker.Plugin, it will additionally be propagated as a worker plugin. + You should likley not also provide it to the worker as that will result in the plugin being applied twice. + """ + + def name(self) -> str: + """Get the name of this plugin. Can be overridden if desired to provide a more appropriate name. + + Returns: + The fully qualified name of the plugin class (module.classname). + """ + return type(self).__module__ + "." + type(self).__qualname__ + + @abstractmethod + def configure_client(self, config: ClientConfig) -> ClientConfig: + """Hook called when creating a client to allow modification of configuration. + + This method is called during client creation and allows plugins to modify + the client configuration before the client is fully initialized. Plugins + can add interceptors, modify connection parameters, or change other settings. + + Args: + config: The client configuration dictionary to potentially modify. + + Returns: + The modified client configuration. + """ + + @abstractmethod + async def connect_service_client( + self, + config: ConnectConfig, + next: Callable[[ConnectConfig], Awaitable[ServiceClient]], + ) -> ServiceClient: + """Hook called when connecting to the Temporal service. + + This method is called during service client connection and allows plugins + to intercept or modify the connection process. Plugins can modify connection + parameters, add authentication, or provide custom connection logic. + + Args: + config: The service connection configuration. + + Returns: + The connected service client. + """ diff --git a/temporalio/client/_schedule.py b/temporalio/client/_schedule.py new file mode 100644 index 000000000..15946cfe8 --- /dev/null +++ b/temporalio/client/_schedule.py @@ -0,0 +1,1604 @@ +"""Client support for accessing Temporal.""" + +from __future__ import annotations + +import dataclasses +from abc import ABC, abstractmethod +from collections.abc import ( + Awaitable, + Callable, + Mapping, + Sequence, +) +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +from enum import IntEnum +from typing import ( + TYPE_CHECKING, + Any, + Concatenate, + overload, +) + +import google.protobuf.duration_pb2 +import google.protobuf.timestamp_pb2 + +import temporalio.api.common.v1 +import temporalio.api.enums.v1 +import temporalio.api.schedule.v1 +import temporalio.api.taskqueue.v1 +import temporalio.api.workflow.v1 +import temporalio.api.workflowservice.v1 +import temporalio.common +import temporalio.converter +import temporalio.workflow +from temporalio.converter import ( + StorageDriverStoreContext, + StorageDriverWorkflowInfo, + WorkflowSerializationContext, +) + +from ..common import HeaderCodecBehavior +from ..types import ( + AnyType, + MethodAsyncNoParam, + MethodAsyncSingleParam, + MultiParamSpec, + ParamType, + ReturnType, + SelfType, +) +from ._helpers import _apply_headers, _encode_user_metadata +from ._interceptor import ( + BackfillScheduleInput, + DeleteScheduleInput, + DescribeScheduleInput, + PauseScheduleInput, + TriggerScheduleInput, + UnpauseScheduleInput, + UpdateScheduleInput, +) + +if TYPE_CHECKING: + from ._client import Client + from ._interceptor import ListSchedulesInput + + +class ScheduleHandle: + """Handle for interacting with a schedule. + + This is usually created via :py:meth:`Client.get_schedule_handle` or + returned from :py:meth:`Client.create_schedule`. + + Attributes: + id: ID of the schedule. + """ + + def __init__(self, client: Client, id: str) -> None: + """Create schedule handle.""" + self._client = client + self.id = id + + async def backfill( + self, + *backfill: ScheduleBackfill, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Backfill the schedule by going through the specified time periods as + if they passed right now. + + Args: + backfill: Backfill periods. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + """ + if not backfill: + raise ValueError("At least one backfill required") + await self._client._impl.backfill_schedule( + BackfillScheduleInput( + id=self.id, + backfills=backfill, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ), + ) + + async def delete( + self, + *, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Delete this schedule. + + Args: + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + """ + await self._client._impl.delete_schedule( + DeleteScheduleInput( + id=self.id, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ), + ) + + async def describe( + self, + *, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ScheduleDescription: + """Fetch this schedule's description. + + Args: + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + """ + return await self._client._impl.describe_schedule( + DescribeScheduleInput( + id=self.id, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ), + ) + + async def pause( + self, + *, + note: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Pause the schedule and set a note. + + Args: + note: Note to set on the schedule. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + """ + await self._client._impl.pause_schedule( + PauseScheduleInput( + id=self.id, + note=note, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ), + ) + + async def trigger( + self, + *, + overlap: ScheduleOverlapPolicy | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Trigger an action on this schedule to happen immediately. + + Args: + overlap: If set, overrides the schedule's overlap policy. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + """ + await self._client._impl.trigger_schedule( + TriggerScheduleInput( + id=self.id, + overlap=overlap, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ), + ) + + async def unpause( + self, + *, + note: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Unpause the schedule and set a note. + + Args: + note: Note to set on the schedule. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + """ + await self._client._impl.unpause_schedule( + UnpauseScheduleInput( + id=self.id, + note=note, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ), + ) + + @overload + async def update( + self, + updater: Callable[[ScheduleUpdateInput], ScheduleUpdate | None], + *, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: ... + + @overload + async def update( + self, + updater: Callable[[ScheduleUpdateInput], Awaitable[ScheduleUpdate | None]], + *, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: ... + + async def update( + self, + updater: Callable[ + [ScheduleUpdateInput], + ScheduleUpdate | None | Awaitable[ScheduleUpdate | None], + ], + *, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Update a schedule using a callback to build the update from the + description. + + The callback may be invoked multiple times in a conflict-resolution + loop. + + Args: + updater: Callback that returns the update. It accepts a + :py:class:`ScheduleUpdateInput` and returns a + :py:class:`ScheduleUpdate`. If None is returned or an error + occurs, the update is not attempted. This may be called multiple + times. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. This is for every call made + within. + rpc_timeout: Optional RPC deadline to set for the RPC call. This is + for each call made within, not overall. + """ + await self._client._impl.update_schedule( + UpdateScheduleInput( + id=self.id, + updater=updater, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ), + ) + + +@dataclass +class ScheduleSpec: + """Specification of the times scheduled actions may occur. + + The times are the union of :py:attr:`calendars`, :py:attr:`intervals`, and + :py:attr:`cron_expressions` excluding anything in :py:attr:`skip`. + """ + + calendars: Sequence[ScheduleCalendarSpec] = dataclasses.field(default_factory=list) + """Calendar-based specification of times.""" + + intervals: Sequence[ScheduleIntervalSpec] = dataclasses.field(default_factory=list) + """Interval-based specification of times.""" + + cron_expressions: Sequence[str] = dataclasses.field(default_factory=list) + """Cron-based specification of times. + + This is provided for easy migration from legacy string-based cron + scheduling. New uses should use :py:attr:`calendars` instead. These + expressions will be translated to calendar-based specifications on the + server. + """ + + skip: Sequence[ScheduleCalendarSpec] = dataclasses.field(default_factory=list) + """Set of matching calendar times that will be skipped.""" + + start_at: datetime | None = None + """Time before which any matching times will be skipped.""" + + end_at: datetime | None = None + """Time after which any matching times will be skipped.""" + + jitter: timedelta | None = None + """Jitter to apply each action. + + An action's scheduled time will be incremented by a random value between 0 + and this value if present (but not past the next schedule). + """ + + time_zone_name: str | None = None + """IANA time zone name, for example ``US/Central``.""" + + @staticmethod + def _from_proto(spec: temporalio.api.schedule.v1.ScheduleSpec) -> ScheduleSpec: + return ScheduleSpec( + calendars=[ + ScheduleCalendarSpec._from_proto(c) for c in spec.structured_calendar + ], + intervals=[ScheduleIntervalSpec._from_proto(i) for i in spec.interval], + cron_expressions=spec.cron_string, + skip=[ + ScheduleCalendarSpec._from_proto(c) + for c in spec.exclude_structured_calendar + ], + start_at=spec.start_time.ToDatetime().replace(tzinfo=timezone.utc) + if spec.HasField("start_time") + else None, + end_at=spec.end_time.ToDatetime().replace(tzinfo=timezone.utc) + if spec.HasField("end_time") + else None, + jitter=spec.jitter.ToTimedelta() if spec.HasField("jitter") else None, + time_zone_name=spec.timezone_name or None, + ) + + def _to_proto(self) -> temporalio.api.schedule.v1.ScheduleSpec: + start_time: google.protobuf.timestamp_pb2.Timestamp | None = None + if self.start_at: + start_time = google.protobuf.timestamp_pb2.Timestamp() + start_time.FromDatetime(self.start_at) + end_time: google.protobuf.timestamp_pb2.Timestamp | None = None + if self.end_at: + end_time = google.protobuf.timestamp_pb2.Timestamp() + end_time.FromDatetime(self.end_at) + jitter: google.protobuf.duration_pb2.Duration | None = None + if self.jitter: + jitter = google.protobuf.duration_pb2.Duration() + jitter.FromTimedelta(self.jitter) + return temporalio.api.schedule.v1.ScheduleSpec( + structured_calendar=[cal._to_proto() for cal in self.calendars], + cron_string=self.cron_expressions, + interval=[i._to_proto() for i in self.intervals], + exclude_structured_calendar=[cal._to_proto() for cal in self.skip], + start_time=start_time, + end_time=end_time, + jitter=jitter, + timezone_name=self.time_zone_name or "", + ) + + +@dataclass(frozen=True) +class ScheduleRange: + """Inclusive range for a schedule match value.""" + + start: int + """Inclusive start of the range.""" + + end: int = 0 + """Inclusive end of the range. + + If unset or less than start, defaults to start. + """ + + step: int = 0 + """ + Step to take between each value. + + Unset or 0 defaults as 1. + """ + + def __post_init__(self): + """Set field defaults.""" + # Class is frozen, so we must setattr bypassing dataclass setattr + if self.end < self.start: + object.__setattr__(self, "end", self.start) + if self.step == 0: + object.__setattr__(self, "step", 1) + + @staticmethod + def _from_protos( + ranges: Sequence[temporalio.api.schedule.v1.Range], + ) -> Sequence[ScheduleRange]: + return tuple(ScheduleRange._from_proto(r) for r in ranges) + + @staticmethod + def _from_proto(range: temporalio.api.schedule.v1.Range) -> ScheduleRange: + return ScheduleRange(start=range.start, end=range.end, step=range.step) + + @staticmethod + def _to_protos( + ranges: Sequence[ScheduleRange], + ) -> Sequence[temporalio.api.schedule.v1.Range]: + return tuple(r._to_proto() for r in ranges) + + def _to_proto(self) -> temporalio.api.schedule.v1.Range: + return temporalio.api.schedule.v1.Range( + start=self.start, end=self.end, step=self.step + ) + + +@dataclass +class ScheduleCalendarSpec: + """Specification relative to calendar time when to run an action. + + A timestamp matches if at least one range of each field matches except for + year. If year is missing, that means all years match. For all fields besides + year, at least one range must be present to match anything. + """ + + second: Sequence[ScheduleRange] = (ScheduleRange(0),) + """Second range to match, 0-59. Default matches 0.""" + + minute: Sequence[ScheduleRange] = (ScheduleRange(0),) + """Minute range to match, 0-59. Default matches 0.""" + + hour: Sequence[ScheduleRange] = (ScheduleRange(0),) + """Hour range to match, 0-23. Default matches 0.""" + + day_of_month: Sequence[ScheduleRange] = (ScheduleRange(1, 31),) + """Day of month range to match, 1-31. Default matches all days.""" + + month: Sequence[ScheduleRange] = (ScheduleRange(1, 12),) + """Month range to match, 1-12. Default matches all months.""" + + year: Sequence[ScheduleRange] = () + """Optional year range to match. Default of empty matches all years.""" + + day_of_week: Sequence[ScheduleRange] = (ScheduleRange(0, 6),) + """Day of week range to match, 0-6, 0 is Sunday. Default matches all + days.""" + + comment: str | None = None + """Description of this schedule.""" + + @staticmethod + def _from_proto( + spec: temporalio.api.schedule.v1.StructuredCalendarSpec, + ) -> ScheduleCalendarSpec: + return ScheduleCalendarSpec( + second=ScheduleRange._from_protos(spec.second), + minute=ScheduleRange._from_protos(spec.minute), + hour=ScheduleRange._from_protos(spec.hour), + day_of_month=ScheduleRange._from_protos(spec.day_of_month), + month=ScheduleRange._from_protos(spec.month), + year=ScheduleRange._from_protos(spec.year), + day_of_week=ScheduleRange._from_protos(spec.day_of_week), + comment=spec.comment or None, + ) + + def _to_proto(self) -> temporalio.api.schedule.v1.StructuredCalendarSpec: + return temporalio.api.schedule.v1.StructuredCalendarSpec( + second=ScheduleRange._to_protos(self.second), + minute=ScheduleRange._to_protos(self.minute), + hour=ScheduleRange._to_protos(self.hour), + day_of_month=ScheduleRange._to_protos(self.day_of_month), + month=ScheduleRange._to_protos(self.month), + year=ScheduleRange._to_protos(self.year), + day_of_week=ScheduleRange._to_protos(self.day_of_week), + comment=self.comment or "", + ) + + +@dataclass +class ScheduleIntervalSpec: + """Specification for scheduling on an interval. + + Matches times expressed as epoch + (n * every) + offset. + """ + + every: timedelta + """Period to repeat the interval.""" + + offset: timedelta | None = None + """Fixed offset added to each interval period.""" + + @staticmethod + def _from_proto( + spec: temporalio.api.schedule.v1.IntervalSpec, + ) -> ScheduleIntervalSpec: + return ScheduleIntervalSpec( + every=spec.interval.ToTimedelta(), + offset=spec.phase.ToTimedelta() if spec.HasField("phase") else None, + ) + + def _to_proto(self) -> temporalio.api.schedule.v1.IntervalSpec: + interval = google.protobuf.duration_pb2.Duration() + interval.FromTimedelta(self.every) + phase: google.protobuf.duration_pb2.Duration | None = None + if self.offset: + phase = google.protobuf.duration_pb2.Duration() + phase.FromTimedelta(self.offset) + return temporalio.api.schedule.v1.IntervalSpec(interval=interval, phase=phase) + + +class ScheduleAction(ABC): + """Base class for an action a schedule can take. + + See :py:class:`ScheduleActionStartWorkflow` for the most commonly used + implementation. + """ + + @staticmethod + def _from_proto( + action: temporalio.api.schedule.v1.ScheduleAction, + ) -> ScheduleAction: + if action.HasField("start_workflow"): + return ScheduleActionStartWorkflow._from_proto(action.start_workflow) + else: + raise ValueError(f"Unsupported action: {action.WhichOneof('action')}") + + @abstractmethod + async def _to_proto( + self, client: Client + ) -> temporalio.api.schedule.v1.ScheduleAction: ... + + +@dataclass +class ScheduleActionStartWorkflow(ScheduleAction): + """Schedule action to start a workflow.""" + + workflow: str + args: Sequence[Any] | Sequence[temporalio.api.common.v1.Payload] + id: str + task_queue: str + execution_timeout: timedelta | None + run_timeout: timedelta | None + task_timeout: timedelta | None + retry_policy: temporalio.common.RetryPolicy | None + memo: None | (Mapping[str, Any] | Mapping[str, temporalio.api.common.v1.Payload]) + typed_search_attributes: temporalio.common.TypedSearchAttributes + untyped_search_attributes: temporalio.common.SearchAttributes + """This is deprecated and is only present in case existing untyped + attributes already exist for update. This should never be used when + creating.""" + static_summary: str | temporalio.api.common.v1.Payload | None + static_details: str | temporalio.api.common.v1.Payload | None + priority: temporalio.common.Priority + + headers: Mapping[str, temporalio.api.common.v1.Payload] | None + """ + Headers may still be encoded by the payload codec if present. + """ + _from_raw: bool = dataclasses.field(compare=False, init=False) + + @staticmethod + def _from_proto( # pyright: ignore + info: temporalio.api.workflow.v1.NewWorkflowExecutionInfo, # type: ignore[override] + ) -> ScheduleActionStartWorkflow: + return ScheduleActionStartWorkflow("", raw_info=info) + + # Overload for no-param workflow + @overload + def __init__( + self, + workflow: MethodAsyncNoParam[SelfType, ReturnType], + *, + id: str, + task_queue: str, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + memo: Mapping[str, Any] | None = None, + typed_search_attributes: temporalio.common.TypedSearchAttributes = temporalio.common.TypedSearchAttributes.empty, + static_summary: str | None = None, + static_details: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + ) -> None: ... + + # Overload for single-param workflow + @overload + def __init__( + self, + workflow: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], + arg: ParamType, + *, + id: str, + task_queue: str, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + memo: Mapping[str, Any] | None = None, + typed_search_attributes: temporalio.common.TypedSearchAttributes = temporalio.common.TypedSearchAttributes.empty, + static_summary: str | None = None, + static_details: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + ) -> None: ... + + # Overload for multi-param workflow + @overload + def __init__( + self, + workflow: Callable[ + Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType] + ], + *, + args: Sequence[Any], + id: str, + task_queue: str, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + memo: Mapping[str, Any] | None = None, + typed_search_attributes: temporalio.common.TypedSearchAttributes = temporalio.common.TypedSearchAttributes.empty, + static_summary: str | None = None, + static_details: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + ) -> None: ... + + # Overload for string-name workflow + @overload + def __init__( + self, + workflow: str, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + memo: Mapping[str, Any] | None = None, + typed_search_attributes: temporalio.common.TypedSearchAttributes = temporalio.common.TypedSearchAttributes.empty, + static_summary: str | None = None, + static_details: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + ) -> None: ... + + # Overload for raw info + @overload + def __init__( + self, + workflow: str, + *, + raw_info: temporalio.api.workflow.v1.NewWorkflowExecutionInfo, + ) -> None: ... + + def __init__( + self, + workflow: str | Callable[..., Awaitable[Any]], + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str | None = None, + task_queue: str | None = None, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + memo: Mapping[str, Any] | None = None, + typed_search_attributes: temporalio.common.TypedSearchAttributes = temporalio.common.TypedSearchAttributes.empty, + untyped_search_attributes: temporalio.common.SearchAttributes = {}, + static_summary: str | None = None, + static_details: str | None = None, + headers: Mapping[str, temporalio.api.common.v1.Payload] | None = None, + raw_info: temporalio.api.workflow.v1.NewWorkflowExecutionInfo | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + ) -> None: + """Create a start-workflow action. + + See :py:meth:`Client.start_workflow` for details on these parameter + values. + """ + super().__init__() + if raw_info: + self._from_raw = True + # Ignore other fields + self.workflow = raw_info.workflow_type.name + self.args = raw_info.input.payloads if raw_info.input else [] + self.id = raw_info.workflow_id + self.task_queue = raw_info.task_queue.name + self.execution_timeout = ( + raw_info.workflow_execution_timeout.ToTimedelta() + if raw_info.HasField("workflow_execution_timeout") + else None + ) + self.run_timeout = ( + raw_info.workflow_run_timeout.ToTimedelta() + if raw_info.HasField("workflow_run_timeout") + else None + ) + self.task_timeout = ( + raw_info.workflow_task_timeout.ToTimedelta() + if raw_info.HasField("workflow_task_timeout") + else None + ) + self.retry_policy = ( + temporalio.common.RetryPolicy.from_proto(raw_info.retry_policy) + if raw_info.HasField("retry_policy") + else None + ) + self.memo = raw_info.memo.fields if raw_info.memo.fields else None + self.typed_search_attributes = ( + temporalio.converter.decode_typed_search_attributes( + raw_info.search_attributes + ) + ) + self.headers = raw_info.header.fields if raw_info.header.fields else None + # Also set the untyped attributes as the set of attributes from + # decode with the typed ones removed + self.untyped_search_attributes = ( + temporalio.converter.decode_search_attributes( + raw_info.search_attributes + ) + ) + for pair in self.typed_search_attributes: + if pair.key.name in self.untyped_search_attributes: + # We know this is mutable here + del self.untyped_search_attributes[pair.key.name] # type: ignore + self.static_summary = ( + raw_info.user_metadata.summary + if raw_info.HasField("user_metadata") and raw_info.user_metadata.summary + else None + ) + self.static_details = ( + raw_info.user_metadata.details + if raw_info.HasField("user_metadata") and raw_info.user_metadata.details + else None + ) + self.priority = ( + temporalio.common.Priority._from_proto(raw_info.priority) + if raw_info.HasField("priority") and raw_info.priority + else temporalio.common.Priority.default + ) + else: + self._from_raw = False + if not id: + raise ValueError("ID required") + if not task_queue: + raise ValueError("Task queue required") + # Use definition if callable + if callable(workflow): + defn = temporalio.workflow._Definition.must_from_run_fn(workflow) + if not defn.name: + raise ValueError("Cannot schedule dynamic workflow explicitly") + workflow = defn.name + elif not isinstance(workflow, str): + raise TypeError("Workflow must be a string or callable") # type:ignore[reportUnreachable] + self.workflow = workflow + self.args = temporalio.common._arg_or_args(arg, args) + self.id = id + self.task_queue = task_queue + self.execution_timeout = execution_timeout + self.run_timeout = run_timeout + self.task_timeout = task_timeout + self.retry_policy = retry_policy + self.memo = memo + self.typed_search_attributes = typed_search_attributes + self.untyped_search_attributes = untyped_search_attributes + self.headers = headers # encode here + self.static_summary = static_summary + self.static_details = static_details + self.priority = priority + + async def _to_proto( + self, client: Client + ) -> temporalio.api.schedule.v1.ScheduleAction: + execution_timeout: google.protobuf.duration_pb2.Duration | None = None + if self.execution_timeout: + execution_timeout = google.protobuf.duration_pb2.Duration() + execution_timeout.FromTimedelta(self.execution_timeout) + run_timeout: google.protobuf.duration_pb2.Duration | None = None + if self.run_timeout: + run_timeout = google.protobuf.duration_pb2.Duration() + run_timeout.FromTimedelta(self.run_timeout) + task_timeout: google.protobuf.duration_pb2.Duration | None = None + if self.task_timeout: + task_timeout = google.protobuf.duration_pb2.Duration() + task_timeout.FromTimedelta(self.task_timeout) + retry_policy: temporalio.api.common.v1.RetryPolicy | None = None + if self.retry_policy: + retry_policy = temporalio.api.common.v1.RetryPolicy() + self.retry_policy.apply_to_proto(retry_policy) + priority: temporalio.api.common.v1.Priority | None = None + if self.priority: + priority = self.priority._to_proto() + data_converter = client.data_converter._with_contexts( + WorkflowSerializationContext( + namespace=client.namespace, + workflow_id=self.id, + ), + StorageDriverStoreContext( + target=StorageDriverWorkflowInfo( + id=self.id, type=self.workflow, namespace=client.namespace + ), + ), + ) + action = temporalio.api.schedule.v1.ScheduleAction( + start_workflow=temporalio.api.workflow.v1.NewWorkflowExecutionInfo( + workflow_id=self.id, + workflow_type=temporalio.api.common.v1.WorkflowType(name=self.workflow), + task_queue=temporalio.api.taskqueue.v1.TaskQueue(name=self.task_queue), + input=( + temporalio.api.common.v1.Payloads( + payloads=[ + a + if isinstance(a, temporalio.api.common.v1.Payload) + else (await data_converter.encode([a]))[0] + for a in self.args + ] + ) + if self.args + else None + ), + workflow_execution_timeout=execution_timeout, + workflow_run_timeout=run_timeout, + workflow_task_timeout=task_timeout, + retry_policy=retry_policy, + memo=await data_converter._encode_memo(self.memo) + if self.memo + else None, + user_metadata=await _encode_user_metadata( + data_converter, self.static_summary, self.static_details + ), + priority=priority, + ), + ) + # Add any untyped attributes that are not also in the typed set + untyped_not_in_typed = { + k: v + for k, v in self.untyped_search_attributes.items() + if k not in self.typed_search_attributes + } + if untyped_not_in_typed: + temporalio.converter.encode_search_attributes( + untyped_not_in_typed, action.start_workflow.search_attributes + ) + # TODO (dan): confirm whether this be `is not None` + if self.typed_search_attributes: + temporalio.converter.encode_search_attributes( + self.typed_search_attributes, + action.start_workflow.search_attributes, + ) + if self.headers: + await _apply_headers( + self.headers, + action.start_workflow.header.fields, + client.config(active_config=True)["header_codec_behavior"] + == HeaderCodecBehavior.CODEC + and not self._from_raw, + client.data_converter, + ) + return action + + +class ScheduleOverlapPolicy(IntEnum): + """Controls what happens when a workflow would be started by a schedule but + one is already running. + """ + + SKIP = int( + temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_SKIP + ) + """Don't start anything. + + When the workflow completes, the next scheduled event after that time will + be considered. + """ + + BUFFER_ONE = int( + temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_BUFFER_ONE + ) + """Start the workflow again soon as the current one completes, but only + buffer one start in this way. + + If another start is supposed to happen when the workflow is running, and one + is already buffered, then only the first one will be started after the + running workflow finishes. + """ + + BUFFER_ALL = int( + temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_BUFFER_ALL + ) + """Buffer up any number of starts to all happen sequentially, immediately + after the running workflow completes.""" + + CANCEL_OTHER = int( + temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_CANCEL_OTHER + ) + """If there is another workflow running, cancel it, and start the new one + after the old one completes cancellation.""" + + TERMINATE_OTHER = int( + temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_TERMINATE_OTHER + ) + """If there is another workflow running, terminate it and start the new one + immediately.""" + + ALLOW_ALL = int( + temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_ALLOW_ALL + ) + """Start any number of concurrent workflows. + + Note that with this policy, last completion result and last failure will not + be available since workflows are not sequential.""" + + +@dataclass +class ScheduleBackfill: + """Time period and policy for actions taken as if the time passed right + now. + """ + + start_at: datetime + """Start of the range to evaluate the schedule in. + + This is exclusive + """ + end_at: datetime + overlap: ScheduleOverlapPolicy | None = None + + def _to_proto(self) -> temporalio.api.schedule.v1.BackfillRequest: + start_time = google.protobuf.timestamp_pb2.Timestamp() + start_time.FromDatetime(self.start_at) + end_time = google.protobuf.timestamp_pb2.Timestamp() + end_time.FromDatetime(self.end_at) + overlap_policy = temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_UNSPECIFIED + if self.overlap: + overlap_policy = temporalio.api.enums.v1.ScheduleOverlapPolicy.ValueType( + self.overlap + ) + return temporalio.api.schedule.v1.BackfillRequest( + start_time=start_time, + end_time=end_time, + overlap_policy=overlap_policy, + ) + + +@dataclass +class SchedulePolicy: + """Policies of a schedule.""" + + overlap: ScheduleOverlapPolicy = dataclasses.field( + default_factory=lambda: ScheduleOverlapPolicy.SKIP + ) + """Controls what happens when an action is started while another is still + running.""" + + catchup_window: timedelta = timedelta(days=365) + """After a Temporal server is unavailable, amount of time in the past to + execute missed actions.""" + + pause_on_failure: bool = False + """Whether to pause the schedule if an action fails or times out. + + Note: For workflows, this only applies after all retries have been + exhausted. + """ + + @staticmethod + def _from_proto(pol: temporalio.api.schedule.v1.SchedulePolicies) -> SchedulePolicy: + return SchedulePolicy( + overlap=ScheduleOverlapPolicy(int(pol.overlap_policy)), + catchup_window=pol.catchup_window.ToTimedelta(), + pause_on_failure=pol.pause_on_failure, + ) + + def _to_proto(self) -> temporalio.api.schedule.v1.SchedulePolicies: + catchup_window = google.protobuf.duration_pb2.Duration() + catchup_window.FromTimedelta(self.catchup_window) + return temporalio.api.schedule.v1.SchedulePolicies( + overlap_policy=temporalio.api.enums.v1.ScheduleOverlapPolicy.ValueType( + self.overlap + ), + catchup_window=catchup_window, + pause_on_failure=self.pause_on_failure, + ) + + +@dataclass +class ScheduleState: + """State of a schedule.""" + + note: str | None = None + """Human readable message for the schedule. + + The system may overwrite this value on certain conditions like + pause-on-failure. + """ + + paused: bool = False + """Whether the schedule is paused.""" + + # Cannot be set to True on create + limited_actions: bool = False + """ + If true, remaining actions will be decremented for each action taken. + + On schedule create, this must be set to true if :py:attr:`remaining_actions` + is non-zero and left false if :py:attr:`remaining_actions` is zero. + """ + + remaining_actions: int = 0 + """Actions remaining on this schedule. + + Once this number hits 0, no further actions are scheduled automatically. + """ + + @staticmethod + def _from_proto(state: temporalio.api.schedule.v1.ScheduleState) -> ScheduleState: + return ScheduleState( + note=state.notes or None, + paused=state.paused, + limited_actions=state.limited_actions, + remaining_actions=state.remaining_actions, + ) + + def _to_proto(self) -> temporalio.api.schedule.v1.ScheduleState: + return temporalio.api.schedule.v1.ScheduleState( + notes=self.note or "", + paused=self.paused, + limited_actions=self.limited_actions, + remaining_actions=self.remaining_actions, + ) + + +@dataclass +class Schedule: + """A schedule for periodically running an action.""" + + action: ScheduleAction + """Action taken when scheduled.""" + + spec: ScheduleSpec + """When the action is taken.""" + + policy: SchedulePolicy = dataclasses.field(default_factory=SchedulePolicy) + """Schedule policies.""" + + state: ScheduleState = dataclasses.field(default_factory=ScheduleState) + """State of the schedule.""" + + @staticmethod + def _from_proto(sched: temporalio.api.schedule.v1.Schedule) -> Schedule: + return Schedule( + action=ScheduleAction._from_proto(sched.action), + spec=ScheduleSpec._from_proto(sched.spec), + policy=SchedulePolicy._from_proto(sched.policies), + state=ScheduleState._from_proto(sched.state), + ) + + async def _to_proto(self, client: Client) -> temporalio.api.schedule.v1.Schedule: + catchup_window = google.protobuf.duration_pb2.Duration() + catchup_window.FromTimedelta(self.policy.catchup_window) + return temporalio.api.schedule.v1.Schedule( + spec=self.spec._to_proto(), + action=await self.action._to_proto(client), + policies=self.policy._to_proto(), + state=self.state._to_proto(), + ) + + +@dataclass +class ScheduleDescription: + """Description of a schedule.""" + + id: str + """ID of the schedule.""" + + schedule: Schedule + """Schedule details that can be mutated.""" + + info: ScheduleInfo + """Information about the schedule.""" + + typed_search_attributes: temporalio.common.TypedSearchAttributes + """Search attributes on the schedule.""" + + search_attributes: temporalio.common.SearchAttributes + """Search attributes on the schedule. + + .. deprecated:: + Use :py:attr:`typed_search_attributes` instead. + """ + + data_converter: temporalio.converter.DataConverter + """Data converter used for memo decoding.""" + + raw_description: temporalio.api.workflowservice.v1.DescribeScheduleResponse + """Raw description of the schedule.""" + + @staticmethod + def _from_proto( + id: str, + desc: temporalio.api.workflowservice.v1.DescribeScheduleResponse, + converter: temporalio.converter.DataConverter, + ) -> ScheduleDescription: + return ScheduleDescription( + id=id, + schedule=Schedule._from_proto(desc.schedule), + info=ScheduleInfo._from_proto(desc.info), + typed_search_attributes=temporalio.converter.decode_typed_search_attributes( + desc.search_attributes + ), + search_attributes=temporalio.converter.decode_search_attributes( + desc.search_attributes + ), + data_converter=converter, + raw_description=desc, + ) + + async def memo(self) -> Mapping[str, Any]: + """Schedule's memo values, converted without type hints. + + Since type hints are not used, the default converted values will come + back. For example, if the memo was originally created with a dataclass, + the value will be a dict. To convert using proper type hints, use + :py:meth:`memo_value`. + + Returns: + Mapping of all memo keys and they values without type hints. + """ + return await self.data_converter._decode_memo(self.raw_description.memo) + + @overload + async def memo_value( + self, key: str, default: Any = temporalio.common._arg_unset + ) -> Any: ... + + @overload + async def memo_value( + self, key: str, *, type_hint: type[ParamType] + ) -> ParamType: ... + + @overload + async def memo_value( + self, key: str, default: AnyType, *, type_hint: type[ParamType] + ) -> AnyType | ParamType: ... + + async def memo_value( + self, + key: str, + default: Any = temporalio.common._arg_unset, + *, + type_hint: type | None = None, + ) -> Any: + """Memo value for the given key, optional default, and optional type + hint. + + Args: + key: Key to get memo value for. + default: Default to use if key is not present. If unset, a + :py:class:`KeyError` is raised when the key does not exist. + type_hint: type hint to use when converting. + + Returns: + Memo value, converted with the type hint if present. + + Raises: + KeyError: Key not present and default not set. + """ + return await self.data_converter._decode_memo_field( + self.raw_description.memo, key, default, type_hint + ) + + +@dataclass +class ScheduleInfo: + """Information about a schedule.""" + + num_actions: int + """Number of actions taken by this schedule.""" + + num_actions_missed_catchup_window: int + """Number of times an action was skipped due to missing the catchup + window.""" + + num_actions_skipped_overlap: int + """Number of actions skipped due to overlap.""" + + running_actions: Sequence[ScheduleActionExecution] + """Currently running actions.""" + + recent_actions: Sequence[ScheduleActionResult] + """10 most recent actions, oldest first.""" + + next_action_times: Sequence[datetime] + """Next 10 scheduled action times.""" + + created_at: datetime + """When the schedule was created.""" + + last_updated_at: datetime | None + """When the schedule was last updated.""" + + @staticmethod + def _from_proto(info: temporalio.api.schedule.v1.ScheduleInfo) -> ScheduleInfo: + return ScheduleInfo( + num_actions=info.action_count, + num_actions_missed_catchup_window=info.missed_catchup_window, + num_actions_skipped_overlap=info.overlap_skipped, + running_actions=[ + ScheduleActionExecutionStartWorkflow._from_proto(r) + for r in info.running_workflows + ], + recent_actions=[ + ScheduleActionResult._from_proto(r) for r in info.recent_actions + ], + next_action_times=[ + f.ToDatetime().replace(tzinfo=timezone.utc) + for f in info.future_action_times + ], + created_at=info.create_time.ToDatetime().replace(tzinfo=timezone.utc), + last_updated_at=info.update_time.ToDatetime().replace(tzinfo=timezone.utc) + if info.HasField("update_time") + else None, + ) + + +class ScheduleActionExecution(ABC): + """Base class for an action execution.""" + + pass + + +@dataclass +class ScheduleActionExecutionStartWorkflow(ScheduleActionExecution): + """Execution of a scheduled workflow start.""" + + workflow_id: str + """Workflow ID.""" + + first_execution_run_id: str + """Workflow run ID.""" + + @staticmethod + def _from_proto( + exec: temporalio.api.common.v1.WorkflowExecution, + ) -> ScheduleActionExecutionStartWorkflow: + return ScheduleActionExecutionStartWorkflow( + workflow_id=exec.workflow_id, + first_execution_run_id=exec.run_id, + ) + + +@dataclass +class ScheduleActionResult: + """Information about when an action took place.""" + + scheduled_at: datetime + """Scheduled time of the action including jitter.""" + + started_at: datetime + """When the action actually started.""" + + action: ScheduleActionExecution + """Action that took place.""" + + @staticmethod + def _from_proto( + res: temporalio.api.schedule.v1.ScheduleActionResult, + ) -> ScheduleActionResult: + return ScheduleActionResult( + scheduled_at=res.schedule_time.ToDatetime().replace(tzinfo=timezone.utc), + started_at=res.actual_time.ToDatetime().replace(tzinfo=timezone.utc), + action=ScheduleActionExecutionStartWorkflow._from_proto( + res.start_workflow_result + ), + ) + + +@dataclass +class ScheduleUpdateInput: + """Parameter for an update callback for :py:meth:`ScheduleHandle.update`.""" + + description: ScheduleDescription + """Current description of the schedule.""" + + +@dataclass +class ScheduleUpdate: + """Result of an update callback for :py:meth:`ScheduleHandle.update`.""" + + schedule: Schedule + """Schedule to update.""" + + search_attributes: temporalio.common.TypedSearchAttributes | None = None + """Search attributes to update.""" + + +@dataclass +class ScheduleListDescription: + """Description of a listed schedule.""" + + id: str + """ID of the schedule.""" + + schedule: ScheduleListSchedule | None + """Schedule details that can be mutated. + + This may not be present in older Temporal servers without advanced + visibility. + """ + + info: ScheduleListInfo | None + """Information about the schedule. + + This may not be present in older Temporal servers without advanced + visibility. + """ + + typed_search_attributes: temporalio.common.TypedSearchAttributes + """Search attributes on the schedule.""" + + search_attributes: temporalio.common.SearchAttributes + """Search attributes on the schedule. + + .. deprecated:: + Use :py:attr:`typed_search_attributes` instead. + """ + + data_converter: temporalio.converter.DataConverter + """Data converter used for memo decoding.""" + + raw_entry: temporalio.api.schedule.v1.ScheduleListEntry + """Raw description of the schedule.""" + + @staticmethod + def _from_proto( + entry: temporalio.api.schedule.v1.ScheduleListEntry, + converter: temporalio.converter.DataConverter, + ) -> ScheduleListDescription: + return ScheduleListDescription( + id=entry.schedule_id, + schedule=ScheduleListSchedule._from_proto(entry.info) + if entry.HasField("info") + else None, + info=ScheduleListInfo._from_proto(entry.info) + if entry.HasField("info") + else None, + typed_search_attributes=temporalio.converter.decode_typed_search_attributes( + entry.search_attributes + ), + search_attributes=temporalio.converter.decode_search_attributes( + entry.search_attributes + ), + data_converter=converter, + raw_entry=entry, + ) + + async def memo(self) -> Mapping[str, Any]: + """Schedule's memo values, converted without type hints. + + Since type hints are not used, the default converted values will come + back. For example, if the memo was originally created with a dataclass, + the value will be a dict. To convert using proper type hints, use + :py:meth:`memo_value`. + + Returns: + Mapping of all memo keys and they values without type hints. + """ + return await self.data_converter._decode_memo(self.raw_entry.memo) + + @overload + async def memo_value( + self, key: str, default: Any = temporalio.common._arg_unset + ) -> Any: ... + + @overload + async def memo_value( + self, key: str, *, type_hint: type[ParamType] + ) -> ParamType: ... + + @overload + async def memo_value( + self, key: str, default: AnyType, *, type_hint: type[ParamType] + ) -> AnyType | ParamType: ... + + async def memo_value( + self, + key: str, + default: Any = temporalio.common._arg_unset, + *, + type_hint: type | None = None, + ) -> Any: + """Memo value for the given key, optional default, and optional type + hint. + + Args: + key: Key to get memo value for. + default: Default to use if key is not present. If unset, a + :py:class:`KeyError` is raised when the key does not exist. + type_hint: type hint to use when converting. + + Returns: + Memo value, converted with the type hint if present. + + Raises: + KeyError: Key not present and default not set. + """ + return await self.data_converter._decode_memo_field( + self.raw_entry.memo, key, default, type_hint + ) + + +@dataclass +class ScheduleListSchedule: + """Details for a listed schedule.""" + + action: ScheduleListAction + """Action taken when scheduled.""" + + spec: ScheduleSpec + """When the action is taken.""" + + state: ScheduleListState + """State of the schedule.""" + + @staticmethod + def _from_proto( + info: temporalio.api.schedule.v1.ScheduleListInfo, + ) -> ScheduleListSchedule: + # Only start workflow supported for now + if not info.HasField("workflow_type"): + raise ValueError("Unknown action on schedule") + return ScheduleListSchedule( + action=ScheduleListActionStartWorkflow(workflow=info.workflow_type.name), + spec=ScheduleSpec._from_proto(info.spec), + state=ScheduleListState._from_proto(info), + ) + + +class ScheduleListAction(ABC): + """Base class for an action a listed schedule can take.""" + + pass + + +@dataclass +class ScheduleListActionStartWorkflow(ScheduleListAction): + """Action to start a workflow on a listed schedule.""" + + workflow: str + """Workflow type name.""" + + +@dataclass +class ScheduleListInfo: + """Information about a listed schedule.""" + + recent_actions: Sequence[ScheduleActionResult] + """Most recent actions, oldest first. + + This may be a smaller amount than present on + :py:attr:`ScheduleDescription.info`. + """ + + next_action_times: Sequence[datetime] + """Next scheduled action times. + + This may be a smaller amount than present on + :py:attr:`ScheduleDescription.info`. + """ + + @staticmethod + def _from_proto( + info: temporalio.api.schedule.v1.ScheduleListInfo, + ) -> ScheduleListInfo: + return ScheduleListInfo( + recent_actions=[ + ScheduleActionResult._from_proto(r) for r in info.recent_actions + ], + next_action_times=[ + f.ToDatetime().replace(tzinfo=timezone.utc) + for f in info.future_action_times + ], + ) + + +@dataclass +class ScheduleListState: + """State of a listed schedule.""" + + note: str | None + """Human readable message for the schedule. + + The system may overwrite this value on certain conditions like + pause-on-failure. + """ + + paused: bool + """Whether the schedule is paused.""" + + @staticmethod + def _from_proto( + info: temporalio.api.schedule.v1.ScheduleListInfo, + ) -> ScheduleListState: + return ScheduleListState( + note=info.notes or None, + paused=info.paused, + ) + + +class ScheduleAsyncIterator: + """Asynchronous iterator for :py:class:`ScheduleListDescription` values. + + Most users should use ``async for`` on this iterator and not call any of the + methods within. + """ + + def __init__( + self, + client: Client, + input: ListSchedulesInput, + ) -> None: + """Create an asynchronous iterator for the given input. + + Users should not create this directly, but rather use + :py:meth:`Client.list_schedules`. + """ + self._client = client + self._input = input + self._next_page_token = input.next_page_token + self._current_page: Sequence[ScheduleListDescription] | None = None + self._current_page_index = 0 + + @property + def current_page_index(self) -> int: + """Index of the entry in the current page that will be returned from + the next :py:meth:`__anext__` call. + """ + return self._current_page_index + + @property + def current_page(self) -> Sequence[ScheduleListDescription] | None: + """Current page, if it has been fetched yet.""" + return self._current_page + + @property + def next_page_token(self) -> bytes | None: + """Token for the next page request if any.""" + return self._next_page_token + + async def fetch_next_page(self, *, page_size: int | None = None) -> None: + """Fetch the next page if any. + + Args: + page_size: Override the page size this iterator was originally + created with. + """ + resp = await self._client.workflow_service.list_schedules( + temporalio.api.workflowservice.v1.ListSchedulesRequest( + namespace=self._client.namespace, + maximum_page_size=page_size or self._input.page_size, + next_page_token=self._next_page_token or b"", + query=self._input.query or "", + ), + retry=True, + metadata=self._input.rpc_metadata, + timeout=self._input.rpc_timeout, + ) + self._current_page = [ + ScheduleListDescription._from_proto(v, self._client.data_converter) + for v in resp.schedules + ] + self._current_page_index = 0 + self._next_page_token = resp.next_page_token or None + + def __aiter__(self) -> ScheduleAsyncIterator: + """Return self as the iterator.""" + return self + + async def __anext__(self) -> ScheduleListDescription: + """Get the next execution on this iterator, fetching next page if + necessary. + """ + while True: + # No page? fetch and continue + if self._current_page is None: + await self.fetch_next_page() + continue + # No more left in page? + if self._current_page_index >= len(self._current_page): + # If there is a next page token, try to get another page and try + # again + if self._next_page_token is not None: + await self.fetch_next_page() + continue + # No more pages means we're done + raise StopAsyncIteration + # Get current, increment page index, and return + ret = self._current_page[self._current_page_index] + self._current_page_index += 1 + return ret diff --git a/temporalio/client/_worker_versioning.py b/temporalio/client/_worker_versioning.py new file mode 100644 index 000000000..d6ef8f257 --- /dev/null +++ b/temporalio/client/_worker_versioning.py @@ -0,0 +1,278 @@ +"""Client support for accessing Temporal.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import ( + Mapping, + Sequence, +) +from dataclasses import dataclass +from enum import Enum + +import temporalio.api.enums.v1 +import temporalio.api.workflowservice.v1 + + +@dataclass(frozen=True) +class WorkerBuildIdVersionSets: + """Represents the sets of compatible Build ID versions associated with some Task Queue, as + fetched by :py:meth:`Client.get_worker_build_id_compatibility`. + """ + + version_sets: Sequence[BuildIdVersionSet] + """All version sets that were fetched for this task queue.""" + + def default_set(self) -> BuildIdVersionSet: + """Returns the default version set for this task queue.""" + return self.version_sets[-1] + + def default_build_id(self) -> str: + """Returns the default Build ID for this task queue.""" + return self.default_set().default() + + @staticmethod + def _from_proto( + resp: temporalio.api.workflowservice.v1.GetWorkerBuildIdCompatibilityResponse, + ) -> WorkerBuildIdVersionSets: + return WorkerBuildIdVersionSets( + version_sets=[ + BuildIdVersionSet(mvs.build_ids) for mvs in resp.major_version_sets + ] + ) + + +@dataclass(frozen=True) +class BuildIdVersionSet: + """A set of Build IDs which are compatible with each other.""" + + build_ids: Sequence[str] + """All Build IDs contained in the set.""" + + def default(self) -> str: + """Returns the default Build ID for this set.""" + return self.build_ids[-1] + + +class BuildIdOp(ABC): + """Base class for Build ID operations as used by + :py:meth:`Client.update_worker_build_id_compatibility`. + """ + + @abstractmethod + def _as_partial_proto( + self, + ) -> temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest: + """Returns a partial request with the operation populated. Caller must populate + non-operation fields. This is done b/c there's no good way to assign a non-primitive message + as the operation after initializing the request. + """ + ... + + +@dataclass(frozen=True) +class BuildIdOpAddNewDefault(BuildIdOp): + """Adds a new Build Id into a new set, which will be used as the default set for + the queue. This means all new workflows will start on this Build Id. + """ + + build_id: str + + def _as_partial_proto( + self, + ) -> temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest: + return ( + temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest( + add_new_build_id_in_new_default_set=self.build_id + ) + ) + + +@dataclass(frozen=True) +class BuildIdOpAddNewCompatible(BuildIdOp): + """Adds a new Build Id into an existing compatible set. The newly added ID becomes + the default for that compatible set, and thus new workflow tasks for workflows which have been + executing on workers in that set will now start on this new Build Id. + """ + + build_id: str + """The Build Id to add to the compatible set.""" + + existing_compatible_build_id: str + """A Build Id which must already be defined on the task queue, and is used to find the + compatible set to add the new id to. + """ + + promote_set: bool = False + """If set to true, the targeted set will also be promoted to become the overall default set for + the queue.""" + + def _as_partial_proto( + self, + ) -> temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest: + return temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest( + add_new_compatible_build_id=temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest.AddNewCompatibleVersion( + new_build_id=self.build_id, + existing_compatible_build_id=self.existing_compatible_build_id, + make_set_default=self.promote_set, + ) + ) + + +@dataclass(frozen=True) +class BuildIdOpPromoteSetByBuildId(BuildIdOp): + """Promotes a set of compatible Build Ids to become the current default set for the task queue. + Any Build Id in the set may be used to target it. + """ + + build_id: str + """A Build Id which must already be defined on the task queue, and is used to find the + compatible set to promote.""" + + def _as_partial_proto( + self, + ) -> temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest: + return ( + temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest( + promote_set_by_build_id=self.build_id + ) + ) + + +@dataclass(frozen=True) +class BuildIdOpPromoteBuildIdWithinSet(BuildIdOp): + """Promotes a Build Id within an existing set to become the default ID for that set.""" + + build_id: str + + def _as_partial_proto( + self, + ) -> temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest: + return ( + temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest( + promote_build_id_within_set=self.build_id + ) + ) + + +@dataclass(frozen=True) +class BuildIdOpMergeSets(BuildIdOp): + """Merges two sets into one set, thus declaring all the Build Ids in both as compatible with one + another. The default of the primary set is maintained as the merged set's overall default. + """ + + primary_build_id: str + """A Build Id which and is used to find the primary set to be merged.""" + + secondary_build_id: str + """A Build Id which and is used to find the secondary set to be merged.""" + + def _as_partial_proto( + self, + ) -> temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest: + return temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest( + merge_sets=temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest.MergeSets( + primary_set_build_id=self.primary_build_id, + secondary_set_build_id=self.secondary_build_id, + ) + ) + + +@dataclass(frozen=True) +class WorkerTaskReachability: + """Contains information about the reachability of some Build IDs""" + + build_id_reachability: Mapping[str, BuildIdReachability] + """Maps Build IDs to information about their reachability""" + + @staticmethod + def _from_proto( + resp: temporalio.api.workflowservice.v1.GetWorkerTaskReachabilityResponse, + ) -> WorkerTaskReachability: + mapping = dict() + for bid_reach in resp.build_id_reachability: + tq_mapping = dict() + unretrieved = set() + for tq_reach in bid_reach.task_queue_reachability: + if tq_reach.reachability == [ + temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_UNSPECIFIED + ]: + unretrieved.add(tq_reach.task_queue) + continue + tq_mapping[tq_reach.task_queue] = [ + TaskReachabilityType._from_proto(r) for r in tq_reach.reachability + ] + + mapping[bid_reach.build_id] = BuildIdReachability( + task_queue_reachability=tq_mapping, + unretrieved_task_queues=frozenset(unretrieved), + ) + + return WorkerTaskReachability(build_id_reachability=mapping) + + +@dataclass(frozen=True) +class BuildIdReachability: + """Contains information about the reachability of a specific Build ID""" + + task_queue_reachability: Mapping[str, Sequence[TaskReachabilityType]] + """Maps Task Queue names to the reachability status of the Build ID on that queue. If the value + is an empty list, the Build ID is not reachable on that queue. + """ + + unretrieved_task_queues: frozenset[str] + """If any Task Queues could not be retrieved because the server limits the number that can be + queried at once, they will be listed here. + """ + + +class TaskReachabilityType(Enum): + """Enumerates how a task might reach certain kinds of workflows""" + + NEW_WORKFLOWS = 1 + EXISTING_WORKFLOWS = 2 + OPEN_WORKFLOWS = 3 + CLOSED_WORKFLOWS = 4 + + @staticmethod + def _from_proto( + reachability: temporalio.api.enums.v1.TaskReachability.ValueType, + ) -> TaskReachabilityType: + if ( + reachability + == temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_NEW_WORKFLOWS + ): + return TaskReachabilityType.NEW_WORKFLOWS + elif ( + reachability + == temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_EXISTING_WORKFLOWS + ): + return TaskReachabilityType.EXISTING_WORKFLOWS + elif ( + reachability + == temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_OPEN_WORKFLOWS + ): + return TaskReachabilityType.OPEN_WORKFLOWS + elif ( + reachability + == temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_CLOSED_WORKFLOWS + ): + return TaskReachabilityType.CLOSED_WORKFLOWS + else: + raise ValueError(f"Cannot convert reachability type: {reachability}") + + def _to_proto(self) -> temporalio.api.enums.v1.TaskReachability.ValueType: + if self == TaskReachabilityType.NEW_WORKFLOWS: + return ( + temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_NEW_WORKFLOWS + ) + elif self == TaskReachabilityType.EXISTING_WORKFLOWS: + return temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_EXISTING_WORKFLOWS + elif self == TaskReachabilityType.OPEN_WORKFLOWS: + return temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_OPEN_WORKFLOWS + elif self == TaskReachabilityType.CLOSED_WORKFLOWS: + return temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_CLOSED_WORKFLOWS + else: + return ( + temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_UNSPECIFIED + ) diff --git a/temporalio/client/_workflow.py b/temporalio/client/_workflow.py new file mode 100644 index 000000000..6b3559c31 --- /dev/null +++ b/temporalio/client/_workflow.py @@ -0,0 +1,2000 @@ +"""Client support for accessing Temporal.""" + +from __future__ import annotations + +import asyncio +import functools +import warnings +from asyncio import Future +from collections.abc import ( + AsyncIterator, + Awaitable, + Callable, + Mapping, + Sequence, +) +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +from enum import IntEnum +from typing import ( + TYPE_CHECKING, + Any, + Concatenate, + Generic, + cast, + overload, +) + +import google.protobuf.json_format +from typing_extensions import Self + +import temporalio.api.common.v1 +import temporalio.api.enums.v1 +import temporalio.api.history.v1 +import temporalio.api.update.v1 +import temporalio.api.workflow.v1 +import temporalio.api.workflowservice.v1 +import temporalio.common +import temporalio.converter +import temporalio.converter._search_attributes +import temporalio.exceptions +import temporalio.workflow +from temporalio.converter import ( + WorkflowSerializationContext, +) +from temporalio.service import ( + RPCError, + RPCStatusCode, +) + +from ..types import ( + AnyType, + LocalReturnType, + MethodAsyncNoParam, + MethodAsyncSingleParam, + MethodSyncOrAsyncNoParam, + MethodSyncOrAsyncSingleParam, + MultiParamSpec, + ParamType, + ReturnType, + SelfType, +) +from ._callback import Callback +from ._exceptions import ( + WorkflowContinuedAsNewError, + WorkflowFailureError, + WorkflowUpdateFailedError, + WorkflowUpdateRPCTimeoutOrCancelledError, +) +from ._helpers import _decode_user_metadata, _history_from_json +from ._interceptor import ( + CancelWorkflowInput, + DescribeWorkflowInput, + FetchWorkflowHistoryEventsInput, + QueryWorkflowInput, + SignalWorkflowInput, + StartWorkflowUpdateInput, + TerminateWorkflowInput, + UpdateWithStartStartWorkflowInput, +) + +if TYPE_CHECKING: + from ._client import Client + from ._interceptor import ListWorkflowsInput + + +class WorkflowHistoryEventFilterType(IntEnum): + """Type of history events to get for a workflow. + + See :py:class:`temporalio.api.enums.v1.HistoryEventFilterType`. + """ + + ALL_EVENT = int( + temporalio.api.enums.v1.HistoryEventFilterType.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT + ) + CLOSE_EVENT = int( + temporalio.api.enums.v1.HistoryEventFilterType.HISTORY_EVENT_FILTER_TYPE_CLOSE_EVENT + ) + + +class WorkflowHandle(Generic[SelfType, ReturnType]): + """Handle for interacting with a workflow. + + This is usually created via :py:meth:`Client.get_workflow_handle` or + returned from :py:meth:`Client.start_workflow`. + """ + + def __init__( + self, + client: Client, + id: str, + *, + run_id: str | None = None, + result_run_id: str | None = None, + first_execution_run_id: str | None = None, + result_type: type | None = None, + start_workflow_response: None + | ( + temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse + | temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse + ) = None, + ) -> None: + """Create workflow handle.""" + self._client = client + self._id = id + self._run_id = run_id + self._result_run_id = result_run_id + self._first_execution_run_id = first_execution_run_id + self._result_type = result_type + self._start_workflow_response = start_workflow_response + self.__temporal_eagerly_started = False + + @functools.cached_property + def _data_converter(self) -> temporalio.converter.DataConverter: + return self._client.data_converter.with_context( + temporalio.converter.WorkflowSerializationContext( + namespace=self._client.namespace, workflow_id=self._id + ) + ) + + @property + def id(self) -> str: + """ID of the workflow.""" + return self._id + + @property + def run_id(self) -> str | None: + """If present, run ID used to ensure that requested operations apply + to this exact run. + + This is only created via :py:meth:`Client.get_workflow_handle`. + :py:meth:`Client.start_workflow` will not set this value. + + This cannot be mutated. If a different run ID is needed, + :py:meth:`Client.get_workflow_handle` must be used instead. + """ + return self._run_id + + @property + def result_run_id(self) -> str | None: + """Run ID used for :py:meth:`result` calls if present to ensure result + is for a workflow starting from this run. + + When this handle is created via :py:meth:`Client.get_workflow_handle`, + this is the same as run_id. When this handle is created via + :py:meth:`Client.start_workflow`, this value will be the resulting run + ID. + + This cannot be mutated. If a different run ID is needed, + :py:meth:`Client.get_workflow_handle` must be used instead. + """ + return self._result_run_id + + @property + def first_execution_run_id(self) -> str | None: + """Run ID used to ensure requested operations apply to a workflow ID + started with this run ID. + + This can be set when using :py:meth:`Client.get_workflow_handle`. When + :py:meth:`Client.start_workflow` is called without a start signal, this + is set to the resulting run. + + This cannot be mutated. If a different first execution run ID is needed, + :py:meth:`Client.get_workflow_handle` must be used instead. + """ + return self._first_execution_run_id + + async def result( + self, + *, + follow_runs: bool = True, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: + """Wait for result of the workflow. + + This will use :py:attr:`result_run_id` if present to base the result on. + To use another run ID, a new handle must be created via + :py:meth:`Client.get_workflow_handle`. + + Args: + follow_runs: If true (default), workflow runs will be continually + fetched, until the most recent one is found. If false, return + the result from the first run targeted by the request if that run + ends in a result, otherwise raise an exception. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for each RPC call. Note, + this is the timeout for each history RPC call not this overall + function. + + Returns: + Result of the workflow after being converted by the data converter. + + Raises: + WorkflowFailureError: Workflow failed, was cancelled, was + terminated, or timed out. Use the + :py:attr:`WorkflowFailureError.cause` to see the underlying + reason. + Exception: Other possible failures during result fetching. + """ + # We have to maintain our own run ID because it can change if we follow + # executions + hist_run_id = self._result_run_id + while True: + async for event in self._fetch_history_events_for_run( + hist_run_id, + wait_new_event=True, + event_filter_type=WorkflowHistoryEventFilterType.CLOSE_EVENT, + skip_archival=True, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ): + if event.HasField("workflow_execution_completed_event_attributes"): + complete_attr = event.workflow_execution_completed_event_attributes + # Follow execution + if follow_runs and complete_attr.new_execution_run_id: + hist_run_id = complete_attr.new_execution_run_id + break + # Ignoring anything after the first response like TypeScript + type_hints = [self._result_type] if self._result_type else None + results = await self._data_converter.decode_wrapper( + complete_attr.result, + type_hints, + ) + if not results: + return cast(ReturnType, None) + elif len(results) > 1: + warnings.warn(f"Expected single result, got {len(results)}") + return cast(ReturnType, results[0]) + elif event.HasField("workflow_execution_failed_event_attributes"): + fail_attr = event.workflow_execution_failed_event_attributes + # Follow execution + if follow_runs and fail_attr.new_execution_run_id: + hist_run_id = fail_attr.new_execution_run_id + break + raise WorkflowFailureError( + cause=await self._data_converter.decode_failure( + fail_attr.failure + ), + ) + elif event.HasField("workflow_execution_canceled_event_attributes"): + cancel_attr = event.workflow_execution_canceled_event_attributes + raise WorkflowFailureError( + cause=temporalio.exceptions.CancelledError( + "Workflow cancelled", + *( + await self._data_converter.decode_wrapper( + cancel_attr.details + ) + ), + ) + ) + elif event.HasField("workflow_execution_terminated_event_attributes"): + term_attr = event.workflow_execution_terminated_event_attributes + raise WorkflowFailureError( + cause=temporalio.exceptions.TerminatedError( + term_attr.reason or "Workflow terminated", + *( + await self._data_converter.decode_wrapper( + term_attr.details + ) + ), + ), + ) + elif event.HasField("workflow_execution_timed_out_event_attributes"): + time_attr = event.workflow_execution_timed_out_event_attributes + # Follow execution + if follow_runs and time_attr.new_execution_run_id: + hist_run_id = time_attr.new_execution_run_id + break + raise WorkflowFailureError( + cause=temporalio.exceptions.TimeoutError( + "Workflow timed out", + type=temporalio.exceptions.TimeoutType.START_TO_CLOSE, + last_heartbeat_details=[], + ), + ) + elif event.HasField( + "workflow_execution_continued_as_new_event_attributes" + ): + cont_attr = ( + event.workflow_execution_continued_as_new_event_attributes + ) + if not cont_attr.new_execution_run_id: + raise RuntimeError( + "Unexpectedly missing new run ID from continue as new" + ) + # Follow execution + if follow_runs: + hist_run_id = cont_attr.new_execution_run_id + break + raise WorkflowContinuedAsNewError(cont_attr.new_execution_run_id) + # This is reached on break which means that there's a different run + # ID if we're following. If there's not, it's an error because no + # event was given (should never happen). + if hist_run_id is None: + raise RuntimeError("No completion event found") + + async def cancel( + self, + *, + reason: str = "", + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Cancel the workflow. + + This will issue a cancellation for :py:attr:`run_id` if present. This + call will make sure to use the run chain starting from + :py:attr:`first_execution_run_id` if present. To create handles with + these values, use :py:meth:`Client.get_workflow_handle`. + + .. warning:: + Handles created as a result of :py:meth:`Client.start_workflow` with + a start signal will cancel the latest workflow with the same + workflow ID even if it is unrelated to the started workflow. + + Args: + reason: Reason recorded with the cancellation request. Available + inside the workflow via :py:func:`temporalio.workflow.cancellation_reason`. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + + Raises: + RPCError: Workflow could not be cancelled. + """ + await self._client._impl.cancel_workflow( + CancelWorkflowInput( + id=self._id, + run_id=self._run_id, + first_execution_run_id=self._first_execution_run_id, + reason=reason, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) + + async def describe( + self, + *, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowExecutionDescription: + """Get workflow details. + + This will get details for :py:attr:`run_id` if present. To use a + different run ID, create a new handle with via + :py:meth:`Client.get_workflow_handle`. + + .. warning:: + Handles created as a result of :py:meth:`Client.start_workflow` will + describe the latest workflow with the same workflow ID even if it is + unrelated to the started workflow. + + Args: + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + + Returns: + Workflow details. + + Raises: + RPCError: Workflow details could not be fetched. + """ + return await self._client._impl.describe_workflow( + DescribeWorkflowInput( + id=self._id, + run_id=self._run_id, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) + + async def fetch_history( + self, + *, + event_filter_type: WorkflowHistoryEventFilterType = WorkflowHistoryEventFilterType.ALL_EVENT, + skip_archival: bool = False, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowHistory: + """Get workflow history. + + This is a shortcut for :py:meth:`fetch_history_events` that just fetches + all events. + """ + return WorkflowHistory( + workflow_id=self.id, + events=[ + v + async for v in self.fetch_history_events( + event_filter_type=event_filter_type, + skip_archival=skip_archival, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ], + ) + + def fetch_history_events( + self, + *, + page_size: int | None = None, + next_page_token: bytes | None = None, + wait_new_event: bool = False, + event_filter_type: WorkflowHistoryEventFilterType = WorkflowHistoryEventFilterType.ALL_EVENT, + skip_archival: bool = False, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowHistoryEventAsyncIterator: + """Get workflow history events as an async iterator. + + This does not make a request until the first iteration is attempted. + Therefore any errors will not occur until then. + + Args: + page_size: Maximum amount to fetch per request if any maximum. + next_page_token: A specific page token to fetch. + wait_new_event: Whether the event fetching request will wait for new + events or just return right away. + event_filter_type: Which events to obtain. + skip_archival: Whether to skip archival. + rpc_metadata: Headers used on each RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for each RPC call. + + Returns: + An async iterator that doesn't begin fetching until iterated on. + """ + return self._fetch_history_events_for_run( + self._run_id, + page_size=page_size, + next_page_token=next_page_token, + wait_new_event=wait_new_event, + event_filter_type=event_filter_type, + skip_archival=skip_archival, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + + def _fetch_history_events_for_run( + self, + run_id: str | None, + *, + page_size: int | None = None, + next_page_token: bytes | None = None, + wait_new_event: bool = False, + event_filter_type: WorkflowHistoryEventFilterType = WorkflowHistoryEventFilterType.ALL_EVENT, + skip_archival: bool = False, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowHistoryEventAsyncIterator: + return self._client._impl.fetch_workflow_history_events( + FetchWorkflowHistoryEventsInput( + id=self._id, + run_id=run_id, + page_size=page_size, + next_page_token=next_page_token, + wait_new_event=wait_new_event, + event_filter_type=event_filter_type, + skip_archival=skip_archival, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) + + # Overload for no-param query + @overload + async def query( + self, + query: MethodSyncOrAsyncNoParam[SelfType, LocalReturnType], + *, + reject_condition: temporalio.common.QueryRejectCondition | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> LocalReturnType: ... + + # Overload for single-param query + @overload + async def query( + self, + query: MethodSyncOrAsyncSingleParam[SelfType, ParamType, LocalReturnType], + arg: ParamType, + *, + reject_condition: temporalio.common.QueryRejectCondition | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> LocalReturnType: ... + + # Overload for multi-param query + @overload + async def query( + self, + query: Callable[ + Concatenate[SelfType, MultiParamSpec], + Awaitable[LocalReturnType] | LocalReturnType, + ], + *, + args: Sequence[Any], + reject_condition: temporalio.common.QueryRejectCondition | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> LocalReturnType: ... + + # Overload for string-name query + @overload + async def query( + self, + query: str, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + result_type: type | None = None, + reject_condition: temporalio.common.QueryRejectCondition | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> Any: ... + + async def query( + self, + query: str | Callable, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + result_type: type | None = None, + reject_condition: temporalio.common.QueryRejectCondition | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> Any: + """Query the workflow. + + This will query for :py:attr:`run_id` if present. To use a different + run ID, create a new handle with + :py:meth:`Client.get_workflow_handle`. + + .. warning:: + Handles created as a result of :py:meth:`Client.start_workflow` will + query the latest workflow with the same workflow ID even if it is + unrelated to the started workflow. + + Args: + query: Query function or name on the workflow. + arg: Single argument to the query. + args: Multiple arguments to the query. Cannot be set if arg is. + result_type: For string queries, this can set the specific result + type hint to deserialize into. + reject_condition: Condition for rejecting the query. If unset/None, + defaults to the client's default (which is defaulted to None). + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + + Returns: + Result of the query. + + Raises: + WorkflowQueryRejectedError: A query reject condition was satisfied. + RPCError: Workflow details could not be fetched. + """ + query_name: str + ret_type = result_type + if callable(query): + defn = temporalio.workflow._QueryDefinition.from_fn(query) + if not defn: + raise RuntimeError( + f"Query definition not found on {query.__qualname__}, " + "is it decorated with @workflow.query?" + ) + elif not defn.name: + raise RuntimeError("Cannot invoke dynamic query definition") + # TODO(cretz): Check count/type of args at runtime? + query_name = defn.name + ret_type = defn.ret_type + else: + query_name = str(query) + + return await self._client._impl.query_workflow( + QueryWorkflowInput( + id=self._id, + run_id=self._run_id, + query=query_name, + args=temporalio.common._arg_or_args(arg, args), + reject_condition=reject_condition + or self._client._config["default_workflow_query_reject_condition"], + headers={}, + ret_type=ret_type, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) + + # Overload for no-param signal + @overload + async def signal( + self, + signal: MethodSyncOrAsyncNoParam[SelfType, None], + *, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: ... + + # Overload for single-param signal + @overload + async def signal( + self, + signal: MethodSyncOrAsyncSingleParam[SelfType, ParamType, None], + arg: ParamType, + *, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: ... + + # Overload for multi-param signal + @overload + async def signal( + self, + signal: Callable[Concatenate[SelfType, MultiParamSpec], Awaitable[None] | None], + *, + args: Sequence[Any], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: ... + + # Overload for string-name signal + @overload + async def signal( + self, + signal: str, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: ... + + async def signal( + self, + signal: str | Callable, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Send a signal to the workflow. + + This will signal for :py:attr:`run_id` if present. To use a different + run ID, create a new handle with via + :py:meth:`Client.get_workflow_handle`. + + .. warning:: + Handles created as a result of :py:meth:`Client.start_workflow` will + signal the latest workflow with the same workflow ID even if it is + unrelated to the started workflow. + + Args: + signal: Signal function or name on the workflow. + arg: Single argument to the signal. + args: Multiple arguments to the signal. Cannot be set if arg is. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + + Raises: + RPCError: Workflow could not be signalled. + """ + await self._client._impl.signal_workflow( + SignalWorkflowInput( + id=self._id, + run_id=self._run_id, + signal=temporalio.workflow._SignalDefinition.must_name_from_fn_or_str( + signal + ), + args=temporalio.common._arg_or_args(arg, args), + headers={}, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) + + async def terminate( + self, + *args: Any, + reason: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Terminate the workflow. + + This will issue a termination for :py:attr:`run_id` if present. This + call will make sure to use the run chain starting from + :py:attr:`first_execution_run_id` if present. To create handles with + these values, use :py:meth:`Client.get_workflow_handle`. + + .. warning:: + Handles created as a result of :py:meth:`Client.start_workflow` with + a start signal will terminate the latest workflow with the same + workflow ID even if it is unrelated to the started workflow. + + Args: + args: Details to store on the termination. + reason: Reason for the termination. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + + Raises: + RPCError: Workflow could not be terminated. + """ + await self._client._impl.terminate_workflow( + TerminateWorkflowInput( + id=self._id, + run_id=self._run_id, + args=args, + reason=reason, + first_execution_run_id=self._first_execution_run_id, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) + + # Overload for no-param update + @overload + async def execute_update( + self, + update: temporalio.workflow.UpdateMethodMultiParam[[SelfType], LocalReturnType], + *, + id: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> LocalReturnType: ... + + # Overload for single-param update + @overload + async def execute_update( + self, + update: temporalio.workflow.UpdateMethodMultiParam[ + [SelfType, ParamType], LocalReturnType + ], + arg: ParamType, + *, + id: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> LocalReturnType: ... + + # Overload for multi-param update + @overload + async def execute_update( + self, + update: temporalio.workflow.UpdateMethodMultiParam[ + MultiParamSpec, LocalReturnType + ], + *, + args: MultiParamSpec.args, # type: ignore + id: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> LocalReturnType: ... + + # Overload for string-name update + @overload + async def execute_update( + self, + update: str, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str | None = None, + result_type: type | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> Any: ... + + async def execute_update( + self, + update: str | Callable, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str | None = None, + result_type: type | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> Any: + """Send an update request to the workflow and wait for it to complete. + + This will target the workflow with :py:attr:`run_id` if present. To use a + different run ID, create a new handle with via :py:meth:`Client.get_workflow_handle`. + + Args: + update: Update function or name on the workflow. + arg: Single argument to the update. + args: Multiple arguments to the update. Cannot be set if arg is. + id: ID of the update. If not set, the default is a new UUID. + result_type: For string updates, this can set the specific result + type hint to deserialize into. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + + Raises: + WorkflowUpdateFailedError: If the update failed. + WorkflowUpdateRPCTimeoutOrCancelledError: This update call timed out + or was cancelled. This doesn't mean the update itself was timed + out or cancelled. + RPCError: There was some issue sending the update to the workflow. + """ + handle = await self._start_update( + update, + arg, + args=args, + wait_for_stage=WorkflowUpdateStage.COMPLETED, + id=id, + result_type=result_type, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + return await handle.result() + + # Overload for no-param start update + @overload + async def start_update( + self, + update: temporalio.workflow.UpdateMethodMultiParam[[SelfType], LocalReturnType], + *, + wait_for_stage: WorkflowUpdateStage, + id: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowUpdateHandle[LocalReturnType]: ... + + # Overload for single-param start update + @overload + async def start_update( + self, + update: temporalio.workflow.UpdateMethodMultiParam[ + [SelfType, ParamType], LocalReturnType + ], + arg: ParamType, + *, + wait_for_stage: WorkflowUpdateStage, + id: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowUpdateHandle[LocalReturnType]: ... + + # Overload for multi-param start update + @overload + async def start_update( + self, + update: temporalio.workflow.UpdateMethodMultiParam[ + MultiParamSpec, LocalReturnType + ], + *, + args: MultiParamSpec.args, # type: ignore + wait_for_stage: WorkflowUpdateStage, + id: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowUpdateHandle[LocalReturnType]: ... + + # Overload for string-name start update + @overload + async def start_update( + self, + update: str, + arg: Any = temporalio.common._arg_unset, + *, + wait_for_stage: WorkflowUpdateStage, + args: Sequence[Any] = [], + id: str | None = None, + result_type: type | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowUpdateHandle[Any]: ... + + async def start_update( + self, + update: str | Callable, + arg: Any = temporalio.common._arg_unset, + *, + wait_for_stage: WorkflowUpdateStage, + args: Sequence[Any] = [], + id: str | None = None, + result_type: type | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowUpdateHandle[Any]: + """Send an update request to the workflow and return a handle to it. + + This will target the workflow with :py:attr:`run_id` if present. To use a + different run ID, create a new handle with via :py:meth:`Client.get_workflow_handle`. + + Args: + update: Update function or name on the workflow. arg: Single argument to the + update. + wait_for_stage: Required stage to wait until returning: either ACCEPTED or + COMPLETED. ADMITTED is not currently supported. See + https://docs.temporal.io/workflows#update for more details. + args: Multiple arguments to the update. Cannot be set if arg is. + id: ID of the update. If not set, the default is a new UUID. + result_type: For string updates, this can set the specific result + type hint to deserialize into. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + + Raises: + WorkflowUpdateRPCTimeoutOrCancelledError: This update call timed out + or was cancelled. This doesn't mean the update itself was timed out or + cancelled. + RPCError: There was some issue sending the update to the workflow. + """ + return await self._start_update( + update, + arg, + wait_for_stage=wait_for_stage, + args=args, + id=id, + result_type=result_type, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + + async def _start_update( + self, + update: str | Callable, + arg: Any = temporalio.common._arg_unset, + *, + wait_for_stage: WorkflowUpdateStage, + args: Sequence[Any] = [], + id: str | None = None, + result_type: type | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + # The following options are for Workflow Updates exposed as Nexus Operations. Experimental and unstable + callbacks: Sequence[Callback] | None = None, + links: Sequence[temporalio.api.common.v1.Link] | None = None, + request_id: str | None = None, + ) -> WorkflowUpdateHandle[Any]: + if wait_for_stage == WorkflowUpdateStage.ADMITTED: + raise ValueError("ADMITTED wait stage not supported") + + update_name, result_type_from_type_hint = ( + temporalio.workflow._UpdateDefinition.get_name_and_result_type(update) + ) + + return await self._client._impl.start_workflow_update( + StartWorkflowUpdateInput( + id=self._id, + run_id=self._run_id, + first_execution_run_id=self._first_execution_run_id, + update_id=id, + update=update_name, + args=temporalio.common._arg_or_args(arg, args), + headers={}, + ret_type=result_type or result_type_from_type_hint, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + wait_for_stage=wait_for_stage, + callbacks=callbacks, + links=links, + request_id=request_id, + ) + ) + + def get_update_handle( + self, + id: str, + *, + workflow_run_id: str | None = None, + result_type: type | None = None, + ) -> WorkflowUpdateHandle[Any]: + """Get a handle for an update. The handle can be used to wait on the + update result. + + Users may prefer the more typesafe :py:meth:`get_update_handle_for` + which accepts an update definition. + + Args: + id: Update ID to get a handle to. + workflow_run_id: Run ID to tie the handle to. If this is not set, + the :py:attr:`run_id` will be used. + result_type: The result type to deserialize into if known. + + Returns: + The update handle. + """ + return WorkflowUpdateHandle( + self._client, + id, + self._id, + workflow_run_id=workflow_run_id or self._run_id, + result_type=result_type, + ) + + def get_update_handle_for( + self, + update: temporalio.workflow.UpdateMethodMultiParam[Any, LocalReturnType], + id: str, + *, + workflow_run_id: str | None = None, + ) -> WorkflowUpdateHandle[LocalReturnType]: + """Get a typed handle for an update. The handle can be used to wait on + the update result. + + This is the same as :py:meth:`get_update_handle` but typed. + + Args: + update: The update method to use for typing the handle. + id: Update ID to get a handle to. + workflow_run_id: Run ID to tie the handle to. If this is not set, + the :py:attr:`run_id` will be used. + + Returns: + The update handle. + """ + return self.get_update_handle( + id, workflow_run_id=workflow_run_id, result_type=update._defn.ret_type + ) + + +class WithStartWorkflowOperation(Generic[SelfType, ReturnType]): + """Defines a start-workflow operation used by update-with-start requests. + + Update-With-Start allows you to send an update to a workflow, while starting the + workflow if necessary. + """ + + # Overload for no-param workflow, with_start + @overload + def __init__( + self, + workflow: MethodAsyncNoParam[SelfType, ReturnType], + *, + id: str, + task_queue: str, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> None: ... + + # Overload for single-param workflow, with_start + @overload + def __init__( + self, + workflow: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], + arg: ParamType, + *, + id: str, + task_queue: str, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> None: ... + + # Overload for multi-param workflow, with_start + @overload + def __init__( + self, + workflow: Callable[ + Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType] + ], + *, + args: Sequence[Any], + id: str, + task_queue: str, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> None: ... + + # Overload for string-name workflow, with_start + @overload + def __init__( + self, + workflow: str, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy, + result_type: type | None = None, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> None: ... + + def __init__( + self, + workflow: str | Callable[..., Awaitable[Any]], + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy, + result_type: type | None = None, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + stack_level: int = 2, + ) -> None: + """Create a WithStartWorkflowOperation. + + See :py:meth:`temporalio.client.Client.start_workflow` for documentation of the + arguments. + """ + temporalio.common._warn_on_deprecated_search_attributes( + search_attributes, stack_level=stack_level + ) + name, result_type_from_run_fn = ( + temporalio.workflow._Definition.get_name_and_result_type(workflow) + ) + if id_conflict_policy == temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED: + raise ValueError("WorkflowIDConflictPolicy is required") + + self._start_workflow_input = UpdateWithStartStartWorkflowInput( + workflow=name, + args=temporalio.common._arg_or_args(arg, args), + id=id, + task_queue=task_queue, + execution_timeout=execution_timeout, + run_timeout=run_timeout, + task_timeout=task_timeout, + id_reuse_policy=id_reuse_policy, + id_conflict_policy=id_conflict_policy, + retry_policy=retry_policy, + cron_schedule=cron_schedule, + memo=memo, + search_attributes=search_attributes, + static_summary=static_summary, + static_details=static_details, + start_delay=start_delay, + headers={}, + ret_type=result_type or result_type_from_run_fn, + priority=priority, + versioning_override=versioning_override, + ) + self._workflow_handle: Future[WorkflowHandle[SelfType, ReturnType]] = Future() + self._used = False + + async def workflow_handle(self) -> WorkflowHandle[SelfType, ReturnType]: + """Wait until workflow is running and return a WorkflowHandle.""" + return await self._workflow_handle + + +@dataclass +class WorkflowExecution: + """Info for a single workflow execution run.""" + + close_time: datetime | None + """When the workflow was closed if closed.""" + + execution_time: datetime | None + """When this workflow run started or should start.""" + + history_length: int + """Number of events in the history.""" + + id: str + """ID for the workflow.""" + + namespace: str + """Namespace for the workflow.""" + + parent_id: str | None + """ID for the parent workflow if this was started as a child.""" + + parent_run_id: str | None + """Run ID for the parent workflow if this was started as a child.""" + + root_id: str | None + """ID for the root workflow.""" + + root_run_id: str | None + """Run ID for the root workflow.""" + + raw_info: temporalio.api.workflow.v1.WorkflowExecutionInfo + """Underlying protobuf info.""" + + run_id: str + """Run ID for this workflow run.""" + + search_attributes: temporalio.common.SearchAttributes + """Current set of search attributes if any. + + .. deprecated:: + Use :py:attr:`typed_search_attributes` instead. + """ + + start_time: datetime + """When the workflow was created.""" + + status: WorkflowExecutionStatus | None + """Status for the workflow.""" + + task_queue: str + """Task queue for the workflow.""" + + typed_search_attributes: temporalio.common.TypedSearchAttributes + """Current set of search attributes if any.""" + + workflow_type: str + """Type name for the workflow.""" + + _context_free_data_converter: temporalio.converter.DataConverter + + @property + def data_converter(self) -> temporalio.converter.DataConverter: + """Data converter for the workflow.""" + return self._context_free_data_converter.with_context( + WorkflowSerializationContext( + namespace=self.namespace, + workflow_id=self.id, + ) + ) + + @classmethod + def _from_raw_info( + cls, + info: temporalio.api.workflow.v1.WorkflowExecutionInfo, + namespace: str, + converter: temporalio.converter.DataConverter, + **additional_fields: Any, + ) -> Self: + return cls( + close_time=( + info.close_time.ToDatetime().replace(tzinfo=timezone.utc) + if info.HasField("close_time") + else None + ), + execution_time=( + info.execution_time.ToDatetime().replace(tzinfo=timezone.utc) + if info.HasField("execution_time") + else None + ), + history_length=info.history_length, + id=info.execution.workflow_id, + namespace=namespace, + parent_id=( + info.parent_execution.workflow_id + if info.HasField("parent_execution") + else None + ), + parent_run_id=( + info.parent_execution.run_id + if info.HasField("parent_execution") + else None + ), + root_id=( + info.root_execution.workflow_id + if info.HasField("root_execution") + else None + ), + root_run_id=( + info.root_execution.run_id if info.HasField("root_execution") else None + ), + raw_info=info, + run_id=info.execution.run_id, + search_attributes=temporalio.converter.decode_search_attributes( + info.search_attributes + ), + start_time=info.start_time.ToDatetime().replace(tzinfo=timezone.utc), + status=WorkflowExecutionStatus(info.status) if info.status else None, + task_queue=info.task_queue, + typed_search_attributes=temporalio.converter.decode_typed_search_attributes( + info.search_attributes + ), + workflow_type=info.type.name, + _context_free_data_converter=converter, + **additional_fields, + ) + + async def memo(self) -> Mapping[str, Any]: + """Workflow's memo values, converted without type hints. + + Since type hints are not used, the default converted values will come + back. For example, if the memo was originally created with a dataclass, + the value will be a dict. To convert using proper type hints, use + :py:meth:`memo_value`. + + Returns: + Mapping of all memo keys and they values without type hints. + """ + return await self.data_converter._decode_memo(self.raw_info.memo) + + @overload + async def memo_value( + self, key: str, default: Any = temporalio.common._arg_unset + ) -> Any: ... + + @overload + async def memo_value( + self, key: str, *, type_hint: type[ParamType] + ) -> ParamType: ... + + @overload + async def memo_value( + self, key: str, default: AnyType, *, type_hint: type[ParamType] + ) -> AnyType | ParamType: ... + + async def memo_value( + self, + key: str, + default: Any = temporalio.common._arg_unset, + *, + type_hint: type | None = None, + ) -> Any: + """Memo value for the given key, optional default, and optional type + hint. + + Args: + key: Key to get memo value for. + default: Default to use if key is not present. If unset, a + :py:class:`KeyError` is raised when the key does not exist. + type_hint: type hint to use when converting. + + Returns: + Memo value, converted with the type hint if present. + + Raises: + KeyError: Key not present and default not set. + """ + return await self.data_converter._decode_memo_field( + self.raw_info.memo, key, default, type_hint + ) + + +@dataclass +class WorkflowExecutionDescription(WorkflowExecution): + """Description for a single workflow execution run.""" + + raw_description: temporalio.api.workflowservice.v1.DescribeWorkflowExecutionResponse + """Underlying protobuf description.""" + + _static_summary: str | None = None + _static_details: str | None = None + _metadata_decoded: bool = False + + async def static_summary(self) -> str | None: + """Gets the single-line fixed summary for this workflow execution that may appear in + UI/CLI. This can be in single-line Temporal markdown format. + """ + if not self._metadata_decoded: + await self._decode_metadata() + return self._static_summary + + async def static_details(self) -> str | None: + """Gets the general fixed details for this workflow execution that may appear in UI/CLI. + This can be in Temporal markdown format and can span multiple lines. + """ + if not self._metadata_decoded: + await self._decode_metadata() + return self._static_details + + async def _decode_metadata(self) -> None: + """Internal method to decode metadata lazily.""" + self._static_summary, self._static_details = await _decode_user_metadata( + self.data_converter, self.raw_description.execution_config.user_metadata + ) + self._metadata_decoded = True + + @staticmethod + async def _from_raw_description( + description: temporalio.api.workflowservice.v1.DescribeWorkflowExecutionResponse, + namespace: str, + converter: temporalio.converter.DataConverter, + ) -> WorkflowExecutionDescription: + return WorkflowExecutionDescription._from_raw_info( + description.workflow_execution_info, + namespace=namespace, + converter=converter, + raw_description=description, + ) + + +class WorkflowExecutionStatus(IntEnum): + """Status of a workflow execution. + + See :py:class:`temporalio.api.enums.v1.WorkflowExecutionStatus`. + """ + + RUNNING = int( + temporalio.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_RUNNING + ) + COMPLETED = int( + temporalio.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_COMPLETED + ) + FAILED = int( + temporalio.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_FAILED + ) + CANCELED = int( + temporalio.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_CANCELED + ) + TERMINATED = int( + temporalio.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_TERMINATED + ) + CONTINUED_AS_NEW = int( + temporalio.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW + ) + TIMED_OUT = int( + temporalio.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_TIMED_OUT + ) + + +@dataclass +class WorkflowExecutionCount: + """Representation of a count from a count workflows call.""" + + count: int + """Approximate number of workflows matching the original query. + + If the query had a group-by clause, this is simply the sum of all the counts + in py:attr:`groups`. + """ + + groups: Sequence[WorkflowExecutionCountAggregationGroup] + """Groups if the query had a group-by clause, or empty if not.""" + + @staticmethod + def _from_raw( + raw: temporalio.api.workflowservice.v1.CountWorkflowExecutionsResponse, + ) -> WorkflowExecutionCount: + return WorkflowExecutionCount( + count=raw.count, + groups=[ + WorkflowExecutionCountAggregationGroup._from_raw(g) for g in raw.groups + ], + ) + + +@dataclass +class WorkflowExecutionCountAggregationGroup: + """Aggregation group if the workflow count query had a group-by clause.""" + + count: int + """Approximate number of workflows matching the original query for this + group. + """ + + group_values: Sequence[temporalio.common.SearchAttributeValue] + """Search attribute values for this group.""" + + @staticmethod + def _from_raw( + raw: temporalio.api.workflowservice.v1.CountWorkflowExecutionsResponse.AggregationGroup, + ) -> WorkflowExecutionCountAggregationGroup: + return WorkflowExecutionCountAggregationGroup( + count=raw.count, + group_values=[ + temporalio.converter._search_attributes._decode_search_attribute_value( + v + ) + for v in raw.group_values + ], + ) + + +class WorkflowExecutionAsyncIterator: + """Asynchronous iterator for :py:class:`WorkflowExecution` values. + + Most users should use ``async for`` on this iterator and not call any of the + methods within. To consume the workflows as histories, call + :py:meth:`map_histories`. + """ + + def __init__( + self, + client: Client, + input: ListWorkflowsInput, + ) -> None: + """Create an asynchronous iterator for the given input. + + Users should not create this directly, but rather use + :py:meth:`Client.list_workflows`. + """ + self._client = client + self._input = input + self._next_page_token = input.next_page_token + self._current_page: Sequence[WorkflowExecution] | None = None + self._current_page_index = 0 + self._limit = input.limit + self._yielded = 0 + + @property + def current_page_index(self) -> int: + """Index of the entry in the current page that will be returned from + the next :py:meth:`__anext__` call. + """ + return self._current_page_index + + @property + def current_page(self) -> Sequence[WorkflowExecution] | None: + """Current page, if it has been fetched yet.""" + return self._current_page + + @property + def next_page_token(self) -> bytes | None: + """Token for the next page request if any.""" + return self._next_page_token + + async def fetch_next_page(self, *, page_size: int | None = None) -> None: + """Fetch the next page if any. + + Args: + page_size: Override the page size this iterator was originally + created with. + """ + page_size = page_size or self._input.page_size + if self._limit is not None and self._limit - self._yielded < page_size: + page_size = self._limit - self._yielded + + resp = await self._client.workflow_service.list_workflow_executions( + temporalio.api.workflowservice.v1.ListWorkflowExecutionsRequest( + namespace=self._client.namespace, + page_size=page_size, + next_page_token=self._next_page_token or b"", + query=self._input.query or "", + ), + retry=True, + metadata=self._input.rpc_metadata, + timeout=self._input.rpc_timeout, + ) + + self._current_page = [ + WorkflowExecution._from_raw_info( + v, self._client.namespace, self._client.data_converter + ) + for v in resp.executions + ] + self._current_page_index = 0 + self._next_page_token = resp.next_page_token or None + + def __aiter__(self) -> WorkflowExecutionAsyncIterator: + """Return self as the iterator.""" + return self + + async def __anext__(self) -> WorkflowExecution: + """Get the next execution on this iterator, fetching next page if + necessary. + """ + if self._limit is not None and self._yielded >= self._limit: + raise StopAsyncIteration + while True: + # No page? fetch and continue + if self._current_page is None: + await self.fetch_next_page() + continue + # No more left in page? + if self._current_page_index >= len(self._current_page): + # If there is a next page token, try to get another page and try + # again + if self._next_page_token is not None: + await self.fetch_next_page() + continue + # No more pages means we're done + raise StopAsyncIteration + # Get current, increment page index, and return + ret = self._current_page[self._current_page_index] + self._current_page_index += 1 + self._yielded += 1 + return ret + + async def map_histories( + self, + *, + event_filter_type: WorkflowHistoryEventFilterType = WorkflowHistoryEventFilterType.ALL_EVENT, + skip_archival: bool = False, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> AsyncIterator[WorkflowHistory]: + """Create an async iterator consuming all workflows and calling + :py:meth:`WorkflowHandle.fetch_history` on each one. + + This is just a shortcut for ``fetch_history``, see that method for + parameter details. + """ + async for v in self: + yield await self._client.get_workflow_handle( + v.id, run_id=v.run_id + ).fetch_history( + event_filter_type=event_filter_type, + skip_archival=skip_archival, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + + +@dataclass(frozen=True) +class WorkflowHistory: + """A workflow's ID and immutable history.""" + + workflow_id: str + """ID of the workflow.""" + + events: Sequence[temporalio.api.history.v1.HistoryEvent] + """History events for the workflow.""" + + @property + def run_id(self) -> str: + """Run ID extracted from the first event.""" + if not self.events: + raise RuntimeError("No events") + if not self.events[0].HasField("workflow_execution_started_event_attributes"): + raise RuntimeError("First event is not workflow start") + return self.events[ + 0 + ].workflow_execution_started_event_attributes.original_execution_run_id + + @staticmethod + def from_json(workflow_id: str, history: str | dict[str, Any]) -> WorkflowHistory: + """Construct a WorkflowHistory from an ID and a json dump of history. + + This is built to work both with Temporal UI/CLI JSON as well as + :py:meth:`to_json` even though they are slightly different. + + Args: + workflow_id: The workflow's ID + history: A string or parsed-to-dict representation of workflow + history + + Returns: + Workflow history + """ + parsed = _history_from_json(history) + return WorkflowHistory(workflow_id, parsed.events) + + def to_json(self) -> str: + """Convert this history to JSON. + + Note, this does not include the workflow ID. + """ + return google.protobuf.json_format.MessageToJson( + temporalio.api.history.v1.History(events=self.events) + ) + + def to_json_dict(self) -> dict[str, Any]: + """Convert this history to JSON-compatible dict. + + Note, this does not include the workflow ID. + """ + return google.protobuf.json_format.MessageToDict( + temporalio.api.history.v1.History(events=self.events) + ) + + +@dataclass +class WorkflowHistoryEventAsyncIterator: + """Asynchronous iterator for history events of a workflow. + + Most users should use ``async for`` on this iterator and not call any of the + methods within. + """ + + def __init__( + self, + client: Client, + input: FetchWorkflowHistoryEventsInput, + ) -> None: + """Create an asynchronous iterator for the given input. + + Users should not create this directly, but rather use + :py:meth:`WorkflowHandle.fetch_history_events`. + """ + self._client = client + self._input = input + self._next_page_token = input.next_page_token + self._current_page: ( + None | (Sequence[temporalio.api.history.v1.HistoryEvent]) + ) = None + self._current_page_index = 0 + + @property + def current_page_index(self) -> int: + """Index of the entry in the current page that will be returned from + the next :py:meth:`__anext__` call. + """ + return self._current_page_index + + @property + def current_page( + self, + ) -> Sequence[temporalio.api.history.v1.HistoryEvent] | None: + """Current page, if it has been fetched yet.""" + return self._current_page + + @property + def next_page_token(self) -> bytes | None: + """Token for the next page request if any.""" + return self._next_page_token + + async def fetch_next_page(self, *, page_size: int | None = None) -> None: # type:ignore[reportUnusedParameter] # https://github.com/temporalio/sdk-python/issues/1239 + """Fetch the next page if any. + + Args: + page_size: Override the page size this iterator was originally + created with. + """ + resp = await self._client.workflow_service.get_workflow_execution_history( + temporalio.api.workflowservice.v1.GetWorkflowExecutionHistoryRequest( + namespace=self._client.namespace, + execution=temporalio.api.common.v1.WorkflowExecution( + workflow_id=self._input.id, + run_id=self._input.run_id or "", + ), + maximum_page_size=page_size or self._input.page_size or 0, + next_page_token=self._next_page_token or b"", + wait_new_event=self._input.wait_new_event, + history_event_filter_type=temporalio.api.enums.v1.HistoryEventFilterType.ValueType( + self._input.event_filter_type + ), + skip_archival=self._input.skip_archival, + ), + retry=True, + metadata=self._input.rpc_metadata, + timeout=self._input.rpc_timeout, + ) + # We don't support raw history + assert len(resp.raw_history) == 0 + self._current_page = list(resp.history.events) + self._current_page_index = 0 + self._next_page_token = resp.next_page_token or None + + def __aiter__(self) -> WorkflowHistoryEventAsyncIterator: + """Return self as the iterator.""" + return self + + async def __anext__(self) -> temporalio.api.history.v1.HistoryEvent: + """Get the next execution on this iterator, fetching next page if + necessary. + """ + while True: + # No page? fetch and continue + if self._current_page is None: + await self.fetch_next_page() + continue + # No more left in page? + if self._current_page_index >= len(self._current_page): + # If there is a next page token, try to get another page and try + # again + if self._next_page_token is not None: + await self.fetch_next_page() + continue + # No more pages means we're done + raise StopAsyncIteration + # Increment page index and return + ret = self._current_page[self._current_page_index] + self._current_page_index += 1 + return ret + + +class WorkflowUpdateHandle(Generic[LocalReturnType]): + """Handle for a workflow update execution request.""" + + def __init__( + self, + client: Client, + id: str, + workflow_id: str, + *, + workflow_run_id: str | None = None, + result_type: type | None = None, + known_outcome: temporalio.api.update.v1.Outcome | None = None, + ): + """Create a workflow update handle. + + Users should not create this directly, but rather use + :py:meth:`WorkflowHandle.start_update` or :py:meth:`WorkflowHandle.get_update_handle`. + """ + self._client = client + self._id = id + self._workflow_id = workflow_id + self._workflow_run_id = workflow_run_id + self._result_type = result_type + self._known_outcome = known_outcome + + @functools.cached_property + def _data_converter(self) -> temporalio.converter.DataConverter: + return self._client.data_converter.with_context( + WorkflowSerializationContext( + namespace=self._client.namespace, + workflow_id=self.workflow_id, + ) + ) + + @property + def id(self) -> str: + """ID of this Update request.""" + return self._id + + @property + def workflow_id(self) -> str: + """The ID of the Workflow targeted by this Update.""" + return self._workflow_id + + @property + def workflow_run_id(self) -> str | None: + """If specified, the specific run of the Workflow targeted by this Update.""" + return self._workflow_run_id + + async def result( + self, + *, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> LocalReturnType: + """Wait for and return the result of the update. The result may already be known in which case no network call + is made. Otherwise the result will be polled for until it is returned. + + Args: + rpc_metadata: Headers used on the RPC call. Keys here override client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for each RPC call. Note: this is the timeout for each + RPC call while polling, not a timeout for the function as a whole. If an individual RPC times out, + it will be retried until the result is available. + + Raises: + WorkflowUpdateFailedError: If the update failed. + WorkflowUpdateRPCTimeoutOrCancelledError: This update call timed out + or was cancelled. This doesn't mean the update itself was timed + out or cancelled. + RPCError: Update result could not be fetched for some other reason. + """ + # Poll until outcome reached + await self._poll_until_outcome( + rpc_metadata=rpc_metadata, rpc_timeout=rpc_timeout + ) + + # Convert outcome to failure or value + assert self._known_outcome + if self._known_outcome.HasField("failure"): + raise WorkflowUpdateFailedError( + await self._data_converter.decode_failure(self._known_outcome.failure), + ) + if not self._known_outcome.success.payloads: + return None # type: ignore + type_hints = [self._result_type] if self._result_type else None + results = await self._data_converter.decode( + self._known_outcome.success.payloads, type_hints + ) + if not results: + return None # type: ignore + elif len(results) > 1: + warnings.warn(f"Expected single update result, got {len(results)}") + return results[0] + + async def _poll_until_outcome( + self, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + if self._known_outcome: + return + req = temporalio.api.workflowservice.v1.PollWorkflowExecutionUpdateRequest( + namespace=self._client.namespace, + update_ref=temporalio.api.update.v1.UpdateRef( + workflow_execution=temporalio.api.common.v1.WorkflowExecution( + workflow_id=self.workflow_id, + run_id=self.workflow_run_id or "", + ), + update_id=self.id, + ), + identity=self._client.identity, + wait_policy=temporalio.api.update.v1.WaitPolicy( + lifecycle_stage=temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED + ), + ) + + # Continue polling as long as we have no outcome + while True: + try: + res = ( + await self._client.workflow_service.poll_workflow_execution_update( + req, + retry=True, + metadata=rpc_metadata, + timeout=rpc_timeout, + ) + ) + if res.HasField("outcome"): + self._known_outcome = res.outcome + return + except RPCError as err: + if ( + err.status == RPCStatusCode.DEADLINE_EXCEEDED + or err.status == RPCStatusCode.CANCELLED + ): + raise WorkflowUpdateRPCTimeoutOrCancelledError() from err + else: + raise + except asyncio.CancelledError as err: + raise WorkflowUpdateRPCTimeoutOrCancelledError() from err + + +class WorkflowUpdateStage(IntEnum): + """Stage to wait for workflow update to reach before returning from + ``start_update``. + """ + + ADMITTED = int( + temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED + ) + ACCEPTED = int( + temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED + ) + COMPLETED = int( + temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED + ) diff --git a/temporalio/common.py b/temporalio/common.py index 844d73f87..ad75b56b9 100644 --- a/temporalio/common.py +++ b/temporalio/common.py @@ -2,35 +2,29 @@ from __future__ import annotations +import asyncio import inspect +import threading import types import warnings from abc import ABC, abstractmethod +from collections.abc import Callable, Collection, Iterator, Mapping, Sequence from dataclasses import dataclass from datetime import datetime, timedelta from enum import IntEnum from typing import ( Any, - Callable, ClassVar, - Collection, Generic, - Iterator, - List, - Mapping, - Optional, - Sequence, - Text, - Tuple, - Type, + TypeAlias, TypeVar, - Union, + get_origin, get_type_hints, overload, ) import google.protobuf.internal.containers -from typing_extensions import NamedTuple, Self, TypeAlias, get_origin +from typing_extensions import NamedTuple, Self import temporalio.api.common.v1 import temporalio.api.deployment.v1 @@ -51,7 +45,7 @@ class RetryPolicy: interval. Default 2.0. """ - maximum_interval: Optional[timedelta] = None + maximum_interval: timedelta | None = None """Maximum backoff interval between retries. Default 100x :py:attr:`initial_interval`. """ @@ -62,7 +56,7 @@ class RetryPolicy: If 0, the default, there is no maximum. """ - non_retryable_error_types: Optional[Sequence[str]] = None + non_retryable_error_types: Sequence[str] | None = None """List of error types that are not retryable.""" @staticmethod @@ -75,7 +69,7 @@ def from_proto(proto: temporalio.api.common.v1.RetryPolicy) -> RetryPolicy: if proto.HasField("maximum_interval") else None, maximum_attempts=proto.maximum_attempts, - non_retryable_error_types=proto.non_retryable_error_types + non_retryable_error_types=list(proto.non_retryable_error_types) if proto.non_retryable_error_types else None, ) @@ -154,6 +148,193 @@ class WorkflowIDConflictPolicy(IntEnum): ) +class ActivityIDReusePolicy(IntEnum): + """How already-closed activity IDs are handled on start. + + .. warning:: + This API is experimental. + + See :py:class:`temporalio.api.enums.v1.ActivityIdReusePolicy`. + """ + + UNSPECIFIED = int( + temporalio.api.enums.v1.ActivityIdReusePolicy.ACTIVITY_ID_REUSE_POLICY_UNSPECIFIED + ) + ALLOW_DUPLICATE = int( + temporalio.api.enums.v1.ActivityIdReusePolicy.ACTIVITY_ID_REUSE_POLICY_ALLOW_DUPLICATE + ) + ALLOW_DUPLICATE_FAILED_ONLY = int( + temporalio.api.enums.v1.ActivityIdReusePolicy.ACTIVITY_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY + ) + REJECT_DUPLICATE = int( + temporalio.api.enums.v1.ActivityIdReusePolicy.ACTIVITY_ID_REUSE_POLICY_REJECT_DUPLICATE + ) + + +class ActivityIDConflictPolicy(IntEnum): + """How already-running activity IDs are handled on start. + + .. warning:: + This API is experimental. + + See :py:class:`temporalio.api.enums.v1.ActivityIdConflictPolicy`. + """ + + UNSPECIFIED = int( + temporalio.api.enums.v1.ActivityIdConflictPolicy.ACTIVITY_ID_CONFLICT_POLICY_UNSPECIFIED + ) + FAIL = int( + temporalio.api.enums.v1.ActivityIdConflictPolicy.ACTIVITY_ID_CONFLICT_POLICY_FAIL + ) + USE_EXISTING = int( + temporalio.api.enums.v1.ActivityIdConflictPolicy.ACTIVITY_ID_CONFLICT_POLICY_USE_EXISTING + ) + + +class NexusOperationIDReusePolicy(IntEnum): + """How already-closed Nexus operation IDs are handled on start. + + .. warning:: + This API is experimental and unstable. + + See :py:class:`temporalio.api.enums.v1.NexusOperationIdReusePolicy`. + """ + + UNSPECIFIED = int( + temporalio.api.enums.v1.NexusOperationIdReusePolicy.NEXUS_OPERATION_ID_REUSE_POLICY_UNSPECIFIED + ) + ALLOW_DUPLICATE = int( + temporalio.api.enums.v1.NexusOperationIdReusePolicy.NEXUS_OPERATION_ID_REUSE_POLICY_ALLOW_DUPLICATE + ) + ALLOW_DUPLICATE_FAILED_ONLY = int( + temporalio.api.enums.v1.NexusOperationIdReusePolicy.NEXUS_OPERATION_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY + ) + REJECT_DUPLICATE = int( + temporalio.api.enums.v1.NexusOperationIdReusePolicy.NEXUS_OPERATION_ID_REUSE_POLICY_REJECT_DUPLICATE + ) + + +class NexusOperationIDConflictPolicy(IntEnum): + """How already-running Nexus operation IDs are handled on start. + + .. warning:: + This API is experimental and unstable. + + See :py:class:`temporalio.api.enums.v1.NexusOperationIdConflictPolicy`. + """ + + UNSPECIFIED = int( + temporalio.api.enums.v1.NexusOperationIdConflictPolicy.NEXUS_OPERATION_ID_CONFLICT_POLICY_UNSPECIFIED + ) + FAIL = int( + temporalio.api.enums.v1.NexusOperationIdConflictPolicy.NEXUS_OPERATION_ID_CONFLICT_POLICY_FAIL + ) + USE_EXISTING = int( + temporalio.api.enums.v1.NexusOperationIdConflictPolicy.NEXUS_OPERATION_ID_CONFLICT_POLICY_USE_EXISTING + ) + + +class NexusOperationExecutionStatus(IntEnum): + """Status of a standalone Nexus operation execution. + + .. warning:: + This API is experimental and unstable. + + See :py:class:`temporalio.api.enums.v1.NexusOperationExecutionStatus`. + """ + + UNSPECIFIED = int( + temporalio.api.enums.v1.NexusOperationExecutionStatus.NEXUS_OPERATION_EXECUTION_STATUS_UNSPECIFIED + ) + RUNNING = int( + temporalio.api.enums.v1.NexusOperationExecutionStatus.NEXUS_OPERATION_EXECUTION_STATUS_RUNNING + ) + COMPLETED = int( + temporalio.api.enums.v1.NexusOperationExecutionStatus.NEXUS_OPERATION_EXECUTION_STATUS_COMPLETED + ) + FAILED = int( + temporalio.api.enums.v1.NexusOperationExecutionStatus.NEXUS_OPERATION_EXECUTION_STATUS_FAILED + ) + CANCELED = int( + temporalio.api.enums.v1.NexusOperationExecutionStatus.NEXUS_OPERATION_EXECUTION_STATUS_CANCELED + ) + TERMINATED = int( + temporalio.api.enums.v1.NexusOperationExecutionStatus.NEXUS_OPERATION_EXECUTION_STATUS_TERMINATED + ) + TIMED_OUT = int( + temporalio.api.enums.v1.NexusOperationExecutionStatus.NEXUS_OPERATION_EXECUTION_STATUS_TIMED_OUT + ) + + +class PendingNexusOperationExecutionState(IntEnum): + """More detailed breakdown of :py:attr:`NexusOperationExecutionStatus.RUNNING`. + + .. warning:: + This API is experimental and unstable. + + See :py:class:`temporalio.api.enums.v1.PendingNexusOperationState`. + """ + + UNSPECIFIED = int( + temporalio.api.enums.v1.PendingNexusOperationState.PENDING_NEXUS_OPERATION_STATE_UNSPECIFIED + ) + SCHEDULED = int( + temporalio.api.enums.v1.PendingNexusOperationState.PENDING_NEXUS_OPERATION_STATE_SCHEDULED + ) + BACKING_OFF = int( + temporalio.api.enums.v1.PendingNexusOperationState.PENDING_NEXUS_OPERATION_STATE_BACKING_OFF + ) + STARTED = int( + temporalio.api.enums.v1.PendingNexusOperationState.PENDING_NEXUS_OPERATION_STATE_STARTED + ) + BLOCKED = int( + temporalio.api.enums.v1.PendingNexusOperationState.PENDING_NEXUS_OPERATION_STATE_BLOCKED + ) + + +class NexusOperationCancellationState(IntEnum): + """State of a Nexus operation cancellation. + + .. warning:: + This API is experimental and unstable. + """ + + UNSPECIFIED = int( + temporalio.api.enums.v1.NexusOperationCancellationState.NEXUS_OPERATION_CANCELLATION_STATE_UNSPECIFIED + ) + """Default value, unspecified state.""" + + SCHEDULED = int( + temporalio.api.enums.v1.NexusOperationCancellationState.NEXUS_OPERATION_CANCELLATION_STATE_SCHEDULED + ) + """Cancellation request is in the queue waiting to be executed or is currently executing.""" + + BACKING_OFF = int( + temporalio.api.enums.v1.NexusOperationCancellationState.NEXUS_OPERATION_CANCELLATION_STATE_BACKING_OFF + ) + """Cancellation request has failed with a retryable error and is backing off before the next attempt.""" + + SUCCEEDED = int( + temporalio.api.enums.v1.NexusOperationCancellationState.NEXUS_OPERATION_CANCELLATION_STATE_SUCCEEDED + ) + """Cancellation request succeeded.""" + + FAILED = int( + temporalio.api.enums.v1.NexusOperationCancellationState.NEXUS_OPERATION_CANCELLATION_STATE_FAILED + ) + """Cancellation request failed with a non-retryable error.""" + + TIMED_OUT = int( + temporalio.api.enums.v1.NexusOperationCancellationState.NEXUS_OPERATION_CANCELLATION_STATE_TIMED_OUT + ) + """The associated operation timed out - exceeded the user supplied schedule-to-close timeout.""" + + BLOCKED = int( + temporalio.api.enums.v1.NexusOperationCancellationState.NEXUS_OPERATION_CANCELLATION_STATE_BLOCKED + ) + """Cancellation request is blocked, eg: by circuit breaker.""" + + class QueryRejectCondition(IntEnum): """Whether a query should be rejected in certain conditions. @@ -200,13 +381,13 @@ def __setstate__(self, state: object) -> None: # We choose to make this a list instead of an sequence so we can catch if people # are not sending lists each time but maybe accidentally sending a string (which # is a sequence) -SearchAttributeValues: TypeAlias = Union[ - List[str], List[int], List[float], List[bool], List[datetime] -] +SearchAttributeValues: TypeAlias = ( + list[str] | list[int] | list[float] | list[bool] | list[datetime] +) SearchAttributes: TypeAlias = Mapping[str, SearchAttributeValues] -SearchAttributeValue: TypeAlias = Union[str, int, float, bool, datetime, Sequence[str]] +SearchAttributeValue: TypeAlias = str | int | float | bool | datetime | Sequence[str] SearchAttributeValueType = TypeVar( "SearchAttributeValueType", str, int, float, bool, datetime, Sequence[str] @@ -247,7 +428,7 @@ def indexed_value_type(self) -> SearchAttributeIndexedValueType: @property @abstractmethod - def value_type(self) -> Type[SearchAttributeValueType]: + def value_type(self) -> type[SearchAttributeValueType]: """Get the Python type of value for the key. This may contain generics which cannot be used in ``isinstance``. @@ -256,7 +437,7 @@ def value_type(self) -> Type[SearchAttributeValueType]: ... @property - def origin_value_type(self) -> Type: + def origin_value_type(self) -> type: """Get the Python type of value for the key without generics.""" return get_origin(self.value_type) or self.value_type @@ -340,29 +521,30 @@ def for_keyword_list(name: str) -> SearchAttributeKey[Sequence[str]]: ) @staticmethod - def _from_metadata_type( - name: str, metadata_type: str - ) -> Optional[SearchAttributeKey]: - if metadata_type == "Text": + def _from_metadata_type(name: str, metadata_type: str) -> SearchAttributeKey | None: + # The type metadata is usually in PascalCase (e.g. "KeywordList") + # but in rare cases may be in SCREAMING_SNAKE_CASE (e.g. + # "INDEXED_VALUE_TYPE_KEYWORD_LIST"). + if metadata_type in ("Text", "INDEXED_VALUE_TYPE_TEXT"): return SearchAttributeKey.for_text(name) - elif metadata_type == "Keyword": + elif metadata_type in ("Keyword", "INDEXED_VALUE_TYPE_KEYWORD"): return SearchAttributeKey.for_keyword(name) - elif metadata_type == "Int": + elif metadata_type in ("Int", "INDEXED_VALUE_TYPE_INT"): return SearchAttributeKey.for_int(name) - elif metadata_type == "Double": + elif metadata_type in ("Double", "INDEXED_VALUE_TYPE_DOUBLE"): return SearchAttributeKey.for_float(name) - elif metadata_type == "Bool": + elif metadata_type in ("Bool", "INDEXED_VALUE_TYPE_BOOL"): return SearchAttributeKey.for_bool(name) - elif metadata_type == "Datetime": + elif metadata_type in ("Datetime", "INDEXED_VALUE_TYPE_DATETIME"): return SearchAttributeKey.for_datetime(name) - elif metadata_type == "KeywordList": + elif metadata_type in ("KeywordList", "INDEXED_VALUE_TYPE_KEYWORD_LIST"): return SearchAttributeKey.for_keyword_list(name) return None @staticmethod def _guess_from_untyped_values( name: str, vals: SearchAttributeValues - ) -> Optional[SearchAttributeKey]: + ) -> SearchAttributeKey | None: if not vals: return None elif len(vals) > 1: @@ -386,7 +568,7 @@ class _SearchAttributeKey(SearchAttributeKey[SearchAttributeValueType]): _name: str _indexed_value_type: SearchAttributeIndexedValueType # No supported way in Python to derive this, so we're setting manually - _value_type: Type[SearchAttributeValueType] + _value_type: type[SearchAttributeValueType] @property def name(self) -> str: @@ -397,7 +579,7 @@ def indexed_value_type(self) -> SearchAttributeIndexedValueType: return self._indexed_value_type @property - def value_type(self) -> Type[SearchAttributeValueType]: + def value_type(self) -> type[SearchAttributeValueType]: return self._value_type @@ -419,7 +601,7 @@ def key(self) -> SearchAttributeKey[SearchAttributeValueType]: @property @abstractmethod - def value(self) -> Optional[SearchAttributeValueType]: + def value(self) -> SearchAttributeValueType | None: """Value that is being set or ``None`` if being unset.""" ... @@ -427,14 +609,14 @@ def value(self) -> Optional[SearchAttributeValueType]: @dataclass(frozen=True) class _SearchAttributeUpdate(SearchAttributeUpdate[SearchAttributeValueType]): _key: SearchAttributeKey[SearchAttributeValueType] - _value: Optional[SearchAttributeValueType] + _value: SearchAttributeValueType | None @property def key(self) -> SearchAttributeKey[SearchAttributeValueType]: return self._key @property - def value(self) -> Optional[SearchAttributeValueType]: + def value(self) -> SearchAttributeValueType | None: return self._value @@ -496,24 +678,24 @@ def __contains__(self, key: object) -> bool: This uses key equality so the key must be the same name and type. """ - return any(v for k, v in self if k == key) + return any(k == key for k, _v in self) @overload def get( self, key: SearchAttributeKey[SearchAttributeValueType] - ) -> Optional[SearchAttributeValueType]: ... + ) -> SearchAttributeValueType | None: ... @overload def get( self, key: SearchAttributeKey[SearchAttributeValueType], default: temporalio.types.AnyType, - ) -> Union[SearchAttributeValueType, temporalio.types.AnyType]: ... + ) -> SearchAttributeValueType | temporalio.types.AnyType: ... def get( self, key: SearchAttributeKey[SearchAttributeValueType], - default: Optional[Any] = None, + default: Any | None = None, ) -> Any: """Get an attribute value for a key (or default). This is similar to dict.get. @@ -548,8 +730,8 @@ def updated(self, *search_attributes: SearchAttributePair) -> TypedSearchAttribu TypedSearchAttributes.empty = TypedSearchAttributes(search_attributes=[]) -def _warn_on_deprecated_search_attributes( - attributes: Optional[Union[SearchAttributes, Any]], +def _warn_on_deprecated_search_attributes( # type:ignore[reportUnusedFunction] + attributes: SearchAttributes | Any | None, stack_level: int = 2, ) -> None: if attributes and isinstance(attributes, Mapping): @@ -560,7 +742,7 @@ def _warn_on_deprecated_search_attributes( ) -MetricAttributes: TypeAlias = Mapping[str, Union[str, int, float, bool]] +MetricAttributes: TypeAlias = Mapping[str, str | int | float | bool] class MetricMeter(ABC): @@ -571,7 +753,7 @@ class MetricMeter(ABC): @abstractmethod def create_counter( - self, name: str, description: Optional[str] = None, unit: Optional[str] = None + self, name: str, description: str | None = None, unit: str | None = None ) -> MetricCounter: """Create a counter metric for adding values. @@ -587,7 +769,7 @@ def create_counter( @abstractmethod def create_histogram( - self, name: str, description: Optional[str] = None, unit: Optional[str] = None + self, name: str, description: str | None = None, unit: str | None = None ) -> MetricHistogram: """Create a histogram metric for recording values. @@ -603,7 +785,7 @@ def create_histogram( @abstractmethod def create_histogram_float( - self, name: str, description: Optional[str] = None, unit: Optional[str] = None + self, name: str, description: str | None = None, unit: str | None = None ) -> MetricHistogramFloat: """Create a histogram metric for recording values. @@ -619,7 +801,7 @@ def create_histogram_float( @abstractmethod def create_histogram_timedelta( - self, name: str, description: Optional[str] = None, unit: Optional[str] = None + self, name: str, description: str | None = None, unit: str | None = None ) -> MetricHistogramTimedelta: """Create a histogram metric for recording values. @@ -638,7 +820,7 @@ def create_histogram_timedelta( @abstractmethod def create_gauge( - self, name: str, description: Optional[str] = None, unit: Optional[str] = None + self, name: str, description: str | None = None, unit: str | None = None ) -> MetricGauge: """Create a gauge metric for setting values. @@ -654,7 +836,7 @@ def create_gauge( @abstractmethod def create_gauge_float( - self, name: str, description: Optional[str] = None, unit: Optional[str] = None + self, name: str, description: str | None = None, unit: str | None = None ) -> MetricGaugeFloat: """Create a gauge metric for setting values. @@ -699,13 +881,13 @@ def name(self) -> str: @property @abstractmethod - def description(self) -> Optional[str]: + def description(self) -> str | None: """Description for the metric if any.""" ... @property @abstractmethod - def unit(self) -> Optional[str]: + def unit(self) -> str | None: """Unit for the metric if any.""" ... @@ -734,7 +916,7 @@ class MetricCounter(MetricCommon): @abstractmethod def add( - self, value: int, additional_attributes: Optional[MetricAttributes] = None + self, value: int, additional_attributes: MetricAttributes | None = None ) -> None: """Add a value to the counter. @@ -755,7 +937,7 @@ class MetricHistogram(MetricCommon): @abstractmethod def record( - self, value: int, additional_attributes: Optional[MetricAttributes] = None + self, value: int, additional_attributes: MetricAttributes | None = None ) -> None: """Record a value on the histogram. @@ -776,7 +958,7 @@ class MetricHistogramFloat(MetricCommon): @abstractmethod def record( - self, value: float, additional_attributes: Optional[MetricAttributes] = None + self, value: float, additional_attributes: MetricAttributes | None = None ) -> None: """Record a value on the histogram. @@ -797,7 +979,7 @@ class MetricHistogramTimedelta(MetricCommon): @abstractmethod def record( - self, value: timedelta, additional_attributes: Optional[MetricAttributes] = None + self, value: timedelta, additional_attributes: MetricAttributes | None = None ) -> None: """Record a value on the histogram. @@ -820,7 +1002,7 @@ class MetricGauge(MetricCommon): @abstractmethod def set( - self, value: int, additional_attributes: Optional[MetricAttributes] = None + self, value: int, additional_attributes: MetricAttributes | None = None ) -> None: """Set a value on the gauge. @@ -841,7 +1023,7 @@ class MetricGaugeFloat(MetricCommon): @abstractmethod def set( - self, value: float, additional_attributes: Optional[MetricAttributes] = None + self, value: float, additional_attributes: MetricAttributes | None = None ) -> None: """Set a value on the gauge. @@ -859,32 +1041,32 @@ def set( class _NoopMetricMeter(MetricMeter): def create_counter( - self, name: str, description: Optional[str] = None, unit: Optional[str] = None + self, name: str, description: str | None = None, unit: str | None = None ) -> MetricCounter: return _NoopMetricCounter(name, description, unit) def create_histogram( - self, name: str, description: Optional[str] = None, unit: Optional[str] = None + self, name: str, description: str | None = None, unit: str | None = None ) -> MetricHistogram: return _NoopMetricHistogram(name, description, unit) def create_histogram_float( - self, name: str, description: Optional[str] = None, unit: Optional[str] = None + self, name: str, description: str | None = None, unit: str | None = None ) -> MetricHistogramFloat: return _NoopMetricHistogramFloat(name, description, unit) def create_histogram_timedelta( - self, name: str, description: Optional[str] = None, unit: Optional[str] = None + self, name: str, description: str | None = None, unit: str | None = None ) -> MetricHistogramTimedelta: return _NoopMetricHistogramTimedelta(name, description, unit) def create_gauge( - self, name: str, description: Optional[str] = None, unit: Optional[str] = None + self, name: str, description: str | None = None, unit: str | None = None ) -> MetricGauge: return _NoopMetricGauge(name, description, unit) def create_gauge_float( - self, name: str, description: Optional[str] = None, unit: Optional[str] = None + self, name: str, description: str | None = None, unit: str | None = None ) -> MetricGaugeFloat: return _NoopMetricGaugeFloat(name, description, unit) @@ -895,9 +1077,7 @@ def with_additional_attributes( class _NoopMetric(MetricCommon): - def __init__( - self, name: str, description: Optional[str], unit: Optional[str] - ) -> None: + def __init__(self, name: str, description: str | None, unit: str | None) -> None: self._name = name self._description = description self._unit = unit @@ -907,11 +1087,11 @@ def name(self) -> str: return self._name @property - def description(self) -> Optional[str]: + def description(self) -> str | None: return self._description @property - def unit(self) -> Optional[str]: + def unit(self) -> str | None: return self._unit def with_additional_attributes( @@ -922,42 +1102,42 @@ def with_additional_attributes( class _NoopMetricCounter(MetricCounter, _NoopMetric): def add( - self, value: int, additional_attributes: Optional[MetricAttributes] = None + self, value: int, additional_attributes: MetricAttributes | None = None ) -> None: pass class _NoopMetricHistogram(MetricHistogram, _NoopMetric): def record( - self, value: int, additional_attributes: Optional[MetricAttributes] = None + self, value: int, additional_attributes: MetricAttributes | None = None ) -> None: pass class _NoopMetricHistogramFloat(MetricHistogramFloat, _NoopMetric): def record( - self, value: float, additional_attributes: Optional[MetricAttributes] = None + self, value: float, additional_attributes: MetricAttributes | None = None ) -> None: pass class _NoopMetricHistogramTimedelta(MetricHistogramTimedelta, _NoopMetric): def record( - self, value: timedelta, additional_attributes: Optional[MetricAttributes] = None + self, value: timedelta, additional_attributes: MetricAttributes | None = None ) -> None: pass class _NoopMetricGauge(MetricGauge, _NoopMetric): def set( - self, value: int, additional_attributes: Optional[MetricAttributes] = None + self, value: int, additional_attributes: MetricAttributes | None = None ) -> None: pass class _NoopMetricGaugeFloat(MetricGaugeFloat, _NoopMetric): def set( - self, value: float, additional_attributes: Optional[MetricAttributes] = None + self, value: float, additional_attributes: MetricAttributes | None = None ) -> None: pass @@ -982,7 +1162,7 @@ class Priority: 2. Then consider "fairness_key" and "fairness_weight" for fairness balancing. """ - priority_key: Optional[int] = None + priority_key: int | None = None """Priority key is a positive integer from 1 to n, where smaller integers correspond to higher priorities (tasks run sooner). In general, tasks in a queue should be processed in close to priority order, although small deviations are possible. @@ -994,7 +1174,7 @@ class Priority: 3. """ - fairness_key: Optional[str] = None + fairness_key: str | None = None """A short string (max 64 bytes) that is used as a key for a fairness balancing mechanism. This can correspond to a tenant id or even fixed strings like "high", "low", etc. @@ -1005,7 +1185,7 @@ class Priority: Default is an empty string. """ - fairness_weight: Optional[float] = None + fairness_weight: float | None = None """A float that represents the weight for task dispatch for the associated fairness key. Tasks for a fairness key are dispatched in proportion to their weight. @@ -1052,10 +1232,7 @@ def __post_init__(self): class VersioningBehavior(IntEnum): - """Specifies when a workflow might move from a worker of one Build Id to another. - - WARNING: Experimental API. - """ + """Specifies when a workflow might move from a worker of one Build Id to another.""" UNSPECIFIED = ( temporalio.api.enums.v1.VersioningBehavior.VERSIONING_BEHAVIOR_UNSPECIFIED @@ -1073,10 +1250,7 @@ class VersioningBehavior(IntEnum): @dataclass(frozen=True) class WorkerDeploymentVersion: - """Represents the version of a specific worker deployment. - - WARNING: Experimental API. - """ + """Represents the version of a specific worker deployment.""" deployment_name: str build_id: str @@ -1088,7 +1262,7 @@ def to_canonical_string(self) -> str: @staticmethod def from_canonical_string(canonical: str) -> WorkerDeploymentVersion: """Parse a version from a canonical string, which must be in the format - `.`. Deployment name must not have a `.` in it. + `.`. Deployment name must not have a ``.`` in it. """ parts = canonical.split(".", maxsplit=1) if len(parts) != 2: @@ -1106,11 +1280,7 @@ def _to_proto(self) -> temporalio.api.deployment.v1.WorkerDeploymentVersion: class VersioningOverride(ABC): - """Represents the override of a worker's versioning behavior for a workflow execution. - - .. warning:: - Experimental API. - """ + """Represents the override of a worker's versioning behavior for a workflow execution.""" @abstractmethod def _to_proto(self) -> temporalio.api.workflow.v1.VersioningOverride: @@ -1120,11 +1290,7 @@ def _to_proto(self) -> temporalio.api.workflow.v1.VersioningOverride: @dataclass(frozen=True) class PinnedVersioningOverride(VersioningOverride): - """Workflow will be pinned to a specific deployment version. - - .. warning:: - Experimental API. - """ + """Workflow will be pinned to a specific deployment version.""" version: WorkerDeploymentVersion @@ -1143,11 +1309,7 @@ def _to_proto(self) -> temporalio.api.workflow.v1.VersioningOverride: @dataclass(frozen=True) class AutoUpgradeVersioningOverride(VersioningOverride): - """The workflow will auto-upgrade to the current deployment version on the next workflow task. - - .. warning:: - Experimental API. - """ + """The workflow will auto-upgrade to the current deployment version on the next workflow task.""" def _to_proto(self) -> temporalio.api.workflow.v1.VersioningOverride: """Convert to proto representation.""" @@ -1163,7 +1325,7 @@ def _to_proto(self) -> temporalio.api.workflow.v1.VersioningOverride: _arg_unset = object() -def _arg_or_args(arg: Any, args: Sequence[Any]) -> Sequence[Any]: +def _arg_or_args(arg: Any, args: Sequence[Any]) -> Sequence[Any]: # type:ignore[reportUnusedFunction] if arg is not _arg_unset: if args: raise ValueError("Cannot have arg and args") @@ -1171,10 +1333,10 @@ def _arg_or_args(arg: Any, args: Sequence[Any]) -> Sequence[Any]: return args -def _apply_headers( - source: Optional[Mapping[str, temporalio.api.common.v1.Payload]], +def _apply_headers( # type:ignore[reportUnusedFunction] + source: Mapping[str, temporalio.api.common.v1.Payload] | None, dest: google.protobuf.internal.containers.MessageMap[ - Text, temporalio.api.common.v1.Payload + str, temporalio.api.common.v1.Payload ], ) -> None: if source is None: @@ -1198,9 +1360,9 @@ def _apply_headers( ) -def _type_hints_from_func( +def _type_hints_from_func( # type:ignore[reportUnusedFunction] func: Callable, -) -> Tuple[Optional[List[Type]], Optional[Type]]: +) -> tuple[list[type] | None, type | None]: """Extracts the type hints from the function. Args: @@ -1236,7 +1398,7 @@ def _type_hints_from_func( hints = get_type_hints(func) ret_hint = hints.get("return") ret = ret_hint if ret_hint is not inspect.Signature.empty else None - args: List[Type] = [] + args: list[type] = [] for index, value in enumerate(sig.parameters.values()): # Ignore self on methods if ( @@ -1273,3 +1435,33 @@ class HeaderCodecBehavior(IntEnum): """Encode and decode all headers automatically""" WORKFLOW_ONLY_CODEC = 3 """Only automatically encode and decode headers in workflow activation encoding and decoding.""" + + +@dataclass +class _CompositeEvent: # pyright: ignore[reportUnusedClass] + # This should always be present, but is sometimes lazily set internally + thread_event: threading.Event | None + # Async event only for async activities + async_event: asyncio.Event | None + + def set(self) -> None: + if not self.thread_event: + raise RuntimeError("Missing event") + self.thread_event.set() + if self.async_event: + self.async_event.set() + + def is_set(self) -> bool: + if not self.thread_event: + raise RuntimeError("Missing event") + return self.thread_event.is_set() + + async def wait(self) -> None: + if not self.async_event: + raise RuntimeError("not in async activity") + await self.async_event.wait() + + def wait_sync(self, timeout: float | None = None) -> None: + if not self.thread_event: + raise RuntimeError("Missing event") + self.thread_event.wait(timeout) diff --git a/temporalio/contrib/aws/__init__.py b/temporalio/contrib/aws/__init__.py new file mode 100644 index 000000000..a8b8c648f --- /dev/null +++ b/temporalio/contrib/aws/__init__.py @@ -0,0 +1 @@ +"""AWS integrations for Temporal SDK.""" diff --git a/temporalio/contrib/aws/lambda_worker/README.md b/temporalio/contrib/aws/lambda_worker/README.md new file mode 100644 index 000000000..c12e7037d --- /dev/null +++ b/temporalio/contrib/aws/lambda_worker/README.md @@ -0,0 +1,122 @@ +# lambda_worker + +A wrapper for running [Temporal](https://temporal.io) workers inside AWS Lambda. A single +`run_worker` call handles the full per-invocation lifecycle: connecting to the Temporal server, +creating a worker with Lambda-tuned defaults, polling for tasks, and gracefully shutting down before +the invocation deadline. + +## Quick start + +```python +# handler.py +from temporalio.common import WorkerDeploymentVersion +from temporalio.contrib.aws.lambda_worker import LambdaWorkerConfig, run_worker + +from my_workflows import MyWorkflow +from my_activities import my_activity + + +def configure(config: LambdaWorkerConfig) -> None: + config.worker_config["task_queue"] = "my-task-queue" + config.worker_config["workflows"] = [MyWorkflow] + config.worker_config["activities"] = [my_activity] + + +lambda_handler = run_worker( + WorkerDeploymentVersion( + deployment_name="my-service", + build_id="v1.0", + ), + configure, +) +``` + +## Configuration + +Client connection settings (address, namespace, TLS, API key) are loaded +automatically from a TOML config file and/or environment variables via +`temporalio.envconfig`. The config file is resolved in order: + +1. `TEMPORAL_CONFIG_FILE` env var, if set. +2. `temporal.toml` in `$LAMBDA_TASK_ROOT` (typically `/var/task`). +3. `temporal.toml` in the current working directory. + +The file is optional -- if absent, only environment variables are used. + +The configure callback receives a `LambdaWorkerConfig` dataclass with fields +pre-populated with Lambda-appropriate defaults. Override any field directly in +the callback. The `task_queue` key in `worker_config` is pre-populated from the +`TEMPORAL_TASK_QUEUE` environment variable if set. + +### Sync, async, and async-generator configure + +The configure callback runs **once per invocation, inside that invocation's +event loop**, before the client connects. It may be: + +- a plain function `def configure(config) -> None`; +- an `async def configure(config) -> None` coroutine — awaited for setup (pair + with `shutdown_hooks` for teardown); +- an `async def configure(config): ...; yield; ...` async generator — statements + before the single `yield` run before the client connects, the worker runs + while the generator is suspended at the `yield`, and statements after the + `yield` run as teardown once the worker has stopped. + +The callback runs per invocation (rather than once at process start) because +event-loop-bound resources cannot be created before an event loop exists and +cannot be shared across invocations — each invocation runs under a fresh +`asyncio.run` loop. + +## Lambda-tuned worker defaults + +The package applies conservative concurrency limits suited to Lambda's resource +constraints: + +| Setting | Default | +| --- | --- | +| `max_concurrent_activities` | 2 | +| `max_concurrent_workflow_tasks` | 10 | +| `max_concurrent_local_activities` | 2 | +| `max_concurrent_nexus_tasks` | 5 | +| `workflow_task_poller_behavior` | `SimpleMaximum(2)` | +| `activity_task_poller_behavior` | `SimpleMaximum(1)` | +| `nexus_task_poller_behavior` | `SimpleMaximum(1)` | +| `graceful_shutdown_timeout` | 5 seconds | +| `max_cached_workflows` | 100 | +| `disable_eager_activity_execution` | Always `True` | + +Worker Deployment Versioning is always enabled. + +## Observability + +Metrics and tracing are opt-in. The `otel` module provides convenience helpers +for AWS Distro for OpenTelemetry (ADOT): + +```python +from temporalio.common import WorkerDeploymentVersion +from temporalio.contrib.aws.lambda_worker import LambdaWorkerConfig, run_worker +from temporalio.contrib.aws.lambda_worker.otel import apply_defaults, OtelOptions + + +def configure(config: LambdaWorkerConfig) -> None: + config.worker_config["task_queue"] = "my-task-queue" + config.worker_config["workflows"] = [MyWorkflow] + config.worker_config["activities"] = [my_activity] + apply_defaults(config, OtelOptions()) + + +lambda_handler = run_worker( + WorkerDeploymentVersion( + deployment_name="my-service", + build_id="v1.0", + ), + configure, +) +``` + +You can also use `apply_metrics` or `apply_tracing` individually. + +If you use OTEL, you can use +[ADOT](https://aws-otel.github.io/docs/getting-started/lambda/lambda-python) +(the AWS Distro For OpenTelemetry) to automatically integrate with AWS +observability functionality. Namely, you will want to add the Lambda layer in +the aforementioned link. We'll handle setting up the SDK for you. diff --git a/temporalio/contrib/aws/lambda_worker/__init__.py b/temporalio/contrib/aws/lambda_worker/__init__.py new file mode 100644 index 000000000..11f748c9b --- /dev/null +++ b/temporalio/contrib/aws/lambda_worker/__init__.py @@ -0,0 +1,49 @@ +"""A wrapper for running Temporal workers inside AWS Lambda. + +A single :py:func:`run_worker` call handles the full per-invocation lifecycle: connecting to the +Temporal server, creating a worker with Lambda-tuned defaults, polling for tasks, and gracefully +shutting down before the invocation deadline. + +Quick start:: + + from temporalio.common import WorkerDeploymentVersion + from temporalio.contrib.aws.lambda_worker import LambdaWorkerConfig, run_worker + + def configure(config: LambdaWorkerConfig) -> None: + config.worker_config["task_queue"] = "my-task-queue" + config.worker_config["workflows"] = [MyWorkflow] + config.worker_config["activities"] = [my_activity] + + lambda_handler = run_worker( + WorkerDeploymentVersion( + deployment_name="my-service", + build_id="v1.0", + ), + configure, + ) + +Configuration +------------- +Client connection settings (address, namespace, TLS, API key) are loaded automatically from a TOML +config file and/or environment variables via :py:mod:`temporalio.envconfig`. The config file is +resolved in order: + +1. ``TEMPORAL_CONFIG_FILE`` env var, if set. +2. ``temporal.toml`` in ``$LAMBDA_TASK_ROOT`` (typically ``/var/task``). +3. ``temporal.toml`` in the current working directory. + +The file is optional -- if absent, only environment variables are used. + +The configure callback receives a :py:class:`LambdaWorkerConfig` dataclass with fields pre-populated +with Lambda-appropriate defaults. Override any field directly in the callback. The ``task_queue`` +key in ``worker_config`` is pre-populated from the ``TEMPORAL_TASK_QUEUE`` environment variable if +set. +""" + +from temporalio.contrib.aws.lambda_worker._configure import LambdaWorkerConfig +from temporalio.contrib.aws.lambda_worker._run_worker import run_worker + +__all__ = [ + "LambdaWorkerConfig", + "run_worker", +] diff --git a/temporalio/contrib/aws/lambda_worker/_configure.py b/temporalio/contrib/aws/lambda_worker/_configure.py new file mode 100644 index 000000000..dd1657f6d --- /dev/null +++ b/temporalio/contrib/aws/lambda_worker/_configure.py @@ -0,0 +1,72 @@ +"""Configuration for the Lambda worker.""" + +from __future__ import annotations + +import asyncio +import logging +from collections.abc import Awaitable, Callable +from dataclasses import dataclass, field +from datetime import timedelta + +from temporalio.client import ClientConnectConfig +from temporalio.worker import WorkerConfig + +logger = logging.getLogger(__name__) + + +@dataclass +class LambdaWorkerConfig: + """Passed to the configure callback of :py:func:`run_worker`. + + Fields are pre-populated with Lambda-appropriate defaults before the configure callback is + invoked; the callback may read and override any of them. + + Use ``worker_config`` to set task queue, register workflows/activities, and tune worker options. + The ``task_queue`` key is pre-populated from the ``TEMPORAL_TASK_QUEUE`` environment variable if + set. + + Attributes: + client_connect_config: Keyword arguments that will be passed to + :py:meth:`temporalio.client.Client.connect`. Pre-populated from the + config file / environment variables via envconfig, with Lambda + defaults applied. + worker_config: Keyword arguments that will be passed to the + :py:class:`temporalio.worker.Worker` constructor (the ``client`` + key is managed internally). Pre-populated with Lambda-appropriate + defaults (low concurrency, eager activities disabled) and + ``task_queue`` from ``TEMPORAL_TASK_QUEUE`` if set. + shutdown_deadline_buffer: How long before the Lambda invocation + deadline the worker begins its shutdown sequence (worker drain + + shutdown hooks). Pre-populated to + ``graceful_shutdown_timeout + 2s``. If you change + ``graceful_shutdown_timeout`` in ``worker_config``, adjust this + accordingly. + shutdown_hooks: Functions called at the end of each Lambda invocation, + after the worker has stopped. Run in list order. Each may be sync + or async. Use this to flush telemetry providers or release other + per-process resources. + """ + + client_connect_config: ClientConnectConfig = field( + default_factory=ClientConnectConfig + ) + worker_config: WorkerConfig = field(default_factory=WorkerConfig) + shutdown_deadline_buffer: timedelta = field( + default_factory=lambda: timedelta(seconds=7) + ) + shutdown_hooks: list[Callable[[], Awaitable[None] | None]] = field( + default_factory=list + ) + + +async def _run_shutdown_hooks( # type:ignore[reportUnusedFunction] + config: LambdaWorkerConfig, +) -> None: + """Run all registered shutdown hooks in order, logging errors.""" + for fn in config.shutdown_hooks: + try: + result = fn() + if asyncio.iscoroutine(result): + await result + except Exception as e: + logger.error(f"shutdown hook error: {e}") diff --git a/temporalio/contrib/aws/lambda_worker/_defaults.py b/temporalio/contrib/aws/lambda_worker/_defaults.py new file mode 100644 index 000000000..1b93e3407 --- /dev/null +++ b/temporalio/contrib/aws/lambda_worker/_defaults.py @@ -0,0 +1,84 @@ +"""Lambda-tuned defaults for Temporal worker and client configuration.""" + +from __future__ import annotations + +import os +from collections.abc import Callable +from datetime import timedelta +from pathlib import Path + +from temporalio.worker import PollerBehaviorSimpleMaximum, WorkerConfig + +# ---- Lambda-tuned worker defaults ---- +# Conservative concurrency limits suited to Lambda's resource constraints. + +DEFAULT_MAX_CONCURRENT_ACTIVITIES: int = 2 +DEFAULT_MAX_CONCURRENT_WORKFLOW_TASKS: int = 10 +DEFAULT_MAX_CONCURRENT_LOCAL_ACTIVITIES: int = 2 +DEFAULT_MAX_CONCURRENT_NEXUS_TASKS: int = 5 +DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT: timedelta = timedelta(seconds=5) +DEFAULT_SHUTDOWN_HOOK_BUFFER: timedelta = timedelta(seconds=2) +DEFAULT_MAX_CACHED_WORKFLOWS: int = 30 + +DEFAULT_WORKFLOW_TASK_POLLER_BEHAVIOR = PollerBehaviorSimpleMaximum(maximum=2) +DEFAULT_ACTIVITY_TASK_POLLER_BEHAVIOR = PollerBehaviorSimpleMaximum(maximum=1) +DEFAULT_NEXUS_TASK_POLLER_BEHAVIOR = PollerBehaviorSimpleMaximum(maximum=1) + +# ---- Environment variable names ---- +ENV_TASK_QUEUE = "TEMPORAL_TASK_QUEUE" +ENV_LAMBDA_TASK_ROOT = "LAMBDA_TASK_ROOT" +ENV_CONFIG_FILE = "TEMPORAL_CONFIG_FILE" +DEFAULT_CONFIG_FILE = "temporal.toml" + + +def apply_lambda_worker_defaults(config: WorkerConfig) -> None: + """Apply Lambda-appropriate defaults to worker config. + + Only sets values that have not already been set (i.e. are absent from *config*). + ``disable_eager_activity_execution`` is always set to ``True``. + """ + config.setdefault("max_concurrent_activities", DEFAULT_MAX_CONCURRENT_ACTIVITIES) + config.setdefault( + "max_concurrent_workflow_tasks", DEFAULT_MAX_CONCURRENT_WORKFLOW_TASKS + ) + config.setdefault( + "max_concurrent_local_activities", DEFAULT_MAX_CONCURRENT_LOCAL_ACTIVITIES + ) + config.setdefault("max_concurrent_nexus_tasks", DEFAULT_MAX_CONCURRENT_NEXUS_TASKS) + config.setdefault("graceful_shutdown_timeout", DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT) + config.setdefault("max_cached_workflows", DEFAULT_MAX_CACHED_WORKFLOWS) + config.setdefault( + "workflow_task_poller_behavior", DEFAULT_WORKFLOW_TASK_POLLER_BEHAVIOR + ) + config.setdefault( + "activity_task_poller_behavior", DEFAULT_ACTIVITY_TASK_POLLER_BEHAVIOR + ) + config.setdefault("nexus_task_poller_behavior", DEFAULT_NEXUS_TASK_POLLER_BEHAVIOR) + # Always disable eager activities in Lambda. + config["disable_eager_activity_execution"] = True + + +def build_lambda_identity(request_id: str, function_arn: str) -> str: + """Build a worker identity string from the Lambda invocation context. + + Format: ``@``. + """ + return f"{request_id or 'unknown'}@{function_arn or 'unknown'}" + + +def lambda_default_config_file_path( + getenv: Callable[[str], str] = os.environ.get, # type: ignore[assignment] +) -> Path: + """Return the config file path for a Lambda environment. + + Resolution order: + + 1. ``TEMPORAL_CONFIG_FILE`` env var, if set. + 2. ``temporal.toml`` in ``$LAMBDA_TASK_ROOT`` (typically ``/var/task``). + 3. ``temporal.toml`` in the current working directory. + """ + config_file = getenv(ENV_CONFIG_FILE) + if config_file: + return Path(config_file) + root = getenv(ENV_LAMBDA_TASK_ROOT) or "." + return Path(root) / DEFAULT_CONFIG_FILE diff --git a/temporalio/contrib/aws/lambda_worker/_run_worker.py b/temporalio/contrib/aws/lambda_worker/_run_worker.py new file mode 100644 index 000000000..f384fb954 --- /dev/null +++ b/temporalio/contrib/aws/lambda_worker/_run_worker.py @@ -0,0 +1,369 @@ +from __future__ import annotations + +import asyncio +import inspect +import logging +import os +import sys +from collections.abc import AsyncGenerator, Awaitable, Callable +from contextlib import AbstractAsyncContextManager, asynccontextmanager +from dataclasses import dataclass, field +from datetime import timedelta +from typing import Any, TypeAlias + +import temporalio.client +import temporalio.worker +from temporalio.client import ClientConnectConfig +from temporalio.common import WorkerDeploymentVersion +from temporalio.contrib.aws.lambda_worker._configure import ( + LambdaWorkerConfig, + _run_shutdown_hooks, +) +from temporalio.contrib.aws.lambda_worker._defaults import ( + DEFAULT_SHUTDOWN_HOOK_BUFFER, + apply_lambda_worker_defaults, + build_lambda_identity, + lambda_default_config_file_path, +) +from temporalio.envconfig import ClientConfigProfile +from temporalio.worker import WorkerConfig, WorkerDeploymentConfig + +logger = logging.getLogger(__name__) + +# A plain, ``async def`` coroutine, ``async def`` generator, or async-context-manager +# callback. See run_worker. +ConfigureCallback: TypeAlias = Callable[ + [LambdaWorkerConfig], + None + | Awaitable[None] + | AsyncGenerator[None, None] + | AbstractAsyncContextManager[None], +] + + +@dataclass +class _WorkerDeps: + """External dependencies injected for testability.""" + + connect: Callable[..., Awaitable[temporalio.client.Client]] = field( + default_factory=lambda: temporalio.client.Client.connect + ) + create_worker: Callable[..., temporalio.worker.Worker] = field( + default_factory=lambda: temporalio.worker.Worker + ) + load_config: Callable[[], ClientConfigProfile] | None = None + getenv: Callable[[str], str | None] = field(default_factory=lambda: os.environ.get) + extract_lambda_ctx: Callable[[Any], tuple[str, str] | None] | None = None + + +def _default_load_config(getenv: Callable[[str], str | None]) -> ClientConfigProfile: + config_path = lambda_default_config_file_path(getenv) # type: ignore[arg-type] + return ClientConfigProfile.load(config_source=config_path) + + +def _default_extract_lambda_ctx( + lambda_context: Any, +) -> tuple[str, str] | None: + """Extract (request_id, function_arn) from a Lambda context object.""" + if lambda_context is None: + return None + request_id = getattr(lambda_context, "aws_request_id", None) + function_arn = getattr(lambda_context, "invoked_function_arn", None) + if request_id is not None and function_arn is not None: + return (request_id, function_arn) + return None + + +def _validate_task_queue(config: LambdaWorkerConfig) -> None: + """Raise if no task queue has been configured.""" + if not config.worker_config.get("task_queue"): + raise ValueError( + "task queue not configured: set " + 'worker_config["task_queue"] or the ' + "TEMPORAL_TASK_QUEUE environment variable" + ) + + +def run_worker( + version: WorkerDeploymentVersion, + configure: ConfigureCallback, +) -> Callable[[Any, Any], None]: + """Create a Temporal worker Lambda handler. + + Calls the *configure* callback to collect workflow/activity registrations and option + overrides, then returns a Lambda handler function. On each invocation the handler + connects to the Temporal server, starts a worker with Lambda-tuned defaults, polls for + tasks until the invocation deadline approaches, and then gracefully shuts down. + + The *configure* callback is invoked **once per invocation** and may be synchronous or + asynchronous: + + * **Synchronous** ``def configure(config) -> None`` — runs per invocation. Use for + static worker definition (task queue, registrations, option tuning) and resources + that are not bound to an event loop. + * **Async** ``async def configure(config) -> None`` — awaited per invocation. Use when + setup must ``await`` (for example, opening an async client). Pair with + ``shutdown_hooks`` for teardown. + * **Async generator** ``async def configure(config): ...; yield; ...`` (or an + equivalent ``@contextlib.asynccontextmanager``-decorated function) — entered per + invocation. Statements before the single ``yield`` run before the client connects; + the worker runs while the generator is suspended at the ``yield``; statements after + the ``yield`` run as teardown once the worker has stopped. Any ``shutdown_hooks`` + registered before the ``yield`` run after the worker stops but *before* the + post-``yield`` teardown, so this resource outlives the hooks (e.g. a telemetry + flush hook can still emit before the resource is closed). This is the recommended + shape for event-loop-bound resources that must live for the duration of the + invocation, such as an ``aioboto3`` S3 client backing the external-storage data + converter (see the async example below). + + The callback runs per invocation (not once at cold start) because event-loop-bound + resources cannot be created at cold start (there is no running loop) and cannot be + shared across invocations (each invocation runs under a fresh ``asyncio.run`` loop). + + The *version* parameter identifies this worker's deployment version. ``run_worker`` + always enables Worker Deployment Versioning (``use_worker_versioning=True``). To + provide a default versioning behavior for workflows that do not specify one at + registration time, set ``deployment_config`` in ``worker_config`` in the configure + callback. + + The returned handler has the signature ``handler(event, context)`` and should be set as + your Lambda function's handler entry point. + + Args: + version: The worker deployment version. Required. + configure: A callback that receives a :py:class:`LambdaWorkerConfig` + (pre-populated with Lambda defaults) and configures workflows, + activities, and options on it. May be sync, async, or an async + generator (see above). + + Returns: + A Lambda handler function. + + Example: + Synchronous configure (static worker definition):: + + from temporalio.common import WorkerDeploymentVersion + from temporalio.contrib.aws.lambda_worker import ( + LambdaWorkerConfig, + run_worker, + ) + + def configure(config: LambdaWorkerConfig) -> None: + config.worker_config["task_queue"] = "my-task-queue" + config.worker_config["workflows"] = [MyWorkflow] + config.worker_config["activities"] = [my_activity] + + lambda_handler = run_worker( + WorkerDeploymentVersion( + deployment_name="my-service", + build_id="v1.0"), + configure, + ) + + Async generator configure, bracketing an ``aioboto3`` S3 client. The session + lives at module scope (it is not event-loop-bound and caches credentials across + warm invocations); only the loop-bound client is opened per invocation:: + + import aioboto3 + import dataclasses + from temporalio.contrib.aws.s3driver import S3StorageDriver + from temporalio.contrib.aws.s3driver.aioboto3 import new_aioboto3_client + from temporalio.converter import DataConverter, ExternalStorage + + session = aioboto3.Session() + + async def configure(config: LambdaWorkerConfig): + config.worker_config["task_queue"] = "my-task-queue" + config.worker_config["workflows"] = [MyWorkflow] + async with session.client("s3") as s3_client: + driver = S3StorageDriver( + client=new_aioboto3_client(s3_client), bucket="my-payloads", + ) + config.client_connect_config["data_converter"] = dataclasses.replace( + DataConverter.default, + external_storage=ExternalStorage(drivers=[driver]), + ) + yield + + lambda_handler = run_worker( + WorkerDeploymentVersion( + deployment_name="my-service", + build_id="v1.0"), + configure, + ) + """ + deps = _WorkerDeps() + try: + return _run_worker_internal(version, configure, deps) + except Exception as e: + logger.error(f"fatal error running lambda worker: {e}") + sys.exit(1) + + +def _run_worker_internal( + version: WorkerDeploymentVersion, + configure: ConfigureCallback, + deps: _WorkerDeps, +) -> Callable[[Any, Any], None]: + """Core logic with injected dependencies for testability.""" + if not version.deployment_name or not version.build_id: + raise ValueError( + "version is required (deployment_name and build_id must be set)" + ) + + # Load client config from envconfig / TOML. + load_config = deps.load_config or (lambda: _default_load_config(deps.getenv)) + profile = load_config() + base_connect_config: ClientConnectConfig = {**profile.to_client_connect_config()} + + # Build base worker config with Lambda defaults. + base_worker_config: WorkerConfig = {} + apply_lambda_worker_defaults(base_worker_config) + + # Always enable deployment versioning. + base_worker_config["deployment_config"] = WorkerDeploymentConfig( + version=version, + use_worker_versioning=True, + ) + + # Calculate default shutdown buffer. + graceful_timeout = base_worker_config.get( + "graceful_shutdown_timeout", timedelta(seconds=5) + ) + shutdown_buffer = graceful_timeout + DEFAULT_SHUTDOWN_HOOK_BUFFER + + env_tq = deps.getenv("TEMPORAL_TASK_QUEUE") + + def _new_config() -> LambdaWorkerConfig: + """Fresh config per invocation; dicts/hooks are copied so nothing leaks across + invocations. + """ + config = LambdaWorkerConfig( + client_connect_config={**base_connect_config}, + worker_config={**base_worker_config}, + shutdown_deadline_buffer=shutdown_buffer, + ) + if env_tq: + config.worker_config["task_queue"] = env_tq + return config + + extract_lambda_ctx = deps.extract_lambda_ctx or _default_extract_lambda_ctx + + def _handler(_event: Any, lambda_context: Any) -> None: + asyncio.run( + _invocation_handler( + lambda_context=lambda_context, + configure=configure, + new_config=_new_config, + deps=deps, + extract_lambda_ctx=extract_lambda_ctx, + ) + ) + + return _handler + + +@asynccontextmanager +async def _invocation_config_scope( + configure: ConfigureCallback, + new_config: Callable[[], LambdaWorkerConfig], +) -> AsyncGenerator[LambdaWorkerConfig, None]: + """Run *configure* (see run_worker for the forms) against a fresh per-invocation config + and yield it. For the generator / context-manager forms, post-``yield`` teardown runs + when the caller's block exits, including on error. Task queue is validated after + setup. + """ + config = new_config() + if inspect.isasyncgenfunction(configure): + # Wrap the bare async generator so it drives like a context manager: setup on + # enter, teardown on exit. + cm: Any = asynccontextmanager(configure)(config) + else: + result = configure(config) + if inspect.isawaitable(result): + await result + # A @asynccontextmanager-decorated callback returns the context manager directly. + cm = result if result is not None and hasattr(result, "__aenter__") else None + + if cm is not None: + async with cm: + _validate_task_queue(config) + yield config + else: + _validate_task_queue(config) + yield config + + +async def _invocation_handler( + *, + lambda_context: Any, + configure: ConfigureCallback, + new_config: Callable[[], LambdaWorkerConfig], + deps: _WorkerDeps, + extract_lambda_ctx: Callable[[Any], tuple[str, str] | None], +) -> None: + """Handle a single Lambda invocation.""" + async with _invocation_config_scope(configure, new_config) as config: + shutdown_buffer = config.shutdown_deadline_buffer + + # Check deadline feasibility. + remaining_ms_fn = getattr(lambda_context, "get_remaining_time_in_millis", None) + deadline_available = remaining_ms_fn is not None + if deadline_available: + assert remaining_ms_fn is not None + remaining = timedelta(milliseconds=remaining_ms_fn()) + work_time = remaining - shutdown_buffer + if work_time <= timedelta(seconds=1): + raise RuntimeError( + f"Lambda timeout is too short: {remaining.total_seconds():.1f}s " + f"remaining but {shutdown_buffer.total_seconds():.1f}s is " + f"reserved for shutdown, leaving no time for work. " + f"Increase the function timeout or decrease the shutdown " + f"deadline buffer" + ) + elif work_time < timedelta(seconds=5): + logger.warning( + "Lambda timeout leaves less than 5s for work after " + "shutdown buffer; consider increasing the function " + "timeout or decreasing the shutdown deadline buffer " + "(work_time=%s, shutdown_buffer=%s)", + work_time, + shutdown_buffer, + ) + + # Build per-invocation connect kwargs with identity from Lambda context. + invocation_connect_kwargs: ClientConnectConfig = { + **config.client_connect_config + } + if "identity" not in invocation_connect_kwargs: + ctx_info = extract_lambda_ctx(lambda_context) + if ctx_info is not None: + request_id, function_arn = ctx_info + invocation_connect_kwargs["identity"] = build_lambda_identity( + request_id, function_arn + ) + + # Connect to Temporal. + client = await deps.connect(**invocation_connect_kwargs) + + # Create the worker. + worker = deps.create_worker(client, **config.worker_config) + + # Run the worker until the deadline approaches or context is done. + if deadline_available: + assert remaining_ms_fn is not None + work_time_secs = ( + timedelta(milliseconds=remaining_ms_fn()) - shutdown_buffer + ).total_seconds() + if work_time_secs > 0: + try: + await asyncio.wait_for(worker.run(), timeout=work_time_secs) + except asyncio.TimeoutError: + pass + else: + # No deadline - run until cancelled. + await worker.run() + + # Run shutdown hooks after worker has stopped, before any async-generator + # configure teardown (which unwinds on scope exit). + await _run_shutdown_hooks(config) diff --git a/temporalio/contrib/aws/lambda_worker/otel.py b/temporalio/contrib/aws/lambda_worker/otel.py new file mode 100644 index 000000000..216e80f48 --- /dev/null +++ b/temporalio/contrib/aws/lambda_worker/otel.py @@ -0,0 +1,241 @@ +"""OpenTelemetry helpers for Temporal workers running inside AWS Lambda. + +Use :py:func:`apply_defaults` inside a :py:func:`run_worker` configure callback for a +batteries-included setup that creates an OTel collector exporter and tracing plugin, suitable +for use with the AWS Distro for OpenTelemetry (ADOT) Lambda layer. + +Use :py:func:`apply_tracing` or :py:func:`build_metrics_telemetry_config` individually if you only +need one. +""" + +from __future__ import annotations + +import logging +import os +from dataclasses import dataclass, field +from datetime import timedelta + +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace.export import BatchSpanProcessor +from opentelemetry.semconv.attributes.service_attributes import SERVICE_NAME +from opentelemetry.trace import get_tracer_provider, set_tracer_provider + +from temporalio.contrib.aws.lambda_worker._configure import LambdaWorkerConfig +from temporalio.contrib.opentelemetry import OpenTelemetryPlugin, create_tracer_provider +from temporalio.runtime import OpenTelemetryConfig, Runtime, TelemetryConfig + +logger = logging.getLogger(__name__) + + +@dataclass +class OtelOptions: + """Options for :py:func:`apply_defaults`. + + Attributes: + metric_periodicity: How often the Core SDK exports metrics to the + collector. Defaults to 10 seconds. Set this shorter than your + Lambda timeout to ensure at least one export per invocation. + service_name: OTel service name resource attribute. If empty, + falls back to ``OTEL_SERVICE_NAME``, then + ``AWS_LAMBDA_FUNCTION_NAME``, then + ``"temporal-lambda-worker"``. + collector_endpoint: OTLP collector endpoint (e.g. + ``"http://localhost:4317"``). If empty, falls back to + ``OTEL_EXPORTER_OTLP_ENDPOINT``, then + ``"http://localhost:4317"``. + """ + + metric_periodicity: timedelta = field(default_factory=lambda: timedelta(seconds=10)) + service_name: str = "" + collector_endpoint: str = "" + + +def _resolve_service_name(options: OtelOptions) -> str: + service_name = options.service_name + if not service_name: + service_name = os.environ.get("OTEL_SERVICE_NAME", "") + if not service_name: + service_name = os.environ.get("AWS_LAMBDA_FUNCTION_NAME", "") + if not service_name: + service_name = "temporal-lambda-worker" + return service_name + + +def _resolve_endpoint(options: OtelOptions) -> str: + endpoint = options.collector_endpoint + if not endpoint: + endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "") + if not endpoint: + endpoint = "http://localhost:4317" + return endpoint + + +def apply_defaults( + config: LambdaWorkerConfig, + options: OtelOptions | None = None, +) -> None: + """Configure OTel metrics and tracing with AWS Lambda defaults. + + Sets up Core SDK metrics export via a :py:class:`temporalio.runtime.Runtime` with an + :py:class:`temporalio.runtime.OpenTelemetryConfig` pointing at the OTLP collector, and adds the + :py:class:`temporalio.contrib.opentelemetry.OpenTelemetryPlugin` for distributed tracing with + workflow sandbox passthrough. + + Creates a replay-safe ``TracerProvider`` (with X-Ray ID generator and OTLP gRPC exporter if + available) and sets it as the global OpenTelemetry tracer provider. The + :py:class:`temporalio.contrib.opentelemetry.OpenTelemetryPlugin` uses the global provider, so + it must be set before the worker starts. + + The collector endpoint defaults to ``http://localhost:4317``, which is the endpoint expected by + the ADOT collector Lambda layer. + + Registers a per-invocation ``ForceFlush`` shutdown hook for the global ``TracerProvider`` so + pending traces are exported before each Lambda invocation completes. + + Metrics are exported on the ``metric_periodicity`` interval by the runtime's internal thread. + There is no explicit flush API for these metrics; set ``metric_periodicity`` short enough to + ensure at least one export per invocation. + + Args: + config: The :py:class:`LambdaWorkerConfig` to configure. + options: Optional overrides for service name, endpoint, etc. + """ + if options is None: + options = OtelOptions() + + endpoint = _resolve_endpoint(options) + service_name = _resolve_service_name(options) + + telemetry_config = build_metrics_telemetry_config( + endpoint=endpoint, + service_name=service_name, + metric_periodicity=options.metric_periodicity, + ) + runtime = Runtime(telemetry=telemetry_config) + config.client_connect_config["runtime"] = runtime + + resource = Resource.create({SERVICE_NAME: service_name}) + + # Try to use X-Ray ID generator if available. + try: + from opentelemetry.sdk.extension.aws.trace import ( # type: ignore[reportMissingTypeStubs] + AwsXRayIdGenerator, + ) + + tracer_provider = create_tracer_provider( + resource=resource, id_generator=AwsXRayIdGenerator() + ) + except ImportError: + logger.warning( + "opentelemetry-sdk-extension-aws is not installed; " + "X-Ray trace ID generation is disabled. " + "Install the 'lambda-worker-otel' extra for full ADOT support." + ) + tracer_provider = create_tracer_provider(resource=resource) + + # Use OTLP gRPC exporter if available, otherwise skip trace export. + try: + from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( + OTLPSpanExporter, + ) + + tracer_provider.add_span_processor( + BatchSpanProcessor(OTLPSpanExporter(endpoint=endpoint, insecure=True)) + ) + except ImportError: + logger.warning( + "opentelemetry-exporter-otlp-proto-grpc is not installed; " + "traces will not be exported to the OTLP collector. " + "Install the 'lambda-worker-otel' extra for full ADOT support." + ) + + # Set as global so the OpenTelemetryPlugin picks it up. + set_tracer_provider(tracer_provider) + + apply_tracing(config) + + +def build_metrics_telemetry_config( + *, + endpoint: str = "", + service_name: str = "", + metric_periodicity: timedelta | None = None, +) -> TelemetryConfig: + """Build a :py:class:`temporalio.runtime.TelemetryConfig` for OTel metrics. + + Returns a ``TelemetryConfig`` with :py:class:`temporalio.runtime.OpenTelemetryConfig` metrics + pointed at the given OTLP collector endpoint. Use this when you need to compose metrics config + with other telemetry settings (e.g. custom logging) into your own + :py:class:`temporalio.runtime.Runtime`. + + Core SDK metrics are exported on the ``metric_periodicity`` interval by the runtime's internal + thread. There is no explicit flush API; set ``metric_periodicity`` short enough to ensure at + least one export per Lambda invocation. + + Example:: + + telemetry = build_metrics_telemetry_config( + endpoint="http://localhost:4317", + service_name="my-service", + ) + # Customize further: + telemetry_config = dataclasses.replace( + telemetry, logging=my_logging_config + ) + runtime = Runtime(telemetry=telemetry_config) + config.client_connect_config["runtime"] = runtime + + Args: + endpoint: OTLP collector endpoint. Defaults to + ``http://localhost:4317``. + service_name: OTel service name. Used as a global tag. + metric_periodicity: How often metrics are exported. + + Returns: + A ``TelemetryConfig`` ready to pass to + :py:class:`temporalio.runtime.Runtime`. + """ + if not endpoint: + endpoint = "http://localhost:4317" + + otel_config = OpenTelemetryConfig( + url=endpoint, + metric_periodicity=metric_periodicity, + ) + + global_tags: dict[str, str] = {} + if service_name: + global_tags["service_name"] = service_name + + return TelemetryConfig( + metrics=otel_config, + global_tags=global_tags, + ) + + +def apply_tracing(config: LambdaWorkerConfig) -> None: + """Configure only OTel tracing (no metrics) on the Lambda worker config. + + Adds an :py:class:`temporalio.contrib.opentelemetry.OpenTelemetryPlugin` to + ``config.worker_config["plugins"]``. The plugin uses the global + ``TracerProvider`` set via ``opentelemetry.trace.set_tracer_provider``. + Ensure your provider is set globally before the worker starts. + + Also registers a ``ForceFlush`` shutdown hook that flushes the global + ``TracerProvider`` (if it supports ``force_flush``). + + Args: + config: The :py:class:`LambdaWorkerConfig` to configure. + """ + plugin = OpenTelemetryPlugin() + plugins = list(config.worker_config.get("plugins", [])) + plugins.append(plugin) + config.worker_config["plugins"] = plugins + + async def _flush() -> None: + provider = get_tracer_provider() + flush = getattr(provider, "force_flush", None) + if flush is not None: + flush() + + config.shutdown_hooks.append(_flush) diff --git a/temporalio/contrib/aws/s3driver/README.md b/temporalio/contrib/aws/s3driver/README.md new file mode 100644 index 000000000..7494e4b96 --- /dev/null +++ b/temporalio/contrib/aws/s3driver/README.md @@ -0,0 +1,119 @@ +# AWS Integration for Temporal Python SDK + +> ⚠️ **This package is currently at an experimental release stage.** ⚠️ + +This package provides AWS integrations for the Temporal Python SDK, including an Amazon S3 driver for [external storage](../../../README.md#external-storage). + +## S3 Driver + +`S3StorageDriver` stores and retrieves Temporal payloads in Amazon S3. It accepts any `S3StorageDriverClient` implementation and a `bucket` — either a static name or a callable for dynamic per-payload selection. + +### Using the built-in aioboto3 client + +The SDK ships with an [`aioboto3`](https://github.com/terrycain/aioboto3)-based client. Install the extra to pull in its dependencies: + + python -m pip install "temporalio[aioboto3]" + +```python +import aioboto3 +import dataclasses +from temporalio.client import Client +from temporalio.contrib.aws.s3driver import S3StorageDriver +from temporalio.contrib.aws.s3driver.aioboto3 import new_aioboto3_client +from temporalio.converter import DataConverter, ExternalStorage + +session = aioboto3.Session() +# To see how to set credentials and region via environment, config objects, or configuration files, +# see: +# https://docs.aws.amazon.com/boto3/latest/guide/configuration.html +async with session.client("s3") as s3_client: + driver = S3StorageDriver( + client=new_aioboto3_client(s3_client), + bucket="my-temporal-payloads", + ) + + client = await Client.connect( + "localhost:7233", + data_converter=dataclasses.replace( + DataConverter.default, + external_storage=ExternalStorage(drivers=[driver]), + ), + ) +``` + +### Custom S3 client implementations + +To use a different S3 library, subclass `S3StorageDriverClient` and implement `put_object`, `get_object`, and `object_exists`. The ABC has no external dependencies, so no AWS packages are required to import it. + +```python +from temporalio.contrib.aws.s3driver import S3StorageDriverClient + +class MyS3Client(S3StorageDriverClient): + async def put_object(self, *, bucket: str, key: str, data: bytes) -> None: ... + async def object_exists(self, *, bucket: str, key: str) -> bool: ... + async def get_object(self, *, bucket: str, key: str) -> bytes: ... + +driver = S3StorageDriver(client=MyS3Client(), bucket="my-temporal-payloads") +``` + +### Key structure + +Payloads are stored under content-addressable keys derived from a SHA-256 hash of the serialized payload bytes, segmented by namespace and workflow/activity identifiers when serialization context is available. + +Workflow key: + + v0/ns/{namespace}/wt/{workflow-type}/wi/{workflow-id}/ri/{run-id}/d/{hash-algorithm}/{hex-digest} + +Activity key: + + v0/ns/{namespace}/at/{activity-type}/ai/{activity-id}/ri/{run-id}/d/{hash-algorithm}/{hex-digest} + +Fallback key (used when no namespace, workflow, or activity information is available): + + v0/d/{hash-algorithm}/{hex-digest} + +- Missing values (including a missing run ID) are encoded as the literal `null`. +- `hex-digest` is the lower-case SHA-256 hex digest (64 characters). +- Dynamic path segments are percent-encoded: any byte outside S3's [safe character set](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html) (`0-9`, `a-z`, `A-Z`, and `! - _ . * ' ( )`) is escaped as `%XX` over its UTF-8 bytes. + +### Notes + +* Any driver used to store payloads must also be configured on the component that retrieves them. If the client stores workflow inputs using this driver, the worker must include it in its `ExternalStorage.drivers` list to retrieve them. +* The target S3 bucket must already exist; the driver will not create it. +* Identical serialized bytes within the same namespace and workflow (or activity) share the same S3 object — the key is content-addressable within that scope. The same bytes used across different workflows or namespaces produce distinct S3 objects because the key includes the namespace and workflow/activity identifiers. +* Only payloads at or above `ExternalStorage.payload_size_threshold` (default: 256 KiB) are offloaded; smaller payloads are stored inline. Set `ExternalStorage.payload_size_threshold` to `0` to offload every payload regardless of size. +* `S3StorageDriver.max_payload_size` (default: 50 MiB) sets a hard upper limit on the serialized size of any single payload. A `ValueError` is raised at store time if a payload exceeds this limit. Increase it if your workflows produce payloads larger than 50 MiB. +* Override `S3StorageDriver.driver_name` only when registering multiple `S3StorageDriver` instances with distinct configurations under the same `ExternalStorage.drivers` list. + +### Dynamic Bucket Selection + +To select the S3 bucket per payload, pass a callable as `bucket`: + +```python +from temporalio.contrib.aws.s3driver import S3StorageDriver +from temporalio.contrib.aws.s3driver.aioboto3 import new_aioboto3_client + +driver = S3StorageDriver( + client=new_aioboto3_client(s3_client), + bucket=lambda context, payload: ( + "large-payloads" if payload.ByteSize() > 10 * 1024 * 1024 else "small-payloads" + ), +) +``` + +### Required IAM permissions + +The AWS credentials used by your S3 client must have the following S3 permissions on the target bucket and its objects: + +```json +{ + "Effect": "Allow", + "Action": [ + "s3:PutObject", + "s3:GetObject" + ], + "Resource": "arn:aws:s3:::my-temporal-payloads/*" +} +``` + +`s3:PutObject` is required by components that store payloads (typically the Temporal client and worker sending workflow/activity inputs), and `s3:GetObject` is required by components that retrieve them (typically workers and clients reading results). Components that only retrieve payloads do not need `s3:PutObject`, and vice versa. diff --git a/temporalio/contrib/aws/s3driver/__init__.py b/temporalio/contrib/aws/s3driver/__init__.py new file mode 100644 index 000000000..cdc349e24 --- /dev/null +++ b/temporalio/contrib/aws/s3driver/__init__.py @@ -0,0 +1,13 @@ +"""Amazon S3 storage driver for Temporal external storage. + +.. warning:: + This API is experimental. +""" + +from temporalio.contrib.aws.s3driver._client import S3StorageDriverClient +from temporalio.contrib.aws.s3driver._driver import S3StorageDriver + +__all__ = [ + "S3StorageDriverClient", + "S3StorageDriver", +] diff --git a/temporalio/contrib/aws/s3driver/_client.py b/temporalio/contrib/aws/s3driver/_client.py new file mode 100644 index 000000000..f49eead87 --- /dev/null +++ b/temporalio/contrib/aws/s3driver/_client.py @@ -0,0 +1,41 @@ +"""S3 storage driver client abstraction for the S3 storage driver. + +.. warning:: + This API is experimental. +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Mapping + + +class S3StorageDriverClient(ABC): + """Abstract base class for S3 object operations. + + Implementations must support ``put_object`` and ``get_object``. Multipart + upload handling (if needed) is an internal concern of each implementation. + + .. warning:: + This API is experimental. + """ + + @abstractmethod + async def put_object(self, *, bucket: str, key: str, data: bytes) -> None: + """Upload *data* to the given S3 *bucket* and *key*.""" + + @abstractmethod + async def object_exists(self, *, bucket: str, key: str) -> bool: + """Return ``True`` if an object exists at the given *bucket* and *key*.""" + + @abstractmethod + async def get_object(self, *, bucket: str, key: str) -> bytes: + """Download and return the bytes stored at the given S3 *bucket* and *key*.""" + + def describe(self) -> Mapping[str, str]: + """Return client-specific diagnostic metadata (e.g. region, credentials + source) that the driver appends to error messages. Implementations may + override this to surface configuration that is useful for debugging + common misconfigurations. Returns an empty mapping by default. + """ + return {} diff --git a/temporalio/contrib/aws/s3driver/_driver.py b/temporalio/contrib/aws/s3driver/_driver.py new file mode 100644 index 000000000..4bcf9de25 --- /dev/null +++ b/temporalio/contrib/aws/s3driver/_driver.py @@ -0,0 +1,250 @@ +"""Amazon S3 storage driver for Temporal external storage. + +.. warning:: + This API is experimental. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import string +from collections.abc import Callable, Coroutine, Sequence +from typing import Any, TypeVar + +from temporalio.api.common.v1 import Payload +from temporalio.contrib.aws.s3driver._client import S3StorageDriverClient +from temporalio.converter import ( + StorageDriver, + StorageDriverActivityInfo, + StorageDriverClaim, + StorageDriverRetrieveContext, + StorageDriverStoreContext, + StorageDriverWorkflowInfo, +) + +_T = TypeVar("_T") + +# S3's safe character set for object key names. See +# https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html +_S3_SAFE_CHARS = frozenset(string.ascii_letters + string.digits + "!-_.*'()") + + +def _percent_encode(val: str | None) -> str | None: + """Percent-encode a path segment per the S3 key spec. + + Encodes every byte outside :data:`_S3_SAFE_CHARS` as ``%XX`` with + upper-case hex digits, operating on the UTF-8 bytes so non-ASCII + characters are escaped byte by byte. Returns ``None`` for empty or + missing values so callers can substitute ``"null"``. + """ + if not val: + return None + return "".join( + chr(byte) if chr(byte) in _S3_SAFE_CHARS else f"%{byte:02X}" + for byte in val.encode("utf-8") + ) + + +def _format_client_context(client: S3StorageDriverClient) -> str: + """Format the client's ``describe()`` output as ", k=v, k=v" for error + messages. Returns an empty string when the client reports no metadata or + describe itself raises (diagnostic output must never mask the real error). + """ + try: + info = client.describe() + except Exception: + return "" + if not info: + return "" + return "".join(f", {k}={v}" for k, v in info.items()) + + +async def _gather_with_cancellation( + coros: Sequence[Coroutine[Any, Any, _T]], +) -> list[_T]: + """Run coroutines concurrently, cancelling all remaining tasks if one fails.""" + if not coros: + return [] + tasks = [asyncio.ensure_future(c) for c in coros] + try: + return list(await asyncio.gather(*tasks)) + except BaseException: + for t in tasks: + t.cancel() + await asyncio.gather(*tasks, return_exceptions=True) + raise + + +class S3StorageDriver(StorageDriver): + """Driver for storing and retrieving Temporal payloads in Amazon S3. + + Requires an :class:`S3StorageDriverClient` and a ``bucket``. Payloads are keyed by + a SHA-256 hash of their serialized bytes, segmented by namespace and + workflow/activity identifiers derived from the serialization context. + + .. warning:: + This API is experimental. + """ + + def __init__( + self, + client: S3StorageDriverClient, + bucket: str | Callable[[StorageDriverStoreContext, Payload], str], + driver_name: str = "aws.s3driver", + max_payload_size: int = 50 * 1024 * 1024, + ): + """Constructs the S3 driver. + + Args: + client: An :class:`S3StorageDriverClient` implementation. Use + :func:`temporalio.contrib.aws.s3driver.aioboto3.new_aioboto3_client` to + wrap an aioboto3 S3 client. + bucket: S3 bucket name, access point ARN, or a callable that + accepts ``(StorageDriverStoreContext, Payload)`` and returns + a bucket name. A callable allows dynamic per-payload bucket + selection. + driver_name: Name of this driver instance. Defaults to + ``"aws.s3driver"``. Override when registering + multiple S3StorageDriver instances with distinct configurations + under the same ``temporalio.extstore.Options.drivers`` list. + max_payload_size: Maximum serialized payload size in bytes that the + driver will accept. Defaults to 52428800 (50 MiB). Raise this + value if your workload requires larger payloads; lower it to + enforce stricter limits. + """ + if max_payload_size <= 0: + raise ValueError("max_payload_size must be greater than zero") + self._client = client + self._bucket = bucket + self._driver_name = driver_name or "aws.s3driver" + self._max_payload_size = max_payload_size + + def name(self) -> str: + """Return the driver instance name.""" + return self._driver_name + + def type(self) -> str: + """Return the driver type identifier.""" + return "aws.s3driver" + + def _get_bucket(self, context: StorageDriverStoreContext, payload: Payload) -> str: + """Resolve bucket using the configured strategy.""" + if callable(self._bucket): + return self._bucket(context, payload) + return self._bucket + + async def store( + self, + context: StorageDriverStoreContext, + payloads: Sequence[Payload], + ) -> list[StorageDriverClaim]: + """Stores payloads in S3 and returns a ``temporalio.extstore.DriverClaim`` for each one. + + Payloads are keyed by their SHA-256 hash, so identical serialized bytes + share the same S3 object. Deduplication is best-effort because the same + Python value may serialize differently across payload converter versions + (e.g. proto binary). The returned list is the same length as + ``payloads``. + """ + # Build context segments from the target identity. + context_segments = "" + target = context.target + namespace = _percent_encode(target.namespace) if target is not None else None + namespace_segment = f"/ns/{namespace}" if namespace else "" + if isinstance(target, StorageDriverWorkflowInfo): + wf_type = _percent_encode(target.type) or "null" + wf_id = _percent_encode(target.id) or "null" + wf_run_id = _percent_encode(target.run_id) or "null" + context_segments = f"/wt/{wf_type}/wi/{wf_id}/ri/{wf_run_id}" + elif isinstance(target, StorageDriverActivityInfo): + act_type = _percent_encode(target.type) or "null" + act_id = _percent_encode(target.id) or "null" + act_run_id = _percent_encode(target.run_id) or "null" + context_segments = f"/at/{act_type}/ai/{act_id}/ri/{act_run_id}" + + async def _upload(payload: Payload) -> StorageDriverClaim: + bucket = self._get_bucket(context, payload) + + payload_bytes = payload.SerializeToString() + if len(payload_bytes) > self._max_payload_size: + raise ValueError( + f"Payload size {len(payload_bytes)} bytes exceeds the configured " + f"max_payload_size of {self._max_payload_size} bytes" + ) + + hash_digest = hashlib.sha256(payload_bytes).hexdigest().lower() + + digest_segments = f"/d/sha256/{hash_digest}" + + key = f"v0{namespace_segment}{context_segments}{digest_segments}" + + try: + if not await self._client.object_exists(bucket=bucket, key=key): + await self._client.put_object( + bucket=bucket, key=key, data=payload_bytes + ) + except Exception as e: + raise RuntimeError( + f"S3StorageDriver store failed [bucket={bucket}, key={key}" + f"{_format_client_context(self._client)}]" + ) from e + + return StorageDriverClaim( + claim_data={ + "bucket": bucket, + "key": key, + "hash_algorithm": "sha256", + "hash_value": hash_digest, + }, + ) + + return await _gather_with_cancellation([_upload(p) for p in payloads]) + + async def retrieve( + self, + context: StorageDriverRetrieveContext, # noqa: ARG002 + claims: Sequence[StorageDriverClaim], + ) -> list[Payload]: + """Retrieves payloads from S3 for the given ``temporalio.extstore.DriverClaim`` list.""" + + async def _download(claim: StorageDriverClaim) -> Payload: + bucket = claim.claim_data["bucket"] + key = claim.claim_data["key"] + + try: + payload_bytes = await self._client.get_object(bucket=bucket, key=key) + except Exception as e: + raise RuntimeError( + f"S3StorageDriver retrieve failed [bucket={bucket}, key={key}" + f"{_format_client_context(self._client)}]" + ) from e + + hash_algorithm = claim.claim_data.get("hash_algorithm") + expected_hash = claim.claim_data.get("hash_value") + if not hash_algorithm or not expected_hash: + raise ValueError( + f"S3StorageDriver claim is missing required content hash information " + f"[bucket={bucket}, key={key}]: " + f"claim_data must contain 'hash_algorithm' and 'hash_value'" + ) + if hash_algorithm != "sha256": + raise ValueError( + f"S3StorageDriver unsupported hash algorithm " + f"[bucket={bucket}, key={key}]: " + f"expected sha256, got {hash_algorithm}" + ) + actual_hash = hashlib.sha256(payload_bytes).hexdigest().lower() + if actual_hash != expected_hash: + raise ValueError( + f"S3StorageDriver integrity check failed " + f"[bucket={bucket}, key={key}]: " + f"expected {hash_algorithm}:{expected_hash}, " + f"got {hash_algorithm}:{actual_hash}" + ) + + payload = Payload() + payload.ParseFromString(payload_bytes) + return payload + + return await _gather_with_cancellation([_download(c) for c in claims]) diff --git a/temporalio/contrib/aws/s3driver/aioboto3.py b/temporalio/contrib/aws/s3driver/aioboto3.py new file mode 100644 index 000000000..f6eda82ec --- /dev/null +++ b/temporalio/contrib/aws/s3driver/aioboto3.py @@ -0,0 +1,79 @@ +"""Aioboto3 adapter for the S3 storage driver client. + +.. warning:: + This API is experimental. +""" + +from __future__ import annotations + +import io +from collections.abc import Mapping + +from botocore.exceptions import ClientError +from types_aiobotocore_s3.client import S3Client + +from temporalio.contrib.aws.s3driver._client import S3StorageDriverClient + + +class _Aioboto3StorageDriverClient(S3StorageDriverClient): + """Adapter that wraps an aioboto3 S3 client as an :class:`S3StorageDriverClient`. + + Internally delegates to ``upload_fileobj`` for uploads (which handles + multipart automatically for objects above the multipart threshold) and + ``get_object`` for downloads. + + .. warning:: + This API is experimental. + """ + + def __init__(self, client: S3Client) -> None: + """Wrap an aioboto3 S3 client. + + Args: + client: An aioboto3 S3 client, typically obtained from + ``aioboto3.Session().client("s3")``. + """ + self._client = client + + def describe(self) -> Mapping[str, str]: + """Region of the wrapped aioboto3 client, surfaced in driver error + messages to short-circuit the most common silent 403 misconfiguration. + """ + region = self._client.meta.region_name + return {"client_region": region} if region else {} + + async def object_exists(self, *, bucket: str, key: str) -> bool: + """Check existence via aioboto3's ``head_object``.""" + try: + await self._client.head_object(Bucket=bucket, Key=key) + return True + except ClientError as e: + # head_object returns 404 as a ClientError when the key doesn't exist. + if e.response.get("Error", {}).get("Code") == "404": + return False + raise + + async def put_object(self, *, bucket: str, key: str, data: bytes) -> None: + """Upload *data* via aioboto3's ``upload_fileobj``.""" + # upload_fileobj is an aioboto3-specific method not in the + # types_aiobotocore_s3 stubs; it handles multipart automatically. + await self._client.upload_fileobj(io.BytesIO(data), bucket, key) # type: ignore[arg-type] + + async def get_object(self, *, bucket: str, key: str) -> bytes: + """Download bytes via aioboto3's ``get_object``.""" + response = await self._client.get_object(Bucket=bucket, Key=key) + # StreamingBody.read() is untyped in aiobotocore, returns bytes at runtime. + return await response["Body"].read() # type: ignore[no-any-return] + + +def new_aioboto3_client(client: S3Client) -> S3StorageDriverClient: + """Create an :class:`S3StorageDriverClient` from an aioboto3 S3 client. + + Args: + client: An aioboto3 S3 client, typically obtained from + ``aioboto3.Session().client("s3")``. + + .. warning:: + This API is experimental. + """ + return _Aioboto3StorageDriverClient(client) diff --git a/temporalio/contrib/deepagents/README.md b/temporalio/contrib/deepagents/README.md new file mode 100644 index 000000000..da9092978 --- /dev/null +++ b/temporalio/contrib/deepagents/README.md @@ -0,0 +1,339 @@ +# DeepAgentsPlugin — Temporal plugin for LangChain Deep Agents + +Make a [Deep Agent](https://github.com/langchain-ai/deepagents) durable by adding +one plugin. Build your agent with `create_temporal_deep_agent(...)` (or vanilla +`create_deep_agent(...)`) inside a `@workflow.defn`, add +`plugins=[DeepAgentsPlugin(...)]` to your Client (or Worker), and each LLM call +and each I/O tool call becomes a Temporal Activity — while the agent's control +loop runs, and deterministically replays, inside the Workflow. + +The code you already wrote against `deepagents` does not change: sub-agents, +planning/todo state, the filesystem middleware, human-in-the-loop interrupts, and +`agent.ainvoke(...)` all keep working. You get crash-durability, resumable +human-in-the-loop, and bounded history on top. + +> This package is experimental and may change in future versions. + +## Install + +```bash +uv add "temporalio[deepagents]" +``` + +(or `pip install "temporalio[deepagents]"`). Requires Python ≥ 3.11 (the same +floor `deepagents` sets). + +## Hello world + +```python +import asyncio +from datetime import timedelta + +from deepagents import create_deep_agent # no import guard needed; see below +from temporalio import workflow +from temporalio.client import Client +from temporalio.contrib.deepagents import ( + DeepAgentsPlugin, + create_temporal_deep_agent, +) +from temporalio.worker import Worker + + +@workflow.defn +class ResearchAgent: + @workflow.run + async def run(self, question: str) -> str: + # create_temporal_deep_agent wraps deepagents' create_deep_agent and + # scopes this agent's model-call activity options explicitly. + agent = create_temporal_deep_agent( + model="anthropic:claude-sonnet-4-5", + system_prompt="You are a careful research assistant.", + activity_options={"start_to_close_timeout": timedelta(minutes=5)}, + ) + result = await agent.ainvoke( + {"messages": [{"role": "user", "content": question}]} + ) + return result["messages"][-1].content + + +async def main() -> None: + # API keys live on the worker via the model provider, never in workflow + # inputs or history. The default provider is LangChain's init_chat_model. + plugin = DeepAgentsPlugin() + # Add the plugin on ONE side. The SDK propagates a Client plugin to any + # Worker built from that Client, so the Worker below inherits it. + client = await Client.connect("localhost:7233", plugins=[plugin]) + worker = Worker( + client, + task_queue="deepagents-task-queue", + workflows=[ResearchAgent], + ) + await worker.run() + + +if __name__ == "__main__": + asyncio.run(main()) +``` + +Two things worth noticing: + +- **No `workflow.unsafe.imports_passed_through()` guard.** The plugin + configures the workflow sandbox to pass the `deepagents` / LangChain import + tree through, so workflow files import them like any other module. +- **Vanilla `create_deep_agent(...)` also works.** The plugin substitutes the + durable model automatically whenever `model=` is a name string; use + `create_temporal_deep_agent` when you want to scope `activity_options` to one + agent instead of configuring plugin-wide defaults. + +## What this plugin gives you + +- **Drop-in durability.** `create_deep_agent(...).ainvoke(...)` runs unchanged + inside a Workflow. The loop replays deterministically; every nondeterministic + step (LLM, I/O tool, real filesystem/shell op) is an Activity. +- **One LLM call per Activity.** The Workflow ships only the model *name*; the + worker's `model_provider` builds the real client. Temporal owns retries and + timeouts (LLM-SDK retries are disabled). +- **Sub-agents inherit durability.** Because sub-agents inherit the parent's + `model` object and tools, substituting them once propagates to the whole agent + tree — no per-sub-agent wiring. +- Tools, real-I/O backends, human-in-the-loop, streaming, and continue-as-new + each get a section below. + +## Configuring activity options + +Per agent (recommended): `create_temporal_deep_agent(..., activity_options=...)` +as in Hello world above. Plugin-wide defaults use two keyed maps, because model +calls and tool calls have different timeout profiles: + +```python +from datetime import timedelta + +from temporalio.contrib.deepagents import DeepAgentsPlugin + +plugin = DeepAgentsPlugin( + # A single config, or a map keyed by MODEL name (thinking-mode models get + # longer timeouts than fast ones). + model_activity_options={"start_to_close_timeout": timedelta(minutes=5)}, + # A single config, or a map keyed by TOOL name. + tool_activity_options={"start_to_close_timeout": timedelta(seconds=30)}, +) +``` + +## Tools: the Workflow-vs-Activity choice, made explicit + +A tool that only mutates agent state can run in-workflow; a tool that does real +I/O must not. Both directions are one call: + +```python +from datetime import timedelta + +from langchain_core.tools import tool +from temporalio import activity +from temporalio.contrib.deepagents import activity_as_tool, tool_as_activity + + +@activity.defn +async def get_weather(city: str) -> str: + """Return the current weather for a city.""" + return f"It is sunny and 22C in {city}." + + +@tool +def web_search(query: str) -> str: + """Search the web for a query.""" + return f"Top result for {query!r}: ..." + + +# An existing Temporal activity, exposed to the agent as a tool: +weather_tool = activity_as_tool( + get_weather, start_to_close_timeout=timedelta(seconds=30) +) +# A LangChain tool whose body does I/O, moved into an activity: +search_tool = tool_as_activity( + web_search, start_to_close_timeout=timedelta(seconds=30) +) +``` + +Pass both to `create_temporal_deep_agent(..., tools=[weather_tool, +search_tool])`. An unwrapped, non-builtin tool runs in-workflow and the plugin +warns at construction, so the choice is never silent. Deep Agents' pure +built-ins (`write_todos`, state-backed file tools) stay in-workflow by design. + +## Durable file and shell backends + +Wrap a real-I/O backend (`FilesystemBackend` / `LocalShellBackend` / +`StoreBackend`) in `TemporalBackend` and the agent's *built-in* file and shell +tools execute as durable `deepagents.backend_op` Activities instead of touching +disk from workflow code: + +```python +from datetime import timedelta + +from deepagents.backends import FilesystemBackend +from temporalio import workflow +from temporalio.contrib.deepagents import TemporalBackend, create_temporal_deep_agent + + +@workflow.defn +class FilesystemAgent: + @workflow.run + async def run(self, root_dir: str) -> str: + backend = TemporalBackend( + FilesystemBackend(root_dir=root_dir, virtual_mode=True), + activity_options={"start_to_close_timeout": timedelta(seconds=30)}, + ) + agent = create_temporal_deep_agent( + model="anthropic:claude-sonnet-4-5", + backend=backend, + ) + result = await agent.ainvoke( + {"messages": [{"role": "user", "content": "Take notes as you work."}]} + ) + return result["messages"][-1].content +``` + +State-only backends (the default) need no wrapping — they are pure workflow +state, replayed deterministically. + +## Human-in-the-loop + +With `interrupt_on=...`, the agent pauses before a guarded tool and +`ainvoke(...)` returns the pending approval under the SDK-native +`__interrupt__` key — directly in your workflow. Expose it via a Query and +resume with an Update; no shim exception, the native LangGraph resume protocol +is used as-is: + +```python +from langgraph.checkpoint.memory import InMemorySaver +from langgraph.types import Command +from temporalio import workflow +from temporalio.contrib.deepagents import create_temporal_deep_agent + + +@workflow.defn +class ApprovalAgent: + def __init__(self) -> None: + self._pending: str | None = None + self._decision: str | None = None + + @workflow.run + async def run(self, request: str) -> str: + agent = create_temporal_deep_agent( + model="anthropic:claude-sonnet-4-5", + interrupt_on={"book_trip": True}, + checkpointer=InMemorySaver(), + ) + config = {"configurable": {"thread_id": workflow.info().workflow_id}} + result = await agent.ainvoke( + {"messages": [{"role": "user", "content": request}]}, config=config + ) + if result.get("__interrupt__"): + self._pending = str(result["__interrupt__"][0].value) + await workflow.wait_condition(lambda: self._decision is not None) + result = await agent.ainvoke( + Command(resume={"decisions": [{"type": self._decision}]}), + config=config, + ) + return result["messages"][-1].content + + @workflow.query + def pending_approval(self) -> str | None: + return self._pending + + @workflow.update + async def resume(self, decision: str) -> None: + self._decision = decision +``` + +A client polls `pending_approval`, shows it to a person, and calls the +`resume` update with `"approve"` / `"reject"`. + +## Streaming + +Set `streaming_topic=` on the plugin and model dispatch switches to a streaming +Activity that publishes chunk batches to a +`temporalio.contrib.workflow_streams` topic for live subscribers — while the +aggregated final message still returns to the workflow, so the durable result +is identical to the non-streaming path: + +```python +from temporalio.contrib.deepagents import DeepAgentsPlugin + +plugin = DeepAgentsPlugin(streaming_topic="agent-stream") +``` + +Subscribers read the topic with `WorkflowStreamClient`; each item is an +`AIMessageChunk` in `langchain_core.load.dumpd` form. + +## Continue-as-new: what carries and what does not + +Long conversations bloat workflow history. `run_deep_agent(agent, input, +state_snapshot=...)` snapshots state and continues into a fresh run when the +turn ends with pending todos and the server recommends continuing +(`workflow.info().is_continue_as_new_suggested()`, the default and recommended +mode — it accounts for history length *and* size): + +```python +from deepagents import create_deep_agent +from temporalio import workflow +from temporalio.contrib.deepagents import run_deep_agent + + +@workflow.defn +class LongResearchAgent: + @workflow.run + async def run(self, input: dict, state_snapshot: dict | None = None) -> dict: + agent = create_deep_agent(model="anthropic:claude-sonnet-4-5") + return await run_deep_agent( + agent, + input, + state_snapshot=state_snapshot, + ) +``` + +Pass `continue_as_new_after=N` instead to trigger on a fixed history-event +count. + +- **Carries forward:** the accumulated messages and the model/tool result cache + (so an LLM/tool call completed before the continue-as-new is *not* re-run + after it). Your `@workflow.run` must accept `state_snapshot=None` as shown. +- **Does not carry forward:** anything held only in an in-memory checkpointer's + own structures beyond the messages/todos snapshot. The default in-workflow + `InMemorySaver` is rehydrated for free by deterministic replay; a durable + checkpointer that does its own I/O is not replay-safe from inside a workflow, + and the plugin warns if you pass one — prefer the snapshot + continue-as-new + path above. + +## Runtime behavior + +While a worker built with this plugin is running, the plugin wraps +`deepagents.create_deep_agent` so a bare `model="provider:name"` string is +auto-routed through an Activity. The wrapper only rewrites arguments when called +*inside a workflow*, so importing `deepagents` on a plain client or activity +worker is unaffected, and the original function is restored when the worker +stops. If you would rather be explicit, use `create_temporal_deep_agent` or +pass `TemporalModel("provider:name")` yourself. + +## Composing with other plugins + +This plugin carries no tracing context of its own. For observability, compose it +with `temporalio.contrib.langsmith` or `temporalio.contrib.opentelemetry` — +registration order does not matter: + +```python +from temporalio.client import Client +from temporalio.contrib.deepagents import DeepAgentsPlugin + + +async def connect(): + return await Client.connect( + "localhost:7233", + plugins=[ + # LangSmithPlugin(), # or OpenTelemetryPlugin(), in either order + DeepAgentsPlugin(), + ], + ) +``` + +For agents built directly as LangGraph graphs (rather than a compiled Deep +Agent), see `temporalio.contrib.langgraph`. diff --git a/temporalio/contrib/deepagents/__init__.py b/temporalio/contrib/deepagents/__init__.py new file mode 100644 index 000000000..26febecb3 --- /dev/null +++ b/temporalio/contrib/deepagents/__init__.py @@ -0,0 +1,68 @@ +"""Temporal plugin for LangChain Deep Agents. + +Make an existing Deep Agent durable by adding one plugin: build your agent with +``create_deep_agent(...)`` inside a ``@workflow.defn`` and add +``plugins=[DeepAgentsPlugin(...)]`` to your Client or Worker. Each LLM call and +each I/O tool call becomes a Temporal activity, while the agent's control loop +runs — and deterministically replays — inside the workflow. + +.. warning:: + This package is experimental and may change in future versions. + +The public names are imported lazily so ``import temporalio.contrib.deepagents`` +succeeds before LangChain is installed; touching a name that needs LangChain +imports it on first access. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +__all__ = [ + "DeepAgentsPlugin", + "TemporalModel", + "TemporalBackend", + "activity_as_tool", + "tool_as_activity", + "run_deep_agent", + "create_temporal_deep_agent", + "DeepAgentsWorkflowError", +] + +if TYPE_CHECKING: + from temporalio.contrib.deepagents._model import TemporalModel + from temporalio.contrib.deepagents._plugin import DeepAgentsPlugin + from temporalio.contrib.deepagents._tools import ( + TemporalBackend, + activity_as_tool, + tool_as_activity, + ) + from temporalio.contrib.deepagents.workflow import ( + DeepAgentsWorkflowError, + create_temporal_deep_agent, + run_deep_agent, + ) + + +def __getattr__(name: str) -> object: + if name == "DeepAgentsPlugin": + from temporalio.contrib.deepagents._plugin import DeepAgentsPlugin + + return DeepAgentsPlugin + if name == "TemporalModel": + from temporalio.contrib.deepagents._model import TemporalModel + + return TemporalModel + if name in ("TemporalBackend", "activity_as_tool", "tool_as_activity"): + from temporalio.contrib.deepagents import _tools + + return getattr(_tools, name) + if name in ( + "DeepAgentsWorkflowError", + "run_deep_agent", + "create_temporal_deep_agent", + ): + from temporalio.contrib.deepagents import workflow + + return getattr(workflow, name) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/temporalio/contrib/deepagents/_activity.py b/temporalio/contrib/deepagents/_activity.py new file mode 100644 index 000000000..3c1501a42 --- /dev/null +++ b/temporalio/contrib/deepagents/_activity.py @@ -0,0 +1,363 @@ +"""The activities that carry every nondeterministic Deep Agents operation. + +The Deep Agents control loop runs *inside* the workflow; the operations that +must not run there — talking to an LLM, executing a tool that does real I/O, or +touching a real filesystem / shell backend — are moved out to these activities. + +Each activity is a method on :class:`DeepAgentActivities` so the worker-only +dependencies (the ``model_provider`` that builds real chat models from a name, +the streaming batch interval) can be captured on the instance rather than +smuggled through activity inputs. API keys therefore live on the worker, never +in a workflow input or in history. + +Every method: + +* takes a single serializable dataclass in and returns a single dataclass out + (LangChain objects travel as their ``dumpd`` JSON form via + :mod:`temporalio.contrib.deepagents._serde`); +* translates the LLM SDK's HTTP error into Temporal's retry contract so a 429 + honors the upstream ``retry-after`` instead of hammering it; +* heartbeats on a background task so a slow (thinking-mode / long-context) call + is not mistaken for a stuck worker. +""" + +from __future__ import annotations + +import asyncio +import dataclasses +import importlib +from datetime import timedelta +from functools import wraps +from typing import Any, Callable + +from temporalio import activity +from temporalio.contrib.deepagents import _serde +from temporalio.exceptions import ApplicationError + +# Activity type names. The workflow dispatches by these strings, so the +# in-workflow model / tool stubs never import the activity class itself. +INVOKE_MODEL = "deepagents.invoke_model" +INVOKE_MODEL_STREAMING = "deepagents.invoke_model_streaming" +INVOKE_TOOL = "deepagents.invoke_tool" +BACKEND_OP = "deepagents.backend_op" + + +# --------------------------------------------------------------------------- +# Boundary payloads +# --------------------------------------------------------------------------- + + +@dataclasses.dataclass +class ModelActivityInput: + """A single LLM request. + + ``model_name`` is resolved to a real model by the worker's + ``model_provider``; the workflow never ships credentials. + """ + + model_name: str + messages: list[Any] + """Messages in ``langchain_core.load.dumpd`` form.""" + tool_schemas: list[dict[str, Any]] = dataclasses.field(default_factory=list) + """OpenAI-format tool advertisements (name + description + argument schema).""" + bind_kwargs: dict[str, Any] = dataclasses.field(default_factory=dict) + config: dict[str, Any] = dataclasses.field(default_factory=dict) + """A stripped ``RunnableConfig`` (see :func:`_serde.strip_runnable_config`).""" + streaming_topic: str | None = None + + +@dataclasses.dataclass +class ModelActivityOutput: + """The model's reply. + + ``message`` is an ``AIMessage`` in ``dumpd`` form, carrying tool calls, + usage, and response metadata. + """ + + message: Any + + +@dataclasses.dataclass +class ToolActivityInput: + """One tool execution routed to an activity.""" + + tool_name: str + tool_call_id: str + args: dict[str, Any] + config: dict[str, Any] = dataclasses.field(default_factory=dict) + + +@dataclasses.dataclass +class ToolActivityOutput: + """A tool result as a ``ToolMessage`` in ``dumpd`` form.""" + + message: Any + + +@dataclasses.dataclass +class BackendOpInput: + """A single filesystem / shell / store operation for a wrapped backend.""" + + backend_ref: str + """Key identifying which registered backend to act on.""" + op: str + """Backend method name, e.g. ``ls`` / ``read_file`` / ``write_file`` / ``execute``.""" + args: list[Any] = dataclasses.field(default_factory=list) + kwargs: dict[str, Any] = dataclasses.field(default_factory=dict) + + +@dataclasses.dataclass +class BackendOpOutput: + """The backend operation's return value. + + Deepagents protocol dataclasses (``WriteResult`` / ``ReadResult`` / …) + ride in the tagged form produced by ``_serde.dump_backend_result`` so the + in-workflow stub can rebuild the real type; plain JSON values pass + through unchanged. + """ + + result: Any + + +# --------------------------------------------------------------------------- +# Heartbeating + error translation +# --------------------------------------------------------------------------- + + +def _auto_heartbeater(fn: Callable) -> Callable: + """Heartbeat at half the configured ``heartbeat_timeout`` while ``fn`` runs. + + Long LLM calls (thinking mode, long context, streaming accumulation) can run + well past a scheduler's patience; without a heartbeat Temporal would cancel + them and surface a ``HeartbeatTimeoutError`` instead of the real problem. + """ + + @wraps(fn) + async def wrapped(*args: Any, **kwargs: Any) -> Any: + heartbeat_timeout = activity.info().heartbeat_timeout + beat_task: asyncio.Task | None = None + if heartbeat_timeout: + interval = heartbeat_timeout.total_seconds() / 2 + + async def beat() -> None: + while True: + activity.heartbeat() + await asyncio.sleep(interval) + + beat_task = asyncio.create_task(beat()) + try: + return await fn(*args, **kwargs) + finally: + if beat_task is not None: + beat_task.cancel() + # Let the cancellation land before returning so no pending task + # outlives the activity (a bare ``cancel()`` leaves the task to + # be destroyed while pending if the loop shuts down first). + # ``asyncio.wait`` never re-raises the task's CancelledError. + await asyncio.wait([beat_task]) + + return wrapped + + +def _translate_api_error(exc: Exception) -> ApplicationError | None: + """Map an LLM SDK HTTP error onto Temporal's retry contract. + + Works by duck typing so neither ``openai`` nor ``anthropic`` needs to be + imported here: both expose ``status_code`` and ``response.headers``. Returns + ``None`` when ``exc`` is not a recognizable HTTP status error, so the caller + can fall through to its generic handling. + """ + status = getattr(exc, "status_code", None) + if status is None: + return None + headers: dict[str, Any] = {} + response = getattr(exc, "response", None) + if response is not None: + headers = dict(getattr(response, "headers", {}) or {}) + # Case-insensitive header access. + lower = {str(k).lower(): v for k, v in headers.items()} + + retryable = status in (408, 409, 429) or 500 <= status < 600 + should_retry = lower.get("x-should-retry") + if should_retry == "false": + retryable = False + elif should_retry == "true": + retryable = True + + delay_ms = lower.get("retry-after-ms") + retry_after = lower.get("retry-after") + next_delay: timedelta | None = None + try: + if delay_ms is not None: + next_delay = timedelta(milliseconds=int(delay_ms)) + elif retry_after is not None: + next_delay = timedelta(seconds=int(retry_after)) + except (TypeError, ValueError): + next_delay = None + + return ApplicationError( + str(exc), + type=type(exc).__name__, + non_retryable=not retryable, + next_retry_delay=next_delay, + ) + + +# --------------------------------------------------------------------------- +# The activities +# --------------------------------------------------------------------------- + + +def _default_model_provider(model_name: str) -> Any: + """Build a chat model from a name string with LLM-SDK retries disabled. + + Temporal owns retries; the model client must not also retry, or a single + logical attempt fans out into nested retry storms that Temporal can neither + see nor bound. + """ + # importlib: `langchain` (unlike langchain-core) is absent on Python 3.10 + # environments where the deepagents extra cannot install; a static import + # here fails type-checking there. + init_chat_model = importlib.import_module("langchain.chat_models").init_chat_model + + return init_chat_model(model_name, max_retries=0) + + +class DeepAgentActivities: + """Holds the worker-side dependencies and exposes the four activities. + + An instance is created by ``DeepAgentsPlugin`` + and its bound methods are registered on the worker. + """ + + def __init__( + self, + *, + model_provider: Callable[[str], Any] | None = None, + streaming_batch_interval: timedelta = timedelta(milliseconds=100), + ) -> None: + """Store the worker-side model provider + streaming configuration.""" + self._model_provider = model_provider or _default_model_provider + self._streaming_batch_interval = streaming_batch_interval + + def _build_bound_model(self, input: ModelActivityInput) -> Any: + model = self._model_provider(input.model_name) + if input.bind_kwargs: + model = model.bind(**input.bind_kwargs) + if input.tool_schemas: + model = model.bind_tools(input.tool_schemas) + return model + + @activity.defn(name=INVOKE_MODEL) + @_auto_heartbeater + async def invoke_model(self, input: ModelActivityInput) -> ModelActivityOutput: + """Run exactly one LLM call and return the resulting ``AIMessage``.""" + messages = _serde.load_messages(input.messages) + config = _serde.rebuild_runnable_config(input.config) + model = self._build_bound_model(input) + try: + message = await model.ainvoke(messages, config=config) + except Exception as exc: + translated = _translate_api_error(exc) + if translated is not None: + activity.logger.warning( + "Model call failed with an HTTP status error", exc_info=True + ) + raise translated from exc + raise + return ModelActivityOutput(message=_serde.dump_object(message)) + + @activity.defn(name=INVOKE_MODEL_STREAMING) + @_auto_heartbeater + async def invoke_model_streaming( + self, input: ModelActivityInput + ) -> ModelActivityOutput: + """Stream one LLM call, publishing chunk batches to ``streaming_topic``. + + Token-level deltas are coalesced at ``streaming_batch_interval`` and + pushed to external subscribers via the shared workflow-streams topic; the + aggregated final ``AIMessage`` is returned to the workflow so the + durable result is identical to the non-streaming path. + """ + from temporalio.contrib.workflow_streams import WorkflowStreamClient + + messages = _serde.load_messages(input.messages) + config = _serde.rebuild_runnable_config(input.config) + model = self._build_bound_model(input) + + final: Any = None + try: + async with WorkflowStreamClient.from_within_activity( + batch_interval=self._streaming_batch_interval + ) as client: + topic = ( + client.topic(input.streaming_topic) + if input.streaming_topic + else None + ) + async for chunk in model.astream(messages, config=config): + if topic is not None: + topic.publish(_serde.dump_object(chunk)) + final = chunk if final is None else final + chunk + except Exception as exc: + translated = _translate_api_error(exc) + if translated is not None: + activity.logger.warning( + "Streaming model call failed with an HTTP status error", + exc_info=True, + ) + raise translated from exc + raise + return ModelActivityOutput(message=_serde.dump_object(final)) + + @activity.defn(name=INVOKE_TOOL) + @_auto_heartbeater + async def invoke_tool(self, input: ToolActivityInput) -> ToolActivityOutput: + """Execute one registered tool and return its ``ToolMessage``.""" + from temporalio.contrib.deepagents._tools import get_registered_tool + + tool = get_registered_tool(input.tool_name) + if tool is None: + raise ApplicationError( + f"Tool {input.tool_name!r} is not registered on this worker. " + f"Wrap it with tool_as_activity(...) or activity_as_tool(...).", + type="DeepAgentsUnknownTool", + non_retryable=True, + ) + config = _serde.rebuild_runnable_config(input.config) + tool_call = { + "name": input.tool_name, + "args": input.args, + "id": input.tool_call_id, + "type": "tool_call", + } + message = await tool.ainvoke(tool_call, config=config) + return ToolActivityOutput(message=_serde.dump_object(message)) + + @activity.defn(name=BACKEND_OP) + @_auto_heartbeater + async def backend_op(self, input: BackendOpInput) -> BackendOpOutput: + """Run one operation against a registered (real-I/O) backend.""" + from temporalio.contrib.deepagents._tools import registered_backends + + backend = registered_backends().get(input.backend_ref) + if backend is None: + raise ApplicationError( + f"Backend {input.backend_ref!r} is not registered on this worker.", + type="DeepAgentsUnknownBackend", + non_retryable=True, + ) + method = getattr(backend, input.op, None) + if method is None: + raise ApplicationError( + f"Backend {input.backend_ref!r} has no operation {input.op!r}.", + type="DeepAgentsUnknownBackendOp", + non_retryable=True, + ) + result = method(*input.args, **input.kwargs) + if asyncio.iscoroutine(result): + result = await result + # Protocol results are plain dataclasses whose attributes the + # middleware reads in-workflow — tag them so the stub can rebuild + # the real type instead of receiving a decayed dict. + return BackendOpOutput(result=_serde.dump_backend_result(result)) diff --git a/temporalio/contrib/deepagents/_model.py b/temporalio/contrib/deepagents/_model.py new file mode 100644 index 000000000..2d97f992d --- /dev/null +++ b/temporalio/contrib/deepagents/_model.py @@ -0,0 +1,372 @@ +"""The model seam: a chat model whose every call becomes a Temporal activity. + +:class:`TemporalModel` is a real ``BaseChatModel``. Placed anywhere Deep Agents +expects a model, it routes each generation through the ``deepagents.invoke_model`` +activity instead of calling the provider inline. Because Deep Agents' sub-agents +inherit the parent's ``model`` instance by default, substituting this one object +makes every model call in the whole agent tree durable — no middleware injection +that a sub-agent could silently miss. + +Two ways to get one: + +* explicit — the user writes ``TemporalModel("anthropic:claude-...")`` and hands + it to ``create_deep_agent(model=...)``; this is the public type users assert + against; +* implicit — while running inside a workflow, the plugin patches + ``create_deep_agent`` so a bare ``model="anthropic:..."`` string is wrapped + automatically (see :func:`install_model_patch`). + +Worker-wide dispatch defaults (activity options, streaming topic) live in +:class:`temporalio.contrib.deepagents._serde.Settings`, a module global set by +the plugin. They live in ``_serde`` (not here) so the plugin can configure them +without importing this module — which would drag LangChain into plugin +construction. This module is under ``temporalio``, which the workflow sandbox +passes through, so the object the workflow reads is the same one the plugin set. +""" + +from __future__ import annotations + +import importlib +from collections.abc import AsyncIterator, Iterator, Mapping, Sequence +from datetime import timedelta +from typing import Any + +from temporalio import workflow +from temporalio.contrib.deepagents import _activity, _serde + +with workflow.unsafe.imports_passed_through(): + from langchain_core.callbacks import ( + AsyncCallbackManagerForLLMRun, + CallbackManagerForLLMRun, + ) + from langchain_core.language_models import BaseChatModel + from langchain_core.messages import AIMessageChunk, BaseMessage + from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult + + +def _as_message_chunk(message: Any) -> Any: + """Re-shape a finished ``AIMessage`` as the ``AIMessageChunk`` a stream yields.""" + return AIMessageChunk( + content=getattr(message, "content", ""), + additional_kwargs=getattr(message, "additional_kwargs", {}) or {}, + response_metadata=getattr(message, "response_metadata", {}) or {}, + id=getattr(message, "id", None), + tool_calls=getattr(message, "tool_calls", []) or [], + usage_metadata=getattr(message, "usage_metadata", None), + ) + + +# --------------------------------------------------------------------------- +# Activity-option resolution (worker-wide defaults live in _serde.Settings) +# --------------------------------------------------------------------------- + + +_DEFAULT_MODEL_TIMEOUT = timedelta(minutes=5) + + +def _resolve_activity_options( + model_name: str, instance_options: Mapping[str, Any] | None +) -> dict[str, Any]: + """Merge worker defaults with a per-instance override into execute_activity kwargs.""" + opts: dict[str, Any] = {} + default = _serde.get_settings().model_activity_options + if isinstance(default, Mapping) and not _looks_like_activity_config(default): + # Mapping[model_name, ActivityConfig]: pick this model's entry. + chosen = default.get(model_name) + if isinstance(chosen, Mapping): + opts.update(chosen) + elif isinstance(default, Mapping): + opts.update(default) + if instance_options: + opts.update(instance_options) + opts.setdefault("start_to_close_timeout", _DEFAULT_MODEL_TIMEOUT) + return opts + + +def _looks_like_activity_config(m: Mapping[str, Any]) -> bool: + """A single ``ActivityConfig`` has known option keys; a per-model map does not.""" + known = { + "task_queue", + "schedule_to_close_timeout", + "schedule_to_start_timeout", + "start_to_close_timeout", + "heartbeat_timeout", + "retry_policy", + "cancellation_type", + "activity_id", + "versioning_intent", + "summary", + "priority", + } + return bool(m) and all(k in known for k in m) + + +# --------------------------------------------------------------------------- +# The model +# --------------------------------------------------------------------------- + + +class TemporalModel(BaseChatModel): + """A ``BaseChatModel`` that runs each generation as a Temporal activity. + + Args: + model: The provider model name resolved worker-side by the plugin's + ``model_provider`` (e.g. ``"anthropic:claude-sonnet-4-5"``). Only the + name crosses the workflow boundary; credentials stay on the worker. + activity_options: Optional per-model ``execute_activity`` overrides + (timeouts, retry policy). Falls back to the plugin's + ``model_activity_options``. + """ + + model: str + activity_options: dict[str, Any] | None = None + + # ``protected_namespaces=()`` silences pydantic's warning about the ``model`` + # field colliding with its ``model_`` namespace. + model_config = {"arbitrary_types_allowed": True, "protected_namespaces": ()} + + @property + def _llm_type(self) -> str: + return "temporal-deepagents" + + def bind_tools( + self, + tools: Sequence[Any], + *, + tool_choice: Any | None = None, + **kwargs: Any, + ) -> Any: + """Bind tools to the model the way LangChain's ``create_agent`` expects. + + ``BaseChatModel.bind_tools`` is abstract (raises ``NotImplementedError``), + but the agent factory calls ``model.bind_tools(tools, ...)`` on every model + node — so a durable model must implement it or the whole loop dies. The + tools are converted to their JSON schema *now* (at bind time) so the bound + object is serialization-safe, and carried as the ``tools`` kwarg that + :meth:`_build_input` already reads and forwards to the activity, where the + real provider model is what actually binds them. + """ + schemas = [ + t if isinstance(t, dict) else _serde.tool_to_schema(t) for t in tools + ] + if tool_choice is not None: + kwargs["tool_choice"] = tool_choice + return self.bind(tools=schemas, **kwargs) + + def _summary(self) -> str: + return f"invoke_model[{self.model}]" + + def _build_input( + self, + messages: Sequence[BaseMessage], + streaming_topic: str | None, + **kwargs: Any, + ) -> _activity.ModelActivityInput: + tools = kwargs.get("tools") or [] + tool_schemas = [ + t if isinstance(t, dict) else _serde.tool_to_schema(t) for t in tools + ] + bind_kwargs = { + k: v + for k, v in kwargs.items() + if k not in ("tools", "config", "run_manager", "stop", "callbacks") + and _serde._is_jsonish(v) + } + if kwargs.get("stop"): + bind_kwargs["stop"] = kwargs["stop"] + return _activity.ModelActivityInput( + model_name=self.model, + messages=_serde.dump_messages(messages), + tool_schemas=tool_schemas, + bind_kwargs=bind_kwargs, + config=_serde.strip_runnable_config(kwargs.get("config") or {}), + streaming_topic=streaming_topic, + ) + + def _generate( + self, + messages: list[BaseMessage], + stop: list[str] | None = None, + run_manager: CallbackManagerForLLMRun | None = None, + **kwargs: Any, + ) -> ChatResult: + raise NotImplementedError( + "TemporalModel is async-only inside a Temporal workflow. Drive the " + "agent with `await agent.ainvoke(...)` / `.astream(...)`, which uses " + "the async model path." + ) + + async def _agenerate( + self, + messages: list[BaseMessage], + stop: list[str] | None = None, + run_manager: AsyncCallbackManagerForLLMRun | None = None, + **kwargs: Any, + ) -> ChatResult: + from temporalio.contrib.deepagents.workflow import call_model + + activity_input = self._build_input(messages, None, stop=stop, **kwargs) + opts = _resolve_activity_options(self.model, self.activity_options) + output = await call_model( + _activity.INVOKE_MODEL, + activity_input, + summary=self._summary(), + **opts, + ) + message = _serde.load_object(output.message) + return ChatResult(generations=[ChatGeneration(message=message)]) + + async def _astream( + self, + messages: list[BaseMessage], + stop: list[str] | None = None, + run_manager: AsyncCallbackManagerForLLMRun | None = None, + **kwargs: Any, + ) -> AsyncIterator[ChatGenerationChunk]: + from temporalio.contrib.deepagents.workflow import call_model + + topic = _serde.get_settings().streaming_topic + opts = _resolve_activity_options(self.model, self.activity_options) + if topic: + activity_input = self._build_input(messages, topic, stop=stop, **kwargs) + output = await call_model( + _activity.INVOKE_MODEL_STREAMING, + activity_input, + summary=f"invoke_model_streaming[{self.model}]", + **opts, + ) + else: + # No topic configured: fall back to a single response-level chunk. + activity_input = self._build_input(messages, None, stop=stop, **kwargs) + output = await call_model( + _activity.INVOKE_MODEL, + activity_input, + summary=self._summary(), + **opts, + ) + message = _serde.load_object(output.message) + yield ChatGenerationChunk(message=_as_message_chunk(message)) + + def _stream( + self, + messages: list[BaseMessage], + stop: list[str] | None = None, + run_manager: CallbackManagerForLLMRun | None = None, + **kwargs: Any, + ) -> Iterator[ChatGenerationChunk]: + raise NotImplementedError( + "TemporalModel streaming is async-only; use `agent.astream(...)`." + ) + + +# --------------------------------------------------------------------------- +# create_deep_agent model patch (implicit wrapping) +# --------------------------------------------------------------------------- +# +# The durability seam is ``deepagents._models.resolve_model``, which +# ``create_deep_agent`` calls to turn a ``model=`` string (or instance) into a +# ``BaseChatModel`` — for both the top-level agent and every ``SubAgent`` +# (``graph.py`` lines 592 and 634). We patch it on the ``deepagents.graph`` +# module, where ``create_deep_agent``'s body resolves the ``resolve_model`` name +# at call time. +# +# Patching *this* seam (not ``deepagents.create_deep_agent``) is what makes the +# rewrite survive the user's import style. A user who writes the idiomatic +# ``from deepagents import create_deep_agent`` binds the *original* function +# object into their module; rebinding the ``deepagents.create_deep_agent`` +# attribute would never be seen by that already-bound reference, so string +# models would reach the real provider inside the workflow (a hang / non- +# determinism). ``create_deep_agent``'s body, by contrast, always looks up +# ``resolve_model`` in the ``deepagents.graph`` globals afresh on each call, so +# rebinding it there is observed no matter how the caller imported the factory. +# It also preserves ``_model_spec`` (the original string), which the factory +# reads *before* calling ``resolve_model`` for harness-profile lookup. +# +# ``create_deep_agent`` is still wrapped separately, best-effort, purely to fire +# the construction-time warnings that need the ``tools`` / ``checkpointer`` +# kwargs (those warnings are advisory and carry no durability weight). + +_original_create_deep_agent: Any = None +_original_resolve_model: Any = None + + +def _wrap_model_arg(model: Any) -> Any: + """Resolve a ``create_deep_agent`` model argument to a durable model. + + A name string becomes a :class:`TemporalModel`. A :class:`TemporalModel` + passes through. Any other live ``BaseChatModel`` instance is rejected at the + workflow boundary: it has no name the activity could rebuild from, so it would + run its provider call *inside the workflow* — nondeterministic and unsafe. + """ + from temporalio.contrib.deepagents.workflow import DeepAgentsWorkflowError + + if isinstance(model, str): + return TemporalModel(model=model) + if isinstance(model, TemporalModel): + return model + if isinstance(model, BaseChatModel): + raise DeepAgentsWorkflowError( + f"create_deep_agent received a live {type(model).__name__} model " + f"instance, which would run inside the workflow (nondeterministic). " + f'Pass model="provider:name" (auto-routed through an activity) or ' + f"wrap it as TemporalModel(...) instead." + ) + return model + + +def install_model_patch() -> None: + """Route Deep Agents' model resolution through :class:`TemporalModel`. + + Patches ``deepagents.graph.resolve_model`` (the seam ``create_deep_agent`` + uses for the main agent *and* every sub-agent) so a bare ``model="..."`` + string becomes a durable :class:`TemporalModel`, and additionally wraps + ``deepagents.create_deep_agent`` to fire the advisory tool / checkpointer + warnings. Both only act when called inside a workflow, so importing + deepagents on a plain client / activity worker is unaffected. Idempotent. + """ + global _original_create_deep_agent, _original_resolve_model + # importlib: `deepagents` is absent on Python 3.10 environments (its floor + # is 3.11), so static imports here fail type-checking there. + deepagents = importlib.import_module("deepagents") + _graph = importlib.import_module("deepagents.graph") + + if _original_resolve_model is None: + _original_resolve_model = _graph.resolve_model + + def patched_resolve_model(model: Any) -> Any: + if workflow.in_workflow(): + return _wrap_model_arg(model) + return _original_resolve_model(model) + + setattr(_graph, "resolve_model", patched_resolve_model) + + if _original_create_deep_agent is None: + _original_create_deep_agent = deepagents.create_deep_agent + + def patched(*args: Any, **kwargs: Any) -> Any: + if workflow.in_workflow(): + from temporalio.contrib.deepagents._tools import warn_unwrapped_tools + from temporalio.contrib.deepagents.workflow import ( + warn_durable_checkpointer, + ) + + warn_unwrapped_tools(kwargs.get("tools")) + warn_durable_checkpointer(kwargs.get("checkpointer")) + return _original_create_deep_agent(*args, **kwargs) + + setattr(deepagents, "create_deep_agent", patched) + + +def uninstall_model_patch() -> None: + """Restore the original ``resolve_model`` / ``create_deep_agent``.""" + global _original_create_deep_agent, _original_resolve_model + if _original_resolve_model is not None: + _graph = importlib.import_module("deepagents.graph") + + setattr(_graph, "resolve_model", _original_resolve_model) + _original_resolve_model = None + if _original_create_deep_agent is not None: + deepagents = importlib.import_module("deepagents") + + setattr(deepagents, "create_deep_agent", _original_create_deep_agent) + _original_create_deep_agent = None diff --git a/temporalio/contrib/deepagents/_plugin.py b/temporalio/contrib/deepagents/_plugin.py new file mode 100644 index 000000000..3b95fcdaf --- /dev/null +++ b/temporalio/contrib/deepagents/_plugin.py @@ -0,0 +1,296 @@ +"""The plugin object users add to ``plugins=[...]``. + +:class:`DeepAgentsPlugin` wires everything together so that existing +``deepagents`` code — ``create_deep_agent(...).ainvoke(...)`` — runs durably +inside a ``@workflow.defn`` with no other changes: + +* registers the four activities that carry the nondeterministic work; +* installs the LangChain-aware data converter (composing, never clobbering, a + user converter); +* passes the LangChain / LangGraph / deepagents import tree through the workflow + sandbox; +* wraps the worker run in a ``run_context`` that patches Deep Agents' model + resolution seam (``deepagents.graph.resolve_model``) to auto-route bare + ``model=`` strings through activities — regardless of how the user imported + ``create_deep_agent``; +* registers :class:`DeepAgentsWorkflowError` as a workflow-failure type; +* pushes the model / tool dispatch defaults down to the seams that read them. + +The plugin auto-propagates from a ``Client`` to any ``Worker`` built from it, so +add it on exactly one side. ``configure_worker`` additionally de-duplicates +activities by name, so a user who mistakenly adds it on both sides gets a clean +no-op instead of a "More than one activity named ..." crash. +""" + +from __future__ import annotations + +import sys +import warnings +from collections.abc import AsyncIterator, Sequence +from contextlib import asynccontextmanager +from dataclasses import replace +from datetime import timedelta +from typing import Any, Callable + +from temporalio import activity as activity_mod +from temporalio.contrib.deepagents import _serde, _tools +from temporalio.contrib.deepagents._activity import DeepAgentActivities +from temporalio.contrib.deepagents.workflow import DeepAgentsWorkflowError +from temporalio.plugin import SimplePlugin +from temporalio.worker import WorkflowRunner +from temporalio.worker.workflow_sandbox import SandboxedWorkflowRunner + +# Runtime floor for the Deep Agents control loop. Kept in a constant so +# static checkers do not narrow `sys.version_info` comparisons into +# unreachable-code findings on new interpreters. +_MIN_PYTHON = (3, 11) + + +class DeepAgentsPlugin(SimplePlugin): + """Temporal plugin that makes LangChain Deep Agents durable. + + Args: + model_provider: Builds a real chat model from a name string, worker-side. + This is where API keys live; only the model name ever crosses the + workflow boundary. Defaults to LangChain's ``init_chat_model`` with + LLM-SDK retries disabled (Temporal owns retries). + model_activity_options: ``ActivityConfig`` (or ``Mapping[model_name, + ActivityConfig]``) for the model activities — timeouts, retry policy. + tool_activity_options: ``ActivityConfig`` (or ``Mapping[tool_name, + ActivityConfig]``) default for tools wrapped with ``tool_as_activity``. + streaming_topic: When set, model calls stream through the streaming + activity and publish chunk batches to this workflow-streams topic. + streaming_batch_interval: How long the streaming activity coalesces + chunks before publishing a batch. + passthrough_modules: Extra sandbox-passthrough modules, merged with the + plugin's LangChain/deepagents defaults. + data_converter: Override the default LangChain-aware converter. ``None`` + installs the default; the SDK default is upgraded in place; any other + converter raises (fold ``DeepAgentsPayloadConverter`` into your own). + """ + + def __init__( + self, + *, + model_provider: Callable[[str], Any] | None = None, + model_activity_options: Any = None, + tool_activity_options: Any = None, + streaming_topic: str | None = None, + streaming_batch_interval: timedelta = timedelta(milliseconds=100), + passthrough_modules: Sequence[str] | None = None, + data_converter: Any = None, + ) -> None: + """Configure the plugin; see the class docstring for parameters.""" + if sys.version_info < _MIN_PYTHON: + warnings.warn( + "DeepAgentsPlugin requires Python >= 3.11 (deepagents pins " + ">=3.11); the Deep Agents control loop relies on contextvars " + "propagation through asyncio that older versions lack.", + stacklevel=2, + ) + + self._passthrough_modules = passthrough_modules + # Held on the instance so the wiring is statically traceable: each is + # passed straight into the call that consumes it (below), not stashed as + # dead config. + self._tool_activity_options = tool_activity_options + self._data_converter = data_converter + + # Push dispatch defaults down to the model and tool seams that read them. + # These live in ``_serde`` / ``_tools`` (langchain-free modules) so + # constructing the plugin never imports LangChain. ``tool_activity_options`` + # is stored under ``_tools._tool_defaults`` and read back on the tool + # dispatch path by ``_tools._resolve_tool_options(...)`` — which + # ``tool_as_activity`` calls to compute each tool activity's timeout/retry + # when the caller does not override ``activity_options``. + _serde.set_settings( + model_activity_options=model_activity_options, + streaming_topic=streaming_topic, + ) + # wired-via-composition: consumed on the tool dispatch path in _tools.py + # (_resolve_tool_options), renamed to ``options`` at the callsite. + _tools.set_tool_defaults(self._tool_activity_options) + + # The activities that carry the nondeterministic work. model_provider and + # the batch interval are captured here so API keys never enter an input. + self._activities = DeepAgentActivities( + model_provider=model_provider, + streaming_batch_interval=streaming_batch_interval, + ) + activities = [ + self._activities.invoke_model, + self._activities.invoke_model_streaming, + self._activities.invoke_tool, + self._activities.backend_op, + ] + + super().__init__( + "langchain.DeepAgentsPlugin", + activities=activities, + # wired-via-composition: ``data_converter`` flows into SimplePlugin's + # own ``data_converter`` kwarg (renamed to ``user_converter`` inside + # build_data_converter), which installs it on the client/worker. + data_converter=_serde.build_data_converter(self._data_converter), + workflow_runner=self._make_workflow_runner(), + workflow_failure_exception_types=[DeepAgentsWorkflowError], + run_context=self._run_context, + ) + + # -- sandbox passthrough ------------------------------------------------- + + def _make_workflow_runner( + self, + ) -> Callable[[WorkflowRunner | None], WorkflowRunner]: + modules = _serde.resolve_passthrough_modules(self._passthrough_modules) + + def workflow_runner(runner: WorkflowRunner | None) -> WorkflowRunner: + if runner is None: + raise ValueError("No WorkflowRunner provided to DeepAgentsPlugin.") + if isinstance(runner, SandboxedWorkflowRunner): + return replace( + runner, + restrictions=runner.restrictions.with_passthrough_modules(*modules), + ) + return runner + + return workflow_runner + + # -- run context --------------------------------------------------------- + + @asynccontextmanager + async def _run_context(self) -> AsyncIterator[None]: + """Patch Deep Agents' model resolution seam for the worker's lifetime. + + The patch (on ``deepagents.graph.resolve_model``, the seam + ``create_deep_agent`` uses for the agent and every sub-agent) only + rewrites ``model=`` strings when running inside a workflow, so it is inert + on plain clients / activity workers. Determinism itself + rides on the same mechanism ``contrib.langgraph`` uses — the loop is + in-workflow and every nondeterministic call is an activity — so no + speculative time/uuid shims are installed here. + + One shim *is* required: LangChain's ``create_agent`` factory wraps every + model node with LangSmith's ``@traceable``, whose async path hops onto a + thread via ``asyncio.run_in_executor`` — which the deterministic workflow + event loop does not implement (it raises ``NotImplementedError``). Tracing + is an observability concern that must not run in-workflow, so we install + LangSmith's own Temporal escape hatch (``set_runtime_overrides``) to run + that setup inline when ``in_workflow()``, and defer to the default thread + hop everywhere else (activities / clients, where tracing is fine). + """ + # Imported lazily (not at module top) so plugin construction stays + # langchain-free; the patch is only needed once the worker is running. + # The import itself is inside the guard: on a worker without LangChain + # installed (e.g. one that only runs the plugin's determinism / failure + # paths) importing ``_model`` raises ``ModuleNotFoundError``, and the + # worker must still start — it simply runs without the auto-wrap patch. + patched = False + try: + from temporalio.contrib.deepagents import _model, _tools + + _model.install_model_patch() + _tools.install_backend_async_patch() + patched = True + except ImportError as exc: # LangChain / deepagents absent on this worker + # Deliberately ImportError only: a *missing* optional dependency + # must not stop the worker, but a genuine patch-installation bug + # (e.g. upstream renaming the seam raises AttributeError) must + # surface at startup, not degrade into silent in-workflow calls. + warnings.warn( + f"DeepAgentsPlugin could not patch create_deep_agent ({exc}); " + "use explicit TemporalModel(...) instances to route model calls " + "through activities.", + stacklevel=2, + ) + # Installed for the life of the process, never uninstalled — see + # _install_langsmith_temporal_override for why. + _install_langsmith_temporal_override() + try: + yield + finally: + if patched: + # Import is cached: patched=True implies the import above succeeded. + from temporalio.contrib.deepagents import _model, _tools + + _model.uninstall_model_patch() + _tools.uninstall_backend_async_patch() + + # -- worker config ------------------------------------------------------- + + def configure_worker(self, config: Any) -> Any: + """Deduplicate activity registrations after the base configuration.""" + config = super().configure_worker(config) + activities = config.get("activities") + if activities: + config["activities"] = _dedupe_activities(activities) + return config + + +async def _temporal_aio_to_thread( + default_aio_to_thread: Callable[..., Any], + ctx: Any, + func: Callable[..., Any], + /, + *args: Any, + **kwargs: Any, +) -> Any: + """Run LangSmith's ``aio_to_thread`` seam safely inside a workflow. + + Outside a workflow (activities, clients) we defer to LangSmith's default + thread hop. Inside a workflow the deterministic event loop cannot spawn a + thread, so we run the setup inline in the requested context. ``func`` here is + LangSmith's own run-tree bookkeeping — cheap and side-effect-free with respect + to workflow determinism. + """ + from temporalio import workflow + + if not workflow.in_workflow(): + return await default_aio_to_thread(ctx, func, *args, **kwargs) + with workflow.unsafe.sandbox_unrestricted(): + return ctx.run(func, *args, **kwargs) + + +def _install_langsmith_temporal_override() -> None: + """Route LangSmith's thread hop inline while in a workflow. + + ``set_runtime_overrides`` mutates a module global in the sandbox-passthrough + ``langsmith`` package, so the in-workflow tracer sees it too. No-op (and + harmless) when LangSmith is not installed or too old to expose + ``set_runtime_overrides``. + + The override is deliberately installed for the life of the process and + never uninstalled: LangSmith exposes a single process-wide override slot + (each ``set_runtime_overrides`` call replaces it wholesale), and + ``temporalio.contrib.langsmith`` installs a behaviorally identical override + exactly once, without reinstalling. Resetting the slot on this worker's + shutdown would therefore permanently strip a composed LangSmith plugin's + workflow-safety override. Leaving it installed is safe: the override defers + to LangSmith's default thread hop whenever ``workflow.in_workflow()`` is + false, so it is inert outside workflows. + """ + try: + import langsmith + + langsmith.set_runtime_overrides(aio_to_thread=_temporal_aio_to_thread) + except Exception: + pass + + +def _dedupe_activities(activities: Sequence[Any]) -> list[Any]: + """Drop duplicate activity registrations by defn name, keeping the first. + + Guards the "plugin added on both Client and Worker" trap, where the plugin's + activities would otherwise be appended twice and the worker would reject the + duplicate names. + """ + seen: set[str] = set() + out: list[Any] = [] + for act in activities: + defn = activity_mod._Definition.from_callable(act) + name = defn.name if defn is not None else getattr(act, "__name__", repr(act)) + key = name or repr(act) + if key in seen: + continue + seen.add(key) + out.append(act) + return out diff --git a/temporalio/contrib/deepagents/_serde.py b/temporalio/contrib/deepagents/_serde.py new file mode 100644 index 000000000..13ed0d1c6 --- /dev/null +++ b/temporalio/contrib/deepagents/_serde.py @@ -0,0 +1,410 @@ +"""Serialization helpers, a result cache, and worker-runtime configuration. + +The Deep Agents control loop runs *inside* the Temporal workflow, so the values +that actually cross the workflow⇄activity boundary are a small set of LangChain +types: chat messages, tool-call descriptors, and the ``RunnableConfig`` metadata +attached to each model / tool call. LangChain messages are polymorphic +``Serializable`` models (an ``AIMessage`` must not be rehydrated as a +``ToolMessage``) and ``RunnableConfig`` carries live callback / checkpointer +references, so neither survives a naive round-trip. This module owns: + +* message (de)serialization via ``langchain_core.load.dumpd`` / ``load`` — the + round-trip that preserves message subtype and tool-call structure; +* the strip → ship → rebuild dance for ``RunnableConfig``; +* tool → JSON-schema advertisement (full name + description + argument schema, + never ``{name, description}`` alone — without the argument schema the model + picks the right tool but hallucinates its arguments); +* the Pydantic data converter (``exclude_unset=True``) the plugin installs on + the client and replayer, so message payloads stay small and round-trip + cleanly; +* a workflow-scoped result cache so model / tool results computed before a + ``continue_as_new`` are reused rather than recomputed after it; +* the sandbox passthrough module list covering LangChain's transitive + eager-import tree. + +LangChain imports are deferred into the functions that need them, so importing +this module — and constructing the plugin — does not require LangChain to be +installed on the machine assembling the worker. +""" + +from __future__ import annotations + +import contextvars +import dataclasses +import hashlib +import json +from typing import TYPE_CHECKING, Any, cast + +if TYPE_CHECKING: + from langchain_core.runnables import RunnableConfig + +from temporalio.contrib.pydantic import PydanticPayloadConverter, ToJsonOptions +from temporalio.converter import DataConverter + +# --------------------------------------------------------------------------- +# Worker-wide dispatch settings +# --------------------------------------------------------------------------- +# +# These are fixed for the worker's lifetime (not per-workflow state), so a module +# global is correct. This module lives under ``temporalio``, which the workflow +# sandbox passes through, so the object the in-workflow model stub reads is the +# same one the plugin configured. They live here (not in ``_model``) so that +# ``DeepAgentsPlugin`` can push them down without importing ``_model`` — which +# would drag in LangChain at plugin-construction time. + + +@dataclasses.dataclass +class Settings: + """Dispatch defaults shared by every ``TemporalModel`` on the worker.""" + + model_activity_options: Any = None + """``ActivityConfig`` or ``Mapping[model_name, ActivityConfig]``.""" + streaming_topic: str | None = None + + +_settings = Settings() + + +def set_settings( + *, + model_activity_options: Any = None, + streaming_topic: str | None = None, +) -> None: + """Install the worker-wide model dispatch defaults (called by the plugin).""" + _settings.model_activity_options = model_activity_options + _settings.streaming_topic = streaming_topic + + +def get_settings() -> Settings: + """Return the active dispatch settings.""" + return _settings + + +# --------------------------------------------------------------------------- +# Data converter +# --------------------------------------------------------------------------- + + +class DeepAgentsPayloadConverter(PydanticPayloadConverter): + """Pydantic payload converter pinned to ``exclude_unset=True``. + + LangChain request/response types (and ``DeepAgentState``) are deeply nested + with many ``Optional[...] = None`` fields. Shipping every unset default + inflates payloads several-fold and some peers reject the explicit nulls on + round-trip, so we exclude unset fields by convention. + """ + + def __init__(self) -> None: + """Construct the converter with ``exclude_unset`` serialization.""" + super().__init__(ToJsonOptions(exclude_unset=True)) + + +data_converter = DataConverter(payload_converter_class=DeepAgentsPayloadConverter) +"""The plugin's default data converter (LangChain messages are shipped as their +``dumpd`` JSON form, so the Pydantic converter only ever sees plain containers).""" + + +def build_data_converter( + user_converter: DataConverter | None, +) -> DataConverter: + """Compose the plugin's converter with whatever the caller already set. + + * ``None`` — install the plugin default. + * the SDK default converter — swap in the LangChain-aware Pydantic + converter via :func:`dataclasses.replace`. + * a custom converter — refuse rather than silently clobber it; the caller + must fold :class:`DeepAgentsPayloadConverter` into their own converter. + """ + if user_converter is None: + return data_converter + if user_converter is DataConverter.default: + return dataclasses.replace( + user_converter, payload_converter_class=DeepAgentsPayloadConverter + ) + raise ValueError( + "DeepAgentsPlugin cannot compose with a custom data_converter " + "automatically. Set payload_converter_class=DeepAgentsPayloadConverter " + "on your own DataConverter (so LangChain messages serialize with " + "exclude_unset=True), or omit data_converter to use the plugin default." + ) + + +# --------------------------------------------------------------------------- +# LangChain object (de)serialization +# --------------------------------------------------------------------------- + + +def dump_object(obj: Any) -> Any: + """Serialize a single LangChain ``Serializable`` (message, tool call, …).""" + from langchain_core.load import dumpd + + return dumpd(obj) + + +def load_object(data: Any) -> Any: + """Rehydrate a value produced by :func:`dump_object`, preserving subtype.""" + from langchain_core.load import load + + return load(data) + + +# --------------------------------------------------------------------------- +# Backend protocol result (de)serialization +# --------------------------------------------------------------------------- + +_BACKEND_DATACLASS_KEY = "__deepagents_dataclass__" + + +def dump_backend_result(value: Any) -> Any: + """Encode a backend op's return value for the activity boundary. + + Backend protocol results (``WriteResult`` / ``ReadResult`` / ``GrepResult`` + and their nested ``FileInfo`` / ``GrepMatch`` items, …) are plain + dataclasses — not LangChain ``Serializable`` objects — and the filesystem + middleware reads their ATTRIBUTES in-workflow, so a plain JSON round-trip + (which decays them to dicts) breaks the seam at the first real backend op. + Tag deepagents dataclasses with their import path so + :func:`load_backend_result` rebuilds the real type; anything else (str, + dict, a custom backend's own types) passes through with today's + plain-JSON behavior. + """ + import dataclasses + + if dataclasses.is_dataclass(value) and not isinstance(value, type): + cls = type(value) + dumped_fields = { + f.name: dump_backend_result(getattr(value, f.name)) + for f in dataclasses.fields(value) + } + if cls.__module__.split(".", 1)[0] == "deepagents": + return { + _BACKEND_DATACLASS_KEY: f"{cls.__module__}:{cls.__qualname__}", + "fields": dumped_fields, + } + return dumped_fields + if isinstance(value, (list, tuple)): + return [dump_backend_result(v) for v in value] + if isinstance(value, dict): + return {k: dump_backend_result(v) for k, v in value.items()} + return value + + +def load_backend_result(value: Any) -> Any: + """Rebuild a value produced by :func:`dump_backend_result`. + + Only ``deepagents.*`` dataclasses are reconstructed (the tag is written + exclusively for them); anything else would mean a forged payload, so + refuse rather than import arbitrary types. Reconstruction suppresses + ``DeprecationWarning``: this is transport, not user code — the backend + already constructed the object once on the activity side, and required + deprecated fields (e.g. ``WriteResult.files_update``) would otherwise + warn on every op. Field values equal to a declared default are omitted + from the constructor call. + """ + import dataclasses + import importlib + import warnings + + if isinstance(value, dict) and _BACKEND_DATACLASS_KEY in value: + path = value[_BACKEND_DATACLASS_KEY] + module_name, _, qualname = path.partition(":") + if module_name.split(".", 1)[0] != "deepagents": + raise ValueError(f"refusing to rebuild non-deepagents type {path!r}") + obj: Any = importlib.import_module(module_name) + for part in qualname.split("."): + obj = getattr(obj, part) + if not (isinstance(obj, type) and dataclasses.is_dataclass(obj)): + raise ValueError(f"{path!r} is not a dataclass") + loaded = {k: load_backend_result(v) for k, v in value["fields"].items()} + kwargs = {} + for f in dataclasses.fields(obj): + if f.name not in loaded: + continue + if f.default is not dataclasses.MISSING and loaded[f.name] == f.default: + continue + kwargs[f.name] = loaded[f.name] + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + return obj(**kwargs) + if isinstance(value, list): + return [load_backend_result(v) for v in value] + if isinstance(value, dict): + return {k: load_backend_result(v) for k, v in value.items()} + return value + + +def dump_messages(messages: Any) -> list[Any]: + """Serialize a sequence of LangChain messages to their ``dumpd`` form.""" + from langchain_core.load import dumpd + + return [dumpd(m) for m in messages] + + +def load_messages(dumped: list[Any]) -> list[Any]: + """Rehydrate messages serialized by :func:`dump_messages`.""" + from langchain_core.load import load + + return [load(d) for d in dumped] + + +def tool_to_schema(tool: Any) -> dict[str, Any]: + """Advertise a tool to the model as a full OpenAI tool schema. + + Carries name + description + argument JSON schema so the model can build + valid arguments, not just select the tool by name. + """ + from langchain_core.utils.function_calling import convert_to_openai_tool + + return convert_to_openai_tool(tool) + + +# --------------------------------------------------------------------------- +# RunnableConfig strip / rebuild +# --------------------------------------------------------------------------- + + +def _is_jsonish(value: Any) -> bool: + if value is None or isinstance(value, (str, int, float, bool)): + return True + if isinstance(value, (list, tuple)): + return all(_is_jsonish(v) for v in value) + if isinstance(value, dict): + return all(isinstance(k, str) and _is_jsonish(v) for k, v in value.items()) + return False + + +def strip_runnable_config(config: Any) -> dict[str, Any]: + """Reduce a live ``RunnableConfig`` to its JSON-safe subset for shipping. + + Keeps ``tags``, ``run_name``, ``run_id``, ``recursion_limit``, JSON-safe + ``metadata`` and the JSON-safe (non-dunder) ``configurable`` keys. Drops + callbacks, checkpointer / store / cache handles and every other live + reference — those are reconstructed activity-side. + """ + if not config: + return {} + out: dict[str, Any] = {} + if config.get("tags"): + out["tags"] = list(config["tags"]) + for key in ("run_id", "run_name"): + if config.get(key) is not None: + out[key] = str(config[key]) + if config.get("recursion_limit") is not None: + out["recursion_limit"] = config["recursion_limit"] + metadata = config.get("metadata") + if isinstance(metadata, dict): + out["metadata"] = {k: v for k, v in metadata.items() if _is_jsonish(v)} + configurable = config.get("configurable") + if isinstance(configurable, dict): + kept = { + k: v + for k, v in configurable.items() + if not k.startswith("__") and _is_jsonish(v) + } + if kept: + out["configurable"] = kept + return out + + +def rebuild_runnable_config(data: dict[str, Any]) -> "RunnableConfig": + """Reconstruct a minimal ``RunnableConfig`` from :func:`strip_runnable_config`.""" + config: dict[str, Any] = {"metadata": dict(data.get("metadata", {}))} + if data.get("tags"): + config["tags"] = list(data["tags"]) + for key in ("run_id", "run_name"): + if data.get(key) is not None: + config[key] = data[key] + if data.get("recursion_limit") is not None: + config["recursion_limit"] = data["recursion_limit"] + if data.get("configurable"): + config["configurable"] = dict(data["configurable"]) + # Double cast: a TypedDict and dict[str, Any] "insufficiently overlap" + # for basedpyright's reportInvalidCast; object is the sanctioned bridge. + return cast("RunnableConfig", cast(object, config)) + + +# --------------------------------------------------------------------------- +# Result cache (continue-as-new dedup) +# --------------------------------------------------------------------------- + +# Per-workflow state: set at the top of the workflow run, read by the model and +# tool dispatch paths, snapshotted for continue-as-new. A ContextVar (not a +# module-global keyed by run id) so update / signal handler tasks spawned by the +# workflow inherit the same cache automatically. +_result_cache: contextvars.ContextVar[dict[str, Any] | None] = contextvars.ContextVar( + "_deepagents_result_cache", default=None +) + + +def set_result_cache(cache: dict[str, Any] | None) -> None: + """Seed the workflow-scoped result cache (e.g. carried across CAN).""" + _result_cache.set(dict(cache) if cache else {}) + + +def result_cache_snapshot() -> dict[str, Any] | None: + """Return a serializable copy of the cache, or ``None`` when empty.""" + cache = _result_cache.get() + return dict(cache) if cache else None + + +def cache_key(kind: str, call_id: str, args: Any) -> str: + """Stable key over ``(kind, call_id, args)`` for cache lookups.""" + blob = json.dumps([kind, call_id, args], default=str, sort_keys=True) + return hashlib.sha256(blob.encode("utf-8")).hexdigest() + + +def cache_lookup(key: str) -> tuple[bool, Any]: + """Return ``(hit, value)`` for ``key`` in the active cache.""" + cache = _result_cache.get() + if cache is not None and key in cache: + return True, cache[key] + return False, None + + +def cache_put(key: str, value: Any) -> None: + """Record ``value`` under ``key`` when a cache is active for this run.""" + cache = _result_cache.get() + if cache is not None: + cache[key] = value + + +# --------------------------------------------------------------------------- +# Sandbox passthrough +# --------------------------------------------------------------------------- + +# The workflow sandbox re-imports modules per run; LangChain / LangGraph / +# deepagents build large class hierarchies with eager import side effects, and +# LangSmith pulls in numpy. Passing them through means "import once in the host +# and share", which is both faster and required for identity checks +# (isinstance across the sandbox boundary) to hold. +_DEFAULT_PASSTHROUGH: tuple[str, ...] = ( + "langchain", + "langchain_core", + "langchain_anthropic", + "langgraph", + "deepagents", + "langsmith", + "numpy", + "pydantic", + "pydantic_core", + "anthropic", + "tiktoken", + "jsonpatch", + "jsonpointer", + "tenacity", + "orjson", + "httpx", + "httpcore", +) + + +def default_passthrough_modules() -> tuple[str, ...]: + """The LangChain / deepagents transitive import tree passed through the sandbox.""" + return _DEFAULT_PASSTHROUGH + + +def resolve_passthrough_modules(user: Any) -> tuple[str, ...]: + """Merge caller-supplied passthrough modules with the plugin defaults.""" + merged = [*_DEFAULT_PASSTHROUGH, *(user or ())] + # dict.fromkeys preserves order while de-duplicating. + return tuple(dict.fromkeys(merged)) diff --git a/temporalio/contrib/deepagents/_tools.py b/temporalio/contrib/deepagents/_tools.py new file mode 100644 index 000000000..1235224c6 --- /dev/null +++ b/temporalio/contrib/deepagents/_tools.py @@ -0,0 +1,517 @@ +"""The tool + backend seams: the explicit per-unit Workflow-vs-Activity choice. + +Deep Agents holds its tools and filesystem/shell backends in-workflow. A tool or +backend op that only reads and writes ``DeepAgentState`` is pure and belongs in +the workflow (deterministic, replay-safe). One that does real I/O — a web +search, a shell command, a disk write — must not run there. This module gives +the user three explicit ways to move that work to an activity: + +* :func:`activity_as_tool` — expose an existing ``@activity.defn`` as a Deep + Agents tool (Temporal adopters already have activities; don't make them + re-declare); +* :func:`tool_as_activity` — wrap a LangChain ``BaseTool`` / callable so its + execution runs as an activity; +* :class:`TemporalBackend` — wrap a real-I/O backend so each file/exec op runs + as an activity. + +The choice is always explicit: an unwrapped non-builtin tool runs in-workflow, +and the plugin warns at construction so that is a conscious decision, never a +silent one. + +Registries here live in a ``temporalio``-namespaced (sandbox-passthrough) module, +so the object the worker's activity sees is the same one the module-level +``tool_as_activity(...)`` / ``TemporalBackend(...)`` call populated. They hold +worker-wide wiring, not per-workflow state. +""" + +from __future__ import annotations + +import importlib +import threading +import uuid as _uuid +import warnings +import weakref +from collections.abc import Mapping +from datetime import timedelta +from functools import wraps +from typing import TYPE_CHECKING, Any, Callable + +from temporalio import activity as activity_mod +from temporalio import workflow +from temporalio.contrib.deepagents import _activity, _serde + +# LangChain is a runtime dependency of the *tool seam*, but importing this module +# must not require it (the plugin imports it just to read tool defaults). So the +# ``langchain_core.tools`` symbols are imported lazily inside the functions that +# actually build tools; ``from __future__ import annotations`` keeps the type +# hints below as strings so they never touch LangChain at import time. +if TYPE_CHECKING: + from langchain_core.tools import BaseTool + + +# --------------------------------------------------------------------------- +# Tool registry (worker-side execution targets) +# --------------------------------------------------------------------------- + +_TOOL_REGISTRY: dict[str, "BaseTool"] = {} +_BACKEND_REGISTRY: dict[str, Any] = {} +# Serializes registration against the GC-time unregister in +# _unregister_backend, which may run on another thread. +_BACKEND_REGISTRY_LOCK = threading.Lock() + +# Worker-wide default activity options for tools wrapped with tool_as_activity, +# set by the plugin. Not per-workflow state: fixed for the worker's lifetime, and +# this module is sandbox-passthrough so the workflow sees the configured value. +_tool_defaults: dict[str, Any] = {} + + +def set_tool_defaults(options: Any) -> None: + """Install the plugin's default ``tool_activity_options`` (called by the plugin). + + Accepts a single ``ActivityConfig`` or a ``Mapping[tool_name, ActivityConfig]``. + """ + _tool_defaults.clear() + if options: + _tool_defaults["__value__"] = options + + +def _resolve_tool_options( + tool_name: str, instance_options: Mapping[str, Any] | None +) -> dict[str, Any]: + """Merge plugin tool defaults (possibly per-tool) with a per-call override.""" + opts: dict[str, Any] = {} + default = _tool_defaults.get("__value__") + if isinstance(default, Mapping): + per_tool = default.get(tool_name) + if isinstance(per_tool, Mapping): + opts.update(per_tool) + elif not any(isinstance(v, Mapping) for v in default.values()): + opts.update(default) + if instance_options: + opts.update(instance_options) + return opts + + +# Names of tools that route through Temporal (activity_as_tool / tool_as_activity), +# used to warn about unwrapped non-builtin tools at workflow build time. +_ROUTED_TOOL_NAMES: set[str] = set() + +# Deep Agents' built-in tools run in-workflow (pure state mutations); they are +# not expected to be wrapped, so they never trigger the unwrapped-tool warning. +# These are the LLM-facing TOOL names (``read_file``, ``write_file``, …) — +# NOT the backend protocol method names in ``_BACKEND_OPS`` (``read``, +# ``aread``, …); the two namespaces intentionally differ. +_BUILTIN_TOOL_NAMES = frozenset( + { + "write_todos", + "ls", + "read_file", + "write_file", + "edit_file", + "glob", + "grep", + "execute", + "task", + } +) + + +def register_tool(tool: BaseTool) -> None: + """Record ``tool`` so :meth:`DeepAgentActivities.invoke_tool` can run it.""" + _TOOL_REGISTRY[tool.name] = tool + + +def warn_unwrapped_tools(tools: Any) -> None: + """Warn once per unwrapped, non-builtin tool passed to ``create_deep_agent``. + + Running a tool in-workflow is only safe if it is pure/deterministic. The + Workflow-vs-Activity choice must be conscious, so any user tool that was not + routed through :func:`tool_as_activity` / :func:`activity_as_tool` gets a + construction-time warning rather than silently executing in the workflow. + """ + for tool in tools or (): + name = getattr(tool, "name", getattr(tool, "__name__", None)) + if name is None or name in _BUILTIN_TOOL_NAMES or name in _ROUTED_TOOL_NAMES: + continue + warnings.warn( + f"Tool {name!r} is passed to create_deep_agent unwrapped and will run " + f"inside the workflow. That is only safe if it is pure/deterministic. " + f"If it does I/O, wrap it with tool_as_activity(...) or expose an " + f"existing activity with activity_as_tool(...).", + stacklevel=3, + ) + + +def get_registered_tool(name: str) -> BaseTool | None: + """Look up a tool registered by :func:`register_tool`.""" + return _TOOL_REGISTRY.get(name) + + +def register_backend(ref: str, backend: Any) -> None: + """Record a backend so :meth:`DeepAgentActivities.backend_op` can reach it.""" + with _BACKEND_REGISTRY_LOCK: + _BACKEND_REGISTRY[ref] = backend + + +def _unregister_backend(ref: str, inner: Any) -> None: + """Drop ``ref`` from the registry if it still maps to ``inner``. + + GC hook for :class:`TemporalBackend` (via ``weakref.finalize``): a wrapper + is typically constructed per workflow run, so without cleanup a long-lived + worker accumulates one registry entry per run. The identity guard is + load-bearing: refs are deterministic per run, so after a cache eviction a + replay re-registers the *same* ref with a fresh inner backend — the evicted + wrapper's finalizer must not remove that live registration. + """ + with _BACKEND_REGISTRY_LOCK: + if _BACKEND_REGISTRY.get(ref) is inner: + del _BACKEND_REGISTRY[ref] + + +def registered_backends() -> dict[str, Any]: + """Return the live backend registry (read by the plugin at worker build).""" + return _BACKEND_REGISTRY + + +# --------------------------------------------------------------------------- +# activity_as_tool +# --------------------------------------------------------------------------- + + +def activity_as_tool( + activity: Callable, + *, + start_to_close_timeout: timedelta, + name: str | None = None, + description: str | None = None, + retry_policy: Any = None, + summary: str | None = None, +) -> BaseTool: + """Expose an existing Temporal activity as a Deep Agents tool. + + The returned tool advertises the activity's argument schema to the model and, + when called in-workflow, dispatches to the activity via + ``workflow.execute_activity`` — Temporal owns its retries and timeout. + + Args: + activity: A function decorated with ``@activity.defn``. + start_to_close_timeout: Required per-call timeout for the activity. + name: Override the tool name advertised to the model (defaults to + the activity definition name). + description: Override the tool description advertised to the model + (defaults to the activity docstring). + retry_policy: Optional Temporal retry policy for the activity. + summary: Optional ``summary=`` recorded on each activity invocation. + """ + with workflow.unsafe.imports_passed_through(): + from langchain_core.tools import StructuredTool + + defn = activity_mod._Definition.from_callable(activity) + if defn is None: + raise ValueError( + "activity_as_tool requires a function decorated with @activity.defn; " + f"{getattr(activity, '__name__', activity)!r} is not an activity." + ) + tool_name = name or defn.name + if tool_name is None: + raise ValueError( + "activity_as_tool requires a named activity (dynamic activities " + "have no definition name); pass name= explicitly." + ) + tool_desc = description or (activity.__doc__ or f"Temporal activity {tool_name}.") + act_summary = summary or f"tool:{tool_name}" + + # Temporal activities take a single positional argument. StructuredTool infers + # the model-facing schema from ``_run``'s signature, so we mirror the activity's + # own parameter names onto ``_run`` (via ``@wraps``) and then collapse the + # keyword call the model produces back into that single positional payload. + import inspect + + params = [ + p for p in inspect.signature(activity).parameters if p not in ("self", "cls") + ] + + @wraps(activity) + async def _run(*args: Any, **kwargs: Any) -> Any: + if args and not kwargs: + payload: Any = args[0] if len(args) == 1 else list(args) + elif len(params) == 1 and len(kwargs) == 1: + # Single-argument activity: pass the value directly, not {"arg": value}. + payload = next(iter(kwargs.values())) + else: + payload = kwargs + return await workflow.execute_activity( + activity, + payload, + start_to_close_timeout=start_to_close_timeout, + retry_policy=retry_policy, + summary=act_summary, + ) + + _ROUTED_TOOL_NAMES.add(tool_name) + return StructuredTool.from_function( + coroutine=_run, + name=tool_name, + description=tool_desc, + ) + + +# --------------------------------------------------------------------------- +# tool_as_activity +# --------------------------------------------------------------------------- + + +def tool_as_activity( + tool: BaseTool | Callable, + *, + start_to_close_timeout: timedelta, + activity_options: Mapping[str, Any] | None = None, +) -> BaseTool: + """Wrap a LangChain tool / callable so its execution runs as an activity. + + The underlying tool is registered on the worker; the returned tool keeps the + same name and argument schema (so the model's calls are unchanged) but, when + invoked in-workflow, dispatches ``deepagents.invoke_tool`` instead of running + the tool body inline. + """ + with workflow.unsafe.imports_passed_through(): + from langchain_core.tools import BaseTool, StructuredTool + + base_tool: BaseTool + if isinstance(tool, BaseTool): + base_tool = tool + else: + base_tool = StructuredTool.from_function( + tool if not _is_coroutine(tool) else None, + coroutine=tool if _is_coroutine(tool) else None, + ) + register_tool(base_tool) + + tool_name = base_tool.name + opts = _resolve_tool_options(tool_name, activity_options) + opts.setdefault("start_to_close_timeout", start_to_close_timeout) + + async def _run(**kwargs: Any) -> Any: + from temporalio.contrib.deepagents.workflow import call_tool + + tool_call_id = kwargs.pop("__tool_call_id__", None) or workflow.uuid4().hex + activity_input = _activity.ToolActivityInput( + tool_name=tool_name, + tool_call_id=tool_call_id, + args=kwargs, + ) + output = await call_tool( + activity_input, + summary=f"tool:{tool_name}", + **opts, + ) + message = _serde.load_object(output.message) + # Return CONTENT, not the pre-built ToolMessage: the activity cannot + # know the model's real tool_call_id, so a ToolMessage assembled there + # carries a generated id that a real provider rejects as an unpaired + # tool_result. Given plain content, the tool node stamps the model's + # own id — exactly as it does for unwrapped tools. + with workflow.unsafe.imports_passed_through(): + from langchain_core.messages import ToolMessage + + if isinstance(message, ToolMessage): + return message.content + return message + + _ROUTED_TOOL_NAMES.add(tool_name) + return StructuredTool( + name=tool_name, + description=base_tool.description, + args_schema=base_tool.args_schema, # type: ignore[arg-type] + coroutine=_run, + ) + + +def _is_coroutine(fn: Any) -> bool: + import inspect + + return inspect.iscoroutinefunction(fn) + + +# --------------------------------------------------------------------------- +# TemporalBackend +# --------------------------------------------------------------------------- + +# Async protocol methods and their sync twins. deepagents' ``BackendProtocol`` +# implements each async DEFAULT as ``asyncio.to_thread(sync_twin, ...)``; the +# deterministic workflow event loop has no thread executor, so any built-in +# tool call against an unwrapped in-workflow backend (e.g. the default +# ``StateBackend``) raises ``NotImplementedError``. A real model hits this on +# its first spontaneous ``grep``/``read_file`` call; scripted-model tests that +# never call built-ins sail past it. +_ASYNC_TO_SYNC_OPS: dict[str, str] = { + "als": "ls", + "als_info": "ls_info", + "aread": "read", + "awrite": "write", + "aedit": "edit", + "aglob": "glob", + "aglob_info": "glob_info", + "agrep": "grep", + "agrep_raw": "grep_raw", + "adownload_files": "download_files", + "aupload_files": "upload_files", + "aexecute": "execute", +} + +_original_backend_async_defaults: dict[str, Any] = {} + + +def install_backend_async_patch() -> None: + """Make ``BackendProtocol``'s async defaults workflow-safe. + + Inside a workflow, run the sync twin inline: for state-only backends that + is deterministic and semantically identical to the upstream default, + which merely moves the same sync call onto a worker thread. Outside a + workflow (activities, clients) the upstream default — thread hop plus + timeout guard — is used unchanged. Subclasses that override an async + method natively are unaffected; only the protocol defaults are replaced. + Idempotent. + """ + # importlib: `deepagents` is absent on Python 3.10 environments (its floor + # is 3.11), so a static import here fails type-checking there. + BackendProtocol = importlib.import_module( + "deepagents.backends.protocol" + ).BackendProtocol + + if _original_backend_async_defaults: + return + for async_name, sync_name in _ASYNC_TO_SYNC_OPS.items(): + original = BackendProtocol.__dict__.get(async_name) + if original is None: + continue + + def _make(sync_name: str, original: Any) -> Any: + async def patched(self: Any, *args: Any, **kwargs: Any) -> Any: + if workflow.in_workflow(): + return getattr(self, sync_name)(*args, **kwargs) + return await original(self, *args, **kwargs) + + return patched + + _original_backend_async_defaults[async_name] = original + setattr(BackendProtocol, async_name, _make(sync_name, original)) + + +def uninstall_backend_async_patch() -> None: + """Restore ``BackendProtocol``'s upstream async defaults.""" + if not _original_backend_async_defaults: + return + BackendProtocol = importlib.import_module( + "deepagents.backends.protocol" + ).BackendProtocol + + for async_name, original in _original_backend_async_defaults.items(): + setattr(BackendProtocol, async_name, original) + _original_backend_async_defaults.clear() + + +# Backend METHOD names (deepagents ``BackendProtocol`` + +# ``SandboxBackendProtocol``) whose calls must cross the activity boundary: +# every sync I/O method, its async ``a``-prefixed twin, and the sandbox/shell +# execute pair. The async twins are load-bearing — deepagents' filesystem +# middleware drives backends through ``als``/``aread``/``awrite``/… — so +# intercepting only sync names lets an agent's built-in file tools run I/O +# in-workflow. These are backend PROTOCOL method names, distinct from the +# LLM-facing tool names in ``_BUILTIN_TOOL_NAMES`` (``read_file`` etc.). +_BACKEND_OPS = ( + # Sync protocol surface. + "ls", + "ls_info", + "read", + "write", + "edit", + "glob", + "glob_info", + "grep", + "grep_raw", + "download_files", + "upload_files", + "execute", + # Async twins (what FilesystemMiddleware actually calls). + "als", + "als_info", + "aread", + "awrite", + "aedit", + "aglob", + "aglob_info", + "agrep", + "agrep_raw", + "adownload_files", + "aupload_files", + "aexecute", +) + + +class TemporalBackend: + """Route a real-I/O backend's operations through Temporal activities. + + Wrap a ``FilesystemBackend`` / ``LocalShellBackend`` / ``StoreBackend`` / + ``CompositeBackend`` so each file or shell operation becomes a durable + ``deepagents.backend_op`` activity instead of touching disk / shell from the + workflow. State-only backends (``StateBackend``) need no wrapping — they are + pure workflow state and run in-workflow. + + Unknown attribute access is forwarded to the inner backend so backend + metadata / configuration the agent reads (but that does no I/O) still works. + """ + + def __init__( + self, + inner: Any, + *, + activity_options: Mapping[str, Any] | None = None, + ) -> None: + """Wrap ``inner`` so its I/O ops dispatch as durable activities.""" + self._inner = inner + # A deterministic id: workflow.uuid4() is seeded per-run, so the ref is + # identical across replays (unlike id(inner)). Falls back to a plain uuid + # when a backend is wrapped outside a workflow (e.g. in a plain test). + if workflow.in_workflow(): + self._ref = f"backend:{workflow.uuid4().hex}" + else: + self._ref = f"backend:{_uuid.uuid4().hex}" + self._opts: dict[str, Any] = dict(activity_options or {}) + self._opts.setdefault("start_to_close_timeout", timedelta(minutes=1)) + register_backend(self._ref, inner) + # Balance the registration when this wrapper is garbage-collected + # (workflow completion / cache eviction) so per-run backends do not + # accumulate in the worker-global registry. The finalizer captures + # (ref, inner) — not ``self`` — so it cannot keep the wrapper alive, + # and it only removes its own registration (see _unregister_backend). + self._finalizer = weakref.finalize(self, _unregister_backend, self._ref, inner) + + async def _dispatch(self, op: str, *args: Any, **kwargs: Any) -> Any: + from temporalio.contrib.deepagents.workflow import call_backend_op + + activity_input = _activity.BackendOpInput( + backend_ref=self._ref, + op=op, + args=list(args), + kwargs=dict(kwargs), + ) + output = await call_backend_op( + activity_input, + summary=f"backend:{op}", + **self._opts, + ) + return _serde.load_backend_result(output.result) + + def __getattr__(self, name: str) -> Any: + """Bound-method access for a known I/O op returns an activity dispatcher. + + Everything else forwards to the inner backend unchanged. + """ + if name in _BACKEND_OPS: + + async def _op(*args: Any, **kwargs: Any) -> Any: + return await self._dispatch(name, *args, **kwargs) + + return _op + return getattr(self._inner, name) diff --git a/temporalio/contrib/deepagents/py.typed b/temporalio/contrib/deepagents/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/temporalio/contrib/deepagents/testing.py b/temporalio/contrib/deepagents/testing.py new file mode 100644 index 000000000..749b1ab96 --- /dev/null +++ b/temporalio/contrib/deepagents/testing.py @@ -0,0 +1,141 @@ +"""Test helpers for users adopting :class:`DeepAgentsPlugin`. + +Unit-testing a Deep Agent under Temporal should not require a live LLM endpoint +or a paid API key. This module ships: + +* :class:`FakeModel` — a real ``BaseChatModel`` returning scripted replies (plain + text or full ``AIMessage`` objects carrying ``tool_calls``), cycling when + exhausted; +* :func:`fake_model_factory` — a one-liner for the common text-only case; +* :func:`mock_model_provider` — a ``model_provider`` (name → model) you pass to + ``DeepAgentsPlugin(model_provider=...)`` so the model activity runs offline; +* :class:`MockTool` — a scripted ``BaseTool`` for exercising the tool seam. + +Importing this module has no process-wide side effects. +""" + +from __future__ import annotations + +from collections.abc import Callable, Sequence +from typing import Any + +from langchain_core.callbacks import ( + AsyncCallbackManagerForLLMRun, + CallbackManagerForLLMRun, +) +from langchain_core.language_models.chat_models import BaseChatModel +from langchain_core.messages import AIMessage, BaseMessage +from langchain_core.outputs import ChatGeneration, ChatResult +from langchain_core.tools import BaseTool +from pydantic import PrivateAttr + +__all__ = ["FakeModel", "fake_model_factory", "mock_model_provider", "MockTool"] + +Response = str | AIMessage + + +class FakeModel(BaseChatModel): + """A ``BaseChatModel`` returning scripted responses, for offline tests. + + Args: + responses: Replies returned one per call, cycling when exhausted. Each is + either a string (becomes an ``AIMessage``) or an ``AIMessage`` (so you + can script ``tool_calls`` to drive the agent's tool path). + """ + + responses: list[Any] + _cursor: int = PrivateAttr(default=0) + + def __init__(self, responses: Sequence[Response], **kwargs: Any) -> None: + """Validate and store the scripted responses.""" + resp = list(responses) + if not resp: + raise ValueError("FakeModel needs at least one scripted response.") + super().__init__(responses=resp, **kwargs) # type: ignore[call-arg] + + @property + def _llm_type(self) -> str: + return "temporalio-deepagents-fake-model" + + def bind_tools(self, tools: Sequence[Any], **kwargs: Any) -> "FakeModel": + """Ignore the tool set; the fake just replays its script. + + Returning ``self`` keeps ``model.bind_tools(...)`` chainable like a + real model. + """ + return self + + def _next(self) -> AIMessage: + item = self.responses[self._cursor % len(self.responses)] + self._cursor += 1 + return item if isinstance(item, AIMessage) else AIMessage(content=item) + + def _generate( + self, + messages: list[BaseMessage], + stop: list[str] | None = None, + run_manager: CallbackManagerForLLMRun | None = None, + **kwargs: Any, + ) -> ChatResult: + return ChatResult(generations=[ChatGeneration(message=self._next())]) + + async def _agenerate( + self, + messages: list[BaseMessage], + stop: list[str] | None = None, + run_manager: AsyncCallbackManagerForLLMRun | None = None, + **kwargs: Any, + ) -> ChatResult: + return ChatResult(generations=[ChatGeneration(message=self._next())]) + + +def fake_model_factory(responses: Sequence[Response]) -> FakeModel: + """One-liner scripted fake chat model. + + Example:: + + model = fake_model_factory(["The capital of France is Paris."]) + """ + return FakeModel(responses) + + +def mock_model_provider( + responses: Sequence[Response], +) -> Callable[[str], FakeModel]: + """A ``model_provider`` that hands out the scripted responses one call at a time. + + Each model activity invocation (main agent, a sub-agent, a follow-up turn + after a tool call) advances through ``responses`` and cycles when exhausted, + so a multi-turn agent can be scripted deterministically. The model activity + builds a fresh model per call, so the cursor lives on the provider closure + (shared for the worker's lifetime) rather than on any one model instance. + + Pass to the plugin so the model activity runs offline:: + + plugin = DeepAgentsPlugin(model_provider=mock_model_provider(["Paris."])) + """ + resp = list(responses) + if not resp: + raise ValueError("mock_model_provider needs at least one response.") + cursor = {"i": 0} + + def provider(_model_name: str) -> FakeModel: + reply = resp[cursor["i"] % len(resp)] + cursor["i"] += 1 + return FakeModel([reply]) + + return provider + + +class MockTool(BaseTool): + """A scripted ``BaseTool`` whose call returns a fixed value, for tests.""" + + name: str = "mock_tool" + description: str = "A mock tool that returns a scripted result." + result: Any = "ok" + + def _run(self, *args: Any, **kwargs: Any) -> Any: + return self.result + + async def _arun(self, *args: Any, **kwargs: Any) -> Any: + return self.result diff --git a/temporalio/contrib/deepagents/workflow.py b/temporalio/contrib/deepagents/workflow.py new file mode 100644 index 000000000..cb3e45b03 --- /dev/null +++ b/temporalio/contrib/deepagents/workflow.py @@ -0,0 +1,317 @@ +"""Workflow-side surface: the failure type, the dispatch helpers, and the runner. + +Everything here runs *inside* the workflow. The dispatch helpers +(:func:`call_model` / :func:`call_tool` / :func:`call_backend_op`) are the single +choke point through which the in-workflow model / tool / backend stubs reach +their activities; they also consult the continue-as-new result cache so work +done before a ``continue_as_new`` is reused rather than repeated after it. + +:func:`run_deep_agent` is the optional driver that adds continue-as-new +state-carry around a native ``agent.ainvoke(...)`` — plain ``agent.ainvoke(...)`` +still works without it. +""" + +from __future__ import annotations + +import importlib +import warnings +from collections.abc import Mapping +from typing import Any + +from temporalio import workflow +from temporalio.contrib.deepagents import _activity, _serde +from temporalio.exceptions import ApplicationError + +# Reserved key under which the CAN result cache rides inside a state snapshot. +_CACHE_KEY = "__temporal_cache__" + +# Checkpointer classes that keep their state in the workflow's own memory and are +# therefore rehydrated for free by deterministic replay. Anything else does its +# own I/O and is not replay-safe from inside the workflow. +_IN_WORKFLOW_SAVERS = frozenset({"InMemorySaver", "MemorySaver"}) + + +def warn_durable_checkpointer(checkpointer: Any) -> None: + """Warn when a user hands ``create_deep_agent`` a durable checkpointer. + + The Deep Agents loop runs inside the workflow, so a checkpointer that does + its own database / disk I/O would run that I/O from workflow code — not + replay-safe. We respect the user's choice (a warning, not a hard failure), + and point them at the durability path that *is* safe: the default in-workflow + ``InMemorySaver`` rehydrated by replay, plus + :func:`run_deep_agent` with ``continue_as_new_after`` for long conversations. + """ + if checkpointer is None: + return + if type(checkpointer).__name__ in _IN_WORKFLOW_SAVERS: + return + warnings.warn( + f"create_deep_agent received a durable checkpointer " + f"{type(checkpointer).__name__!r}. The agent loop runs inside the " + f"workflow, so this checkpointer's I/O would run from workflow code, " + f"which is not replay-safe. Prefer the default in-workflow InMemorySaver " + f"(rehydrated by replay) plus run_deep_agent(continue_as_new_after=...) " + f"for long-conversation durability.", + stacklevel=3, + ) + + +class DeepAgentsWorkflowError(ApplicationError): + """Raised for non-retryable Deep Agents failures surfaced in the workflow. + + This is the type registered in the plugin's + ``workflow_failure_exception_types``, so a model / tool failure that Temporal + has exhausted (or an invalid agent configuration) fails the workflow with a + stable ``ApplicationError.type`` — never a stringified peer exception. + """ + + TYPE = "deepagents.DeepAgentsWorkflowError" + + def __init__(self, message: str, *, non_retryable: bool = True) -> None: + """Construct the error with the plugin's stable failure ``type``.""" + super().__init__(message, type=self.TYPE, non_retryable=non_retryable) + + +# --------------------------------------------------------------------------- +# Dispatch helpers (the model / tool / backend choke point) +# --------------------------------------------------------------------------- + + +async def call_model( + activity_name: str, + activity_input: _activity.ModelActivityInput, + *, + summary: str, + **opts: Any, +) -> _activity.ModelActivityOutput: + """Dispatch one model call, reusing a cached result across continue-as-new.""" + key = _serde.cache_key( + "model", + activity_input.model_name, + [activity_input.messages, activity_input.tool_schemas], + ) + hit, cached = _serde.cache_lookup(key) + if hit: + return _activity.ModelActivityOutput(message=cached) + output = await workflow.execute_activity( + activity_name, + activity_input, + result_type=_activity.ModelActivityOutput, + summary=summary, + **opts, + ) + _serde.cache_put(key, output.message) + return output + + +async def call_tool( + activity_input: _activity.ToolActivityInput, + *, + summary: str, + **opts: Any, +) -> _activity.ToolActivityOutput: + """Dispatch one tool call, reusing a cached result across continue-as-new.""" + key = _serde.cache_key("tool", activity_input.tool_name, activity_input.args) + hit, cached = _serde.cache_lookup(key) + if hit: + return _activity.ToolActivityOutput(message=cached) + output = await workflow.execute_activity( + _activity.INVOKE_TOOL, + activity_input, + result_type=_activity.ToolActivityOutput, + summary=summary, + **opts, + ) + _serde.cache_put(key, output.message) + return output + + +async def call_backend_op( + activity_input: _activity.BackendOpInput, + *, + summary: str, + **opts: Any, +) -> _activity.BackendOpOutput: + """Dispatch one backend op, reusing a cached result across continue-as-new.""" + key = _serde.cache_key( + f"backend:{activity_input.backend_ref}", + activity_input.op, + [activity_input.args, activity_input.kwargs], + ) + hit, cached = _serde.cache_lookup(key) + if hit: + return _activity.BackendOpOutput(result=cached) + output = await workflow.execute_activity( + _activity.BACKEND_OP, + activity_input, + result_type=_activity.BackendOpOutput, + summary=summary, + **opts, + ) + _serde.cache_put(key, output.result) + return output + + +# --------------------------------------------------------------------------- +# run_deep_agent (continue-as-new state carry) +# --------------------------------------------------------------------------- + + +def _merge_snapshot(input: Any, snapshot: Mapping[str, Any]) -> Any: + """Prepend a snapshot's carried messages onto the next turn's input.""" + raw_prior: Any = snapshot.get("messages") or [] + prior = list(raw_prior) + if not prior: + return input + if isinstance(input, Mapping): + merged = dict(input) + raw_next: Any = input.get("messages") or [] + merged["messages"] = [*prior, *list(raw_next)] + return merged + return {"messages": [*prior, *_as_message_list(input)]} + + +def _as_message_list(input: Any) -> list[Any]: + if isinstance(input, (list, tuple)): + return list(input) + return [input] + + +async def run_deep_agent( + agent: Any, + input: Any, + *, + continue_as_new_after: int | None = None, + state_snapshot: Mapping[str, Any] | None = None, +) -> Any: + """Drive ``agent.ainvoke(input)`` with continue-as-new state carry. + + Once the completed turn leaves pending todos AND history has grown past the + limit, the turn's state (messages + the model/tool result cache) is + snapshotted and carried into a fresh run via ``workflow.continue_as_new``, + so long conversations do not accumulate unbounded history. + + By default (``continue_as_new_after=None``) the limit is the server's own + recommendation — ``workflow.info().is_continue_as_new_suggested()`` — which + accounts for both history length and size; this is the recommended mode. + Pass an explicit ``continue_as_new_after=N`` to trigger on a fixed history + event count instead. To run an agent with NO continue-as-new behavior, call + ``agent.ainvoke(...)`` directly rather than using this driver. + + The enclosing ``@workflow.run`` method must accept the continued call — i.e. + its signature is ``(input, state_snapshot=None)`` — because that is how the + carried state is threaded into the next run. + """ + # Resume path: rehydrate the result cache and fold carried messages in. + if state_snapshot is not None: + _serde.set_result_cache(dict(state_snapshot.get(_CACHE_KEY) or {})) + input = _merge_snapshot(input, state_snapshot) + else: + _serde.set_result_cache({}) + + try: + result = await agent.ainvoke(input) + except ApplicationError: + raise + except Exception as exc: + # If the framework wrapped a Temporal failure, surface the registered + # workflow-failure type rather than the framework's generic exception. + cause = exc.__cause__ + if cause is not None and workflow.is_failure_exception(cause): + raise DeepAgentsWorkflowError(f"Deep Agents run failed: {exc}") from cause + raise + + if continue_as_new_after is None: + # Default: follow the server's judgement. The suggestion accounts for + # history count AND size limits, which a fixed event threshold cannot. + should_continue = workflow.info().is_continue_as_new_suggested() + else: + should_continue = ( + workflow.info().get_current_history_length() >= continue_as_new_after + ) + if should_continue and _has_pending_work(result): + snapshot = { + "messages": _extract_messages(result), + _CACHE_KEY: _serde.result_cache_snapshot() or {}, + } + # ``continue_as_new`` threads positional args into the next run via + # ``args=``; the enclosing ``@workflow.run`` receives them as + # ``(input, state_snapshot)``. + workflow.continue_as_new(args=[input, snapshot]) + + return result + + +def _extract_messages(result: Any) -> list[Any]: + if isinstance(result, Mapping): + raw: Any = result.get("messages") or [] + return list(raw) + return [] + + +def _has_pending_work(result: Any) -> bool: + """True when the agent left unfinished todos worth carrying past a CAN. + + A finished single-shot run has no pending todos, so this returns False and the + driver returns the result instead of looping on continue-as-new forever. + """ + if isinstance(result, Mapping): + todos: Any = result.get("todos") or [] + return any( + isinstance(t, Mapping) and t.get("status") not in ("completed", "done") + for t in todos + ) + return False + + +def create_temporal_deep_agent( + *args: Any, + activity_options: Mapping[str, Any] | None = None, + **kwargs: Any, +) -> Any: + """Build a Deep Agent whose model calls run as durable activities. + + A thin wrapper over ``deepagents.create_deep_agent`` that makes the + Temporal wiring explicit: a ``model=`` name string is wrapped in + ``TemporalModel`` carrying this + agent's ``activity_options`` (``execute_activity`` overrides — timeouts, + retry policy — for its model calls). Every other argument — tools, + backend, sub-agents, ``interrupt_on`` — is forwarded unchanged. + + Unmodified ``create_deep_agent(...)`` also works inside a workflow (the + plugin substitutes the durable model automatically, using the plugin's + ``model_activity_options``); use this wrapper to scope activity options + to one agent instead of configuring them plugin-wide. + """ + with workflow.unsafe.imports_passed_through(): + # importlib keeps this resolution absolute: a static + # `import deepagents` from inside this same-named package directory + # is flagged (and on 3.10, mis-resolved) as implicitly relative. + deepagents_mod: Any = importlib.import_module("deepagents") + + from temporalio.contrib.deepagents._model import TemporalModel + + model = args[0] if args else kwargs.pop("model", None) + if isinstance(model, str): + model = TemporalModel( + model=model, + activity_options=( + dict(activity_options) if activity_options is not None else None + ), + ) + elif activity_options is not None: + if isinstance(model, TemporalModel): + model = TemporalModel( + model=model.model, activity_options=dict(activity_options) + ) + else: + raise ValueError( + "activity_options requires model= to be a model-name string " + "or a TemporalModel; got " + f"{type(model).__name__ if model is not None else 'no model'}." + ) + if args: + args = (model, *args[1:]) + else: + kwargs["model"] = model + return deepagents_mod.create_deep_agent(*args, **kwargs) diff --git a/temporalio/contrib/google_adk_agents/README.md b/temporalio/contrib/google_adk_agents/README.md new file mode 100644 index 000000000..d1ba2133a --- /dev/null +++ b/temporalio/contrib/google_adk_agents/README.md @@ -0,0 +1,257 @@ +# Google ADK Agents SDK Integration for Temporal + +This package provides the integration layer between the Google ADK and Temporal. It allows ADK Agents to run reliably within Temporal Workflows by ensuring determinism and correctly routing external calls (network I/O) through Temporal Activities. + +## Benefits of Temporal to the ADK + +Temporal provides a holistic, unified solution that centralizes your orchestration needs in one Workflow abstraction. Rather than cobbling together separate servers, task queues, gateways, and databases, you get: + +- **Recovering from crashes and stalls automatically**, rather than manually managing [sessions](https://google.github.io/adk-docs/sessions/session/#example-examining-session-properties) and [resuming](https://google.github.io/adk-docs/runtime/resume/#resume-a-stopped-workflow) them. (Google offers [Vertex Agent Engine](https://docs.cloud.google.com/agent-builder/agent-engine/sessions/manage-sessions-adk), which still leaves resumption to the user). No need to set up a separate [database](https://dev.to/greyisheepai/mastering-google-adk-databasesessionservice-and-events-complete-guide-to-event-injection-and-pdm#understanding-adk-databasesessionservice) + - Along with [Retries](https://docs.temporal.io/encyclopedia/retry-policies) and mechanisms for handling backpressure and rate limits. +- **Support for [ambient](https://temporal.io/blog/orchestrating-ambient-agents-with-temporal)/long-running agent patterns** via blocking awaits and [worker versioning](https://docs.temporal.io/production-deployment/worker-deployments/worker-versioning). +- **Automatic execution state [persistence](https://docs.temporal.io/temporal-service/persistence)**, not just for agent interactions but for any custom automations in your workflows, without setting up a separate [database](https://dev.to/greyisheepai/mastering-google-adk-databasesessionservice-and-events-complete-guide-to-event-injection-and-pdm#understanding-adk-databasesessionservice). +- For **Human-in-the-Loop patterns,** an api gateway to scalably [route](https://docs.temporal.io/task-routing) incoming messages (such as user chats) to awaken the correct workflow on your worker pool. +- [**Long-running tools](https://google.github.io/adk-docs/tools-custom/function-tools/#long-run-tool) support** using [Activities](https://docs.temporal.io/activities) — no need to set up and maintain microservices. +- [Manage and debug your agent workflow](https://temporal.io/resources/on-demand/demo-ai-agent) execution and pinpoint problems using Temporal UI. + +## Benefits of the ADK to Temporal + +ADK provides: (from the [ADK overview](https://google.github.io/adk-docs/#learn-more)): + +- Improved Agent development velocity with a first-class Agentic abstraction and integration with LLMs and an ecosystem of tools. +- Improved agent robustness using built-in evals +- Build complex agents using its Multi-agent architecture. +- [Safety and security](https://google.github.io/adk-docs/safety/), via guardrails and integrations with sandboxing solutions like Vertex Agent Runtime. + +## What's Included + +### Core ADK Integration +- **`TemporalModel`**: Intercepts model calls and executes them as Temporal activities +- **`GoogleAdkPlugin`**: Worker plugin that configures runtime determinism and Pydantic serialization +- **`invoke_model`**: Activity for executing LLM model calls with proper error handling + +### MCP (Model Context Protocol) Integration +- **`TemporalMcpToolSet`**: Executes MCP tools as Temporal activities +- **`TemporalMcpToolSetProvider`**: Manages toolset creation and activity registration +- Full support for tool confirmation and event actions within workflows + +### OpenTelemetry Integration +- Automatic instrumentation for ADK components when exporters are provided +- Tracing integration that works within Temporal's execution context +- Support for custom span exporters + +### Key Features + +#### 1. Deterministic Runtime +- Replaces `time.time()` with `workflow.now()` when in workflow context +- Replaces `uuid.uuid4()` with `workflow.uuid4()` for deterministic IDs +- Automatic setup when using `GoogleAdkPlugin` + +#### 2. Activity-Based Model Execution +Model calls are intercepted and executed as Temporal activities with configurable: +- Timeouts (schedule-to-close, start-to-close, heartbeat) +- Retry policies +- Task queues +- Cancellation behavior +- Priority levels + +#### 3. Sandbox Compatibility +- Automatic passthrough for `google.adk`, `google.genai`, and `mcp` modules +- Works with both sandboxed and unsandboxed workflow runners + +#### 4. Advanced Serialization +- Pydantic payload converter for ADK objects +- Proper handling of complex ADK data types +- Maintains type safety across workflow boundaries + +## Usage + +### Basic Setup + +**Agent (Workflow) Side:** +```python +from temporalio.contrib.google_adk_agents import TemporalModel +from temporalio.workflow import ActivityConfig +from google.adk import Agent + + +# Add to agent +agent = Agent( + name="test_agent", + model=TemporalModel("gemini-2.5-pro", activity_config=ActivityConfig(summary="Researcher Agent")), +) +``` + +**Worker Side:** + +```python +from temporalio.client import Client +from temporalio.worker import Worker +from temporalio.contrib.google_adk_agents import GoogleAdkPlugin + +client = await Client.connect( + "localhost:7233", + plugins=[ + GoogleAdkPlugin(), + ], +) + +worker = Worker( + client, + task_queue="my-queue", +) +``` + +### Advanced Features + +**With MCP Tools:** + +```python +import os +from google.adk import Agent +from google.adk.tools.mcp_tool import McpToolset +from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams +from mcp import StdioServerParameters +from temporalio.client import Client +from temporalio.worker import Worker + +from temporalio.contrib.google_adk_agents import ( + GoogleAdkPlugin, + TemporalMcpToolSetProvider, + TemporalMcpToolSet, +) + + +def toolset_factory(_): + return McpToolset( + connection_params=StdioConnectionParams( + server_params=StdioServerParameters( + command="npx", + args=[ + "-y", + "@modelcontextprotocol/server-filesystem", + os.path.dirname(os.path.abspath(__file__)), + ], + ), + ), + ) + +# Use in agent workflow +agent = Agent( + name="test_agent", + model="gemini-2.5-pro", + tools=[ + TemporalMcpToolSet( + "my-tools", + not_in_workflow_toolset=toolset_factory, + ) + ], +) + +client = await Client.connect( + "localhost:7233", + plugins=[ + GoogleAdkPlugin( + toolset_providers=[ + TemporalMcpToolSetProvider("my-tools", toolset_factory), + ], + ), + ], +) + +# Configure worker +worker = Worker( + client, + task_queue="task-queue" +) +``` + +`TemporalMcpToolSet` also accepts an optional `factory_argument`. It is sent to the toolset activities and passed to the registered `toolset_factory` when the `McpToolset` is created. + +**Do not pass secrets, credentials, or API keys through `factory_argument`.** It is an activity argument, so it is recorded in workflow history and, without a payload codec, visible in the web UI. Resolve credentials worker-side inside the toolset factory instead. + +### Reading Session State in Activity Tools + +ADK's live `ToolContext` holds non-serializable objects, so it cannot be an +activity argument. To read the serializable subset from an activity-backed +tool, declare a parameter named `tool_context` annotated with +`ToolContextSnapshot`: + +```python +from datetime import timedelta + +from temporalio import activity +from temporalio.contrib.google_adk_agents.workflow import ( + ToolContextSnapshot, + activity_as_tool, +) + + +@activity.defn +async def get_weather(query: str, tool_context: ToolContextSnapshot) -> dict: + db_url = tool_context.state.get("url", "") + ... + + +weather_tool = activity_as_tool( + get_weather, start_to_close_timeout=timedelta(seconds=30) +) +``` + +Exactly like a native ADK function tool's `tool_context` parameter, it is +excluded from the LLM-facing tool schema; at invocation the wrapper snapshots +the live `ToolContext` (session state as a plain dict, plus the function-call +id) and passes it to the activity. Annotating any parameter with a live ADK +context type raises `ValueError` at wrap time, since ADK would inject the +non-serializable context into it regardless of its name. + +When running under Temporal, the entire session state crosses the activity +boundary: every value in it must be serializable by the configured data +converter and the total size must fit within payload limits, even for keys +the tool never reads. Local ADK runs pass the snapshot in memory and have no +such constraint. + +The snapshot is one-way and should be treated as read-only: mutations inside +the activity do not propagate back to the session, because the activity may +run on a different worker (and in local ADK runs, where the snapshot is +passed in memory, nested state values may alias the live session state, so +mutating them can corrupt the session). To modify session state, return the +needed information from the activity and apply it in workflow-side code (for +example an ADK callback or a plain tool function). + +### Local ADK Runs + +The same agent definitions can also be exercised outside Temporal with +`adk run` or `adk web`. + +- `TemporalModel` and `activity_as_tool(...)` work in local ADK runs without + additional configuration. +- If the agent uses `TemporalMcpToolSet`, define a shared toolset factory, + register it with `TemporalMcpToolSetProvider(...)` for workflow runs, and + reuse the same function for `not_in_workflow_toolset=...` so the agent can + fall back to the underlying `McpToolset` when it is not running inside + `workflow.in_workflow()`. + +Example: + +```python +# Reuse the same toolset_factory registered in GoogleAdkPlugin above. +agent = Agent( + name="test_agent", + model=TemporalModel("gemini-2.5-pro"), + tools=[ + TemporalMcpToolSet( + "my-tools", + not_in_workflow_toolset=toolset_factory, + ) + ], +) +``` + +## Integration Points + +This integration provides comprehensive support for running Google ADK Agents within Temporal workflows while maintaining: +- **Determinism**: All non-deterministic operations are routed through Temporal +- **Observability**: Full tracing and activity visibility +- **Reliability**: Proper retry handling and error propagation +- **Extensibility**: Support for custom tools via MCP protocol diff --git a/temporalio/contrib/google_adk_agents/__init__.py b/temporalio/contrib/google_adk_agents/__init__.py new file mode 100644 index 000000000..d4c969fb3 --- /dev/null +++ b/temporalio/contrib/google_adk_agents/__init__.py @@ -0,0 +1,24 @@ +"""Temporal Integration for ADK. + +This module provides the necessary components to run ADK Agents within Temporal Workflows. +""" + +from temporalio.contrib.google_adk_agents._mcp import ( + TemporalMcpToolSet, + TemporalMcpToolSetProvider, + TemporalStatefulMcpToolSet, + TemporalStatefulMcpToolSetProvider, +) +from temporalio.contrib.google_adk_agents._model import TemporalModel +from temporalio.contrib.google_adk_agents._plugin import ( + GoogleAdkPlugin, +) + +__all__ = [ + "GoogleAdkPlugin", + "TemporalMcpToolSet", + "TemporalMcpToolSetProvider", + "TemporalStatefulMcpToolSet", + "TemporalStatefulMcpToolSetProvider", + "TemporalModel", +] diff --git a/temporalio/contrib/google_adk_agents/_mcp.py b/temporalio/contrib/google_adk_agents/_mcp.py new file mode 100644 index 000000000..b342c9c3c --- /dev/null +++ b/temporalio/contrib/google_adk_agents/_mcp.py @@ -0,0 +1,718 @@ +import asyncio +import functools +from collections.abc import Sequence +from dataclasses import dataclass +from datetime import timedelta +from types import TracebackType +from typing import Any, Callable + +from google.adk.agents.readonly_context import ReadonlyContext +from google.adk.events import EventActions +from google.adk.tools.base_tool import BaseTool +from google.adk.tools.base_toolset import BaseToolset +from google.adk.tools.mcp_tool import McpToolset +from google.adk.tools.tool_confirmation import ToolConfirmation +from google.adk.tools.tool_context import ToolContext +from google.genai import types +from google.genai.types import FunctionDeclaration + +from temporalio import activity, workflow +from temporalio.api.enums.v1.workflow_pb2 import ( + TIMEOUT_TYPE_HEARTBEAT, + TIMEOUT_TYPE_SCHEDULE_TO_START, +) +from temporalio.exceptions import ( + ActivityError, + ApplicationError, + is_cancelled_exception, +) +from temporalio.worker import PollerBehaviorSimpleMaximum, Worker +from temporalio.workflow import ActivityConfig, ActivityHandle + + +@dataclass +class _GetToolsArguments: + factory_argument: Any | None + + +@dataclass +class _ToolResult: + name: str + description: str + is_long_running: bool + custom_metadata: dict[str, Any] | None + function_declaration: FunctionDeclaration | None + + +@dataclass +class TemporalToolContext: + """Context for tools running within Temporal workflows. + + Provides access to tool confirmation and event actions for ADK integration. + """ + + tool_confirmation: ToolConfirmation | None + function_call_id: str | None + event_actions: EventActions + + def request_confirmation( + self, + *, + hint: str | None = None, + payload: Any | None = None, + ) -> None: + """Requests confirmation for the given function call. + + Args: + hint: A hint to the user on how to confirm the tool call. + payload: The payload used to confirm the tool call. + """ + if not self.function_call_id: + raise ValueError("function_call_id is not set.") + self.event_actions.requested_tool_confirmations[self.function_call_id] = ( + ToolConfirmation( + hint=hint or "", + payload=payload, + ) + ) + + +@dataclass +class _CallToolResult: + result: Any + tool_context: TemporalToolContext + + +@dataclass +class _CallToolArguments: + factory_argument: Any | None + name: str + arguments: dict[str, Any] + tool_context: TemporalToolContext + + +class TemporalMcpToolSetProvider: + """Provider for creating Temporal-aware MCP toolsets. + + .. warning:: + This class is experimental and may change in future versions. + Use with caution in production environments. + + Manages the creation of toolset activities and handles tool execution + within Temporal workflows. + + This provider is *stateless*: every ``list-tools``/``call-tool`` activity + invocation builds a fresh ``McpToolset`` via ``toolset_factory``, runs the + operation, and always closes the toolset in a ``finally`` block. This means + ``factory_argument`` is honored on every single call (no cross-workflow + connection sharing or silent mis-routing) and no MCP session or stdio + subprocess is leaked. State is not maintained across calls; if a persistent + connection is required, use :class:`TemporalStatefulMcpToolSetProvider`. + """ + + def __init__( + self, + name: str, + toolset_factory: Callable[[Any | None], McpToolset], + ) -> None: + """Initializes the toolset provider. + + Args: + name: Name prefix for the generated activities. + toolset_factory: Factory function that creates McpToolset instances. + It should return a new toolset each time so that no state is + shared between workflow runs. + """ + super().__init__() + self._name = name + self._toolset_factory = toolset_factory + + def _get_activities(self) -> Sequence[Callable]: + @activity.defn(name=self._name + "-list-tools") + async def get_tools( + args: _GetToolsArguments, + ) -> list[_ToolResult]: + # Build a fresh toolset per call, honoring this call's + # ``factory_argument``, and always close it so no MCP session + # (or stdio subprocess) leaks. See issue #1663. + toolset = self._toolset_factory(args.factory_argument) + try: + tools = await toolset.get_tools() + return [ + _ToolResult( + tool.name, + tool.description, + tool.is_long_running, + tool.custom_metadata, + tool._get_declaration(), + ) + for tool in tools + ] + finally: + await toolset.close() + + @activity.defn(name=self._name + "-call-tool") + async def call_tool( + args: _CallToolArguments, + ) -> _CallToolResult: + toolset = self._toolset_factory(args.factory_argument) + try: + tools = await toolset.get_tools() + + tool_match = [tool for tool in tools if tool.name == args.name] + if len(tool_match) == 0: + raise ApplicationError( + f"Unable to find matching mcp tool by name: {args.name}" + ) + if len(tool_match) > 1: + raise ApplicationError( + f"Found multiple MCP tools with the same name: {args.name}" + ) + tool = tool_match[0] + + # We cannot provide a full-fledged ToolContext so we need to provide only what is needed by the tool + result = await tool.run_async( + args=args.arguments, + tool_context=args.tool_context, # type:ignore + ) + return _CallToolResult(result=result, tool_context=args.tool_context) + finally: + await toolset.close() + + return get_tools, call_tool + + +class _TemporalTool(BaseTool): + def __init__( + self, + set_name: str, + factory_argument: Any | None, + config: ActivityConfig | None, + declaration: FunctionDeclaration | None, + *, + name: str, + description: str, + is_long_running: bool = False, + custom_metadata: dict[str, Any] | None = None, + ): + super().__init__( + name=name, + description=description, + is_long_running=is_long_running, + custom_metadata=custom_metadata, + ) + self._set_name = set_name + self._factory_argument = factory_argument + self._config = config or ActivityConfig( + start_to_close_timeout=timedelta(minutes=1) + ) + self._declaration = declaration + + def _get_declaration(self) -> types.FunctionDeclaration | None: + return self._declaration + + async def run_async( + self, *, args: dict[str, Any], tool_context: ToolContext + ) -> Any: + result: _CallToolResult = await workflow.execute_activity( + self._set_name + "-call-tool", + _CallToolArguments( + self._factory_argument, + self.name, + arguments=args, + tool_context=TemporalToolContext( + tool_confirmation=tool_context.tool_confirmation, + function_call_id=tool_context.function_call_id, + event_actions=tool_context._event_actions, + ), + ), + result_type=_CallToolResult, + **self._config, + ) + + # We need to propagate any event actions back to the main context + tool_context._event_actions = result.tool_context.event_actions + return result.result + + +class TemporalMcpToolSet(BaseToolset): + """Temporal-aware MCP toolset implementation. + + .. warning:: + This class is experimental and may change in future versions. + Use with caution in production environments. + + Executes MCP tools as Temporal activities, providing proper isolation + and execution guarantees within workflows. + """ + + def __init__( + self, + name: str, + config: ActivityConfig | None = None, + factory_argument: Any | None = None, + not_in_workflow_toolset: Callable[[Any | None], McpToolset] | None = None, + ): + """Initializes the Temporal MCP toolset. + + .. warning:: + Do not pass secrets, credentials, or API keys through ``factory_argument``. It + is an activity argument, so it is recorded in workflow history and, without a + payload codec, visible in the web UI. Resolve credentials worker-side inside the + toolset factory instead. + + Args: + name: Name of the toolset (used for activity naming). + config: Optional activity configuration. + factory_argument: Optional argument passed to ``toolset_factory``. + Must not contain secrets. + not_in_workflow_toolset: Optional factory that returns the + underlying ``McpToolset`` to use when this wrapper executes + outside ``workflow.in_workflow()``, such as local ADK runs. + This is not needed during normal workflow execution, but + ``get_tools()`` raises ``ValueError`` outside a workflow if it + is omitted. + """ + super().__init__() + self._name = name + self._factory_argument = factory_argument + self._config = config or ActivityConfig( + start_to_close_timeout=timedelta(minutes=1) + ) + self._not_in_workflow_toolset = not_in_workflow_toolset + + async def get_tools( + self, readonly_context: ReadonlyContext | None = None + ) -> list[BaseTool]: + """Retrieves available tools from the MCP toolset. + + Args: + readonly_context: Optional readonly context (unused in this implementation). + + Returns: + List of available tools wrapped as Temporal activities. + """ + # If executed outside a workflow, like when doing local adk runs, use the mcp server directly + if not workflow.in_workflow(): + if self._not_in_workflow_toolset is None: + raise ValueError( + "Attempted to use TemporalMcpToolSet outside a workflow, but " + "no not_in_workflow_toolset was provided. Either use " + "McpToolSet directly or pass a factory that returns the " + "underlying McpToolset for non-workflow execution." + ) + return await self._not_in_workflow_toolset(None).get_tools(readonly_context) + + tool_results: list[_ToolResult] = await workflow.execute_activity( + self._name + "-list-tools", + _GetToolsArguments(self._factory_argument), + result_type=list[_ToolResult], + **self._config, + ) + return [ + _TemporalTool( + set_name=self._name, + factory_argument=self._factory_argument, + config=self._config, + declaration=tool_result.function_declaration, + name=tool_result.name, + description=tool_result.description, + is_long_running=tool_result.is_long_running, + custom_metadata=tool_result.custom_metadata, + ) + for tool_result in tool_results + ] + + +def _handle_worker_failure(func: Callable) -> Callable: + """Surface dedicated-worker failures on the run-scoped task queue. + + A schedule-to-start timeout means the dedicated ``-server-session`` worker + never picked the activity up (it is gone); a heartbeat timeout means it + died mid-session. Either way Temporal cannot recreate the in-memory toolset + state, so we re-raise as an ``ApplicationError`` of type + ``"DedicatedWorkerFailure"`` for the caller to handle. + + Duplicated (rather than shared) from ``openai_agents._mcp`` on purpose: + these two contribs do not currently share internal code, and importing + across them would create an unwanted dependency. + """ + + @functools.wraps(func) + async def wrapper(*args: Any, **kwargs: Any): + try: + return await func(*args, **kwargs) + except ActivityError as e: + failure = e.failure + if failure: + cause = failure.cause + if cause: + if ( + cause.timeout_failure_info.timeout_type + == TIMEOUT_TYPE_SCHEDULE_TO_START + ): + raise ApplicationError( + "MCP Stateful Server Worker failed to schedule activity.", + type="DedicatedWorkerFailure", + ) from e + if ( + cause.timeout_failure_info.timeout_type + == TIMEOUT_TYPE_HEARTBEAT + ): + raise ApplicationError( + "MCP Stateful Server Worker failed to heartbeat.", + type="DedicatedWorkerFailure", + ) from e + raise e + + return wrapper + + +@dataclass +class _StatefulServerSessionArguments: + factory_argument: Any | None + + +@dataclass +class _StatefulCallToolArguments: + name: str + arguments: dict[str, Any] + tool_context: TemporalToolContext + + +class TemporalStatefulMcpToolSetProvider: + """Provider for a stateful, pooled MCP toolset backed by a dedicated worker. + + .. warning:: + This class is experimental and may change in future versions. + Use with caution in production environments. + + Unlike :class:`TemporalMcpToolSetProvider` (which builds and closes a fresh + ``McpToolset`` per activity call), this provider maintains a single + ``McpToolset`` for the lifetime of a workflow run. The workflow-side + :class:`TemporalStatefulMcpToolSet` starts a dedicated ``-server-session`` + activity on a task queue scoped to that specific run + (``name@run_id``); that activity builds the toolset once via + ``toolset_factory(factory_argument)``, holds it open, and runs a nested + :py:class:`temporalio.worker.Worker` serving the ``-list-tools``/``-call-tool`` + activities off the same run-scoped queue. The toolset is closed when the + workflow cancels the session activity on cleanup. + + Because state lives in a dedicated worker's memory, the caller must handle + the case where that worker fails, as Temporal cannot seamlessly recreate the + lost toolset. Failure surfaces as an ``ApplicationError`` with + ``type="DedicatedWorkerFailure"``. Prefer the stateless + :class:`TemporalMcpToolSetProvider` unless a persistent connection is + genuinely required. + """ + + def __init__( + self, + name: str, + toolset_factory: Callable[[Any | None], McpToolset], + ) -> None: + """Initializes the stateful toolset provider. + + Args: + name: Name prefix for the generated activities. It is suffixed with + ``-stateful`` so it never collides with a stateless provider of + the same base name registered on the same worker. + toolset_factory: Factory function that creates McpToolset instances. + It should return a new toolset each time so that no state is + shared between workflow runs. + """ + super().__init__() + self._name = name + "-stateful" + self._toolset_factory = toolset_factory + self._toolsets: dict[str, McpToolset] = {} + + @property + def name(self) -> str: + """The activity-name prefix (base name with the ``-stateful`` suffix).""" + return self._name + + def _get_activities(self) -> Sequence[Callable]: + def _server_id() -> str: + return self._name + "@" + (activity.info().workflow_run_id or "") + + @activity.defn(name=self._name + "-list-tools") + async def get_tools() -> list[_ToolResult]: + toolset = self._toolsets[_server_id()] + tools = await toolset.get_tools() + return [ + _ToolResult( + tool.name, + tool.description, + tool.is_long_running, + tool.custom_metadata, + tool._get_declaration(), + ) + for tool in tools + ] + + @activity.defn(name=self._name + "-call-tool") + async def call_tool(args: _StatefulCallToolArguments) -> _CallToolResult: + toolset = self._toolsets[_server_id()] + tools = await toolset.get_tools() + + tool_match = [tool for tool in tools if tool.name == args.name] + if len(tool_match) == 0: + raise ApplicationError( + f"Unable to find matching mcp tool by name: {args.name}" + ) + if len(tool_match) > 1: + raise ApplicationError( + f"Found multiple MCP tools with the same name: {args.name}" + ) + tool = tool_match[0] + + # We cannot provide a full-fledged ToolContext so we need to provide only what is needed by the tool + result = await tool.run_async( + args=args.arguments, + tool_context=args.tool_context, # type:ignore + ) + return _CallToolResult(result=result, tool_context=args.tool_context) + + async def heartbeat_every(delay: float, *details: Any) -> None: + """Heartbeat every ``delay`` seconds until cancelled.""" + while True: + await asyncio.sleep(delay) + activity.heartbeat(*details) + + @activity.defn(name=self._name + "-server-session") + async def connect( + args: _StatefulServerSessionArguments | None = None, + ) -> None: + server_id = self._name + "@" + (activity.info().workflow_run_id or "") + if server_id in self._toolsets: + raise ApplicationError( + "Cannot connect to an already running toolset. Use a distinct " + "name if running multiple stateful toolsets in one workflow." + ) + + # Created only once the duplicate-connect check has passed, and + # cancelled in the outermost ``finally`` below so it is torn down + # on every exit path -- including a ``toolset.close()`` failure, + # which would otherwise leave it heartbeating forever after this + # activity has already exited. + heartbeat_task = asyncio.create_task(heartbeat_every(30)) + try: + toolset = self._toolset_factory(args.factory_argument if args else None) + try: + self._toolsets[server_id] = toolset + try: + worker = Worker( + activity.client(), + task_queue=server_id, + activities=[get_tools, call_tool], + activity_task_poller_behavior=PollerBehaviorSimpleMaximum( + 1 + ), + ) + await worker.run() + finally: + await toolset.close() + finally: + del self._toolsets[server_id] + finally: + heartbeat_task.cancel() + try: + await heartbeat_task + except asyncio.CancelledError: + pass + + return (connect,) + + +class _TemporalStatefulTool(BaseTool): + def __init__( + self, + set_name: str, + config: ActivityConfig, + declaration: FunctionDeclaration | None, + *, + name: str, + description: str, + is_long_running: bool = False, + custom_metadata: dict[str, Any] | None = None, + ): + super().__init__( + name=name, + description=description, + is_long_running=is_long_running, + custom_metadata=custom_metadata, + ) + self._set_name = set_name + self._config = config + self._declaration = declaration + + def _get_declaration(self) -> types.FunctionDeclaration | None: + return self._declaration + + @_handle_worker_failure + async def run_async( + self, *, args: dict[str, Any], tool_context: ToolContext + ) -> Any: + result: _CallToolResult = await workflow.execute_activity( + self._set_name + "-call-tool", + _StatefulCallToolArguments( + self.name, + arguments=args, + tool_context=TemporalToolContext( + tool_confirmation=tool_context.tool_confirmation, + function_call_id=tool_context.function_call_id, + event_actions=tool_context._event_actions, + ), + ), + result_type=_CallToolResult, + **self._config, + ) + + # We need to propagate any event actions back to the main context + tool_context._event_actions = result.tool_context.event_actions + return result.result + + +class TemporalStatefulMcpToolSet(BaseToolset): + """Workflow-side handle for a stateful MCP toolset. + + .. warning:: + This class is experimental and may change in future versions. + Use with caution in production environments. + + Use it as an async context manager inside a workflow so the dedicated + ``-server-session`` worker is started on ``connect`` and torn down on + ``cleanup``:: + + async with TemporalStatefulMcpToolSet("my-tools") as toolset: + agent = Agent(..., tools=[toolset]) + await runner.run(...) + + The connection (and any MCP subprocess) lives for the duration of the + ``async with`` block and is reused across every tool call in that block, + then closed on exit. Callers must be prepared to handle + ``ApplicationError(type="DedicatedWorkerFailure")`` should the dedicated + worker die mid-run. + """ + + def __init__( + self, + name: str, + config: ActivityConfig | None = None, + server_session_config: ActivityConfig | None = None, + factory_argument: Any | None = None, + not_in_workflow_toolset: Callable[[Any | None], McpToolset] | None = None, + ): + """Initializes the stateful Temporal MCP toolset handle. + + Args: + name: Base name of the toolset. Must match the ``name`` passed to + the :class:`TemporalStatefulMcpToolSetProvider` (the + ``-stateful`` suffix is applied here automatically). + config: Optional activity configuration for the per-operation + (``-list-tools``/``-call-tool``) activities. A + ``schedule_to_start_timeout`` is used to detect a dead + dedicated worker. + server_session_config: Optional activity configuration for the + long-running ``-server-session`` activity. + factory_argument: Optional argument passed once to the toolset + factory when the session connects. + not_in_workflow_toolset: Optional factory that returns the + underlying ``McpToolset`` to use when this wrapper executes + outside ``workflow.in_workflow()``, such as local ADK runs. + """ + super().__init__() + self._name = name + "-stateful" + self._config = config or ActivityConfig( + start_to_close_timeout=timedelta(minutes=1), + schedule_to_start_timeout=timedelta(seconds=30), + ) + self._server_session_config = server_session_config or ActivityConfig( + start_to_close_timeout=timedelta(hours=1), + ) + self._factory_argument = factory_argument + self._not_in_workflow_toolset = not_in_workflow_toolset + self._connect_handle: ActivityHandle | None = None + + async def connect(self) -> None: + """Starts the dedicated ``-server-session`` activity for this run.""" + if not workflow.in_workflow(): + return + self._config["task_queue"] = self._name + "@" + workflow.info().run_id + self._connect_handle = workflow.start_activity( + self._name + "-server-session", + _StatefulServerSessionArguments(self._factory_argument), + **self._server_session_config, + ) + + async def cleanup(self) -> None: + """Cancels the dedicated session activity and awaits its teardown.""" + if self._connect_handle: + self._connect_handle.cancel() + try: + await self._connect_handle + except Exception as e: + if not is_cancelled_exception(e): + raise + finally: + self._connect_handle = None + + async def close(self) -> None: + """``BaseToolset`` teardown hook; delegates to :meth:`cleanup`.""" + await self.cleanup() + + async def __aenter__(self) -> "TemporalStatefulMcpToolSet": + """Connects the toolset and returns it for use as a context manager.""" + await self.connect() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Cleans up the toolset when exiting the context manager.""" + await self.cleanup() + + @_handle_worker_failure + async def get_tools( + self, readonly_context: ReadonlyContext | None = None + ) -> list[BaseTool]: + """Retrieves available tools from the stateful MCP toolset.""" + # If executed outside a workflow, like when doing local adk runs, use the mcp server directly + if not workflow.in_workflow(): + if self._not_in_workflow_toolset is None: + raise ValueError( + "Attempted to use TemporalStatefulMcpToolSet outside a " + "workflow, but no not_in_workflow_toolset was provided. " + "Either use McpToolSet directly or pass a factory that " + "returns the underlying McpToolset for non-workflow execution." + ) + return await self._not_in_workflow_toolset( + self._factory_argument + ).get_tools(readonly_context) + + if not self._connect_handle: + raise ApplicationError( + "Stateful MCP toolset not connected. Use it as an async context " + "manager (async with ...) or call connect() first." + ) + + tool_results: list[_ToolResult] = await workflow.execute_activity( + self._name + "-list-tools", + result_type=list[_ToolResult], + **self._config, + ) + return [ + _TemporalStatefulTool( + set_name=self._name, + config=self._config, + declaration=tool_result.function_declaration, + name=tool_result.name, + description=tool_result.description, + is_long_running=tool_result.is_long_running, + custom_metadata=tool_result.custom_metadata, + ) + for tool_result in tool_results + ] diff --git a/temporalio/contrib/google_adk_agents/_model.py b/temporalio/contrib/google_adk_agents/_model.py new file mode 100644 index 000000000..1992d0f4c --- /dev/null +++ b/temporalio/contrib/google_adk_agents/_model.py @@ -0,0 +1,209 @@ +from collections.abc import AsyncGenerator, Callable +from dataclasses import dataclass +from datetime import timedelta + +from google.adk.models import BaseLlm, LLMRegistry +from google.adk.models.llm_request import LlmRequest +from google.adk.models.llm_response import LlmResponse + +import temporalio.workflow +from temporalio import activity, workflow +from temporalio.contrib.workflow_streams import WorkflowStreamClient +from temporalio.exceptions import ApplicationError +from temporalio.workflow import ActivityConfig + + +@activity.defn +async def invoke_model(llm_request: LlmRequest) -> list[LlmResponse]: + """Activity that invokes an LLM model. + + Args: + llm_request: The LLM request containing model name and parameters. + + Returns: + List of LLM responses from the model. + + Raises: + ValueError: If model name is not provided or LLM creation fails. + """ + if llm_request.model is None: + raise ValueError(f"No model name provided, could not create LLM.") + + llm = LLMRegistry.new_llm(llm_request.model) + if not llm: + raise ValueError(f"Failed to create LLM for model: {llm_request.model}") + + return [ + response + async for response in llm.generate_content_async(llm_request=llm_request) + ] + + +@dataclass +class StreamingInvokeInput: + """Input for :func:`invoke_model_streaming`.""" + + llm_request: LlmRequest + streaming_topic: str + streaming_batch_interval: timedelta + + +@activity.defn +async def invoke_model_streaming( + input: StreamingInvokeInput, +) -> list[LlmResponse]: + """Streaming-aware model activity. + + .. warning:: + Streaming support is experimental and may change in future + versions. + + Calls the LLM with ``stream=True`` and returns the collected list of + raw ``LlmResponse`` chunks. The workflow's ``TemporalModel.generate_content_async`` + yields these to the caller. + + Each response is also published to the workflow's stream on + ``streaming_topic`` so external consumers (UIs, tracing, etc.) + can observe responses as they arrive. + """ + llm_request = input.llm_request + if llm_request.model is None: + raise ValueError("No model name provided, could not create LLM.") + + llm = LLMRegistry.new_llm(llm_request.model) + if not llm: + raise ValueError(f"Failed to create LLM for model: {llm_request.model}") + + responses: list[LlmResponse] = [] + + stream = WorkflowStreamClient.from_within_activity( + batch_interval=input.streaming_batch_interval, + ) + events = stream.topic(input.streaming_topic, type=LlmResponse) + async with stream: + async for response in llm.generate_content_async( + llm_request=llm_request, stream=True + ): + activity.heartbeat() + responses.append(response) + events.publish(response) + + return responses + + +class TemporalModel(BaseLlm): + """A Temporal-based LLM model that executes model invocations as activities.""" + + def __init__( + self, + model_name: str, + activity_config: ActivityConfig | None = None, + *, + summary_fn: Callable[[LlmRequest], str | None] | None = None, + streaming_topic: str | None = None, + streaming_batch_interval: timedelta = timedelta(milliseconds=100), + ) -> None: + """Initialize the TemporalModel. + + Streaming is selected by the caller via the ADK + ``generate_content_async(stream=True)`` argument; no plugin-level + flag is needed. + + Args: + model_name: The name of the model to use. + activity_config: Configuration options for the activity execution. + summary_fn: Optional callable that receives the LlmRequest and + returns a summary string (or None) for the activity. Must be + deterministic as it is called during workflow execution. If + the callable raises, the exception will propagate and fail + the workflow task. + streaming_topic: Stream topic to publish raw + ``LlmResponse`` chunks to when streaming. Required when + callers invoke ``generate_content_async(stream=True)``; + if ``None``, the streaming call raises before scheduling + an activity. The workflow must host a + :class:`temporalio.contrib.workflow_streams.WorkflowStream` + to receive the publishes; otherwise the signals are + unhandled and dropped. Streaming support is + experimental and may change in future versions. + streaming_batch_interval: Interval between automatic + flushes for the stream publisher used by the streaming + activity. Streaming support is experimental and may + change in future versions. + + Raises: + ValueError: If both ``ActivityConfig["summary"]`` and ``summary_fn`` are set. + """ + super().__init__(model=model_name) + self._model_name = model_name + self._summary_fn = summary_fn + self._streaming_topic = streaming_topic + self._streaming_batch_interval = streaming_batch_interval + self._activity_config = ActivityConfig( + start_to_close_timeout=timedelta(seconds=60) + ) + if activity_config is not None: + if summary_fn is not None and activity_config.get("summary") is not None: + raise ValueError( + "Cannot specify both ActivityConfig 'summary' and 'summary_fn'" + ) + self._activity_config.update(activity_config) + + async def generate_content_async( + self, llm_request: LlmRequest, stream: bool = False + ) -> AsyncGenerator[LlmResponse, None]: + """Generate content asynchronously by executing model invocation as a Temporal activity. + + Args: + llm_request: The LLM request containing model parameters and content. + stream: Whether to use the streaming activity. When ``True``, + each chunk is also published to ``streaming_topic`` + (if set) for external consumers. Streaming support is + experimental and may change in future versions. + + Yields: + The responses from the model. + """ + # If executed outside a workflow, like when doing local adk runs, use the model directly + if not temporalio.workflow.in_workflow(): + async for response in LLMRegistry.new_llm( + self._model_name + ).generate_content_async(llm_request, stream=stream): + yield response + return + + config = self._activity_config.copy() + if self._summary_fn is not None: + summary = self._summary_fn(llm_request) + if summary is not None: + config["summary"] = summary + elif "summary" not in config: + if llm_request.config and llm_request.config.labels: + agent_name = llm_request.config.labels.get("adk_agent_name") + if agent_name: + config["summary"] = agent_name + + if stream: + if self._streaming_topic is None: + raise ApplicationError( + "generate_content_async(stream=True) requires " + "TemporalModel(streaming_topic=...) to be set.", + non_retryable=True, + ) + responses = await workflow.execute_activity( + invoke_model_streaming, + StreamingInvokeInput( + llm_request=llm_request, + streaming_topic=self._streaming_topic, + streaming_batch_interval=self._streaming_batch_interval, + ), + **config, + ) + else: + responses = await workflow.execute_activity( + invoke_model, + args=[llm_request], + **config, + ) + for response in responses: + yield response diff --git a/temporalio/contrib/google_adk_agents/_plugin.py b/temporalio/contrib/google_adk_agents/_plugin.py new file mode 100644 index 000000000..15b6613e3 --- /dev/null +++ b/temporalio/contrib/google_adk_agents/_plugin.py @@ -0,0 +1,146 @@ +from __future__ import annotations + +import dataclasses +import time +import uuid +from collections.abc import AsyncIterator, Callable +from contextlib import asynccontextmanager +from typing import Any + +from temporalio import workflow +from temporalio.contrib.google_adk_agents._mcp import ( + TemporalMcpToolSetProvider, + TemporalStatefulMcpToolSetProvider, +) +from temporalio.contrib.google_adk_agents._model import ( + invoke_model, + invoke_model_streaming, +) +from temporalio.contrib.pydantic import ( + PydanticPayloadConverter, + ToJsonOptions, +) +from temporalio.converter import DataConverter, DefaultPayloadConverter +from temporalio.plugin import SimplePlugin +from temporalio.worker import ( + WorkflowRunner, +) +from temporalio.worker.workflow_sandbox import SandboxedWorkflowRunner + + +def setup_deterministic_runtime(): + """Configures ADK runtime for Temporal determinism. + + .. warning:: + This function is experimental and may change in future versions. + Use with caution in production environments. + + This should be called at the start of a Temporal Workflow before any ADK components + (like SessionService) are used, if they rely on runtime.get_time() or runtime.new_uuid(). + """ + try: + import google.adk.platform.time + import google.adk.platform.uuid + + # Define safer, context-aware providers + def _deterministic_time_provider() -> float: + if workflow.in_workflow(): + return workflow.now().timestamp() + return time.time() + + def _deterministic_id_provider() -> str: + if workflow.in_workflow(): + return str(workflow.uuid4()) + return str(uuid.uuid4()) + + google.adk.platform.time.set_time_provider(_deterministic_time_provider) + google.adk.platform.uuid.set_id_provider(_deterministic_id_provider) + except ImportError: + pass + except Exception as e: + print(f"Warning: Failed to set deterministic runtime providers: {e}") + + +class GoogleAdkPlugin(SimplePlugin): + """A Temporal Worker Plugin configured for ADK. + + .. warning:: + This class is experimental and may change in future versions. + Use with caution in production environments. + + This plugin configures: + - Pydantic Payload Converter (required for ADK objects). + - Sandbox Passthrough for google.adk and google.genai modules. + """ + + def __init__( + self, + toolset_providers: list[ + TemporalMcpToolSetProvider | TemporalStatefulMcpToolSetProvider + ] + | None = None, + ): + """Initializes the Temporal ADK Plugin. + + Args: + toolset_providers: Optional list of stateless + (:class:`TemporalMcpToolSetProvider`) or stateful + (:class:`TemporalStatefulMcpToolSetProvider`) toolset providers + for MCP integration. + """ + + @asynccontextmanager + async def run_context() -> AsyncIterator[None]: + setup_deterministic_runtime() + yield + + def workflow_runner(runner: WorkflowRunner | None) -> WorkflowRunner: + if not runner: + raise ValueError("No WorkflowRunner provided to the ADK plugin.") + + # If in sandbox, add additional passthrough + if isinstance(runner, SandboxedWorkflowRunner): + return dataclasses.replace( + runner, + restrictions=runner.restrictions.with_passthrough_modules( + "google.adk", "google.genai", "mcp" + ), + ) + return runner + + # Annotate as Sequence[Callable[..., Any]] because invoke_model + # and invoke_model_streaming have different signatures, so the + # inferred list type would not satisfy SimplePlugin's parameter. + new_activities: list[Callable[..., Any]] = [ + invoke_model, + invoke_model_streaming, + ] + if toolset_providers is not None: + for toolset_provider in toolset_providers: + new_activities.extend(toolset_provider._get_activities()) + + super().__init__( + name="google.AdkPlugin", + data_converter=self._configure_data_converter, + activities=new_activities, + run_context=lambda: run_context(), + workflow_runner=workflow_runner, + ) + + def _configure_data_converter( + self, converter: DataConverter | None + ) -> DataConverter: + if converter is None: + return DataConverter(payload_converter_class=_AdkPayloadConverter) + elif converter.payload_converter_class is DefaultPayloadConverter: + return dataclasses.replace( + converter, payload_converter_class=_AdkPayloadConverter + ) + return converter + + +class _AdkPayloadConverter(PydanticPayloadConverter): + """PayloadConverter for Google ADK that strips unset None fields.""" + + def __init__(self) -> None: + super().__init__(ToJsonOptions(exclude_unset=True)) diff --git a/temporalio/contrib/google_adk_agents/workflow.py b/temporalio/contrib/google_adk_agents/workflow.py new file mode 100644 index 000000000..23b254123 --- /dev/null +++ b/temporalio/contrib/google_adk_agents/workflow.py @@ -0,0 +1,251 @@ +"""Workflow utilities for Google ADK agents integration with Temporal.""" + +import functools +import inspect +import types +import typing +from dataclasses import dataclass, field +from typing import Any, Callable, cast + +import temporalio.workflow +from temporalio import workflow + +_TOOL_CONTEXT_PARAM = "tool_context" + + +@dataclass(frozen=True) +class ToolContextSnapshot: + """Serializable snapshot of the ADK ``ToolContext`` for activity-backed tools. + + .. warning:: + This class is experimental and may change in future versions. + Use with caution in production environments. + + ADK's ``ToolContext`` holds live, non-serializable objects, so it cannot + cross the activity boundary: activity inputs are sent to the server and + may run on a different worker than the workflow. This snapshot carries the + serializable subset instead. + + Declare a parameter named ``tool_context`` annotated with this type (or + ``ToolContextSnapshot | None``) on an activity wrapped by + :func:`activity_as_tool`: + + .. code-block:: python + + @activity.defn + async def get_weather(query: str, tool_context: ToolContextSnapshot) -> dict: + db_url = tool_context.state.get("url", "") + ... + + Exactly like a native ADK function tool's ``tool_context`` parameter, it + is excluded from the LLM-facing tool schema and filled in at invocation + time — here with a snapshot taken from the live ``ToolContext`` before the + activity is scheduled. + + When running under Temporal, the entire session state crosses the + activity boundary: every value in it must be serializable by the + configured data converter and the total size must fit within payload + limits, even for keys the tool never reads. A non-serializable value + fails the workflow task when the activity is scheduled. Local ADK runs + pass the snapshot in memory and have no such constraint. + + The snapshot is one-way and should be treated as read-only: mutating it + inside the activity does not propagate back to the session (and in local + runs nested values may alias the live session state, so mutating them can + corrupt the session). To modify session state, return the needed + information from the activity and apply it in workflow-side code (for + example an ADK callback or a plain tool function). + + Attributes: + state: The session state visible to this tool call, as a plain dict. + function_call_id: The id of the function call being handled, when + available. + """ + + state: dict[str, Any] = field(default_factory=dict) + function_call_id: str | None = None + + +def _annotation_members(annotation: Any) -> tuple[Any, ...]: + """Returns a union annotation's members, or the annotation itself.""" + origin = typing.get_origin(annotation) + if origin is typing.Union or origin is types.UnionType: # pyright: ignore[reportDeprecated] + return typing.get_args(annotation) + return (annotation,) + + +def _annotation_display(members: tuple[Any, ...]) -> str: + """Renders annotation members for error messages.""" + return " | ".join( + "None" if member is type(None) else getattr(member, "__name__", str(member)) + for member in members + ) + + +def _adk_context_error( + activity_def: Callable, name: str, annotation_name: str +) -> ValueError: + """Builds the error for a parameter annotated with a live ADK context.""" + return ValueError( + f"Activity '{activity_def.__name__}' declares '{name}:" + f" {annotation_name}', but ADK context objects are not serializable" + " and cannot be activity arguments. Declare a parameter named" + " 'tool_context' annotated with ToolContextSnapshot instead to receive" + " the serializable subset (session state and function-call id)." + ) + + +def _validated_tool_context_parameter( + activity_def: Callable, parameter: inspect.Parameter, members: tuple[Any, ...] +) -> inspect.Parameter: + """Returns the ``tool_context`` parameter after validating its annotation. + + The parameter must be annotated with :class:`ToolContextSnapshot` or + ``ToolContextSnapshot | None``. + """ + if parameter.annotation is inspect.Parameter.empty: + raise ValueError( + f"Activity '{activity_def.__name__}' has an unannotated" + " 'tool_context' parameter. Annotate it with ToolContextSnapshot" + " to receive the serializable subset of the ADK tool context" + " (session state and function-call id)." + ) + non_none = [member for member in members if member is not type(None)] + if non_none == [ToolContextSnapshot]: + return parameter + annotation_name = _annotation_display(members) + if any( + getattr(member, "__module__", "").startswith("google.adk") + for member in non_none + ): + raise _adk_context_error(activity_def, _TOOL_CONTEXT_PARAM, annotation_name) + raise ValueError( + f"Activity '{activity_def.__name__}' has a 'tool_context' parameter" + f" annotated with {annotation_name}. The name 'tool_context' is" + " reserved by ADK for context injection; annotate the parameter with" + " ToolContextSnapshot to receive the serializable subset of the tool" + " context." + ) + + +def _tool_context_parameter(activity_def: Callable) -> inspect.Parameter | None: + """Validates context-related parameters and returns the ``tool_context`` one. + + ADK injects the live context into the first parameter annotated with an + ADK context type (regardless of name), or failing that into one named + ``tool_context``, and excludes that parameter from the LLM-facing tool + schema. Live context objects cannot cross the activity boundary, so the + only supported declaration is a parameter named ``tool_context`` annotated + with :class:`ToolContextSnapshot` (or ``ToolContextSnapshot | None``); + anything else ADK would treat as a context parameter is rejected at wrap + time, as is a misplaced ToolContextSnapshot annotation that would leak + into the tool schema. + """ + try: + hints = typing.get_type_hints(activity_def) + except Exception: + hints = {} + adk_context: type[Any] | None + try: + from google.adk.tools.tool_context import ToolContext + + adk_context = ToolContext + except ImportError: + adk_context = None + tool_context_parameter: inspect.Parameter | None = None + for name, parameter in inspect.signature(activity_def).parameters.items(): + members = _annotation_members(hints.get(name, parameter.annotation)) + if name == _TOOL_CONTEXT_PARAM: + tool_context_parameter = _validated_tool_context_parameter( + activity_def, parameter, members + ) + elif adk_context is not None and any( + member is adk_context for member in members + ): + raise _adk_context_error(activity_def, name, _annotation_display(members)) + elif any(member is ToolContextSnapshot for member in members): + raise ValueError( + f"Activity '{activity_def.__name__}' annotates parameter" + f" '{name}' with ToolContextSnapshot, but ADK only injects the" + " tool context into a parameter named 'tool_context'; under" + " any other name it would appear in the LLM-facing tool" + " schema. Rename the parameter to 'tool_context'." + ) + return tool_context_parameter + + +def _snapshot_tool_context(tool_context: Any) -> ToolContextSnapshot: + """Builds the serializable snapshot from a live ADK ``ToolContext``.""" + state: dict[str, Any] = {} + state_object = getattr(tool_context, "state", None) + if state_object is not None: + to_dict = getattr(state_object, "to_dict", None) + state = dict(cast(Any, to_dict() if callable(to_dict) else state_object)) + return ToolContextSnapshot( + state=state, + function_call_id=getattr(tool_context, "function_call_id", None), + ) + + +def activity_as_tool(activity_def: Callable, **kwargs: Any) -> Callable: + """Decorator/Wrapper to wrap a Temporal Activity as an ADK Tool. + + .. warning:: + This function is experimental and may change in future versions. + Use with caution in production environments. + + This ensures the activity's signature is preserved for ADK's tool schema generation + while marking it as a tool that executes via 'workflow.execute_activity'. + + If the activity declares a parameter named ``tool_context``, it must be + annotated with :class:`ToolContextSnapshot` (or ``ToolContextSnapshot | + None``). ADK excludes the parameter from the tool schema and injects the + live ``ToolContext`` into the wrapper, which passes the activity a + serializable snapshot of it (session state and function-call id) in that + parameter's position. Annotating any parameter with a live ADK context + type raises ``ValueError`` at wrap time, since ADK would inject the + non-serializable context into it regardless of its name. + """ + tool_context_param = _tool_context_parameter(activity_def) + + @functools.wraps(activity_def) + async def wrapper(*args: Any, **kw: Any): + # ADK injects the live ToolContext by name; replace it with the + # serializable snapshot the activity actually declares. + if tool_context_param is not None and _TOOL_CONTEXT_PARAM in kw: + kw[_TOOL_CONTEXT_PARAM] = _snapshot_tool_context(kw[_TOOL_CONTEXT_PARAM]) + + # Inspect signature to bind arguments + sig = inspect.signature(activity_def) + bound = sig.bind(*args, **kw) + bound.apply_defaults() + + # Convert to positional args for Temporal + activity_args = list(bound.arguments.values()) + + # Decorator kwargs are defaults. + options = kwargs.copy() + + if not temporalio.workflow.in_workflow(): + # If executed outside a workflow, like when doing local adk runs, use the function directly + result = activity_def(*args, **kw) + if inspect.isawaitable(result): + return await result + else: + return result + + if not activity_args: + return await workflow.execute_activity(activity_def, **options) + if len(activity_args) == 1: + return await workflow.execute_activity( + activity_def, activity_args[0], **options + ) + return await workflow.execute_activity( + activity_def, args=activity_args, **options + ) + + # functools.wraps copies name/doc/module/annotations/qualname/dict. + # Signature must be set explicitly since the wrapper uses *args/**kw. + setattr(wrapper, "__signature__", inspect.signature(activity_def)) + + return wrapper diff --git a/temporalio/contrib/google_genai/README.md b/temporalio/contrib/google_genai/README.md new file mode 100644 index 000000000..fca5311f5 --- /dev/null +++ b/temporalio/contrib/google_genai/README.md @@ -0,0 +1,285 @@ +# Google Gemini SDK Integration for Temporal + +> ⚠️ **Experimental.** This integration may change in future versions. Use with +> caution in production. + +## Overview + +This plugin lets you use the [Google Gemini SDK](https://googleapis.github.io/python-genai/) +(`google-genai`) inside Temporal workflows with durable execution. Every Gemini +API call becomes a **Temporal activity**, so model calls, tool calls, file +operations, interactions, and managed agents are retried, recorded in history, +and survive worker restarts. + +Key properties: + +- **Credentials never enter the workflow.** The real `genai.Client` lives only + on the worker, inside activities; no API keys or tokens appear in event + history. +- **The SDK's automatic function calling (AFC) loop runs in the workflow**, so + tool wrappers (`activity_as_tool`) work naturally — no manual agent loop. +- **Temporal owns retries.** Configure them via the activity `retry_policy`; the + SDK's own retry loop is rejected to avoid double-retry (see + [Retries & errors](#retries--errors)). + +## Install + +```bash +uv add temporalio google-genai +# For client-side MCP support, also: +uv add mcp +``` + +## Hello World + +```python +import os +from datetime import timedelta + +from google import genai +from google.genai import types + +from temporalio import activity, workflow +from temporalio.client import Client +from temporalio.contrib.google_genai import ( + GoogleGenAIPlugin, + TemporalAsyncClient, + activity_as_tool, +) +from temporalio.worker import Worker +from temporalio.workflow import ActivityConfig + + +# ---- a tool, as a normal Temporal activity (runs on the worker) ---- +@activity.defn +async def get_weather(city: str) -> str: + return f"It's sunny in {city}." + + +# ---- the workflow (runs in the Temporal sandbox) ---- +@workflow.defn +class WeatherAgent: + @workflow.run + async def run(self, prompt: str) -> str: + client = TemporalAsyncClient() + response = await client.models.generate_content( + model="gemini-2.5-flash", + contents=prompt, + config=types.GenerateContentConfig( + tools=[ + activity_as_tool( + get_weather, + activity_config=ActivityConfig( + start_to_close_timeout=timedelta(seconds=30), + ), + ), + ], + ), + ) + return response.text or "" + + +# ---- worker setup (outside the sandbox: real client + credentials) ---- +async def main() -> None: + gemini = genai.Client(api_key=os.environ["GOOGLE_API_KEY"]) + plugin = GoogleGenAIPlugin(gemini) + + client = await Client.connect("localhost:7233", plugins=[plugin]) + async with Worker( + client, + task_queue="gemini", + workflows=[WeatherAgent], + activities=[get_weather], + ): + result = await client.execute_workflow( + WeatherAgent.run, + "What's the weather in Tokyo?", + id="weather-1", + task_queue="gemini", + ) + print(result) +``` + +Construct `TemporalAsyncClient` **inside** the workflow; construct the real +`genai.Client` and `GoogleGenAIPlugin` **on the worker**. + +## What this plugin gives you + +| Surface | Workflow API | Runs as | +| --- | --- | --- | +| Model calls | `client.models.generate_content` / `generate_content_stream` | activity (AFC loop in workflow) | +| Tools | `activity_as_tool(fn, ...)` | one activity per tool call | +| Files | `client.files.upload` / `download` | activity | +| File search | `client.file_search_stores.upload_to_file_search_store` | activity | +| Interactions | `client.interactions.create` / `get` / `cancel` / `delete` | whole-operation activity | +| Managed agents | `client.agents.create` / `get` / `list` / `delete` | whole-operation activity | +| MCP (client-side) | `TemporalMcpClientSession(name)` in `tools=[...]` | `list_tools` / `call_tool` activities | + +Streamed responses are batched: the activity drains the stream and the workflow +iterates the collected chunks/events. `client.webhooks` is not supported in +workflows and raises. + +## Tool calling + +`activity_as_tool` wraps any `@activity.defn` function as a Gemini tool. When the +model calls it, the AFC loop (running in the workflow) dispatches it as a +durable activity: + +```python +activity_as_tool( + get_weather, + activity_config=ActivityConfig(start_to_close_timeout=timedelta(seconds=30)), +) +``` + +A timeout is required — `activity_config` must set `start_to_close_timeout` or +`schedule_to_close_timeout` (Temporal needs one; there is no default for tools). + +## MCP support + +Client-side MCP (Gemini Developer API) is wired through the plugin: register the +server on the worker and reference it by name in the workflow. + +```python +from contextlib import asynccontextmanager +import sys + +from mcp import ClientSession, StdioServerParameters +from mcp.client.stdio import stdio_client + +from temporalio.contrib.google_genai import TemporalMcpClientSession + + +# ---- worker: a factory yielding a connected, initialized session ---- +@asynccontextmanager +async def weather_mcp(): + params = StdioServerParameters(command=sys.executable, args=["weather_server.py"]) + async with stdio_client(params) as (read, write): + async with ClientSession(read, write) as session: + await session.initialize() + yield session + + +plugin = GoogleGenAIPlugin( + genai.Client(api_key=os.environ["GOOGLE_API_KEY"]), + mcp_servers={"weather": weather_mcp}, + mcp_connection_idle_timeout=timedelta(minutes=5), +) + + +# ---- workflow: reference the server by name in the tools list ---- +@workflow.defn +class McpAgent: + @workflow.run + async def run(self, prompt: str) -> str: + client = TemporalAsyncClient() + session = TemporalMcpClientSession( + "weather", + activity_config=ActivityConfig(start_to_close_timeout=timedelta(seconds=30)), + ) + response = await client.models.generate_content( + model="gemini-2.5-flash", + contents=prompt, + config=types.GenerateContentConfig(tools=[session]), + ) + return response.text or "" +``` + +The MCP connection lives on the worker (pooled, idle-evicted); the workflow only +carries the server name. Tool discovery and calls run as `{name}-list-tools` / +`{name}-call-tool` activities, so the full tool parameter schema reaches the +model. Set `cache_tools=True` to list a server's tools once per workflow instead +of per turn. + +## Streaming + +`generate_content_stream` works as usual — the workflow iterates chunks (batched +from the activity). To let an **external** consumer (a chat UI) observe chunks in +real time while the workflow runs durably, set `streaming_topic` on the client +and host a [`WorkflowStream`](../workflow_streams/) in the workflow. Each +streamed `GenerateContentResponse` is published to that topic as it arrives: + +```python +from temporalio.contrib.workflow_streams import WorkflowStream + + +@workflow.defn +class StreamingAgent: + @workflow.init + def __init__(self, prompt: str) -> None: + self.stream = WorkflowStream() # required when streaming_topic is set + + @workflow.run + async def run(self, prompt: str) -> str: + client = TemporalAsyncClient(streaming_topic="gemini") + text = [] + async for chunk in await client.models.generate_content_stream( + model="gemini-2.5-flash", contents=prompt, + ): + text.append(chunk.text or "") + return "".join(text) +``` + +Consume the stream from outside the workflow: + +```python +from temporalio.contrib.workflow_streams import WorkflowStreamClient + + +async def consume(client, workflow_id): + stream = WorkflowStreamClient.create(client, workflow_id) + async for item in stream.subscribe( + ["gemini"], result_type=types.GenerateContentResponse, + ): + print(item.data.text, end="", flush=True) +``` + +The workflow's own iteration is unchanged (it still receives batched chunks for +the SDK to parse); the topic is purely for external real-time observation. If +`streaming_topic` is set but the workflow hosts no `WorkflowStream`, the call +raises `GoogleGenAIError`. Tune flush cadence with +`TemporalAsyncClient(streaming_topic=..., streaming_batch_interval=...)` +(default 100ms). + +## Retries & errors + +Temporal owns retries. Configure them with the activity `retry_policy` via +`activity_config`. The plugin **rejects** the SDK's own retry config so retries +don't compound: + +- Constructing the plugin with a `genai.Client` that has + `http_options.retry_options` raises `ValueError`. +- Setting `http_options.retry_options` on a per-request call raises + `GoogleGenAIError`. + +API-call activities classify failures: transient statuses (408, 429, 5xx) stay +retryable (the activity's `retry_policy` applies); other statuses (e.g. 4xx) are +non-retryable so the workflow fails fast. + +## Vertex AI + +Pass `vertexai=True` to both the worker-side `genai.Client` and the +workflow-side `TemporalAsyncClient`. On the workflow side you must also set +`project` and `location` **explicitly**: + +```python +# worker +genai.Client(vertexai=True, project="my-project", location="us-central1") + +# workflow +TemporalAsyncClient(vertexai=True, project="my-project", location="us-central1") +``` + +Normally the SDK auto-discovers `project`/`location` from the environment +(credentials, ADC, metadata server). That discovery +would be non-deterministic and break replay. Setting them by hand +keeps it deterministic. + +## Composing with other plugins + +`GoogleGenAIPlugin` is a `temporalio.plugin.SimplePlugin`; pass it in the +`plugins=[...]` list alongside others (e.g. OpenTelemetry). It contributes a +Pydantic data converter, the Gemini activities, a sandbox-passthrough config for +`google.genai` (and `mcp`), and registers `GoogleGenAIError` as a workflow +failure type. When composing data converters, construct the plugins so their +converters are compatible. diff --git a/temporalio/contrib/google_genai/__init__.py b/temporalio/contrib/google_genai/__init__.py new file mode 100644 index 000000000..8cedcbc63 --- /dev/null +++ b/temporalio/contrib/google_genai/__init__.py @@ -0,0 +1,119 @@ +"""First-class Temporal integration for the Google Gemini SDK. + +.. warning:: + This module is experimental and may change in future versions. + Use with caution in production environments. + +This integration lets you use the Gemini SDK's async client with full +automatic function calling (AFC) support. Every API call becomes a +**durable Temporal activity**. Tools default to plain workflow methods +that run deterministically in-workflow; wrap any ``@activity.defn`` with +:func:`activity_as_tool` to run a tool as a durable activity instead. + +No credentials are fetched in the workflow, and no auth material appears in +Temporal's event history. + +- :class:`GoogleGenAIPlugin` — registers the Gemini SDK activities using a + caller-provided ``genai.Client`` on the worker side. +- :class:`TemporalAsyncClient` — construct from a workflow to get an + ``AsyncClient`` that routes API calls through activities. +- :func:`activity_as_tool` — convert any ``@activity.defn`` function into a + Gemini tool callable; Gemini's AFC invokes it as a Temporal activity. + +The Interactions API (``client.interactions``) and managed agents +(``client.agents``) are supported as whole-operation activities; streamed +interactions are batched (the activity drains the SSE stream and the +workflow iterates the collected events). ``client.webhooks`` is not +supported in workflows. The Interactions API has no automatic function +calling: declare tools as ``{"type": "function", ...}`` dicts (per the +Gemini docs) and drive the tool loop yourself, executing each call via +``workflow.execute_activity`` or an :func:`activity_as_tool` callable. + +MCP is supported across three paths. Client-side MCP (Gemini Developer API) +uses :class:`TemporalMcpClientSession`: register a server with +``GoogleGenAIPlugin(mcp_servers={name: factory})`` on the worker, then place +``TemporalMcpClientSession(name)`` in a ``generate_content`` ``tools`` list — +the SDK's AFC loop drives it, with ``list_tools`` / ``call_tool`` running as +activities against a pooled worker-side connection. Server-side MCP on Vertex +AI (``Tool(mcp_servers=[McpServer(...)])``) and the Interactions API's MCP step +types are executed by Google's backend and flow through unchanged as request / +response data — no extra wiring needed. Client-side MCP requires the ``mcp`` +package. + +Streaming: set ``TemporalAsyncClient(streaming_topic=...)`` and host a +:class:`temporalio.contrib.workflow_streams.WorkflowStream` in the workflow's +``@workflow.init``. Each ``generate_content_stream`` chunk is then published to +that topic as it arrives, so external consumers can observe model output in real +time while the workflow runs durably; the workflow's own iteration is unchanged. + +Quickstart:: + + # ---- worker setup (outside the Temporal Python Sandbox) ---- + client = genai.Client(api_key=os.environ["GOOGLE_API_KEY"]) + plugin = GoogleGenAIPlugin(client) + + @activity.defn + async def get_weather(state: str) -> str: ... + + # ---- workflow (inside the Temporal Python Sandbox) ---- + @workflow.defn + class AgentWorkflow: + @workflow.run + async def run(self, query: str) -> str: + client = TemporalAsyncClient() + response = await client.models.generate_content( + model="gemini-2.5-flash", + contents=query, + config=types.GenerateContentConfig( + tools=[ + activity_as_tool( + get_weather, + activity_config=ActivityConfig( + start_to_close_timeout=timedelta(seconds=30), + ), + ), + ], + ), + ) + return response.text +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from temporalio.contrib.google_genai._errors import GoogleGenAIError +from temporalio.contrib.google_genai._google_genai_plugin import GoogleGenAIPlugin +from temporalio.contrib.google_genai._temporal_async_client import ( + TemporalAsyncClient, +) +from temporalio.contrib.google_genai.workflow import ( + activity_as_tool, +) + +if TYPE_CHECKING: + from temporalio.contrib.google_genai._temporal_mcp import TemporalMcpClientSession + +__all__ = [ + "GoogleGenAIError", + "GoogleGenAIPlugin", + "TemporalAsyncClient", + "TemporalMcpClientSession", + "activity_as_tool", +] + + +def __getattr__(name: str) -> Any: + """Lazily expose ``TemporalMcpClientSession`` without importing ``mcp`` eagerly. + + ``mcp`` is an optional dependency, so importing this package must not require + it; the import (and any resulting ``ImportError``) is deferred until the + symbol is actually accessed. + """ + if name == "TemporalMcpClientSession": + from temporalio.contrib.google_genai._temporal_mcp import ( + TemporalMcpClientSession, + ) + + return TemporalMcpClientSession + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/temporalio/contrib/google_genai/_compat.py b/temporalio/contrib/google_genai/_compat.py new file mode 100644 index 000000000..6613e2b9c --- /dev/null +++ b/temporalio/contrib/google_genai/_compat.py @@ -0,0 +1,24 @@ +"""Version-compatibility shims for the ``google-genai`` SDK. + +Single home for workarounds that keep the plugin importable and +type-checkable across the supported ``google-genai`` range; remove entries +as upstream fixes land. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + # google-genai 2.12.0 exports two ``Interaction`` names: the interaction + # resource class and a ``TypeAliasType`` over the trigger-request + # variants. At runtime we get the resource class but type checkers + # resolve to the alias which causes failures (the alias has no ``id``, + # etc.). Force the type checker to see the class' defining module (in + # the same location in 2.10 and 2.12). + from google.genai._gaos.types.interactions.interaction import Interaction +else: + # At runtime, Interaction resolves properly for 2.10 and 2.12 + from google.genai.interactions import Interaction + +__all__ = ["Interaction"] diff --git a/temporalio/contrib/google_genai/_errors.py b/temporalio/contrib/google_genai/_errors.py new file mode 100644 index 000000000..3f6595e4b --- /dev/null +++ b/temporalio/contrib/google_genai/_errors.py @@ -0,0 +1,16 @@ +"""Error types for the Google Gemini SDK Temporal integration.""" + +from __future__ import annotations + +from temporalio.exceptions import ApplicationError + + +class GoogleGenAIError(ApplicationError): + """Error raised by the Google Gemini Temporal integration. + + Registered with the worker (and replayer) via + ``workflow_failure_exception_types`` so that, when raised in workflow code, + it terminally fails the workflow execution rather than failing the workflow + task and retrying it indefinitely. Use it for conditions that cannot be + recovered by retry — e.g. a tool that is not a valid Temporal activity. + """ diff --git a/temporalio/contrib/google_genai/_gemini_activity.py b/temporalio/contrib/google_genai/_gemini_activity.py new file mode 100644 index 000000000..496bbf1ab --- /dev/null +++ b/temporalio/contrib/google_genai/_gemini_activity.py @@ -0,0 +1,356 @@ +"""Temporal activity that executes Gemini SDK API calls with real credentials. + +The ``TemporalApiClient`` in the workflow dispatches calls here. This +activity holds a user-provided ``genai.Client`` and forwards structured +requests. Credentials are fetched/refreshed only within the activity — +they never appear in workflow event history. +""" + +from __future__ import annotations + +from collections.abc import Sequence +from contextlib import AsyncExitStack +from datetime import timedelta +from typing import Any, Callable + +import google.auth.credentials +from google.genai import Client as GeminiClient +from google.genai import errors as genai_errors +from google.genai import types +from google.genai.types import HttpOptions +from google.genai.types import HttpResponse as SdkHttpResponse + +from temporalio import activity +from temporalio.contrib.google_genai._compat import Interaction +from temporalio.contrib.google_genai._models import ( + _GeminiApiRequest, + _GeminiApiResponse, + _GeminiApiStreamedResponse, + _GeminiDownloadFileRequest, + _GeminiInteractionIdRequest, + _GeminiInteractionRequest, + _GeminiInteractionStreamedResponse, + _GeminiRegisterFilesRequest, + _GeminiUploadFileRequest, + _GeminiUploadToFileSearchStoreRequest, +) +from temporalio.contrib.workflow_streams import WorkflowStreamClient +from temporalio.exceptions import ApplicationError + + +def _resolve_http_options( + overrides: Any, +) -> HttpOptions | None: + """Reconstruct ``HttpOptions`` from serializable overrides, or None.""" + if overrides is None: + return None + return HttpOptions.model_validate(overrides.model_dump(exclude_none=True)) + + +# HTTP status codes the Gemini SDK itself treats as transient/retryable. +_RETRYABLE_HTTP_STATUS = frozenset({408, 429, 500, 502, 503, 504}) + + +def _classify_api_error(err: genai_errors.APIError) -> ApplicationError: + """Map a Gemini ``APIError`` to an ``ApplicationError`` Temporal can act on. + + Transient statuses (timeouts, rate limits, 5xx) stay retryable so the + activity's retry policy applies; everything else (e.g. 4xx client errors) + is marked non-retryable so the workflow fails fast instead of retrying a + request that cannot succeed. + """ + code = getattr(err, "code", None) + retryable = code in _RETRYABLE_HTTP_STATUS + return ApplicationError( + str(err), + type=type(err).__name__, + non_retryable=not retryable, + ) + + +async def _drain_interaction_stream( + stream: Any, +) -> _GeminiInteractionStreamedResponse: + """Collect every SSE event from an interaction stream, heartbeating per event. + + ``stream`` is the SDK's async streaming response; its concrete class is not a + stable public name, so it is typed structurally — only ``async with`` / + ``async for`` / ``event.model_dump(...)`` are used. + """ + events: list[dict[str, Any]] = [] + async with stream: + async for event in stream: + activity.heartbeat() + events.append( + event.model_dump(by_alias=True, exclude_none=True, mode="json") + ) + return _GeminiInteractionStreamedResponse(events=events) + + +class GeminiApiCaller: + """Wraps a ``genai.Client`` and exposes Temporal activities for SDK calls. + + The caller owns a reference to the user-provided ``genai.Client``. + All credential management, HTTP client configuration, etc. is the + responsibility of whoever constructs the client. + """ + + def __init__( + self, + client: GeminiClient, + credentials: google.auth.credentials.Credentials | None = None, + ) -> None: + """Initialize with a genai.Client and optional extra credentials.""" + self._client = client + self._credentials = credentials + + def activities(self) -> Sequence[Callable]: + """Return activities that route SDK calls through this client.""" + + @activity.defn + async def gemini_api_client_async_request( + req: _GeminiApiRequest, + ) -> _GeminiApiResponse: + """Execute a Gemini SDK API call with real credentials.""" + try: + response: SdkHttpResponse = ( + await self._client.aio._api_client.async_request( + http_method=req.http_method, + path=req.path, + request_dict=req.request_dict, + http_options=_resolve_http_options(req.http_options_overrides), + ) + ) + except genai_errors.APIError as err: + raise _classify_api_error(err) from err + return _GeminiApiResponse( + headers=response.headers or {}, + body=response.body or "", + ) + + @activity.defn + async def gemini_api_client_async_request_streamed( + req: _GeminiApiRequest, + ) -> _GeminiApiStreamedResponse: + """Execute a streamed Gemini SDK API call, collecting all chunks. + + When ``req.streaming_topic`` is set, each chunk is also published to + that workflow-stream topic (parsed as ``GenerateContentResponse``) + as it arrives, so external consumers see the model output in real + time. Chunks are still returned batched for the SDK to parse + in-workflow; publishing is best-effort and never breaks that path. + """ + chunks: list[_GeminiApiResponse] = [] + try: + async with AsyncExitStack() as stack: + topic = None + if req.streaming_topic: + publisher = WorkflowStreamClient.from_within_activity( + batch_interval=timedelta( + milliseconds=req.streaming_batch_interval_ms + ), + ) + await stack.enter_async_context(publisher) + topic = publisher.topic( + req.streaming_topic, type=types.GenerateContentResponse + ) + + stream = await self._client.aio._api_client.async_request_streamed( + http_method=req.http_method, + path=req.path, + request_dict=req.request_dict, + http_options=_resolve_http_options(req.http_options_overrides), + ) + async for chunk in stream: + body = chunk.body or "" + chunks.append( + _GeminiApiResponse(headers=chunk.headers or {}, body=body) + ) + if topic is not None and body: + try: + topic.publish( + types.GenerateContentResponse.model_validate_json( + body + ) + ) + except Exception: + # Best-effort: a malformed/transform-needing chunk + # must not break the batched return. + pass + except genai_errors.APIError as err: + raise _classify_api_error(err) from err + return _GeminiApiStreamedResponse(chunks=chunks) + + @activity.defn + async def gemini_files_upload( + req: _GeminiUploadFileRequest, + ) -> types.File: + """Upload a file using the real genai.Client on the worker.""" + if req.file_bytes is not None: + import io + + file_arg: Any = io.BytesIO(req.file_bytes) + else: + file_arg = req.file_path + + return await self._client.aio.files.upload(file=file_arg, config=req.config) + + @activity.defn + async def gemini_files_download( + req: _GeminiDownloadFileRequest, + ) -> bytes: + """Download a file using the real genai.Client on the worker.""" + return await self._client.aio.files.download( + file=req.file, config=req.config + ) + + @activity.defn + async def gemini_files_register( + req: _GeminiRegisterFilesRequest, + ) -> types.RegisterFilesResponse: + """Register GCS files using the real genai.Client on the worker. + + Uses ``credentials`` if provided at plugin init, + otherwise falls back to the client's own credentials. + Token refresh happens here on the worker side, so no auth + material enters the workflow event history. + """ + auth = self._credentials or self._client._api_client._credentials + if auth is None: + raise ValueError( + "No credentials available for register_files(). " + "Pass extra_credentials to GoogleGenAIPlugin or initialize " + "the genai.Client with credentials." + ) + return await self._client.aio.files.register_files( + auth=auth, + uris=req.uris, + config=req.config, + ) + + @activity.defn + async def gemini_file_search_stores_upload( + req: _GeminiUploadToFileSearchStoreRequest, + ) -> types.UploadToFileSearchStoreOperation: + """Upload a file to a file search store on the worker.""" + if req.file_bytes is not None: + import io + + file_arg: Any = io.BytesIO(req.file_bytes) + else: + file_arg = req.file_path + + return ( + await self._client.aio.file_search_stores.upload_to_file_search_store( + file_search_store_name=req.file_search_store_name, + file=file_arg, + config=req.config, + ) + ) + + @activity.defn + async def gemini_interactions_create( + req: _GeminiInteractionRequest, + ) -> dict[str, Any]: + """Create an interaction using the real genai.Client on the worker.""" + interaction = await self._client.aio.interactions.create(**req.params) + assert isinstance(interaction, Interaction) + return interaction.model_dump(by_alias=True, exclude_none=True, mode="json") + + @activity.defn + async def gemini_interactions_create_streamed( + req: _GeminiInteractionRequest, + ) -> _GeminiInteractionStreamedResponse: + """Create a streamed interaction, collecting all SSE events.""" + stream = await self._client.aio.interactions.create( + stream=True, **req.params + ) + assert not isinstance(stream, Interaction) + return await _drain_interaction_stream(stream) + + @activity.defn + async def gemini_interactions_get( + req: _GeminiInteractionIdRequest, + ) -> dict[str, Any]: + """Get an interaction using the real genai.Client on the worker.""" + interaction = await self._client.aio.interactions.get(req.id, **req.params) + return interaction.model_dump(by_alias=True, exclude_none=True, mode="json") + + @activity.defn + async def gemini_interactions_get_streamed( + req: _GeminiInteractionIdRequest, + ) -> _GeminiInteractionStreamedResponse: + """Get a streamed interaction, collecting all SSE events.""" + stream = await self._client.aio.interactions.get( + req.id, stream=True, **req.params + ) + assert not isinstance(stream, Interaction) + return await _drain_interaction_stream(stream) + + @activity.defn + async def gemini_interactions_delete( + req: _GeminiInteractionIdRequest, + ) -> Any: + """Delete an interaction using the real genai.Client on the worker.""" + return await self._client.aio.interactions.delete(req.id, **req.params) + + @activity.defn + async def gemini_interactions_cancel( + req: _GeminiInteractionIdRequest, + ) -> dict[str, Any]: + """Cancel an interaction using the real genai.Client on the worker.""" + interaction = await self._client.aio.interactions.cancel( + req.id, **req.params + ) + return interaction.model_dump(by_alias=True, exclude_none=True, mode="json") + + @activity.defn + async def gemini_agents_create( + req: _GeminiInteractionRequest, + ) -> dict[str, Any]: + """Create a managed agent using the real genai.Client on the worker.""" + agent = await self._client.aio.agents.create(**req.params) + return agent.model_dump(by_alias=True, exclude_none=True, mode="json") + + @activity.defn + async def gemini_agents_list( + req: _GeminiInteractionRequest, + ) -> dict[str, Any]: + """List managed agents using the real genai.Client on the worker.""" + response = await self._client.aio.agents.list(**req.params) + return response.model_dump(by_alias=True, exclude_none=True, mode="json") + + @activity.defn + async def gemini_agents_get( + req: _GeminiInteractionIdRequest, + ) -> dict[str, Any]: + """Get a managed agent using the real genai.Client on the worker.""" + agent = await self._client.aio.agents.get(req.id, **req.params) + return agent.model_dump(by_alias=True, exclude_none=True, mode="json") + + @activity.defn + async def gemini_agents_delete( + req: _GeminiInteractionIdRequest, + ) -> dict[str, Any]: + """Delete a managed agent using the real genai.Client on the worker.""" + response = await self._client.aio.agents.delete(req.id, **req.params) + return response.model_dump(by_alias=True, exclude_none=True, mode="json") + + return [ + gemini_api_client_async_request, + gemini_api_client_async_request_streamed, + gemini_files_upload, + gemini_files_download, + gemini_files_register, + gemini_file_search_stores_upload, + gemini_interactions_create, + gemini_interactions_create_streamed, + gemini_interactions_get, + gemini_interactions_get_streamed, + gemini_interactions_delete, + gemini_interactions_cancel, + gemini_agents_create, + gemini_agents_list, + gemini_agents_get, + gemini_agents_delete, + ] diff --git a/temporalio/contrib/google_genai/_google_genai_plugin.py b/temporalio/contrib/google_genai/_google_genai_plugin.py new file mode 100644 index 000000000..f7b65b769 --- /dev/null +++ b/temporalio/contrib/google_genai/_google_genai_plugin.py @@ -0,0 +1,158 @@ +"""Temporal plugin for Google Gemini SDK integration.""" + +from __future__ import annotations + +import dataclasses +from datetime import timedelta +from typing import TYPE_CHECKING + +import google.auth.credentials +from google.genai import Client as GeminiClient + +from temporalio.contrib.google_genai._errors import GoogleGenAIError +from temporalio.contrib.google_genai._gemini_activity import GeminiApiCaller +from temporalio.contrib.pydantic import PydanticPayloadConverter +from temporalio.converter import DataConverter, DefaultPayloadConverter +from temporalio.plugin import SimplePlugin +from temporalio.worker import WorkflowRunner +from temporalio.worker.workflow_sandbox import SandboxedWorkflowRunner + +if TYPE_CHECKING: + from temporalio.contrib.google_genai._mcp import McpSessionFactory + + +_RETRY_OPTIONS_MESSAGE = ( + "genai.Client is configured with http_options.retry_options, but Temporal " + "owns retries for durable execution. Remove retry_options from the client " + "and configure retries with the activity retry_policy instead — e.g. " + "TemporalAsyncClient(activity_config=ActivityConfig(retry_policy=...)) or " + "activity_as_tool(fn, activity_config=ActivityConfig(retry_policy=...))." +) + + +def _reject_sdk_retries(client: GeminiClient) -> None: + """Raise if the client enables the SDK's own retry loop. + + Temporal must own retries so each attempt is a separate, observable activity + attempt; an SDK-internal retry loop would hide retries inside one activity + and compound with Temporal's retry policy. + """ + http_options = getattr(client._api_client, "_http_options", None) + if http_options is not None and getattr(http_options, "retry_options", None): + raise ValueError(_RETRY_OPTIONS_MESSAGE) + + +def _data_converter(converter: DataConverter | None) -> DataConverter: + if converter is None: + return DataConverter(payload_converter_class=PydanticPayloadConverter) + elif converter.payload_converter_class is DefaultPayloadConverter: + return dataclasses.replace( + converter, payload_converter_class=PydanticPayloadConverter + ) + return converter + + +class GoogleGenAIPlugin(SimplePlugin): + """A Temporal Worker Plugin configured for the Google Gemini SDK. + + .. warning:: + This class is experimental and may change in future versions. + Use with caution in production environments. + + This plugin registers the ``gemini_api_client_async_request`` activity + using the provided ``genai.Client`` with real credentials. Workflows + construct a :class:`temporalio.contrib.google_genai.TemporalAsyncClient` + to get an ``AsyncClient`` backed by a ``TemporalApiClient`` that routes all + API calls through this activity. + + No credentials are passed to or from the workflow. Auth material never + appears in Temporal's event history. + + Example (Gemini Developer API):: + + client = genai.Client(api_key=os.environ["GOOGLE_API_KEY"]) + plugin = GoogleGenAIPlugin(client) + + Example (Vertex AI):: + + client = genai.Client( + vertexai=True, project="my-project", location="us-central1", + ) + plugin = GoogleGenAIPlugin(client) + + Example (with separate GCS credentials for file registration):: + + client = genai.Client(api_key=os.environ["GOOGLE_API_KEY"]) + gcs_creds, _ = google.auth.default() + plugin = GoogleGenAIPlugin(client, extra_credentials=gcs_creds) + """ + + def __init__( + self, + client: GeminiClient, + extra_credentials: google.auth.credentials.Credentials | None = None, + mcp_servers: dict[str, McpSessionFactory] | None = None, + mcp_connection_idle_timeout: timedelta | None = None, + ) -> None: + """Initialize the Gemini plugin. + + Args: + client: A fully configured ``genai.Client`` instance. + All credential management, HTTP client configuration, etc. + is the responsibility of the caller. + extra_credentials: Optional Google Cloud credentials used for + operations that require explicit auth (e.g. + ``files.register_files()``). If not provided, the + client's own credentials are used. + mcp_servers: MCP servers to expose to workflows, keyed by name. + Each value is a factory returning an async context manager that + yields a connected, initialized ``mcp.ClientSession``. A + workflow references a server by name with + ``TemporalMcpClientSession(name)`` in a ``generate_content`` + ``tools`` list; ``list_tools`` / ``call_tool`` then run as the + ``{name}-list-tools`` / ``{name}-call-tool`` activities against a + worker-side connection. Requires the ``mcp`` package. + mcp_connection_idle_timeout: How long a worker-process MCP + connection stays open while idle before being disconnected + (the timer resets on each reuse). Defaults to 5 minutes. + """ + _reject_sdk_retries(client) + self._api_caller = GeminiApiCaller(client, credentials=extra_credentials) + + activities = list(self._api_caller.activities()) + if mcp_servers: + # Imported lazily: ``mcp`` is an optional dependency, only needed + # when MCP servers are registered. + from temporalio.contrib.google_genai._mcp import build_mcp_activities + + activities.extend( + build_mcp_activities(mcp_servers, mcp_connection_idle_timeout) + ) + + def workflow_runner(runner: WorkflowRunner | None) -> WorkflowRunner: + if not runner: + raise ValueError("No WorkflowRunner provided to GoogleGenAIPlugin.") + if isinstance(runner, SandboxedWorkflowRunner): + return dataclasses.replace( + runner, + restrictions=runner.restrictions.with_passthrough_modules( + # The SDK's request formatting + AFC loop run in-workflow + # and validate google.genai's Pydantic models; mcp is + # imported to subclass ClientSession. pydantic itself is + # in the SDK default passthrough, but its compiled core + # and Annotated helper are not, so extend them. + "google.genai", + "mcp", + "pydantic_core", + "annotated_types", + ), + ) + return runner + + super().__init__( + name="google_genai.GoogleGenAIPlugin", + data_converter=_data_converter, + activities=activities, + workflow_runner=workflow_runner, + workflow_failure_exception_types=[GoogleGenAIError], + ) diff --git a/temporalio/contrib/google_genai/_mcp.py b/temporalio/contrib/google_genai/_mcp.py new file mode 100644 index 000000000..157ce458c --- /dev/null +++ b/temporalio/contrib/google_genai/_mcp.py @@ -0,0 +1,226 @@ +"""Worker-side MCP activities and a pooled-connection subsystem. + +The Gemini SDK's automatic-function-calling loop runs *inside* the workflow, +where it would otherwise call ``McpClientSession.list_tools`` / +``call_tool`` directly (network I/O — forbidden in a workflow). The +workflow-side ``TemporalMcpClientSession`` shim redirects those two methods to +the ``{server}-list-tools`` / ``{server}-call-tool`` activities defined here, +so the real ``mcp.ClientSession`` lives only on the worker. + +A single live session per server is held open in the worker process and reused +across activity invocations, with idle eviction — modeled on the strands +plugin's ``_temporal_mcp_client``. The MCP transport and ``ClientSession`` are +anyio context managers whose cancel scope is bound to the task that enters +them, so a dedicated owner task (``_ConnectionRecord._run``) holds them open for +the connection's lifetime while concurrent activities on the same event loop +call through the shared session. +""" + +from __future__ import annotations + +import asyncio +from collections.abc import Callable +from contextlib import AbstractAsyncContextManager +from datetime import timedelta + +from mcp import ClientSession +from mcp.types import CallToolResult, ListToolsResult + +from temporalio import activity +from temporalio.contrib.google_genai._models import _McpCallToolRequest + +# A factory yields a ready-to-use (connected and ``initialize()``-d) +# ``ClientSession`` as an async context manager. Mirrors the strands +# ``mcp_clients={name: factory}`` shape; the user writes a small +# ``@asynccontextmanager`` that enters the transport, opens the session, and +# initializes it before ``yield``. +McpSessionFactory = Callable[[], AbstractAsyncContextManager[ClientSession]] + +# Default time an idle MCP connection stays open before being disconnected. +# The timer resets on every call that reuses the connection. Override per +# worker via ``GoogleGenAIPlugin(mcp_connection_idle_timeout=...)``. +_MCP_CONNECTION_IDLE = timedelta(minutes=5) + +# Server name -> live connection held open in the activity worker process. +# Activities run in the worker process, so this module state is shared across +# activity invocations on the worker. +_CONNECTIONS: dict[str, _ConnectionRecord] = {} + + +class _ConnectionRecord: + """A single MCP session held open by a dedicated owner task. + + ``_run`` enters and exits the session's context manager in the same task + for the connection's whole lifetime; ``list_tools`` / ``call_tool`` + activities on the same event loop call through the shared session (MCP + multiplexes concurrent requests by id). + """ + + def __init__( + self, + server: str, + factory: McpSessionFactory, + idle_timeout: timedelta, + ) -> None: + loop = asyncio.get_running_loop() + self._server = server + self._idle_timeout = idle_timeout + self._stop = asyncio.Event() + self._ready: asyncio.Future[ClientSession] = loop.create_future() + self._idle_handle: asyncio.TimerHandle | None = None + self._inflight = 0 + self._owner = asyncio.create_task(self._run(factory)) + + async def _run(self, factory: McpSessionFactory) -> None: + try: + async with factory() as session: + self._ready.set_result(session) + await self._stop.wait() + except BaseException as err: + # A failed connect should not be cached; drop it so the next call + # retries instead of awaiting a permanently rejected future. + if not self._ready.done(): + self._ready.set_exception(err) + _CONNECTIONS.pop(self._server, None) + raise + + def acquire(self) -> None: + """Mark a call in flight; pause idle eviction while calls are active.""" + self._inflight += 1 + if self._idle_handle is not None: + self._idle_handle.cancel() + self._idle_handle = None + + def release(self) -> None: + """Mark a call done; arm idle eviction once no calls remain in flight.""" + self._inflight -= 1 + # Only the record still cached under this server arms a timer; a record + # already evicted or never cached must not schedule one, or it could + # later evict a different, healthy connection for the same server. + if self._inflight == 0 and _CONNECTIONS.get(self._server) is self: + loop = asyncio.get_running_loop() + self._idle_handle = loop.call_later( + self._idle_timeout.total_seconds(), self._on_idle + ) + + def _on_idle(self) -> None: + asyncio.ensure_future(self._maybe_evict()) + + async def _maybe_evict(self) -> None: + # A call may have acquired the connection between the timer firing and + # this task running; only evict if it is still idle. + if self._inflight == 0: + await _evict_connection(self._server) + + async def aclose(self) -> None: + """Signal the owner task to exit its context manager and wait for it.""" + if self._idle_handle is not None: + self._idle_handle.cancel() + self._idle_handle = None + self._stop.set() + try: + await self._owner + except BaseException: + pass + + async def session(self) -> ClientSession: + """Return the live session, or raise the connect failure.""" + return await self._ready + + +async def get_connection( + server: str, factory: McpSessionFactory, idle_timeout: timedelta +) -> tuple[ClientSession, _ConnectionRecord]: + """Return the cached session for ``server``, opening one lazily if needed. + + Concurrent first-callers dedupe onto a single connect handshake by awaiting + the same record. The returned record is acquired; the caller must + ``release()`` it once the call completes so idle eviction can resume. + """ + record = _CONNECTIONS.get(server) + if record is None: + record = _ConnectionRecord(server, factory, idle_timeout) + _CONNECTIONS[server] = record + record.acquire() + try: + session = await record.session() + except BaseException: + record.release() + raise + return session, record + + +async def _evict_connection(server: str) -> None: + record = _CONNECTIONS.pop(server, None) + if record is not None: + await record.aclose() + + +def build_list_tools_activity( + server: str, + factory: McpSessionFactory, + idle_timeout: timedelta | None = None, +) -> Callable: + """Return the per-server ``{server}-list-tools`` activity for registration. + + Reuses a lazily-opened, idle-evicted worker-process MCP session. Returns + the raw ``mcp.types.ListToolsResult`` so the workflow-side shim can hand it + to the Gemini SDK exactly as a live session would (preserving the full tool + parameter schema). + """ + idle = idle_timeout if idle_timeout is not None else _MCP_CONNECTION_IDLE + + @activity.defn(name=f"{server}-list-tools") + async def list_tools() -> ListToolsResult: + session, record = await get_connection(server, factory, idle) + try: + return await session.list_tools() + except Exception: + # The session may be broken; drop it so the next call reconnects. + await _evict_connection(server) + raise + finally: + record.release() + + return list_tools + + +def build_call_tool_activity( + server: str, + factory: McpSessionFactory, + idle_timeout: timedelta | None = None, +) -> Callable: + """Return the per-server ``{server}-call-tool`` activity for registration. + + Reuses the same lazily-opened, idle-evicted worker-process MCP session as + ``{server}-list-tools``. Returns the raw ``mcp.types.CallToolResult`` — + including tool-level error results (``isError=True``), which the model is + meant to see; only transport/protocol failures raise (and evict). + """ + idle = idle_timeout if idle_timeout is not None else _MCP_CONNECTION_IDLE + + @activity.defn(name=f"{server}-call-tool") + async def call_tool(req: _McpCallToolRequest) -> CallToolResult: + session, record = await get_connection(server, factory, idle) + try: + return await session.call_tool(name=req.name, arguments=req.arguments) + except Exception: + # The session may be broken; drop it so the next call reconnects. + await _evict_connection(server) + raise + finally: + record.release() + + return call_tool + + +def build_mcp_activities( + mcp_servers: dict[str, McpSessionFactory], + idle_timeout: timedelta | None = None, +) -> list[Callable]: + """Build the list-tools and call-tool activities for every registered server.""" + activities: list[Callable] = [] + for server, factory in mcp_servers.items(): + activities.append(build_list_tools_activity(server, factory, idle_timeout)) + activities.append(build_call_tool_activity(server, factory, idle_timeout)) + return activities diff --git a/temporalio/contrib/google_genai/_models.py b/temporalio/contrib/google_genai/_models.py new file mode 100644 index 000000000..2f70d9b4d --- /dev/null +++ b/temporalio/contrib/google_genai/_models.py @@ -0,0 +1,172 @@ +"""Serializable Pydantic models for the Gemini SDK Temporal integration. + +These models cross the activity boundary — they're constructed on the +workflow side and deserialized on the activity side (or vice versa). +""" + +from __future__ import annotations + +from typing import Any + +from google.genai import types +from pydantic import BaseModel + +__all__ = [ + "_GeminiApiRequest", + "_GeminiApiResponse", + "_GeminiApiStreamedResponse", + "_GeminiDownloadFileRequest", + "_GeminiInteractionIdRequest", + "_GeminiInteractionRequest", + "_GeminiInteractionStreamedResponse", + "_GeminiRegisterFilesRequest", + "_GeminiUploadFileRequest", + "_GeminiUploadToFileSearchStoreRequest", + "_McpCallToolRequest", + "_SerializableHttpOptions", +] + + +class _SerializableHttpOptions(BaseModel): + """Per-request HTTP options that can be serialized across the activity boundary. + + Non-serializable fields (httpx_client, httpx_async_client, aiohttp_client, + client_args, async_client_args) must be configured at GoogleGenAIPlugin init. + + ``timeout`` is excluded because Temporal owns timeouts/retries — configure + via ``ActivityConfig`` instead. + """ + + base_url: str | None = None + base_url_resource_scope: str | None = None + api_version: str | None = None + headers: dict[str, str] | None = None + extra_body: dict[str, Any] | None = None + + +# ── async_request models ────────────────────────────────────────────────── + + +class _GeminiApiRequest(BaseModel): + """Serializable activity input for a Gemini SDK API call. + + ``streaming_topic`` / ``streaming_batch_interval_ms`` are only read by the + streamed activity: when a topic is set, each streamed chunk is published to + that workflow-stream topic as it arrives (in addition to being returned + batched), so external consumers can observe the model output in real time. + """ + + http_method: str + path: str + request_dict: dict[str, object] + http_options_overrides: _SerializableHttpOptions | None = None + streaming_topic: str | None = None + streaming_batch_interval_ms: int = 100 + + +class _GeminiApiResponse(BaseModel): + """Serializable activity output for a Gemini SDK API call.""" + + headers: dict[str, str] + body: str + + +class _GeminiApiStreamedResponse(BaseModel): + """Serializable activity output for a batched streamed API call. + + The activity collects all streamed chunks and returns them as a list. + The ``TemporalApiClient`` then yields them one at a time to the SDK. + """ + + chunks: list[_GeminiApiResponse] + + +# ── files upload/download models ────────────────────────────────────────── + + +class _GeminiUploadFileRequest(BaseModel): + """Serializable activity input for a file upload. + + For file path uploads the path is resolved on the worker. For + in-memory uploads the raw bytes are sent across the activity boundary. + """ + + file_bytes: bytes | None = None + file_path: str | None = None + config: types.UploadFileConfig | None = None + + +class _GeminiDownloadFileRequest(BaseModel): + """Serializable activity input for a file download.""" + + file: str + config: types.DownloadFileConfig | None = None + + +class _GeminiRegisterFilesRequest(BaseModel): + """Serializable activity input for registering GCS files.""" + + uris: list[str] + config: types.RegisterFilesConfig | None = None + + +class _GeminiUploadToFileSearchStoreRequest(BaseModel): + """Serializable activity input for uploading a file to a file search store.""" + + file_search_store_name: str + file_bytes: bytes | None = None + file_path: str | None = None + config: types.UploadToFileSearchStoreConfig | None = None + + +# ── interactions / agents models ────────────────────────────────────────── + + +class _GeminiInteractionRequest(BaseModel): + """Serializable activity input for interactions/agents calls without an id. + + ``params`` is the caller's kwargs forwarded verbatim to the real SDK + method on the worker — ``stream`` and ``timeout`` are popped by the + workflow-side shim before dispatch (``stream`` selects the activity, + ``timeout`` maps to the activity's ``start_to_close_timeout``). + """ + + params: dict[str, Any] = {} + + +class _GeminiInteractionIdRequest(BaseModel): + """Serializable activity input for id-addressed interactions/agents calls.""" + + id: str + params: dict[str, Any] = {} + + +class _GeminiInteractionStreamedResponse(BaseModel): + """Serializable activity output for a batched streamed interaction call. + + ``events`` is the verbatim sequence of ``InteractionSSEEvent`` objects + yielded by the SDK's stream, each serialized via + ``model_dump(exclude_none=True, mode="json")``. The workflow-side shim + rehydrates each entry with ``_temporal_interactions._deserialize`` so + workflow code iterates the same typed events it would get from the SDK + directly. + """ + + events: list[dict[str, Any]] = [] + + +# ── MCP models ───────────────────────────────────────────────────────────── + + +class _McpCallToolRequest(BaseModel): + """Serializable activity input for an MCP ``call_tool`` invocation. + + Carries the tool name and arguments the Gemini SDK's AFC loop selected; + the worker-side activity forwards them to the real ``mcp.ClientSession``. + The ``mcp.types.ListToolsResult`` / ``CallToolResult`` returned by the + activities are themselves Pydantic models, so they serialize directly via + the plugin's ``PydanticPayloadConverter`` and need no wrapper here. + """ + + name: str + arguments: dict[str, Any] = {} diff --git a/temporalio/contrib/google_genai/_temporal_agents.py b/temporalio/contrib/google_genai/_temporal_agents.py new file mode 100644 index 000000000..2aaac8f78 --- /dev/null +++ b/temporalio/contrib/google_genai/_temporal_agents.py @@ -0,0 +1,143 @@ +"""Temporal-aware agents resource shim. + +``TemporalAsyncAgents`` exposes the same surface as google-genai's +``AsyncClient.agents`` resource, but each operation is dispatched through a +Temporal activity holding the real ``genai.Client`` on the worker. Agents are +server-side managed agent definitions (created once, then referenced by id in +``interactions.create(agent=...)``); like the Interactions API, the resource +lives in the vendored Stainless client that bypasses ``BaseApiClient``, so each +operation is routed as a whole through an activity instead. + +The shim depends only on the public ``google.genai.interactions`` surface, not +on google-genai internals. +""" + +from __future__ import annotations + +from datetime import timedelta +from typing import Any, cast + +# These types are runtime-public (in ``google.genai.interactions.__all__``) but +# pyright's stubs don't mark them re-exported; the alternative it suggests is a +# private ``_gaos`` path, so suppress the false positive. +from google.genai.interactions import ( + Agent, # pyright: ignore[reportPrivateImportUsage] + AgentDeleteResponse, # pyright: ignore[reportPrivateImportUsage] + AgentListResponse, # pyright: ignore[reportPrivateImportUsage] +) + +from temporalio import workflow as temporal_workflow +from temporalio.contrib.google_genai._models import ( + _GeminiInteractionIdRequest, + _GeminiInteractionRequest, +) +from temporalio.contrib.google_genai._temporal_interactions import ( + _deserialize, + _pop_timeout, +) +from temporalio.workflow import ActivityConfig + + +class TemporalAsyncAgents: + """Agents resource shim that routes calls through activities. + + Methods accept the same keyword arguments as the real resource and + forward them verbatim — the SDK validates them on the worker side, so + a bad argument surfaces as an activity failure (retried per the + activity's retry policy) rather than a workflow-side error. + + ``with_raw_response`` / ``with_streaming_response`` are not supported + in workflows. + """ + + def __init__( + self, + activity_config: ActivityConfig | None = None, + ) -> None: + """Initialize with activity config for agent operation timeouts.""" + self._activity_config = ( + ActivityConfig(start_to_close_timeout=timedelta(seconds=60)) + if activity_config is None + else activity_config + ) + + def _config(self, summary: str, params: dict[str, Any]) -> ActivityConfig: + config: ActivityConfig = {**self._activity_config} + if "summary" not in config: + config["summary"] = summary + _pop_timeout(params, config) + return config + + async def create( + self, + **kwargs: Any, + ) -> Agent: + """Create a managed agent definition via a Temporal activity.""" + params = dict(kwargs) + config = self._config("agents.create", params) + raw = await temporal_workflow.execute_activity( + "gemini_agents_create", + _GeminiInteractionRequest(params=params), + result_type=dict[str, Any], + **config, + ) + return cast(Agent, _deserialize(raw, Agent)) + + async def list( + self, + **kwargs: Any, + ) -> AgentListResponse: + """List managed agent definitions via a Temporal activity.""" + params = dict(kwargs) + config = self._config("agents.list", params) + raw = await temporal_workflow.execute_activity( + "gemini_agents_list", + _GeminiInteractionRequest(params=params), + result_type=dict[str, Any], + **config, + ) + return cast(AgentListResponse, _deserialize(raw, AgentListResponse)) + + async def get( + self, + id: str, + **kwargs: Any, + ) -> Agent: + """Get a managed agent definition via a Temporal activity.""" + params = dict(kwargs) + config = self._config("agents.get", params) + raw = await temporal_workflow.execute_activity( + "gemini_agents_get", + _GeminiInteractionIdRequest(id=id, params=params), + result_type=dict[str, Any], + **config, + ) + return cast(Agent, _deserialize(raw, Agent)) + + async def delete( + self, + id: str, + **kwargs: Any, + ) -> AgentDeleteResponse: + """Delete a managed agent definition via a Temporal activity.""" + params = dict(kwargs) + config = self._config("agents.delete", params) + raw = await temporal_workflow.execute_activity( + "gemini_agents_delete", + _GeminiInteractionIdRequest(id=id, params=params), + result_type=dict[str, Any], + **config, + ) + return cast(AgentDeleteResponse, _deserialize(raw, AgentDeleteResponse)) + + @property + def with_raw_response(self) -> Any: + """Raise — raw responses are not available in workflows.""" + raise RuntimeError("with_raw_response is not supported in Temporal workflows.") + + @property + def with_streaming_response(self) -> Any: + """Raise — streaming responses are not available in workflows.""" + raise RuntimeError( + "with_streaming_response is not supported in Temporal workflows." + ) diff --git a/temporalio/contrib/google_genai/_temporal_api_client.py b/temporalio/contrib/google_genai/_temporal_api_client.py new file mode 100644 index 000000000..0763d6890 --- /dev/null +++ b/temporalio/contrib/google_genai/_temporal_api_client.py @@ -0,0 +1,319 @@ +"""Temporal-aware BaseApiClient that routes SDK calls through activities. + +This module provides ``_TemporalApiClient``, a ``BaseApiClient`` subclass +whose HTTP methods dispatch through Temporal activities instead of making +direct calls. The real ``genai.Client`` with real credentials only exists +on the worker side inside the activity. + +This ensures: + +- No credential fetching or refreshing happens in the workflow. +- No auth material (tokens, API keys) appears in Temporal event history. +- The SDK's AFC (automatic function calling) loop runs in the workflow, + so ``activity_as_tool()`` wrappers work naturally. +""" + +from __future__ import annotations + +from datetime import timedelta +from typing import Any + +from google.genai._api_client import BaseApiClient +from google.genai.types import HttpOptions, HttpOptionsOrDict +from google.genai.types import HttpResponse as SdkHttpResponse + +from temporalio import workflow as temporal_workflow +from temporalio.contrib.google_genai._errors import GoogleGenAIError +from temporalio.contrib.google_genai._models import ( + _GeminiApiRequest, + _GeminiApiResponse, + _GeminiApiStreamedResponse, + _SerializableHttpOptions, +) +from temporalio.contrib.workflow_streams._stream import _PUBLISH_SIGNAL +from temporalio.workflow import ActivityConfig + +# Fields on HttpOptions that cannot be serialized or should not be forwarded. +_REJECTED_HTTP_OPTION_FIELDS = frozenset( + { + "httpx_client", + "httpx_async_client", + "aiohttp_client", + "client_args", + "async_client_args", + } +) + + +def _validate_http_options(http_options: HttpOptions | None) -> None: + """Raise if http_options contains non-serializable fields.""" + if http_options is None: + return + bad_fields = [ + f + for f in _REJECTED_HTTP_OPTION_FIELDS + if getattr(http_options, f, None) is not None + ] + if bad_fields: + raise ValueError( + f"http_options cannot include {bad_fields}. " + f"Configure custom HTTP clients at GoogleGenAIPlugin init instead." + ) + + +class _TemporalApiClient(BaseApiClient): # pyright: ignore[reportUnusedClass] + """A ``BaseApiClient`` that routes all API calls through Temporal activities. + + This client is used on the workflow side. It does NOT initialize HTTP + clients, load credentials, or make any network calls. It only holds the + minimal configuration needed for the SDK's request formatting logic + (e.g., choosing between Vertex AI and ML Dev parameter transformations). + + All actual HTTP calls are dispatched via ``workflow.execute_activity``. + """ + + def __init__( # pyright: ignore[reportMissingSuperCall] + self, + *, + vertexai: bool = False, + project: str | None = None, + location: str | None = None, + activity_config: ActivityConfig | None = None, + streaming_topic: str | None = None, + streaming_batch_interval: timedelta = timedelta(milliseconds=100), + ) -> None: + """Initialize without calling super (no HTTP clients needed).""" + # Do NOT call super().__init__() — it creates HTTP clients, loads + # credentials, etc. We only set the properties the SDK's request + # formatting code accesses. + self.vertexai = vertexai + self.project = project + self.location = location + self.api_key: str | None = None + self.custom_base_url: str | None = None + + self._activity_config = ( + ActivityConfig(start_to_close_timeout=timedelta(seconds=60)) + if activity_config is None + else activity_config + ) + self._streaming_topic = streaming_topic + self._streaming_batch_interval = streaming_batch_interval + + def _verify_response(self, response_model: Any) -> None: + """No-op — matches the base implementation.""" + pass + + def close(self) -> None: + """No-op — no HTTP resources to close.""" + pass + + async def aclose(self) -> None: + """No-op — no HTTP resources to close.""" + pass + + def __del__(self) -> None: + """No-op — no HTTP resources to clean up.""" + pass + + @staticmethod + def _process_http_options( + http_options: HttpOptionsOrDict | None, + config: ActivityConfig, + ) -> _SerializableHttpOptions | None: + """Validate and extract serializable per-request HTTP options. + + Rejects non-serializable fields (custom HTTP clients), maps timeout + to the Temporal activity config, and returns the remaining options + for forwarding to the activity. + + Args: + http_options: Per-request options from the SDK call. + config: Mutable activity config dict — timeout is applied here. + + Returns: + Serializable options to forward, or None if nothing to forward. + """ + if http_options is None: + return None + + if isinstance(http_options, HttpOptions): + opts = http_options + else: + opts = HttpOptions.model_validate(http_options) + + _validate_http_options(opts) + + if opts.retry_options is not None: + raise GoogleGenAIError( + "Per-request http_options.retry_options is not supported in " + "Temporal workflows. Temporal owns retries; configure them with " + "the activity retry_policy via activity_config instead." + ) + + # timeout is owned by Temporal — apply it to the activity config + # rather than forwarding to the underlying HTTP client. + if opts.timeout is not None: + config["start_to_close_timeout"] = timedelta(milliseconds=opts.timeout) + + result = _SerializableHttpOptions( + base_url=opts.base_url, + base_url_resource_scope=( + opts.base_url_resource_scope.value + if opts.base_url_resource_scope + else None + ), + api_version=opts.api_version, + headers=opts.headers, + extra_body=opts.extra_body, + ) + # Only return if there are actual values set + if not result.model_dump(exclude_none=True): + return None + return result + + # ── Async (primary path for workflows) ────────────────────────────── + + async def async_request( + self, + http_method: str, + path: str, + request_dict: dict[str, object], + http_options: HttpOptionsOrDict | None = None, + ) -> SdkHttpResponse: + """Dispatch an async API request through a Temporal activity.""" + config: ActivityConfig = {**self._activity_config} + if "summary" not in config: + # Default summary is the API path (e.g. "models/gemini-2.5-flash:generateContent"). + config["summary"] = f"{http_method.upper()} {path}" + overrides = self._process_http_options(http_options, config) + + resp = await temporal_workflow.execute_activity( + "gemini_api_client_async_request", + _GeminiApiRequest( + http_method=http_method, + path=path, + request_dict=request_dict, + http_options_overrides=overrides, + ), + result_type=_GeminiApiResponse, + **config, + ) + return SdkHttpResponse(headers=resp.headers, body=resp.body) + + # ── Sync (not expected in async workflows, but raise clearly) ─────── + + def request( + self, + http_method: str, + path: str, + request_dict: dict[str, object], + http_options: HttpOptionsOrDict | None = None, + ) -> SdkHttpResponse: + """Raise — sync requests not supported in workflows.""" + raise RuntimeError( + "Synchronous requests are not supported in Temporal workflows. " + "Use TemporalAsyncClient instead." + ) + + def request_streamed( + self, + http_method: str, + path: str, + request_dict: dict[str, object], + http_options: HttpOptionsOrDict | None = None, + ) -> Any: + """Raise — sync streaming not supported in workflows.""" + raise RuntimeError( + "Synchronous streaming is not supported in Temporal workflows. " + "Use TemporalAsyncClient instead." + ) + + async def async_request_streamed( + self, + http_method: str, + path: str, + request_dict: dict[str, object], + http_options: HttpOptionsOrDict | None = None, + ) -> Any: + """Dispatch a streamed request, batching chunks in the activity. + + When a ``streaming_topic`` is configured, the activity also publishes + each chunk to that workflow-stream topic as it arrives; the workflow + must host a ``WorkflowStream`` to receive them. + """ + config: ActivityConfig = {**self._activity_config} + if "summary" not in config: + config["summary"] = f"{http_method.upper()} {path}" + overrides = self._process_http_options(http_options, config) + + if self._streaming_topic is not None: + self._require_workflow_stream() + + resp = await temporal_workflow.execute_activity( + "gemini_api_client_async_request_streamed", + _GeminiApiRequest( + http_method=http_method, + path=path, + request_dict=request_dict, + http_options_overrides=overrides, + streaming_topic=self._streaming_topic, + streaming_batch_interval_ms=int( + self._streaming_batch_interval.total_seconds() * 1000 + ), + ), + result_type=_GeminiApiStreamedResponse, + **config, + ) + + async def _yield_chunks(): + for chunk in resp.chunks: + yield SdkHttpResponse(headers=chunk.headers, body=chunk.body) + + return _yield_chunks() + + def _require_workflow_stream(self) -> None: + """Fail fast if streaming is configured but no WorkflowStream is hosted. + + Published chunks are delivered to the workflow's ``WorkflowStream`` via + a signal; without a registered handler the signals would be silently + dropped, so surface a clear error instead. + """ + if temporal_workflow.get_signal_handler(_PUBLISH_SIGNAL) is None: + raise GoogleGenAIError( + "streaming_topic is set but this workflow has no WorkflowStream. " + "Construct WorkflowStream() in the workflow's @workflow.init " + "(from temporalio.contrib.workflow_streams).", + non_retryable=True, + ) + + # ── File upload/download ───────────────────────────────────────────── + # File operations are handled at a higher level by TemporalAsyncFiles + # (in _temporal_files.py), which dispatches the entire upload/download + # as a Temporal activity using the real client on the worker side. + # These internal BaseApiClient methods are not called in that path, + # so we raise here to catch any unexpected direct usage. + + def upload_file(self, *args: Any, **kwargs: Any) -> Any: + """Raise — use client.files.upload() instead.""" + raise NotImplementedError( + "Use client.files.upload() instead of the internal upload_file() method." + ) + + async def async_upload_file(self, *args: Any, **kwargs: Any) -> Any: + """Raise — use client.files.upload() instead.""" + raise NotImplementedError( + "Use client.files.upload() instead of the internal async_upload_file() method." + ) + + def download_file(self, *args: Any, **kwargs: Any) -> Any: + """Raise — use client.files.download() instead.""" + raise NotImplementedError( + "Use client.files.download() instead of the internal download_file() method." + ) + + async def async_download_file(self, *args: Any, **kwargs: Any) -> Any: + """Raise — use client.files.download() instead.""" + raise NotImplementedError( + "Use client.files.download() instead of the internal async_download_file() method." + ) diff --git a/temporalio/contrib/google_genai/_temporal_async_client.py b/temporalio/contrib/google_genai/_temporal_async_client.py new file mode 100644 index 000000000..c77cd5cee --- /dev/null +++ b/temporalio/contrib/google_genai/_temporal_async_client.py @@ -0,0 +1,285 @@ +"""Temporal-aware ``AsyncClient``. + +``TemporalAsyncClient`` is an ``AsyncClient`` whose every Gemini API call runs +as a Temporal activity. It builds and wraps a private ``BaseApiClient`` that +dispatches HTTP through ``workflow.execute_activity`` instead of making network +calls, so the SDK's request-formatting code (including the AFC loop) runs in +the workflow while the real ``genai.Client`` with real credentials only exists +on the worker side inside the activity. + +Construct it from within a workflow:: + + client = TemporalAsyncClient(activity_config=...) + response = await client.models.generate_content(...) + +This ensures: + +- No credential fetching or refreshing happens in the workflow. +- No auth material (tokens, API keys) appears in Temporal event history. +- The SDK's AFC (automatic function calling) loop runs in the workflow, so + ``activity_as_tool()`` wrappers work naturally. + +``AsyncFiles`` and ``AsyncFileSearchStores`` are replaced with shims that run +upload/download as activities; ``interactions`` and ``agents`` — which bypass +``BaseApiClient`` via a vendored HTTP client — are likewise replaced with +activity-backed shims; ``webhooks`` is not supported in workflows and raises. + +Replay determinism +------------------ +The SDK's request-formatting and automatic-function-calling loop run *in the +workflow*, so they must be deterministic. A survey of ``google.genai`` found no +``time``/``uuid``/``random`` use on the ``generate_content``/AFC path; the SDK's +own non-deterministic code (HTTP retry backoff, the interactions/agents vendored +client, local tokenizer temp paths) runs only inside activities on the worker. +The SDK exposes no time/id provider hooks to override, and none are needed. + +The one in-workflow exception is ``batches.create`` on Vertex AI: when +``display_name``/``dest`` are omitted the SDK auto-generates them from a +timestamp + UUID (``_common.timestamped_unique_name``), which is not +replay-safe. Pass explicit ``display_name`` and ``dest`` when creating Vertex +batch jobs from a workflow. +""" + +from __future__ import annotations + +import functools +import inspect +from collections.abc import AsyncIterator, Callable +from datetime import timedelta +from typing import Any, NoReturn, cast + +from google.genai import types +from google.genai.client import AsyncClient +from google.genai.models import AsyncModels + +from temporalio.contrib.google_genai._temporal_agents import ( + TemporalAsyncAgents, +) +from temporalio.contrib.google_genai._temporal_api_client import ( + _TemporalApiClient, +) +from temporalio.contrib.google_genai._temporal_file_search_stores import ( + TemporalAsyncFileSearchStores, +) +from temporalio.contrib.google_genai._temporal_files import ( + TemporalAsyncFiles, +) +from temporalio.contrib.google_genai._temporal_interactions import ( + TemporalAsyncInteractions, +) +from temporalio.workflow import ActivityConfig + + +def _closure_if_bound_method(tool: object) -> object: + """Return a plain-function wrapper for a bound method, else ``tool`` as-is. + + google-genai >= 2.8.0 deep-copies the request config internally + (``config.model_copy(deep=True)``). ``copy.deepcopy`` clones a bound + method's ``__self__``, so a workflow-method tool would run against a throwaway + clone of the workflow instance and its in-workflow state mutation would be + lost. ``deepcopy`` leaves plain functions untouched, so wrapping the method + in a closure keeps the tool bound to the real instance. Plain functions and + ``activity_as_tool`` wrappers are already closures and pass through unchanged. + """ + if not inspect.ismethod(tool): + return tool + method: Callable = tool + if inspect.iscoroutinefunction(method): + + @functools.wraps(method) + async def async_wrapper(*args: object, **kwargs: object) -> object: + return await method(*args, **kwargs) + + return async_wrapper + + @functools.wraps(method) + def sync_wrapper(*args: object, **kwargs: object) -> object: + return method(*args, **kwargs) + + return sync_wrapper + + +def _wrap_bound_method_tools( + config: types.GenerateContentConfigOrDict | None, +) -> types.GenerateContentConfigOrDict | None: + """Closure-wrap any bound-method tools in ``config`` (see :func:`_closure_if_bound_method`). + + Returns ``config`` unchanged when it holds no bound-method tools; otherwise + returns a shallow copy with the tools list rewritten, so the caller's config + is never mutated. + """ + if not config: + return config + if isinstance(config, dict): + tools = config.get("tools") + if not tools or not any(inspect.ismethod(t) for t in tools): + return config + updated: Any = { + **config, + "tools": [_closure_if_bound_method(t) for t in tools], + } + return cast(types.GenerateContentConfigDict, updated) + if not config.tools or not any(inspect.ismethod(t) for t in config.tools): + return config + return config.model_copy( + update={"tools": [_closure_if_bound_method(t) for t in config.tools]} + ) + + +class _TemporalAsyncModels(AsyncModels): + """``AsyncModels`` that closure-wraps bound-method tools before each call. + + This shields workflow-method tools from google-genai's internal deep-copy of + the config (>= 2.8.0), which would otherwise clone the workflow instance and + silently drop the tool's in-workflow state mutations. + """ + + async def generate_content( # type: ignore[override] + self, + *, + model: str, + contents: types.ContentListUnion | types.ContentListUnionDict, + config: types.GenerateContentConfigOrDict | None = None, + ) -> types.GenerateContentResponse: + return await super().generate_content( + model=model, + contents=contents, + config=_wrap_bound_method_tools(config), + ) + + async def generate_content_stream( # type: ignore[override] + self, + *, + model: str, + contents: types.ContentListUnion | types.ContentListUnionDict, + config: types.GenerateContentConfigOrDict | None = None, + ) -> AsyncIterator[types.GenerateContentResponse]: + return await super().generate_content_stream( + model=model, + contents=contents, + config=_wrap_bound_method_tools(config), + ) + + +class TemporalAsyncClient(AsyncClient): + """An ``AsyncClient`` whose API calls run as Temporal activities. + + .. warning:: + This API is experimental and may change in future versions. + Use with caution in production environments. + + Builds a private ``BaseApiClient`` that dispatches HTTP calls through + ``workflow.execute_activity`` and wraps it, so the SDK's request-formatting + code (including the AFC loop) runs in the workflow while only the actual + API calls cross into activities. Credentials are never fetched or stored + in the workflow — the activity worker handles authentication independently. + + ``AsyncFiles`` and ``AsyncFileSearchStores`` are replaced with shims that + run upload/download as activities; ``interactions`` and ``agents`` — which + bypass ``BaseApiClient`` via a vendored HTTP client — are likewise replaced + with activity-backed shims; ``webhooks`` is not supported in workflows and + raises. Other modules (models, tunings, caches, batches, live, tokens, + operations) are inherited unchanged and work through the private api + client's activity-backed HTTP methods. + + Construct it from within a workflow ``run`` method: + + .. code-block:: python + + @workflow.defn + class MyWorkflow: + @workflow.run + async def run(self, query: str) -> str: + client = TemporalAsyncClient() + response = await client.models.generate_content( + model="gemini-2.0-flash", + contents=query, + config=GenerateContentConfig( + tools=[ + activity_as_tool( + my_tool, + activity_config=ActivityConfig( + start_to_close_timeout=timedelta(seconds=30), + ), + ), + ], + ), + ) + return response.text + """ + + def __init__( + self, + *, + vertexai: bool = False, + project: str | None = None, + location: str | None = None, + activity_config: ActivityConfig | None = None, + streaming_topic: str | None = None, + streaming_batch_interval: timedelta = timedelta(milliseconds=100), + ) -> None: + """Initialize a Temporal-aware client. + + Args: + vertexai: Whether to use Vertex AI API endpoints. Must match the + ``GoogleGenAIPlugin`` configuration on the worker side. + Defaults to ``False`` (Gemini Developer API). + project: Google Cloud project ID. Only needed when + ``vertexai=True`` and the SDK's request formatting requires it + (e.g., cache operations). + location: Google Cloud location. Same conditions as ``project``. + activity_config: Override the default activity configuration + (timeouts, retry policy, etc.) for Gemini API call activities. + When not provided, every operation (model calls, files, + interactions, managed agents) defaults to a 60-second + ``start_to_close_timeout`` and Temporal's default retry policy. + streaming_topic: When set, ``generate_content_stream`` publishes each + streamed ``GenerateContentResponse`` to this + :class:`temporalio.contrib.workflow_streams.WorkflowStream` + topic as it arrives, so external consumers can observe the model + output in real time. The workflow must construct a + ``WorkflowStream`` in ``@workflow.init``; otherwise the call + raises. The workflow's own stream iteration is unchanged. + streaming_batch_interval: How often the streaming activity flushes + published chunks to the workflow stream. Defaults to 100ms. + """ + api_client = _TemporalApiClient( + vertexai=vertexai, + project=project, + location=location, + activity_config=activity_config, + streaming_topic=streaming_topic, + streaming_batch_interval=streaming_batch_interval, + ) + super().__init__(api_client) + # Closure-wrap bound-method tools so google-genai's internal + # config deep-copy (>= 2.8.0) can't clone the workflow instance. + self._models = _TemporalAsyncModels(api_client) + self._files = TemporalAsyncFiles(api_client, activity_config) + self._file_search_stores = TemporalAsyncFileSearchStores( + api_client, activity_config + ) + self._temporal_interactions = TemporalAsyncInteractions(activity_config) + self._temporal_agents = TemporalAsyncAgents(activity_config) + + @property + def interactions( # type: ignore[override] + self, + ) -> TemporalAsyncInteractions: # pyright: ignore[reportIncompatibleMethodOverride] + """Temporal-aware interactions resource; operations run as activities.""" + return self._temporal_interactions + + @property + def agents( # type: ignore[override] + self, + ) -> TemporalAsyncAgents: # pyright: ignore[reportIncompatibleMethodOverride] + """Temporal-aware agents resource; operations run as activities.""" + return self._temporal_agents + + @property + def webhooks(self) -> NoReturn: # pyright: ignore[reportIncompatibleMethodOverride] + """Raise — webhooks are not supported in Temporal workflows.""" + raise RuntimeError( + "client.webhooks is not supported in Temporal workflows. " + "Manage webhooks outside the workflow with a regular genai.Client." + ) diff --git a/temporalio/contrib/google_genai/_temporal_file_search_stores.py b/temporalio/contrib/google_genai/_temporal_file_search_stores.py new file mode 100644 index 000000000..7b7e3bbc8 --- /dev/null +++ b/temporalio/contrib/google_genai/_temporal_file_search_stores.py @@ -0,0 +1,109 @@ +"""Temporal-aware AsyncFileSearchStores shim. + +``TemporalAsyncFileSearchStores`` is an ``AsyncFileSearchStores`` subclass +whose ``upload_to_file_search_store`` method dispatches through a Temporal +activity so the entire upload (including filesystem access and resumable +upload negotiation) runs on the activity worker. +""" + +from __future__ import annotations + +import io +import os +from datetime import timedelta + +from google.genai import types +from google.genai.file_search_stores import AsyncFileSearchStores + +from temporalio import workflow as temporal_workflow +from temporalio.contrib.google_genai._models import ( + _GeminiUploadToFileSearchStoreRequest, +) +from temporalio.contrib.google_genai._temporal_api_client import ( + _TemporalApiClient, + _validate_http_options, +) +from temporalio.workflow import ActivityConfig + + +class TemporalAsyncFileSearchStores(AsyncFileSearchStores): + """``AsyncFileSearchStores`` subclass that routes ``upload_to_file_search_store`` through an activity. + + The entire upload operation — including filesystem access, resumable + upload negotiation, and chunked transfer — runs inside a Temporal + activity on the worker. All other methods (``create``, ``get``, + ``delete``, ``list``, ``import_file``, ``documents``) are inherited + and already work through the ``_TemporalApiClient``'s ``async_request`` + activity. + """ + + def __init__( + self, + api_client: _TemporalApiClient, + activity_config: ActivityConfig | None = None, + ) -> None: + """Initialize with activity config for upload timeouts.""" + super().__init__(api_client) + self._activity_config = ( + ActivityConfig(start_to_close_timeout=timedelta(seconds=60)) + if activity_config is None + else activity_config + ) + + async def upload_to_file_search_store( + self, + *, + file_search_store_name: str, + file: str | os.PathLike[str] | io.IOBase, + config: types.UploadToFileSearchStoreConfigOrDict | None = None, + ) -> types.UploadToFileSearchStoreOperation: + """Upload a file to a file search store via a Temporal activity. + + Accepts a file path (resolved on the worker), ``os.PathLike``, or + an ``io.IOBase`` (bytes sent across the activity boundary). + """ + act_config: ActivityConfig = {**self._activity_config} + if "summary" not in act_config: + act_config["summary"] = "file_search_stores.upload" + + upload_config = None + if config is not None: + if isinstance(config, dict): + upload_config = types.UploadToFileSearchStoreConfig.model_validate( + config + ) + else: + upload_config = config + _validate_http_options(upload_config.http_options) + + if isinstance(file, io.IOBase): + file_bytes = file.read() + if not isinstance(file_bytes, bytes): + raise TypeError( + "file must be a binary stream when passing an io.IOBase; " + f"file.read() must return bytes (got {type(file_bytes).__name__})" + ) + req = _GeminiUploadToFileSearchStoreRequest( + file_search_store_name=file_search_store_name, + file_bytes=file_bytes, + config=upload_config, + ) + elif isinstance(file, str): + req = _GeminiUploadToFileSearchStoreRequest( + file_search_store_name=file_search_store_name, + file_path=file, + config=upload_config, + ) + else: + req = _GeminiUploadToFileSearchStoreRequest( + file_search_store_name=file_search_store_name, + file_path=file.__fspath__(), + config=upload_config, + ) + + return await temporal_workflow.execute_activity( + "gemini_file_search_stores_upload", + req, + result_type=types.UploadToFileSearchStoreOperation, + **act_config, + ) diff --git a/temporalio/contrib/google_genai/_temporal_files.py b/temporalio/contrib/google_genai/_temporal_files.py new file mode 100644 index 000000000..f785c00cb --- /dev/null +++ b/temporalio/contrib/google_genai/_temporal_files.py @@ -0,0 +1,169 @@ +"""Temporal-aware AsyncFiles shim. + +``TemporalAsyncFiles`` is an ``AsyncFiles`` subclass whose ``upload`` +and ``download`` methods dispatch through Temporal activities so the +entire file operation (including filesystem access) runs on the +activity worker. +""" + +from __future__ import annotations + +import io +import os +from datetime import timedelta +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import google.auth.credentials +from google.genai import types +from google.genai.files import AsyncFiles + +from temporalio import workflow as temporal_workflow +from temporalio.contrib.google_genai._models import ( + _GeminiDownloadFileRequest, + _GeminiRegisterFilesRequest, + _GeminiUploadFileRequest, +) +from temporalio.contrib.google_genai._temporal_api_client import ( + _TemporalApiClient, + _validate_http_options, +) +from temporalio.workflow import ActivityConfig + + +class TemporalAsyncFiles(AsyncFiles): + """``AsyncFiles`` subclass that routes ``upload`` and ``download`` through activities. + + The entire file operation — including filesystem access, resumable + upload negotiation, and chunked transfer — runs inside a Temporal + activity on the worker. ``get``, ``delete``, and ``list`` are + inherited from ``AsyncFiles`` and already work through the + ``_TemporalApiClient``'s ``async_request`` activity. + """ + + def __init__( + self, + api_client: _TemporalApiClient, + activity_config: ActivityConfig | None = None, + ) -> None: + """Initialize with activity config for file operation timeouts.""" + super().__init__(api_client) + self._activity_config = ( + ActivityConfig(start_to_close_timeout=timedelta(seconds=60)) + if activity_config is None + else activity_config + ) + + async def upload( + self, + *, + file: str | os.PathLike[str] | io.IOBase, + config: types.UploadFileConfigOrDict | None = None, + ) -> types.File: + """Upload a file via a Temporal activity. + + Accepts a file path (resolved on the worker), ``os.PathLike``, or + an ``io.IOBase`` (bytes sent across the activity boundary). + """ + act_config: ActivityConfig = {**self._activity_config} + if "summary" not in act_config: + act_config["summary"] = "files.upload" + + upload_config = None + if config is not None: + if isinstance(config, dict): + upload_config = types.UploadFileConfig.model_validate(config) + else: + upload_config = config + _validate_http_options(upload_config.http_options) + + if isinstance(file, io.IOBase): + file_bytes = file.read() + if not isinstance(file_bytes, bytes): + raise TypeError( + "file must be a binary stream when passing an io.IOBase; " + f"file.read() must return bytes (got {type(file_bytes).__name__})" + ) + req = _GeminiUploadFileRequest(file_bytes=file_bytes, config=upload_config) + elif isinstance(file, str): + req = _GeminiUploadFileRequest(file_path=file, config=upload_config) + else: + # os.PathLike — convert via __fspath__() to avoid importing os + req = _GeminiUploadFileRequest( + file_path=file.__fspath__(), config=upload_config + ) + + return await temporal_workflow.execute_activity( + "gemini_files_upload", + req, + result_type=types.File, + **act_config, + ) + + async def download( + self, + *, + file: str | types.File, + config: types.DownloadFileConfigOrDict | None = None, + ) -> bytes: + """Download a file via a Temporal activity.""" + act_config: ActivityConfig = {**self._activity_config} + if "summary" not in act_config: + act_config["summary"] = "files.download" + + download_config = None + if config is not None: + if isinstance(config, dict): + download_config = types.DownloadFileConfig.model_validate(config) + else: + download_config = config + _validate_http_options(download_config.http_options) + + if isinstance(file, types.File): + if not file.name: + raise ValueError("File object must have a name to download.") + file_name = file.name + else: + file_name = file + + return await temporal_workflow.execute_activity( + "gemini_files_download", + _GeminiDownloadFileRequest(file=file_name, config=download_config), + result_type=bytes, + **act_config, + ) + + async def register_files( + self, + *, + auth: google.auth.credentials.Credentials, + uris: list[str], + config: types.RegisterFilesConfigOrDict | None = None, + ) -> types.RegisterFilesResponse: + """Register GCS files via a Temporal activity. + + .. note:: + The ``auth`` parameter is **ignored**. The activity uses + ``credentials`` if provided to ``GoogleGenAIPlugin``, + otherwise falls back to the ``genai.Client``'s own credentials. + Either way, those credentials must have access to the GCS URIs + being registered. + """ + act_config: ActivityConfig = {**self._activity_config} + if "summary" not in act_config: + act_config["summary"] = "files.register_files" + + register_config = None + if config is not None: + if isinstance(config, dict): + register_config = types.RegisterFilesConfig.model_validate(config) + else: + register_config = config + _validate_http_options(register_config.http_options) + + return await temporal_workflow.execute_activity( + "gemini_files_register", + _GeminiRegisterFilesRequest(uris=uris, config=register_config), + result_type=types.RegisterFilesResponse, + **act_config, + ) diff --git a/temporalio/contrib/google_genai/_temporal_interactions.py b/temporalio/contrib/google_genai/_temporal_interactions.py new file mode 100644 index 000000000..54d8353cd --- /dev/null +++ b/temporalio/contrib/google_genai/_temporal_interactions.py @@ -0,0 +1,258 @@ +"""Temporal-aware interactions resource shim. + +``TemporalAsyncInteractions`` exposes the same surface as google-genai's +``AsyncClient.interactions`` resource, but each operation is dispatched through +a Temporal activity holding the real ``genai.Client`` on the worker. The +Interactions API does not go through ``BaseApiClient`` — it uses a vendored, +Stainless-generated HTTP client that the ``TemporalApiClient`` shim never sees — +so each operation is routed as a whole through an activity instead. + +The shim depends only on the public ``google.genai.interactions`` surface (types +plus ``client.aio.interactions`` on the worker), not on google-genai internals, +so it is unaffected by regeneration of the vendored client. +""" + +from __future__ import annotations + +from collections.abc import AsyncIterator +from datetime import timedelta +from types import TracebackType +from typing import Any, cast + +import pydantic +from google.genai.interactions import InteractionSSEEvent + +from temporalio import workflow as temporal_workflow +from temporalio.contrib.google_genai._compat import Interaction +from temporalio.contrib.google_genai._models import ( + _GeminiInteractionIdRequest, + _GeminiInteractionRequest, + _GeminiInteractionStreamedResponse, +) +from temporalio.workflow import ActivityConfig + +_DEFAULT_INTERACTION_TIMEOUT = timedelta(seconds=60) + +# ``InteractionSSEEvent`` is a discriminated union (on ``event_type``); a +# ``TypeAdapter`` dispatches it — including nested unions such as a +# ``step.start`` event's ``step`` — leniently. +_SSE_EVENT_ADAPTER: pydantic.TypeAdapter[Any] = pydantic.TypeAdapter( + InteractionSSEEvent +) + + +def _deserialize(value: dict[str, Any], type_: Any) -> Any: + """Rehydrate a dict returned by an activity into its public genai model. + + ``InteractionSSEEvent`` is deserialized through its ``TypeAdapter``, which + dispatches the discriminated union (and nested unions) and tolerates the + sparse nested payloads the API legitimately emits (e.g. an + ``interaction.created`` event carrying an ``Interaction`` with just ``id`` + and ``object``). Plain models use ``model_validate``, which recurses nested + models (e.g. ``AgentListResponse.agents`` into ``Agent``) and resolves + aliases; the SDK's optional fields keep it tolerant of the minimal objects + the API returns. Both paths are pure functions, safe to run in the workflow + on every replay. + """ + if type_ is InteractionSSEEvent: + return _SSE_EVENT_ADAPTER.validate_python(value) + return type_.model_validate(value) + + +def _pop_timeout(params: dict[str, Any], config: ActivityConfig) -> None: + """Pop a per-call ``timeout`` kwarg and apply it to the activity config. + + The Interactions API expresses timeouts in seconds. Temporal owns + timeouts/retries, so the value maps to ``start_to_close_timeout`` + rather than being forwarded to the underlying HTTP client. + """ + timeout = params.pop("timeout", None) + if timeout is None: + return + if not isinstance(timeout, (int, float)) or isinstance(timeout, bool): + raise ValueError( + "timeout must be numeric seconds when calling the Interactions " + "API from a workflow; configure anything more granular via " + "activity_config instead." + ) + config["start_to_close_timeout"] = timedelta(seconds=timeout) + + +class _TemporalInteractionAsyncStream: + """Async stream over interaction events already drained in an activity. + + Presents the same ``async for`` / ``async with`` / ``close()`` surface as + the SDK's streaming response, but iterates an in-memory event list drained + inside the activity (there is no httpx response or client on the workflow + side), rehydrating each event back into its typed form on iteration. + """ + + def __init__( + self, + events: list[dict[str, Any]], + ) -> None: + self._events = events + + def __aiter__(self) -> AsyncIterator[InteractionSSEEvent]: + return self._iter() + + async def _iter(self) -> AsyncIterator[InteractionSSEEvent]: + for event in self._events: + yield cast(InteractionSSEEvent, _deserialize(event, InteractionSSEEvent)) + + async def __aenter__(self) -> _TemporalInteractionAsyncStream: + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + pass + + async def close(self) -> None: + """No-op — the upstream stream was drained inside the activity.""" + pass + + +class TemporalAsyncInteractions: + """Interactions resource shim that routes calls through activities. + + Methods accept the same keyword arguments as the real resource and + forward them verbatim — the SDK validates them on the worker side, so + a bad argument surfaces as an activity failure (retried per the + activity's retry policy) rather than a workflow-side error. + + ``with_raw_response`` / ``with_streaming_response`` are not supported + in workflows. + """ + + def __init__( + self, + activity_config: ActivityConfig | None = None, + ) -> None: + """Initialize with activity config for interaction operation timeouts.""" + self._activity_config = ( + ActivityConfig(start_to_close_timeout=_DEFAULT_INTERACTION_TIMEOUT) + if activity_config is None + else activity_config + ) + + def _config(self, summary: str, params: dict[str, Any]) -> ActivityConfig: + config: ActivityConfig = {**self._activity_config} + if "summary" not in config: + config["summary"] = summary + _pop_timeout(params, config) + return config + + async def create( + self, + *, + stream: bool = False, + **kwargs: Any, + ) -> Interaction | _TemporalInteractionAsyncStream: + """Create an interaction via a Temporal activity. + + ``kwargs`` is forwarded verbatim to ``client.aio.interactions.create`` + on the worker. With ``stream=True`` the activity drains the SSE + stream and returns all events batched; the returned object supports + ``async for`` / ``async with`` like the SDK's streaming response. + """ + params = dict(kwargs) + config = self._config( + "interactions.create (stream)" if stream else "interactions.create", + params, + ) + req = _GeminiInteractionRequest(params=params) + if stream: + resp = await temporal_workflow.execute_activity( + "gemini_interactions_create_streamed", + req, + result_type=_GeminiInteractionStreamedResponse, + **config, + ) + return _TemporalInteractionAsyncStream(resp.events) + raw = await temporal_workflow.execute_activity( + "gemini_interactions_create", + req, + result_type=dict[str, Any], + **config, + ) + return cast(Interaction, _deserialize(raw, Interaction)) + + async def get( + self, + id: str, + *, + stream: bool = False, + **kwargs: Any, + ) -> Interaction | _TemporalInteractionAsyncStream: + """Get an interaction via a Temporal activity. + + Supports ``stream=True`` (with the SDK's ``last_event_id`` kwarg + for resumption); events come back batched like :meth:`create`. + """ + params = dict(kwargs) + config = self._config( + "interactions.get (stream)" if stream else "interactions.get", + params, + ) + req = _GeminiInteractionIdRequest(id=id, params=params) + if stream: + resp = await temporal_workflow.execute_activity( + "gemini_interactions_get_streamed", + req, + result_type=_GeminiInteractionStreamedResponse, + **config, + ) + return _TemporalInteractionAsyncStream(resp.events) + raw = await temporal_workflow.execute_activity( + "gemini_interactions_get", + req, + result_type=dict[str, Any], + **config, + ) + return cast(Interaction, _deserialize(raw, Interaction)) + + async def delete( + self, + id: str, + **kwargs: Any, + ) -> object: + """Delete an interaction via a Temporal activity.""" + params = dict(kwargs) + config = self._config("interactions.delete", params) + return await temporal_workflow.execute_activity( + "gemini_interactions_delete", + _GeminiInteractionIdRequest(id=id, params=params), + **config, + ) + + async def cancel( + self, + id: str, + **kwargs: Any, + ) -> Interaction: + """Cancel an interaction via a Temporal activity.""" + params = dict(kwargs) + config = self._config("interactions.cancel", params) + raw = await temporal_workflow.execute_activity( + "gemini_interactions_cancel", + _GeminiInteractionIdRequest(id=id, params=params), + result_type=dict[str, Any], + **config, + ) + return cast(Interaction, _deserialize(raw, Interaction)) + + @property + def with_raw_response(self) -> Any: + """Raise — raw responses are not available in workflows.""" + raise RuntimeError("with_raw_response is not supported in Temporal workflows.") + + @property + def with_streaming_response(self) -> Any: + """Raise — streaming responses are not available in workflows.""" + raise RuntimeError( + "with_streaming_response is not supported in Temporal workflows." + ) diff --git a/temporalio/contrib/google_genai/_temporal_mcp.py b/temporalio/contrib/google_genai/_temporal_mcp.py new file mode 100644 index 000000000..718c6aaa3 --- /dev/null +++ b/temporalio/contrib/google_genai/_temporal_mcp.py @@ -0,0 +1,116 @@ +"""Temporal-aware ``mcp.ClientSession`` shim. + +``TemporalMcpClientSession`` is an ``mcp.ClientSession`` subclass that the user +places in ``generate_content(config=GenerateContentConfig(tools=[...]))`` just +like a real MCP session. The Gemini SDK recognizes it via +``isinstance(tool, McpClientSession)`` and, inside ``generate_content`` (which +runs in the workflow), calls only two methods on it: ``list_tools()`` at tool +discovery and ``call_tool(name, arguments)`` in the automatic-function-calling +loop. Both are overridden here to dispatch to the ``{server}-list-tools`` / +``{server}-call-tool`` activities, so the real ``mcp.ClientSession`` lives only +on the worker (registered via ``GoogleGenAIPlugin(mcp_servers=...)``). + +This mirrors strands' ``TemporalMCPClient``: the handle carries only the server +name (which selects the worker-side factory) plus activity options; the +connection factory is never passed to the workflow or the root client. +""" + +from __future__ import annotations + +from datetime import timedelta +from typing import Any + +from mcp import ClientSession +from mcp.shared.session import ProgressFnT +from mcp.types import CallToolResult, ListToolsResult, PaginatedRequestParams + +from temporalio import workflow as temporal_workflow +from temporalio.contrib.google_genai._models import _McpCallToolRequest +from temporalio.workflow import ActivityConfig + +_DEFAULT_MCP_TIMEOUT = timedelta(seconds=60) + + +class TemporalMcpClientSession(ClientSession): + """``mcp.ClientSession`` whose tool discovery and calls run as activities. + + .. warning:: + This API is experimental and may change in future versions. + + Construct inside a workflow and pass it in the ``tools`` list of a + ``generate_content`` call. The matching server name must be registered on + the worker via ``GoogleGenAIPlugin(mcp_servers={name: factory})``. + + ``cache_tools`` controls how often tools are listed. When ``False`` (the + default) the ``{server}-list-tools`` activity runs each time the SDK + discovers tools (i.e. per ``generate_content`` call), so a server whose + tools changed mid-workflow is picked up. When ``True`` the first listing is + cached on this instance and reused for its lifetime (replay-safe in-workflow + state). + + Args: + server_name: Name selecting the worker-side factory; also the activity + prefix (``{server_name}-list-tools`` / ``{server_name}-call-tool``). + cache_tools: Cache the tool listing after the first call. + activity_config: Activity configuration (timeouts, retry policy, etc.) + for the MCP activities. Defaults to a 60-second + ``start_to_close_timeout``. + """ + + def __init__( # pyright: ignore[reportMissingSuperCall] + self, + server_name: str, + *, + cache_tools: bool = False, + activity_config: ActivityConfig | None = None, + ) -> None: + """Initialize without calling super (no real streams exist here).""" + self._server_name = server_name + self._cache_tools = cache_tools + self._cached_tools: ListToolsResult | None = None + self._activity_config: ActivityConfig = ( + ActivityConfig(start_to_close_timeout=_DEFAULT_MCP_TIMEOUT) + if activity_config is None + else activity_config + ) + + def _config(self, summary: str) -> ActivityConfig: + config: ActivityConfig = {**self._activity_config} + if "summary" not in config: + config["summary"] = summary + return config + + async def list_tools( # pyright: ignore[reportIncompatibleMethodOverride] + self, + cursor: str | None = None, + *, + params: PaginatedRequestParams | None = None, + ) -> ListToolsResult: + """List the server's tools via the ``{server}-list-tools`` activity.""" + if self._cache_tools and self._cached_tools is not None: + return self._cached_tools + result = await temporal_workflow.execute_activity( + f"{self._server_name}-list-tools", + result_type=ListToolsResult, + **self._config(f"mcp.{self._server_name}.list_tools"), + ) + if self._cache_tools: + self._cached_tools = result + return result + + async def call_tool( # pyright: ignore[reportIncompatibleMethodOverride] + self, + name: str, + arguments: dict[str, Any] | None = None, + read_timeout_seconds: timedelta | None = None, + progress_callback: ProgressFnT | None = None, + *, + meta: dict[str, Any] | None = None, + ) -> CallToolResult: + """Call a tool via the ``{server}-call-tool`` activity.""" + return await temporal_workflow.execute_activity( + f"{self._server_name}-call-tool", + _McpCallToolRequest(name=name, arguments=arguments or {}), + result_type=CallToolResult, + **self._config(f"mcp.{self._server_name}.call_tool:{name}"), + ) diff --git a/temporalio/contrib/google_genai/testing.py b/temporalio/contrib/google_genai/testing.py new file mode 100644 index 000000000..5d4a40486 --- /dev/null +++ b/temporalio/contrib/google_genai/testing.py @@ -0,0 +1,177 @@ +"""Testing utilities for the Google Gemini SDK Temporal integration. + +These let you exercise workflows that use +:class:`temporalio.contrib.google_genai.TemporalAsyncClient` without making +real Gemini API calls. Script the model's responses with :func:`text_response` +/ :func:`function_call_response`, build a plugin with +:class:`GeminiTestServer`, and register it on your worker like the real +:class:`temporalio.contrib.google_genai.GoogleGenAIPlugin`. + +Example:: + + server = GeminiTestServer( + [ + function_call_response("get_weather", {"city": "Tokyo"}), + text_response("It's sunny in Tokyo."), + ] + ) + async with Worker( + client, + task_queue="test", + workflows=[MyAgentWorkflow], + activities=[get_weather], + plugins=[server.plugin()], + ): + ... + assert len(server.requests) == 2 # one per model turn +""" + +from __future__ import annotations + +import json +from collections.abc import Sequence +from datetime import timedelta +from typing import TYPE_CHECKING, Any + +from google.genai import Client as GeminiClient +from google.genai.types import HttpResponse as SdkHttpResponse + +from temporalio.contrib.google_genai._google_genai_plugin import GoogleGenAIPlugin + +if TYPE_CHECKING: + from temporalio.contrib.google_genai._mcp import McpSessionFactory + +__all__ = [ + "GeminiTestServer", + "function_call_response", + "text_response", +] + + +def text_response(text: str) -> str: + """Build a ``generate_content`` response body with a single text part.""" + return json.dumps( + { + "candidates": [ + { + "content": {"role": "model", "parts": [{"text": text}]}, + "finishReason": "STOP", + } + ], + "usageMetadata": { + "promptTokenCount": 5, + "candidatesTokenCount": 10, + }, + } + ) + + +def function_call_response(name: str, args: dict[str, Any]) -> str: + """Build a ``generate_content`` response body with a single function call. + + The Gemini SDK's automatic function calling loop will invoke the matching + tool, then request another response — so pair each function-call response + with a following :func:`text_response` (or further calls). + """ + return json.dumps( + { + "candidates": [ + { + "content": { + "role": "model", + "parts": [{"functionCall": {"name": name, "args": args}}], + }, + "finishReason": "STOP", + } + ], + "usageMetadata": { + "promptTokenCount": 10, + "candidatesTokenCount": 15, + }, + } + ) + + +class GeminiTestServer: + """Scripts Gemini model responses so workflows run without real API calls. + + Pass canned response bodies built with :func:`text_response` / + :func:`function_call_response`. Each model call — including each turn of an + automatic-function-calling loop and each ``generate_content_stream`` call — + consumes the next response in order. Build a plugin with :meth:`plugin` + and register it on your worker; inspect :attr:`requests` afterwards to + assert exactly what the integration sent. + + Only model calls (``client.models``) are scripted. File, interaction, and + agent operations are not; mock those on a ``genai.Client`` directly if a + test needs them. MCP servers can be registered with + ``plugin(mcp_servers=...)`` and run for real. + """ + + def __init__(self, responses: Sequence[str]) -> None: + """Initialize with the response bodies to serve, in order.""" + self._responses = list(responses) + self._index = 0 + self.requests: list[dict[str, Any]] = [] + + def _next(self) -> str: + idx = self._index + self._index += 1 + if idx >= len(self._responses): + raise AssertionError( + f"GeminiTestServer ran out of responses (call {idx + 1}, " + f"have {len(self._responses)}); script another response." + ) + return self._responses[idx] + + def plugin( + self, + *, + mcp_servers: dict[str, McpSessionFactory] | None = None, + mcp_connection_idle_timeout: timedelta | None = None, + ) -> GoogleGenAIPlugin: + """Return a :class:`GoogleGenAIPlugin` whose model calls serve the script. + + The real plugin activities run; only the underlying HTTP layer is + replaced, so request formatting and the AFC loop are exercised exactly + as in production. + + Args: + mcp_servers: MCP servers to expose to workflows, as on + :class:`temporalio.contrib.google_genai.GoogleGenAIPlugin`. + These are *not* scripted — ``list_tools`` / ``call_tool`` run + for real against the given sessions — so a test can drive an + MCP tool call with a scripted + :func:`function_call_response` and assert on what the server + actually returned. + mcp_connection_idle_timeout: How long an idle worker-side MCP + connection stays open, as on + :class:`temporalio.contrib.google_genai.GoogleGenAIPlugin`. + """ + client = GeminiClient(api_key="fake-test-key") + + async def fake_async_request(*_args: Any, **kwargs: Any) -> SdkHttpResponse: + self.requests.append(dict(kwargs.get("request_dict") or {})) + return SdkHttpResponse( + headers={"content-type": "application/json"}, + body=self._next(), + ) + + async def fake_async_request_streamed(*_args: Any, **kwargs: Any) -> Any: + self.requests.append(dict(kwargs.get("request_dict") or {})) + body = self._next() + + async def _gen() -> Any: + yield SdkHttpResponse( + headers={"content-type": "application/json"}, body=body + ) + + return _gen() + + client._api_client.async_request = fake_async_request # type: ignore[assignment] + client._api_client.async_request_streamed = fake_async_request_streamed # type: ignore[assignment] + return GoogleGenAIPlugin( + client, + mcp_servers=mcp_servers, + mcp_connection_idle_timeout=mcp_connection_idle_timeout, + ) diff --git a/temporalio/contrib/google_genai/workflow.py b/temporalio/contrib/google_genai/workflow.py new file mode 100644 index 000000000..08e40f0c5 --- /dev/null +++ b/temporalio/contrib/google_genai/workflow.py @@ -0,0 +1,111 @@ +"""Workflow utilities for Google Gemini SDK integration with Temporal. + +This module provides utilities for using the Google Gemini SDK within Temporal +workflows. The key entry points are: + +- :func:`activity_as_tool` — converts a Temporal activity into a Gemini tool + callable for use with automatic function calling (AFC). +""" + +from __future__ import annotations + +import functools +import inspect +from collections.abc import Callable +from typing import Any + +from temporalio import activity +from temporalio import workflow as temporal_workflow +from temporalio.contrib.google_genai._errors import GoogleGenAIError +from temporalio.workflow import ActivityConfig + + +def activity_as_tool( + fn: Callable, + *, + activity_config: ActivityConfig | None = None, +) -> Callable: + """Convert a Temporal activity into a Gemini-compatible async tool callable. + + .. warning:: + This API is experimental and may change in future versions. + Use with caution in production environments. + + Returns an async callable with the same name, docstring, and type signature as + ``fn``. When Gemini's automatic function calling (AFC) invokes the returned + callable from within a Temporal workflow, the call is executed as a Temporal + activity via :func:`workflow.execute_activity`. Each tool invocation therefore + appears as a separate, durable entry in the workflow event history. + + Because AFC is left **enabled**, the Gemini SDK owns the agentic loop — no + manual ``while`` loop or ``run_agent()`` helper is required. Pass the returned + callable directly to ``GenerateContentConfig(tools=[...])``. + + Args: + fn: A Temporal activity function decorated with ``@activity.defn``. + activity_config: Configuration for the activity execution (timeouts, + retry policy, etc.). Must set ``start_to_close_timeout`` or + ``schedule_to_close_timeout`` — Temporal requires one, and there is + no default; otherwise the tool call raises when the activity is + invoked. + + Returns: + An async callable suitable for use as a Gemini tool. + + Raises: + GoogleGenAIError: If ``fn`` is not decorated with ``@activity.defn`` or + has no activity name. + """ + ret = activity._Definition.from_callable(fn) + if not ret: + raise GoogleGenAIError( + "Bare function without @activity.defn decorator is not supported", + "invalid_tool", + ) + if ret.name is None: + raise GoogleGenAIError( + "Activity must have a name to be used as a Gemini tool", + "invalid_tool", + ) + + config: ActivityConfig = {**(activity_config or {})} + if "summary" not in config: + config["summary"] = "tool_call" + + # For class-based activities the first parameter is 'self'. Partially apply + # it so that Gemini inspects only the user-facing parameters when building + # the function-call schema, while the worker resolves the real instance at + # execution time. + params = list(inspect.signature(fn).parameters.keys()) + schema_fn: Callable = fn + if params and params[0] == "self": + partial = functools.partial(fn, None) + setattr(partial, "__name__", fn.__name__) + partial.__annotations__ = getattr(fn, "__annotations__", {}) + setattr( + partial, + "__temporal_activity_definition", + getattr(fn, "__temporal_activity_definition", None), + ) + partial.__doc__ = fn.__doc__ + schema_fn = partial + + activity_name: str = ret.name + + async def wrapper(*args: Any, **kwargs: Any) -> Any: + sig = inspect.signature(schema_fn) + bound = sig.bind(*args, **kwargs) + bound.apply_defaults() + activity_args = list(bound.arguments.values()) + return await temporal_workflow.execute_activity( + activity_name, + args=activity_args, + **config, + ) + + wrapper.__name__ = schema_fn.__name__ # type: ignore + wrapper.__doc__ = schema_fn.__doc__ + setattr(wrapper, "__signature__", inspect.signature(schema_fn)) + wrapper.__annotations__ = getattr(schema_fn, "__annotations__", {}) + + return wrapper diff --git a/temporalio/contrib/langgraph/README.md b/temporalio/contrib/langgraph/README.md new file mode 100644 index 000000000..d5b7d4e0f --- /dev/null +++ b/temporalio/contrib/langgraph/README.md @@ -0,0 +1,311 @@ +# LangGraph Plugin for Temporal Python SDK + +⚠️ **This package is currently at an experimental release stage.** ⚠️ + +This Temporal [Plugin](https://docs.temporal.io/develop/plugins-guide) allows you to run [LangGraph](https://www.langchain.com/langgraph) nodes and tasks as Temporal Activities, giving your AI workflows durable execution, automatic retries, and timeouts. It supports both the LangGraph Graph API (``StateGraph``) and Functional API (``@entrypoint`` / ``@task``). + +## Installation + +```sh +uv add temporalio[langgraph] +``` + +## Plugin Initialization + +### Graph API + +```python +from langgraph.graph import StateGraph +from temporalio.contrib.langgraph import LangGraphPlugin + +g = StateGraph(State) +g.add_node("my_node", my_node, metadata={"execute_in": "activity"}) + +plugin = LangGraphPlugin(graphs={"my-graph": g}) +``` + +### Functional API + +```python +from temporalio.contrib.langgraph import LangGraphPlugin + +plugin = LangGraphPlugin( + entrypoints={"my_entrypoint": my_entrypoint}, + tasks=[my_task], + activity_options={"my_task": {"execute_in": "activity"}}, +) +``` + +## Checkpointer + +If your LangGraph code requires a checkpointer (for example, if you're using interrupts), use `InMemorySaver`. +Temporal handles durability, so third-party checkpointers (like PostgreSQL or Redis) are not needed. + +```python +import langgraph.checkpoint.memory +import typing + +from temporalio.contrib.langgraph import graph +from temporalio import workflow + +@workflow.defn +class MyWorkflow: + @workflow.run + async def run(self, input: str) -> typing.Any: + g = graph("my-graph").compile( + checkpointer=langgraph.checkpoint.memory.InMemorySaver(), + ) + + ... +``` + +## Execution Location + +Every node (Graph API) and task (Functional API) must be labeled with `execute_in`, set to either `"activity"` or `"workflow"`. This is required per node/task; it cannot be set in `default_activity_options`. + +```python +# Graph API +graph.add_node("my_node", my_node, metadata={"execute_in": "activity"}) +graph.add_node("tool_node", tool_node, metadata={"execute_in": "workflow"}) + +# Functional API +plugin = LangGraphPlugin( + tasks=[my_task, tool_task], + activity_options={ + "my_task": {"execute_in": "activity"}, + "tool_task": {"execute_in": "workflow"}, + }, +) +``` + +## Activity Options + +Options are passed through to [`workflow.execute_activity()`](https://python.temporal.io/temporalio.workflow.html#execute_activity), which supports parameters like `start_to_close_timeout`, `retry_policy`, `schedule_to_close_timeout`, `heartbeat_timeout`, and more. + +### Graph API + +Pass Activity options as node `metadata` when calling `add_node`: + +```python +from datetime import timedelta +from temporalio.common import RetryPolicy + +g = StateGraph(State) +g.add_node("my_node", my_node, metadata={ + "execute_in": "activity", + "start_to_close_timeout": timedelta(seconds=30), + "retry_policy": RetryPolicy(maximum_attempts=3), +}) +``` + +### Functional API + +Pass Activity options to the `LangGraphPlugin` constructor, keyed by task function name: + +```python +from datetime import timedelta +from temporalio.common import RetryPolicy +from temporalio.contrib.langgraph import LangGraphPlugin + +plugin = LangGraphPlugin( + entrypoints={"my_entrypoint": my_entrypoint}, + tasks=[my_task], + activity_options={ + "my_task": { + "execute_in": "activity", + "start_to_close_timeout": timedelta(seconds=30), + "retry_policy": RetryPolicy(maximum_attempts=3), + }, + }, +) +``` + +### Runtime Context + +LangGraph's run-scoped context (`context_schema`) is reconstructed on the Activity side, so nodes and tasks can read from and write to `runtime.context`: + +```python +from langgraph.runtime import Runtime +from typing_extensions import TypedDict + +from temporalio.contrib.langgraph import graph + +class Context(TypedDict): + user_id: str + +async def my_node(state: State, runtime: Runtime[Context]) -> dict: + return {"user": runtime.context["user_id"]} + +# In the Workflow: +g = graph("my-graph").compile() +await g.ainvoke({...}, context=Context(user_id="alice")) +``` + +Your `context` object must be serializable by the configured Temporal payload converter, since it crosses the Activity boundary. + +## Summaries + +Summaries are short, human-readable labels that show up in the Temporal UI and CLI, making it easier to see what each step of a run is doing. + +### Static summary + +`summary` is an ordinary Activity option, so a fixed per-node label works today — pass it like any other option: + +```python +g.add_node("plan", plan, metadata={"execute_in": "activity", "summary": "Planning step"}) +``` + +It is attached to the node's scheduled-activity event (`execute_in="activity"` only). + +### Dynamic summary (`summary_fn`) + +To derive the label from the node's input at runtime, supply a `summary_fn`. It receives the node's `(args, kwargs)` and returns a summary string, or `None`/`""` for no summary. For a `StateGraph` node `args[0]` is the state; for a Functional `@task` it is the task's arguments. + +```python +def summarize(args, kwargs) -> str | None: + state = args[0] + return f"stage={state['stage']} doc={state['doc_id']}" + +# Graph API: per-node +g.add_node("plan", plan, metadata={"execute_in": "activity", "summary_fn": summarize}) + +# Functional API: per-task +plugin = LangGraphPlugin( + tasks=[plan], + activity_options={"plan": {"execute_in": "activity", "summary_fn": summarize}}, +) +``` + +`summary_fn` is set per node/task (like the static `summary`), so different nodes — which receive different inputs — can compute their summaries independently. You can also put a `summary` or `summary_fn` in `default_activity_options` as a fallback for every node; a node/task that sets either form overrides the inherited default (you just can't set both forms at the same level). + +- For `execute_in="activity"` nodes the result sets the activity `summary` (one per scheduled-activity event, visible in history). +- For `execute_in="workflow"` nodes there is no activity, so the result updates the workflow's current details via [`workflow.set_current_details()`](https://python.temporal.io/temporalio.workflow.html#set_current_details). This is a single workflow-level slot (last-writer-wins) reflecting the most recent workflow-bound node that defines a `summary_fn`; a `None`/`""` result clears it. It is queryable via `__temporal_workflow_metadata`. + +`summary_fn` runs in workflow context on every replay, so it **must be deterministic and must not raise** (an exception fails the workflow task). Setting both a static `summary` and a `summary_fn` on the same node raises `ValueError`. + +## Streaming + +When `streaming_topic` is set on `LangGraphPlugin`, calls to `langgraph.config.get_stream_writer()` inside a node publish to the named topic on the workflow's [`WorkflowStream`](https://github.com/temporalio/sdk-python/tree/main/temporalio/contrib/workflow_streams). Activity-side nodes publish via `WorkflowStreamClient` (a signal carrying batched items, controlled by `streaming_batch_interval`); workflow-side nodes publish synchronously to the in-workflow stream (no signal). External subscribers consume the stream with `WorkflowStreamClient.create(...).topic(...).subscribe(...)`. + +The workflow **must** construct `WorkflowStream()` in its `@workflow.init` (i.e. `__init__`) + +```python +from datetime import timedelta +from typing import Any + +from langgraph.config import get_stream_writer +from langgraph.graph import START, StateGraph +from typing_extensions import TypedDict + +from temporalio import workflow +from temporalio.client import Client +from temporalio.contrib.langgraph import LangGraphPlugin, graph +from temporalio.contrib.workflow_streams import WorkflowStream, WorkflowStreamClient +from temporalio.worker import Worker + + +class State(TypedDict): + value: str + + +async def token_node(state: State) -> dict[str, str]: + writer = get_stream_writer() + for token in ["hello", " ", "world"]: + writer({"token": token}) + writer({"done": True}) + return {"value": "hello world"} + + +@workflow.defn +class StreamingWorkflow: + def __init__(self) -> None: + # Required when streaming_topic is set on the plugin. + _ = WorkflowStream() + self.app = graph("streaming").compile() + + @workflow.run + async def run(self) -> str: + result = await self.app.ainvoke({"value": ""}) + return result["value"] + + +async def main(client: Client) -> None: + g = StateGraph(State) + g.add_node("token_node", token_node, metadata={"execute_in": "activity"}) + g.add_edge(START, "token_node") + + async with Worker( + client, + task_queue="streaming-tq", + workflows=[StreamingWorkflow], + plugins=[ + LangGraphPlugin( + graphs={"streaming": g}, + default_activity_options={ + "start_to_close_timeout": timedelta(seconds=10) + }, + streaming_topic="tokens", + ) + ], + ): + handle = await client.start_workflow( + StreamingWorkflow.run, id="streaming-wf", task_queue="streaming-tq" + ) + + ws_client = WorkflowStreamClient.create(client, handle.id) + async for item in ws_client.topic("tokens", type=dict).subscribe(from_offset=0): + print(item.data) + if item.data.get("done"): + break + + print(await handle.result()) +``` + +### What's covered, and what isn't + +`streaming_topic` wires up exactly **one** LangGraph stream mode: `stream_mode="custom"`, i.e. values written through `get_stream_writer()`. The other modes — `"messages"`, `"values"`, `"updates"`, `"debug"` — are **not** captured by `streaming_topic`. They aren't produced by node-side writers; LangGraph's orchestrator emits them as it walks the graph. The documented pattern is to **bridge `astream()` in the workflow** and republish each yielded chunk to a `WorkflowStream` topic yourself: + +```python +@workflow.defn +class AstreamBridge: + def __init__(self) -> None: + self.stream = WorkflowStream() + self.app = graph("g").compile() + + @workflow.run + async def run(self) -> None: + topic = self.stream.topic("astream") + async for chunk in self.app.astream({...}, stream_mode="messages"): + topic.publish(chunk) + topic.publish({"done": True}) +``` + +### Retry semantics + +Streaming has **at-least-once** delivery per activity attempt. When an activity-wrapped node retries (transient failure, worker crash, etc.), the user function re-runs from scratch and re-publishes its writes — earlier publishes from the failed attempt are not rolled back. Subscribers should be ready to see duplicates and recover idempotently (e.g. dedupe on a sequence id you include in each chunk, or treat the stream as advisory and rely on the workflow's final result for state). + +## Tracing + +We recommend the [Temporal LangSmith Plugin](https://github.com/temporalio/sdk-python/tree/main/temporalio/contrib/langsmith) to trace your LangGraph Workflows and Activities. + +## Stores are not supported + +LangGraph's `Store` (e.g. `InMemoryStore` passed via `graph.compile(store=...)` or `@entrypoint(store=...)`) isn't accessible inside Activity-wrapped nodes: the Store holds live state that can't cross the Activity boundary, and Activities may run on a different worker than the Workflow. If you pass a store, the plugin logs a warning on first use and `runtime.store` is `None` inside nodes. + +Use Workflow state for per-run memory, or an external database (Postgres/Redis/etc.) configured on each worker if you need shared memory across runs. + +## Running Tests + +Install dependencies: + +```sh +uv sync --all-extras +``` + +Run the test suite: + +```sh +uv run pytest tests/contrib/langgraph +``` + +Tests start a local Temporal dev server automatically — no external server needed. diff --git a/temporalio/contrib/langgraph/__init__.py b/temporalio/contrib/langgraph/__init__.py new file mode 100644 index 000000000..e9aaf5605 --- /dev/null +++ b/temporalio/contrib/langgraph/__init__.py @@ -0,0 +1,25 @@ +"""LangGraph plugin for Temporal SDK. + +.. warning:: + This package is experimental and may change in future versions. + Use with caution in production environments. + +This plugin runs `LangGraph `_ nodes +and tasks as Temporal Activities, giving your AI agent workflows durable +execution, automatic retries, and timeouts. It supports both the LangGraph Graph +API (``StateGraph``) and Functional API (``@entrypoint`` / ``@task``). +""" + +from temporalio.contrib.langgraph._plugin import ( + LangGraphPlugin, + cache, + entrypoint, + graph, +) + +__all__ = [ + "LangGraphPlugin", + "cache", + "entrypoint", + "graph", +] diff --git a/temporalio/contrib/langgraph/_activity.py b/temporalio/contrib/langgraph/_activity.py new file mode 100644 index 000000000..c8447df47 --- /dev/null +++ b/temporalio/contrib/langgraph/_activity.py @@ -0,0 +1,188 @@ +"""Activity wrappers for executing LangGraph nodes and tasks.""" + +import asyncio +from collections.abc import Awaitable +from dataclasses import dataclass +from datetime import timedelta +from inspect import iscoroutinefunction, signature +from typing import Any, Callable + +from langgraph.errors import GraphInterrupt +from langgraph.types import Command, Interrupt + +from temporalio import workflow +from temporalio.contrib.langgraph._langgraph_config import ( + get_langgraph_config, + set_langgraph_config, + strip_runnable_config, +) +from temporalio.contrib.langgraph._task_cache import ( + cache_key, + cache_lookup, + cache_put, +) +from temporalio.contrib.workflow_streams import WorkflowStreamClient + +# Per-run dedupe so we only warn once when a user passes a Store via +# graph.compile(store=...) / @entrypoint(store=...). Cleared by +# LangGraphInterceptor.execute_workflow on workflow exit. +_warned_store_runs: set[str] = set() + + +def clear_store_warning(run_id: str) -> None: + """Drop the store-warning dedupe entry for a workflow run.""" + _warned_store_runs.discard(run_id) + + +@dataclass +class ActivityInput: + """Input for a LangGraph activity, containing args, kwargs, and config.""" + + args: tuple[Any, ...] + kwargs: dict[str, Any] + langgraph_config: dict[str, Any] + + +@dataclass +class ActivityOutput: + """Output from an Activity, containing result, command, or interrupts.""" + + result: Any = None + langgraph_command: Any = None + langgraph_interrupts: tuple[Interrupt] | None = None + + +def wrap_activity( + func: Callable, + *, + streaming_topic: str | None = None, + streaming_batch_interval: timedelta = timedelta(milliseconds=100), +) -> Callable[[ActivityInput], Awaitable[ActivityOutput]]: + """Wrap a function as a Temporal activity that handles LangGraph config and interrupts.""" + accepts_runtime = "runtime" in signature(func).parameters + + async def wrapper(input: ActivityInput) -> ActivityOutput: + async def run(stream_writer: Callable[[Any], None] | None) -> ActivityOutput: + # Sync funcs run on a thread (so the loop keeps flushing the + # stream client mid-execution); marshal writer calls back to + # the loop thread because the client's flush event is an + # asyncio.Event and isn't safe to set off-thread. + effective_writer = stream_writer + if not iscoroutinefunction(func) and stream_writer is not None: + loop = asyncio.get_running_loop() + inner_writer = stream_writer + + def thread_safe_writer(value: Any) -> None: + loop.call_soon_threadsafe(inner_writer, value) + + effective_writer = thread_safe_writer + + runtime = set_langgraph_config( + input.langgraph_config, stream_writer=effective_writer + ) + kwargs = dict(input.kwargs) + if accepts_runtime: + kwargs["runtime"] = runtime + + try: + if iscoroutinefunction(func): + result = await func(*input.args, **kwargs) + else: + result = await asyncio.to_thread(func, *input.args, **kwargs) + if isinstance(result, Command): + return ActivityOutput(langgraph_command=result) + return ActivityOutput(result=result) + except GraphInterrupt as e: + return ActivityOutput(langgraph_interrupts=e.args[0]) + + if streaming_topic is None: + return await run(stream_writer=None) + async with WorkflowStreamClient.from_within_activity( + batch_interval=streaming_batch_interval, + ) as client: + topic = client.topic(streaming_topic) + return await run(stream_writer=topic.publish) + + return wrapper + + +def wrap_execute_activity( + afunc: Callable[[ActivityInput], Awaitable[ActivityOutput]], + task_id: str = "", + summary_fn: Callable[[tuple[Any, ...], dict[str, Any]], str | None] | None = None, + **execute_activity_kwargs: Any, +) -> Callable[..., Any]: + """Wrap an activity function to be called via workflow.execute_activity with caching.""" + + async def wrapper(*args: Any, **kwargs: Any) -> Any: + # LangGraph may inject a RunnableConfig as the 'config' kwarg. Strip it + # down to a serializable subset so it can cross the activity boundary; + # callbacks, stores, etc. aren't serializable. + if "config" in kwargs: + kwargs["config"] = strip_runnable_config(kwargs["config"]) + + # LangGraph may inject a Runtime as the 'runtime' kwarg. It's + # reconstructed on the activity side from the serialized langgraph + # config, so drop the live Runtime from the kwargs that cross the + # activity boundary (it holds non-serializable stream_writer, store). + runtime = kwargs.pop("runtime", None) + run_id = workflow.info().run_id + if ( + getattr(runtime, "store", None) is not None + and run_id not in _warned_store_runs + ): + _warned_store_runs.add(run_id) + workflow.logger.warning( + "LangGraph Store passed via compile(store=...) / @entrypoint(store=...) " + "is not accessible inside activity-wrapped nodes and tasks: the Store " + "object isn't serializable across the activity boundary, and activities " + "may run on a different worker than the workflow. Use a backend-backed " + "store (Postgres/Redis) configured on each worker if you need shared " + "memory, or use workflow state for per-run memory." + ) + + langgraph_config = get_langgraph_config() + + # Check task result cache (for continue-as-new deduplication). + key = ( + cache_key(task_id, args, kwargs, langgraph_config.get("context")) + if task_id + else "" + ) + if task_id: + found, cached = cache_lookup(key) + if found: + return cached + + input = ActivityInput( + args=args, kwargs=kwargs, langgraph_config=langgraph_config + ) + # Compute a dynamic activity summary (if configured) on the schedule + # path only; a cache hit above returns before reaching here, so no + # activity is scheduled and no summary is needed. + call_kwargs = dict(execute_activity_kwargs) + if summary_fn is not None: + summary = summary_fn(args, kwargs) + if summary: + call_kwargs["summary"] = summary + output = await workflow.execute_activity(afunc, input, **call_kwargs) + if output.langgraph_interrupts is not None: + raise GraphInterrupt(output.langgraph_interrupts) + + result = output.result + if output.langgraph_command is not None: + cmd = output.langgraph_command + result = Command( + graph=cmd["graph"], + update=cmd["update"], + resume=cmd["resume"], + goto=cmd["goto"], + ) + + # Store in cache for future continue-as-new cycles. + if task_id: + cache_put(key, result) + + return result + + return wrapper diff --git a/temporalio/contrib/langgraph/_interceptor.py b/temporalio/contrib/langgraph/_interceptor.py new file mode 100644 index 000000000..f68d9d45d --- /dev/null +++ b/temporalio/contrib/langgraph/_interceptor.py @@ -0,0 +1,77 @@ +"""Workflow interceptor that scopes LangGraph graphs/entrypoints to the workflow run.""" + +# pyright: reportMissingTypeStubs=false + +from __future__ import annotations + +from typing import Any + +from langgraph.graph import StateGraph +from langgraph.pregel import Pregel + +from temporalio import workflow +from temporalio.contrib.langgraph._activity import clear_store_warning +from temporalio.contrib.workflow_streams._stream import _PUBLISH_SIGNAL +from temporalio.worker import ( + ExecuteWorkflowInput, + Interceptor, + WorkflowInboundInterceptor, + WorkflowInterceptorClassInput, + WorkflowOutboundInterceptor, +) + +_workflow_graphs: dict[str, dict[str, StateGraph[Any, Any, Any, Any]]] = {} +_workflow_entrypoints: dict[str, dict[str, Pregel[Any, Any, Any, Any]]] = {} + + +class LangGraphInterceptor(Interceptor): + """Interceptor that registers a workflow's graphs and entrypoints for the run.""" + + def __init__( + self, + graphs: dict[str, StateGraph[Any, Any, Any, Any]], + entrypoints: dict[str, Pregel[Any, Any, Any, Any]], + streaming_topic: str | None = None, + ) -> None: + """Initialize with the graphs and entrypoints to scope to each workflow run.""" + self._graphs = graphs + self._entrypoints = entrypoints + self._streaming_topic = streaming_topic + + def workflow_interceptor_class( + self, input: WorkflowInterceptorClassInput + ) -> type[WorkflowInboundInterceptor]: + """Return the inbound interceptor class used to scope graphs per run.""" + graphs = self._graphs + entrypoints = self._entrypoints + streaming_topic = self._streaming_topic + + class Inbound(WorkflowInboundInterceptor): + def init(self, outbound: WorkflowOutboundInterceptor) -> None: + run_id = outbound.info().run_id + _workflow_graphs[run_id] = graphs + _workflow_entrypoints[run_id] = entrypoints + super().init(outbound) + + async def execute_workflow(self, input: ExecuteWorkflowInput) -> Any: + if ( + streaming_topic is not None + and workflow.get_signal_handler(_PUBLISH_SIGNAL) is None + ): + raise RuntimeError( + f"LangGraphPlugin was configured with " + f"streaming_topic={streaming_topic!r}, but workflow " + f"{workflow.info().workflow_type!r} did not register a " + f"WorkflowStream. Construct WorkflowStream() in the " + f"workflow's @workflow.init (i.e. __init__) method so " + f"streaming activities can publish to it." + ) + try: + return await self.next.execute_workflow(input) + finally: + run_id = workflow.info().run_id + _workflow_graphs.pop(run_id, None) + _workflow_entrypoints.pop(run_id, None) + clear_store_warning(run_id) + + return Inbound diff --git a/temporalio/contrib/langgraph/_langgraph_config.py b/temporalio/contrib/langgraph/_langgraph_config.py new file mode 100644 index 000000000..90c6c810d --- /dev/null +++ b/temporalio/contrib/langgraph/_langgraph_config.py @@ -0,0 +1,164 @@ +"""LangGraph configuration management for Temporal workflows.""" + +# pyright: reportMissingTypeStubs=false + +import dataclasses +from typing import Any, Callable + +from langchain_core.runnables.config import var_child_runnable_config +from langgraph._internal._constants import ( + CONFIG_KEY_CHECKPOINT_ID, + CONFIG_KEY_CHECKPOINT_MAP, + CONFIG_KEY_CHECKPOINT_NS, + CONFIG_KEY_DURABILITY, + CONFIG_KEY_RESUMING, + CONFIG_KEY_RUNTIME, + CONFIG_KEY_SCRATCHPAD, + CONFIG_KEY_SEND, + CONFIG_KEY_TASK_ID, + CONFIG_KEY_THREAD_ID, +) +from langgraph._internal._scratchpad import PregelScratchpad +from langgraph.graph.state import RunnableConfig +from langgraph.pregel._algo import LazyAtomicCounter +from langgraph.runtime import ExecutionInfo, Runtime + + +def strip_runnable_config(config: RunnableConfig | None) -> RunnableConfig: + """Return a serializable subset of a RunnableConfig. + + LangGraph injects the active RunnableConfig into user functions as a + config kwarg. The full object holds non-serializable things (callbacks, + checkpointer/store/cache handles, pregel send/read callables) that can't + cross an activity boundary, so we keep only primitive fields and the + serializable subset of configurable. + """ + orig = config or {} + configurable = orig.get("configurable") or {} + + result: RunnableConfig = { + "tags": list(orig.get("tags") or []), + "metadata": dict(orig.get("metadata") or {}), + } + if run_name := orig.get("run_name"): + result["run_name"] = run_name + if run_id := orig.get("run_id"): + result["run_id"] = run_id + if (recursion_limit := orig.get("recursion_limit")) is not None: + result["recursion_limit"] = recursion_limit + + stripped_configurable: dict[str, Any] = { + key: configurable[key] + for key in ( + CONFIG_KEY_CHECKPOINT_NS, + CONFIG_KEY_CHECKPOINT_ID, + CONFIG_KEY_CHECKPOINT_MAP, + CONFIG_KEY_THREAD_ID, + CONFIG_KEY_TASK_ID, + CONFIG_KEY_RESUMING, + CONFIG_KEY_DURABILITY, + ) + if key in configurable + } + if stripped_configurable: + result["configurable"] = stripped_configurable + return result + + +def get_langgraph_config() -> dict[str, Any]: + """Get the current LangGraph runnable config as a serializable dict.""" + config = var_child_runnable_config.get() + configurable = (config or {}).get("configurable") or {} + scratchpad = configurable.get(CONFIG_KEY_SCRATCHPAD) + runtime = configurable.get(CONFIG_KEY_RUNTIME) + execution_info = getattr(runtime, "execution_info", None) + + stripped = strip_runnable_config(config) + return { + **stripped, + "configurable": { + **(stripped.get("configurable") or {}), + CONFIG_KEY_SCRATCHPAD: { + "step": getattr(scratchpad, "step", 0), + "stop": getattr(scratchpad, "stop", 0), + "resume": list(getattr(scratchpad, "resume", [])), + "null_resume": scratchpad.get_null_resume() if scratchpad else None, + }, + }, + "context": getattr(runtime, "context", None), + "previous": getattr(runtime, "previous", None), + "execution_info": ( + dataclasses.asdict(execution_info) if execution_info else None + ), + } + + +def set_langgraph_config( + config: dict[str, Any], + *, + stream_writer: Callable[[Any], None] | None = None, +) -> Runtime: + """Restore a LangGraph runnable config from a serialized dict. + + Returns the reconstructed Runtime so callers can re-inject it into the + user function's kwargs without needing to know the configurable layout. + """ + configurable = config.get("configurable") or {} + scratchpad = configurable.get(CONFIG_KEY_SCRATCHPAD) or {} + null_resume_box = [scratchpad.get("null_resume")] + + def get_null_resume(consume: bool = False) -> Any: + val = null_resume_box[0] + if consume and val is not None: + null_resume_box[0] = None + return val + + execution_info_dict = config.get("execution_info") + runtime = Runtime( + context=config.get("context"), + stream_writer=stream_writer or (lambda _: None), + previous=config.get("previous"), + execution_info=( + ExecutionInfo(**execution_info_dict) if execution_info_dict else None + ), + ) + + restored_configurable: dict[str, Any] = { + key: configurable[key] + for key in ( + CONFIG_KEY_CHECKPOINT_NS, + CONFIG_KEY_CHECKPOINT_ID, + CONFIG_KEY_CHECKPOINT_MAP, + CONFIG_KEY_THREAD_ID, + CONFIG_KEY_TASK_ID, + CONFIG_KEY_RESUMING, + CONFIG_KEY_DURABILITY, + ) + if key in configurable + } + restored_configurable[CONFIG_KEY_SCRATCHPAD] = PregelScratchpad( + step=scratchpad.get("step", 0), + stop=scratchpad.get("stop", 0), + call_counter=LazyAtomicCounter(), + interrupt_counter=LazyAtomicCounter(), + get_null_resume=get_null_resume, + resume=list(scratchpad.get("resume", [])), + subgraph_counter=LazyAtomicCounter(), + ) + restored_configurable[CONFIG_KEY_SEND] = lambda _: None + restored_configurable[CONFIG_KEY_RUNTIME] = runtime + + runnable_config: RunnableConfig = {"configurable": restored_configurable} + if tags := config.get("tags"): + runnable_config["tags"] = tags + if metadata := config.get("metadata"): + runnable_config["metadata"] = metadata + if run_name := config.get("run_name"): + runnable_config["run_name"] = run_name + if run_id := config.get("run_id"): + runnable_config["run_id"] = run_id + if (recursion_limit := config.get("recursion_limit")) is not None: + runnable_config["recursion_limit"] = recursion_limit + + var_child_runnable_config.set(runnable_config) + return runtime diff --git a/temporalio/contrib/langgraph/_plugin.py b/temporalio/contrib/langgraph/_plugin.py new file mode 100644 index 000000000..03881ca2a --- /dev/null +++ b/temporalio/contrib/langgraph/_plugin.py @@ -0,0 +1,382 @@ +"""LangGraph plugin for running LangGraph nodes and tasks as Temporal activities.""" + +# pyright: reportMissingTypeStubs=false + +from __future__ import annotations + +import inspect +import sys +import warnings +from dataclasses import replace +from datetime import timedelta +from typing import Any, Callable + +from langgraph._internal._runnable import RunnableCallable +from langgraph.graph import StateGraph +from langgraph.pregel import Pregel + +from temporalio import activity, workflow +from temporalio.contrib.langgraph._activity import wrap_activity, wrap_execute_activity +from temporalio.contrib.langgraph._interceptor import ( + LangGraphInterceptor, + _workflow_entrypoints, + _workflow_graphs, +) +from temporalio.contrib.langgraph._task_cache import ( + get_task_cache, + set_task_cache, + task_id, +) +from temporalio.contrib.langgraph._workflow import wrap_workflow +from temporalio.plugin import SimplePlugin +from temporalio.worker import WorkflowRunner +from temporalio.worker.workflow_sandbox import SandboxedWorkflowRunner + +_ACTIVITY_OPTION_KEYS: frozenset[str] = frozenset( + {"execute_in", *inspect.signature(workflow.execute_activity).parameters} +) +# Node/task option keys beyond the raw execute_activity parameters: +# 'summary_fn' is a callable consumed in the workflow (not a Temporal +# option), so it must be split out of Graph API metadata too. +_LANGGRAPH_OPTION_KEYS: frozenset[str] = _ACTIVITY_OPTION_KEYS | frozenset( + {"summary_fn"} +) + + +def _constant_summary_fn( + value: str, +) -> Callable[[tuple[Any, ...], dict[str, Any]], str]: + """Adapt a static summary string to the summary_fn interface.""" + return lambda args, kwargs: value + + +def _merge_activity_opts( + defaults: dict[str, Any] | None, specific: dict[str, Any] +) -> dict[str, Any]: + """Layer per-node/task options over the plugin defaults. + + ``summary`` and ``summary_fn`` are two forms of one setting, so a node or + task that supplies either form overrides an inherited default of *either* + form, rather than coexisting with it and tripping the exclusivity check. + """ + merged = dict(defaults or {}) + if "summary" in specific or "summary_fn" in specific: + merged.pop("summary", None) + merged.pop("summary_fn", None) + merged.update(specific) + return merged + + +class LangGraphPlugin(SimplePlugin): + """LangGraph plugin for Temporal SDK. + + .. warning:: + This package is experimental and may change in future versions. + Use with caution in production environments. + + This plugin runs `LangGraph `_ nodes + and tasks as Temporal Activities, giving your AI agent workflows durable + execution, automatic retries, and timeouts. It supports both the LangGraph Graph + API (``StateGraph``) and Functional API (``@entrypoint`` / ``@task``). + + Args: + graphs: Graph API graphs to make available to workflows, keyed by name. + Workflows retrieve them with :func:`graph` and call + ``.compile()`` to get a runnable. Each node's ``metadata`` must + include ``execute_in`` (``"activity"`` or ``"workflow"``) and + may include any kwarg accepted by + :func:`workflow.execute_activity` (e.g. ``start_to_close_timeout``, + ``retry_policy``). + entrypoints: Functional API entrypoints to make available to + workflows, keyed by name. Workflows retrieve them with + :func:`entrypoint`. + tasks: Functional API ``@task`` functions to wrap as Temporal + Activities. + activity_options: Per-task activity options for the Functional + API, keyed by task function name. Each entry must include + ``execute_in`` and may include any + :func:`workflow.execute_activity` kwarg. Used because LangGraph's + Functional API has no per-task ``metadata`` channel. + default_activity_options: Activity options applied to every + activity-bound node and task, overridable per-node (Graph API + ``metadata``) or per-task (``activity_options[name]``). A + node/task that sets ``summary`` or ``summary_fn`` overrides an + inherited default of either form. + streaming_topic: When set, ``langgraph.config.get_stream_writer()`` + inside a node publishes to this topic on the workflow's + :class:`WorkflowStream`. The workflow must construct + ``WorkflowStream()`` in its ``@workflow.init`` (the plugin's + interceptor verifies this on workflow start). Nodes with + ``execute_in='activity'`` publish through + :class:`WorkflowStreamClient` (signal); nodes with + ``execute_in='workflow'`` publish synchronously to the + in-workflow stream (no signal). + streaming_batch_interval: How often the activity-side stream + client flushes buffered publishes into a single + ``__temporal_workflow_stream_publish`` signal. Has no effect + on workflow-side nodes (their publishes are synchronous + in-memory log appends). Lower values reduce streaming + latency at the cost of more signals (more workflow history + events); higher values amortize signal cost but make + chunks arrive in larger bursts. Default 100ms suits + interactive token streaming; raise to 250–1000ms for + non-interactive aggregation, lower toward 10–50ms only if + you've measured the latency need and accept the history + cost. + """ + + def __init__( + self, + # Graph API + graphs: dict[str, StateGraph[Any, Any, Any, Any]] | None = None, + # Functional API + entrypoints: dict[str, Pregel[Any, Any, Any, Any]] | None = None, + tasks: list | None = None, + # TODO: Remove activity_options when we have support for @task(metadata=...) + activity_options: dict[str, dict[str, Any]] | None = None, + default_activity_options: dict[str, Any] | None = None, + streaming_topic: str | None = None, + streaming_batch_interval: timedelta = timedelta(milliseconds=100), + ): + """Initialize the LangGraph plugin with graphs, entrypoints, and tasks. + + .. warning:: + Streaming support is experimental and may change in + future versions. + """ + if sys.version_info < (3, 11): + warnings.warn( # type: ignore[reportUnreachable] + "LangGraphPlugin requires Python >= 3.11 for full async support. " + "On older versions, the Functional API (@task/@entrypoint) and " + "interrupt() will not work because LangGraph relies on " + "contextvars propagation through asyncio.create_task(), which is " + "only available in Python 3.11+. See " + "https://reference.langchain.com/python/langgraph/config/get_store/", + stacklevel=2, + ) + + if default_activity_options and "execute_in" in default_activity_options: + raise ValueError( + "execute_in cannot be set in default_activity_options. " + "Set it on each node's metadata (Graph API) or in " + "activity_options[task_name] (Functional API)." + ) + + if ( + default_activity_options + and "summary" in default_activity_options + and "summary_fn" in default_activity_options + ): + raise ValueError( + "Set either 'summary' or 'summary_fn' in default_activity_options, " + "not both." + ) + + self.activities: list = [] + self._streaming_topic = streaming_topic + self._streaming_batch_interval = streaming_batch_interval + + # Graph API: Wrap graph nodes as Temporal Activities. + if graphs: + for graph_name, graph in graphs.items(): + for node_name, node in graph.nodes.items(): + if node.retry_policy: + raise ValueError( + f"Node {graph_name}.{node_name} has a LangGraph " + f"retry_policy set. Use Temporal activity options " + f"instead, e.g. pass retry_policy=RetryPolicy(...) " + f"via default_activity_options or in the node's " + f"metadata dict." + ) + runnable = node.runnable + if not isinstance(runnable, RunnableCallable): + raise ValueError(f"Node {node_name} must be a RunnableCallable") + user_func = runnable.func or runnable.afunc + if user_func is None: + raise ValueError(f"Node {node_name} must have a function") + # Keep 'config' (for metadata/tags) and 'runtime' (for + # context + store — reconstructed on the activity side). + # Drop writer/etc., which hold non-serializable objects + # that can't cross the activity boundary. + runnable.func_accepts = { + k: v + for k, v in runnable.func_accepts.items() + if k in ("config", "runtime") + } + # Split node.metadata into activity options vs. user + # metadata. Activity-option keys (timeouts, retry policy, + # etc.) become kwargs to workflow.execute_activity; user + # keys stay on node.metadata so LangGraph exposes them to + # the node function via config["metadata"]. + node_meta = node.metadata or {} + node_opts = { + k: v + for k, v in node_meta.items() + if k in _LANGGRAPH_OPTION_KEYS + } + node.metadata = { + k: v + for k, v in node_meta.items() + if k not in _LANGGRAPH_OPTION_KEYS + } + if "execute_in" not in node_opts: + raise ValueError( + f"Node {graph_name}.{node_name} is missing required " + f"'execute_in' in metadata. Set it to 'activity' or " + f"'workflow'." + ) + opts = _merge_activity_opts(default_activity_options, node_opts) + # Route all LangGraph node calls through afunc so the async + # activity wrapper is always used. wrap_activity handles + # sync vs. async user functions inside the activity itself. + runnable.afunc = self.execute( + f"{graph_name}.{node_name}", user_func, opts + ) + runnable.func = None + + # Functional API: Wrap @task functions as Temporal Activities. + if tasks: + for task in tasks: + name = task.func.__name__ + if task.retry_policy: + raise ValueError( + f"Task {name} has a LangGraph retry_policy set. " + f"Use Temporal activity options instead, e.g. pass " + f"retry_policy=RetryPolicy(...) via " + f"default_activity_options or activity_options[{name!r}]." + ) + task_opts = (activity_options or {}).get(name, {}) + if "execute_in" not in task_opts: + raise ValueError( + f"Task {name} is missing required 'execute_in' in " + f"activity_options[{name!r}]. Set it to 'activity' or " + f"'workflow'." + ) + opts = _merge_activity_opts(default_activity_options, task_opts) + + task.func = self.execute(task_id(task.func), task.func, opts) + task.func.__name__ = name + task.func.__qualname__ = getattr(task.func, "__qualname__", name) + + def workflow_runner(runner: WorkflowRunner | None) -> WorkflowRunner: + if not runner: + raise ValueError("No WorkflowRunner provided to the LangGraph plugin.") + if isinstance(runner, SandboxedWorkflowRunner): + return replace( + runner, + restrictions=runner.restrictions.with_passthrough_modules( + "langchain", + "langchain_core", + "langgraph", + "langsmith", + "numpy", # LangSmith uses numpy + ), + ) + return runner + + super().__init__( + "langchain.LangGraphPlugin", + activities=self.activities, + workflow_runner=workflow_runner, + interceptors=[ + LangGraphInterceptor( + graphs or {}, entrypoints or {}, streaming_topic=streaming_topic + ) + ], + ) + + def execute( + self, + activity_name: str, + func: Callable, + kwargs: dict[str, Any] | None = None, + ) -> Callable: + """Prepare a node or task to execute as an activity or inline in the workflow.""" + opts = kwargs or {} + execute_in = opts.pop("execute_in") + # Normalize the node's summary to a single summary_fn. Both keys are + # popped so neither reaches workflow.execute_activity (which takes no + # summary_fn, and would get a duplicate summary kwarg); a static + # summary becomes a summary_fn that ignores its input. + summary_fn = opts.pop("summary_fn", None) + static_summary = opts.pop("summary", None) + if summary_fn is not None and static_summary is not None: + raise ValueError( + f"{activity_name}: set either 'summary' or 'summary_fn', not both." + ) + if static_summary is not None: + summary_fn = _constant_summary_fn(static_summary) + + if execute_in == "activity": + wrapped = wrap_activity( + func, + streaming_topic=self._streaming_topic, + streaming_batch_interval=self._streaming_batch_interval, + ) + a = activity.defn(name=activity_name)(wrapped) + self.activities.append(a) + return wrap_execute_activity( + a, task_id=task_id(func), summary_fn=summary_fn, **opts + ) + elif execute_in == "workflow": + return wrap_workflow( + func, streaming_topic=self._streaming_topic, summary_fn=summary_fn + ) + else: + raise ValueError(f"Invalid execute_in value: {execute_in}") + + +def graph( + name: str, cache: dict[str, Any] | None = None +) -> StateGraph[Any, Any, Any, Any]: + """Retrieve a registered graph by name. + + Args: + name: Graph name as registered with LangGraphPlugin. + cache: Optional task result cache from a previous cache() call. + Restores cached results so previously-completed nodes are + not re-executed after continue-as-new. + """ + set_task_cache(cache or {}) + graphs = _workflow_graphs.get(workflow.info().run_id) + if graphs is None: + raise RuntimeError( + "graph() must be called from inside a workflow running under LangGraphPlugin" + ) + if name not in graphs: + raise KeyError(f"Graph {name!r} not found. Available graphs: {list(graphs)}") + return graphs[name] + + +def entrypoint( + name: str, cache: dict[str, Any] | None = None +) -> Pregel[Any, Any, Any, Any]: + """Retrieve a registered entrypoint by name. + + Args: + name: Entrypoint name as registered with Plugin. + cache: Optional task result cache from a previous cache() call. + Restores cached results so previously-completed tasks are + not re-executed after continue-as-new. + """ + set_task_cache(cache or {}) + entrypoints = _workflow_entrypoints.get(workflow.info().run_id) + if entrypoints is None: + raise RuntimeError( + "entrypoint() must be called from inside a workflow running under LangGraphPlugin" + ) + if name not in entrypoints: + raise KeyError( + f"Entrypoint {name!r} not found. Available entrypoints: {list(entrypoints)}" + ) + return entrypoints[name] + + +def cache() -> dict[str, Any] | None: + """Return the task result cache as a serializable dict. + + Returns a dict suitable for passing to entrypoint(name, cache=...) to + restore cached task results across continue-as-new boundaries. + Returns None if the cache is empty. + """ + return get_task_cache() or None diff --git a/temporalio/contrib/langgraph/_task_cache.py b/temporalio/contrib/langgraph/_task_cache.py new file mode 100644 index 000000000..ab3e683d7 --- /dev/null +++ b/temporalio/contrib/langgraph/_task_cache.py @@ -0,0 +1,83 @@ +"""Task result cache for continue-as-new support. + +Caches task results by (module.qualname, args, kwargs) hash so that previously +completed tasks are not re-executed after a continue-as-new. The cache state +is a plain dict that can travel through workflow.continue_as_new(). +""" + +from __future__ import annotations + +from contextvars import ContextVar +from hashlib import sha256 +from json import dumps +from typing import Any + +_task_cache: ContextVar[dict[str, Any] | None] = ContextVar( + "_temporal_task_cache", default=None +) + + +def set_task_cache(cache: dict[str, Any] | None) -> None: + """Set the task result cache for the current context.""" + _task_cache.set(cache) + + +def get_task_cache() -> dict[str, Any] | None: + """Get the task result cache for the current context.""" + return _task_cache.get() + + +def task_id(func: Any) -> str: + """Return the fully-qualified module.qualname for a function. + + Raises ValueError for functions that cannot be identified unambiguously + (lambdas, closures, __main__ functions). + """ + module = getattr(func, "__module__", None) + qualname = getattr(func, "__qualname__", None) or getattr(func, "__name__", None) + + if module is None or qualname is None: + raise ValueError( + f"Cannot identify task {func}: missing __module__ or __qualname__. " + "Tasks must be defined at module level." + ) + if module == "__main__": + raise ValueError( + f"Cannot identify task {qualname}: defined in __main__. " + "Tasks must be importable from a named module." + ) + if "" in qualname: + raise ValueError( + f"Cannot identify task {qualname}: closures/local functions are not supported. " + "Tasks must be defined at module level." + ) + return f"{module}.{qualname}" + + +def cache_key( + task_id: str, + args: tuple[Any, ...], + kwargs: dict[str, Any], + context: Any = None, +) -> str: + """Build a cache key from the full task identifier, arguments, and runtime context.""" + try: + key_str = dumps([task_id, args, kwargs, context], sort_keys=True, default=str) + except (TypeError, ValueError): + key_str = repr([task_id, args, kwargs, context]) + return sha256(key_str.encode()).hexdigest()[:32] + + +def cache_lookup(key: str) -> tuple[bool, Any]: + """Return (True, value) if cached, (False, None) otherwise.""" + cache = _task_cache.get() + if cache is not None and key in cache: + return True, cache[key] + return False, None + + +def cache_put(key: str, value: Any) -> None: + """Store a value in the task result cache.""" + cache = _task_cache.get() + if cache is not None: + cache[key] = value diff --git a/temporalio/contrib/langgraph/_workflow.py b/temporalio/contrib/langgraph/_workflow.py new file mode 100644 index 000000000..43b3d06ae --- /dev/null +++ b/temporalio/contrib/langgraph/_workflow.py @@ -0,0 +1,73 @@ +"""Workflow-side wrappers for executing LangGraph nodes inline in a workflow.""" + +# pyright: reportMissingTypeStubs=false + +from __future__ import annotations + +import dataclasses +from collections.abc import Awaitable +from inspect import iscoroutinefunction +from typing import Any, Callable + +from langchain_core.runnables.config import var_child_runnable_config +from langgraph._internal._constants import CONFIG_KEY_RUNTIME + +from temporalio import workflow +from temporalio.contrib.workflow_streams._stream import _PUBLISH_SIGNAL + + +def wrap_workflow( + func: Callable[..., Any], + *, + streaming_topic: str | None = None, + summary_fn: Callable[[tuple[Any, ...], dict[str, Any]], str | None] | None = None, +) -> Callable[..., Awaitable[Any]]: + """Wrap a function as a workflow-side LangGraph node. + + Mirrors :func:`wrap_activity`: the outer wrapper resolves a stream + writer and passes it to an inner ``run`` that invokes the user + function with the writer installed. Workflow-side nodes publish + synchronously to the in-workflow ``WorkflowStream`` (no signal + round-trip); activity-side nodes go through ``WorkflowStreamClient``. + + Workflow-side nodes have no activity to carry a summary, so a + ``summary_fn`` result updates the workflow's current details via + :func:`temporalio.workflow.set_current_details` (last-writer-wins); + an empty result clears it. + """ + + async def wrapper(*args: Any, **kwargs: Any) -> Any: + if summary_fn is not None: + # Always write (clearing when empty) so this node never shows a + # stale summary left by an earlier workflow-bound node. + workflow.set_current_details(summary_fn(args, kwargs) or "") + + async def run(stream_writer: Callable[[Any], None] | None) -> Any: + token = None + if stream_writer is not None: + config = var_child_runnable_config.get() or {} + configurable = dict(config.get("configurable") or {}) + runtime = configurable.get(CONFIG_KEY_RUNTIME) + if runtime is not None: + configurable[CONFIG_KEY_RUNTIME] = dataclasses.replace( + runtime, stream_writer=stream_writer + ) + token = var_child_runnable_config.set( + {**config, "configurable": configurable} + ) + try: + if iscoroutinefunction(func): + return await func(*args, **kwargs) + return func(*args, **kwargs) + finally: + if token is not None: + var_child_runnable_config.reset(token) + + if streaming_topic is None: + return await run(stream_writer=None) + publish_handler = workflow.get_signal_handler(_PUBLISH_SIGNAL) + stream = getattr(publish_handler, "__self__") + topic = stream.topic(streaming_topic) + return await run(stream_writer=topic.publish) + + return wrapper diff --git a/temporalio/contrib/langsmith/README.md b/temporalio/contrib/langsmith/README.md new file mode 100644 index 000000000..421a76c02 --- /dev/null +++ b/temporalio/contrib/langsmith/README.md @@ -0,0 +1,246 @@ +# LangSmith Plugin for Temporal Python SDK + +> ⚠️ **This package is currently at an experimental release stage.** ⚠️ + +This Temporal [Plugin](https://docs.temporal.io/develop/plugins-guide) allows your [LangSmith](https://smith.langchain.com/) traces to work within Temporal Workflows. It propagates trace context across Worker boundaries so that `@traceable` calls, LLM invocations, and Temporal operations show up in a single connected trace, and ensures that replaying does not generate duplicate traces. + +## Quick Start + +Install Temporal with the LangSmith feature enabled: + +```bash +uv add temporalio[langsmith] +``` + +Register the Plugin on your Temporal Client. You need it on both the Client (starter) side and the Workers: + +```python +from temporalio.client import Client +from temporalio.contrib.langsmith import LangSmithPlugin + +client = await Client.connect( + "localhost:7233", + plugins=[LangSmithPlugin(project_name="my-project")], +) +``` + +Once that's set up, any `@traceable` function inside your Workflows and Activities will show up in LangSmith with correct parent-child relationships, even across Worker boundaries. + +## Example: AI Chatbot + +A conversational chatbot using OpenAI, orchestrated by a Temporal Workflow. The Workflow stays alive waiting for user messages via Signals, and dispatches each message to an Activity that calls the LLM. + +### Activity (Wraps the LLM Call) + +```python +from langsmith import traceable + +@traceable(name="Call OpenAI", run_type="chain") +@activity.defn +async def call_openai(request: OpenAIRequest) -> Response: + client = wrap_openai(AsyncOpenAI()) # This is a traced langsmith function + return await client.responses.create( + model=request.model, + input=request.input, + instructions=request.instructions, + ) +``` + +### Workflow (Orchestrates the Conversation) + +```python +@workflow.defn +class ChatbotWorkflow: + @workflow.run + async def run(self) -> str: + # @traceable works inside Workflows — fully replay-safe + now = workflow.now().strftime("%b %d %H:%M") + return await traceable( + name=f"Session {now}", run_type="chain", + )(self._run_with_trace)() + + async def _run_with_trace(self) -> str: + while not self._done: + await workflow.wait_condition( + lambda: self._pending_message is not None or self._done + ) + if self._done: + break + + message = self._pending_message + self._pending_message = None + + @traceable(name=f"Query: {message[:60]}", run_type="chain") + async def _query(msg: str) -> str: + response = await workflow.execute_activity( + call_openai, + OpenAIRequest(model="gpt-4o-mini", input=msg), + start_to_close_timeout=timedelta(seconds=60), + ) + return response.output_text + + self._last_response = await _query(message) + + return "Session ended." +``` + +### Worker + +```python +client = await Client.connect( + "localhost:7233", + plugins=[LangSmithPlugin(project_name="chatbot")], +) + +worker = Worker( + client, + task_queue="chatbot", + workflows=[ChatbotWorkflow], + activities=[call_openai], +) +await worker.run() +``` + +### What you see in LangSmith + +With the default configuration (`add_temporal_runs=False`), the trace contains only your application logic: + +``` +Session Apr 03 14:30 + Query: "What's the weather in NYC?" + Call OpenAI + openai.responses.create (auto-traced by wrap_openai) +``` + +An actual look at the LangSmith UI: + +![Screenshot: LangSmith trace tree with add_temporal_runs=False showing clean application-only hierarchy](images/langsmith-no-temporal.png) + +## `add_temporal_runs` — Temporal Operation Visibility + +By default, `add_temporal_runs` is `False` and only your `@traceable` application logic appears in traces. Setting it to `True` also adds Temporal operations (StartWorkflow, RunWorkflow, StartActivity, RunActivity, etc.): + +```python +plugins=[LangSmithPlugin(project_name="my-project", add_temporal_runs=True)] +``` + +This adds Temporal operation nodes to the trace tree so that the orchestration layer is visible alongside your application logic. If the caller wraps `start_workflow` in a `@traceable` function, the full trace looks like: + +``` +Ask Chatbot # @traceable wrapper around client.start_workflow + StartWorkflow:ChatbotWorkflow + RunWorkflow:ChatbotWorkflow + Session Apr 03 14:30 + Query: "What's the weather in NYC?" + StartActivity:call_openai + RunActivity:call_openai + Call OpenAI + openai.responses.create +``` + +Note: `StartFoo` and `RunFoo` appear as siblings. The start is the short-lived outbound RPC that enqueues work on a task queue and completes immediately, and the run is the actual execution which may be delayed and may take much longer. + +An actual look at the LangSmith UI: + +![Screenshot: LangSmith trace tree with add_temporal_runs=True showing Temporal operation nodes](images/langsmith-with-temporal.png) + +And here is a waterfall view of the Workflow in Temporal UI: + +![Screenshot: Temporal UI showing the corresponding Workflow execution](images/temporal-ui.png) + +## Migrating Existing LangSmith Code to Temporal + +If you already have code with LangSmith tracing, you should be able to move it into a Temporal Workflow and keep the same trace hierarchy. The Plugin handles sandbox restrictions and context propagation behind the scenes, so anything that was traceable before should remain traceable after the move. More details below: + +### Where `@traceable` Works + +The Plugin allows `@traceable` to work inside Temporal's deterministic Workflow sandbox, where it normally can't run. Note that `@traceable` on an Activity fires on each retry. + +| Location | Works? | Notes | +|-------------------------------|--------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Inside Workflow methods | Yes | Traces called from inside `@workflow.run`, `@workflow.signal`, etc.; can trace sync and async methods | +| Inside Activity methods | Yes | Traces called from inside `@activity.defn`; can trace sync and async methods | +| On `@activity.defn` functions | Yes | Must stack `@traceable` decorator on top of `@activity.defn` decorator for correct functionality. *Note*: This trace fires on every retry; see [Wrapping Retriable Steps section](#example-wrapping-retriable-steps-in-a-trace) for more info | +| On `@workflow.defn` classes | No | Use `@traceable` inside `@workflow.run` instead. Decorating the workflow class or the `@workflow.run` function is not supported. | + +## Replay Safety + +Temporal Workflows are deterministic and get replayed from event history on recovery. The Plugin accounts for this by injecting replay-safe data into your traceable runs: + +- **No duplicate traces on replay.** Run IDs are derived deterministically from the Workflow's random seed, so replayed operations produce the same IDs and LangSmith deduplicates them. +- **No non-deterministic calls.** The Plugin injects metadata using `workflow.now()` for timestamps and `workflow.random()` for UUIDs instead of `datetime.now()` and `uuid4()`. +- **Background I/O stays outside the sandbox.** LangSmith HTTP calls to the server are submitted to a background thread pool that doesn't interfere with the deterministic Workflow execution. + +You don't need to do anything special for this. Your `@traceable` functions behave the same whether it's a fresh execution or a replay. + +### Example: Worker Crash Mid-Workflow + +``` +1. Workflow starts, executes Activity A -> trace appears in LangSmith +2. Worker crashes during Activity B +3. New Worker picks up the Workflow +4. Workflow replays Activity A (skips execution) -> NO duplicate trace +5. Workflow executes Activity B (new work) -> new trace appears +``` + +As you can see in the UI example below, a crash in the `Call OpenAI` activity didn't cause earlier traces to be duplicated: + +![Screenshot: LangSmith showing a Workflow trace that survived a Worker restart with no duplicate runs](images/langsmith-with-crash-no-temporal.png) + +### Example: Wrapping Retriable Steps in a Trace + +Since Temporal retries failed Activities, you can use an outer `@traceable` to group the attempts together: + +```python +@traceable(name="Call OpenAI", run_type="llm") +@activity.defn +async def call_openai(...): + ... + +@traceable(name="my_step", run_type="chain") +async def my_step(message: str) -> str: + return await workflow.execute_activity( + call_openai, + ... + ) +``` + +This groups everything under one run: +``` +my_step + Call OpenAI # first attempt + openai.responses.create + Call OpenAI # retry + openai.responses.create +``` + +## Context Propagation + +The Plugin propagates trace context across process boundaries (Client -> Workflow -> Activity -> Child Workflow -> Nexus) via Temporal headers. You don't need to pass any context manually. + +``` +Client Process Worker Process (Workflow) Worker Process (Activity) +───────────── ────────────────────────── ───────────────────────── +@traceable("my workflow") + start_workflow ──headers──> RunWorkflow + @traceable("session") + execute_activity ──headers──> RunActivity + @traceable("Call OpenAI") + openai.create(...) +``` + +## API Reference + +### `LangSmithPlugin` + +```python +LangSmithPlugin( + client=None, # langsmith.Client instance (auto-created if None) + project_name=None, # LangSmith project name + add_temporal_runs=False, # Show Temporal operation nodes in traces + default_metadata=None, # Custom metadata attached to all LangSmith traces (https://docs.smith.langchain.com/observability/how_to_guides/add_metadata_tags) + default_tags=None, # Custom tags attached to all LangSmith traces (see link above) +) +``` + +We recommend registering the Plugin on both the Client and all Workers. Strictly speaking, you only need it on the sides that produce traces, but adding it everywhere avoids surprises with context propagation. The Client and Worker don't need to share the same configuration — for example, they can use different `add_temporal_runs` settings. diff --git a/temporalio/contrib/langsmith/__init__.py b/temporalio/contrib/langsmith/__init__.py new file mode 100644 index 000000000..c174fe92b --- /dev/null +++ b/temporalio/contrib/langsmith/__init__.py @@ -0,0 +1,18 @@ +"""LangSmith integration for Temporal SDK. + +.. warning:: + This package is experimental and may change in future versions. + Use with caution in production environments. + +This package provides LangSmith tracing integration for Temporal workflows, +activities, and other operations. It includes automatic run creation and +context propagation for distributed tracing in LangSmith. +""" + +from temporalio.contrib.langsmith._interceptor import LangSmithInterceptor +from temporalio.contrib.langsmith._plugin import LangSmithPlugin + +__all__ = [ + "LangSmithInterceptor", + "LangSmithPlugin", +] diff --git a/temporalio/contrib/langsmith/_interceptor.py b/temporalio/contrib/langsmith/_interceptor.py new file mode 100644 index 000000000..a7eea0714 --- /dev/null +++ b/temporalio/contrib/langsmith/_interceptor.py @@ -0,0 +1,983 @@ +"""LangSmith interceptor implementation for Temporal SDK.""" + +from __future__ import annotations + +import json +import logging +import random +import uuid +from collections.abc import Callable, Iterator, Mapping, Sequence +from concurrent.futures import Future, ThreadPoolExecutor +from contextlib import contextmanager +from typing import Any, ClassVar, NoReturn, Protocol + +import langsmith +import langsmith.utils +import nexusrpc.handler +from langsmith import tracing_context +from langsmith.run_helpers import get_current_run_tree +from langsmith.run_trees import RunTree, WriteReplica + +import temporalio.activity +import temporalio.client +import temporalio.converter +import temporalio.worker +import temporalio.workflow +from temporalio.api.common.v1 import Payload +from temporalio.exceptions import ApplicationError, ApplicationErrorCategory + +# This logger is only used in _log_future_exception, which runs on the +# executor thread (not the workflow thread). Never log directly from +# workflow interceptor code — the sandbox blocks logging I/O. +logger = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- + +HEADER_KEY = "_temporal-langsmith-context" + +_BUILTIN_QUERIES: frozenset[str] = frozenset( + { + "__stack_trace", + "__enhanced_stack_trace", + } +) + + +# --------------------------------------------------------------------------- +# Context helpers +# --------------------------------------------------------------------------- + +_payload_converter = temporalio.converter.PayloadConverter.default + + +class _InputWithHeaders(Protocol): + headers: Mapping[str, Payload] + + +def _inject_context( + headers: Mapping[str, Payload], + run_tree: RunTree, +) -> dict[str, Payload]: + """Inject LangSmith context into Temporal payload headers. + + Serializes the run's trace context (trace ID, parent run ID, dotted order) + into a Temporal header under ``_temporal-langsmith-context``, enabling parent-child + trace nesting across process boundaries (client → worker, workflow → activity). + """ + ls_headers = run_tree.to_headers() + return { + **headers, + HEADER_KEY: _payload_converter.to_payloads([ls_headers])[0], + } + + +def _inject_current_context( + headers: Mapping[str, Payload], +) -> Mapping[str, Payload]: + """Inject the current ambient LangSmith context into Temporal payload headers. + + Reads ``_get_current_run_for_propagation()`` and injects if present. Returns + headers unchanged if no context is active. Called unconditionally so that + context propagation is independent of the ``add_temporal_runs`` toggle. + """ + current = _get_current_run_for_propagation() + if current is not None: + return _inject_context(headers, current) + return headers + + +def _extract_context( + headers: Mapping[str, Payload], + executor: ThreadPoolExecutor, + ls_client: langsmith.Client, +) -> _ReplaySafeRunTree | None: + """Extract LangSmith context from Temporal payload headers. + + Reconstructs a ``RunTree`` from the ``_temporal-langsmith-context`` header on + the receiving side, wrapped in a :class:`_ReplaySafeRunTree` so inbound + interceptors can establish a parent-child relationship with the sender's + run. Returns ``None`` if no header is present. + """ + header = headers.get(HEADER_KEY) + if not header: + return None + ls_headers = _payload_converter.from_payloads([header])[0] + run = RunTree.from_headers(ls_headers) + if run is None: + return None + run.ls_client = ls_client + return _ReplaySafeRunTree(run, executor=executor) + + +def _inject_nexus_context( + headers: Mapping[str, str], + run_tree: RunTree, +) -> dict[str, str]: + """Inject LangSmith context into Nexus string headers.""" + ls_headers = run_tree.to_headers() + return { + **headers, + HEADER_KEY: json.dumps(ls_headers), + } + + +def _extract_nexus_context( + headers: Mapping[str, str], + executor: ThreadPoolExecutor, + ls_client: langsmith.Client, +) -> _ReplaySafeRunTree | None: + """Extract LangSmith context from Nexus string headers.""" + raw = headers.get(HEADER_KEY) + if not raw: + return None + ls_headers = json.loads(raw) + run = RunTree.from_headers(ls_headers) + if run is None: + return None + run.ls_client = ls_client + return _ReplaySafeRunTree(run, executor=executor) + + +def _get_current_run_for_propagation() -> RunTree | None: + """Get the current ambient run for context propagation. + + Filters out ``_RootReplaySafeRunTreeFactory``, which is internal + scaffolding that should never be serialized into headers or used as + parent runs. + """ + run = get_current_run_tree() + if isinstance(run, _RootReplaySafeRunTreeFactory): + return None + return run + + +# --------------------------------------------------------------------------- +# Workflow event loop safety: override @traceable's aio_to_thread +# --------------------------------------------------------------------------- + +_aio_to_thread_override_installed = False + + +async def _temporal_aio_to_thread( + default_aio_to_thread: Callable[..., Any], + ctx: Any, + func: Callable[..., Any], + /, + *args: Any, + **kwargs: Any, +) -> Any: + """Run LangSmith's ``aio_to_thread`` synchronously inside Temporal workflows. + + The ``@traceable`` decorator on async functions uses ``aio_to_thread()`` → + ``loop.run_in_executor()`` for run setup/teardown. The Temporal workflow + event loop does not support ``run_in_executor``. This override runs those + functions synchronously on the workflow thread when inside a workflow, + and delegates to the default implementation outside workflows. + + Registered via ``langsmith.set_runtime_overrides(aio_to_thread=...)``. + """ + if not temporalio.workflow.in_workflow(): + return await default_aio_to_thread(ctx, func, *args, **kwargs) + with temporalio.workflow.unsafe.sandbox_unrestricted(): + return ctx.run(func, *args, **kwargs) + + +def _install_aio_to_thread_override() -> None: + """Install the ``aio_to_thread`` override via LangSmith's official API. + + Safe to call multiple times; the override is only installed once. + """ + global _aio_to_thread_override_installed # noqa: PLW0603 + if _aio_to_thread_override_installed: + return + langsmith.set_runtime_overrides(aio_to_thread=_temporal_aio_to_thread) + _aio_to_thread_override_installed = True + + +# --------------------------------------------------------------------------- +# Replay safety +# --------------------------------------------------------------------------- + + +def _is_replaying() -> bool: + """Check if we're currently replaying workflow history.""" + return ( + temporalio.workflow.in_workflow() + and temporalio.workflow.unsafe.is_replaying_history_events() + ) + + +def _get_workflow_random() -> random.Random | None: + """Get a deterministic random generator for the current workflow. + + Creates a workflow-safe random generator once via + ``workflow.new_random()`` and stores it on the workflow instance so + subsequent calls return the same generator. The generator is seeded + from the workflow's deterministic seed, so it produces identical UUIDs + across replays and worker restarts. + + Returns ``None`` outside a workflow, in read-only (query) contexts, or + when workflow APIs are mocked (unit tests). + """ + try: + if not temporalio.workflow.in_workflow(): + return None + if temporalio.workflow.unsafe.is_read_only(): + return None + inst = temporalio.workflow.instance() + rng = getattr(inst, "__temporal_langsmith_random", None) + if rng is None: + rng = temporalio.workflow.new_random() + setattr(inst, "__temporal_langsmith_random", rng) + return rng + except Exception: + return None + + +def _uuid_from_random(rng: random.Random) -> uuid.UUID: + """Generate a deterministic UUID4 from a workflow-bound random generator.""" + return uuid.UUID(int=rng.getrandbits(128), version=4) + + +# --------------------------------------------------------------------------- +# _ReplaySafeRunTree wrapper +# --------------------------------------------------------------------------- + + +class _ReplaySafeRunTree(RunTree): + """Wrapper around a ``RunTree`` with replay-safe ``post``, ``end``, and ``patch``. + + Inherits from ``RunTree`` so ``isinstance`` checks pass, but does + **not** call ``super().__init__()``—the wrapped ``_run`` is the real + RunTree. Attribute access is delegated via ``__getattr__``/``__setattr__``. + + During replay, ``post()``, ``end()``, and ``patch()`` become no-ops + (I/O suppression), but ``create_child()`` still runs to maintain + parent-child linkage so ``@traceable``'s ``_setup_run`` can build the + run tree across the replay boundary. In workflow context, ``post()`` + and ``patch()`` submit to a single-worker ``ThreadPoolExecutor`` for + FIFO ordering, avoiding blocking on the workflow task thread. + """ + + def __init__( # pyright: ignore[reportMissingSuperCall] + self, + run_tree: RunTree, + *, + executor: ThreadPoolExecutor, + ) -> None: + """Wrap an existing RunTree with replay-safe overrides.""" + object.__setattr__(self, "_run", run_tree) + object.__setattr__(self, "_executor", executor) + + def __getattr__(self, name: str) -> Any: + """Delegate attribute access to the wrapped RunTree.""" + return getattr(self._run, name) + + def __setattr__(self, name: str, value: Any) -> None: + """Delegate attribute setting to the wrapped RunTree.""" + setattr(self._run, name, value) + + def to_headers(self) -> dict[str, str]: + """Delegate to the wrapped RunTree's to_headers.""" + return self._run.to_headers() + + def _inject_deterministic_ids(self, kwargs: dict[str, Any]) -> None: + """Inject deterministic run_id and start_time in workflow context.""" + if temporalio.workflow.in_workflow(): + if kwargs.get("run_id") is None: + rng = _get_workflow_random() + if rng is not None: + kwargs["run_id"] = _uuid_from_random(rng) + if kwargs.get("start_time") is None: + kwargs["start_time"] = temporalio.workflow.now() + + def create_child(self, *args: Any, **kwargs: Any) -> _ReplaySafeRunTree: + """Create a child run, returning another _ReplaySafeRunTree. + + In workflow context, injects deterministic ``run_id`` and ``start_time`` + unless they are passed in manually via ``kwargs``. + """ + self._inject_deterministic_ids(kwargs) + child_run = self._run.create_child(*args, **kwargs) + return _ReplaySafeRunTree(child_run, executor=self._executor) + + def _submit(self, fn: Callable[..., object], *args: Any, **kwargs: Any) -> None: + """Submit work to the background executor.""" + + def _log_future_exception(future: Future[None]) -> None: + exc = future.exception() + if exc is not None: + logger.error("LangSmith background I/O error: %s", exc) + + future = self._executor.submit(fn, *args, **kwargs) + future.add_done_callback(_log_future_exception) + + def post(self, exclude_child_runs: bool = True) -> None: + """Post the run to LangSmith, skipping during replay.""" + if temporalio.workflow.in_workflow(): + if _is_replaying(): + return + self._submit(self._run.post, exclude_child_runs=exclude_child_runs) + else: + self._run.post(exclude_child_runs=exclude_child_runs) + + def end(self, **kwargs: Any) -> None: + """End the run, skipping during replay. + + Pre-computes ``end_time`` via ``workflow.now()`` in workflow context + so ``RunTree.end()`` doesn't call ``datetime.now()`` (non-deterministic + and sandbox-restricted). + """ + if _is_replaying(): + return + if temporalio.workflow.in_workflow(): + kwargs.setdefault("end_time", temporalio.workflow.now()) + self._run.end(**kwargs) + + def patch(self, *, exclude_inputs: bool = False) -> None: + """Patch the run to LangSmith, skipping during replay.""" + if temporalio.workflow.in_workflow(): + if _is_replaying(): + return + self._submit(self._run.patch, exclude_inputs=exclude_inputs) + else: + self._run.patch(exclude_inputs=exclude_inputs) + + +class _RootReplaySafeRunTreeFactory(_ReplaySafeRunTree): + """Factory that produces independent root ``_ReplaySafeRunTree`` instances with no parent link. + + When ``add_temporal_runs=False`` and no parent was propagated via headers, + ``@traceable`` functions still need *something* in the LangSmith + ``tracing_context`` to call ``create_child()`` on — otherwise they + cannot create ``_ReplaySafeRunTree`` children at all and instead default to + creating generic ``RunTree``s, which are not replay safe. This class fills + that role: it sits in the context as the nominal parent so + ``@traceable`` has a ``create_child()`` target. + + However, ``create_child()`` deliberately creates fresh ``RunTree`` + instances with **no** ``parent_run_id``. This means every child appears + as an independent root run in LangSmith rather than being nested under + a phantom parent that was never meant to be visible. + + ``post()``, ``patch()``, and ``end()`` all raise ``RuntimeError`` + because this object is purely internal scaffolding — it must never + appear in LangSmith. If any of these methods are called, it indicates + a programming error. + """ + + def __init__( # pyright: ignore[reportMissingSuperCall] + self, + *, + ls_client: langsmith.Client, + executor: ThreadPoolExecutor, + session_name: str | None = None, + replicas: Sequence[WriteReplica] | None = None, + ) -> None: + """Create a root factory with the given LangSmith client.""" + # Create a minimal RunTree for the factory — it will never be posted + factory_run = RunTree( + name="__root_factory__", + run_type="chain", + ls_client=ls_client, + ) + if session_name is not None: + factory_run.session_name = session_name + if replicas is not None: + factory_run.replicas = replicas + object.__setattr__(self, "_run", factory_run) + object.__setattr__(self, "_executor", executor) + + def post(self, exclude_child_runs: bool = True) -> NoReturn: + """Factory must never be posted.""" + raise RuntimeError("_RootReplaySafeRunTreeFactory must never be posted") + + def patch(self, *, exclude_inputs: bool = False) -> NoReturn: + """Factory must never be patched.""" + raise RuntimeError("_RootReplaySafeRunTreeFactory must never be patched") + + def end(self, **kwargs: Any) -> NoReturn: + """Factory must never be ended.""" + raise RuntimeError("_RootReplaySafeRunTreeFactory must never be ended") + + def create_child(self, *args: Any, **kwargs: Any) -> _ReplaySafeRunTree: + """Create a root _ReplaySafeRunTree (no parent_run_id). + + Creates a fresh ``RunTree(...)`` directly (bypassing + ``self._run.create_child``) so children are independent root runs + with no link back to the factory. + """ + self._inject_deterministic_ids(kwargs) + + # RunTree expects "id", but callers pass "run_id". RunTree.create_child + # also does the same mapping internally. + if "run_id" in kwargs: + kwargs["id"] = kwargs.pop("run_id") + + # Inherit ls_client and session_name from factory. + # session_name must be passed at construction time. + kwargs.setdefault("ls_client", self._run.ls_client) + kwargs.setdefault("session_name", self._run.session_name) + + child_run = RunTree(*args, **kwargs) + # Replicas must be set post-construction + if self._run.replicas is not None: + child_run.replicas = self._run.replicas + return _ReplaySafeRunTree(child_run, executor=self._executor) + + +# --------------------------------------------------------------------------- +# _maybe_run context manager +# --------------------------------------------------------------------------- + + +def _is_benign_error(exc: Exception) -> bool: + """Check if an exception is a benign ApplicationError.""" + return ( + isinstance(exc, ApplicationError) + and getattr(exc, "category", None) == ApplicationErrorCategory.BENIGN + ) + + +@contextmanager +def _maybe_run( + client: langsmith.Client, + name: str, + *, + add_temporal_runs: bool, + run_type: str = "chain", + inputs: dict[str, Any] | None = None, + metadata: dict[str, Any] | None = None, + tags: list[str] | None = None, + parent: RunTree | None = None, + project_name: str | None = None, + executor: ThreadPoolExecutor, +) -> Iterator[None]: + """Create a LangSmith run, handling errors. + + - If add_temporal_runs is False **or** ``langsmith.utils.tracing_is_enabled()`` + returns False, yields None (no run created). + Context propagation is handled unconditionally by callers. + - When a run IS created, uses :class:`_ReplaySafeRunTree` for + replay and event loop safety, then sets it as ambient context via + ``tracing_context(parent=run_tree)`` so ``get_current_run_tree()`` + returns it and ``_inject_current_context()`` can inject it. + - On exception: marks run as errored (unless benign ApplicationError), re-raises. + + Note on ``tracing_is_enabled()`` and cross-process traces: + ``tracing_is_enabled()`` checks for an active run tree in context + *before* consulting the ``LANGSMITH_TRACING`` env var (langsmith + semantics). If a parent run is propagated into this worker via + headers from an upstream tracer, tracing continues regardless of + ``LANGSMITH_TRACING=false``. This matches langsmith's "continue + mid-trace" model: the env var suppresses *new* local traces but + does not break an inbound parent trace. + + Args: + client: LangSmith client instance. + name: Display name for the run. + add_temporal_runs: Whether to create Temporal-level trace runs. + run_type: LangSmith run type (default ``"chain"``). + inputs: Input data to record on the run. + metadata: Extra metadata to attach to the run. + tags: Tags to attach to the run. + parent: Parent run for nesting. + project_name: LangSmith project name override. + executor: ThreadPoolExecutor for background I/O. + """ + if not add_temporal_runs or not langsmith.utils.tracing_is_enabled(): + yield None + return + + # If no explicit parent, inherit from ambient @traceable context + if parent is None: + parent = _get_current_run_for_propagation() + + run_tree_args: dict[str, Any] = dict( + name=name, + run_type=run_type, + inputs=inputs or {}, + ls_client=client, + ) + # Deterministic IDs so replayed workflows produce identical runs + # instead of duplicates (see _get_workflow_random for details). + rng = _get_workflow_random() + # In read-only contexts (queries, update validators), _get_workflow_random() + # returns None. Deterministic IDs aren't needed — these aren't replayed. + # LangSmith will auto-generate a random UUID. + if rng is not None: + run_tree_args["id"] = _uuid_from_random(rng) + run_tree_args["start_time"] = temporalio.workflow.now() + if project_name is not None: + run_tree_args["project_name"] = project_name + if parent is not None: + run_tree_args["parent_run"] = parent + if metadata: + run_tree_args["extra"] = {"metadata": metadata} + if tags: + run_tree_args["tags"] = tags + run_tree = _ReplaySafeRunTree(RunTree(**run_tree_args), executor=executor) + run_tree.post() + try: + with tracing_context(parent=run_tree, client=client): + yield None + except Exception as exc: + if not _is_benign_error(exc): + run_tree.end(error=f"{type(exc).__name__}: {exc}") + run_tree.patch() + raise + else: + run_tree.end(outputs={"status": "ok"}) + run_tree.patch() + + +# --------------------------------------------------------------------------- +# LangSmithInterceptor +# --------------------------------------------------------------------------- + + +class LangSmithInterceptor( + temporalio.client.Interceptor, temporalio.worker.Interceptor +): + """Interceptor that supports client and worker LangSmith run creation + and context propagation. + + .. warning:: + This class is experimental and may change in future versions. + Use with caution in production environments. + """ + + def __init__( + self, + *, + client: langsmith.Client | None = None, + project_name: str | None = None, + add_temporal_runs: bool = False, + default_metadata: dict[str, Any] | None = None, + default_tags: list[str] | None = None, + ) -> None: + """Initialize the LangSmith interceptor with tracing configuration.""" + super().__init__() + if client is None: + client = langsmith.Client() + self._client = client + self._project_name = project_name + self._add_temporal_runs = add_temporal_runs + self._default_metadata = default_metadata or {} + self._default_tags = default_tags or [] + self._executor = ThreadPoolExecutor(max_workers=1) + + @contextmanager + def maybe_run( + self, + name: str, + *, + run_type: str = "chain", + parent: RunTree | None = None, + extra_metadata: dict[str, Any] | None = None, + ) -> Iterator[None]: + """Create a LangSmith run with this interceptor's config already applied.""" + metadata = {**self._default_metadata, **(extra_metadata or {})} + with _maybe_run( + self._client, + name, + add_temporal_runs=self._add_temporal_runs, + run_type=run_type, + metadata=metadata, + tags=list(self._default_tags), + parent=parent, + executor=self._executor, + project_name=self._project_name, + ) as run: + yield run + + def intercept_client( + self, next: temporalio.client.OutboundInterceptor + ) -> temporalio.client.OutboundInterceptor: + """Create a client outbound interceptor for LangSmith tracing.""" + return _LangSmithClientOutboundInterceptor(next, self) + + def intercept_activity( + self, next: temporalio.worker.ActivityInboundInterceptor + ) -> temporalio.worker.ActivityInboundInterceptor: + """Create an activity inbound interceptor for LangSmith tracing.""" + return _LangSmithActivityInboundInterceptor(next, self) + + def workflow_interceptor_class( + self, input: temporalio.worker.WorkflowInterceptorClassInput + ) -> type[_LangSmithWorkflowInboundInterceptor]: + """Return the workflow interceptor class with config bound.""" + _install_aio_to_thread_override() + config = self + + class InterceptorWithConfig(_LangSmithWorkflowInboundInterceptor): + _config = config + + return InterceptorWithConfig + + def intercept_nexus_operation( + self, next: temporalio.worker.NexusOperationInboundInterceptor + ) -> temporalio.worker.NexusOperationInboundInterceptor: + """Create a Nexus operation inbound interceptor for LangSmith tracing.""" + return _LangSmithNexusOperationInboundInterceptor(next, self) + + +# --------------------------------------------------------------------------- +# Client Outbound Interceptor +# --------------------------------------------------------------------------- + + +class _LangSmithClientOutboundInterceptor(temporalio.client.OutboundInterceptor): + """Instruments all client-side calls with LangSmith runs.""" + + def __init__( + self, + next: temporalio.client.OutboundInterceptor, + config: LangSmithInterceptor, + ) -> None: + super().__init__(next) + self._config = config + + @contextmanager + def _traced_call(self, name: str, input: _InputWithHeaders) -> Iterator[None]: + """Wrap a client call with a LangSmith run and inject context into headers.""" + with self._config.maybe_run(name): + input.headers = _inject_current_context(input.headers) + yield + + @contextmanager + def _traced_start(self, name: str, input: _InputWithHeaders) -> Iterator[None]: + """Wrap a start operation, injecting ambient parent context before creating the run. + + Unlike ``_traced_call``, this injects headers *before* ``maybe_run`` + so the downstream ``RunFoo`` becomes a sibling of ``StartFoo`` rather + than a child. + """ + input.headers = _inject_current_context(input.headers) + with self._config.maybe_run(name): + yield + + async def start_workflow( + self, input: temporalio.client.StartWorkflowInput + ) -> temporalio.client.WorkflowHandle[Any, Any]: + prefix = "SignalWithStartWorkflow" if input.start_signal else "StartWorkflow" + with self._traced_start(f"{prefix}:{input.workflow}", input): + return await super().start_workflow(input) + + async def query_workflow(self, input: temporalio.client.QueryWorkflowInput) -> Any: + with self._traced_call(f"QueryWorkflow:{input.query}", input): + return await super().query_workflow(input) + + async def signal_workflow( + self, input: temporalio.client.SignalWorkflowInput + ) -> None: + with self._traced_call(f"SignalWorkflow:{input.signal}", input): + return await super().signal_workflow(input) + + async def start_workflow_update( + self, input: temporalio.client.StartWorkflowUpdateInput + ) -> temporalio.client.WorkflowUpdateHandle[Any]: + with self._traced_call(f"StartWorkflowUpdate:{input.update}", input): + return await super().start_workflow_update(input) + + async def start_update_with_start_workflow( + self, input: temporalio.client.StartWorkflowUpdateWithStartInput + ) -> temporalio.client.WorkflowUpdateHandle[Any]: + input.start_workflow_input.headers = _inject_current_context( + input.start_workflow_input.headers + ) + input.update_workflow_input.headers = _inject_current_context( + input.update_workflow_input.headers + ) + with self._config.maybe_run( + f"StartUpdateWithStartWorkflow:{input.start_workflow_input.workflow}", + ): + return await super().start_update_with_start_workflow(input) + + +# --------------------------------------------------------------------------- +# Activity Inbound Interceptor +# --------------------------------------------------------------------------- + + +class _LangSmithActivityInboundInterceptor( + temporalio.worker.ActivityInboundInterceptor +): + """Instruments activity execution with LangSmith runs.""" + + def __init__( + self, + next: temporalio.worker.ActivityInboundInterceptor, + config: LangSmithInterceptor, + ) -> None: + super().__init__(next) + self._config = config + + async def execute_activity( + self, input: temporalio.worker.ExecuteActivityInput + ) -> Any: + parent = _extract_context( + input.headers, self._config._executor, self._config._client + ) + info = temporalio.activity.info() + extra_metadata = { + "temporalWorkflowID": info.workflow_id or "", + "temporalRunID": info.workflow_run_id or "", + "temporalActivityID": info.activity_id or "", + } + tracing_args: dict[str, Any] = { + "client": self._config._client, + "project_name": self._config._project_name, + "parent": parent, + } + with tracing_context(**tracing_args): + with self._config.maybe_run( + f"RunActivity:{info.activity_type}", + run_type="tool", + parent=parent, + extra_metadata=extra_metadata, + ): + return await super().execute_activity(input) + + +# --------------------------------------------------------------------------- +# Workflow Inbound Interceptor +# --------------------------------------------------------------------------- + + +class _LangSmithWorkflowInboundInterceptor( + temporalio.worker.WorkflowInboundInterceptor +): + """Instruments workflow execution with LangSmith runs.""" + + _config: ClassVar[LangSmithInterceptor] + + def init(self, outbound: temporalio.worker.WorkflowOutboundInterceptor) -> None: + super().init(_LangSmithWorkflowOutboundInterceptor(outbound, self._config)) + + @contextmanager + def _workflow_maybe_run( + self, + name: str, + headers: Mapping[str, Payload] | None = None, + ) -> Iterator[None]: + """Workflow-specific run creation with metadata. + + Extracts parent from headers (if provided) and sets up + ``tracing_context`` so ``@traceable`` functions called from workflow + code can discover the parent and LangSmith client, independent of the + ``add_temporal_runs`` toggle. + """ + parent = ( + _extract_context(headers, self._config._executor, self._config._client) + if headers + else None + ) + # When add_temporal_runs=False and no external parent, create a + # _RootReplaySafeRunTreeFactory so @traceable calls get a + # _ReplaySafeRunTree parent via create_child. The factory is + # invisible in LangSmith. + # tracing_parent can be None when add_temporal_runs=True but no parent was + # propagated via headers — maybe_run will later create a root run in that case. + tracing_parent: _ReplaySafeRunTree | _RootReplaySafeRunTreeFactory | None = ( + parent + if parent is not None or self._config._add_temporal_runs + else _RootReplaySafeRunTreeFactory( + ls_client=self._config._client, + executor=self._config._executor, + session_name=self._config._project_name, + ) + ) + tracing_args: dict[str, Any] = { + "client": self._config._client, + "project_name": self._config._project_name, + "parent": tracing_parent, + } + info = temporalio.workflow.info() + extra_metadata = { + "temporalWorkflowID": info.workflow_id, + "temporalRunID": info.run_id, + } + with tracing_context(**tracing_args): + with self._config.maybe_run( + name, + parent=parent, + extra_metadata=extra_metadata, + ) as run: + yield run + + async def execute_workflow( + self, input: temporalio.worker.ExecuteWorkflowInput + ) -> Any: + wf_type = temporalio.workflow.info().workflow_type + with self._workflow_maybe_run( + f"RunWorkflow:{wf_type}", + input.headers, + ): + return await super().execute_workflow(input) + + async def handle_signal(self, input: temporalio.worker.HandleSignalInput) -> None: + with self._workflow_maybe_run(f"HandleSignal:{input.signal}", input.headers): + return await super().handle_signal(input) + + async def handle_query(self, input: temporalio.worker.HandleQueryInput) -> Any: + if input.query.startswith("__temporal") or input.query in _BUILTIN_QUERIES: + return await super().handle_query(input) + with self._workflow_maybe_run(f"HandleQuery:{input.query}", input.headers): + return await super().handle_query(input) + + def handle_update_validator( + self, input: temporalio.worker.HandleUpdateInput + ) -> None: + with self._workflow_maybe_run(f"ValidateUpdate:{input.update}", input.headers): + return super().handle_update_validator(input) + + async def handle_update_handler( + self, input: temporalio.worker.HandleUpdateInput + ) -> Any: + with self._workflow_maybe_run(f"HandleUpdate:{input.update}", input.headers): + return await super().handle_update_handler(input) + + +# --------------------------------------------------------------------------- +# Workflow Outbound Interceptor +# --------------------------------------------------------------------------- + + +class _LangSmithWorkflowOutboundInterceptor( + temporalio.worker.WorkflowOutboundInterceptor +): + """Instruments all outbound calls from workflow code.""" + + def __init__( + self, + next: temporalio.worker.WorkflowOutboundInterceptor, + config: LangSmithInterceptor, + ) -> None: + super().__init__(next) + self._config = config + + @contextmanager + def _traced_outbound(self, name: str, input: _InputWithHeaders) -> Iterator[None]: + """Outbound workflow run creation with context injection into input.headers. + + Uses ambient context so ``@traceable`` step functions that wrap + outbound calls correctly parent the outbound run under themselves. + """ + context_source = _get_current_run_for_propagation() + with self._config.maybe_run(name): + if context_source: + input.headers = _inject_context(input.headers, context_source) + yield None + + def start_activity( + self, input: temporalio.worker.StartActivityInput + ) -> temporalio.workflow.ActivityHandle[Any]: + with self._traced_outbound(f"StartActivity:{input.activity}", input): + return super().start_activity(input) + + def start_local_activity( + self, input: temporalio.worker.StartLocalActivityInput + ) -> temporalio.workflow.ActivityHandle[Any]: + with self._traced_outbound(f"StartActivity:{input.activity}", input): + return super().start_local_activity(input) + + async def start_child_workflow( + self, input: temporalio.worker.StartChildWorkflowInput + ) -> temporalio.workflow.ChildWorkflowHandle[Any, Any]: + with self._traced_outbound(f"StartChildWorkflow:{input.workflow}", input): + return await super().start_child_workflow(input) + + async def signal_child_workflow( + self, input: temporalio.worker.SignalChildWorkflowInput + ) -> None: + with self._traced_outbound(f"SignalChildWorkflow:{input.signal}", input): + return await super().signal_child_workflow(input) + + async def signal_external_workflow( + self, input: temporalio.worker.SignalExternalWorkflowInput + ) -> None: + with self._traced_outbound(f"SignalExternalWorkflow:{input.signal}", input): + return await super().signal_external_workflow(input) + + def continue_as_new(self, input: temporalio.worker.ContinueAsNewInput) -> NoReturn: + # No trace created, but inject context from ambient run + current_run = _get_current_run_for_propagation() + if current_run: + input.headers = _inject_context(input.headers, current_run) + super().continue_as_new(input) + + async def start_nexus_operation( + self, input: temporalio.worker.StartNexusOperationInput[Any, Any] + ) -> temporalio.workflow.NexusOperationHandle[Any]: + context_source = _get_current_run_for_propagation() + with self._config.maybe_run( + f"StartNexusOperation:{input.service}/{input.operation_name}", + ): + if context_source: + input.headers = _inject_nexus_context( + input.headers or {}, context_source + ) + return await super().start_nexus_operation(input) + + +# --------------------------------------------------------------------------- +# Nexus Operation Inbound Interceptor +# --------------------------------------------------------------------------- + + +class _LangSmithNexusOperationInboundInterceptor( + temporalio.worker.NexusOperationInboundInterceptor +): + """Instruments Nexus operations with LangSmith runs.""" + + def __init__( + self, + next: temporalio.worker.NexusOperationInboundInterceptor, + config: LangSmithInterceptor, + ) -> None: + super().__init__(next) + self._config = config + + async def execute_nexus_operation_start( + self, input: temporalio.worker.ExecuteNexusOperationStartInput + ) -> ( + nexusrpc.handler.StartOperationResultSync[Any] + | nexusrpc.handler.StartOperationResultAsync + ): + parent = _extract_nexus_context( + input.ctx.headers, self._config._executor, self._config._client + ) + tracing_args: dict[str, Any] = { + "client": self._config._client, + "project_name": self._config._project_name, + "parent": parent, + } + with tracing_context(**tracing_args): + with self._config.maybe_run( + f"RunStartNexusOperationHandler:{input.ctx.service}/{input.ctx.operation}", + run_type="tool", + parent=parent, + ): + return await self.next.execute_nexus_operation_start(input) + + async def execute_nexus_operation_cancel( + self, input: temporalio.worker.ExecuteNexusOperationCancelInput + ) -> None: + parent = _extract_nexus_context( + input.ctx.headers, self._config._executor, self._config._client + ) + tracing_args: dict[str, Any] = { + "client": self._config._client, + "project_name": self._config._project_name, + "parent": parent, + } + with tracing_context(**tracing_args): + with self._config.maybe_run( + f"RunCancelNexusOperationHandler:{input.ctx.service}/{input.ctx.operation}", + run_type="tool", + parent=parent, + ): + return await self.next.execute_nexus_operation_cancel(input) diff --git a/temporalio/contrib/langsmith/_plugin.py b/temporalio/contrib/langsmith/_plugin.py new file mode 100644 index 000000000..cb2333ff1 --- /dev/null +++ b/temporalio/contrib/langsmith/_plugin.py @@ -0,0 +1,93 @@ +"""LangSmith plugin for Temporal SDK.""" + +from __future__ import annotations + +import dataclasses +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager +from typing import Any + +import langsmith + +# langsmith conditionally imports langchain_core when it is installed. +# Pre-import the lazily-loaded submodule so it is in sys.modules before the +# workflow sandbox starts; otherwise the sandbox's __getattr__-triggered +# import hits restrictions on concurrent.futures.ThreadPoolExecutor. +try: + import langchain_core.runnables.config # noqa: F401 # pyright: ignore[reportUnusedImport] +except ImportError: + pass + +from temporalio.contrib.langsmith._interceptor import LangSmithInterceptor +from temporalio.plugin import SimplePlugin +from temporalio.worker import WorkflowRunner +from temporalio.worker.workflow_sandbox import SandboxedWorkflowRunner + + +class LangSmithPlugin(SimplePlugin): + """LangSmith tracing plugin for Temporal SDK. + + .. warning:: + This class is experimental and may change in future versions. + Use with caution in production environments. + + Provides automatic LangSmith run creation for workflows, activities, + and other Temporal operations with context propagation. + """ + + def __init__( + self, + *, + client: langsmith.Client | None = None, + project_name: str | None = None, + add_temporal_runs: bool = False, + default_metadata: dict[str, Any] | None = None, + default_tags: list[str] | None = None, + ) -> None: + """Initialize the LangSmith plugin. + + Args: + client: A langsmith.Client instance. If None, one will be created + automatically (using LANGSMITH_API_KEY env var). + project_name: LangSmith project name for traces. + add_temporal_runs: Whether to create LangSmith runs for Temporal + operations. Defaults to False. + default_metadata: Default metadata to attach to all runs. + default_tags: Default tags to attach to all runs. + """ + interceptor = LangSmithInterceptor( + client=client, + project_name=project_name, + add_temporal_runs=add_temporal_runs, + default_metadata=default_metadata, + default_tags=default_tags, + ) + interceptors = [interceptor] + + def workflow_runner(runner: WorkflowRunner | None) -> WorkflowRunner: + if not runner: + raise ValueError("No WorkflowRunner provided to the LangSmith plugin.") + if isinstance(runner, SandboxedWorkflowRunner): + return dataclasses.replace( + runner, + restrictions=runner.restrictions.with_passthrough_modules( + "langsmith", + "langchain_core", + "opentelemetry", + ), + ) + return runner + + @asynccontextmanager + async def run_context() -> AsyncIterator[None]: + try: + yield + finally: + interceptor._client.flush() + + super().__init__( + "langchain.LangSmithPlugin", + interceptors=interceptors, + workflow_runner=workflow_runner, + run_context=run_context, + ) diff --git a/temporalio/contrib/langsmith/images/langsmith-no-temporal.png b/temporalio/contrib/langsmith/images/langsmith-no-temporal.png new file mode 100644 index 000000000..037e0d9ba Binary files /dev/null and b/temporalio/contrib/langsmith/images/langsmith-no-temporal.png differ diff --git a/temporalio/contrib/langsmith/images/langsmith-with-crash-no-temporal.png b/temporalio/contrib/langsmith/images/langsmith-with-crash-no-temporal.png new file mode 100644 index 000000000..9a84c5dbe Binary files /dev/null and b/temporalio/contrib/langsmith/images/langsmith-with-crash-no-temporal.png differ diff --git a/temporalio/contrib/langsmith/images/langsmith-with-temporal.png b/temporalio/contrib/langsmith/images/langsmith-with-temporal.png new file mode 100644 index 000000000..fc4c4a76a Binary files /dev/null and b/temporalio/contrib/langsmith/images/langsmith-with-temporal.png differ diff --git a/temporalio/contrib/langsmith/images/temporal-ui.png b/temporalio/contrib/langsmith/images/temporal-ui.png new file mode 100644 index 000000000..55ee90970 Binary files /dev/null and b/temporalio/contrib/langsmith/images/temporal-ui.png differ diff --git a/temporalio/contrib/openai_agents/README.md b/temporalio/contrib/openai_agents/README.md index d28ceb7b5..83539044c 100644 --- a/temporalio/contrib/openai_agents/README.md +++ b/temporalio/contrib/openai_agents/README.md @@ -1,9 +1,7 @@ # OpenAI Agents SDK Integration for Temporal -⚠️ **Public Preview** - The interface to this module is subject to change prior to General Availability. We welcome questions and feedback in the [#python-sdk](https://temporalio.slack.com/archives/CTT84RS0P) Slack channel at [temporalio.slack.com](https://temporalio.slack.com/). - ## Introduction This integration combines [OpenAI Agents SDK](https://github.com/openai/openai-agents-python) with [Temporal's durable execution](https://docs.temporal.io/evaluate/understanding-temporal#durable-execution). @@ -14,14 +12,15 @@ Temporal provides a crash-proof system foundation, taking care of the distribute OpenAI Agents SDK offers a lightweight yet powerful framework for defining those agents. This document is organized as follows: - - **[Hello World Durable Agent](#hello-world-durable-agent).** Your first durable agent example. - - **[Background Concepts](#core-concepts).** Background on durable execution and AI agents. - - **[Full Example](#full-example)** Running the Hello World Durable Agent example. - - **[Tool Calling](#tool-calling).** Calling agent Tools in Temporal. - - **[Feature Support](#feature-support).** Compatibility matrix. -The [samples repository](https://github.com/temporalio/samples-python/tree/main/openai_agents) contains examples including basic usage, common agent patterns, and more complete samples. +- **[Hello World Durable Agent](#hello-world-durable-agent).** Your first durable agent example. +- **[Background Concepts](#core-concepts).** Background on durable execution and AI agents. +- **[Full Example](#full-example)** Running the Hello World Durable Agent example. +- **[Tool Calling](#tool-calling).** Calling agent Tools in Temporal. +- **[Sandbox Support](#sandbox-support).** Running sandbox agents in Temporal. +- **[Feature Support](#feature-support).** Compatibility matrix. +The [samples repository](https://github.com/temporalio/samples-python/tree/main/openai_agents) contains examples including basic usage, common agent patterns, and more complete samples. ## Hello World Durable Agent @@ -54,7 +53,6 @@ The `@workflow.defn` annotation on the `HelloWorldAgent` indicates that this cla We use the `Agent` class from OpenAI Agents SDK to define a simple agent, instructing it to always respond with haikus. We then run that agent, using the `Runner` class from OpenAI Agents SDK, passing through `prompt` as an argument. - We will [complete this example below](#full-example). Before digging further into the code, we will review some background that will make it easier to understand. @@ -70,13 +68,13 @@ In the OpenAI Agents SDK, an agent is an AI model configured with instructions, We describe each of these briefly: -- *AI model*. An LLM such as OpenAI's GPT, Google's Gemini, or one of many others. -- *Instructions*. Also known as a system prompt, the instructions contain the initial input to the model, which configures it for the job it will do. -- *Tools*. Typically, Python functions that the model may choose to invoke. Tools are functions with text-descriptions that explain their functionality to the model. -- *MCP servers*. Best known for providing tools, MCP offers a pluggable standard for interoperability, including file-like resources, prompt templates, and human approvals. MCP servers may be accessed over the network or run in a local process. -- *Guardrails*. Checks on the input or the output of an agent to ensure compliance or safety. Guardrails may be implemented as regular code or as AI agents. -- *Handoffs*. A handoff occurs when an agent delegates a task to another agent. During a handoff the conversation history remains the same, and passes to a new agent with its own model, instructions, tools. -- *Context*. This is an overloaded term. Here, context refers to a framework object that is shared across tools and other code, but is not passed to the model. +- _AI model_. An LLM such as OpenAI's GPT, Google's Gemini, or one of many others. +- _Instructions_. Also known as a system prompt, the instructions contain the initial input to the model, which configures it for the job it will do. +- _Tools_. Typically, Python functions that the model may choose to invoke. Tools are functions with text-descriptions that explain their functionality to the model. +- _MCP servers_. Best known for providing tools, MCP offers a pluggable standard for interoperability, including file-like resources, prompt templates, and human approvals. MCP servers may be accessed over the network or run in a local process. +- _Guardrails_. Checks on the input or the output of an agent to ensure compliance or safety. Guardrails may be implemented as regular code or as AI agents. +- _Handoffs_. A handoff occurs when an agent delegates a task to another agent. During a handoff the conversation history remains the same, and passes to a new agent with its own model, instructions, tools. +- _Context_. This is an overloaded term. Here, context refers to a framework object that is shared across tools and other code, but is not passed to the model. Now, let's see how these components work together. In a common pattern, the model first receives user input and then reasons about which tool to invoke. @@ -126,23 +124,21 @@ As the program makes progress, Temporal saves key inputs and decisions, allowing The key to making this work is to separate the applications repeatable (deterministic) and non-repeatable (non-deterministic) parts: -1. Deterministic pieces, termed *workflows*, execute the same way when re-run with the same inputs. -2. Non-deterministic pieces, termed *activities*, can run arbitrary code, performing I/O and any other operations. +1. Deterministic pieces, termed _workflows_, execute the same way when re-run with the same inputs. +2. Non-deterministic pieces, termed _activities_, can run arbitrary code, performing I/O and any other operations. Workflow code can run for extended periods and, if interrupted, resume exactly where it left off. Activity code faces no restrictions on I/O or external interactions, but if it fails part-way through it restarts from the beginning. -In the AI-agent example above, model invocations and tool calls run inside activities, while the logic that coordinates them lives in the workflow. +In this integration, model invocations are automatically routed through Temporal activities, while the logic that coordinates them lives in the workflow. +Tools that perform I/O or other non-deterministic work should run as Temporal activities, while deterministic, workflow-safe tools can run directly in the workflow. This pattern generalizes to more sophisticated agents. -We refer to that coordinating logic as *agent orchestration*. - -As a general rule, agent orchestration code executes within the Temporal workflow, whereas model calls and any I/O-bound tool invocations execute as Temporal activities. +We refer to the coordinating logic as _agent orchestration_. The diagram below shows the overall architecture of an agentic application in Temporal. The Temporal Server is responsible to tracking program execution and making sure associated state is preserved reliably (i.e., stored to a database, possibly replicated across cloud regions). Temporal Server manages data in encrypted form, so all data processing occurs on the Worker, which runs the workflow and activities. - ```text +---------------------+ | Temporal Server | (Stores workflow state, @@ -157,13 +153,14 @@ Temporal Server manages data in encrypted form, so all data processing occurs on | Worker | | +----------------------------------------------+ | | | Workflow Code | | -| | (Agent Orchestration Loop) | | +| | (Agent orchestration + deterministic tools) | | | +----------------------------------------------+ | | | | | | | v v v | | +-----------+ +-----------+ +-------------+ | | | Activity | | Activity | | Activity | | -| | (Tool 1) | | (Tool 2) | | (Model API) | | +| | (I/O Tool | | (I/O Tool | | (Model API) | | +| | 1) | | 2) | | | | | +-----------+ +-----------+ +-------------+ | | | | | | +------------------------------------------------------+ @@ -172,17 +169,14 @@ Temporal Server manages data in encrypted form, so all data processing occurs on [External APIs, services, databases, etc.] ``` - See the [Temporal documentation](https://docs.temporal.io/evaluate/understanding-temporal#temporal-application-the-building-blocks) for more information. - ## Complete Example To make the [Hello World durable agent](#hello-world-durable-agent) shown earlier available in Temporal, we need to create a worker program. To see it run, we also need a client to launch it. We show these files below. - ### File 2: Launch Worker (`run_worker.py`) ```python @@ -225,12 +219,12 @@ if __name__ == "__main__": We use the `OpenAIAgentsPlugin` to configure Temporal for use with OpenAI Agents SDK. The plugin automatically handles several important setup tasks: + - Ensures proper serialization of Pydantic types - Propagates context for [OpenAI Agents tracing](https://openai.github.io/openai-agents-python/tracing/). - Registers an activity for invoking model calls with the Temporal worker. - Configures OpenAI Agents SDK to run model calls as Temporal activities. - ### File 3: Client Execution (`run_hello_world_workflow.py`) ```python @@ -257,7 +251,8 @@ async def main(): "Tell me about recursion in programming.", id="my-workflow-id", task_queue="my-task-queue", - id_reuse_policy=WorkflowIDReusePolicy.TERMINATE_IF_RUNNING, + id_reuse_policy=WorkflowIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy=WorkflowIDConflictPolicy.TERMINATE_EXISTING, ) print(f"Result: {result}") @@ -268,15 +263,26 @@ if __name__ == "__main__": This file is a standard Temporal launch script. We also configure the client with the `OpenAIAgentsPlugin` to ensure serialization is compatible with the worker. - To run this example, see the detailed instructions in the [Temporal Python Samples Repository](https://github.com/temporalio/samples-python/tree/main/openai_agents). ## Tool Calling +Model invocations are automatically routed through Temporal activities. +OpenAI-hosted tools are passed through the model invocation and executed by the model provider. +User-defined `FunctionTool`s, including tools created with `@function_tool`, are not automatically converted into Temporal activities; they execute in the workflow unless explicitly backed by a Temporal activity. +Where a tool executes depends on how it is defined: + +| Tool | Execution | Use for | +| --- | --- | --- | +| `activity_as_tool()` | Temporal activity | External I/O and non-deterministic operations | +| `FunctionTool` / `@function_tool` | Workflow | Deterministic, workflow-safe computation | +| OpenAI-hosted tool | Model provider | Provider-hosted features executed as part of the model invocation | + ### Temporal Activities as OpenAI Agents Tools One of the powerful features of this integration is the ability to convert Temporal activities into agent tools using `activity_as_tool`. This allows your agent to leverage Temporal's durable execution for tool calls. +`activity_as_tool()` creates an OpenAI Agents `FunctionTool` whose invocation schedules the underlying Temporal activity. In the example below, we apply the `@activity.defn` decorator to the `get_weather` function to create a Temporal activity. We then pass this through the `activity_as_tool` helper function to create an OpenAI Agents tool that is passed to the `Agent`. @@ -308,7 +314,7 @@ class WeatherAgent: instructions="You are a helpful weather agent.", tools=[ openai_agents.workflow.activity_as_tool( - get_weather, + get_weather, start_to_close_timeout=timedelta(seconds=10) ) ], @@ -317,9 +323,23 @@ class WeatherAgent: return result.final_output ``` +The activity must also be registered with a Worker. +`activity_as_tool()` controls how the Agent invokes the activity; it does not register the activity with the Worker. + +```python +from temporalio.worker import Worker + +worker = Worker( + client, + task_queue="my-task-queue", + workflows=[WeatherAgent], + activities=[get_weather], +) +``` + ### Calling OpenAI Agents Tools inside Temporal Workflows -For simple computations that don't involve external calls you can call the tool directly from the workflow by using the standard OpenAI Agents SDK `@functiontool` annotation. +For simple computations that don't involve external calls, you can call the tool directly from the workflow by using the standard OpenAI Agents SDK `@function_tool` decorator. ```python from temporalio import workflow @@ -345,33 +365,344 @@ class MathAssistantAgent: return result.final_output ``` -Note that any tools that run in the workflow must respect the workflow execution restrictions, meaning no I/O or non-deterministic operations. -Of course, code running in the workflow can invoke a Temporal activity at any time. +Use regular `@function_tool` tools only for deterministic, workflow-safe logic. +Do not perform network, database, filesystem, or other external I/O directly from these tools. +Use a Temporal activity with `activity_as_tool()` instead. +Code running in the workflow can also invoke a Temporal activity directly when needed. Tools that run in the workflow can also update OpenAI Agents context, which is read-only for tools run as Temporal activities. +## MCP Support + +This integration provides support for Model Context Protocol (MCP) servers through two wrapper approaches designed to handle different implications of failures. + +While Temporal provides durable execution for your workflows, this durability does not extend to MCP servers, which operate independently of the workflow and must provide their own durability. The integration handles this by offering stateless and stateful wrappers that you can choose based on your MCP server's design. + +### Stateless vs Stateful MCP Servers + +You need to understand your MCP server's behavior to choose the correct wrapper: + +**Stateless MCP servers** treat each operation independently. For example, a weather server with a `get_weather(location)` tool is stateless because each call is self-contained and includes all necessary information. These servers can be safely restarted or reconnected to without changing their behavior. + +**Stateful MCP servers** maintain session state between calls. For example, a weather server that requires calling `set_location(location)` followed by `get_weather()` is stateful because it remembers the configured location and uses it for subsequent calls. If the session or the server is restarted, state crucial for operation is lost. Temporal identifies such failures and raises an `ApplicationError` to signal the need for application-level failure handling. + +### Usage Example (Stateless MCP) + +The code below gives an example of using a stateless MCP server. + +#### Worker Configuration + +```python +import asyncio +from datetime import timedelta +from agents.mcp import MCPServerStdio +from temporalio.client import Client +from temporalio.contrib.openai_agents import ( + ModelActivityParameters, + OpenAIAgentsPlugin, + StatelessMCPServerProvider, +) +from temporalio.worker import Worker + + +async def main(): + # Create the MCP server provider + filesystem_server = StatelessMCPServerProvider( + lambda: MCPServerStdio( + name="FileSystemServer", + params={ + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/files"], + }, + ) + ) + + # Register the MCP server with the OpenAI Agents plugin + client = await Client.connect( + "localhost:7233", + plugins=[ + OpenAIAgentsPlugin( + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=60) + ), + mcp_server_providers=[filesystem_server], + ), + ], + ) + + worker = Worker( + client, + task_queue="my-task-queue", + workflows=[FileSystemWorkflow], + ) + await worker.run() + + +if __name__ == "__main__": + asyncio.run(main()) +``` + +#### Workflow Implementation + +```python +from temporalio import workflow +from temporalio.contrib import openai_agents +from agents import Agent, Runner + +@workflow.defn +class FileSystemWorkflow: + @workflow.run + async def run(self, query: str) -> str: + # Reference the MCP server by name (matches name in worker configuration) + server = openai_agents.workflow.stateless_mcp_server("FileSystemServer") + + agent = Agent( + name="File Assistant", + instructions="Use the filesystem tools to read files and answer questions.", + mcp_servers=[server], + ) + + result = await Runner.run(agent, input=query) + return result.final_output +``` + +The `StatelessMCPServerProvider` takes a factory function that creates new MCP server instances. The server name used in `stateless_mcp_server()` must match the name configured in the MCP server instance. In this example, the name is `FileSystemServer`. + +### Stateful MCP Servers + +For implementation details and examples, see the [samples repository](https://github.com/temporalio/samples-python/tree/main/openai_agents/mcp). + +When using stateful servers, the dedicated worker maintaining the connection may fail due to network issues or server problems. When this happens, Temporal raises an `ApplicationError` and cannot automatically recover because it cannot restore the lost server state. +To recover from such failures, you need to implement your own application-level retry logic. + +### Factory Arguments + +Both `stateless_mcp_server()` and `stateful_mcp_server()` accept an optional `factory_argument`, which is passed to the registered server factory when the MCP server is created. + +A stateless factory that declares no parameters — like the `lambda: MCPServerStdio(...)` example above — ignores the value, but it is still recorded in history. + +**Do not pass secrets, credentials, or API keys through `factory_argument`.** It is an activity argument, so it is recorded in workflow history and, without a payload codec, visible in the web UI. Resolve credentials worker-side inside the server factory instead. + +### Hosted MCP Tool + +For network-accessible MCP servers, you can also use `HostedMCPTool` from the OpenAI Agents SDK, which uses an MCP client hosted by OpenAI. + +## Sandbox Support + +⚠️ **Pre-release** - This functionality is subject to change prior to General Availability. + +The sandbox integration lets `SandboxAgent` from the OpenAI Agents SDK execute inside a remote or local sandbox (Daytona, Docker, E2B, local Unix, etc.) while keeping all coordination durable in Temporal. + +Every sandbox operation — creating a session, running commands, reading/writing files, PTY interactions — is dispatched as a Temporal activity. This means sandbox work is fully observable, retryable, and recoverable like any other activity, and sandbox session state is serialized with the workflow so it survives worker restarts. + +### Architecture + +```text +Workflow Code + ↓ +temporal_sandbox_client("daytona") [returns TemporalSandboxClient] + ↓ +SandboxAgent.run(run_config=RunConfig(sandbox=SandboxRunConfig(client=...))) + ↓ +sandbox agent calls session.exec / session.read / session.write / … + ↓ +TemporalSandboxSession routes each call as a Temporal activity +("daytona-sandbox_session_exec", "daytona-sandbox_session_read", …) + ↓ +SandboxClientProvider activities on the worker call the real sandbox client + ↓ +Actual sandbox backend (Daytona, Docker, local, …) +``` + +### Worker Configuration + +Register one or more `SandboxClientProvider` instances with the plugin. Each provider pairs a unique name with a real `BaseSandboxClient` implementation. The plugin automatically registers all required activities on the worker. + +```python +import asyncio +import docker +from temporalio.client import Client +from temporalio.worker import Worker +from temporalio.contrib.openai_agents import OpenAIAgentsPlugin, SandboxClientProvider, ModelActivityParameters +from agents.extensions.sandbox.daytona import DaytonaSandboxClient +from agents.extensions.sandbox.unix_local import UnixLocalSandboxClient + +async def main(): + client = await Client.connect( + "localhost:7233", + plugins=[ + OpenAIAgentsPlugin( + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=30) + ), + sandbox_clients=[ + SandboxClientProvider("daytona", DaytonaSandboxClient()), + SandboxClientProvider("local", UnixLocalSandboxClient()), + ], + ), + ], + ) + + worker = Worker( + client, + task_queue="my-task-queue", + workflows=[MyWorkflow], + ) + await worker.run() +``` + +Provider names must be unique. Each name becomes the prefix for that backend's activities, allowing multiple backends to coexist on a single worker. + +### Workflow Usage + +In the workflow, use `temporal_sandbox_client()` to create a reference to a registered backend by name. Pass it to `SandboxRunConfig` inside `RunConfig`: + +```python +from temporalio import workflow +from temporalio.contrib.openai_agents.workflow import temporal_sandbox_client +from agents import Runner +from agents.sandbox import SandboxAgent, SandboxRunConfig +from agents.run import RunConfig + +@workflow.defn +class MyWorkflow: + @workflow.run + async def run(self, prompt: str) -> str: + agent = SandboxAgent( + name="Coding Assistant", + instructions="You are a helpful coding assistant with access to a sandbox.", + ) + + result = await Runner.run( + agent, + prompt, + run_config=RunConfig( + sandbox=SandboxRunConfig( + client=temporal_sandbox_client("daytona"), + options=DaytonaSandboxClientOptions(pause_on_exit=False), + ), + ), + ) + return result.final_output +``` + +The name passed to `temporal_sandbox_client()` must exactly match the name used in `SandboxClientProvider` on the worker. + +### Multiple Backends + +A single workflow can target different backends by name. Register all backends on the worker and reference each by name in the workflow: + +```python +# Run a task on the "daytona" backend +result = await Runner.run( + agent, prompt, + run_config=RunConfig(sandbox=SandboxRunConfig( + client=temporal_sandbox_client("daytona"), + options=DaytonaSandboxClientOptions(pause_on_exit=False), + )), +) + +# Run a different task on the "local" backend +result = await Runner.run( + agent, prompt, + run_config=RunConfig(sandbox=SandboxRunConfig( + client=temporal_sandbox_client("local"), + options=UnixLocalSandboxClientOptions(), + )), +) +``` + +## Streaming + +⚠️ **Experimental** - This functionality is subject to change prior to General Availability. + +The integration supports streaming model responses via the SDK-native +`Runner.run_streamed` API. Inside a workflow, model calls execute as a +streaming activity (`invoke_model_activity_streaming`) that consumes +`Model.stream_response` and returns the collected list of native OpenAI +response events. The workflow surfaces those events to the caller +through `RunResultStreaming.stream_events()`, which wraps them in the +agents-SDK `StreamEvent` union (so raw model events arrive as +`RawResponsesStreamEvent.data`). + +External consumers (UIs, tracing pipelines, etc.) observe events as +they arrive by hosting a [`WorkflowStream`](../workflow_streams/README.md) +in the workflow and subscribing with `WorkflowStreamClient`. The +streaming activity publishes each event to the topic configured on +`ModelActivityParameters.streaming_topic`. The topic is required +when using `Runner.run_streamed`; calling it without a configured topic +raises before any activity is scheduled. + +Example workflow consuming events via `stream_events()` while the +streaming activity publishes to the `"events"` topic: + +```python +from agents import Agent, Runner +from agents.stream_events import RawResponsesStreamEvent + +from temporalio import workflow + +@workflow.defn +class MyAgent: + @workflow.run + async def run(self, prompt: str) -> str: + agent = Agent(name="Assistant", instructions="...") + result = Runner.run_streamed(agent, prompt) + async for event in result.stream_events(): + if isinstance(event, RawResponsesStreamEvent): + raw_event = event.data # native OpenAI ResponseStreamEvent + ... + return result.final_output +``` + +To publish raw model events to external subscribers, host a +`WorkflowStream` in the workflow and configure +`OpenAIAgentsPlugin(model_params=ModelActivityParameters(streaming_topic="events"))`. See [`temporalio.contrib.workflow_streams`](../workflow_streams/README.md) for the +publisher and subscriber API. + +`RunResultStreaming.stream_events()` yields the agents-SDK +`StreamEvent` union (`RawResponsesStreamEvent`, `RunItemStreamEvent`, +`AgentUpdatedStreamEvent`); native OpenAI response events arrive +wrapped as `RawResponsesStreamEvent.data`. Workflow-stream subscribers, +by contrast, receive the unwrapped native events directly because the +streaming activity publishes them straight from `Model.stream_response`. + +Streaming is incompatible with `use_local_activity` because local +activities support neither activity heartbeats nor the workflow stream +signal channel. + +Activity retries surface to workflow-stream subscribers but not to +`RunResultStreaming.stream_events()`. Events are published to the +stream as `Model.stream_response` produces them, so a partial attempt +that fails mid-response leaves its emitted events on the stream and the +retry attempt publishes a second sequence. `stream_events()` only sees +the final successful attempt's collected events because it consumes the +activity's return value. Workflow-stream subscribers should treat +retries the same way as any other workflow_streams publisher — see +[Delivery semantics](../workflow_streams/README.md) for the trade and +the conventional `RETRY` event pattern for surfacing the transition to +consumers. ## Feature Support This integration is presently subject to certain limitations. -Streaming and voice agents are not supported. +Realtime agents are not supported. Streaming is supported via +`Runner.run_streamed` — see [Streaming](#streaming) above. Certain tools are not suitable for a distributed computing environment, so these have been disabled as well. ### Model Providers | Model Provider | Supported | -|:--------------|:---------:| -| OpenAI | Yes | -| LiteLLM | Yes | +| :------------- | :-------: | +| OpenAI | Yes | +| LiteLLM | Yes | ### Model Response format -This integration does not presently support streaming. - -| Model Response | Supported | -|:--------------|:---------:| -| Get Response | Yes | -| Streaming | No | +| Model Response | Supported | +| :------------- | :------------------: | +| Get Response | Yes | +| Streaming | Yes (experimental) | ### Tools @@ -380,7 +711,7 @@ This integration does not presently support streaming. `LocalShellTool` and `ComputerTool` are not suited to a distributed computing setting. | Tool Type | Supported | -|:-------------------|:---------:| +| :------------------ | :-------: | | FunctionTool | Yes | | LocalShellTool | No | | WebSearchTool | Yes | @@ -394,28 +725,31 @@ This integration does not presently support streaming. As described in [Tool Calling](#tool-calling), context propagation is read-only when Temporal activities are used as tools. -| Context Propagation | Supported | -|:----------------------------------------|:---------:| -| Activity Tool receives copy of context | Yes | -| Activity Tool can update context | No | -| Function Tool received context | Yes | -| Function Tool can update context | Yes | +| Context Propagation | Supported | +| :------------------------------------- | :-------: | +| Activity Tool receives copy of context | Yes | +| Activity Tool can update context | No | +| Function Tool received context | Yes | +| Function Tool can update context | Yes | ### MCP -Presently, MCP is supported only via `HostedMCPTool`, which uses the OpenAI Responses API and cloud MCP client behind it. -The OpenAI Agents SDK also supports MCP clients that run in application code, but this integration does not. +The MCP protocol is stateful, but many MCP servers are stateless. +We let you choose between two MCP wrappers, one designed for stateless MCP servers and one for stateful MCP servers. +These wrappers work with all transport varieties. + +Note that when using network-accessible MCP servers, you also can also use the tool `HostedMCPTool`, which is part of the OpenAI Responses API and uses an MCP client hosted by OpenAI. -| MCP Class | Supported | -|:-----------------------|:---------:| -| MCPServerStdio | No | -| MCPServerSse | No | -| MCPServerStreamableHttp| No | +| MCP Class | Supported | +| :---------------------- | :-------: | +| MCPServerStdio | Yes | +| MCPServerSse | Yes | +| MCPServerStreamableHttp | Yes | ### Guardrails | Guardrail Type | Supported | -|:---------------|:---------:| +| :------------- | :-------: | | Code | Yes | | Agent | Yes | @@ -423,33 +757,273 @@ The OpenAI Agents SDK also supports MCP clients that run in application code, bu SQLite storage is not suited to a distributed environment. -| Feature | Supported | -|:---------------|:---------:| -| SQLiteSession | No | +| Feature | Supported | +| :------------ | :-------: | +| SQLiteSession | No | ### Tracing | Tracing Provider | Supported | -|:-----------------|:---------:| +| :--------------- | :-------: | | OpenAI platform | Yes | -### Voice +## OpenTelemetry Integration + +⚠️ **Public Preview** - This functionality is subject to change prior to General Availability. + +This integration provides seamless export of OpenAI agent telemetry to OpenTelemetry (OTEL) endpoints for observability and monitoring. The integration automatically handles workflow replay semantics, ensuring spans are only exported when workflows actually complete. + +### Quick Start + +To enable OTEL telemetry export, you need to set up a global `ReplaySafeTracerProvider` and enable the integration in the `OpenAIAgentsPlugin`: + +```python +from datetime import timedelta +from temporalio.client import Client +from temporalio.contrib.openai_agents import OpenAIAgentsPlugin, ModelActivityParameters +from temporalio.contrib.opentelemetry import create_tracer_provider +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter +from opentelemetry import trace +from opentelemetry.sdk.trace.export import SimpleSpanProcessor + +# Configure your OTEL exporters + +# Set up the global tracer provider +tracer_provider = create_tracer_provider() +tracer_provider.add_span_processor(SimpleSpanProcessor(OTLPSpanExporter(endpoint="http://localhost:4317"))) +trace.set_tracer_provider(tracer_provider) + +client = await Client.connect( + "localhost:7233", + plugins=[ + OpenAIAgentsPlugin( + use_otel_instrumentation=True, # Enable OTEL integration + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=30) + ) + ), + ], +) +``` + +### Features + +- **Multiple Exporters**: Send telemetry to multiple OTEL endpoints simultaneously via the global tracer provider +- **Replay-Safe**: Spans are only exported when workflows actually complete, not during replays +- **Deterministic IDs**: Consistent span IDs across workflow replays for reliable correlation +- **Automatic Setup**: No manual instrumentation required - just enable the flag and set up the global tracer provider +- **Graceful Degradation**: Works seamlessly whether OTEL dependencies are installed or not + +### Dependencies + +OTEL integration requires additional dependencies: + +```bash +pip install openinference-instrumentation-openai-agents opentelemetry-sdk +``` + +Choose the appropriate OTEL exporter for your monitoring system: + +```bash +# For OTLP (works with most OTEL collectors and monitoring systems) +pip install opentelemetry-exporter-otlp + +# For Console output (development/debugging) +pip install opentelemetry-exporter-console -| Mode | Supported | -|:------------------------|:---------:| -| Voice agents (pipelines)| No | -| Realtime agents | No | +# Other exporters available for specific systems +pip install opentelemetry-exporter- +``` + +### Example: Multiple Exporters + +```python +from temporalio.contrib.opentelemetry import create_tracer_provider +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter +from opentelemetry.exporter.console import ConsoleSpanExporter +from opentelemetry import trace + +exporters = [ + # Production monitoring system + OTLPSpanExporter( + endpoint="https://your-monitoring-system:4317", + headers={"api-key": "your-api-key"} + ), + + # Secondary monitoring endpoint + OTLPSpanExporter(endpoint="https://backup-collector:4317"), + + # Development debugging + ConsoleSpanExporter(), +] + +# Set up global tracer provider with multiple exporters +tracer_provider = create_tracer_provider(exporters=exporters) +trace.set_tracer_provider(tracer_provider) + +plugin = OpenAIAgentsPlugin(use_otel_instrumentation=True) +``` + +### Error Handling + +If you enable OTEL instrumentation but the required dependencies are not installed, you'll receive a clear error message: + +``` +ImportError: OTEL dependencies not available. Install with: pip install openinference-instrumentation-openai-agents opentelemetry-sdk +``` + +If you enable OTEL instrumentation but don't have a proper global tracer provider set up, you'll get: + +``` +ValueError: Global tracer provider must a ReplaySafeTracerProvider. Use temporalio.contrib.opentelemtry.create_trace_provider to create one. +``` + +### Direct OpenTelemetry API Calls in Workflows + +When using direct OpenTelemetry API calls within workflows (e.g., `opentelemetry.trace.get_tracer(__name__).start_as_current_span()`), you need to ensure proper context bridging and sandbox configuration. + +#### Sandbox Configuration + +Workflows run in a sandbox that restricts module access. To use direct OTEL API calls, you must explicitly allow OpenTelemetry passthrough: + +```python +from temporalio.worker import Worker +from temporalio.worker.workflow_sandbox import SandboxedWorkflowRunner, SandboxRestrictions + +# Configure worker with OpenTelemetry passthrough +worker = Worker( + client, + task_queue="my-task-queue", + workflows=[MyWorkflow], + workflow_runner=SandboxedWorkflowRunner( + SandboxRestrictions.default.with_passthrough_modules("opentelemetry") + ) +) +``` + +#### Context Bridging Pattern + +Direct OTEL spans must be created within an active OpenAI Agents SDK span to ensure proper parenting: + +```python +import opentelemetry.trace +from agents import custom_span +from temporalio import workflow + +@workflow.defn +class MyWorkflow: + @workflow.run + async def run(self) -> str: + # Start an SDK span first to establish OTEL context bridge + with custom_span("Workflow coordination"): + # Now direct OTEL spans will be properly parented + tracer = opentelemetry.trace.get_tracer(__name__) + with tracer.start_as_current_span("Custom workflow span"): + # Your workflow logic here + result = await self.do_work() + return result +``` + +#### Why This Pattern is Required + +- **OpenInference instrumentation** bridges OpenAI Agents SDK spans to OpenTelemetry context +- **Direct OTEL API calls** without an active SDK span become root spans with no parent +- **SDK spans** (`custom_span()`) establish the context bridge that allows subsequent direct OTEL spans to inherit proper trace parenting + +#### Complete Example + +```python +import opentelemetry.trace +from agents import custom_span +from temporalio import workflow +from temporalio.worker import Worker +from temporalio.worker.workflow_sandbox import SandboxedWorkflowRunner, SandboxRestrictions + +@workflow.defn +class TracedWorkflow: + @workflow.run + async def run(self) -> str: + # Establish OTEL context with SDK span + with custom_span("Main workflow"): + # Create direct OTEL spans for fine-grained tracing + tracer = opentelemetry.trace.get_tracer(__name__) + + with tracer.start_as_current_span("Data processing"): + data = await self.process_data() + + with tracer.start_as_current_span("Business logic"): + result = await self.execute_business_logic(data) + + return result + +# Worker configuration +worker = Worker( + client, + task_queue="traced-workflows", + workflows=[TracedWorkflow], + workflow_runner=SandboxedWorkflowRunner( + SandboxRestrictions.default.with_passthrough_modules("opentelemetry") + ) +) +``` + +This ensures your direct OTEL spans are properly parented within the trace hierarchy initiated by your client SDK traces. + +### Client-Side Trace Initialization + +You can also start an Agents SDK trace on the client side before executing a workflow. This is useful when you want the entire workflow execution to be part of a larger trace context: + +```python +from agents import trace, custom_span +from temporalio.contrib.openai_agents import OpenAIAgentsPlugin + +# Set up the plugin with OTEL integration +plugin = OpenAIAgentsPlugin(use_otel_instrumentation=True) + +# Client setup +client = await Client.connect( + "localhost:7233", + plugins=[plugin] +) + +# Start a trace on the client side +with plugin.tracing_context(): + with trace("Customer support workflow"): + with custom_span("Workflow execution"): + # Execute workflow within the trace context + result = await client.execute_workflow( + CustomerSupportAgent.run, + "Help me with my order", + id="customer-support-123", + task_queue="my-task-queue", + ) + print(f"Result: {result}") +``` + +The `plugin.tracing_context()` is required when starting traces outside of a worker context. This ensures proper instrumentation setup and trace propagation into the workflow execution. + +If OTEL instrumentation is not enabled, the integration works normally without any OTEL setup. + +### Voice + +| Mode | Supported | +| :----------------------- | :-----------: | +| Voice agents (pipelines) | Yes [^voice] | +| Realtime agents | No | + +[^voice]: `VoicePipeline` runs in your process and delegates the agent + step (`VoiceWorkflowBase.run`) to a Temporal workflow that uses + `Runner.run` or `Runner.run_streamed`. STT and TTS run outside + Temporal; the agent loop is durable. ### Utilities The REPL utility is not suitable for a distributed setting. | Utility | Supported | -|:--------|:---------:| +| :------ | :-------: | | REPL | No | - ## Additional Examples You can find additional examples in the [Temporal Python Samples Repository](https://github.com/temporalio/samples-python/tree/main/openai_agents). - diff --git a/temporalio/contrib/openai_agents/__init__.py b/temporalio/contrib/openai_agents/__init__.py index 998cf61eb..3976f633c 100644 --- a/temporalio/contrib/openai_agents/__init__.py +++ b/temporalio/contrib/openai_agents/__init__.py @@ -2,30 +2,32 @@ This module provides compatibility between the `OpenAI Agents SDK `_ and Temporal workflows. - -.. warning:: - This module is experimental and may change in future versions. - Use with caution in production environments. """ +from temporalio.contrib.openai_agents._mcp import ( + StatefulMCPServerProvider, + StatelessMCPServerProvider, +) from temporalio.contrib.openai_agents._model_parameters import ModelActivityParameters from temporalio.contrib.openai_agents._temporal_openai_agents import ( OpenAIAgentsPlugin, - TestModel, - TestModelProvider, + OpenAIPayloadConverter, ) -from temporalio.contrib.openai_agents._trace_interceptor import ( - OpenAIAgentsTracingInterceptor, +from temporalio.contrib.openai_agents.sandbox._sandbox_client_provider import ( + SandboxClientProvider, ) from temporalio.contrib.openai_agents.workflow import AgentsWorkflowError -from . import workflow +from . import testing, workflow __all__ = [ "AgentsWorkflowError", - "OpenAIAgentsPlugin", "ModelActivityParameters", + "OpenAIAgentsPlugin", + "OpenAIPayloadConverter", + "SandboxClientProvider", + "StatelessMCPServerProvider", + "StatefulMCPServerProvider", + "testing", "workflow", - "TestModel", - "TestModelProvider", ] diff --git a/temporalio/contrib/openai_agents/_heartbeat_decorator.py b/temporalio/contrib/openai_agents/_heartbeat_decorator.py index d25814a42..7c5b9193d 100644 --- a/temporalio/contrib/openai_agents/_heartbeat_decorator.py +++ b/temporalio/contrib/openai_agents/_heartbeat_decorator.py @@ -1,29 +1,29 @@ import asyncio +from collections.abc import Awaitable, Callable from functools import wraps -from typing import Any, Awaitable, Callable, TypeVar, cast +from typing import Any, TypeVar, cast from temporalio import activity F = TypeVar("F", bound=Callable[..., Awaitable[Any]]) -def _auto_heartbeater(fn: F) -> F: - # Propagate type hints from the original callable. +def auto_heartbeater(fn: F) -> F: + """Decorator that heartbeats at half the activity's heartbeat timeout.""" + @wraps(fn) async def wrapper(*args: Any, **kwargs: Any) -> Any: heartbeat_timeout = activity.info().heartbeat_timeout heartbeat_task = None if heartbeat_timeout: - # Heartbeat twice as often as the timeout heartbeat_task = asyncio.create_task( - heartbeat_every(heartbeat_timeout.total_seconds() / 2) + _heartbeat_every(heartbeat_timeout.total_seconds() / 2) ) try: return await fn(*args, **kwargs) finally: if heartbeat_task: heartbeat_task.cancel() - # Wait for heartbeat cancellation to complete try: await heartbeat_task except asyncio.CancelledError: @@ -32,8 +32,7 @@ async def wrapper(*args: Any, **kwargs: Any) -> Any: return cast(F, wrapper) -async def heartbeat_every(delay: float, *details: Any) -> None: - """Heartbeat every so often while not cancelled""" +async def _heartbeat_every(delay: float) -> None: while True: await asyncio.sleep(delay) - activity.heartbeat(*details) + activity.heartbeat() diff --git a/temporalio/contrib/openai_agents/_invoke_model_activity.py b/temporalio/contrib/openai_agents/_invoke_model_activity.py index ed4313537..5435b6369 100644 --- a/temporalio/contrib/openai_agents/_invoke_model_activity.py +++ b/temporalio/contrib/openai_agents/_invoke_model_activity.py @@ -4,10 +4,9 @@ """ import enum -import json from dataclasses import dataclass from datetime import timedelta -from typing import Any, Optional, Union +from typing import Any, NoReturn from agents import ( AgentOutputSchemaBase, @@ -28,16 +27,26 @@ UserError, WebSearchTool, ) +from agents.items import TResponseStreamEvent +from agents.tool import ( + ApplyPatchTool, + CustomTool, + LocalShellTool, + ShellTool, + ShellToolEnvironment, + ToolSearchTool, +) from openai import ( APIStatusError, AsyncOpenAI, ) +from openai.types.responses import CustomToolParam from openai.types.responses.tool_param import Mcp -from pydantic_core import to_json from typing_extensions import Required, TypedDict from temporalio import activity -from temporalio.contrib.openai_agents._heartbeat_decorator import _auto_heartbeater +from temporalio.contrib.openai_agents._heartbeat_decorator import auto_heartbeater +from temporalio.contrib.workflow_streams import WorkflowStreamClient from temporalio.exceptions import ApplicationError @@ -75,23 +84,67 @@ class HostedMCPToolInput: tool_config: Mcp -ToolInput = Union[ - FunctionToolInput, - FileSearchTool, - WebSearchTool, - ImageGenerationTool, - CodeInterpreterTool, - HostedMCPToolInput, -] +@dataclass +class ShellToolInput: + """Data conversion friendly representation of a ShellTool. Contains only the fields which are needed by the model + execution to determine what tool to call, not the actual tool invocation, which remains in the workflow context. + """ + + name: str = "shell" + environment: ShellToolEnvironment | None = None + + +class _NoopApplyPatchEditor: + """Satisfies the ApplyPatchEditor protocol for tool reconstruction during model calls.""" + + def create_file(self, operation: Any) -> None: # type: ignore[reportUnusedParameter] + return None + + def update_file(self, operation: Any) -> None: # type: ignore[reportUnusedParameter] + return None + + def delete_file(self, operation: Any) -> None: # type: ignore[reportUnusedParameter] + return None + + +@dataclass +class ApplyPatchToolInput: + """Data conversion friendly representation of an ApplyPatchTool.""" + + name: str = "apply_patch" + + +@dataclass +class CustomToolInput: + """Data conversion friendly representation of a CustomTool. Contains only the fields which are needed by the model + execution to determine what tool to call, not the actual tool invocation, which remains in the workflow context. + """ + + tool_config: CustomToolParam + + +ToolInput = ( + FunctionToolInput + | FileSearchTool + | WebSearchTool + | ImageGenerationTool + | CodeInterpreterTool + | HostedMCPToolInput + | ShellToolInput + | LocalShellTool + | ApplyPatchToolInput + | CustomToolInput + | ToolSearchTool +) @dataclass class AgentOutputSchemaInput(AgentOutputSchemaBase): """Data conversion friendly representation of AgentOutputSchema.""" - output_type_name: Optional[str] + output_type_name: str | None is_wrapped: bool - output_schema: Optional[dict[str, Any]] + output_schema: dict[str, Any] | None strict_json_schema: bool def is_plain_text(self) -> bool: @@ -135,16 +188,138 @@ class ModelTracingInput(enum.IntEnum): class ActivityModelInput(TypedDict, total=False): """Input for the invoke_model_activity activity.""" - model_name: Optional[str] - system_instructions: Optional[str] - input: Required[Union[str, list[TResponseInputItem]]] + model_name: str | None + system_instructions: str | None + input: Required[str | list[TResponseInputItem]] model_settings: Required[ModelSettings] tools: list[ToolInput] - output_schema: Optional[AgentOutputSchemaInput] + output_schema: AgentOutputSchemaInput | None handoffs: list[HandoffInput] tracing: Required[ModelTracingInput] - previous_response_id: Optional[str] - prompt: Optional[Any] + previous_response_id: str | None + conversation_id: str | None + prompt: Any | None + + +class StreamingActivityModelInput(ActivityModelInput, total=False): + """Input for the invoke_model_activity_streaming activity. + + Adds the streaming-only fields on top of :class:`ActivityModelInput`. + """ + + streaming_topic: Required[str] + streaming_batch_interval: timedelta + + +async def _empty_on_invoke_tool(_ctx: RunContextWrapper[Any], _input: str) -> str: + return "" + + +async def _empty_on_invoke_handoff(_ctx: RunContextWrapper[Any], _input: str) -> Any: + return None + + +async def _noop_shell_executor(*_a: Any, **_kw: Any) -> str: + return "" + + +def _build_tool(tool: ToolInput) -> Tool: + """Reconstruct a Tool from its data-conversion-friendly input form.""" + if isinstance( + tool, + ( + FileSearchTool, + WebSearchTool, + ImageGenerationTool, + CodeInterpreterTool, + LocalShellTool, + ToolSearchTool, + ), + ): + return tool + elif isinstance(tool, ShellToolInput): + return ShellTool( + name=tool.name, + environment=tool.environment, + executor=_noop_shell_executor, + ) + elif isinstance(tool, ApplyPatchToolInput): + return ApplyPatchTool(name=tool.name, editor=_NoopApplyPatchEditor()) + elif isinstance(tool, HostedMCPToolInput): + return HostedMCPTool(tool_config=tool.tool_config) + elif isinstance(tool, CustomToolInput): + return CustomTool( + name=tool.tool_config["name"], + description=tool.tool_config.get("description", ""), + on_invoke_tool=_empty_on_invoke_tool, + format=tool.tool_config.get("format"), + defer_loading=tool.tool_config.get("defer_loading", False), + ) + elif isinstance(tool, FunctionToolInput): + return FunctionTool( + name=tool.name, + description=tool.description, + params_json_schema=tool.params_json_schema, + on_invoke_tool=_empty_on_invoke_tool, + strict_json_schema=tool.strict_json_schema, + ) + else: + raise UserError(f"Unknown tool type: {tool.name}") # type:ignore[reportUnreachable] + + +def _build_tools_and_handoffs( + input: ActivityModelInput, +) -> tuple[list[Tool], list[Handoff[Any, Any]]]: + tools = [_build_tool(x) for x in input.get("tools", [])] + handoffs: list[Handoff[Any, Any]] = [ + Handoff( + tool_name=x.tool_name, + tool_description=x.tool_description, + input_json_schema=x.input_json_schema, + agent_name=x.agent_name, + strict_json_schema=x.strict_json_schema, + on_invoke_handoff=_empty_on_invoke_handoff, + ) + for x in input.get("handoffs", []) + ] + return tools, handoffs + + +def _raise_for_openai_status(e: APIStatusError) -> NoReturn: + """Translate an OpenAI APIStatusError into the right retry posture.""" + retry_after: timedelta | None = None + retry_after_ms_header = e.response.headers.get("retry-after-ms") + if retry_after_ms_header is not None: + retry_after = timedelta(milliseconds=float(retry_after_ms_header)) + + if retry_after is None: + retry_after_header = e.response.headers.get("retry-after") + if retry_after_header is not None: + retry_after = timedelta(seconds=float(retry_after_header)) + + should_retry_header = e.response.headers.get("x-should-retry") + if should_retry_header == "true": + raise e + if should_retry_header == "false": + raise ApplicationError( + "Non retryable OpenAI error", + non_retryable=True, + next_retry_delay=retry_after, + ) from e + + # Retry on 408 (Request Timeout), 409 (Conflict / often transient + # state mismatch), 429 (Too Many Requests / rate-limited), and any + # 5xx (server-side errors). All other 4xx codes are caller errors + # that won't recover on retry. + retryable = ( + e.response.status_code in [408, 409, 429] or e.response.status_code >= 500 + ) + raise ApplicationError( + f"{'Retryable' if retryable else 'Non retryable'} OpenAI status code: " + f"{e.response.status_code}", + non_retryable=not retryable, + next_retry_delay=retry_after, + ) from e class ModelActivity: @@ -152,114 +327,98 @@ class ModelActivity: Disabling retries in your model of choice is recommended to allow activity retries to define the retry model. """ - def __init__(self, model_provider: Optional[ModelProvider] = None): + def __init__(self, model_provider: ModelProvider | None = None): """Initialize the activity with a model provider.""" self._model_provider = model_provider or OpenAIProvider( openai_client=AsyncOpenAI(max_retries=0) ) @activity.defn - @_auto_heartbeater + @auto_heartbeater async def invoke_model_activity(self, input: ActivityModelInput) -> ModelResponse: """Activity that invokes a model with the given input.""" model = self._model_provider.get_model(input.get("model_name")) - - async def empty_on_invoke_tool(ctx: RunContextWrapper[Any], input: str) -> str: - return "" - - async def empty_on_invoke_handoff( - ctx: RunContextWrapper[Any], input: str - ) -> Any: - return None - - # workaround for https://github.com/pydantic/pydantic/issues/9541 - # ValidatorIterator returned - input_json = to_json(input["input"]) - input_input = json.loads(input_json) - - def make_tool(tool: ToolInput) -> Tool: - if isinstance( - tool, - ( - FileSearchTool, - WebSearchTool, - ImageGenerationTool, - CodeInterpreterTool, - ), - ): - return tool - elif isinstance(tool, HostedMCPToolInput): - return HostedMCPTool( - tool_config=tool.tool_config, - ) - elif isinstance(tool, FunctionToolInput): - return FunctionTool( - name=tool.name, - description=tool.description, - params_json_schema=tool.params_json_schema, - on_invoke_tool=empty_on_invoke_tool, - strict_json_schema=tool.strict_json_schema, - ) - else: - raise UserError(f"Unknown tool type: {tool.name}") - - tools = [make_tool(x) for x in input.get("tools", [])] - handoffs: list[Handoff[Any, Any]] = [ - Handoff( - tool_name=x.tool_name, - tool_description=x.tool_description, - input_json_schema=x.input_json_schema, - agent_name=x.agent_name, - strict_json_schema=x.strict_json_schema, - on_invoke_handoff=empty_on_invoke_handoff, - ) - for x in input.get("handoffs", []) - ] + tools, handoffs = _build_tools_and_handoffs(input) try: return await model.get_response( system_instructions=input.get("system_instructions"), - input=input_input, + input=input["input"], model_settings=input["model_settings"], tools=tools, output_schema=input.get("output_schema"), handoffs=handoffs, tracing=ModelTracing(input["tracing"]), previous_response_id=input.get("previous_response_id"), + conversation_id=input.get("conversation_id"), prompt=input.get("prompt"), ) except APIStatusError as e: - # Listen to server hints - retry_after = None - retry_after_ms_header = e.response.headers.get("retry-after-ms") - if retry_after_ms_header is not None: - retry_after = timedelta(milliseconds=float(retry_after_ms_header)) - - if retry_after is None: - retry_after_header = e.response.headers.get("retry-after") - if retry_after_header is not None: - retry_after = timedelta(seconds=float(retry_after_header)) - - should_retry_header = e.response.headers.get("x-should-retry") - if should_retry_header == "true": - raise e - if should_retry_header == "false": - raise ApplicationError( - "Non retryable OpenAI error", - non_retryable=True, - next_retry_delay=retry_after, - ) from e - - # Specifically retryable status codes - if e.response.status_code in [408, 409, 429, 500]: - raise ApplicationError( - "Retryable OpenAI status code", - non_retryable=False, - next_retry_delay=retry_after, - ) from e - - raise ApplicationError( - "Non retryable OpenAI status code", - non_retryable=True, - next_retry_delay=retry_after, - ) from e + _raise_for_openai_status(e) + + @activity.defn + @auto_heartbeater + async def invoke_model_activity_streaming( + self, input: StreamingActivityModelInput + ) -> list[TResponseStreamEvent]: + """Streaming-aware model activity. + + .. warning:: + Streaming support is experimental and may change in future + versions. + + Calls ``model.stream_response()`` and returns the collected list + of native OpenAI stream events. The workflow's + ``Model.stream_response`` stub yields these to the agents + framework, which builds the final ``ModelResponse`` from the + terminal ``ResponseCompletedEvent``. + + Each event is also published to the workflow's stream on + ``streaming_topic`` so external consumers (UIs, tracing, + etc.) can observe events as they arrive. + + Heartbeats run on a background task via ``auto_heartbeater`` so + long initial-token latency or long pauses between chunks do not + trip ``heartbeat_timeout``. + """ + model = self._model_provider.get_model(input.get("model_name")) + tools, handoffs = _build_tools_and_handoffs(input) + + topic = input["streaming_topic"] + batch_interval = input.get( + "streaming_batch_interval", timedelta(milliseconds=100) + ) + events: list[TResponseStreamEvent] = [] + + stream = WorkflowStreamClient.from_within_activity( + batch_interval=batch_interval + ) + # TResponseStreamEvent is a typing.Annotated[Union[...]] — a typing + # special form, not a class — so it cannot be passed as type[T]. + # Leave the topic untyped (default Any); subscribers that want + # typed decode can pass result_type=TResponseStreamEvent on + # their own subscribe call. + events_topic = stream.topic(topic) + async with stream: + try: + async for event in model.stream_response( + system_instructions=input.get("system_instructions"), + input=input["input"], + model_settings=input["model_settings"], + tools=tools, + output_schema=input.get("output_schema"), + handoffs=handoffs, + tracing=ModelTracing(input["tracing"]), + previous_response_id=input.get("previous_response_id"), + conversation_id=input.get("conversation_id"), + prompt=input.get("prompt"), + ): + # OpenAI models set defer_build=True, so an event's pydantic + # schema may still be an unbuilt placeholder. + type(event).model_rebuild() + events.append(event) + events_topic.publish(event) + except APIStatusError as e: + _raise_for_openai_status(e) + + return events diff --git a/temporalio/contrib/openai_agents/_mcp.py b/temporalio/contrib/openai_agents/_mcp.py new file mode 100644 index 000000000..8f5294c42 --- /dev/null +++ b/temporalio/contrib/openai_agents/_mcp.py @@ -0,0 +1,539 @@ +import asyncio +import dataclasses +import functools +import inspect +from collections.abc import Callable, Sequence +from contextlib import AbstractAsyncContextManager +from datetime import timedelta +from types import TracebackType +from typing import Any, cast + +from agents import AgentBase, RunContextWrapper +from agents.mcp import MCPServer +from mcp import GetPromptResult, ListPromptsResult # type:ignore +from mcp import Tool as MCPTool # type:ignore +from mcp.types import CallToolResult # type:ignore + +from temporalio import activity, workflow +from temporalio.api.enums.v1.workflow_pb2 import ( + TIMEOUT_TYPE_HEARTBEAT, + TIMEOUT_TYPE_SCHEDULE_TO_START, +) +from temporalio.exceptions import ( + ActivityError, + ApplicationError, + is_cancelled_exception, +) +from temporalio.worker import PollerBehaviorSimpleMaximum, Worker +from temporalio.workflow import ActivityConfig, ActivityHandle + + +@dataclasses.dataclass +class _StatelessListToolsArguments: + factory_argument: Any | None + + +@dataclasses.dataclass +class _StatelessCallToolsArguments: + tool_name: str + arguments: dict[str, Any] | None + factory_argument: Any | None + meta: dict[str, Any] | None = None + + +@dataclasses.dataclass +class _StatelessListPromptsArguments: + factory_argument: Any | None + + +@dataclasses.dataclass +class _StatelessGetPromptArguments: + name: str + arguments: dict[str, Any] | None + factory_argument: Any | None + + +class _StatelessMCPServerReference(MCPServer): # type:ignore[reportUnusedClass] + def __init__( + self, + server: str, + config: ActivityConfig | None, + cache_tools_list: bool, + factory_argument: Any | None = None, + ): + self._name = server + "-stateless" + self._config = config or ActivityConfig( + start_to_close_timeout=timedelta(minutes=1) + ) + self._cache_tools_list = cache_tools_list + self._tools = None + self._factory_argument = factory_argument + super().__init__() + + @property + def name(self) -> str: + return self._name + + async def connect(self) -> None: + pass + + async def cleanup(self) -> None: + pass + + async def list_tools( + self, + run_context: RunContextWrapper[Any] | None = None, + agent: AgentBase | None = None, + ) -> list[MCPTool]: + if self._tools: + return self._tools + tools = await workflow.execute_activity( + self.name + "-list-tools", + _StatelessListToolsArguments(self._factory_argument), + result_type=list[MCPTool], + **self._config, + ) + if self._cache_tools_list: + self._tools = tools + return tools + + async def call_tool( + self, + tool_name: str, + arguments: dict[str, Any] | None, + meta: dict[str, Any] | None = None, + ) -> CallToolResult: + return await workflow.execute_activity( + self.name + "-call-tool-v2", + _StatelessCallToolsArguments( + tool_name, arguments, self._factory_argument, meta + ), + result_type=CallToolResult, + **self._config, + ) + + async def list_prompts(self) -> ListPromptsResult: + return await workflow.execute_activity( + self.name + "-list-prompts", + _StatelessListPromptsArguments(self._factory_argument), + result_type=ListPromptsResult, + **self._config, + ) + + async def get_prompt( + self, name: str, arguments: dict[str, Any] | None = None + ) -> GetPromptResult: + return await workflow.execute_activity( + self.name + "-get-prompt-v2", + _StatelessGetPromptArguments(name, arguments, self._factory_argument), + result_type=GetPromptResult, + **self._config, + ) + + +class StatelessMCPServerProvider: + """A stateless MCP server implementation for Temporal workflows. + + This class wraps a function to create MCP servers to make them stateless by executing each MCP operation + as a separate Temporal activity. Each operation (list_tools, call_tool, etc.) will + connect to the underlying server, execute the operation, and then clean up the connection. + + This approach will not maintain state across calls. If the desired MCPServer needs persistent state in order to + function, this cannot be used. + """ + + def __init__( + self, + name: str, + server_factory: (Callable[[], MCPServer] | Callable[[Any | None], MCPServer]), + ): + """Initialize the stateless temporal MCP server. + + Args: + name: The name of the MCP server. + server_factory: A function which will produce MCPServer instances. It should return a new server each time + so that state is not shared between workflow runs. It may accept a single positional parameter, which + receives a ``factory_argument`` from the workflow. + """ + self._server_factory = server_factory + + # Cache whether the server factory needs to be provided with arguments + sig = inspect.signature(self._server_factory) + self._server_accepts_arguments = len(sig.parameters) != 0 + + self._name = name + "-stateless" + super().__init__() + + def _create_server(self, factory_argument: Any | None) -> MCPServer: + if self._server_accepts_arguments: + return cast(Callable[[Any | None], MCPServer], self._server_factory)( + factory_argument + ) + else: + return cast(Callable[[], MCPServer], self._server_factory)() + + @property + def name(self) -> str: + """Get the server name.""" + return self._name + + def _get_activities(self) -> Sequence[Callable]: + @activity.defn(name=self.name + "-list-tools") + async def list_tools( + args: _StatelessListToolsArguments | None = None, + ) -> list[MCPTool]: + server = self._create_server(args.factory_argument if args else None) + try: + await server.connect() + return await server.list_tools() + finally: + await server.cleanup() + + @activity.defn(name=self.name + "-call-tool-v2") + async def call_tool(args: _StatelessCallToolsArguments) -> CallToolResult: + server = self._create_server(args.factory_argument) + try: + await server.connect() + return await server.call_tool(args.tool_name, args.arguments, args.meta) + finally: + await server.cleanup() + + @activity.defn(name=self.name + "-list-prompts") + async def list_prompts( + args: _StatelessListPromptsArguments | None = None, + ) -> ListPromptsResult: + server = self._create_server(args.factory_argument if args else None) + try: + await server.connect() + return await server.list_prompts() + finally: + await server.cleanup() + + @activity.defn(name=self.name + "-get-prompt-v2") + async def get_prompt(args: _StatelessGetPromptArguments) -> GetPromptResult: + server = self._create_server(args.factory_argument) + try: + await server.connect() + return await server.get_prompt(args.name, args.arguments) + finally: + await server.cleanup() + + @activity.defn(name=self.name + "-call-tool") + async def call_tool_deprecated( + tool_name: str, + arguments: dict[str, Any] | None, + ) -> CallToolResult: + return await call_tool( + _StatelessCallToolsArguments(tool_name, arguments, None) + ) + + @activity.defn(name=self.name + "-get-prompt") + async def get_prompt_deprecated( + name: str, + arguments: dict[str, Any] | None, + ) -> GetPromptResult: + return await get_prompt(_StatelessGetPromptArguments(name, arguments, None)) + + return ( + list_tools, + call_tool, + list_prompts, + get_prompt, + call_tool_deprecated, + get_prompt_deprecated, + ) + + +def _handle_worker_failure(func: Callable) -> Callable: + @functools.wraps(func) + async def wrapper(*args: Any, **kwargs: Any): + try: + return await func(*args, **kwargs) + except ActivityError as e: + failure = e.failure + if failure: + cause = failure.cause + if cause: + if ( + cause.timeout_failure_info.timeout_type + == TIMEOUT_TYPE_SCHEDULE_TO_START + ): + raise ApplicationError( + "MCP Stateful Server Worker failed to schedule activity.", + type="DedicatedWorkerFailure", + ) from e + if ( + cause.timeout_failure_info.timeout_type + == TIMEOUT_TYPE_HEARTBEAT + ): + raise ApplicationError( + "MCP Stateful Server Worker failed to heartbeat.", + type="DedicatedWorkerFailure", + ) from e + raise e + + return wrapper + + +@dataclasses.dataclass +class _StatefulCallToolsArguments: + tool_name: str + arguments: dict[str, Any] | None + meta: dict[str, Any] | None = None + + +@dataclasses.dataclass +class _StatefulGetPromptArguments: + name: str + arguments: dict[str, Any] | None + + +@dataclasses.dataclass +class _StatefulServerSessionArguments: + factory_argument: Any | None + + +class _StatefulMCPServerReference(MCPServer, AbstractAsyncContextManager): # type:ignore[reportUnusedClass] + def __init__( + self, + server: str, + config: ActivityConfig | None, + server_session_config: ActivityConfig | None, + factory_argument: Any | None, + ): + self._name = server + "-stateful" + self._config = config or ActivityConfig( + start_to_close_timeout=timedelta(minutes=1), + schedule_to_start_timeout=timedelta(seconds=30), + ) + self._server_session_config = server_session_config or ActivityConfig( + start_to_close_timeout=timedelta(hours=1), + ) + self._connect_handle: ActivityHandle | None = None + self._factory_argument = factory_argument + super().__init__() + + @property + def name(self) -> str: + return self._name + + async def connect(self) -> None: + self._config["task_queue"] = self.name + "@" + workflow.info().run_id + self._connect_handle = workflow.start_activity( + self.name + "-server-session", + _StatefulServerSessionArguments(self._factory_argument), + **self._server_session_config, + ) + + async def cleanup(self) -> None: + if self._connect_handle: + self._connect_handle.cancel() + try: + await self._connect_handle + except Exception as e: + if is_cancelled_exception(e): + pass + else: + raise + + async def __aenter__(self): + await self.connect() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + await self.cleanup() + + @_handle_worker_failure + async def list_tools( + self, + run_context: RunContextWrapper[Any] | None = None, + agent: AgentBase | None = None, + ) -> list[MCPTool]: + if not self._connect_handle: + raise ApplicationError( + "Stateful MCP Server not connected. Call connect first." + ) + return await workflow.execute_activity( + self.name + "-list-tools", + args=[], + result_type=list[MCPTool], + **self._config, + ) + + @_handle_worker_failure + async def call_tool( + self, + tool_name: str, + arguments: dict[str, Any] | None, + meta: dict[str, Any] | None = None, + ) -> CallToolResult: + if not self._connect_handle: + raise ApplicationError( + "Stateful MCP Server not connected. Call connect first." + ) + return await workflow.execute_activity( + self.name + "-call-tool-v2", + _StatefulCallToolsArguments(tool_name, arguments, meta), + result_type=CallToolResult, + **self._config, + ) + + @_handle_worker_failure + async def list_prompts(self) -> ListPromptsResult: + if not self._connect_handle: + raise ApplicationError( + "Stateful MCP Server not connected. Call connect first." + ) + return await workflow.execute_activity( + self.name + "-list-prompts", + args=[], + result_type=ListPromptsResult, + **self._config, + ) + + @_handle_worker_failure + async def get_prompt( + self, name: str, arguments: dict[str, Any] | None = None + ) -> GetPromptResult: + if not self._connect_handle: + raise ApplicationError( + "Stateful MCP Server not connected. Call connect first." + ) + return await workflow.execute_activity( + self.name + "-get-prompt-v2", + _StatefulGetPromptArguments(name, arguments), + result_type=GetPromptResult, + **self._config, + ) + + +class StatefulMCPServerProvider: + """A stateful MCP server implementation for Temporal workflows. + + This class wraps a function to create MCP servers to maintain a persistent connection throughout + the workflow execution. It creates a dedicated worker that stays connected to + the MCP server and processes operations on a dedicated task queue. + + This approach will allow the MCPServer to maintain state across calls if needed, but the caller + will have to handle cases where the dedicated worker fails, as Temporal is unable to seamlessly + recreate any lost state in that case. It is discouraged to use this approach unless necessary. + + Handling dedicated worker failure will entail catching ApplicationError with type "DedicatedWorkerFailure". + Depending on the usage pattern, the caller will then have to either restart from the point at which the Stateful + server was needed or handle continuing from that loss of state in some other way. + """ + + def __init__( + self, + name: str, + server_factory: Callable[[Any | None], MCPServer], + ): + """Initialize the stateful temporal MCP server. + + Args: + name: The name of the MCP server. + server_factory: A function which will produce MCPServer instances. It should return a new server each time + so that state is not shared between workflow runs. It receives an optional ``factory_argument`` from the + workflow. + """ + self._server_factory = server_factory + self._name = name + "-stateful" + self._connect_handle: ActivityHandle | None = None + self._servers: dict[str, MCPServer] = {} + super().__init__() + + @property + def name(self) -> str: + """Get the server name.""" + return self._name + + def _get_activities(self) -> Sequence[Callable]: + def _server_id(): + return self.name + "@" + (activity.info().workflow_run_id or "") + + @activity.defn(name=self.name + "-list-tools") + async def list_tools() -> list[MCPTool]: + return await self._servers[_server_id()].list_tools() + + @activity.defn(name=self.name + "-call-tool") + async def call_tool_deprecated( + tool_name: str, arguments: dict[str, Any] | None + ) -> CallToolResult: + return await self._servers[_server_id()].call_tool(tool_name, arguments) + + @activity.defn(name=self.name + "-call-tool-v2") + async def call_tool(args: _StatefulCallToolsArguments) -> CallToolResult: + return await self._servers[_server_id()].call_tool( + args.tool_name, args.arguments, args.meta + ) + + @activity.defn(name=self.name + "-list-prompts") + async def list_prompts() -> ListPromptsResult: + return await self._servers[_server_id()].list_prompts() + + @activity.defn(name=self.name + "-get-prompt") + async def get_prompt_deprecated( + name: str, arguments: dict[str, Any] | None + ) -> GetPromptResult: + return await self._servers[_server_id()].get_prompt(name, arguments) + + @activity.defn(name=self.name + "-get-prompt-v2") + async def get_prompt(args: _StatefulGetPromptArguments) -> GetPromptResult: + return await self._servers[_server_id()].get_prompt( + args.name, args.arguments + ) + + async def heartbeat_every(delay: float, *details: Any) -> None: + """Heartbeat every so often while not cancelled""" + while True: + await asyncio.sleep(delay) + activity.heartbeat(*details) + + @activity.defn(name=self.name + "-server-session") + async def connect( + args: _StatefulServerSessionArguments | None = None, + ) -> None: + heartbeat_task = asyncio.create_task(heartbeat_every(30)) + + server_id = self.name + "@" + (activity.info().workflow_run_id or "") + if server_id in self._servers: + raise ApplicationError( + "Cannot connect to an already running server. Use a distinct name if running multiple servers in one workflow." + ) + server = self._server_factory(args.factory_argument if args else None) + try: + self._servers[server_id] = server + try: + await server.connect() + + worker = Worker( + activity.client(), + task_queue=server_id, + activities=[ + list_tools, + call_tool, + list_prompts, + get_prompt, + call_tool_deprecated, + get_prompt_deprecated, + ], + activity_task_poller_behavior=PollerBehaviorSimpleMaximum(1), + ) + + await worker.run() + finally: + await server.cleanup() + heartbeat_task.cancel() + try: + await heartbeat_task + except asyncio.CancelledError: + pass + finally: + del self._servers[server_id] + + return (connect,) diff --git a/temporalio/contrib/openai_agents/_model_parameters.py b/temporalio/contrib/openai_agents/_model_parameters.py index 12d83331c..c7dcf0a35 100644 --- a/temporalio/contrib/openai_agents/_model_parameters.py +++ b/temporalio/contrib/openai_agents/_model_parameters.py @@ -3,7 +3,7 @@ from abc import ABC, abstractmethod from dataclasses import dataclass from datetime import timedelta -from typing import Any, Callable, Optional, Union +from typing import Any from agents import Agent, TResponseInputItem @@ -19,9 +19,9 @@ class ModelSummaryProvider(ABC): @abstractmethod def provide( self, - agent: Optional[Agent[Any]], - instructions: Optional[str], - input: Union[str, list[TResponseInputItem]], + agent: Agent[Any] | None, + instructions: str | None, + input: str | list[TResponseInputItem], ) -> str: """Given the provided information, produce a summary for the model invocation activity.""" pass @@ -36,37 +36,61 @@ class ModelActivityParameters: OpenAI Agents integration. """ - task_queue: Optional[str] = None + task_queue: str | None = None """Specific task queue to use for model activities.""" - schedule_to_close_timeout: Optional[timedelta] = None + schedule_to_close_timeout: timedelta | None = None """Maximum time from scheduling to completion.""" - schedule_to_start_timeout: Optional[timedelta] = None + schedule_to_start_timeout: timedelta | None = None """Maximum time from scheduling to starting.""" - start_to_close_timeout: Optional[timedelta] = timedelta(seconds=60) + start_to_close_timeout: timedelta | None = timedelta(seconds=60) """Maximum time for the activity to complete.""" - heartbeat_timeout: Optional[timedelta] = None - """Maximum time between heartbeats.""" + heartbeat_timeout: timedelta | None = None + """Maximum time between heartbeats. For streaming + (``Runner.run_streamed``), set this lower than + ``start_to_close_timeout`` so a stuck model call is detected before the + overall activity timeout fires.""" - retry_policy: Optional[RetryPolicy] = None + retry_policy: RetryPolicy | None = None """Policy for retrying failed activities.""" cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL """How the activity handles cancellation.""" - versioning_intent: Optional[VersioningIntent] = None + versioning_intent: VersioningIntent | None = None """Versioning intent for the activity.""" - summary_override: Optional[ - Union[ - str, - ModelSummaryProvider, - ] - ] = None + summary_override: None | (str | ModelSummaryProvider) = None """Summary for the activity execution.""" priority: Priority = Priority.default """Priority for the activity execution.""" + + use_local_activity: bool = False + """Whether to use a local activity. If changed during a workflow execution, that would break determinism.""" + + streaming_topic: str | None = None + """Stream topic to publish raw model stream events to when the workflow + calls ``Runner.run_streamed``. Required for ``Runner.run_streamed``; + if left as ``None``, ``run_streamed`` raises before scheduling any + activity. The workflow must host a + :class:`temporalio.contrib.workflow_streams.WorkflowStream` to receive + the publishes; otherwise the signals are unhandled and dropped. + + Streaming is incompatible with ``use_local_activity`` (local activities + do not support heartbeats or the workflow stream signal channel). + + .. warning:: + Streaming support is experimental and may change in future + versions.""" + + streaming_batch_interval: timedelta = timedelta(milliseconds=100) + """Interval between automatic flushes for the stream publisher used + by the streaming activity. + + .. warning:: + Streaming support is experimental and may change in future + versions.""" diff --git a/temporalio/contrib/openai_agents/_openai_runner.py b/temporalio/contrib/openai_agents/_openai_runner.py index 4f9dbbf65..ea2e6e5df 100644 --- a/temporalio/contrib/openai_agents/_openai_runner.py +++ b/temporalio/contrib/openai_agents/_openai_runner.py @@ -1,7 +1,6 @@ import dataclasses -import json -import typing -from typing import Any, Optional, Union +from collections.abc import AsyncIterator, Awaitable +from typing import Any, Callable from agents import ( Agent, @@ -11,20 +10,117 @@ RunContextWrapper, RunResult, RunResultStreaming, + RunState, SQLiteSession, TContext, - Tool, TResponseInputItem, ) -from agents.run import DEFAULT_AGENT_RUNNER, DEFAULT_MAX_TURNS, AgentRunner -from pydantic_core import to_json +from agents.run import DEFAULT_AGENT_RUNNER, AgentRunner, RunOptions +from agents.sandbox import SandboxAgent +from typing_extensions import Unpack from temporalio import workflow from temporalio.contrib.openai_agents._model_parameters import ModelActivityParameters from temporalio.contrib.openai_agents._temporal_model_stub import _TemporalModelStub +from temporalio.contrib.openai_agents.sandbox._temporal_sandbox_client import ( + TemporalSandboxClient, +) from temporalio.contrib.openai_agents.workflow import AgentsWorkflowError +# Recursively replace models in all agents +def _convert_agent( + model_params: ModelActivityParameters, + agent: Agent[Any], + seen: dict[int, Agent] | None, +) -> Agent[Any]: + if seen is None: + seen = dict() + + # Short circuit if this model was already seen to prevent looping from circular handoffs + if id(agent) in seen: + return seen[id(agent)] + + # This agent has already been processed in some other run + if isinstance(agent.model, _TemporalModelStub): + return agent + + # Save the new version of the agent so that we can replace loops + new_agent = dataclasses.replace(agent) + seen[id(agent)] = new_agent + + name = _model_name(agent) + + new_handoffs: list[Agent | Handoff] = [] + for handoff in agent.handoffs: + if isinstance(handoff, Agent): + new_handoffs.append(_convert_agent(model_params, handoff, seen)) + elif isinstance(handoff, Handoff): + original_invoke = handoff.on_invoke_handoff + + # Use default parameter to capture original_invoke by value, not reference + async def on_invoke( + context: RunContextWrapper[Any], + args: str, + invoke_func: Callable[ + [RunContextWrapper[Any], str], Awaitable[Any] + ] = original_invoke, + ) -> Agent: + handoff_agent = await invoke_func(context, args) + return _convert_agent(model_params, handoff_agent, seen) + + new_handoffs.append( + dataclasses.replace(handoff, on_invoke_handoff=on_invoke) + ) + else: + raise TypeError(f"Unknown handoff type: {type(handoff)}") + + new_agent.model = _TemporalModelStub( + model_name=name, + model_params=model_params, + agent=agent, + ) + new_agent.handoffs = new_handoffs + return new_agent + + +def _has_sandbox_agent(agent: Agent[Any], seen: set[int] | None = None) -> bool: + """Check if any agent in the graph (following direct Agent handoffs) is a SandboxAgent.""" + if seen is None: + seen = set() + if id(agent) in seen: + return False + seen.add(id(agent)) + if isinstance(agent, SandboxAgent): + return True + for handoff in agent.handoffs: + if isinstance(handoff, Agent) and _has_sandbox_agent(handoff, seen): + return True + return False + + +def _coerce_run_config(value: object) -> RunConfig: + """openai-agents >= 0.19 also accepts a plain dict for ``run_config``. + + This function normalizes to a RunConfig instance. + """ + if isinstance(value, RunConfig): + return value + if not isinstance(value, dict): + raise TypeError( + f"run_config must be a RunConfig instance or a dict, got {type(value).__name__}" + ) + field_names = { + config_field.name + for config_field in dataclasses.fields(RunConfig) + if config_field.init + } + unknown_fields = sorted(str(name) for name in value if name not in field_names) + if unknown_fields: + raise TypeError(f"Unknown run_config settings: {', '.join(unknown_fields)}") + return RunConfig(**value) + + class TemporalOpenAIRunner(AgentRunner): """Temporal Runner for OpenAI agents. @@ -32,56 +128,53 @@ class TemporalOpenAIRunner(AgentRunner): """ - def __init__(self, model_params: ModelActivityParameters) -> None: + def __init__( + self, + model_params: ModelActivityParameters, + ) -> None: """Initialize the Temporal OpenAI Runner.""" self._runner = DEFAULT_AGENT_RUNNER or AgentRunner() self.model_params = model_params - async def run( + def _prepare_workflow_run( self, starting_agent: Agent[TContext], - input: Union[str, list[TResponseInputItem]], - **kwargs: Any, - ) -> RunResult: - """Run the agent in a Temporal workflow.""" - if not workflow.in_workflow(): - return await self._runner.run( - starting_agent, - input, - **kwargs, - ) - - tool_types = typing.get_args(Tool) + kwargs: RunOptions[TContext], + ) -> Agent[Any]: + """Workflow-only validation and ``kwargs`` rewrite shared by ``run()`` and ``run_streamed()``.""" for t in starting_agent.tools: - if not isinstance(t, tool_types): + if callable(t): raise ValueError( "Provided tool is not a tool type. If using an activity, make sure to wrap it with openai_agents.workflow.activity_as_tool." ) if starting_agent.mcp_servers: - raise ValueError( - "Temporal OpenAI agent does not support on demand MCP servers." + from temporalio.contrib.openai_agents._mcp import ( + _StatefulMCPServerReference, + _StatelessMCPServerReference, ) - # workaround for https://github.com/pydantic/pydantic/issues/9541 - # ValidatorIterator returned - input_json = to_json(input) - input = json.loads(input_json) - - context = kwargs.get("context") - max_turns = kwargs.get("max_turns", DEFAULT_MAX_TURNS) - hooks = kwargs.get("hooks") - run_config = kwargs.get("run_config") - previous_response_id = kwargs.get("previous_response_id") - session = kwargs.get("session") + for s in starting_agent.mcp_servers: + if not isinstance( + s, + ( + _StatelessMCPServerReference, + _StatefulMCPServerReference, + ), + ): + raise ValueError( + f"Unknown mcp_server type {type(s)} may not work durably." + ) - if isinstance(session, SQLiteSession): + if isinstance(kwargs.get("session"), SQLiteSession): raise ValueError("Temporal workflows don't support SQLite sessions.") - if run_config is None: - run_config = RunConfig() + run_config = kwargs.get("run_config") + run_config = ( + RunConfig() if run_config is None else _coerce_run_config(run_config) + ) - if run_config.model: + if run_config.model and not isinstance(run_config.model, _TemporalModelStub): if not isinstance(run_config.model, str): raise ValueError( "Temporal workflows require a model name to be a string in the run config." @@ -93,61 +186,53 @@ async def run( ), ) - # Recursively replace models in all agents - def convert_agent(agent: Agent[Any], seen: Optional[set[int]]) -> Agent[Any]: - if seen is None: - seen = set() - - # Short circuit if this model was already seen to prevent looping from circular handoffs - if id(agent) in seen: - return agent - seen.add(id(agent)) - - # This agent has already been processed in some other run - if isinstance(agent.model, _TemporalModelStub): - return agent - - name = _model_name(agent) - - new_handoffs: list[Union[Agent, Handoff]] = [] - for handoff in agent.handoffs: - if isinstance(handoff, Agent): - new_handoffs.append(convert_agent(handoff, seen)) - elif isinstance(handoff, Handoff): - original_invoke = handoff.on_invoke_handoff - - async def on_invoke( - context: RunContextWrapper[Any], args: str - ) -> Agent: - handoff_agent = await original_invoke(context, args) - return convert_agent(handoff_agent, seen) - - new_handoffs.append( - dataclasses.replace(handoff, on_invoke_handoff=on_invoke) - ) - else: - raise ValueError(f"Unknown handoff type: {type(handoff)}") + # run_config.sandbox is global for the entire run — configure it if any agent needs it. + if _has_sandbox_agent(starting_agent) or run_config.sandbox: + if run_config.sandbox is None: + raise ValueError( + "A SandboxAgent was provided but run_config.sandbox is not configured. " + "You must set run_config.sandbox to a SandboxRunConfig. " + "For example:\n" + " from temporalio.contrib.openai_agents.workflow import temporal_sandbox_client\n" + " run_config = RunConfig(sandbox=SandboxRunConfig(client=temporal_sandbox_client('my-backend')))" + ) + elif run_config.sandbox.client is None: + raise ValueError( + "run_config.sandbox.client must be set to a temporal sandbox client. " + "Use temporalio.contrib.openai_agents.workflow.temporal_sandbox_client(name) " + "to create one, where name matches a SandboxClientProvider registered on the plugin." + ) + elif not isinstance(run_config.sandbox.client, TemporalSandboxClient): + raise ValueError( + "run_config.sandbox.client must be created via " + "temporalio.contrib.openai_agents.workflow.temporal_sandbox_client(name). " + "Do not pass a raw sandbox client directly." + ) - return dataclasses.replace( - agent, - model=_TemporalModelStub( - model_name=name, - model_params=self.model_params, - agent=agent, - ), - handoffs=new_handoffs, + kwargs["run_config"] = run_config + return _convert_agent(self.model_params, starting_agent, None) + + async def run( + self, + starting_agent: Agent[TContext], + input: str | list[TResponseInputItem] | RunState[TContext], + **kwargs: Unpack[RunOptions[TContext]], + ) -> RunResult: + """Run the agent in a Temporal workflow.""" + if not workflow.in_workflow(): + return await self._runner.run( + starting_agent, + input, + **kwargs, ) + converted_agent = self._prepare_workflow_run(starting_agent, kwargs) + try: return await self._runner.run( - starting_agent=convert_agent(starting_agent, None), + starting_agent=converted_agent, input=input, - context=context, - max_turns=max_turns, - hooks=hooks, - run_config=run_config, - previous_response_id=previous_response_id, - session=session, + **kwargs, ) except AgentsException as e: # In order for workflow failures to properly fail the workflow, we need to rewrap them in @@ -164,7 +249,7 @@ async def on_invoke( def run_sync( self, starting_agent: Agent[TContext], - input: Union[str, list[TResponseInputItem]], + input: str | list[TResponseInputItem] | RunState[TContext], **kwargs: Any, ) -> RunResult: """Run the agent synchronously (not supported in Temporal workflows).""" @@ -179,20 +264,109 @@ def run_sync( def run_streamed( self, starting_agent: Agent[TContext], - input: Union[str, list[TResponseInputItem]], - **kwargs: Any, + input: str | list[TResponseInputItem] | RunState[TContext], + **kwargs: Unpack[RunOptions[TContext]], ) -> RunResultStreaming: - """Run the agent with streaming responses (not supported in Temporal workflows).""" + """Run the agent with streaming responses. + + .. warning:: + Streaming inside Temporal workflows is experimental and may + change in future versions. + + Inside a workflow, model calls execute as the streaming model + activity. The workflow consumes events via + ``RunResultStreaming.stream_events()`` after each activity + completes; external clients can subscribe to the configured + stream topic to receive events as they arrive. + """ if not workflow.in_workflow(): return self._runner.run_streamed( starting_agent, input, **kwargs, ) - raise RuntimeError("Temporal workflows do not support streaming.") + + # Fail-fast before the agents framework starts a background task: + # validation raised inside ``Model.stream_response`` is otherwise + # captured into ``RunResultStreaming._stored_exception`` and may + # be silently dropped if the queue completion sentinel is read + # before the run_loop_task is observed as done. + if self.model_params.streaming_topic is None: + raise AgentsWorkflowError( + "Runner.run_streamed requires " + "ModelActivityParameters.streaming_topic to be set." + ) + if self.model_params.use_local_activity: + raise AgentsWorkflowError( + "Runner.run_streamed is incompatible with " + "use_local_activity (local activities do not support " + "heartbeats or the workflow stream signal channel)." + ) + + converted_agent = self._prepare_workflow_run(starting_agent, kwargs) + + streamed_result = self._runner.run_streamed( + starting_agent=converted_agent, + input=input, + **kwargs, + ) + + # Mirror the AgentsException -> AgentsWorkflowError rewrap done + # in run() above. The streaming runner attaches the actual run + # to ``run_loop_task``; we wrap ``stream_events()`` (rather than + # the task itself) so the rewrap happens on the consumer's + # coroutine. Wrapping in a second asyncio task introduces a + # scheduling gap: ``RunResultStreaming.stream_events()`` reads + # the queue completion sentinel as soon as the run loop ends, + # but the wrapper task only resumes its ``await`` after another + # event-loop tick — between those two points, ``_check_errors`` + # sees no exception and ``_await_task_safely`` later swallows + # the rewrapped one. Iterating the underlying generator first, + # then inspecting the finished task on exit, keeps the rewrap + # race-free without touching ``run_loop_task``. + original_stream_events = streamed_result.stream_events + run_loop_task = streamed_result.run_loop_task + + async def _stream_events_with_rewrap() -> AsyncIterator[Any]: + try: + async for event in original_stream_events(): + yield event + except AgentsException as e: + _reraise_workflow_failure(e) + raise + # The agents framework may have stored the run-loop + # exception on ``_stored_exception`` (or surfaced it through + # the iterator) without re-raising it through stream_events. + # By the time the iterator is exhausted, ``run_loop_task`` + # is done — surface its exception here so a failed run + # cannot appear successful, applying the workflow-failure + # rewrap when applicable. + if run_loop_task is not None and run_loop_task.done(): + exc = run_loop_task.exception() + if exc is not None: + if isinstance(exc, AgentsException): + _reraise_workflow_failure(exc) + raise exc + + streamed_result.stream_events = _stream_events_with_rewrap # type: ignore[method-assign] + return streamed_result + + +def _reraise_workflow_failure(e: AgentsException) -> None: + """Rewrap an AgentsException whose cause is a Temporal workflow failure. + + Returns normally when ``e`` is not workflow-failure-bearing so the + caller can re-raise the original. + """ + if e.__cause__ and workflow.is_failure_exception(e.__cause__): + reraise = AgentsWorkflowError( + f"Workflow failure exception in Agents Framework: {e}" + ) + reraise.__traceback__ = e.__traceback__ + raise reraise from e.__cause__ -def _model_name(agent: Agent[Any]) -> Optional[str]: +def _model_name(agent: Agent[Any]) -> str | None: name = agent.model if name is not None and not isinstance(name, str): raise ValueError( diff --git a/temporalio/contrib/openai_agents/_otel_trace_interceptor.py b/temporalio/contrib/openai_agents/_otel_trace_interceptor.py new file mode 100644 index 000000000..63f8f9d83 --- /dev/null +++ b/temporalio/contrib/openai_agents/_otel_trace_interceptor.py @@ -0,0 +1,88 @@ +"""OTEL-aware variant of OpenAI Agents trace interceptor.""" + +from __future__ import annotations + +from typing import Any + +import opentelemetry.trace + +import temporalio.converter + +from ..opentelemetry._id_generator import TemporalIdGenerator +from ._trace_interceptor import ( + OpenAIAgentsContextPropagationInterceptor, + _InputWithHeaders, +) + + +class OTelOpenAIAgentsContextPropagationInterceptor( + OpenAIAgentsContextPropagationInterceptor +): + """OTEL-aware variant that enhances headers with OpenTelemetry span context.""" + + def __init__( + self, + otel_id_generator: TemporalIdGenerator, + payload_converter: temporalio.converter.PayloadConverter = temporalio.converter.default().payload_converter, + add_temporal_spans: bool = True, + ) -> None: + """Initialize OTEL-aware context propagation interceptor. + + Args: + otel_id_generator: Generator for OTEL-compatible IDs. + payload_converter: Converter for serializing trace context. + add_temporal_spans: Whether to add Temporal-specific spans. + """ + super().__init__(payload_converter, add_temporal_spans, start_traces=True) + self._otel_id_generator = otel_id_generator + + def header_contents(self) -> dict[str, Any]: + """Get header contents enhanced with OpenTelemetry span context. + + Returns: + Dictionary containing trace context with OTEL span information. + """ + otel_span = opentelemetry.trace.get_current_span() + + if otel_span and otel_span.get_span_context().is_valid: + span_context = otel_span.get_span_context() + return { + **super().header_contents(), + "otelSpanId": span_context.span_id, + "otelTraceId": span_context.trace_id, + } + else: + return super().header_contents() + + def context_from_header( + self, + input: _InputWithHeaders, + ): + """Extracts and initializes trace information the input header.""" + span_info = self.get_header_contents(input) + + if span_info is None: + return + otel_span_id = span_info.get("otelSpanId") + otel_trace_id = span_info.get("otelTraceId") + + # Seed the trace id before the trace is reconstructed so the workflow's root + # OTEL span shares the caller's trace id rather than generating a new one. + if otel_trace_id and self._otel_id_generator: + self._otel_id_generator.seed_trace_id(otel_trace_id) + + # If only a trace was propagated from the caller, we need to seed for trace context + if otel_span_id and self._otel_id_generator and span_info.get("spanId") is None: + self._otel_id_generator.seed_span_id(otel_span_id) + + super().trace_context_from_header_contents(span_info) + + # If a span was propagated from the caller, we need to seed for span context + if ( + otel_span_id + and self._otel_id_generator + and span_info.get("spanId") is not None + ): + self._otel_id_generator.seed_span_id(otel_span_id) + + super().span_context_from_header_contents(span_info) diff --git a/temporalio/contrib/openai_agents/_temporal_model_stub.py b/temporalio/contrib/openai_agents/_temporal_model_stub.py index 11f1ddc5e..d184daa4a 100644 --- a/temporalio/contrib/openai_agents/_temporal_model_stub.py +++ b/temporalio/contrib/openai_agents/_temporal_model_stub.py @@ -1,14 +1,7 @@ from __future__ import annotations -import logging -from typing import Optional - -from temporalio import workflow -from temporalio.contrib.openai_agents._model_parameters import ModelActivityParameters - -logger = logging.getLogger(__name__) - -from typing import Any, AsyncIterator, Union, cast +from collections.abc import AsyncIterator +from typing import Any from agents import ( Agent, @@ -29,47 +22,61 @@ WebSearchTool, ) from agents.items import TResponseStreamEvent +from agents.tool import ( + ApplyPatchTool, + CustomTool, + LocalShellTool, + ShellTool, + ToolSearchTool, +) from openai.types.responses.response_prompt_param import ResponsePromptParam +from temporalio import workflow from temporalio.contrib.openai_agents._invoke_model_activity import ( ActivityModelInput, AgentOutputSchemaInput, + ApplyPatchToolInput, + CustomToolInput, FunctionToolInput, HandoffInput, HostedMCPToolInput, ModelActivity, ModelTracingInput, + ShellToolInput, + StreamingActivityModelInput, ToolInput, ) +from temporalio.contrib.openai_agents._model_parameters import ModelActivityParameters -class _TemporalModelStub(Model): +class _TemporalModelStub(Model): # type:ignore[reportUnusedClass] """A stub that allows invoking models as Temporal activities.""" def __init__( self, - model_name: Optional[str], + model_name: str | None, *, model_params: ModelActivityParameters, - agent: Optional[Agent[Any]], + agent: Agent[Any] | None, ) -> None: self.model_name = model_name self.model_params = model_params self.agent = agent - async def get_response( + def _build_activity_input( self, - system_instructions: Optional[str], - input: Union[str, list[TResponseInputItem]], + *, + system_instructions: str | None, + input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: Optional[AgentOutputSchemaBase], + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], tracing: ModelTracing, - *, - previous_response_id: Optional[str], - prompt: Optional[ResponsePromptParam], - ) -> ModelResponse: + previous_response_id: str | None, + conversation_id: str | None, + prompt: ResponsePromptParam | None, + ) -> tuple[ActivityModelInput, str | None]: def make_tool_info(tool: Tool) -> ToolInput: if isinstance( tool, @@ -78,11 +85,22 @@ def make_tool_info(tool: Tool) -> ToolInput: WebSearchTool, ImageGenerationTool, CodeInterpreterTool, + LocalShellTool, + ToolSearchTool, ), ): return tool + elif isinstance(tool, ShellTool): + return ShellToolInput( + name=tool.name, + environment=tool.environment, + ) + elif isinstance(tool, ApplyPatchTool): + return ApplyPatchToolInput(name=tool.name) elif isinstance(tool, HostedMCPTool): return HostedMCPToolInput(tool_config=tool.tool_config) + elif isinstance(tool, CustomTool): + return CustomToolInput(tool_config=tool.tool_config) elif isinstance(tool, FunctionTool): return FunctionToolInput( name=tool.name, @@ -134,6 +152,7 @@ def make_tool_info(tool: Tool) -> ToolInput: handoffs=handoff_infos, tracing=ModelTracingInput(tracing.value), previous_response_id=previous_response_id, + conversation_id=conversation_id, prompt=prompt, ) @@ -152,6 +171,46 @@ def make_tool_info(tool: Tool) -> ToolInput: else: summary = None + return activity_input, summary + + async def get_response( + self, + system_instructions: str | None, + input: str | list[TResponseInputItem], + model_settings: ModelSettings, + tools: list[Tool], + output_schema: AgentOutputSchemaBase | None, + handoffs: list[Handoff], + tracing: ModelTracing, + *, + previous_response_id: str | None, + conversation_id: str | None, + prompt: ResponsePromptParam | None, + ) -> ModelResponse: + activity_input, summary = self._build_activity_input( + system_instructions=system_instructions, + input=input, + model_settings=model_settings, + tools=tools, + output_schema=output_schema, + handoffs=handoffs, + tracing=tracing, + previous_response_id=previous_response_id, + conversation_id=conversation_id, + prompt=prompt, + ) + + if self.model_params.use_local_activity: + return await workflow.execute_local_activity_method( + ModelActivity.invoke_model_activity, + activity_input, + summary=summary, + schedule_to_close_timeout=self.model_params.schedule_to_close_timeout, + schedule_to_start_timeout=self.model_params.schedule_to_start_timeout, + start_to_close_timeout=self.model_params.start_to_close_timeout, + retry_policy=self.model_params.retry_policy, + cancellation_type=self.model_params.cancellation_type, + ) return await workflow.execute_activity_method( ModelActivity.invoke_model_activity, activity_input, @@ -167,48 +226,71 @@ def make_tool_info(tool: Tool) -> ToolInput: priority=self.model_params.priority, ) - def stream_response( + async def stream_response( self, - system_instructions: Optional[str], - input: Union[str, list[TResponseInputItem]], + system_instructions: str | None, + input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: Optional[AgentOutputSchemaBase], + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], tracing: ModelTracing, *, - previous_response_id: Optional[str], + previous_response_id: str | None, + conversation_id: str | None, prompt: ResponsePromptParam | None, ) -> AsyncIterator[TResponseStreamEvent]: - raise NotImplementedError("Temporal model doesn't support streams yet") - - -def _extract_summary(input: Union[str, list[TResponseInputItem]]) -> str: - ### Activity summary shown in the UI - try: - max_size = 100 - if isinstance(input, str): - return input[:max_size] - elif isinstance(input, list): - # Find all message inputs, which are reasonably summarizable - messages: list[TResponseInputItem] = [ - item for item in input if item.get("type", "message") == "message" - ] - if not messages: - return "" - - content: Any = messages[-1].get("content", "") - - # In the case of multiple contents, take the last one - if isinstance(content, list): - if not content: - return "" - content = content[-1] - - # Take the text field from the content if present - if isinstance(content, dict) and content.get("text") is not None: - content = content.get("text") - return str(content)[:max_size] - except Exception as e: - logger.error(f"Error getting summary: {e}") - return "" + # Streaming relies on activity heartbeats to detect a stuck LLM + # call and on WorkflowStreamClient.from_within_activity() to signal + # partial results back to the workflow. Local activities support + # neither: their result commits with the workflow task, so there + # is no independent task to heartbeat against or to send signals + # from. + if self.model_params.use_local_activity: + raise ValueError( + "Streaming is incompatible with use_local_activity " + "(local activities do not support heartbeats or the " + "workflow stream signal channel)." + ) + + topic = self.model_params.streaming_topic + if topic is None: + raise ValueError( + "Runner.run_streamed requires " + "ModelActivityParameters.streaming_topic to be set." + ) + + base_input, summary = self._build_activity_input( + system_instructions=system_instructions, + input=input, + model_settings=model_settings, + tools=tools, + output_schema=output_schema, + handoffs=handoffs, + tracing=tracing, + previous_response_id=previous_response_id, + conversation_id=conversation_id, + prompt=prompt, + ) + streaming_input: StreamingActivityModelInput = { + **base_input, + "streaming_topic": topic, + "streaming_batch_interval": self.model_params.streaming_batch_interval, + } + + events = await workflow.execute_activity_method( + ModelActivity.invoke_model_activity_streaming, + streaming_input, + summary=summary, + task_queue=self.model_params.task_queue, + schedule_to_close_timeout=self.model_params.schedule_to_close_timeout, + schedule_to_start_timeout=self.model_params.schedule_to_start_timeout, + start_to_close_timeout=self.model_params.start_to_close_timeout, + heartbeat_timeout=self.model_params.heartbeat_timeout, + retry_policy=self.model_params.retry_policy, + cancellation_type=self.model_params.cancellation_type, + versioning_intent=self.model_params.versioning_intent, + priority=self.model_params.priority, + ) + for event in events: + yield event diff --git a/temporalio/contrib/openai_agents/_temporal_openai_agents.py b/temporalio/contrib/openai_agents/_temporal_openai_agents.py index 0c698aa98..63e8cb10b 100644 --- a/temporalio/contrib/openai_agents/_temporal_openai_agents.py +++ b/temporalio/contrib/openai_agents/_temporal_openai_agents.py @@ -1,30 +1,25 @@ """Initialize Temporal OpenAI Agents overrides.""" +import dataclasses +import json +import threading +import typing +from collections.abc import AsyncIterator, Callable, Iterator, Sequence from contextlib import asynccontextmanager, contextmanager from datetime import timedelta -from typing import AsyncIterator, Callable, Optional, Union - -from agents import ( - AgentOutputSchemaBase, - Handoff, - Model, - ModelProvider, - ModelResponse, - ModelSettings, - ModelTracing, - Tool, - TResponseInputItem, - set_trace_provider, -) -from agents.items import TResponseStreamEvent + +import pydantic +from agents import ModelProvider, Trace, set_trace_provider from agents.run import get_default_agent_runner, set_default_agent_runner from agents.tracing import get_trace_provider from agents.tracing.provider import DefaultTraceProvider -from openai.types.responses import ResponsePromptParam -import temporalio.client -import temporalio.worker -from temporalio.client import ClientConfig +# construct_type is OpenAI's lenient (non-validating) model builder, the same +# one the SDK uses to parse live API responses. It is in a private module but +# has no public alias. +from openai._models import construct_type + +import temporalio.api.common.v1 from temporalio.contrib.openai_agents._invoke_model_activity import ModelActivity from temporalio.contrib.openai_agents._model_parameters import ModelActivityParameters from temporalio.contrib.openai_agents._openai_runner import ( @@ -34,57 +29,116 @@ TemporalTraceProvider, ) from temporalio.contrib.openai_agents._trace_interceptor import ( - OpenAIAgentsTracingInterceptor, + OpenAIAgentsContextPropagationInterceptor, ) from temporalio.contrib.openai_agents.workflow import AgentsWorkflowError +from temporalio.contrib.opentelemetry._tracer_provider import ReplaySafeTracerProvider from temporalio.contrib.pydantic import ( - PydanticPayloadConverter, + PydanticJSONPlainPayloadConverter, ToJsonOptions, ) from temporalio.converter import ( + CompositePayloadConverter, DataConverter, + DefaultPayloadConverter, + JSONPlainPayloadConverter, ) -from temporalio.worker import ( - Replayer, - ReplayerConfig, - Worker, - WorkerConfig, - WorkflowReplayResult, -) +from temporalio.plugin import SimplePlugin +from temporalio.worker import WorkflowRunner +from temporalio.worker.workflow_sandbox import SandboxedWorkflowRunner + +if typing.TYPE_CHECKING: + from temporalio.contrib.openai_agents import ( + SandboxClientProvider, + StatefulMCPServerProvider, + StatelessMCPServerProvider, + ) -@contextmanager -def set_open_ai_agent_temporal_overrides( - model_params: ModelActivityParameters, - auto_close_tracing_in_workflows: bool = False, -): - """Configure Temporal-specific overrides for OpenAI agents. +_otel_trace_start_patch_lock = threading.RLock() +_otel_trace_start_patch_ref_count = 0 +_otel_trace_start_original: Callable[..., typing.Any] | None = None +_otel_trace_start_instrumentor: typing.Any | None = None - .. warning:: - This API is experimental and may change in future versions. - Use with caution in production environments. Future versions may wrap the worker directly - instead of requiring this context manager. - This context manager sets up the necessary Temporal-specific runners and trace providers - for running OpenAI agents within Temporal workflows. It should be called in the main - entry point of your application before initializing the Temporal client and worker. +def _install_otel_instrumentation(tracer_provider: typing.Any) -> None: + """Configure OpenInference while at least one tracing context is active.""" + global _otel_trace_start_instrumentor + global _otel_trace_start_original + global _otel_trace_start_patch_ref_count - The context manager handles: - 1. Setting up a Temporal-specific runner for OpenAI agents - 2. Configuring a Temporal-aware trace provider - 3. Restoring previous settings when the context exits + from openinference.instrumentation.openai_agents import OpenAIAgentsInstrumentor + from openinference.instrumentation.openai_agents._processor import ( + OpenInferenceTracingProcessor, + ) + from opentelemetry.context import attach + from opentelemetry.trace import set_span_in_context + + with _otel_trace_start_patch_lock: + if _otel_trace_start_patch_ref_count == 0: + original_on_trace_start = OpenInferenceTracingProcessor.on_trace_start + _otel_trace_start_original = original_on_trace_start + + def on_trace_start(self: typing.Any, trace: Trace) -> None: # type: ignore[reportUnusedFunction] + original_on_trace_start(self, trace) + attach(set_span_in_context(self._root_spans[trace.trace_id])) + + setattr(OpenInferenceTracingProcessor, "on_trace_start", on_trace_start) + try: + _otel_trace_start_instrumentor = OpenAIAgentsInstrumentor() + _otel_trace_start_instrumentor.instrument( + tracer_provider=tracer_provider + ) + except BaseException: + setattr( + OpenInferenceTracingProcessor, + "on_trace_start", + _otel_trace_start_original, + ) + _otel_trace_start_original = None + _otel_trace_start_instrumentor = None + raise + _otel_trace_start_patch_ref_count += 1 - Args: - model_params: Configuration parameters for Temporal activity execution of model calls. - auto_close_tracing_in_workflows: If set to true, close tracing spans immediately. - Returns: - A context manager that yields the configured TemporalTraceProvider. - """ +def _uninstall_otel_instrumentation() -> None: + """Tear down OpenInference after the final tracing context exits.""" + global _otel_trace_start_instrumentor + global _otel_trace_start_original + global _otel_trace_start_patch_ref_count + + from openinference.instrumentation.openai_agents._processor import ( + OpenInferenceTracingProcessor, + ) + + with _otel_trace_start_patch_lock: + if _otel_trace_start_patch_ref_count == 0: + raise RuntimeError("OpenInference instrumentation was not acquired") + _otel_trace_start_patch_ref_count -= 1 + if _otel_trace_start_patch_ref_count == 0: + try: + if _otel_trace_start_instrumentor is not None: + _otel_trace_start_instrumentor.uninstrument() + finally: + if _otel_trace_start_original is not None: + setattr( + OpenInferenceTracingProcessor, + "on_trace_start", + _otel_trace_start_original, + ) + _otel_trace_start_original = None + _otel_trace_start_instrumentor = None + + +@contextmanager +def _set_open_ai_agent_temporal_overrides( + model_params: ModelActivityParameters, + start_spans_in_replay: bool = False, +): previous_runner = get_default_agent_runner() previous_trace_provider = get_trace_provider() provider = TemporalTraceProvider( - auto_close_in_workflows=auto_close_tracing_in_workflows + start_spans_in_replay=start_spans_in_replay, ) try: @@ -96,69 +150,90 @@ def set_open_ai_agent_temporal_overrides( set_trace_provider(previous_trace_provider or DefaultTraceProvider()) -class TestModelProvider(ModelProvider): - """Test model provider which simply returns the given module.""" +def _lenient_construct(type_: typing.Any, value: typing.Any) -> typing.Any: + """Build ``value`` into ``type_`` without enforcing required fields. - def __init__(self, model: Model): - """Initialize a test model provider with a model.""" - self._model = model - - def get_model(self, model_name: Union[str, None]) -> Model: - """Get a model from the model provider.""" - return self._model + OpenAI's ``construct_type`` handles its own response models (and the + unions/lists thereof), but not the ``agents`` dataclasses that wrap them + (e.g. ``ModelResponse``), so the dataclass layer is reconstructed here and + each field delegated to ``construct_type``. ``include_extras`` preserves the + ``Annotated`` discriminators the unions rely on. + """ + if ( + isinstance(type_, type) + and dataclasses.is_dataclass(type_) + and isinstance(value, dict) + ): + hints = typing.get_type_hints(type_, include_extras=True) + return type_( + **{ + field.name: _lenient_construct( + hints.get(field.name, object), value[field.name] + ) + for field in dataclasses.fields(type_) + if field.name in value + } + ) + return construct_type(type_=type_, value=value) -class TestModel(Model): - """Test model for use mocking model responses.""" +class _OpenAIJSONPlainPayloadConverter(PydanticJSONPlainPayloadConverter): + """Strict pydantic deserialization with a lenient fallback. - def __init__(self, fn: Callable[[], ModelResponse]) -> None: - """Initialize a test model with a callable.""" - self.fn = fn + OpenAI's response models can drift from live API payloads (e.g. a + deprecated-but-required field the API has stopped sending). The SDK tolerates + this when parsing responses, but strict ``validate_json`` on the workflow + side does not, so fall back to lenient construction when validation fails. + """ - async def get_response( - self, - system_instructions: Union[str, None], - input: Union[str, list[TResponseInputItem]], - model_settings: ModelSettings, - tools: list[Tool], - output_schema: Union[AgentOutputSchemaBase, None], - handoffs: list[Handoff], - tracing: ModelTracing, - *, - previous_response_id: Union[str, None], - prompt: Union[ResponsePromptParam, None] = None, - ) -> ModelResponse: - """Get a response from the model.""" - return self.fn() - - def stream_response( + def from_payload( self, - system_instructions: Optional[str], - input: Union[str, list[TResponseInputItem]], - model_settings: ModelSettings, - tools: list[Tool], - output_schema: Optional[AgentOutputSchemaBase], - handoffs: list[Handoff], - tracing: ModelTracing, - *, - previous_response_id: Optional[str], - prompt: Optional[ResponsePromptParam], - ) -> AsyncIterator[TResponseStreamEvent]: - """Get a streamed response from the model. Unimplemented.""" - raise NotImplementedError() - - -class _OpenAIPayloadConverter(PydanticPayloadConverter): + payload: temporalio.api.common.v1.Payload, + type_hint: type | None = None, + ) -> typing.Any: + """See base class.""" + try: + return super().from_payload(payload, type_hint) + except pydantic.ValidationError: + if type_hint is None: + raise + return _lenient_construct(type_hint, json.loads(payload.data)) + + +class OpenAIPayloadConverter(CompositePayloadConverter): + """PayloadConverter for OpenAI agents.""" + def __init__(self) -> None: - super().__init__(ToJsonOptions(exclude_unset=True)) + """Initialize a payload converter.""" + json_payload_converter = _OpenAIJSONPlainPayloadConverter( + ToJsonOptions(exclude_unset=True) + ) + super().__init__( + *( + c + if not isinstance(c, JSONPlainPayloadConverter) + else json_payload_converter + for c in DefaultPayloadConverter.default_encoding_payload_converters + ) + ) -class OpenAIAgentsPlugin(temporalio.client.Plugin, temporalio.worker.Plugin): - """Temporal plugin for integrating OpenAI agents with Temporal workflows. +def _data_converter(converter: DataConverter | None) -> DataConverter: + if converter is None: + return DataConverter(payload_converter_class=OpenAIPayloadConverter) + elif converter.payload_converter_class is DefaultPayloadConverter: + return dataclasses.replace( + converter, payload_converter_class=OpenAIPayloadConverter + ) + elif not isinstance(converter.payload_converter, OpenAIPayloadConverter): + raise ValueError( + "The payload converter must be of type OpenAIPayloadConverter." + ) + return converter + - .. warning:: - This class is experimental and may change in future versions. - Use with caution in production environments. +class OpenAIAgentsPlugin(SimplePlugin): + """Temporal plugin for integrating OpenAI agents with Temporal workflows. This plugin provides seamless integration between the OpenAI Agents SDK and Temporal workflows. It automatically configures the necessary interceptors, @@ -169,18 +244,14 @@ class OpenAIAgentsPlugin(temporalio.client.Plugin, temporalio.worker.Plugin): 1. Configures the Pydantic data converter for type-safe serialization 2. Sets up tracing interceptors for OpenAI agent interactions 3. Registers model execution activities - 4. Manages the OpenAI agent runtime overrides during worker execution - - Args: - model_params: Configuration parameters for Temporal activity execution - of model calls. If None, default parameters will be used. - model_provider: Optional model provider for custom model implementations. - Useful for testing or custom model integrations. + 4. Automatically registers MCP server activities and manages their lifecycles + 5. Manages the OpenAI agent runtime overrides during worker execution Example: >>> from temporalio.client import Client >>> from temporalio.worker import Worker - >>> from temporalio.contrib.openai_agents import OpenAIAgentsPlugin, ModelActivityParameters + >>> from temporalio.contrib.openai_agents import OpenAIAgentsPlugin, ModelActivityParameters, StatelessMCPServerProvider + >>> from agents.mcp import MCPServerStdio >>> from datetime import timedelta >>> >>> # Configure model parameters @@ -189,8 +260,17 @@ class OpenAIAgentsPlugin(temporalio.client.Plugin, temporalio.worker.Plugin): ... retry_policy=RetryPolicy(maximum_attempts=3) ... ) >>> - >>> # Create plugin - >>> plugin = OpenAIAgentsPlugin(model_params=model_params) + >>> # Create MCP servers + >>> filesystem_server = StatelessMCPServerProvider(MCPServerStdio( + ... name="Filesystem Server", + ... params={"command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "."]} + ... )) + >>> + >>> # Create plugin with MCP servers + >>> plugin = OpenAIAgentsPlugin( + ... model_params=model_params, + ... mcp_server_providers=[filesystem_server] + ... ) >>> >>> # Use with client and worker >>> client = await Client.connect( @@ -206,8 +286,15 @@ class OpenAIAgentsPlugin(temporalio.client.Plugin, temporalio.worker.Plugin): def __init__( self, - model_params: Optional[ModelActivityParameters] = None, - model_provider: Optional[ModelProvider] = None, + model_params: ModelActivityParameters | None = None, + model_provider: ModelProvider | None = None, + mcp_server_providers: Sequence[ + "StatelessMCPServerProvider | StatefulMCPServerProvider" + ] = (), + sandbox_clients: Sequence["SandboxClientProvider"] = (), + register_activities: bool = True, + add_temporal_spans: bool = True, + use_otel_instrumentation: bool = False, ) -> None: """Initialize the OpenAI agents plugin. @@ -216,6 +303,26 @@ def __init__( of model calls. If None, default parameters will be used. model_provider: Optional model provider for custom model implementations. Useful for testing or custom model integrations. + mcp_server_providers: Sequence of MCP servers to automatically register with the worker. + Each server will be wrapped in a TemporalMCPServer if not already wrapped, + and their activities will be automatically registered with the worker. + The plugin manages the connection lifecycle of these servers. + sandbox_clients: Sequence of named sandbox client providers to register + on the worker. Each provider pairs a unique name with a real + ``BaseSandboxClient`` (e.g. ``DaytonaSandboxClient``, + ``UnixLocalSandboxClient``). On the workflow side, use + ``temporal_sandbox_client`` + with the matching name to target the correct backend. + Warning: sandbox_clients is experimental and behavior may change in future versions. + Use with caution in production environments. + register_activities: Whether to register activities during the worker execution. + This can be disabled on some workers to allow a separation of workflows and activities + but should not be disabled on all workers, or agents will not be able to progress. + add_temporal_spans: Whether to add temporal spans to traces + use_otel_instrumentation: If set to true, enable open telemetry instrumentation. + Warning: use_otel_instrumentation is experimental and behavior may change in future versions. + Use with caution in production environments. + """ if model_params is None: model_params = ModelActivityParameters() @@ -233,97 +340,125 @@ def __init__( "When configuring a custom provider, the model activity must have start_to_close_timeout or schedule_to_close_timeout" ) - self._model_params = model_params - self._model_provider = model_provider + self._use_otel_instrumentation = use_otel_instrumentation - def init_client_plugin(self, next: temporalio.client.Plugin) -> None: - """Set the next client plugin""" - self.next_client_plugin = next + # Delay activity construction until they are actually needed + def add_activities( + activities: Sequence[Callable] | None, + ) -> Sequence[Callable]: + if not register_activities: + return activities or [] - async def connect_service_client( - self, config: temporalio.service.ConnectConfig - ) -> temporalio.service.ServiceClient: - """No modifications to service client""" - return await self.next_client_plugin.connect_service_client(config) + model_activity = ModelActivity(model_provider) + new_activities = [ + model_activity.invoke_model_activity, + model_activity.invoke_model_activity_streaming, + ] - def init_worker_plugin(self, next: temporalio.worker.Plugin) -> None: - """Set the next worker plugin""" - self.next_worker_plugin = next + server_names = [server.name for server in mcp_server_providers] + if len(server_names) != len(set(server_names)): + raise ValueError( + "More than one mcp server registered with the same name. Please provide unique names." + ) - def configure_client(self, config: ClientConfig) -> ClientConfig: - """Configure the Temporal client for OpenAI agents integration. + for mcp_server in mcp_server_providers: + new_activities.extend(mcp_server._get_activities()) - This method sets up the Pydantic data converter to enable proper - serialization of OpenAI agent objects and responses. + sandbox_names = [sc.name for sc in sandbox_clients] + if len(sandbox_names) != len(set(sandbox_names)): + raise ValueError( + "More than one sandbox client registered with the same name. Please provide unique names." + ) - Args: - config: The client configuration to modify. + for sandbox_provider in sandbox_clients: + new_activities.extend(sandbox_provider._get_activities()) - Returns: - The modified client configuration. - """ - config["data_converter"] = DataConverter( - payload_converter_class=_OpenAIPayloadConverter - ) - return self.next_client_plugin.configure_client(config) + return list(activities or []) + new_activities - def configure_worker(self, config: WorkerConfig) -> WorkerConfig: - """Configure the Temporal worker for OpenAI agents integration. + def workflow_runner(runner: WorkflowRunner | None) -> WorkflowRunner: + if not runner: + raise ValueError("No WorkflowRunner provided to the OpenAI plugin.") - This method adds the necessary interceptors and activities for OpenAI - agent execution: - - Adds tracing interceptors for OpenAI agent interactions - - Registers model execution activities + # If in sandbox, add additional passthrough + if isinstance(runner, SandboxedWorkflowRunner): + return dataclasses.replace( + runner, + restrictions=runner.restrictions.with_passthrough_modules( + "openai", "agents", "mcp" + ), + ) + return runner - Args: - config: The worker configuration to modify. + if not use_otel_instrumentation: + interceptor = OpenAIAgentsContextPropagationInterceptor( + add_temporal_spans=add_temporal_spans, + ) + else: + from opentelemetry import trace as otel_trace - Returns: - The modified worker configuration. - """ - config["interceptors"] = list(config.get("interceptors") or []) + [ - OpenAIAgentsTracingInterceptor() - ] - config["activities"] = list(config.get("activities") or []) + [ - ModelActivity(self._model_provider).invoke_model_activity - ] - config["workflow_failure_exception_types"] = list( - config.get("workflow_failure_exception_types") or [] - ) + [AgentsWorkflowError] - return self.next_worker_plugin.configure_worker(config) - - async def run_worker(self, worker: Worker) -> None: - """Run the worker with OpenAI agents temporal overrides. - - This method sets up the necessary runtime overrides for OpenAI agents - to work within the Temporal worker context, including custom runners - and trace providers. + from ._otel_trace_interceptor import ( + OTelOpenAIAgentsContextPropagationInterceptor, + ) - Args: - worker: The worker instance to run. - """ - with set_open_ai_agent_temporal_overrides(self._model_params): - await self.next_worker_plugin.run_worker(worker) - - def configure_replayer(self, config: ReplayerConfig) -> ReplayerConfig: - """Configure the replayer for OpenAI Agents.""" - config["interceptors"] = list(config.get("interceptors") or []) + [ - OpenAIAgentsTracingInterceptor() - ] - config["data_converter"] = DataConverter( - payload_converter_class=_OpenAIPayloadConverter + provider = otel_trace.get_tracer_provider() + if not isinstance(provider, ReplaySafeTracerProvider): + raise ValueError( + "Global tracer provider must a ReplaySafeTracerProvider. Use temporalio.contrib.opentelemtry.create_trace_provider to create one." + ) + + interceptor = OTelOpenAIAgentsContextPropagationInterceptor( + add_temporal_spans=add_temporal_spans, + otel_id_generator=provider.id_generator(), + ) + + @asynccontextmanager + async def run_context() -> AsyncIterator[None]: + with self.tracing_context(): + with _set_open_ai_agent_temporal_overrides( + model_params, + start_spans_in_replay=use_otel_instrumentation, + ): + yield + + super().__init__( + name="OpenAIAgentsPlugin", + data_converter=_data_converter, + interceptors=[interceptor], + activities=add_activities, + workflow_runner=workflow_runner, + workflow_failure_exception_types=[AgentsWorkflowError], + run_context=lambda: run_context(), ) - return config - @asynccontextmanager - async def run_replayer( - self, - replayer: Replayer, - histories: AsyncIterator[temporalio.client.WorkflowHistory], - ) -> AsyncIterator[AsyncIterator[WorkflowReplayResult]]: - """Set the OpenAI Overrides during replay""" - with set_open_ai_agent_temporal_overrides(self._model_params): - async with self.next_worker_plugin.run_replayer( - replayer, histories - ) as results: - yield results + @contextmanager + def tracing_context(self) -> Iterator[None]: + """Context manager for setting up OpenAI Agents tracing instrumentation. + + This should be called if AgentsSDK traces and/or spans are started outside of the context of a worker. + For example: + + .. code-block:: python + + with env.openai_agents_plugin.tracing_context(): + with trace("External trace"): + with custom_span("External span"): + workflow_handle = await new_client.start_workflow( + ... + ) + + Yields: + Context with tracing instrumentation enabled. + """ + # Set up OTEL instrumentation if enabled + otel_instrumentation_installed = False + if self._use_otel_instrumentation: + from opentelemetry import trace + + _install_otel_instrumentation(trace.get_tracer_provider()) + otel_instrumentation_installed = True + try: + yield + finally: + # Clean up OTEL instrumentation + if otel_instrumentation_installed: + _uninstall_otel_instrumentation() diff --git a/temporalio/contrib/openai_agents/_temporal_trace_provider.py b/temporalio/contrib/openai_agents/_temporal_trace_provider.py index 46905f590..347473545 100644 --- a/temporalio/contrib/openai_agents/_temporal_trace_provider.py +++ b/temporalio/contrib/openai_agents/_temporal_trace_provider.py @@ -2,7 +2,7 @@ import uuid from types import TracebackType -from typing import Any, Optional, cast +from typing import Any, cast from agents import SpanData, Trace, TracingProcessor from agents.tracing import ( @@ -14,8 +14,8 @@ ) from agents.tracing.spans import Span +import temporalio.workflow from temporalio import workflow -from temporalio.contrib.openai_agents._trace_interceptor import RunIdRandom from temporalio.workflow import ReadOnlyContextError @@ -27,10 +27,10 @@ def __init__( activity_id: str, activity_type: str, task_queue: str, - schedule_to_close_timeout: Optional[float] = None, - schedule_to_start_timeout: Optional[float] = None, - start_to_close_timeout: Optional[float] = None, - heartbeat_timeout: Optional[float] = None, + schedule_to_close_timeout: float | None = None, + schedule_to_start_timeout: float | None = None, + start_to_close_timeout: float | None = None, + heartbeat_timeout: float | None = None, ): """Initialize an ActivitySpanData instance.""" self.activity_id = activity_id @@ -79,11 +79,13 @@ def activity_span( class _TemporalTracingProcessor(SynchronousMultiTracingProcessor): def __init__( - self, impl: SynchronousMultiTracingProcessor, auto_close_in_workflows: bool + self, + impl: SynchronousMultiTracingProcessor, + start_spans_in_replay: bool, ): super().__init__() self._impl = impl - self._auto_close_in_workflows = auto_close_in_workflows + self._emit_spans_in_replay = start_spans_in_replay def add_tracing_processor(self, tracing_processor: TracingProcessor): self._impl.add_tracing_processor(tracing_processor) @@ -92,65 +94,71 @@ def set_processors(self, processors: list[TracingProcessor]): self._impl.set_processors(processors) def on_trace_start(self, trace: Trace) -> None: - if workflow.in_workflow() and workflow.unsafe.is_replaying(): - # In replay mode, don't report - return + if not self._emit_spans_in_replay: + if workflow.in_workflow() and workflow.unsafe.is_replaying_history_events(): + # In replay mode, don't report + return self._impl.on_trace_start(trace) - if self._auto_close_in_workflows and workflow.in_workflow(): - self._impl.on_trace_end(trace) def on_trace_end(self, trace: Trace) -> None: - if workflow.in_workflow() and workflow.unsafe.is_replaying(): - # In replay mode, don't report - return - if self._auto_close_in_workflows and workflow.in_workflow(): - return + if not self._emit_spans_in_replay: + if workflow.in_workflow() and workflow.unsafe.is_replaying_history_events(): + # In replay mode, don't report + return self._impl.on_trace_end(trace) def on_span_start(self, span: Span[Any]) -> None: - if workflow.in_workflow() and workflow.unsafe.is_replaying(): - # In replay mode, don't report - return - + if not self._emit_spans_in_replay: + if workflow.in_workflow() and workflow.unsafe.is_replaying_history_events(): + # In replay mode, don't report + return self._impl.on_span_start(span) - if self._auto_close_in_workflows and workflow.in_workflow(): - self._impl.on_span_end(span) def on_span_end(self, span: Span[Any]) -> None: - if workflow.in_workflow() and workflow.unsafe.is_replaying(): - # In replay mode, don't report - return - if self._auto_close_in_workflows and workflow.in_workflow(): - return + if not self._emit_spans_in_replay: + if workflow.in_workflow() and workflow.unsafe.is_replaying_history_events(): + # In replay mode, don't report + return self._impl.on_span_end(span) - def shutdown(self) -> None: - self._impl.shutdown() + def shutdown(self, timeout: float | None = None) -> None: + self._impl.shutdown(timeout) def force_flush(self) -> None: self._impl.force_flush() def _workflow_uuid() -> str: - random = cast( - RunIdRandom, getattr(workflow.instance(), "__temporal_openai_tracing_random") - ) - return random.uuid4() + if ( + getattr( + temporalio.workflow.instance(), "__temporal_openai_tracing_random", None + ) + is None + ): + setattr( + temporalio.workflow.instance(), + "__temporal_openai_tracing_random", + temporalio.workflow.new_random(), + ) + random = getattr(temporalio.workflow.instance(), "__temporal_openai_tracing_random") + return uuid.UUID( + bytes=random.getrandbits(16 * 8).to_bytes(16, "big"), version=4 + ).hex[:24] class TemporalTraceProvider(DefaultTraceProvider): """A trace provider that integrates with Temporal workflows.""" - def __init__(self, auto_close_in_workflows: bool = False): + def __init__(self, start_spans_in_replay: bool = False): """Initialize the TemporalTraceProvider.""" super().__init__() self._original_provider = cast(DefaultTraceProvider, get_trace_provider()) self._multi_processor = _TemporalTracingProcessor( self._original_provider._multi_processor, - auto_close_in_workflows, + start_spans_in_replay, ) def time_iso(self) -> str: diff --git a/temporalio/contrib/openai_agents/_trace_interceptor.py b/temporalio/contrib/openai_agents/_trace_interceptor.py index 20d489b65..66297e20b 100644 --- a/temporalio/contrib/openai_agents/_trace_interceptor.py +++ b/temporalio/contrib/openai_agents/_trace_interceptor.py @@ -2,25 +2,24 @@ from __future__ import annotations -import random -import uuid +import abc +from collections.abc import Mapping from contextlib import contextmanager -from typing import Any, Mapping, Optional, Protocol, Type +from typing import Any, Protocol from agents import CustomSpanData, custom_span, get_current_span, trace from agents.tracing import ( get_trace_provider, ) from agents.tracing.scope import Scope -from agents.tracing.spans import NoOpSpan, Span +from agents.tracing.spans import Span -import temporalio.activity import temporalio.api.common.v1 import temporalio.client import temporalio.converter import temporalio.worker import temporalio.workflow -from temporalio import activity, workflow +from temporalio import activity HEADER_KEY = "__openai_span" @@ -29,44 +28,22 @@ class _InputWithHeaders(Protocol): headers: Mapping[str, temporalio.api.common.v1.Payload] -def set_header_from_context( - input: _InputWithHeaders, payload_converter: temporalio.converter.PayloadConverter -) -> None: - """Inserts the OpenAI Agents trace/span data in the input header.""" - current = get_current_span() - if current is None or isinstance(current, NoOpSpan): - return - - trace = get_trace_provider().get_current_trace() - input.headers = { - **input.headers, - HEADER_KEY: payload_converter.to_payload( - { - "traceName": trace.name if trace else "Unknown Workflow", - "spanId": current.span_id, - "traceId": current.trace_id, - } - ), - } - - @contextmanager -def context_from_header( +def temporal_span( + add_temporal_spans: bool, span_name: str, - input: _InputWithHeaders, - payload_converter: temporalio.converter.PayloadConverter, ): - """Extracts and initializes trace information the input header.""" - payload = input.headers.get(HEADER_KEY) - span_info = payload_converter.from_payload(payload) if payload else None - if span_info is None: - yield - else: - workflow_type = ( - activity.info().workflow_type - if activity.in_activity() - else workflow.info().workflow_type - ) + """Create a temporal span context manager. + + Args: + add_temporal_spans: Whether to add temporal-specific span data. + span_name: The name of the span to create. + + Yields: + A span context with temporal metadata if enabled. + """ + if add_temporal_spans: + """Extracts and initializes trace information the input header.""" data = ( { "activityId": activity.info().activity_id, @@ -75,43 +52,19 @@ def context_from_header( if activity.in_activity() else None ) - current_trace = get_trace_provider().get_current_trace() - if current_trace is None: - metadata = { - "temporal:workflowId": activity.info().workflow_id - if activity.in_activity() - else workflow.info().workflow_id, - "temporal:runId": activity.info().workflow_run_id - if activity.in_activity() - else workflow.info().run_id, - "temporal:workflowType": workflow_type, - } - current_trace = trace( - span_info["traceName"], - trace_id=span_info["traceId"], - metadata=metadata, - ) - Scope.set_current_trace(current_trace) current_span = get_trace_provider().get_current_span() - if current_span is None: - current_span = get_trace_provider().create_span( - span_data=CustomSpanData(name="", data={}), span_id=span_info["spanId"] - ) - Scope.set_current_span(current_span) with custom_span(name=span_name, parent=current_span, data=data): yield + else: + yield -class OpenAIAgentsTracingInterceptor( +class OpenAIAgentsContextPropagationInterceptor( temporalio.client.Interceptor, temporalio.worker.Interceptor ): """Interceptor that propagates OpenAI agent tracing context through Temporal workflows and activities. - .. warning:: - This API is experimental and may change in future versions. - Use with caution in production environments. - This interceptor enables tracing of OpenAI agent operations across Temporal workflows and activities. It propagates trace context through workflow and activity boundaries, allowing for end-to-end tracing of agent operations. @@ -130,14 +83,22 @@ class OpenAIAgentsTracingInterceptor( def __init__( self, payload_converter: temporalio.converter.PayloadConverter = temporalio.converter.default().payload_converter, + add_temporal_spans: bool = True, + start_traces: bool = False, ) -> None: """Initialize the interceptor with a payload converter. Args: payload_converter: The payload converter to use for serializing/deserializing trace context. Defaults to the default Temporal payload converter. + add_temporal_spans: Whether to add temporal-specific spans to traces. + start_traces: Whether to start new traces if none exist. This will cause duplication if the underlying + trace provider actually process start events. Primarily designed for use with Open Telemetry integration. """ + super().__init__() self._payload_converter = payload_converter + self._start_traces = start_traces + self._add_temporal_spans = add_temporal_spans def intercept_client( self, next: temporalio.client.OutboundInterceptor @@ -150,9 +111,7 @@ def intercept_client( Returns: An interceptor that propagates trace context for client operations. """ - return _ContextPropagationClientOutboundInterceptor( - next, self._payload_converter - ) + return _ContextPropagationClientOutboundInterceptor(next, self) def intercept_activity( self, next: temporalio.worker.ActivityInboundInterceptor @@ -165,11 +124,11 @@ def intercept_activity( Returns: An interceptor that propagates trace context for activity operations. """ - return _ContextPropagationActivityInboundInterceptor(next) + return _ContextPropagationActivityInboundInterceptor(next, self) def workflow_interceptor_class( self, input: temporalio.worker.WorkflowInterceptorClassInput - ) -> Type[_ContextPropagationWorkflowInboundInterceptor]: + ) -> type[_ContextPropagationWorkflowInboundInterceptor]: """Returns the workflow interceptor class to propagate trace context. Args: @@ -178,7 +137,110 @@ def workflow_interceptor_class( Returns: The class of the workflow interceptor that propagates trace context. """ - return _ContextPropagationWorkflowInboundInterceptor + _root = self + + class ModifiedInterceptor(_ContextPropagationWorkflowInboundInterceptor): + def root(self): + return _root + + return ModifiedInterceptor + + def set_header_from_context(self, input: _InputWithHeaders) -> None: + """Inserts the OpenAI Agents trace/span data in the input header.""" + input.headers = { + **input.headers, + HEADER_KEY: temporalio.converter.PayloadConverter.default.to_payload( + self.header_contents() + ), + } + + def header_contents(self) -> dict[str, Any]: + """Gets the OpenAI Agents trace/span data for the input header.""" + current = get_current_span() + trace = get_trace_provider().get_current_trace() + return { + "traceName": trace.name if trace else "Unknown Workflow", + "spanId": current.span_id if current else None, + "traceId": trace.trace_id if trace else None, + } + + def get_header_contents(self, input: _InputWithHeaders) -> dict[str, Any] | None: + """Extract trace context information from input headers. + + Args: + input: Input with headers containing trace information. + + Returns: + Dictionary containing trace context or None if no headers present. + """ + payload = input.headers.get(HEADER_KEY) + return self._payload_converter.from_payload(payload) if payload else None + + def trace_context_from_header_contents(self, span_info: dict[str, Any]): + """Initialize trace context from header contents. + + Args: + span_info: Dictionary containing trace information from headers. + """ + current_trace = get_trace_provider().get_current_trace() + if current_trace is None and span_info["traceId"] is not None: + current_trace = trace( + span_info["traceName"], + trace_id=span_info["traceId"], + ) + + if self._start_traces: + current_trace.start(mark_as_current=True) + else: + Scope.set_current_trace(current_trace) + + def span_context_from_header_contents(self, span_info: dict[str, Any]): + """Initialize span context from header contents. + + Args: + span_info: Dictionary containing span information from headers. + """ + current_span = get_trace_provider().get_current_span() + if current_span is None and span_info["spanId"] is not None: + current_span = get_trace_provider().create_span( + span_data=CustomSpanData(name="", data={}), span_id=span_info["spanId"] + ) + if self._start_traces: + current_span.start(mark_as_current=True) + else: + Scope.set_current_span(current_span) + + def context_from_header( + self, + input: _InputWithHeaders, + ): + """Extracts and initializes trace information the input header.""" + span_info = self.get_header_contents(input) + if span_info is None: + return + + self.trace_context_from_header_contents(span_info) + self.span_context_from_header_contents(span_info) + + @contextmanager + def maybe_span(self, span_name: str, data: dict[str, Any] | None): + """Context manager that conditionally creates a span. + + Args: + span_name: Name for the span. + data: Optional data to attach to the span. + + Yields: + Context with optional span tracking. + """ + if ( + self._add_temporal_spans + and get_trace_provider().get_current_trace() is not None + ): + with custom_span(name=span_name, data=data): + yield + else: + yield class _ContextPropagationClientOutboundInterceptor( @@ -187,228 +249,164 @@ class _ContextPropagationClientOutboundInterceptor( def __init__( self, next: temporalio.client.OutboundInterceptor, - payload_converter: temporalio.converter.PayloadConverter, + root: OpenAIAgentsContextPropagationInterceptor, ) -> None: super().__init__(next) - self._payload_converter = payload_converter + self._root = root async def start_workflow( self, input: temporalio.client.StartWorkflowInput ) -> temporalio.client.WorkflowHandle[Any, Any]: - metadata = { - "temporal:workflowType": input.workflow, - **({"temporal:workflowId": input.id} if input.id else {}), - } data = {"workflowId": input.id} if input.id else None span_name = "temporal:startWorkflow" - if get_trace_provider().get_current_trace() is None: - with trace( - span_name + ":" + input.workflow, metadata=metadata, group_id=input.id - ): - with custom_span(name=span_name + ":" + input.workflow, data=data): - set_header_from_context(input, self._payload_converter) - return await super().start_workflow(input) - else: - with custom_span(name=span_name, data=data): - set_header_from_context(input, self._payload_converter) - return await super().start_workflow(input) + with self._root.maybe_span( + span_name + ":" + input.workflow, + data=data, + ): + self._root.set_header_from_context(input) + return await super().start_workflow(input) async def query_workflow(self, input: temporalio.client.QueryWorkflowInput) -> Any: - metadata = { - "temporal:queryWorkflow": input.query, - **({"temporal:workflowId": input.id} if input.id else {}), - } data = {"workflowId": input.id, "query": input.query} span_name = "temporal:queryWorkflow" - if get_trace_provider().get_current_trace() is None: - with trace(span_name, metadata=metadata, group_id=input.id): - with custom_span(name=span_name, data=data): - set_header_from_context(input, self._payload_converter) - return await super().query_workflow(input) - else: - with custom_span(name=span_name, data=data): - set_header_from_context(input, self._payload_converter) - return await super().query_workflow(input) + with self._root.maybe_span( + span_name, + data=data, + ): + self._root.set_header_from_context(input) + return await super().query_workflow(input) async def signal_workflow( self, input: temporalio.client.SignalWorkflowInput ) -> None: - metadata = { - "temporal:signalWorkflow": input.signal, - **({"temporal:workflowId": input.id} if input.id else {}), - } data = {"workflowId": input.id, "signal": input.signal} span_name = "temporal:signalWorkflow" - if get_trace_provider().get_current_trace() is None: - with trace(span_name, metadata=metadata, group_id=input.id): - with custom_span(name=span_name, data=data): - set_header_from_context(input, self._payload_converter) - await super().signal_workflow(input) - else: - with custom_span(name=span_name, data=data): - set_header_from_context(input, self._payload_converter) - await super().signal_workflow(input) + with self._root.maybe_span( + span_name, + data=data, + ): + self._root.set_header_from_context(input) + await super().signal_workflow(input) async def start_workflow_update( self, input: temporalio.client.StartWorkflowUpdateInput ) -> temporalio.client.WorkflowUpdateHandle[Any]: - metadata = { - "temporal:updateWorkflow": input.update, - **({"temporal:workflowId": input.id} if input.id else {}), - } data = { **({"workflowId": input.id} if input.id else {}), "update": input.update, } span_name = "temporal:updateWorkflow" - if get_trace_provider().get_current_trace() is None: - with trace(span_name, metadata=metadata, group_id=input.id): - with custom_span(name=span_name, data=data): - set_header_from_context(input, self._payload_converter) - return await self.next.start_workflow_update(input) - else: - with custom_span(name=span_name, data=data): - set_header_from_context(input, self._payload_converter) - return await self.next.start_workflow_update(input) + with self._root.maybe_span( + span_name, + data=data, + ): + self._root.set_header_from_context(input) + return await self.next.start_workflow_update(input) class _ContextPropagationActivityInboundInterceptor( temporalio.worker.ActivityInboundInterceptor ): + def __init__( + self, + next: temporalio.worker.ActivityInboundInterceptor, + root: OpenAIAgentsContextPropagationInterceptor, + ) -> None: + super().__init__(next) + self._root = root + async def execute_activity( self, input: temporalio.worker.ExecuteActivityInput ) -> Any: - with context_from_header( - "temporal:executeActivity", input, temporalio.activity.payload_converter() - ): + self._root.context_from_header(input) + with temporal_span(self._root._add_temporal_spans, "temporal:executeActivity"): return await self.next.execute_activity(input) -class RunIdRandom: - """Random uuid generator seeded by the run id of the workflow. - Doesn't currently support replay over reset correctly. - """ - - def __init__(self): - """Create a new random UUID generator.""" - self._random = random.Random("OpenAIPlugin" + workflow.info().run_id) - - def uuid4(self) -> str: - """Generate a random UUID.""" - return uuid.UUID( - bytes=random.getrandbits(16 * 8).to_bytes(16, "big"), version=4 - ).hex[:24] - - -def _ensure_tracing_random() -> None: - """We use a custom uuid generator for spans to ensure that changes to user code workflow.random usage - do not affect tracing and vice versa. - """ - instance = workflow.instance() - if not hasattr(instance, "__temporal_openai_tracing_random"): - setattr( - workflow.instance(), - "__temporal_openai_tracing_random", - RunIdRandom(), - ) - - class _ContextPropagationWorkflowInboundInterceptor( - temporalio.worker.WorkflowInboundInterceptor + temporalio.worker.WorkflowInboundInterceptor, abc.ABC ): + @abc.abstractmethod + def root(self): + raise NotImplementedError + def init(self, outbound: temporalio.worker.WorkflowOutboundInterceptor) -> None: - self.next.init(_ContextPropagationWorkflowOutboundInterceptor(outbound)) + _root = self.root() + + class ModifiedInterceptor(_ContextPropagationWorkflowOutboundInterceptor): + def root(self): + return _root + + self.next.init(ModifiedInterceptor(outbound)) async def execute_workflow( self, input: temporalio.worker.ExecuteWorkflowInput ) -> Any: - _ensure_tracing_random() - with context_from_header( - "temporal:executeWorkflow", input, temporalio.workflow.payload_converter() - ): + self.root().context_from_header(input) + with temporal_span(self.root()._add_temporal_spans, "temporal:executeWorkflow"): return await self.next.execute_workflow(input) async def handle_signal(self, input: temporalio.worker.HandleSignalInput) -> None: - _ensure_tracing_random() - with context_from_header( - "temporal:handleSignal", input, temporalio.workflow.payload_converter() - ): + self.root().context_from_header(input) + with temporal_span(self.root()._add_temporal_spans, "temporal:handleSignal"): return await self.next.handle_signal(input) async def handle_query(self, input: temporalio.worker.HandleQueryInput) -> Any: - _ensure_tracing_random() - with context_from_header( - "temporal:handleQuery", input, temporalio.workflow.payload_converter() - ): + with temporal_span(self.root()._add_temporal_spans, "temporal:handleQuery"): return await self.next.handle_query(input) def handle_update_validator( self, input: temporalio.worker.HandleUpdateInput ) -> None: - with context_from_header( - "temporal:handleUpdateValidator", - input, - temporalio.workflow.payload_converter(), - ): - self.next.handle_update_validator(input) + self.root().context_from_header(input) + self.next.handle_update_validator(input) async def handle_update_handler( self, input: temporalio.worker.HandleUpdateInput ) -> Any: - _ensure_tracing_random() - with context_from_header( - "temporal:handleUpdateHandler", - input, - temporalio.workflow.payload_converter(), - ): - return await self.next.handle_update_handler(input) + self.root().context_from_header(input) + return await self.next.handle_update_handler(input) class _ContextPropagationWorkflowOutboundInterceptor( - temporalio.worker.WorkflowOutboundInterceptor + temporalio.worker.WorkflowOutboundInterceptor, abc.ABC ): + @abc.abstractmethod + def root(self): + raise NotImplementedError + async def signal_child_workflow( self, input: temporalio.worker.SignalChildWorkflowInput ) -> None: - trace = get_trace_provider().get_current_trace() - if trace: - with custom_span( - name="temporal:signalChildWorkflow", - data={"workflowId": input.child_workflow_id}, - ): - set_header_from_context(input, temporalio.workflow.payload_converter()) - await self.next.signal_child_workflow(input) - else: - set_header_from_context(input, temporalio.workflow.payload_converter()) + with self.root().maybe_span( + "temporal:signalChildWorkflow", + data={"workflowId": input.child_workflow_id}, + ): + self.root().set_header_from_context(input) await self.next.signal_child_workflow(input) async def signal_external_workflow( self, input: temporalio.worker.SignalExternalWorkflowInput ) -> None: - trace = get_trace_provider().get_current_trace() - if trace: - with custom_span( - name="temporal:signalExternalWorkflow", - data={"workflowId": input.workflow_id}, - ): - set_header_from_context(input, temporalio.workflow.payload_converter()) - await self.next.signal_external_workflow(input) - else: - set_header_from_context(input, temporalio.workflow.payload_converter()) + with self.root().maybe_span( + "temporal:signalExternalWorkflow", + data={"workflowId": input.workflow_id}, + ): + self.root().set_header_from_context(input) await self.next.signal_external_workflow(input) def start_activity( self, input: temporalio.worker.StartActivityInput ) -> temporalio.workflow.ActivityHandle: trace = get_trace_provider().get_current_trace() - span: Optional[Span] = None - if trace: + span: Span | None = None + if trace and self.root()._add_temporal_spans: span = custom_span( name="temporal:startActivity", data={"activity": input.activity} ) span.start(mark_as_current=True) - set_header_from_context(input, temporalio.workflow.payload_converter()) + self.root().set_header_from_context(input) handle = self.next.start_activity(input) if span: handle.add_done_callback(lambda _: span.finish()) # type: ignore @@ -418,13 +416,13 @@ async def start_child_workflow( self, input: temporalio.worker.StartChildWorkflowInput ) -> temporalio.workflow.ChildWorkflowHandle: trace = get_trace_provider().get_current_trace() - span: Optional[Span] = None - if trace: + span: Span | None = None + if trace and self.root()._add_temporal_spans: span = custom_span( name="temporal:startChildWorkflow", data={"workflow": input.workflow} ) span.start(mark_as_current=True) - set_header_from_context(input, temporalio.workflow.payload_converter()) + self.root().set_header_from_context(input) handle = await self.next.start_child_workflow(input) if span: handle.add_done_callback(lambda _: span.finish()) # type: ignore @@ -434,13 +432,13 @@ def start_local_activity( self, input: temporalio.worker.StartLocalActivityInput ) -> temporalio.workflow.ActivityHandle: trace = get_trace_provider().get_current_trace() - span: Optional[Span] = None - if trace: + span: Span | None = None + if trace and self.root()._add_temporal_spans: span = custom_span( name="temporal:startLocalActivity", data={"activity": input.activity} ) span.start(mark_as_current=True) - set_header_from_context(input, temporalio.workflow.payload_converter()) + self.root().set_header_from_context(input) handle = self.next.start_local_activity(input) if span: handle.add_done_callback(lambda _: span.finish()) # type: ignore diff --git a/temporalio/contrib/openai_agents/sandbox/__init__.py b/temporalio/contrib/openai_agents/sandbox/__init__.py new file mode 100644 index 000000000..632b264cd --- /dev/null +++ b/temporalio/contrib/openai_agents/sandbox/__init__.py @@ -0,0 +1 @@ +"""Sandbox support for Temporal OpenAI Agents plugin.""" diff --git a/temporalio/contrib/openai_agents/sandbox/_sandbox_client_provider.py b/temporalio/contrib/openai_agents/sandbox/_sandbox_client_provider.py new file mode 100644 index 000000000..4aa6fd38e --- /dev/null +++ b/temporalio/contrib/openai_agents/sandbox/_sandbox_client_provider.py @@ -0,0 +1,285 @@ +"""Public-facing provider that pairs a name with a real sandbox client.""" + +from __future__ import annotations + +import io +from collections.abc import Callable, Iterator, Sequence +from contextlib import contextmanager +from pathlib import Path +from typing import Any + +from agents.sandbox.errors import SandboxError +from agents.sandbox.session.sandbox_client import BaseSandboxClient +from agents.sandbox.session.sandbox_session import SandboxSession + +from temporalio import activity +from temporalio.contrib.openai_agents.sandbox._temporal_activity_models import ( + CreateSessionArgs, + ExecArgs, + HydrateWorkspaceArgs, + PersistWorkspaceArgs, + PersistWorkspaceResult, + PtyExecStartArgs, + PtyExecUpdateResult, + PtyWriteStdinArgs, + ReadArgs, + ReadResult, + ResumeSessionArgs, + RunningArgs, + RunningResult, + SessionResult, + StartArgs, + StopArgs, + WriteArgs, + _HasState, +) +from temporalio.contrib.openai_agents.sandbox._temporal_activity_models import ( + ExecResult as ExecResultModel, +) +from temporalio.exceptions import ApplicationError + + +@contextmanager +def _translate_sandbox_errors() -> Iterator[None]: + # Temporal retries every activity exception by default, so only a SandboxError + # the library has classified as terminal (retryable is False) is turned into a + # non-retryable ApplicationError. + try: + yield + except SandboxError as e: + if e.retryable is False: + raise ApplicationError( + str(e), type=str(e.error_code), non_retryable=True + ) from e + raise + + +class SandboxClientProvider: + """A named sandbox client provider for Temporal workflows. + + .. warning:: + This class is experimental and may change in future versions. + Use with caution in production environments. + + Wraps a ``BaseSandboxClient`` with a unique name so that multiple + sandbox backends can be registered on a single Temporal worker. Each + provider gets its own set of Temporal activities whose names are prefixed + with the provider name, allowing them to coexist on the same task queue. + + On the **worker side**, pass one or more providers to the plugin:: + + plugin = OpenAIAgentsPlugin( + sandbox_clients=[ + SandboxClientProvider("daytona", DaytonaSandboxClient()), + SandboxClientProvider("local", UnixLocalSandboxClient()), + ], + ) + + On the **workflow side**, reference a provider by name via + :func:`temporalio.contrib.openai_agents.workflow.temporal_sandbox_client`:: + + run_config = RunConfig( + sandbox=SandboxRunConfig( + client=temporal_sandbox_client("daytona"), + ... + ), + ) + + Args: + name: A unique name for this sandbox backend (e.g. ``"daytona"``, + ``"local"``). Must match the name used on the workflow side. + client: The real ``BaseSandboxClient`` that performs sandbox + lifecycle and I/O operations on the worker. + """ + + def __init__(self, name: str, client: BaseSandboxClient[Any]) -> None: + """Initialize the provider.""" + self._name = name + self._client = client + self._sessions: dict[str, SandboxSession] = {} + + @property + def name(self) -> str: + """The provider name used as an activity-name prefix.""" + return self._name + + async def _session(self, args: _HasState) -> SandboxSession: + key = str(args.state.session_id) + if key not in self._sessions: + self._sessions[key] = await self._client.resume(args.state) + return self._sessions[key] + + def _get_activities(self) -> Sequence[Callable[..., Any]]: + """Return all activity callables for registration with a Temporal Worker.""" + prefix = self._name + + # -- Client-level operations (lifecycle) -- + + @activity.defn(name=f"{prefix}-sandbox_client_create") + async def create_session(args: CreateSessionArgs) -> SessionResult: + with _translate_sandbox_errors(): + session = await self._client.create( + snapshot=args.snapshot_spec, + manifest=args.manifest, + options=args.client_options, + ) + self._sessions[str(session.state.session_id)] = session + return SessionResult( + state=session.state, supports_pty=session.supports_pty() + ) + + @activity.defn(name=f"{prefix}-sandbox_client_resume") + async def resume_session(args: ResumeSessionArgs) -> SessionResult: + with _translate_sandbox_errors(): + session = await self._client.resume(args.state) + self._sessions[str(session.state.session_id)] = session + return SessionResult( + state=session.state, supports_pty=session.supports_pty() + ) + + @activity.defn(name=f"{prefix}-sandbox_client_delete") + async def delete_session(args: StopArgs) -> None: + with _translate_sandbox_errors(): + session = await self._session(args) + await self._client.delete(session) + return None + + # -- Session-level operations (I/O and lifecycle) -- + + @activity.defn(name=f"{prefix}-sandbox_session_exec") + async def exec_(args: ExecArgs) -> ExecResultModel: + with _translate_sandbox_errors(): + session = await self._session(args) + result = await session.exec( + *args.command, + timeout=args.timeout, + shell=args.shell, + user=args.user, + ) + return ExecResultModel( + stdout=result.stdout, + stderr=result.stderr, + exit_code=result.exit_code, + ) + + @activity.defn(name=f"{prefix}-sandbox_session_read") + async def read(args: ReadArgs) -> ReadResult: + with _translate_sandbox_errors(): + session = await self._session(args) + handle = await session.read(Path(args.path)) + return ReadResult(data=handle.read()) + + @activity.defn(name=f"{prefix}-sandbox_session_write") + async def write(args: WriteArgs) -> None: + with _translate_sandbox_errors(): + session = await self._session(args) + await session.write(Path(args.path), io.BytesIO(args.data)) + return None + + @activity.defn(name=f"{prefix}-sandbox_session_running") + async def running(args: RunningArgs) -> RunningResult: + with _translate_sandbox_errors(): + session = await self._session(args) + return RunningResult(is_running=await session.running()) + + @activity.defn(name=f"{prefix}-sandbox_session_persist_workspace") + async def persist_workspace( + args: PersistWorkspaceArgs, + ) -> PersistWorkspaceResult: + with _translate_sandbox_errors(): + session = await self._session(args) + stream = await session.persist_workspace() + return PersistWorkspaceResult(data=stream.read()) + + @activity.defn(name=f"{prefix}-sandbox_session_hydrate_workspace") + async def hydrate_workspace(args: HydrateWorkspaceArgs) -> None: + with _translate_sandbox_errors(): + session = await self._session(args) + await session.hydrate_workspace(io.BytesIO(args.data)) + return None + + @activity.defn(name=f"{prefix}-sandbox_session_pty_exec_start") + async def pty_exec_start(args: PtyExecStartArgs) -> PtyExecUpdateResult: + with _translate_sandbox_errors(): + session = await self._session(args) + update = await session.pty_exec_start( + *args.command, + timeout=args.timeout, + shell=args.shell, + user=args.user, + tty=args.tty, + yield_time_s=args.yield_time_s, + max_output_tokens=args.max_output_tokens, + ) + return PtyExecUpdateResult( + process_id=update.process_id, + output=update.output, + exit_code=update.exit_code, + original_token_count=update.original_token_count, + ) + + @activity.defn(name=f"{prefix}-sandbox_session_pty_write_stdin") + async def pty_write_stdin(args: PtyWriteStdinArgs) -> PtyExecUpdateResult: + with _translate_sandbox_errors(): + session = await self._session(args) + update = await session.pty_write_stdin( + session_id=args.session_id, + chars=args.chars, + yield_time_s=args.yield_time_s, + max_output_tokens=args.max_output_tokens, + ) + return PtyExecUpdateResult( + process_id=update.process_id, + output=update.output, + exit_code=update.exit_code, + original_token_count=update.original_token_count, + ) + + @activity.defn(name=f"{prefix}-sandbox_session_start") + async def start(args: StartArgs) -> None: + with _translate_sandbox_errors(): + session = await self._session(args) + await session.start() + return None + + @activity.defn(name=f"{prefix}-sandbox_session_stop") + async def session_stop(args: StopArgs) -> None: + with _translate_sandbox_errors(): + session = await self._session(args) + await session.stop() + return None + + @activity.defn(name=f"{prefix}-sandbox_session_shutdown") + async def session_shutdown(args: StopArgs) -> None: + key = str(args.state.session_id) + session = self._sessions.get(key) + if session is None: + return None + try: + with _translate_sandbox_errors(): + await session.shutdown() + except ApplicationError: + # Terminal failure: the session is dead, so evict it before + # re-raising. A retryable error instead propagates with the + # entry kept so the activity's retry can still shut it down. + del self._sessions[key] + raise + del self._sessions[key] + return None + + return [ + create_session, + resume_session, + delete_session, + exec_, + read, + write, + running, + persist_workspace, + hydrate_workspace, + pty_exec_start, + pty_write_stdin, + start, + session_stop, + session_shutdown, + ] diff --git a/temporalio/contrib/openai_agents/sandbox/_temporal_activity_models.py b/temporalio/contrib/openai_agents/sandbox/_temporal_activity_models.py new file mode 100644 index 000000000..5cdc0ccba --- /dev/null +++ b/temporalio/contrib/openai_agents/sandbox/_temporal_activity_models.py @@ -0,0 +1,218 @@ +"""Pydantic models for Temporal sandbox activity arguments and results. + +Using ``pydantic_data_converter`` on the Temporal client means these models are +serialized/deserialized automatically. Each activity receives a single typed +model instance rather than a positional arg list. +""" + +from __future__ import annotations + +from base64 import b64decode, b64encode +from typing import Annotated, Any, cast + +from agents.sandbox import Manifest +from agents.sandbox.session.sandbox_client import BaseSandboxClientOptions +from agents.sandbox.session.sandbox_session_state import SandboxSessionState +from agents.sandbox.snapshot import SnapshotBase, SnapshotSpecUnion +from agents.sandbox.types import User +from pydantic import ( + BaseModel, + BeforeValidator, + PlainSerializer, + SerializeAsAny, + field_validator, +) + + +def _coerce_bytes(v: Any) -> bytes: + if isinstance(v, bytes): + return v + if isinstance(v, str): + return b64decode(v) + raise ValueError(f"Expected bytes or base64 string, got {type(v)}") + + +# Bytes type that is stored as raw bytes in Python but base64-encoded in JSON, +# ensuring lossless serialization of arbitrary binary data through pydantic. +JsonSafeBytes = Annotated[ + bytes, + BeforeValidator(_coerce_bytes), + PlainSerializer(lambda v: b64encode(v).decode("ascii"), return_type=str), +] + +# --------------------------------------------------------------------------- +# Shared base for all argument models that carry a session state field. +# --------------------------------------------------------------------------- + + +class _HasState(BaseModel): + state: SerializeAsAny[SandboxSessionState] + + @field_validator("state", mode="before") + @classmethod + def _coerce_state(cls, value: object) -> SandboxSessionState: + return SandboxSessionState.parse(value) + + +# --------------------------------------------------------------------------- +# Argument models (workflow -> activity) +# --------------------------------------------------------------------------- + + +class ExecArgs(_HasState): + """Arguments for exec activity.""" + + command: list[str] + timeout: float | None = None + shell: bool | list[str] = True + user: str | User | None = None + + +class ReadArgs(_HasState): + """Arguments for read activity.""" + + path: str + + +class WriteArgs(_HasState): + """Arguments for write activity.""" + + path: str + data: JsonSafeBytes + + +class RunningArgs(_HasState): + """Arguments for running check activity.""" + + pass + + +class PersistWorkspaceArgs(_HasState): + """Arguments for persist workspace activity.""" + + pass + + +class HydrateWorkspaceArgs(_HasState): + """Arguments for hydrate workspace activity.""" + + data: JsonSafeBytes + + +class PtyExecStartArgs(_HasState): + """Arguments for PTY exec start activity.""" + + command: list[str] + timeout: float | None = None + shell: bool | list[str] = True + user: str | User | None = None + tty: bool = False + yield_time_s: float | None = None + max_output_tokens: int | None = None + + +class PtyWriteStdinArgs(_HasState): + """Arguments for PTY write stdin activity.""" + + session_id: int + chars: str + yield_time_s: float | None = None + max_output_tokens: int | None = None + + +class StartArgs(_HasState): + """Arguments for start activity.""" + + pass + + +class StopArgs(_HasState): + """Arguments for stop activity.""" + + pass + + +# --------------------------------------------------------------------------- +# Result models (activity -> workflow) +# --------------------------------------------------------------------------- + + +class ExecResult(BaseModel): + """Result of an exec activity.""" + + stdout: JsonSafeBytes + stderr: JsonSafeBytes + exit_code: int + + +class PtyExecUpdateResult(BaseModel): + """Result of a PTY exec activity.""" + + process_id: int | None + output: JsonSafeBytes + exit_code: int | None + original_token_count: int | None + + +class ReadResult(BaseModel): + """Result of a read activity.""" + + data: JsonSafeBytes + + +class RunningResult(BaseModel): + """Result of a running check activity.""" + + is_running: bool + + +class PersistWorkspaceResult(BaseModel): + """Result of a persist workspace activity.""" + + data: JsonSafeBytes + + +# --------------------------------------------------------------------------- +# Session lifecycle models (create / resume) +# --------------------------------------------------------------------------- + + +class CreateSessionArgs(BaseModel): + """Arguments for create session activity.""" + + snapshot_spec: SnapshotSpecUnion | SerializeAsAny[SnapshotBase] | None = None + manifest: Manifest | None = None + client_options: SerializeAsAny[BaseSandboxClientOptions] | None = None + + @field_validator("snapshot_spec", mode="before") + @classmethod + def _coerce_snapshot_spec( + cls, value: object + ) -> SnapshotSpecUnion | SnapshotBase | None: + if value is None or isinstance(value, SnapshotBase): + return value + # SnapshotBase subclasses always carry an `id` field; + # SnapshotSpec subclasses do not. Use that to distinguish + # serialized SnapshotBase dicts from SnapshotSpecUnion dicts. + if isinstance(value, dict) and "id" in value: + return SnapshotBase.parse(value) + return cast(SnapshotSpecUnion | None, value) + + @field_validator("client_options", mode="before") + @classmethod + def _coerce_client_options(cls, value: object) -> BaseSandboxClientOptions | None: + if value is None: + return None + return BaseSandboxClientOptions.parse(value) + + +class ResumeSessionArgs(_HasState): + """Arguments for resume session activity.""" + + pass + + +class SessionResult(_HasState): + """Result of create/resume -- session state + capabilities.""" + + supports_pty: bool diff --git a/temporalio/contrib/openai_agents/sandbox/_temporal_sandbox_client.py b/temporalio/contrib/openai_agents/sandbox/_temporal_sandbox_client.py new file mode 100644 index 000000000..891c65f4b --- /dev/null +++ b/temporalio/contrib/openai_agents/sandbox/_temporal_sandbox_client.py @@ -0,0 +1,124 @@ +"""Temporal-aware sandbox client that dispatches lifecycle operations as activities.""" + +from __future__ import annotations + +from datetime import timedelta +from typing import Any + +from agents.sandbox import Manifest +from agents.sandbox.session.sandbox_client import ( + BaseSandboxClient, + BaseSandboxClientOptions, +) +from agents.sandbox.session.sandbox_session import SandboxSession +from agents.sandbox.session.sandbox_session_state import SandboxSessionState +from agents.sandbox.snapshot import SnapshotBase, SnapshotSpec, SnapshotSpecUnion +from pydantic.type_adapter import TypeAdapter + +from temporalio import workflow +from temporalio.contrib.openai_agents.sandbox._temporal_activity_models import ( + CreateSessionArgs, + ResumeSessionArgs, + SessionResult, + StopArgs, +) +from temporalio.contrib.openai_agents.sandbox._temporal_sandbox_session import ( + TemporalSandboxSession, +) +from temporalio.workflow import ActivityConfig + + +class TemporalSandboxClient(BaseSandboxClient[BaseSandboxClientOptions]): + """Stateless client that dispatches all lifecycle operations as Temporal activities. + + No inner client is needed -- session creation, resumption, and deletion are + all handled by activities whose names are prefixed with the provider + ``name`` (e.g. ``"daytona-sandbox_create_session"``). The real + ``BaseSandboxClient`` lives inside :class:`SandboxClientProvider` on the worker. + + Users should never need to instantiate this directly -- use + :func:`temporalio.contrib.openai_agents.workflow.temporal_sandbox_client` + instead. + + Args: + name: The name of the :class:`SandboxClientProvider` registered on the + worker. Used as an activity-name prefix so that the correct + sandbox backend is targeted. + config: Optional activity configuration for controlling timeouts, + retries, etc. Defaults to a 5-minute ``start_to_close_timeout``. + """ + + def __init__( + self, + name: str, + config: ActivityConfig | None = None, + ) -> None: + """Initialize the client.""" + self._name = name + self._config: ActivityConfig = config or ActivityConfig( + start_to_close_timeout=timedelta(minutes=5), + ) + self.backend_id = name + + async def create( + self, + *, + snapshot: SnapshotSpec | SnapshotBase | None = None, + manifest: Manifest | None = None, + options: BaseSandboxClientOptions, + ) -> SandboxSession: + """Create a new sandbox session via activity.""" + result: SessionResult = await workflow.execute_activity( + f"{self._name}-sandbox_client_create", + arg=CreateSessionArgs( + snapshot_spec=TypeAdapter(SnapshotSpecUnion).validate_python(snapshot) + if isinstance(snapshot, SnapshotSpec) + else snapshot, + manifest=manifest, + client_options=options, + ), + result_type=SessionResult, + **self._config, + ) + return self._wrap_session( + TemporalSandboxSession( + name=self._name, + config=self._config, + state=result.state, + supports_pty_flag=result.supports_pty, + ), + # Real instrumentation runs in the activity in the real client session. + instrumentation=None, + ) + + async def resume(self, state: SandboxSessionState) -> SandboxSession: + """Resume an existing sandbox session via activity.""" + result: SessionResult = await workflow.execute_activity( + f"{self._name}-sandbox_client_resume", + arg=ResumeSessionArgs(state=state), + result_type=SessionResult, + **self._config, + ) + return self._wrap_session( + TemporalSandboxSession( + name=self._name, + config=self._config, + state=result.state, + supports_pty_flag=result.supports_pty, + ), + # Real instrumentation runs in the activity in the real client session. + instrumentation=None, + ) + + async def delete(self, session: TemporalSandboxSession) -> TemporalSandboxSession: # type: ignore[override] + """Delete a sandbox session via activity.""" + await workflow.execute_activity( + f"{self._name}-sandbox_client_delete", + arg=StopArgs(state=session.state), + **self._config, + ) + return session + + def deserialize_session_state(self, payload: dict[str, Any]) -> SandboxSessionState: + """Deserialize a session state from a dict.""" + return SandboxSessionState.parse(payload) diff --git a/temporalio/contrib/openai_agents/sandbox/_temporal_sandbox_session.py b/temporalio/contrib/openai_agents/sandbox/_temporal_sandbox_session.py new file mode 100644 index 000000000..cccb72936 --- /dev/null +++ b/temporalio/contrib/openai_agents/sandbox/_temporal_sandbox_session.py @@ -0,0 +1,239 @@ +"""Temporal-aware sandbox session that routes all I/O through Temporal activities.""" + +from __future__ import annotations + +import io +from pathlib import Path + +from agents.sandbox.session.base_sandbox_session import BaseSandboxSession +from agents.sandbox.session.pty_types import PtyExecUpdate +from agents.sandbox.session.sandbox_session_state import SandboxSessionState +from agents.sandbox.types import ExecResult, User + +from temporalio import workflow +from temporalio.contrib.openai_agents.sandbox._temporal_activity_models import ( + ExecArgs, + HydrateWorkspaceArgs, + PersistWorkspaceArgs, + PersistWorkspaceResult, + PtyExecStartArgs, + PtyExecUpdateResult, + PtyWriteStdinArgs, + ReadArgs, + ReadResult, + RunningArgs, + RunningResult, + StartArgs, + StopArgs, + WriteArgs, +) +from temporalio.contrib.openai_agents.sandbox._temporal_activity_models import ( + ExecResult as ExecResultModel, +) +from temporalio.workflow import ActivityConfig + + +class TemporalSandboxSession(BaseSandboxSession): + """A BaseSandboxSession that routes all I/O through Temporal activities. + + This class is fully stateless with respect to the physical sandbox -- it + holds only the serializable ``SandboxSessionState`` and a ``supports_pty`` + flag (both provided by the worker-side ``SessionResult``). + + Activity names are prefixed with the provider ``name`` so that dispatches + reach the correct sandbox backend's activities on the worker. + + Each activity receives a single Pydantic model instance. Because the Temporal + client is configured with ``pydantic_data_converter``, all fields are + serialized and deserialized automatically. + """ + + def __init__( + self, + name: str, + config: ActivityConfig, + state: SandboxSessionState, + supports_pty_flag: bool = True, + ) -> None: + """Initialize the session.""" + self._name = name + self._config = config + self._state = state + self._supports_pty = supports_pty_flag + + @property + def state(self) -> SandboxSessionState: + """The current session state.""" + return self._state + + @state.setter + def state(self, value: SandboxSessionState) -> None: # type: ignore[reportIncompatibleVariableOverride] + self._state = value + + async def exec( + self, + *command: str | Path, + timeout: float | None = None, + shell: bool | list[str] = True, + user: str | User | None = None, + ) -> ExecResult: + """Execute a command in the sandbox via activity.""" + result: ExecResultModel = await workflow.execute_activity( + f"{self._name}-sandbox_session_exec", + arg=ExecArgs( + state=self.state, + command=[str(c) for c in command], + timeout=timeout, + shell=shell, + user=user, + ), + result_type=ExecResultModel, + **self._config, + ) + return ExecResult( + stdout=result.stdout, stderr=result.stderr, exit_code=result.exit_code + ) + + async def _exec_internal( + self, + *command: str | Path, + timeout: float | None = None, + ) -> ExecResult: + raise NotImplementedError("TemporalSandboxSession overrides exec() directly") + + async def read(self, path: Path, *, user: str | User | None = None) -> io.IOBase: + """Read a file from the sandbox via activity.""" + result: ReadResult = await workflow.execute_activity( + f"{self._name}-sandbox_session_read", + arg=ReadArgs(state=self.state, path=str(path)), + result_type=ReadResult, + **self._config, + ) + return io.BytesIO(result.data) + + async def write( + self, path: Path, data: io.IOBase, *, user: str | User | None = None + ) -> None: + """Write a file to the sandbox via activity.""" + await workflow.execute_activity( + f"{self._name}-sandbox_session_write", + arg=WriteArgs(state=self.state, path=str(path), data=data.read()), + **self._config, + ) + + async def running(self) -> bool: + """Check if the sandbox is running via activity.""" + result: RunningResult = await workflow.execute_activity( + f"{self._name}-sandbox_session_running", + arg=RunningArgs(state=self.state), + result_type=RunningResult, + **self._config, + ) + return result.is_running + + async def shutdown(self) -> None: + """Shut down the sandbox via activity.""" + await workflow.execute_activity( + f"{self._name}-sandbox_session_shutdown", + arg=StopArgs(state=self.state), + **self._config, + ) + + async def persist_workspace(self) -> io.IOBase: + """Persist the workspace via activity.""" + result: PersistWorkspaceResult = await workflow.execute_activity( + f"{self._name}-sandbox_session_persist_workspace", + arg=PersistWorkspaceArgs(state=self.state), + result_type=PersistWorkspaceResult, + **self._config, + ) + return io.BytesIO(result.data) + + async def hydrate_workspace(self, data: io.IOBase) -> None: + """Hydrate the workspace via activity.""" + await workflow.execute_activity( + f"{self._name}-sandbox_session_hydrate_workspace", + arg=HydrateWorkspaceArgs(state=self.state, data=data.read()), + **self._config, + ) + + def supports_pty(self) -> bool: + """Whether this session supports PTY operations.""" + return self._supports_pty + + async def pty_exec_start( + self, + *command: str | Path, + timeout: float | None = None, + shell: bool | list[str] = True, + user: str | User | None = None, + tty: bool = False, + yield_time_s: float | None = None, + max_output_tokens: int | None = None, + ) -> PtyExecUpdate: + """Start a PTY exec via activity.""" + result: PtyExecUpdateResult = await workflow.execute_activity( + f"{self._name}-sandbox_session_pty_exec_start", + arg=PtyExecStartArgs( + state=self.state, + command=[str(c) for c in command], + timeout=timeout, + shell=shell, + user=user, + tty=tty, + yield_time_s=yield_time_s, + max_output_tokens=max_output_tokens, + ), + result_type=PtyExecUpdateResult, + **self._config, + ) + return PtyExecUpdate( + process_id=result.process_id, + output=result.output, + exit_code=result.exit_code, + original_token_count=result.original_token_count, + ) + + async def pty_write_stdin( + self, + *, + session_id: int, + chars: str, + yield_time_s: float | None = None, + max_output_tokens: int | None = None, + ) -> PtyExecUpdate: + """Write to PTY stdin via activity.""" + result: PtyExecUpdateResult = await workflow.execute_activity( + f"{self._name}-sandbox_session_pty_write_stdin", + arg=PtyWriteStdinArgs( + state=self.state, + session_id=session_id, + chars=chars, + yield_time_s=yield_time_s, + max_output_tokens=max_output_tokens, + ), + result_type=PtyExecUpdateResult, + **self._config, + ) + return PtyExecUpdate( + process_id=result.process_id, + output=result.output, + exit_code=result.exit_code, + original_token_count=result.original_token_count, + ) + + async def start(self) -> None: + """Start the sandbox session via activity.""" + await workflow.execute_activity( + f"{self._name}-sandbox_session_start", + arg=StartArgs(state=self.state), + **self._config, + ) + + async def stop(self) -> None: + """Stop the sandbox session via activity.""" + await workflow.execute_activity( + f"{self._name}-sandbox_session_stop", + arg=StopArgs(state=self.state), + **self._config, + ) diff --git a/temporalio/contrib/openai_agents/testing.py b/temporalio/contrib/openai_agents/testing.py new file mode 100644 index 000000000..110ca20b7 --- /dev/null +++ b/temporalio/contrib/openai_agents/testing.py @@ -0,0 +1,262 @@ +"""Testing utilities for OpenAI agents.""" + +from collections.abc import AsyncIterator, Callable, Sequence +from typing import Any + +from agents import ( + AgentOutputSchemaBase, + Handoff, + Model, + ModelProvider, + ModelResponse, + ModelSettings, + ModelTracing, + Tool, + TResponseInputItem, + Usage, +) +from agents.items import TResponseOutputItem, TResponseStreamEvent +from openai.types.responses import ( + ResponseFunctionToolCall, + ResponseOutputMessage, + ResponseOutputText, +) + +from temporalio.client import Client +from temporalio.contrib.openai_agents._mcp import ( + StatefulMCPServerProvider, + StatelessMCPServerProvider, +) +from temporalio.contrib.openai_agents._model_parameters import ModelActivityParameters +from temporalio.contrib.openai_agents._temporal_openai_agents import OpenAIAgentsPlugin + +__all__ = [ + "AgentEnvironment", + "ResponseBuilders", + "TestModel", + "TestModelProvider", +] + + +class ResponseBuilders: + """Builders for creating model responses for testing.""" + + @staticmethod + def model_response(output: TResponseOutputItem) -> ModelResponse: + """Create a ModelResponse with the given output.""" + return ModelResponse( + output=[output], + usage=Usage(), + response_id=None, + ) + + @staticmethod + def response_output_message(text: str) -> ResponseOutputMessage: + """Create a ResponseOutputMessage with text content.""" + return ResponseOutputMessage( + id="", + content=[ + ResponseOutputText( + text=text, + annotations=[], + type="output_text", + ) + ], + role="assistant", + status="completed", + type="message", + ) + + @staticmethod + def tool_call(arguments: str, name: str) -> ModelResponse: + """Create a ModelResponse with a function tool call.""" + return ResponseBuilders.model_response( + ResponseFunctionToolCall( + arguments=arguments, + call_id="call", + name=name, + type="function_call", + id="id", + status="completed", + ) + ) + + @staticmethod + def output_message(text: str) -> ModelResponse: + """Create a ModelResponse with an output message.""" + return ResponseBuilders.model_response( + ResponseBuilders.response_output_message(text) + ) + + +class TestModelProvider(ModelProvider): + """Test model provider which simply returns the given model.""" + + __test__ = False + + def __init__(self, model: Model): + """Initialize a test model provider with a model.""" + self._model = model + + def get_model(self, model_name: str | None) -> Model: + """Get a model from the model provider.""" + return self._model + + +class TestModel(Model): + """Test model for use mocking model responses.""" + + __test__ = False + + def __init__(self, fn: Callable[[], ModelResponse]) -> None: + """Initialize a test model with a callable.""" + self.fn = fn + + async def get_response( + self, + system_instructions: str | None, + input: str | list[TResponseInputItem], + model_settings: ModelSettings, + tools: list[Tool], + output_schema: AgentOutputSchemaBase | None, + handoffs: list[Handoff], + tracing: ModelTracing, + **kwargs: Any, + ) -> ModelResponse: + """Get a response from the mocked model, by calling the callable passed to the constructor.""" + return self.fn() + + def stream_response( + self, + system_instructions: str | None, + input: str | list[TResponseInputItem], + model_settings: ModelSettings, + tools: list[Tool], + output_schema: AgentOutputSchemaBase | None, + handoffs: list[Handoff], + tracing: ModelTracing, + **kwargs: Any, + ) -> AsyncIterator[TResponseStreamEvent]: + """Get a streamed response from the model. Unimplemented.""" + raise NotImplementedError() + + @staticmethod + def returning_responses(responses: list[ModelResponse]) -> "TestModel": + """Create a mock model which sequentially returns responses from a list.""" + i = iter(responses) + return TestModel(lambda: next(i)) + + +class AgentEnvironment: + """Testing environment for OpenAI agents with Temporal integration. + + This async context manager provides a convenient way to set up testing environments + for OpenAI agents with mocked model calls and Temporal integration. + + Example: + >>> from temporalio.contrib.openai_agents.testing import AgentEnvironment, TestModelProvider, ResponseBuilders + >>> from temporalio.client import Client + >>> + >>> # Create a mock model that returns predefined responses + >>> mock_model = TestModel.returning_responses([ + ... ResponseBuilders.output_message("Hello, world!"), + ... ResponseBuilders.output_message("How can I help you?") + ... ]) + >>> + >>> async with AgentEnvironment(model=mock_model) as env: + ... client = env.applied_on_client(client) + ... # Use client for testing workflows with mocked model calls + """ + + __test__ = False + + def __init__( + self, + model_params: ModelActivityParameters | None = None, + model_provider: ModelProvider | None = None, + model: Model | None = None, + mcp_server_providers: Sequence[ + StatelessMCPServerProvider | StatefulMCPServerProvider + ] = (), + register_activities: bool = True, + add_temporal_spans: bool = True, + use_otel_instrumentation: bool = False, + ) -> None: + """Initialize the AgentEnvironment. + + Args: + model_params: Configuration parameters for Temporal activity execution + of model calls. If None, default parameters will be used. + model_provider: Optional model provider for custom model implementations. + Only one of model_provider or model should be provided. + If both are provided, model_provider will be used. + model: Optional model for custom model implementations. + Use TestModel for mocking model responses. + Equivalent to model_provider=TestModelProvider(model). + Only one of model_provider or model should be provided. + If both are provided, model_provider will be used. + mcp_server_providers: Sequence of MCP servers to automatically register with the worker. + register_activities: Whether to register activities during worker execution. + add_temporal_spans: Whether to add temporal spans to traces + use_otel_instrumentation: If set to true, enable open telemetry instrumentation. + Warning: use_otel_instrumentation is experimental and behavior may change in future versions. + Use with caution in production environments. + """ + self._model_params = model_params + self._model_provider = None + if model_provider is not None: + self._model_provider = model_provider + elif model is not None: + self._model_provider = TestModelProvider(model) + self._mcp_server_providers = mcp_server_providers + self._register_activities = register_activities + self._plugin: OpenAIAgentsPlugin | None = None + self._add_temporal_spans = add_temporal_spans + self._use_otel_instrumentation = use_otel_instrumentation + + async def __aenter__(self) -> "AgentEnvironment": + """Enter the async context manager.""" + # Create the plugin with the provided configuration + self._plugin = OpenAIAgentsPlugin( + model_params=self._model_params, + model_provider=self._model_provider, + mcp_server_providers=self._mcp_server_providers, + register_activities=self._register_activities, + add_temporal_spans=self._add_temporal_spans, + use_otel_instrumentation=self._use_otel_instrumentation, + ) + + return self + + async def __aexit__(self, *args: Any) -> None: + """Exit the async context manager.""" + # No cleanup needed currently + pass + + def applied_on_client(self, client: Client) -> Client: + """Apply the agent environment's plugin to a client and return a new client instance. + + Args: + client: The base Temporal client to apply the plugin to. + + Returns: + A new Client instance with the OpenAI agents plugin applied. + """ + if self._plugin is None: + raise RuntimeError( + "AgentEnvironment must be entered before applying to client" + ) + + new_config = client.config() + existing_plugins = new_config.get("plugins", []) + new_config["plugins"] = list(existing_plugins) + [self._plugin] + return Client(**new_config) + + @property + def openai_agents_plugin(self) -> OpenAIAgentsPlugin: + """Get the underlying OpenAI agents plugin.""" + if self._plugin is None: + raise RuntimeError( + "AgentEnvironment must be entered before accessing plugin" + ) + return self._plugin diff --git a/temporalio/contrib/openai_agents/workflow.py b/temporalio/contrib/openai_agents/workflow.py index 2f69866ce..d99028d68 100644 --- a/temporalio/contrib/openai_agents/workflow.py +++ b/temporalio/contrib/openai_agents/workflow.py @@ -3,63 +3,70 @@ import functools import inspect import json +import typing +from collections.abc import Callable +from contextlib import AbstractAsyncContextManager from datetime import timedelta -from typing import Any, Callable, Optional, Type, Union, overload +from typing import Any import nexusrpc from agents import ( - Agent, RunContextWrapper, Tool, ) -from agents.function_schema import DocstringStyle, function_schema +from agents.function_schema import function_schema from agents.tool import ( FunctionTool, - ToolErrorFunction, - ToolFunction, - ToolParams, - default_tool_error_function, - function_tool, ) -from agents.util._types import MaybeAwaitable from temporalio import activity from temporalio import workflow as temporal_workflow from temporalio.common import Priority, RetryPolicy +from temporalio.contrib.openai_agents.sandbox._temporal_sandbox_client import ( + TemporalSandboxClient, +) from temporalio.exceptions import ApplicationError, TemporalError -from temporalio.workflow import ActivityCancellationType, VersioningIntent +from temporalio.workflow import ( + ActivityCancellationType, + ActivityConfig, + VersioningIntent, +) + +if typing.TYPE_CHECKING: + from agents.mcp import MCPServer def activity_as_tool( fn: Callable, *, - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[RetryPolicy] = None, + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: RetryPolicy | None = None, cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, priority: Priority = Priority.default, + strict_json_schema: bool = True, ) -> Tool: """Convert a single Temporal activity function to an OpenAI agent tool. - .. warning:: - This API is experimental and may change in future versions. - Use with caution in production environments. - This function takes a Temporal activity function and converts it into an OpenAI agent tool that can be used by the agent to execute the activity during workflow execution. The tool will automatically handle the conversion of inputs and outputs between the agent and the activity. Note that if you take a context, mutation will not be persisted, as the activity may not be running in the same location. + For undocumented arguments, refer to :py:mod:`workflow` and :py:meth:`start_activity` + Args: fn: A Temporal activity function to convert to a tool. - For other arguments, refer to :py:mod:`workflow` :py:meth:`start_activity` + strict_json_schema: Whether the tool should follow a strict schema. + See https://openai.github.io/openai-agents-python/ref/tool/#agents.tool.FunctionTool.strict_json_schema + Returns: An OpenAI agent tool that wraps the provided activity. @@ -149,32 +156,30 @@ async def run_activity(ctx: RunContextWrapper[Any], input: str) -> Any: description=schema.description or "", params_json_schema=schema.params_json_schema, on_invoke_tool=run_activity, - strict_json_schema=True, + strict_json_schema=strict_json_schema, ) def nexus_operation_as_tool( operation: nexusrpc.Operation[Any, Any], *, - service: Type[Any], + service: type[Any], endpoint: str, - schedule_to_close_timeout: Optional[timedelta] = None, + schedule_to_close_timeout: timedelta | None = None, + strict_json_schema: bool = True, ) -> Tool: """Convert a Nexus operation into an OpenAI agent tool. - .. warning:: - This API is experimental and may change in future versions. - Use with caution in production environments. - This function takes a Nexus operation and converts it into an OpenAI agent tool that can be used by the agent to execute the operation during workflow execution. The tool will automatically handle the conversion of inputs and outputs between the agent and the operation. Args: - fn: A Nexus operation to convert into a tool. + operation: A Nexus operation to convert into a tool. service: The Nexus service class that contains the operation. endpoint: The Nexus endpoint to use for the operation. + strict_json_schema: Whether the tool should follow a strict schema Returns: An OpenAI agent tool that wraps the provided operation. @@ -193,7 +198,7 @@ def nexus_operation_as_tool( >>> # Use tool with an OpenAI agent """ - def operation_callable(input): + def operation_callable(input: Any): # type: ignore[reportUnusedParameter] raise NotImplementedError("This function definition is used as a type only") operation_callable.__annotations__ = { @@ -204,7 +209,7 @@ def operation_callable(input): schema = function_schema(operation_callable) - async def run_operation(ctx: RunContextWrapper[Any], input: str) -> Any: + async def run_operation(_ctx: RunContextWrapper[Any], input: str) -> Any: try: json_data = json.loads(input) except Exception as e: @@ -235,17 +240,126 @@ async def run_operation(ctx: RunContextWrapper[Any], input: str) -> Any: description=schema.description or "", params_json_schema=schema.params_json_schema, on_invoke_tool=run_operation, - strict_json_schema=True, + strict_json_schema=strict_json_schema, ) -class ToolSerializationError(TemporalError): - """Error that occurs when a tool output could not be serialized. +def temporal_sandbox_client( + name: str, + config: ActivityConfig | None = None, +) -> Any: + """Create a sandbox client reference for use in a Temporal workflow ``RunConfig``. .. warning:: - This exception is experimental and may change in future versions. + This is experimental and may change in future versions. Use with caution in production environments. + This returns a ``BaseSandboxClient`` that dispatches all sandbox operations + as Temporal activities, targeting the ``SandboxClientProvider`` registered + on the worker with the matching ``name``. + + Example:: + + run_config = RunConfig( + sandbox=SandboxRunConfig( + client=temporal_sandbox_client("daytona"), + options=DaytonaSandboxClientOptions(...), + ), + ) + + Args: + name: The name of the ``SandboxClientProvider`` registered on the + worker. Must match exactly. + config: Optional activity configuration for controlling timeouts, + retries, etc. Defaults to a 5-minute ``start_to_close_timeout``. + """ + return TemporalSandboxClient(name=name, config=config) + + +def stateless_mcp_server( + name: str, + config: ActivityConfig | None = None, + cache_tools_list: bool = False, + factory_argument: Any | None = None, +) -> "MCPServer": + """A stateless MCP server implementation for Temporal workflows. + + This uses a TemporalMCPServer of the same name registered with the OpenAIAgents plugin to implement + durable MCP operations statelessly. + + This approach is suitable for simple use cases where connection overhead is acceptable + and you don't need to maintain state between operations. It should be preferred to stateful when possible due to its + superior durability guarantees. + + .. warning:: + Do not pass secrets, credentials, or API keys through ``factory_argument``. It is an + activity argument, so it is recorded in workflow history and, without a payload codec, + visible in the web UI. Resolve credentials worker-side inside the server factory + instead. + + Args: + name: A string name for the server. Should match that provided in the plugin. + config: Optional activity configuration for MCP operation activities. + Defaults to 1-minute start-to-close timeout. + cache_tools_list: If true, the list of tools will be cached for the duration of the server + factory_argument: Optional argument to be provided to the factory when producing an MCPServer. + Must not contain secrets. + """ + from temporalio.contrib.openai_agents._mcp import ( + _StatelessMCPServerReference, + ) + + return _StatelessMCPServerReference( + name, config, cache_tools_list, factory_argument + ) + + +def stateful_mcp_server( + name: str, + config: ActivityConfig | None = None, + server_session_config: ActivityConfig | None = None, + factory_argument: Any | None = None, +) -> AbstractAsyncContextManager["MCPServer"]: + """A stateful MCP server implementation for Temporal workflows. + + This wraps an MCP server to maintain a persistent connection throughout + the workflow execution. It creates a dedicated worker that stays connected to + the MCP server and processes operations on a dedicated task queue. + + This approach is more efficient for workflows that make multiple MCP calls, + as it avoids connection overhead, but requires more resources to maintain + the persistent connection and worker. + + The caller will have to handle cases where the dedicated worker fails, as Temporal is + unable to seamlessly recreate any lost state in that case. + + .. warning:: + Do not pass secrets, credentials, or API keys through ``factory_argument``. It is an + activity argument, so it is recorded in workflow history and, without a payload codec, + visible in the web UI. Resolve credentials worker-side inside the server factory + instead. + + Args: + name: A string name for the server. Should match that provided in the plugin. + config: Optional activity configuration for MCP operation activities. + Defaults to 1-minute start-to-close and 30-second schedule-to-start timeouts. + server_session_config: Optional activity configuration for the connection activity. + Defaults to 1-hour start-to-close timeout. + factory_argument: Optional argument to be provided to the factory when producing an MCPServer. + Must not contain secrets. + """ + from temporalio.contrib.openai_agents._mcp import ( + _StatefulMCPServerReference, + ) + + return _StatefulMCPServerReference( + name, config, server_session_config, factory_argument + ) + + +class ToolSerializationError(TemporalError): + """Error that occurs when a tool output could not be serialized. + This exception is raised when a tool (created from an activity or Nexus operation) returns a value that cannot be properly serialized for use by the OpenAI agent. All tool outputs must be convertible to strings for the agent to process them. @@ -266,9 +380,4 @@ class ToolSerializationError(TemporalError): class AgentsWorkflowError(TemporalError): - """Error that occurs when the agents SDK raises an error which should terminate the calling workflow or update. - - .. warning:: - This exception is experimental and may change in future versions. - Use with caution in production environments. - """ + """Error that occurs when the agents SDK raises an error which should terminate the calling workflow or update.""" diff --git a/temporalio/contrib/opentelemetry/README.md b/temporalio/contrib/opentelemetry/README.md new file mode 100644 index 000000000..2c6e39817 --- /dev/null +++ b/temporalio/contrib/opentelemetry/README.md @@ -0,0 +1,259 @@ +# OpenTelemetry Integration for Temporal Python SDK + +This package provides OpenTelemetry tracing integration for Temporal workflows, activities, and other operations. It includes automatic span creation and propagation for distributed tracing across your Temporal applications. + +## Overview + +There are **two different approaches** for integrating OpenTelemetry with the Temporal Python SDK: + +1. **🆕 New Approach (Recommended)**: `OpenTelemetryPlugin` - Provides accurate duration spans and direct OpenTelemetry usage within workflows +2. **📊 Legacy Approach**: `TracingInterceptor` - Provides immediate span visibility but with zero-duration workflow spans + +## Quick Start + +### New Approach (OpenTelemetryPlugin) + +```python +import opentelemetry.trace +from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor +from temporalio.contrib.opentelemetry import OpenTelemetryPlugin, create_tracer_provider + +# Create a replay-safe tracer provider +provider = create_tracer_provider() +provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter())) +opentelemetry.trace.set_tracer_provider(provider) + +# Register plugin on CLIENT (automatically applies to workers using this client) +client = await Client.connect( + "localhost:7233", + plugins=[OpenTelemetryPlugin()] +) + +# Workers created with this client automatically get the plugin +worker = Worker( + client, + task_queue="my-task-queue", + workflows=[MyWorkflow], + activities=[my_activity] + # NO NEED to specify plugins here - they come from the client +) +``` + +### Legacy Approach (TracingInterceptor) + +```python +from temporalio.contrib.opentelemetry import TracingInterceptor + +# Register interceptor on CLIENT (automatically applies to workers using this client) +client = await Client.connect( + "localhost:7233", + interceptors=[TracingInterceptor()] +) + +# Workers created with this client automatically get the interceptor +worker = Worker( + client, + task_queue="my-task-queue", + workflows=[MyWorkflow], + activities=[my_activity] + # NO NEED to specify interceptors here - they come from the client +) +``` + +## Detailed Comparison + +### New Approach: OpenTelemetryPlugin + +#### ✅ Advantages: +- **Accurate Duration Spans**: Workflow spans have real durations reflecting actual execution time +- **Direct OpenTelemetry Usage**: Use `opentelemetry.trace.get_tracer()` directly within workflows +- **Better Span Hierarchy**: More accurate parent-child relationships within workflows +- **Workflow Context Access**: Access spans within workflows using `opentelemetry.trace.get_tracer()` + +#### ⚠️ Considerations: +- **Experimental Status**: Subject to breaking changes in future versions +- **Delayed Span Visibility**: Workflow spans only appear after workflow completion +- **Different Trace Structure**: Migration from legacy approach may break dependencies on specific trace structures + +#### Usage Example: +```python +@workflow.defn +class MyWorkflow: + @workflow.run + async def run(self): + # Direct OpenTelemetry usage works correctly + tracer = get_tracer(__name__) + with tracer.start_as_current_span("workflow-operation"): + # This span will have accurate duration + await workflow.execute_activity( + my_activity, + start_to_close_timeout=timedelta(seconds=30) + ) +``` + +### Legacy Approach: TracingInterceptor + +**File**: `temporalio/contrib/opentelemetry/_interceptor.py` + +#### ✅ Advantages: +- **Immediate Span Visibility**: Spans appear as soon as they're created +- **Stable API**: Well-established interface, not subject to experimental changes +- **Workflow Progress Tracking**: Can see workflow spans even before workflow completes + +#### ⚠️ Limitations: +- **Zero-Duration Workflow Spans**: All workflow spans are immediately ended with 0ms duration +- **No Direct OpenTelemetry Usage**: Cannot use standard OpenTelemetry APIs within workflows +- **Limited Workflow Span Creation**: Must use `temporalio.contrib.opentelemetry.workflow.completed_span()` + +#### Usage Example: +```python +@workflow.defn +class MyWorkflow: + @workflow.run + async def run(self): + # Must use Temporal-specific span creation + temporalio.contrib.opentelemetry.workflow.completed_span( + "workflow-operation", + attributes={"custom": "attribute"} + ) + # Standard OpenTelemetry APIs don't work properly here +``` + +## When to Use Each Approach + +### Choose OpenTelemetryPlugin When: +- You need accurate span durations for performance analysis +- You want to use standard OpenTelemetry APIs within workflows +- You're building new applications +- You can tolerate experimental API changes + +### Choose TracingInterceptor When: +- You need immediate visibility into workflow progress +- You have existing dependencies on the current trace structure +- You require a stable, non-experimental API +- You primarily need basic tracing without complex workflow span hierarchies + +## Configuration Options + +### OpenTelemetryPlugin Options + +```python +plugin = OpenTelemetryPlugin( + add_temporal_spans=False # Whether to add additional Temporal-specific spans +) +``` + +### TracingInterceptor Options + +```python +interceptor = TracingInterceptor( + tracer=None, # Custom tracer (defaults to global tracer) + always_create_workflow_spans=False # Create spans even without parent context +) +``` + +## Migration Guide + +### From TracingInterceptor to OpenTelemetryPlugin + +1. **Replace interceptor with plugin on client**: + ```python + # Old + client = await Client.connect( + "localhost:7233", + interceptors=[TracingInterceptor()] + ) + + # New + provider = create_tracer_provider() + opentelemetry.trace.set_tracer_provider(provider) + client = await Client.connect( + "localhost:7233", + plugins=[OpenTelemetryPlugin()] + ) + ``` + +2. **Update workflow span creation**: + ```python + # Old + temporalio.contrib.opentelemetry.workflow.completed_span("my-span") + + # New - use standard OpenTelemetry + tracer = get_tracer(__name__) + with tracer.start_as_current_span("my-span"): + # Your workflow logic + pass + ``` + +3. **Test trace structure changes**: Verify that any monitoring or analysis tools still work with the new trace structure. + +## Advanced Usage + +### Creating Custom Spans in Workflows (New Approach) + +```python +from opentelemetry.trace import get_tracer + +@workflow.defn +class MyWorkflow: + @workflow.run + async def run(self): + tracer = get_tracer(__name__) + + # Create spans with accurate durations + with tracer.start_as_current_span("business-logic") as span: + span.set_attribute("workflow.step", "processing") + + # Nested spans work correctly + with tracer.start_as_current_span("data-validation"): + await self.validate_input() + + await workflow.execute_activity( + process_data, + start_to_close_timeout=timedelta(seconds=60) + ) +``` + +### Custom Span Attributes + +Both approaches support adding custom attributes to spans: + +```python +# Legacy approach +temporalio.contrib.opentelemetry.workflow.completed_span( + "my-operation", + attributes={ + "business.unit": "payments", + "request.id": "req-123" + } +) + +# New approach +with tracer.start_as_current_span("my-operation") as span: + span.set_attributes({ + "business.unit": "payments", + "request.id": "req-123" + }) +``` + +## Best Practices + +1. **Register on Client**: Always register plugins/interceptors on the client, not the worker, to ensure proper context propagation + +2. **Use create_tracer_provider()**: Always use the provided function to create replay-safe tracer providers when using the new approach + +3. **Set Global Tracer Provider**: Ensure the tracer provider is set globally before creating clients + +4. **Avoid Duplication**: Never register the same plugin/interceptor on both client and worker + +## Troubleshooting + +### Common Issues + +1. **"ReplaySafeTracerProvider required" error**: Make sure you're using `create_tracer_provider()` when using OpenTelemetryPlugin + +2. **Missing spans**: Verify that the tracer provider is set before creating clients, and that plugins/interceptors are registered on the client + +3. **Duplicate spans**: Check that you haven't registered the same plugin/interceptor on both client and worker + +4. **Zero-duration spans**: This is expected behavior with TracingInterceptor for workflow spans diff --git a/temporalio/contrib/opentelemetry/__init__.py b/temporalio/contrib/opentelemetry/__init__.py new file mode 100644 index 000000000..74f069322 --- /dev/null +++ b/temporalio/contrib/opentelemetry/__init__.py @@ -0,0 +1,22 @@ +"""OpenTelemetry v2 integration for Temporal SDK. + +This package provides OpenTelemetry tracing integration for Temporal workflows, +activities, and other operations. It includes automatic span creation and +propagation for distributed tracing. +""" + +from temporalio.contrib.opentelemetry._interceptor import ( + TracingInterceptor, + TracingWorkflowInboundInterceptor, +) +from temporalio.contrib.opentelemetry._otel_interceptor import OpenTelemetryInterceptor +from temporalio.contrib.opentelemetry._plugin import OpenTelemetryPlugin +from temporalio.contrib.opentelemetry._tracer_provider import create_tracer_provider + +__all__ = [ + "TracingInterceptor", + "TracingWorkflowInboundInterceptor", + "OpenTelemetryInterceptor", + "OpenTelemetryPlugin", + "create_tracer_provider", +] diff --git a/temporalio/contrib/opentelemetry/_id_generator.py b/temporalio/contrib/opentelemetry/_id_generator.py new file mode 100644 index 000000000..ea2859263 --- /dev/null +++ b/temporalio/contrib/opentelemetry/_id_generator.py @@ -0,0 +1,102 @@ +import random + +from opentelemetry.sdk.trace.id_generator import IdGenerator +from opentelemetry.trace import ( + INVALID_SPAN_ID, + INVALID_TRACE_ID, +) + +import temporalio.workflow + + +def _get_workflow_random() -> random.Random | None: + if ( + temporalio.workflow.in_workflow() + and not temporalio.workflow.unsafe.is_read_only() + ): + if ( + getattr(temporalio.workflow.instance(), "__temporal_otel_id_random", None) + is None + ): + setattr( + temporalio.workflow.instance(), + "__temporal_otel_id_random", + temporalio.workflow.new_random(), + ) + return getattr(temporalio.workflow.instance(), "__temporal_otel_id_random") + + return None + + +class TemporalIdGenerator(IdGenerator): + """OpenTelemetry ID generator that uses Temporal's deterministic random generator. + + .. warning:: + This class is experimental and may change in future versions. + Use with caution in production environments. + + This generator uses Temporal's workflow-safe random number generator when + inside a workflow execution, ensuring deterministic span and trace IDs + across workflow replays. Falls back to standard random generation outside + of workflows. + + Can be seeded with OpenTelemetry span IDs from client context to maintain + proper span parenting across the client-workflow boundary. + """ + + def __init__(self, id_generator: IdGenerator): + """Initialize a TemporalIdGenerator.""" + self._id_generator = id_generator + self.traces: list[int] = [] + self.spans: list[int] = [] + + def seed_span_id(self, span_id: int) -> None: + """Seed the generator with a span ID to use as the first result. + + This is typically used to maintain OpenTelemetry span parenting + when crossing the client-workflow boundary. + + Args: + span_id: The span ID to use as the first generated span ID. + """ + self.spans.append(span_id) + + def seed_trace_id(self, trace_id: int) -> None: + """Seed the generator with a trace ID to use as the first result. + + Args: + trace_id: The trace ID to use as the first generated trace ID. + """ + self.traces.append(trace_id) + + def generate_span_id(self) -> int: + """Generate a span ID using Temporal's deterministic random when in workflow. + + Returns: + A 64-bit span ID. + """ + if len(self.spans) > 0: + return self.spans.pop() + + if workflow_random := _get_workflow_random(): + span_id = workflow_random.getrandbits(64) + while span_id == INVALID_SPAN_ID: + span_id = workflow_random.getrandbits(64) + return span_id + return self._id_generator.generate_span_id() + + def generate_trace_id(self) -> int: + """Generate a trace ID using Temporal's deterministic random when in workflow. + + Returns: + A 128-bit trace ID. + """ + if len(self.traces) > 0: + return self.traces.pop() + + if workflow_random := _get_workflow_random(): + trace_id = workflow_random.getrandbits(128) + while trace_id == INVALID_TRACE_ID: + trace_id = workflow_random.getrandbits(128) + return trace_id + return self._id_generator.generate_trace_id() diff --git a/temporalio/contrib/opentelemetry.py b/temporalio/contrib/opentelemetry/_interceptor.py similarity index 71% rename from temporalio/contrib/opentelemetry.py rename to temporalio/contrib/opentelemetry/_interceptor.py index 380b666dc..eb22f8be6 100644 --- a/temporalio/contrib/opentelemetry.py +++ b/temporalio/contrib/opentelemetry/_interceptor.py @@ -2,21 +2,20 @@ from __future__ import annotations +import dataclasses +from collections.abc import Callable, Iterator, Mapping, Sequence from contextlib import contextmanager from dataclasses import dataclass from typing import ( Any, - Callable, - Dict, - Iterator, - Mapping, + Generic, NoReturn, - Optional, - Sequence, - Type, + TypeAlias, + TypeVar, cast, ) +import nexusrpc.handler import opentelemetry.baggage.propagation import opentelemetry.context import opentelemetry.context.context @@ -25,7 +24,9 @@ import opentelemetry.trace import opentelemetry.trace.propagation.tracecontext import opentelemetry.util.types -from typing_extensions import Protocol, TypeAlias, TypedDict +from opentelemetry.context import Context +from opentelemetry.trace import Status, StatusCode +from typing_extensions import Protocol, TypedDict import temporalio.activity import temporalio.api.common.v1 @@ -34,6 +35,7 @@ import temporalio.exceptions import temporalio.worker import temporalio.workflow +from temporalio.exceptions import ApplicationError, ApplicationErrorCategory # OpenTelemetry dynamically, lazily chooses its context implementation at # runtime. When first accessed, they use pkg_resources.iter_entry_points + load. @@ -51,7 +53,9 @@ ) """Default text map propagator used by :py:class:`TracingInterceptor`.""" -_CarrierDict: TypeAlias = Dict[str, opentelemetry.propagators.textmap.CarrierValT] +_CarrierDict: TypeAlias = dict[str, opentelemetry.propagators.textmap.CarrierValT] + +_ContextT = TypeVar("_ContextT", bound=nexusrpc.handler.OperationContext) class TracingInterceptor(temporalio.client.Interceptor, temporalio.worker.Interceptor): @@ -73,7 +77,7 @@ class should return the workflow interceptor subclass from def __init__( # type: ignore[reportMissingSuperCall] self, - tracer: Optional[opentelemetry.trace.Tracer] = None, + tracer: opentelemetry.trace.Tracer | None = None, *, always_create_workflow_spans: bool = False, ) -> None: @@ -120,7 +124,7 @@ def intercept_activity( def workflow_interceptor_class( self, input: temporalio.worker.WorkflowInterceptorClassInput - ) -> Type[TracingWorkflowInboundInterceptor]: + ) -> type[TracingWorkflowInboundInterceptor]: """Implementation of :py:meth:`temporalio.worker.Interceptor.workflow_interceptor_class`. """ @@ -132,6 +136,14 @@ def workflow_interceptor_class( ) return TracingWorkflowInboundInterceptor + def intercept_nexus_operation( + self, next: temporalio.worker.NexusOperationInboundInterceptor + ) -> temporalio.worker.NexusOperationInboundInterceptor: + """Implementation of + :py:meth:`temporalio.worker.Interceptor.intercept_nexus_operation`. + """ + return _TracingNexusOperationInboundInterceptor(next, self) + def _context_to_headers( self, headers: Mapping[str, temporalio.api.common.v1.Payload] ) -> Mapping[str, temporalio.api.common.v1.Payload]: @@ -146,7 +158,7 @@ def _context_to_headers( def _context_from_headers( self, headers: Mapping[str, temporalio.api.common.v1.Payload] - ) -> Optional[opentelemetry.context.context.Context]: + ) -> opentelemetry.context.context.Context | None: if self.header_key not in headers: return None header_payload = headers.get(self.header_key) @@ -165,17 +177,54 @@ def _start_as_current_span( name: str, *, attributes: opentelemetry.util.types.Attributes, - input: Optional[_InputWithHeaders] = None, + input_with_headers: _InputWithHeaders | None = None, + input_with_ctx: _InputWithOperationContext | None = None, kind: opentelemetry.trace.SpanKind, + context: Context | None = None, ) -> Iterator[None]: - with self.tracer.start_as_current_span(name, attributes=attributes, kind=kind): - if input: - input.headers = self._context_to_headers(input.headers) - yield None + token = opentelemetry.context.attach(context) if context else None + try: + with self.tracer.start_as_current_span( + name, + attributes=attributes, + kind=kind, + context=context, + set_status_on_exception=False, + ) as span: + if input_with_headers: + input_with_headers.headers = self._context_to_headers( + input_with_headers.headers + ) + if input_with_ctx: + carrier: _CarrierDict = {} + self.text_map_propagator.inject(carrier) + input_with_ctx.ctx = dataclasses.replace( + input_with_ctx.ctx, + headers=_carrier_to_nexus_headers( + carrier, input_with_ctx.ctx.headers + ), + ) + try: + yield None + except Exception as exc: + if ( + not isinstance(exc, ApplicationError) + or exc.category != ApplicationErrorCategory.BENIGN + ): + span.set_status( + Status( + status_code=StatusCode.ERROR, + description=f"{type(exc).__name__}: {exc}", + ) + ) + raise + finally: + if token and context is opentelemetry.context.get_current(): + opentelemetry.context.detach(token) def _completed_workflow_span( self, params: _CompletedWorkflowSpanParams - ) -> Optional[_CarrierDict]: + ) -> _CarrierDict | None: # Carrier to context, start span, set span as current on context, # context back to carrier @@ -187,7 +236,7 @@ def _completed_workflow_span( # Extract the context context = self.text_map_propagator.extract(params.context) # Create link if there is a span present - links: Optional[Sequence[opentelemetry.trace.Link]] = [] + links: Sequence[opentelemetry.trace.Link] | None = [] if params.link_context: link_span = opentelemetry.trace.get_current_span( self.text_map_propagator.extract(params.link_context) @@ -232,7 +281,7 @@ async def start_workflow( with self.root._start_as_current_span( f"{prefix}:{input.workflow}", attributes={"temporalWorkflowID": input.id}, - input=input, + input_with_headers=input, kind=opentelemetry.trace.SpanKind.CLIENT, ): return await super().start_workflow(input) @@ -241,7 +290,7 @@ async def query_workflow(self, input: temporalio.client.QueryWorkflowInput) -> A with self.root._start_as_current_span( f"QueryWorkflow:{input.query}", attributes={"temporalWorkflowID": input.id}, - input=input, + input_with_headers=input, kind=opentelemetry.trace.SpanKind.CLIENT, ): return await super().query_workflow(input) @@ -252,7 +301,7 @@ async def signal_workflow( with self.root._start_as_current_span( f"SignalWorkflow:{input.signal}", attributes={"temporalWorkflowID": input.id}, - input=input, + input_with_headers=input, kind=opentelemetry.trace.SpanKind.CLIENT, ): return await super().signal_workflow(input) @@ -263,11 +312,49 @@ async def start_workflow_update( with self.root._start_as_current_span( f"StartWorkflowUpdate:{input.update}", attributes={"temporalWorkflowID": input.id}, - input=input, + input_with_headers=input, kind=opentelemetry.trace.SpanKind.CLIENT, ): return await super().start_workflow_update(input) + async def start_update_with_start_workflow( + self, input: temporalio.client.StartWorkflowUpdateWithStartInput + ) -> temporalio.client.WorkflowUpdateHandle[Any]: + attrs = { + "temporalWorkflowID": input.start_workflow_input.id, + } + if input.update_workflow_input.update_id is not None: + attrs["temporalUpdateID"] = input.update_workflow_input.update_id + + with self.root._start_as_current_span( + f"StartUpdateWithStartWorkflow:{input.start_workflow_input.workflow}", + attributes=attrs, + input_with_headers=input.start_workflow_input, + kind=opentelemetry.trace.SpanKind.CLIENT, + ): + otel_header = input.start_workflow_input.headers.get(self.root.header_key) + if otel_header: + input.update_workflow_input.headers = { + **input.update_workflow_input.headers, + self.root.header_key: otel_header, + } + + return await super().start_update_with_start_workflow(input) + + async def start_activity( + self, input: temporalio.client.StartActivityInput + ) -> temporalio.client.ActivityHandle[Any]: + with self.root._start_as_current_span( + f"StartActivity:{input.activity_type}", + attributes={ + "temporalActivityID": input.id, + "temporalActivityType": input.activity_type, + }, + input_with_headers=input, + kind=opentelemetry.trace.SpanKind.CLIENT, + ): + return await super().start_activity(input) + class _TracingActivityInboundInterceptor(temporalio.worker.ActivityInboundInterceptor): def __init__( @@ -282,26 +369,77 @@ async def execute_activity( self, input: temporalio.worker.ExecuteActivityInput ) -> Any: info = temporalio.activity.info() - with self.root.tracer.start_as_current_span( + attributes: dict[str, str] = {"temporalActivityID": info.activity_id} + if info.workflow_id: + attributes["temporalWorkflowID"] = info.workflow_id + if info.workflow_run_id: + attributes["temporalRunID"] = info.workflow_run_id + with self.root._start_as_current_span( f"RunActivity:{info.activity_type}", context=self.root._context_from_headers(input.headers), - attributes={ - "temporalWorkflowID": info.workflow_id, - "temporalRunID": info.workflow_run_id, - "temporalActivityID": info.activity_id, - }, + attributes=attributes, kind=opentelemetry.trace.SpanKind.SERVER, ): return await super().execute_activity(input) +class _TracingNexusOperationInboundInterceptor( + temporalio.worker.NexusOperationInboundInterceptor +): + def __init__( + self, + next: temporalio.worker.NexusOperationInboundInterceptor, + root: TracingInterceptor, + ) -> None: + super().__init__(next) + self._root = root + + def _context_from_nexus_headers(self, headers: Mapping[str, str]): + return self._root.text_map_propagator.extract(headers) + + async def execute_nexus_operation_start( + self, input: temporalio.worker.ExecuteNexusOperationStartInput + ) -> ( + nexusrpc.handler.StartOperationResultSync[Any] + | nexusrpc.handler.StartOperationResultAsync + ): + with self._root._start_as_current_span( + f"RunStartNexusOperationHandler:{input.ctx.service}/{input.ctx.operation}", + context=self._context_from_nexus_headers(input.ctx.headers), + attributes={}, + input_with_ctx=input, + kind=opentelemetry.trace.SpanKind.SERVER, + ): + return await self.next.execute_nexus_operation_start(input) + + async def execute_nexus_operation_cancel( + self, input: temporalio.worker.ExecuteNexusOperationCancelInput + ) -> None: + with self._root._start_as_current_span( + f"RunCancelNexusOperationHandler:{input.ctx.service}/{input.ctx.operation}", + context=self._context_from_nexus_headers(input.ctx.headers), + attributes={}, + input_with_ctx=input, + kind=opentelemetry.trace.SpanKind.SERVER, + ): + return await self.next.execute_nexus_operation_cancel(input) + + class _InputWithHeaders(Protocol): headers: Mapping[str, temporalio.api.common.v1.Payload] +class _InputWithStringHeaders(Protocol): + headers: Mapping[str, str] | None + + +class _InputWithOperationContext(Generic[_ContextT], Protocol): + ctx: _ContextT + + class _WorkflowExternFunctions(TypedDict): __temporal_opentelemetry_completed_span: Callable[ - [_CompletedWorkflowSpanParams], Optional[_CarrierDict] + [_CompletedWorkflowSpanParams], _CarrierDict | None ] @@ -311,8 +449,8 @@ class _CompletedWorkflowSpanParams: name: str attributes: opentelemetry.util.types.Attributes time_ns: int - link_context: Optional[_CarrierDict] - exception: Optional[Exception] + link_context: _CarrierDict | None + exception: Exception | None kind: opentelemetry.trace.SpanKind parent_missing: bool @@ -330,7 +468,7 @@ class TracingWorkflowInboundInterceptor(temporalio.worker.WorkflowInboundInterce """ @staticmethod - def _from_context() -> Optional[TracingWorkflowInboundInterceptor]: + def _from_context() -> TracingWorkflowInboundInterceptor | None: ret = opentelemetry.context.get_value(_interceptor_context_key) if ret and isinstance(ret, TracingWorkflowInboundInterceptor): return ret @@ -349,7 +487,7 @@ def __init__(self, next: temporalio.worker.WorkflowInboundInterceptor) -> None: # TODO(cretz): Should I be using the configured one for this workflow? self.payload_converter = temporalio.converter.PayloadConverter.default # This is the context for the overall workflow, lazily created - self._workflow_context_carrier: Optional[_CarrierDict] = None + self._workflow_context_carrier: _CarrierDict | None = None def init(self, outbound: temporalio.worker.WorkflowOutboundInterceptor) -> None: """Implementation of @@ -378,7 +516,7 @@ async def handle_signal(self, input: temporalio.worker.HandleSignalInput) -> Non # Create a span in the current context for the signal and link any # header given link_context_header = input.headers.get(self.header_key) - link_context_carrier: Optional[_CarrierDict] = None + link_context_carrier: _CarrierDict | None = None if link_context_header: link_context_carrier = self.payload_converter.from_payloads( [link_context_header] @@ -400,7 +538,7 @@ async def handle_query(self, input: temporalio.worker.HandleQueryInput) -> Any: # span. context_header = input.headers.get(self.header_key) context: opentelemetry.context.Context - link_context_carrier: Optional[_CarrierDict] = None + link_context_carrier: _CarrierDict | None = None if context_header: context_carrier = self.payload_converter.from_payloads([context_header])[0] context = self.text_map_propagator.extract(context_carrier) @@ -425,7 +563,12 @@ async def handle_query(self, input: temporalio.worker.HandleQueryInput) -> Any: ) return await super().handle_query(input) finally: - opentelemetry.context.detach(token) + # In some exceptional cases this finally is executed with a + # different contextvars.Context than the one the token was created + # on. As such we do a best effort detach to avoid using a mismatched + # token. + if context is opentelemetry.context.get_current(): + opentelemetry.context.detach(token) def handle_update_validator( self, input: temporalio.worker.HandleUpdateInput @@ -434,7 +577,7 @@ def handle_update_validator( :py:meth:`temporalio.worker.WorkflowInboundInterceptor.handle_update_validator`. """ link_context_header = input.headers.get(self.header_key) - link_context_carrier: Optional[_CarrierDict] = None + link_context_carrier: _CarrierDict | None = None if link_context_header: link_context_carrier = self.payload_converter.from_payloads( [link_context_header] @@ -454,7 +597,7 @@ async def handle_update_handler( :py:meth:`temporalio.worker.WorkflowInboundInterceptor.handle_update_handler`. """ link_context_header = input.headers.get(self.header_key) - link_context_carrier: Optional[_CarrierDict] = None + link_context_carrier: _CarrierDict | None = None if link_context_header: link_context_carrier = self.payload_converter.from_payloads( [link_context_header] @@ -467,7 +610,7 @@ async def handle_update_handler( ) return await super().handle_update_handler(input) - def _load_workflow_context_carrier(self) -> Optional[_CarrierDict]: + def _load_workflow_context_carrier(self) -> _CarrierDict | None: if self._workflow_context_carrier: return self._workflow_context_carrier context_header = temporalio.workflow.info().headers.get(self.header_key) @@ -494,9 +637,10 @@ def _top_level_workflow_context( # Need to know whether completed and whether there was a fail-workflow # exception success = False - exception: Optional[Exception] = None + exception: Exception | None = None # Run under this context token = opentelemetry.context.attach(context) + try: yield None success = True @@ -513,7 +657,13 @@ def _top_level_workflow_context( exception=exception, kind=opentelemetry.trace.SpanKind.INTERNAL, ) - opentelemetry.context.detach(token) + + # In some exceptional cases this finally is executed with a + # different contextvars.Context than the one the token was created + # on. As such we do a best effort detach to avoid using a mismatched + # token. + if context is opentelemetry.context.get_current(): + opentelemetry.context.detach(token) def _context_to_headers( self, headers: Mapping[str, temporalio.api.common.v1.Payload] @@ -538,11 +688,12 @@ def _completed_span( self, span_name: str, *, - link_context_carrier: Optional[_CarrierDict] = None, - add_to_outbound: Optional[_InputWithHeaders] = None, + link_context_carrier: _CarrierDict | None = None, + add_to_outbound: _InputWithHeaders | None = None, + add_to_outbound_str: _InputWithStringHeaders | None = None, new_span_even_on_replay: bool = False, additional_attributes: opentelemetry.util.types.Attributes = None, - exception: Optional[Exception] = None, + exception: Exception | None = None, kind: opentelemetry.trace.SpanKind = opentelemetry.trace.SpanKind.INTERNAL, ) -> None: # If we are replaying and they don't want a span on replay, no span @@ -552,12 +703,14 @@ def _completed_span( # Create the span. First serialize current context to carrier. new_context_carrier: _CarrierDict = {} self.text_map_propagator.inject(new_context_carrier) + # Invoke info = temporalio.workflow.info() - attributes: Dict[str, opentelemetry.util.types.AttributeValue] = { + attributes: dict[str, opentelemetry.util.types.AttributeValue] = { "temporalWorkflowID": info.workflow_id, "temporalRunID": info.run_id, } + if additional_attributes: attributes.update(additional_attributes) updated_context_carrier = self._extern_functions[ @@ -578,10 +731,16 @@ def _completed_span( ) # Add to outbound if needed - if add_to_outbound and updated_context_carrier: - add_to_outbound.headers = self._context_carrier_to_headers( - updated_context_carrier, add_to_outbound.headers - ) + if updated_context_carrier: + if add_to_outbound: + add_to_outbound.headers = self._context_carrier_to_headers( + updated_context_carrier, add_to_outbound.headers + ) + + if add_to_outbound_str: + add_to_outbound_str.headers = _carrier_to_nexus_headers( + updated_context_carrier, add_to_outbound_str.headers + ) def _set_on_context( self, context: opentelemetry.context.Context @@ -660,42 +819,25 @@ def start_local_activity( ) return super().start_local_activity(input) + async def start_nexus_operation( + self, input: temporalio.worker.StartNexusOperationInput[Any, Any] + ) -> temporalio.workflow.NexusOperationHandle[Any]: + self.root._completed_span( + f"StartNexusOperation:{input.service}/{input.operation_name}", + kind=opentelemetry.trace.SpanKind.CLIENT, + add_to_outbound_str=input, + ) -class workflow: - """Contains static methods that are safe to call from within a workflow. - - .. warning:: - Using any other ``opentelemetry`` API could cause non-determinism. - """ - - def __init__(self) -> None: # noqa: D107 - raise NotImplementedError - - @staticmethod - def completed_span( - name: str, - *, - attributes: opentelemetry.util.types.Attributes = None, - exception: Optional[Exception] = None, - ) -> None: - """Create and end an OpenTelemetry span. - - Note, this will only create and record when the workflow is not - replaying and if there is a current span (meaning the client started a - span and this interceptor is configured on the worker and the span is on - the context). + return await super().start_nexus_operation(input) - There is currently no way to create a long-running span or to create a - span that actually spans other code. - Args: - name: Name of the span. - attributes: Attributes to set on the span if any. Workflow ID and - run ID are automatically added. - exception: Optional exception to record on the span. - """ - interceptor = TracingWorkflowInboundInterceptor._from_context() - if interceptor: - interceptor._completed_span( - name, additional_attributes=attributes, exception=exception - ) +def _carrier_to_nexus_headers( + carrier: _CarrierDict, initial: Mapping[str, str] | None = None +) -> Mapping[str, str]: + out = {**initial} if initial else {} + for k, v in carrier.items(): + if isinstance(v, list): + out[k] = ",".join(v) + else: + out[k] = v + return out diff --git a/temporalio/contrib/opentelemetry/_otel_interceptor.py b/temporalio/contrib/opentelemetry/_otel_interceptor.py new file mode 100644 index 000000000..c120fcd03 --- /dev/null +++ b/temporalio/contrib/opentelemetry/_otel_interceptor.py @@ -0,0 +1,602 @@ +"""OpenTelemetry interceptor that creates/propagates spans.""" + +from __future__ import annotations + +from collections.abc import Iterator, Mapping +from contextlib import contextmanager +from typing import ( + Any, + NoReturn, + TypeAlias, +) + +import nexusrpc.handler +import opentelemetry.baggage.propagation +import opentelemetry.context +import opentelemetry.propagators.composite +import opentelemetry.propagators.textmap +import opentelemetry.trace +import opentelemetry.trace.propagation.tracecontext +import opentelemetry.util.types +from opentelemetry.context import Context +from opentelemetry.trace import ( + Status, + StatusCode, + Tracer, + get_tracer, + get_tracer_provider, +) +from typing_extensions import Protocol + +import temporalio.activity +import temporalio.api.common.v1 +import temporalio.client +import temporalio.converter +import temporalio.worker +import temporalio.workflow +from temporalio.contrib.opentelemetry._tracer_provider import ( + ReplaySafeTracerProvider, +) +from temporalio.exceptions import ApplicationError, ApplicationErrorCategory + +# OpenTelemetry dynamically, lazily chooses its context implementation at +# runtime. When first accessed, they use pkg_resources.iter_entry_points + load. +# The load uses built-in open() which we don't allow in sandbox mode at runtime, +# only import time. Therefore if the first use of a OTel context is inside the +# sandbox, which it may be for a workflow worker, this will fail. So instead we +# eagerly reference it here to force loading at import time instead of lazily. +opentelemetry.context.get_current() + +default_text_map_propagator = opentelemetry.propagators.composite.CompositePropagator( + [ + opentelemetry.trace.propagation.tracecontext.TraceContextTextMapPropagator(), + opentelemetry.baggage.propagation.W3CBaggagePropagator(), + ] +) +"""Default text map propagator used by :py:class:`TracingInterceptor`.""" + +_CarrierDict: TypeAlias = dict[str, opentelemetry.propagators.textmap.CarrierValT] + + +def _context_to_headers( + headers: Mapping[str, temporalio.api.common.v1.Payload], +) -> Mapping[str, temporalio.api.common.v1.Payload]: + carrier: _CarrierDict = {} + default_text_map_propagator.inject(carrier) + if carrier: + headers = { + **headers, + "_tracer-data": temporalio.converter.PayloadConverter.default.to_payloads( + [carrier] + )[0], + } + return headers + + +def _context_to_nexus_headers(headers: Mapping[str, str]) -> Mapping[str, str]: + carrier: _CarrierDict = {} + default_text_map_propagator.inject(carrier) + if carrier: + out = {**headers} if headers else {} + for k, v in carrier.items(): + if isinstance(v, list): + out[k] = ",".join(v) + else: + out[k] = v + return out + else: + return headers + + +def _headers_to_context( + headers: Mapping[str, temporalio.api.common.v1.Payload], +) -> Context: + context_header = headers.get("_tracer-data") + if context_header: + context_carrier: _CarrierDict = ( + temporalio.converter.PayloadConverter.default.from_payloads( + [context_header] + )[0] + ) + + context = default_text_map_propagator.extract(context_carrier) + else: + context = opentelemetry.context.Context() + return context + + +def _nexus_headers_to_context(headers: Mapping[str, str]) -> Context: + context = default_text_map_propagator.extract(headers) + return context + + +@contextmanager +def _maybe_span( + tracer: Tracer, + name: str, + *, + add_temporal_spans: bool, + attributes: opentelemetry.util.types.Attributes, + kind: opentelemetry.trace.SpanKind, + context: Context | None = None, +) -> Iterator[None]: + if not add_temporal_spans: + yield + return + + token = opentelemetry.context.attach(context) if context else None + try: + with tracer.start_as_current_span( + name, + attributes=attributes, + kind=kind, + context=context, + set_status_on_exception=False, + ) as span: + try: + yield + except Exception as exc: + if ( + not isinstance(exc, ApplicationError) + or exc.category != ApplicationErrorCategory.BENIGN + ): + span.set_status( + Status( + status_code=StatusCode.ERROR, + description=f"{type(exc).__name__}: {exc}", + ) + ) + raise + finally: + if token and context is opentelemetry.context.get_current(): + opentelemetry.context.detach(token) + + +class OpenTelemetryInterceptor( + temporalio.client.Interceptor, temporalio.worker.Interceptor +): + """Interceptor that supports client and worker OpenTelemetry span creation + and propagation. + + .. warning:: + This class is experimental and may change in future versions. + Use with caution in production environments. + + This should be created and used for ``interceptors`` on the + :py:meth:`temporalio.client.Client.connect` call to apply to all client + calls and worker calls using that client. To only apply to workers, set as + worker creation option instead of in client. + """ + + def __init__( # type: ignore[reportMissingSuperCall] + self, + add_temporal_spans: bool = False, + ) -> None: + """Initialize a OpenTelemetry tracing interceptor.""" + self._add_temporal_spans = add_temporal_spans + + def intercept_client( + self, next: temporalio.client.OutboundInterceptor + ) -> temporalio.client.OutboundInterceptor: + """Implementation of + :py:meth:`temporalio.client.Interceptor.intercept_client`. + """ + return _TracingClientOutboundInterceptor(next, self._add_temporal_spans) + + def intercept_activity( + self, next: temporalio.worker.ActivityInboundInterceptor + ) -> temporalio.worker.ActivityInboundInterceptor: + """Implementation of + :py:meth:`temporalio.worker.Interceptor.intercept_activity`. + """ + return _TracingActivityInboundInterceptor(next, self._add_temporal_spans) + + def workflow_interceptor_class( + self, input: temporalio.worker.WorkflowInterceptorClassInput + ) -> type[_TracingWorkflowInboundInterceptor]: + """Implementation of + :py:meth:`temporalio.worker.Interceptor.workflow_interceptor_class`. + """ + provider = get_tracer_provider() + if not isinstance(provider, ReplaySafeTracerProvider): + raise ValueError( + "When using OpenTelemetryPlugin, the global trace provider must be a ReplaySafeTracerProvider. Use create_tracer_provider to create one." + ) + + class InterceptorWithState(_TracingWorkflowInboundInterceptor): + _add_temporal_spans = self._add_temporal_spans + + return InterceptorWithState + + def intercept_nexus_operation( + self, next: temporalio.worker.NexusOperationInboundInterceptor + ) -> temporalio.worker.NexusOperationInboundInterceptor: + """Implementation of + :py:meth:`temporalio.worker.Interceptor.intercept_nexus_operation`. + """ + return _TracingNexusOperationInboundInterceptor(next, self._add_temporal_spans) + + +class _TracingClientOutboundInterceptor(temporalio.client.OutboundInterceptor): + def __init__( + self, + next: temporalio.client.OutboundInterceptor, + add_temporal_spans: bool, + ) -> None: + super().__init__(next) + self._add_temporal_spans = add_temporal_spans + + async def start_workflow( + self, input: temporalio.client.StartWorkflowInput + ) -> temporalio.client.WorkflowHandle[Any, Any]: + prefix = ( + "StartWorkflow" if not input.start_signal else "SignalWithStartWorkflow" + ) + with _maybe_span( + get_tracer(__name__), + f"{prefix}:{input.workflow}", + add_temporal_spans=self._add_temporal_spans, + attributes={"temporalWorkflowID": input.id}, + kind=opentelemetry.trace.SpanKind.CLIENT, + ): + input.headers = _context_to_headers(input.headers) + return await super().start_workflow(input) + + async def query_workflow(self, input: temporalio.client.QueryWorkflowInput) -> Any: + with _maybe_span( + get_tracer(__name__), + f"QueryWorkflow:{input.query}", + add_temporal_spans=self._add_temporal_spans, + attributes={"temporalWorkflowID": input.id}, + kind=opentelemetry.trace.SpanKind.CLIENT, + ): + input.headers = _context_to_headers(input.headers) + return await super().query_workflow(input) + + async def signal_workflow( + self, input: temporalio.client.SignalWorkflowInput + ) -> None: + with _maybe_span( + get_tracer(__name__), + f"SignalWorkflow:{input.signal}", + add_temporal_spans=self._add_temporal_spans, + attributes={"temporalWorkflowID": input.id}, + kind=opentelemetry.trace.SpanKind.CLIENT, + ): + input.headers = _context_to_headers(input.headers) + return await super().signal_workflow(input) + + async def start_workflow_update( + self, input: temporalio.client.StartWorkflowUpdateInput + ) -> temporalio.client.WorkflowUpdateHandle[Any]: + with _maybe_span( + get_tracer(__name__), + f"StartWorkflowUpdate:{input.update}", + add_temporal_spans=self._add_temporal_spans, + attributes={"temporalWorkflowID": input.id}, + kind=opentelemetry.trace.SpanKind.CLIENT, + ): + input.headers = _context_to_headers(input.headers) + return await super().start_workflow_update(input) + + async def start_update_with_start_workflow( + self, input: temporalio.client.StartWorkflowUpdateWithStartInput + ) -> temporalio.client.WorkflowUpdateHandle[Any]: + attrs = { + "temporalWorkflowID": input.start_workflow_input.id, + } + if input.update_workflow_input.update_id is not None: + attrs["temporalUpdateID"] = input.update_workflow_input.update_id + + with _maybe_span( + get_tracer(__name__), + f"StartUpdateWithStartWorkflow:{input.start_workflow_input.workflow}", + add_temporal_spans=self._add_temporal_spans, + attributes=attrs, + kind=opentelemetry.trace.SpanKind.CLIENT, + ): + input.start_workflow_input.headers = _context_to_headers( + input.start_workflow_input.headers + ) + input.update_workflow_input.headers = _context_to_headers( + input.update_workflow_input.headers + ) + return await super().start_update_with_start_workflow(input) + + async def start_activity( + self, input: temporalio.client.StartActivityInput + ) -> temporalio.client.ActivityHandle[Any]: + with _maybe_span( + get_tracer(__name__), + f"StartActivity:{input.activity_type}", + add_temporal_spans=self._add_temporal_spans, + attributes={ + "temporalActivityID": input.id, + "temporalActivityType": input.activity_type, + }, + kind=opentelemetry.trace.SpanKind.CLIENT, + ): + input.headers = _context_to_headers(input.headers) + return await super().start_activity(input) + + +class _TracingActivityInboundInterceptor(temporalio.worker.ActivityInboundInterceptor): + def __init__( + self, + next: temporalio.worker.ActivityInboundInterceptor, + add_temporal_spans: bool, + ) -> None: + super().__init__(next) + self._add_temporal_spans = add_temporal_spans + + async def execute_activity( + self, input: temporalio.worker.ExecuteActivityInput + ) -> Any: + context = _headers_to_context(input.headers) + token = opentelemetry.context.attach(context) + try: + info = temporalio.activity.info() + with _maybe_span( + get_tracer(__name__), + f"RunActivity:{info.activity_type}", + add_temporal_spans=self._add_temporal_spans, + attributes={ + "temporalWorkflowID": info.workflow_id or "", + "temporalRunID": info.workflow_run_id or "", + "temporalActivityID": info.activity_id, + }, + kind=opentelemetry.trace.SpanKind.SERVER, + ): + return await super().execute_activity(input) + finally: + if context is opentelemetry.context.get_current(): + opentelemetry.context.detach(token) + + +class _TracingNexusOperationInboundInterceptor( + temporalio.worker.NexusOperationInboundInterceptor +): + def __init__( + self, + next: temporalio.worker.NexusOperationInboundInterceptor, + add_temporal_spans: bool, + ) -> None: + super().__init__(next) + self._add_temporal_spans = add_temporal_spans + + @contextmanager + def _top_level_context(self, headers: Mapping[str, str]) -> Iterator[None]: + context = _nexus_headers_to_context(headers) + token = opentelemetry.context.attach(context) + try: + yield + finally: + if context is opentelemetry.context.get_current(): + opentelemetry.context.detach(token) + + async def execute_nexus_operation_start( + self, input: temporalio.worker.ExecuteNexusOperationStartInput + ) -> ( + nexusrpc.handler.StartOperationResultSync[Any] + | nexusrpc.handler.StartOperationResultAsync + ): + with self._top_level_context(input.ctx.headers): + with _maybe_span( + get_tracer(__name__), + f"RunStartNexusOperationHandler:{input.ctx.service}/{input.ctx.operation}", + add_temporal_spans=self._add_temporal_spans, + attributes={}, + kind=opentelemetry.trace.SpanKind.SERVER, + ): + return await self.next.execute_nexus_operation_start(input) + + async def execute_nexus_operation_cancel( + self, input: temporalio.worker.ExecuteNexusOperationCancelInput + ) -> None: + with self._top_level_context(input.ctx.headers): + with _maybe_span( + get_tracer(__name__), + f"RunCancelNexusOperationHandler:{input.ctx.service}/{input.ctx.operation}", + add_temporal_spans=self._add_temporal_spans, + attributes={}, + kind=opentelemetry.trace.SpanKind.SERVER, + ): + return await self.next.execute_nexus_operation_cancel(input) + + +class _InputWithHeaders(Protocol): + headers: Mapping[str, temporalio.api.common.v1.Payload] + + +class _TracingWorkflowInboundInterceptor(temporalio.worker.WorkflowInboundInterceptor): + """Tracing interceptor for workflow calls.""" + + _add_temporal_spans: bool = False + + def __init__(self, next: temporalio.worker.WorkflowInboundInterceptor) -> None: + """Initialize a tracing workflow interceptor.""" + super().__init__(next) + + def init(self, outbound: temporalio.worker.WorkflowOutboundInterceptor) -> None: + """Implementation of + :py:meth:`temporalio.worker.WorkflowInboundInterceptor.init`. + """ + super().init( + _TracingWorkflowOutboundInterceptor(outbound, self._add_temporal_spans) + ) + + @contextmanager + def _workflow_maybe_span(self, name: str) -> Iterator[None]: + info = temporalio.workflow.info() + attributes: dict[str, opentelemetry.util.types.AttributeValue] = { + "temporalWorkflowID": info.workflow_id, + "temporalRunID": info.run_id, + } + with _maybe_span( + get_tracer(__name__), + name, + add_temporal_spans=self._add_temporal_spans, + attributes=attributes, + kind=opentelemetry.trace.SpanKind.SERVER, + ): + yield + + async def execute_workflow( + self, input: temporalio.worker.ExecuteWorkflowInput + ) -> Any: + """Implementation of + :py:meth:`temporalio.worker.WorkflowInboundInterceptor.execute_workflow`. + """ + with self._top_level_workflow_context(input): + with self._workflow_maybe_span( + f"RunWorkflow:{temporalio.workflow.info().workflow_type}" + ): + return await super().execute_workflow(input) + + async def handle_signal(self, input: temporalio.worker.HandleSignalInput) -> None: + """Implementation of + :py:meth:`temporalio.worker.WorkflowInboundInterceptor.handle_signal`. + """ + with self._top_level_workflow_context(input): + with self._workflow_maybe_span( + f"HandleSignal:{input.signal}", + ): + await super().handle_signal(input) + + async def handle_query(self, input: temporalio.worker.HandleQueryInput) -> Any: + """Implementation of + :py:meth:`temporalio.worker.WorkflowInboundInterceptor.handle_query`. + """ + with self._top_level_workflow_context(input): + with self._workflow_maybe_span( + f"HandleQuery:{input.query}", + ): + return await super().handle_query(input) + + def handle_update_validator( + self, input: temporalio.worker.HandleUpdateInput + ) -> None: + """Implementation of + :py:meth:`temporalio.worker.WorkflowInboundInterceptor.handle_update_validator`. + """ + with self._top_level_workflow_context(input): + with self._workflow_maybe_span( + f"ValidateUpdate:{input.update}", + ): + super().handle_update_validator(input) + + async def handle_update_handler( + self, input: temporalio.worker.HandleUpdateInput + ) -> Any: + """Implementation of + :py:meth:`temporalio.worker.WorkflowInboundInterceptor.handle_update_handler`. + """ + with self._top_level_workflow_context(input): + with self._workflow_maybe_span( + f"HandleUpdate:{input.update}", + ): + return await super().handle_update_handler(input) + + @contextmanager + def _top_level_workflow_context(self, input: _InputWithHeaders) -> Iterator[None]: + context = _headers_to_context(input.headers) + token = opentelemetry.context.attach(context) + try: + yield + finally: + if context is opentelemetry.context.get_current(): + opentelemetry.context.detach(token) + + +class _TracingWorkflowOutboundInterceptor( + temporalio.worker.WorkflowOutboundInterceptor +): + def __init__( + self, + next: temporalio.worker.WorkflowOutboundInterceptor, + add_temporal_spans: bool, + ) -> None: + super().__init__(next) + self._add_temporal_spans = add_temporal_spans + + @contextmanager + def _workflow_maybe_span( + self, name: str, kind: opentelemetry.trace.SpanKind + ) -> Iterator[None]: + info = temporalio.workflow.info() + attributes: dict[str, opentelemetry.util.types.AttributeValue] = { + "temporalWorkflowID": info.workflow_id, + "temporalRunID": info.run_id, + } + with _maybe_span( + get_tracer(__name__), + name, + add_temporal_spans=self._add_temporal_spans, + attributes=attributes, + kind=kind, + ): + yield + + def continue_as_new(self, input: temporalio.worker.ContinueAsNewInput) -> NoReturn: + input.headers = _context_to_headers(input.headers) + super().continue_as_new(input) + + async def signal_child_workflow( + self, input: temporalio.worker.SignalChildWorkflowInput + ) -> None: + with self._workflow_maybe_span( + f"SignalChildWorkflow:{input.signal}", + kind=opentelemetry.trace.SpanKind.SERVER, + ): + input.headers = _context_to_headers(input.headers) + await super().signal_child_workflow(input) + + async def signal_external_workflow( + self, input: temporalio.worker.SignalExternalWorkflowInput + ) -> None: + with self._workflow_maybe_span( + f"SignalExternalWorkflow:{input.signal}", + kind=opentelemetry.trace.SpanKind.CLIENT, + ): + input.headers = _context_to_headers(input.headers) + await super().signal_external_workflow(input) + + def start_activity( + self, input: temporalio.worker.StartActivityInput + ) -> temporalio.workflow.ActivityHandle: + with self._workflow_maybe_span( + f"StartActivity:{input.activity}", + kind=opentelemetry.trace.SpanKind.CLIENT, + ): + input.headers = _context_to_headers(input.headers) + return super().start_activity(input) + + async def start_child_workflow( + self, input: temporalio.worker.StartChildWorkflowInput + ) -> temporalio.workflow.ChildWorkflowHandle: + with self._workflow_maybe_span( + f"StartChildWorkflow:{input.workflow}", + kind=opentelemetry.trace.SpanKind.CLIENT, + ): + input.headers = _context_to_headers(input.headers) + return await super().start_child_workflow(input) + + def start_local_activity( + self, input: temporalio.worker.StartLocalActivityInput + ) -> temporalio.workflow.ActivityHandle: + with self._workflow_maybe_span( + f"StartActivity:{input.activity}", + kind=opentelemetry.trace.SpanKind.CLIENT, + ): + input.headers = _context_to_headers(input.headers) + return super().start_local_activity(input) + + async def start_nexus_operation( + self, input: temporalio.worker.StartNexusOperationInput[Any, Any] + ) -> temporalio.workflow.NexusOperationHandle[Any]: + with self._workflow_maybe_span( + f"StartNexusOperation:{input.service}/{input.operation_name}", + kind=opentelemetry.trace.SpanKind.CLIENT, + ): + input.headers = _context_to_nexus_headers(input.headers or {}) + return await super().start_nexus_operation(input) diff --git a/temporalio/contrib/opentelemetry/_plugin.py b/temporalio/contrib/opentelemetry/_plugin.py new file mode 100644 index 000000000..2537c1776 --- /dev/null +++ b/temporalio/contrib/opentelemetry/_plugin.py @@ -0,0 +1,55 @@ +import dataclasses + +from temporalio.contrib.opentelemetry import OpenTelemetryInterceptor +from temporalio.plugin import SimplePlugin +from temporalio.worker import WorkflowRunner +from temporalio.worker.workflow_sandbox import SandboxedWorkflowRunner + + +class OpenTelemetryPlugin(SimplePlugin): + """OpenTelemetry plugin for Temporal SDK. + + .. warning:: + This class is experimental and may change in future versions. + Use with caution in production environments. + + This plugin integrates OpenTelemetry tracing with the Temporal SDK, providing + automatic span creation for workflows, activities, and other Temporal operations. + It uses the new OpenTelemetryInterceptor implementation. + + Unlike the prior TracingInterceptor, this allows for accurate duration spans and parenting inside a workflow + using opentelemetry.trace.get_tracer() directly. + + Your tracer provider should be created with `create_tracer_provider` for it to be used within a Temporal worker. + """ + + def __init__(self, *, add_temporal_spans: bool = False): + """Initialize the OpenTelemetry plugin. + + Args: + add_temporal_spans: Whether to add additional Temporal-specific spans + for operations like StartWorkflow, RunWorkflow, etc. + """ + interceptors = [OpenTelemetryInterceptor(add_temporal_spans)] + + def workflow_runner(runner: WorkflowRunner | None) -> WorkflowRunner: + if not runner: + raise ValueError( + "No WorkflowRunner provided to the OpenTelemetry plugin." + ) + + # If in sandbox, add additional passthrough + if isinstance(runner, SandboxedWorkflowRunner): + return dataclasses.replace( + runner, + restrictions=runner.restrictions.with_passthrough_modules( + "opentelemetry" + ), + ) + return runner + + super().__init__( + "OpenTelemetryPlugin", + interceptors=interceptors, + workflow_runner=workflow_runner, + ) diff --git a/temporalio/contrib/opentelemetry/_tracer_provider.py b/temporalio/contrib/opentelemetry/_tracer_provider.py new file mode 100644 index 000000000..929f8bf27 --- /dev/null +++ b/temporalio/contrib/opentelemetry/_tracer_provider.py @@ -0,0 +1,284 @@ +from collections.abc import Iterator, Mapping, Sequence + +import opentelemetry.sdk.trace as trace_sdk +from opentelemetry.context import Context +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import ( + ConcurrentMultiSpanProcessor, + SpanLimits, + SynchronousMultiSpanProcessor, + sampling, +) +from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator +from opentelemetry.trace import ( + Link, + Span, + SpanContext, + SpanKind, + Status, + StatusCode, + Tracer, + TracerProvider, + use_span, +) +from opentelemetry.util import types +from opentelemetry.util._decorator import _agnosticcontextmanager + +from temporalio import workflow +from temporalio.contrib.opentelemetry._id_generator import TemporalIdGenerator + + +class _ReplaySafeSpan(Span): + def __init__(self, span: Span): + self._exception: BaseException | None = None + self._span = span + + def __getattr__(self, name: str) -> object: + return getattr(self._span, name) + + def end(self, end_time: int | None = None) -> None: + if workflow.in_workflow() and workflow.unsafe.is_replaying_history_events(): + # Skip ending spans during workflow replay to avoid duplicate telemetry + return + + if ( + workflow.in_workflow() + and self._exception is not None + and not workflow.is_failure_exception(self._exception) + ): + # Skip ending spans with workflow task failures. Otherwise, each failure will create its own span + # This may still occur for spans which were completed during failed workflow tasks. + return + + self._span.end(end_time=end_time) + + def get_span_context(self) -> SpanContext: + return self._span.get_span_context() + + def set_attributes(self, attributes: Mapping[str, types.AttributeValue]) -> None: + self._span.set_attributes(attributes) + + def set_attribute(self, key: str, value: types.AttributeValue) -> None: + self._span.set_attribute(key, value) + + def add_event( + self, + name: str, + attributes: types.Attributes = None, + timestamp: int | None = None, + ) -> None: + self._span.add_event(name, attributes, timestamp) + + def update_name(self, name: str) -> None: + self._span.update_name(name) + + def is_recording(self) -> bool: + return self._span.is_recording() + + def set_status( + self, status: Status | StatusCode, description: str | None = None + ) -> None: + self._span.set_status(status, description) + + def record_exception( + self, + exception: BaseException, + attributes: types.Attributes = None, + timestamp: int | None = None, + escaped: bool = False, + ) -> None: + self._exception = exception + self._span.record_exception(exception, attributes, timestamp, escaped) + + +class _ReplaySafeTracer(Tracer): # type: ignore[reportUnusedClass] # Used outside file + def __init__(self, tracer: Tracer): + self._tracer = tracer + + def start_span( + self, + name: str, + context: Context | None = None, + kind: SpanKind = SpanKind.INTERNAL, + attributes: types.Attributes = None, + links: Sequence[Link] | None = None, + start_time: int | None = None, + record_exception: bool = True, + set_status_on_exception: bool = True, + ) -> "Span": + if workflow.in_workflow() and workflow.unsafe.is_replaying_history_events(): + start_time = start_time or workflow.time_ns() + span = self._tracer.start_span( + name, + context, + kind, + attributes, + links, + start_time, + record_exception, + set_status_on_exception, + ) + return _ReplaySafeSpan(span) + + @_agnosticcontextmanager + def start_as_current_span( + self, + name: str, + context: Context | None = None, + kind: SpanKind = SpanKind.INTERNAL, + attributes: types.Attributes = None, + links: Sequence[Link] | None = None, + start_time: int | None = None, + record_exception: bool = True, + set_status_on_exception: bool = True, + end_on_exit: bool = True, + ) -> Iterator["Span"]: + if workflow.in_workflow() and workflow.unsafe.is_replaying_history_events(): + start_time = start_time or workflow.time_ns() + span = self._tracer.start_span( + name, + context, + kind, + attributes, + links, + start_time, + record_exception, + set_status_on_exception, + ) + span = _ReplaySafeSpan(span) + with use_span( + span, + end_on_exit=end_on_exit, + record_exception=record_exception, + set_status_on_exception=set_status_on_exception, + ) as span: + yield span + + +class ReplaySafeTracerProvider(TracerProvider): + """A tracer provider that is safe for use during workflow replay. + + .. warning:: + This class is experimental and may change in future versions. + Use with caution in production environments. + + This tracer provider wraps an OpenTelemetry TracerProvider and ensures + that telemetry operations are safe during workflow replay by using + replay-safe spans and tracers. + """ + + def __init__( + self, + tracer_provider: trace_sdk.TracerProvider, + id_generator: TemporalIdGenerator, + ): + """Initialize the replay-safe tracer provider. + + Args: + tracer_provider: The underlying OpenTelemetry TracerProvider to wrap. + Must use a _TemporalIdGenerator for replay safety. + + Raises: + ValueError: If the tracer provider doesn't use a _TemporalIdGenerator. + """ + if not isinstance(tracer_provider.id_generator, TemporalIdGenerator): + raise ValueError( + "ReplaySafeTracerProvider should only be used with a TemporalIdGenerator for replay safety. The given TracerProvider doesnt use one." + ) + self._id_generator = id_generator + self._tracer_provider = tracer_provider + + def add_span_processor(self, span_processor: trace_sdk.SpanProcessor) -> None: + """Add a span processor to the underlying tracer provider. + + Args: + span_processor: The span processor to add. + """ + self._tracer_provider.add_span_processor(span_processor) + + def shutdown(self) -> None: + """Shutdown the underlying tracer provider.""" + self._tracer_provider.shutdown() + + def force_flush(self, timeout_millis: int = 30000) -> bool: + """Force flush the underlying tracer provider. + + Args: + timeout_millis: Timeout in milliseconds. + + Returns: + True if flush was successful, False otherwise. + """ + return self._tracer_provider.force_flush(timeout_millis) + + def get_tracer( + self, + instrumenting_module_name: str, + instrumenting_library_version: str | None = None, + schema_url: str | None = None, + attributes: types.Attributes | None = None, + ) -> Tracer: + """Get a replay-safe tracer from the underlying provider. + + Args: + instrumenting_module_name: The name of the instrumenting module. + instrumenting_library_version: The version of the instrumenting library. + schema_url: The schema URL for the tracer. + attributes: Additional attributes for the tracer. + + Returns: + A replay-safe tracer instance. + """ + tracer = self._tracer_provider.get_tracer( + instrumenting_module_name, + instrumenting_library_version, + schema_url, + attributes, + ) + return _ReplaySafeTracer(tracer) + + def id_generator(self) -> TemporalIdGenerator: + """Gets the temporal id generator associated with this provider.""" + return self._id_generator + + +def create_tracer_provider( + sampler: sampling.Sampler | None = None, + resource: Resource | None = None, + shutdown_on_exit: bool = True, + active_span_processor: SynchronousMultiSpanProcessor + | ConcurrentMultiSpanProcessor + | None = None, + id_generator: IdGenerator | None = None, + span_limits: SpanLimits | None = None, +) -> ReplaySafeTracerProvider: + """Initialize a replay-safe tracer provider. + + .. warning:: + This function is experimental and may change in future versions. + Use with caution in production environments. + + Creates a new TracerProvider with a TemporalIdGenerator for replay safety + and wraps it in a ReplaySafeTracerProvider. + + Args: + sampler: The sampler to use for sampling spans. + resource: The resource to associate with the tracer provider. + shutdown_on_exit: Whether to shutdown the provider on exit. + active_span_processor: The active span processor to use. + id_generator: The ID generator to wrap with TemporalIdGenerator. + span_limits: The span limits to apply. + + Returns: + A replay-safe tracer provider instance. + """ + generator = TemporalIdGenerator(id_generator or RandomIdGenerator()) + provider = trace_sdk.TracerProvider( + sampler=sampler, + resource=resource, + shutdown_on_exit=shutdown_on_exit, + active_span_processor=active_span_processor, + span_limits=span_limits, + id_generator=generator, + ) + return ReplaySafeTracerProvider(provider, generator) diff --git a/temporalio/contrib/opentelemetry/workflow.py b/temporalio/contrib/opentelemetry/workflow.py new file mode 100644 index 000000000..e872979a4 --- /dev/null +++ b/temporalio/contrib/opentelemetry/workflow.py @@ -0,0 +1,53 @@ +"""OpenTelemetry workflow utilities for Temporal SDK. + +This module provides workflow-safe OpenTelemetry span creation and context +management utilities for use within Temporal workflows. All functions in +this module are designed to work correctly during workflow replay. +""" + +from __future__ import annotations + +import warnings + +import opentelemetry.util.types +from opentelemetry.trace import ( + get_tracer, +) + +from temporalio.contrib.opentelemetry import TracingWorkflowInboundInterceptor + + +def completed_span( + name: str, + *, + attributes: opentelemetry.util.types.Attributes = None, + exception: Exception | None = None, +) -> None: + """Create and end an OpenTelemetry span. + + Note, this will only create and record when the workflow is not + replaying and if there is a current span (meaning the client started a + span and this interceptor is configured on the worker and the span is on + the context). + + To create a long-running span or to create a span that actually spans other code use OpenTelemetryPlugin and opentelemetry.trace.get_tracer(). + + Args: + name: Name of the span. + attributes: Attributes to set on the span if any. Workflow ID and + run ID are automatically added. + exception: Optional exception to record on the span. + """ + if interceptor := TracingWorkflowInboundInterceptor._from_context(): + interceptor._completed_span( + name, additional_attributes=attributes, exception=exception + ) + else: + warnings.warn( + "When using OpenTelemetryPlugin, you should prefer using opentelemetry directly.", + DeprecationWarning, + ) + span = get_tracer(__name__).start_span(name, attributes=attributes) + if exception: + span.record_exception(exception) + span.end() diff --git a/temporalio/contrib/pydantic.py b/temporalio/contrib/pydantic.py index 97f1b6ac3..dd5d0e67a 100644 --- a/temporalio/contrib/pydantic.py +++ b/temporalio/contrib/pydantic.py @@ -13,8 +13,9 @@ Pydantic v1 is not supported. """ +import functools from dataclasses import dataclass -from typing import Any, Optional, Type +from typing import Any from pydantic import TypeAdapter from pydantic_core import SchemaSerializer, to_json @@ -53,17 +54,35 @@ class PydanticJSONPlainPayloadConverter(EncodingPayloadConverter): See https://docs.pydantic.dev/latest/api/standard_library_types/ """ - def __init__(self, to_json_options: Optional[ToJsonOptions] = None): - """Create a new payload converter.""" + def __init__( + self, + to_json_options: ToJsonOptions | None = None, + *, + max_cached_type_adapters: int | None = 1024, + ) -> None: + """Create a new payload converter. + + Args: + to_json_options: Options for serializing values to JSON. + max_cached_type_adapters: Maximum number of type adapters to + cache, with least-recently-used eviction. Defaults to 1024. + If ``None``, the cache is unbounded. If zero, caching is + disabled. + """ + if max_cached_type_adapters is not None and max_cached_type_adapters < 0: + raise ValueError("max_cached_type_adapters cannot be negative") self._schema_serializer = SchemaSerializer(any_schema()) self._to_json_options = to_json_options + self._type_adapter = functools.lru_cache(maxsize=max_cached_type_adapters)( + TypeAdapter + ) @property def encoding(self) -> str: """See base class.""" return "json/plain" - def to_payload(self, value: Any) -> Optional[temporalio.api.common.v1.Payload]: + def to_payload(self, value: Any) -> temporalio.api.common.v1.Payload | None: """See base class. Uses ``pydantic_core.to_json`` to serialize ``value`` to JSON. @@ -85,18 +104,32 @@ def to_payload(self, value: Any) -> Optional[temporalio.api.common.v1.Payload]: def from_payload( self, payload: temporalio.api.common.v1.Payload, - type_hint: Optional[Type] = None, + type_hint: type | None = None, ) -> Any: """See base class. Uses ``pydantic.TypeAdapter.validate_json`` to construct an instance of the type specified by ``type_hint`` from the JSON payload. + Type adapters are cached per hashable type hint; see + ``max_cached_type_adapters`` on the constructor. See https://docs.pydantic.dev/latest/api/type_adapter/#pydantic.type_adapter.TypeAdapter.validate_json. """ _type_hint = type_hint if type_hint is not None else Any - return TypeAdapter(_type_hint).validate_json(payload.data) + type_adapter: TypeAdapter[Any] + try: + type_adapter = self._type_adapter(_type_hint) + except TypeError: + # Distinguish an unhashable hint (bypass the cache) from a + # TypeError raised while constructing the adapter (re-raise). + try: + hash(_type_hint) + except TypeError: + type_adapter = TypeAdapter(_type_hint) + else: + raise + return type_adapter.validate_json(payload.data) class PydanticPayloadConverter(CompositePayloadConverter): @@ -106,9 +139,36 @@ class PydanticPayloadConverter(CompositePayloadConverter): :py:class:`PydanticJSONPlainPayloadConverter`. """ - def __init__(self, to_json_options: Optional[ToJsonOptions] = None) -> None: - """Initialize object""" - json_payload_converter = PydanticJSONPlainPayloadConverter(to_json_options) + def __init__( + self, + to_json_options: ToJsonOptions | None = None, + *, + max_cached_type_adapters: int | None = 1024, + ) -> None: + """Initialize object. + + Args: + to_json_options: Options for serializing values to JSON. + max_cached_type_adapters: Maximum number of type adapters to + cache, with least-recently-used eviction. Defaults to 1024. + If ``None``, the cache is unbounded. If zero, caching is + disabled. + + To configure this through a :py:class:`DataConverter`, use a + nullary subclass as the payload converter class:: + + class MyPayloadConverter(PydanticPayloadConverter): + def __init__(self) -> None: + super().__init__(max_cached_type_adapters=128) + + my_data_converter = DataConverter( + payload_converter_class=MyPayloadConverter + ) + """ + json_payload_converter = PydanticJSONPlainPayloadConverter( + to_json_options, + max_cached_type_adapters=max_cached_type_adapters, + ) super().__init__( *( c diff --git a/temporalio/contrib/strands/README.md b/temporalio/contrib/strands/README.md new file mode 100644 index 000000000..126f4bd95 --- /dev/null +++ b/temporalio/contrib/strands/README.md @@ -0,0 +1,452 @@ +# Strands Agents + +⚠️ **This package is currently at an experimental release stage.** ⚠️ + +This Temporal [Plugin](https://docs.temporal.io/develop/plugins-guide) allows you to run [Strands Agents](https://strandsagents.com/) inside Temporal Workflows, routing model invocations, tool calls, and MCP tool calls through Temporal Activities for durable execution, Temporal-managed retries, and timeouts. + +## Installation + +```sh +uv add temporalio[strands-agents] +``` + +## Quickstart + +`workflow.py` defines the workflow and runs the worker: + +```python +import asyncio +from datetime import timedelta + +from temporalio import workflow +from temporalio.client import Client +from temporalio.contrib.strands import StrandsPlugin, TemporalAgent +from temporalio.worker import Worker + + +@workflow.defn +class MyWorkflow: + def __init__(self) -> None: + self.agent = TemporalAgent(start_to_close_timeout=timedelta(seconds=60)) + + @workflow.run + async def run(self, prompt: str) -> str: + result = await self.agent.invoke_async(prompt) + return str(result) + + +async def main() -> None: + client = await Client.connect("localhost:7233") + worker = Worker( + client, + task_queue="strands", + workflows=[MyWorkflow], + plugins=[StrandsPlugin()], + ) + await worker.run() + + +if __name__ == "__main__": + asyncio.run(main()) +``` + +`client.py` starts the workflow: + +```python +import asyncio + +from temporalio.client import Client + +from workflow import MyWorkflow + + +async def main() -> None: + client = await Client.connect("localhost:7233") + result = await client.execute_workflow( + MyWorkflow.run, + "Hello", + id="strands-quickstart", + task_queue="strands", + ) + print(result) + + +if __name__ == "__main__": + asyncio.run(main()) +``` + +Note: Use `agent.invoke_async(message)` instead of `agent(message)`. The synchronous form spawns a worker thread, which the workflow sandbox blocks. + +## Models + +`StrandsPlugin(models=...)` takes a mapping of `name → factory`. Each factory is called lazily on first use (on the worker, outside the workflow sandbox) and the constructed model is cached for the worker's lifetime. `TemporalAgent(model="name", ...)` selects which factory to invoke and carries the activity options for that agent's model calls. If `models` is omitted, the plugin registers a single `BedrockModel()` factory under the name `"bedrock"`, matching Strands' own implicit default. + +```python +from strands.models.anthropic import AnthropicModel +from strands.models.bedrock import BedrockModel + +# workflow +@workflow.defn +class MultiModelWorkflow: + def __init__(self) -> None: + self.agent_a = TemporalAgent( + model="claude", + start_to_close_timeout=timedelta(seconds=60), + ) + self.agent_b = TemporalAgent( + model="bedrock", + start_to_close_timeout=timedelta(seconds=60), + ) + +# worker +Worker(..., plugins=[StrandsPlugin(models={ + "claude": lambda: AnthropicModel(client_args={"api_key": "..."}), + "bedrock": lambda: BedrockModel(), +})]) +``` + +Each `TemporalAgent` carries its own activity options (timeouts, retry policy, task queue, streaming topic) and dispatches to the shared model activity, which resolves the model name against the registered factories at runtime. A name not present in `models` raises `ValueError` inside the activity. + +## Retries + +`TemporalAgent` disables Strands' built-in `ModelRetryStrategy` so retries are handled exclusively by Temporal. Configure retries via `retry_policy` on `TemporalAgent`, and on the activity options accepted by `workflow.activity_as_tool`, `workflow.activity_as_hook`, and `TemporalMCPClient`: + +```python +from temporalio.common import RetryPolicy + +TemporalAgent( + start_to_close_timeout=timedelta(seconds=60), + retry_policy=RetryPolicy(maximum_attempts=3), +) +``` + +Passing `retry_strategy=...` to `TemporalAgent(...)` raises `ValueError`; remove the argument (or pass `retry_strategy=None`) and put the retry config on the activity options instead. + +## Snapshots + +`TemporalAgent.take_snapshot()` and `TemporalAgent.load_snapshot()` raise `NotImplementedError`. Temporal's event history already persists workflow state durably at a finer granularity than Strands snapshots, so calling either inside a workflow is redundant. + +## Structured Output + +Like Strands `Agent`, `TemporalAgent` supports structured output with `structured_output_model`. The plugin defaults to [`pydantic_data_converter`](../pydantic), so Pydantic types easily serialize across the activity and workflow boundary. + +```python +from pydantic import BaseModel + +class PersonInfo(BaseModel): + name: str + age: int + +@workflow.defn +class MyWorkflow: + def __init__(self) -> None: + self.agent = TemporalAgent( + start_to_close_timeout=timedelta(seconds=60), + structured_output_model=PersonInfo, + ) + + @workflow.run + async def run(self, prompt: str) -> PersonInfo: + result = await self.agent.invoke_async(prompt) + return result.structured_output +``` + +## Streaming + +To forward model chunks to external consumers, pass `streaming_topic="..."` to `TemporalAgent` and host a `WorkflowStream` on the workflow. Each `StreamEvent` is published on the named topic from inside the model activity; subscribers read via `WorkflowStreamClient`. Chunks are batched on `streaming_batch_interval` (default 100ms). + +```python +# workflow +@workflow.defn +class MyWorkflow: + def __init__(self) -> None: + self.stream = WorkflowStream() + self.agent = TemporalAgent(streaming_topic="events") + +# client +async for item in WorkflowStreamClient.create(client, workflow_id).subscribe( + ["events"], result_type=StreamEvent, +): + print(item.data) +``` + +## Tools + +Decorate non-deterministic tools with `@activity.defn`, or if you're importing tools from `strands_tools`, wrap them in a thin async function. Then, register the activity on the worker via `Worker(activities=[...])` and pass it to the agent with `workflow.activity_as_tool(activity, **options)` along with any activity options (e.g. `start_to_close_timeout`): + +```python +from strands_tools import shell +from temporalio.contrib.strands import workflow as strands_workflow + +@activity.defn +async def fetch_user(user_id: str) -> dict: + ... + +@activity.defn(name="shell") +async def shell_activity(command: str) -> dict: + return shell.shell(command=command, non_interactive=True) + +# workflow +agent = TemporalAgent( + start_to_close_timeout=timedelta(seconds=60), + tools=[ + strands_workflow.activity_as_tool(fetch_user, start_to_close_timeout=timedelta(seconds=30)), + strands_workflow.activity_as_tool(shell_activity, start_to_close_timeout=timedelta(seconds=15)), + ], +) + +# worker +Worker( + ..., + activities=[fetch_user, shell_activity], + plugins=[StrandsPlugin(models=MODELS)], +) +``` + +## Hooks + +Strands' [hook system](https://strandsagents.com/) (`strands.hooks`) lets you subscribe callbacks to events in the agent lifecycle — invocation start/end, model call before/after, tool call before/after, message added. Pass `hooks=[MyHookProvider()]` to `TemporalAgent`: every single-agent hook event fires in workflow context, so deterministic callbacks just work. + +```python +from strands.hooks import HookProvider, HookRegistry +from strands.hooks.events import AfterToolCallEvent + +class AuditHook(HookProvider): + def register_hooks(self, registry: HookRegistry) -> None: + registry.add_callback(AfterToolCallEvent, self._on_tool_call) + + def _on_tool_call(self, event: AfterToolCallEvent) -> None: + # Pure local state - deterministic across replay. + workflow.logger.info(f"tool {event.tool_use['name']} finished") + +agent = TemporalAgent(start_to_close_timeout=..., hooks=[AuditHook()]) +``` + +Callbacks run in workflow context, so they must be deterministic: no `time.time()`, `uuid.uuid4()`, or I/O — same rules as workflow code. For callbacks that need I/O (audit logging, metrics, alerting), use `workflow.activity_as_hook()` to dispatch the work as a Temporal activity: + +```python +from temporalio.contrib.strands.workflow import activity_as_hook + +@activity.defn +async def persist_tool_call(tool_name: str) -> None: + # I/O safely in an activity. + ... + +class AuditHook(HookProvider): + def register_hooks(self, registry: HookRegistry) -> None: + registry.add_callback( + AfterToolCallEvent, + activity_as_hook( + persist_tool_call, + activity_input=lambda event: event.tool_use["name"], + start_to_close_timeout=timedelta(seconds=10), + ), + ) +``` + +`activity_input` extracts serializable values from the event to pass as the activity's input. Use a dataclass or Pydantic model for multiple values. This is needed because events hold references to the `Agent`, `AgentTool` instances, etc., none of which cross the activity boundary. + +## Human-in-the-loop interrupts + +Strands offers two HITL surfaces; both work with the plugin. In each case, `agent.invoke_async()` returns `AgentResult(stop_reason="interrupt", interrupts=[...])` instead of raising. Pair this with a signal handler that supplies responses, then resume by calling `agent.invoke_async(responses)`. + +### Hook-based interrupts + +A hook on an interruptible event (e.g. `BeforeToolCallEvent`) can pause the agent by calling `event.interrupt(name, reason=...)`. The hook runs in workflow context, so it must be deterministic — no I/O. + +```python +from strands.hooks import HookProvider, HookRegistry +from strands.hooks.events import BeforeToolCallEvent + +class ApprovalHook(HookProvider): + def register_hooks(self, registry: HookRegistry) -> None: + registry.add_callback(BeforeToolCallEvent, self._gate) + + def _gate(self, event: BeforeToolCallEvent) -> None: + if event.interrupt("approval", reason="confirm delete") != "approve": + event.cancel_tool = "denied" + +@workflow.defn +class MyWorkflow: + def __init__(self) -> None: + self.agent = TemporalAgent( + start_to_close_timeout=timedelta(seconds=60), + tools=[delete_thing], + hooks=[ApprovalHook()], + ) + self._approval: str | None = None + + @workflow.signal + def approve(self, response: str) -> None: + self._approval = response + + @workflow.run + async def run(self, prompt: str) -> str: + result = await self.agent.invoke_async(prompt) + if result.stop_reason == "interrupt": + await workflow.wait_condition(lambda: self._approval is not None) + result = await self.agent.invoke_async([ + {"interruptResponse": {"interruptId": result.interrupts[0].id, "response": self._approval}} + ]) + return str(result) +``` + +### Tool-body interrupts + +A `@strands.tool` function can raise `InterruptException(Interrupt(...))` directly. The agent stops with the interrupt, the workflow handles the resume the same way as for hooks. + +```python +from strands import tool +from strands.interrupt import Interrupt, InterruptException + +@tool +def delete_thing(name: str) -> str: + raise InterruptException( + Interrupt(id=f"delete:{name}", name="approval", reason=f"delete {name}?") + ) +``` + +The same works from an `activity_as_tool`-wrapped activity. The plugin's failure converter preserves the `Interrupt` payload across the activity boundary, so `AgentResult.interrupts` is populated just like the in-workflow case: + +```python +from strands.interrupt import Interrupt, InterruptException +from temporalio.contrib.strands.workflow import activity_as_tool + +@activity.defn +async def delete_thing(name: str) -> str: + if not await policy.is_authorized(name): + raise InterruptException( + Interrupt(id=f"delete:{name}", name="approval", reason=f"delete {name}?") + ) + await storage.delete(name) + return f"deleted {name}" + +@workflow.defn +class MyWorkflow: + def __init__(self) -> None: + self.agent = TemporalAgent( + start_to_close_timeout=timedelta(seconds=60), + tools=[activity_as_tool(delete_thing, start_to_close_timeout=timedelta(seconds=10))], + ) +``` + +This relies on the plugin's failure converter, which is installed via the client's data converter. **Attach `StrandsPlugin` to the client** (not just the worker) for activity-tool interrupts to work — workers built from that client pick up the plugin automatically. + +```python +client = await Client.connect("localhost:7233", plugins=[StrandsPlugin(models=MODELS)]) +Worker(client, task_queue="strands", workflows=[MyWorkflow], activities=[delete_thing]) +``` + +## Continue-as-new + +A chat-style workflow accumulates history with every turn and will eventually hit Temporal's per-workflow history limit. `workflow.info().is_continue_as_new_suggested()` flips true once the server decides history has grown large enough; check it after each turn and hand off to a fresh run, carrying `agent.messages` as input: + +```python +from dataclasses import dataclass, field +from strands.types.content import Messages + +@dataclass +class ChatInput: + messages: Messages = field(default_factory=list) + +@workflow.defn +class ChatWorkflow: + def __init__(self) -> None: + self._pending: list[str] = [] + self._done = False + + @workflow.signal + def user_says(self, prompt: str) -> None: + self._pending.append(prompt) + + @workflow.signal + def end_chat(self) -> None: + self._done = True + + @workflow.run + async def run(self, input: ChatInput) -> None: + agent = TemporalAgent( + start_to_close_timeout=timedelta(seconds=60), + messages=list(input.messages), + ) + while True: + await workflow.wait_condition(lambda: self._pending or self._done) + if self._done: + return + await agent.invoke_async(self._pending.pop(0)) + if workflow.info().is_continue_as_new_suggested(): + workflow.continue_as_new(ChatInput(messages=agent.messages)) +``` + +## MCP + +`StrandsPlugin(mcp_clients=...)` takes a mapping of `name → MCPClient factory`, mirroring the `models=` pattern. The plugin registers per-server `{name}-call-tool` and `{name}-list-tools` activities. Workflow-side, `TemporalMCPClient(server="name")` is a pure handle: it references the server by name, discovers tools by running `{name}-list-tools`, and carries the per-call activity options. + +```python +from mcp import StdioServerParameters, stdio_client +from strands.tools.mcp.mcp_client import MCPClient +from temporalio.contrib.strands import TemporalMCPClient + +# workflow +@workflow.defn +class MyWorkflow: + def __init__(self) -> None: + echo = TemporalMCPClient(server="echo", start_to_close_timeout=timedelta(seconds=30)) + self.agent = TemporalAgent( + start_to_close_timeout=timedelta(seconds=60), + tools=[echo], + ) + +# worker +Worker( + ..., + plugins=[StrandsPlugin( + mcp_clients={ + "echo": lambda: MCPClient( + lambda: stdio_client( + StdioServerParameters(command="...", args=[...]), + ), + ), + }, + )], +) +``` + +Each factory returns a fully configured `MCPClient`, so you can pass options like `tool_filters`, `prefix`, `elicitation_callback`, or `tasks_config` to it. + +By default, `TemporalMCPClient` re-lists the server's tools (via `{name}-list-tools`) on every agent turn, so an MCP server that is restarted mid-workflow — with tools added, removed, or renamed — is picked up. To list the tools just once at the beginning of the workflow and reuse that schema for the workflow's lifetime (one fewer activity per turn), set `cache_tools=True`: + +```python +echo = TemporalMCPClient(server="echo", cache_tools=True, start_to_close_timeout=timedelta(seconds=30)) +``` + +To amortize connection setup, the `{name}-call-tool` and `{name}-list-tools` activities share a worker-process MCP connection that is opened lazily and reused across calls. The connection is disconnected after it sits idle for `mcp_connection_idle_timeout` (default 5 minutes); the timer resets on every reuse: + +```python +StrandsPlugin( + mcp_clients={"echo": lambda: MCPClient(...)}, + mcp_connection_idle_timeout=timedelta(seconds=30), +) +``` + +## Observability + +`StrandsPlugin` composes cleanly with [`OpenTelemetryPlugin`](../opentelemetry). Register `OpenTelemetryPlugin` on the client (workers built from that client pick it up automatically) and `StrandsPlugin` on the worker. You'll get OTel spans around the model, tool, and MCP activities the plugin schedules, plus any spans Strands itself emits inside `invoke_async`: + +```python +import opentelemetry.trace +from temporalio.contrib.opentelemetry import OpenTelemetryPlugin, create_tracer_provider + +opentelemetry.trace.set_tracer_provider(create_tracer_provider()) + +client = await Client.connect("localhost:7233", plugins=[OpenTelemetryPlugin()]) + +Worker( + client, + task_queue="strands", + workflows=[MyWorkflow], + plugins=[StrandsPlugin(models=MODELS)], +) +``` + +Set the tracer provider before connecting the client. See the [OpenTelemetry plugin README](../opentelemetry) for exporter setup. diff --git a/temporalio/contrib/strands/__init__.py b/temporalio/contrib/strands/__init__.py new file mode 100644 index 000000000..39a8e7401 --- /dev/null +++ b/temporalio/contrib/strands/__init__.py @@ -0,0 +1,13 @@ +"""Temporal integration for the Strands Agents SDK.""" + +from . import workflow +from ._plugin import StrandsPlugin +from ._temporal_agent import TemporalAgent +from ._temporal_mcp_client import TemporalMCPClient + +__all__ = [ + "StrandsPlugin", + "TemporalAgent", + "TemporalMCPClient", + "workflow", +] diff --git a/temporalio/contrib/strands/_failure_converter.py b/temporalio/contrib/strands/_failure_converter.py new file mode 100644 index 000000000..c387f47f4 --- /dev/null +++ b/temporalio/contrib/strands/_failure_converter.py @@ -0,0 +1,69 @@ +"""Failure converter for Strands-specific exceptions.""" + +from strands.interrupt import InterruptException +from strands.types.exceptions import ( + ContextWindowOverflowException, + MaxTokensReachedException, + SessionException, + StructuredOutputException, +) + +import temporalio.api.failure.v1 +from temporalio.converter import DefaultFailureConverter, PayloadConverter +from temporalio.exceptions import ApplicationError + +# Activity-side: when a Strands ``InterruptException`` would otherwise be +# serialized by the default converter, the ``Interrupt`` payload on +# ``exc.interrupt`` is dropped (it lives on the instance, not in the +# serialized ApplicationError). We translate to a typed ApplicationError so +# the interrupt data survives the activity boundary and the workflow side +# can rebuild a real ``Interrupt``. +STRANDS_INTERRUPT_TYPE = "StrandsInterrupt" + +# Strands' model/session exceptions that are deterministic failures (token +# limits, context overflow, structured-output validation, session I/O). They +# won't succeed on retry, so they cross the boundary as non-retryable typed +# ApplicationErrors. TemporalAgent.invoke_async rewraps these as +# StrandsWorkflowError on the workflow side so users can `except` cleanly. +_TERMINAL_EXCEPTIONS: tuple[type[BaseException], ...] = ( + MaxTokensReachedException, + ContextWindowOverflowException, + StructuredOutputException, + SessionException, +) + + +class StrandsFailureConverter(DefaultFailureConverter): + """Failure converter that preserves Strands exception payloads and retryability.""" + + def to_failure( + self, + exception: BaseException, + payload_converter: PayloadConverter, + failure: temporalio.api.failure.v1.Failure, + ) -> None: + """Translate Strands exceptions to typed ApplicationErrors.""" + if isinstance(exception, InterruptException): + super().to_failure( + ApplicationError( + f"interrupt:{exception.interrupt.name}", + exception.interrupt.to_dict(), + type=STRANDS_INTERRUPT_TYPE, + non_retryable=True, + ), + payload_converter, + failure, + ) + return + if isinstance(exception, _TERMINAL_EXCEPTIONS): + super().to_failure( + ApplicationError( + str(exception), + type=type(exception).__name__, + non_retryable=True, + ), + payload_converter, + failure, + ) + return + super().to_failure(exception, payload_converter, failure) diff --git a/temporalio/contrib/strands/_heartbeat_decorator.py b/temporalio/contrib/strands/_heartbeat_decorator.py new file mode 100644 index 000000000..7c5b9193d --- /dev/null +++ b/temporalio/contrib/strands/_heartbeat_decorator.py @@ -0,0 +1,38 @@ +import asyncio +from collections.abc import Awaitable, Callable +from functools import wraps +from typing import Any, TypeVar, cast + +from temporalio import activity + +F = TypeVar("F", bound=Callable[..., Awaitable[Any]]) + + +def auto_heartbeater(fn: F) -> F: + """Decorator that heartbeats at half the activity's heartbeat timeout.""" + + @wraps(fn) + async def wrapper(*args: Any, **kwargs: Any) -> Any: + heartbeat_timeout = activity.info().heartbeat_timeout + heartbeat_task = None + if heartbeat_timeout: + heartbeat_task = asyncio.create_task( + _heartbeat_every(heartbeat_timeout.total_seconds() / 2) + ) + try: + return await fn(*args, **kwargs) + finally: + if heartbeat_task: + heartbeat_task.cancel() + try: + await heartbeat_task + except asyncio.CancelledError: + pass + + return cast(F, wrapper) + + +async def _heartbeat_every(delay: float) -> None: + while True: + await asyncio.sleep(delay) + activity.heartbeat() diff --git a/temporalio/contrib/strands/_model_activity.py b/temporalio/contrib/strands/_model_activity.py new file mode 100644 index 000000000..fba30658d --- /dev/null +++ b/temporalio/contrib/strands/_model_activity.py @@ -0,0 +1,106 @@ +from collections.abc import AsyncIterable, Callable +from dataclasses import dataclass, field +from datetime import timedelta +from typing import Any + +from strands.models import Model +from strands.types.streaming import StreamEvent + +from temporalio import activity +from temporalio.contrib.strands._heartbeat_decorator import auto_heartbeater +from temporalio.contrib.workflow_streams import WorkflowStreamClient + + +# Fields are typed as Any because strands TypedDicts (Message, ToolSpec) use +# NotRequired, which Python < 3.11's get_type_hints leaks through unchanged +# and the default JSON converter then fails to deserialize. Values flow +# through unchanged to ``Model.stream`` which accepts the raw dicts. +@dataclass +class _InvokeModelInput: + model_name: str | None + messages: Any + invocation_state: dict[str, Any] = field(default_factory=dict) + tool_specs: Any = None + system_prompt: str | None = None + tool_choice: Any = None + system_prompt_content: Any = None + + +@dataclass +class _StreamingInvokeModelInput(_InvokeModelInput): + streaming_topic: str = "" + streaming_batch_interval_seconds: float = 0.1 + + +class ModelActivity: + """Holds the registered model factories and exposes the model activities.""" + + def __init__( + self, + factories: dict[str, Callable[[], Model]], + *, + default_name: str | None = None, + ) -> None: + """Store the factories; models are constructed lazily on first use. + + ``default_name`` is set only by the plugin's own auto-registered + ``BedrockModel`` default. User-supplied ``models`` leave it ``None``, + which forces every ``TemporalAgent`` to specify ``model=`` explicitly. + """ + self._factories = factories + self._default_name = default_name + self._models: dict[str, Model] = {} + + def _get_model(self, name: str | None) -> Model: + if name is None: + if self._default_name is None: + raise ValueError( + f"TemporalAgent was constructed without an explicit `model`, " + f"but the plugin was configured with user-supplied `models=`. " + f"Pass model='...' to TemporalAgent. " + f"Known: {sorted(self._factories)}" + ) + name = self._default_name + if name not in self._models: + if name not in self._factories: + raise ValueError( + f"Unknown model name {name!r}. Known: {sorted(self._factories)}" + ) + self._models[name] = self._factories[name]() + return self._models[name] + + @activity.defn + @auto_heartbeater + async def invoke_model(self, input: _InvokeModelInput) -> list[StreamEvent]: + """Run the named model and return its stream events as a list.""" + model = self._get_model(input.model_name) + return [event async for event in _stream(model, input)] + + @activity.defn + @auto_heartbeater + async def invoke_model_streaming( + self, input: _StreamingInvokeModelInput + ) -> list[StreamEvent]: + """Run the named model and publish each stream event to a WorkflowStream.""" + model = self._get_model(input.model_name) + events: list[StreamEvent] = [] + stream = WorkflowStreamClient.from_within_activity( + batch_interval=timedelta(seconds=input.streaming_batch_interval_seconds), + ) + topic = stream.topic(input.streaming_topic) + async with stream: + async for event in _stream(model, input): + events.append(event) + topic.publish(event) + return events + + +def _stream(model: Model, input: _InvokeModelInput) -> AsyncIterable[StreamEvent]: + return model.stream( + input.messages, + input.tool_specs, + input.system_prompt, + tool_choice=input.tool_choice, + system_prompt_content=input.system_prompt_content, + invocation_state=input.invocation_state, + ) diff --git a/temporalio/contrib/strands/_plugin.py b/temporalio/contrib/strands/_plugin.py new file mode 100644 index 000000000..0f1972666 --- /dev/null +++ b/temporalio/contrib/strands/_plugin.py @@ -0,0 +1,123 @@ +from collections.abc import AsyncGenerator, Callable +from contextlib import asynccontextmanager +from dataclasses import replace +from datetime import timedelta + +from strands.models import BedrockModel, Model +from strands.tools.mcp import MCPClient + +from temporalio.contrib.pydantic import pydantic_data_converter +from temporalio.converter import DataConverter, DefaultPayloadConverter +from temporalio.plugin import SimplePlugin +from temporalio.worker import WorkflowRunner +from temporalio.worker.workflow_sandbox import SandboxedWorkflowRunner + +from ._failure_converter import StrandsFailureConverter +from ._model_activity import ModelActivity +from ._temporal_mcp_client import ( + _evict_connection, + build_call_tool_activity, + build_list_tools_activity, +) + + +class StrandsPlugin(SimplePlugin): + """Temporal Worker plugin for the Strands Agents SDK. + + When ``models`` is supplied, registers a single pair of model invocation + activities; each call carries the chosen ``model_name`` in its input and + the worker resolves it against the factories. Factories are called lazily + on first use, then cached for the worker's lifetime. Use the same name in + ``TemporalAgent(model=...)`` inside the workflow. + + When ``mcp_clients`` is supplied, registers per-server + ``{server}-call-tool`` and ``{server}-list-tools`` activities for each + entry. Workflow-side ``TemporalMCPClient(server="...")`` discovers tools by + running ``{server}-list-tools``; whether it lists once per workflow or once + per agent turn is controlled by its ``cache_tools`` option. + + ``mcp_connection_idle_timeout`` controls how long a worker-process MCP + connection is kept open between ``call-tool`` activities before it is + disconnected; the timer resets on every reuse. Defaults to 5 minutes. + """ + + def __init__( + self, + *, + models: dict[str, Callable[[], Model]] | None = None, + mcp_clients: dict[str, Callable[[], MCPClient]] | None = None, + mcp_connection_idle_timeout: timedelta | None = None, + ) -> None: + """Build the plugin from optional model and MCP transport factories. + + If ``models`` is omitted, registers a single ``BedrockModel()`` factory + under the name ``"bedrock"``, matching Strands' own implicit default. + """ + default_name: str | None = None + if models is None: + models = {"bedrock": lambda: BedrockModel()} + default_name = "bedrock" + activities: list[Callable] = [] + if models: + ma = ModelActivity(models, default_name=default_name) + activities.extend([ma.invoke_model, ma.invoke_model_streaming]) + + mcp_clients = mcp_clients or {} + for server, client_factory in mcp_clients.items(): + activities.append( + build_call_tool_activity( + server, client_factory, mcp_connection_idle_timeout + ) + ) + activities.append( + build_list_tools_activity( + server, client_factory, mcp_connection_idle_timeout + ) + ) + + @asynccontextmanager + async def run_context() -> AsyncGenerator[None, None]: + try: + yield + finally: + for server in mcp_clients: + await _evict_connection(server) + + super().__init__( + "aws.StrandsPlugin", + workflow_runner=_workflow_runner, + data_converter=_data_converter, + activities=activities or None, + run_context=run_context, + ) + + +def _workflow_runner(runner: WorkflowRunner | None) -> WorkflowRunner: + if not runner: + raise ValueError("No WorkflowRunner provided to the Strands plugin.") + if isinstance(runner, SandboxedWorkflowRunner): + return replace( + runner, + restrictions=runner.restrictions.with_passthrough_modules( + "strands", + "strands_tools", + "mcp", + # ``pydantic`` is already in the SDK default passthrough; extend it + # to its compiled validation core and ``Annotated`` helper. + "pydantic_core", + "annotated_types", + ), + ) + return runner + + +def _data_converter(converter: DataConverter | None) -> DataConverter: + if ( + converter is None + or converter.payload_converter_class is DefaultPayloadConverter + ): + return replace( + pydantic_data_converter, + failure_converter_class=StrandsFailureConverter, + ) + return converter diff --git a/temporalio/contrib/strands/_temporal_activity_tool.py b/temporalio/contrib/strands/_temporal_activity_tool.py new file mode 100644 index 000000000..bb838834e --- /dev/null +++ b/temporalio/contrib/strands/_temporal_activity_tool.py @@ -0,0 +1,95 @@ +import inspect +import json +from collections.abc import Callable +from typing import Any + +from strands.interrupt import Interrupt +from strands.tools.decorator import FunctionToolMetadata +from strands.types._events import ToolInterruptEvent, ToolResultEvent +from strands.types.tools import AgentTool, ToolGenerator, ToolResult, ToolSpec, ToolUse + +from temporalio import activity, workflow +from temporalio.exceptions import ActivityError, ApplicationError + +from ._failure_converter import STRANDS_INTERRUPT_TYPE + + +class TemporalActivityTool(AgentTool): + """Strands ``AgentTool`` whose body dispatches a Temporal activity.""" + + def __init__(self, activity_fn: Callable, options: dict[str, Any]) -> None: + """Capture the target activity and the options to invoke it with.""" + super().__init__() + defn = activity._Definition.from_callable(activity_fn) + if not defn or not defn.name: + raise ValueError("activity_fn must be decorated with @activity.defn") + self._activity_name = defn.name + self._options = options + self._signature = inspect.signature(activity_fn) + spec = FunctionToolMetadata(activity_fn).extract_metadata() + spec["name"] = self._activity_name + self._spec: ToolSpec = spec + + @property + def tool_name(self) -> str: + """Name of the underlying Temporal activity.""" + return self._activity_name + + @property + def tool_spec(self) -> ToolSpec: + """Strands ToolSpec derived from the activity's signature.""" + return self._spec + + @property + def tool_type(self) -> str: + """Tool kind identifier used by Strands.""" + return "temporal_activity" + + async def stream( + self, + tool_use: ToolUse, + invocation_state: dict[str, Any], + **kwargs: Any, + ) -> ToolGenerator: + """Execute the tool by dispatching to the bound Temporal activity.""" + bound = self._signature.bind(**tool_use["input"]) + bound.apply_defaults() + positional = list(bound.arguments.values()) + try: + if not positional: + result = await workflow.execute_activity( + self._activity_name, **self._options + ) + elif len(positional) == 1: + result = await workflow.execute_activity( + self._activity_name, positional[0], **self._options + ) + else: + result = await workflow.execute_activity( + self._activity_name, args=positional, **self._options + ) + except ActivityError as e: + cause = e.__cause__ + if ( + isinstance(cause, ApplicationError) + and cause.type == STRANDS_INTERRUPT_TYPE + ): + yield ToolInterruptEvent(tool_use, [Interrupt(**cause.details[0])]) + return + raise + yield ToolResultEvent( + ToolResult( + toolUseId=tool_use["toolUseId"], + status="success", + content=[{"text": _to_text(result)}], + ) + ) + + +def _to_text(result: Any) -> str: + if isinstance(result, str): + return result + try: + return json.dumps(result) + except (TypeError, ValueError): + return str(result) diff --git a/temporalio/contrib/strands/_temporal_agent.py b/temporalio/contrib/strands/_temporal_agent.py new file mode 100644 index 000000000..c2f9f14c7 --- /dev/null +++ b/temporalio/contrib/strands/_temporal_agent.py @@ -0,0 +1,132 @@ +from datetime import timedelta +from typing import Any + +from strands import Agent +from strands.hooks import BeforeModelCallEvent, HookCallback + +from temporalio.common import Priority, RetryPolicy +from temporalio.workflow import ActivityCancellationType, VersioningIntent + +from ._temporal_mcp_client import TemporalMCPClient +from ._temporal_model import TemporalModel + +_SNAPSHOT_DISABLED = ( + "TemporalAgent disables take_snapshot()/load_snapshot(). Temporal " + "workflows already persist agent state durably via the event history at " + "a finer granularity than Strands snapshots. Remove the snapshot call " + "and rely on Temporal's durable execution instead." +) + + +class TemporalAgent(Agent): + """A Strands ``Agent`` that routes model calls through a Temporal activity. + + ``model`` is the name of a factory registered in + ``StrandsPlugin(models={...})``. The activity options apply to every model + invocation this agent makes. All other keyword arguments are forwarded to + Strands' ``Agent`` (``tools``, ``hooks``, ``system_prompt``, + ``structured_output_model``, ``messages``, etc.). + + Strands' ``retry_strategy`` is disabled; configure retries via + ``retry_policy`` here and on the activity options accepted by + ``activity_as_tool``, ``activity_as_hook``, and ``TemporalMCPClient``. + """ + + def __init__( + self, + *, + model: str | None = None, + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: Priority = Priority.default, + streaming_topic: str | None = None, + streaming_batch_interval: timedelta = timedelta(milliseconds=100), + **agent_kwargs: Any, + ) -> None: + """Build a TemporalAgent from a registered model name and activity options.""" + if agent_kwargs.get("retry_strategy") is not None: + raise ValueError( + "TemporalAgent disables Strands retries; configure retries via " + "retry_policy on TemporalAgent and on the activity options " + "passed to workflow.activity_as_tool, workflow.activity_as_hook, " + "or TemporalMCPClient. Remove retry_strategy from " + "TemporalAgent(...) or pass retry_strategy=None." + ) + agent_kwargs["retry_strategy"] = None + + temporal_model = TemporalModel( + model_name=model, + task_queue=task_queue, + schedule_to_close_timeout=schedule_to_close_timeout, + schedule_to_start_timeout=schedule_to_start_timeout, + start_to_close_timeout=start_to_close_timeout, + heartbeat_timeout=heartbeat_timeout, + retry_policy=retry_policy, + cancellation_type=cancellation_type, + versioning_intent=versioning_intent, + summary=summary, + priority=priority, + streaming_topic=streaming_topic, + streaming_batch_interval=streaming_batch_interval, + ) + super().__init__(model=temporal_model, **agent_kwargs) + + # Strands invokes ToolProvider.load_tools() once at construction on a + # separate run_async thread that has no workflow runtime, so a + # TemporalMCPClient cannot list its tools there. Instead refresh from a + # BeforeModelCallEvent hook, which runs on the workflow loop just before + # the registry is read each turn. cache_tools=True lists once (guarded + # by _fetched); cache_tools=False re-lists every turn. + for provider in self.tool_registry._tool_providers: + if isinstance(provider, TemporalMCPClient): + self.hooks.add_callback( + BeforeModelCallEvent, self._make_mcp_refresh_hook(provider) + ) + + def _make_mcp_refresh_hook( + self, provider: TemporalMCPClient + ) -> HookCallback[BeforeModelCallEvent]: + async def hook(event: BeforeModelCallEvent) -> None: + if provider._cache_tools and provider._fetched: + return + old_names = {tool.tool_name for tool in provider._tools} + await provider._refresh() + self._reconcile_mcp_tools(event, provider, old_names) + + return hook + + def _reconcile_mcp_tools( + self, + event: BeforeModelCallEvent, + provider: TemporalMCPClient, + old_names: set[str], + ) -> None: + reg = event.agent.tool_registry + new = {tool.tool_name: tool for tool in provider._tools} + # Tools the server dropped or renamed since the last listing. There is + # no public unregister, so remove them from the registry directly. + for name in old_names - set(new): + reg.registry.pop(name, None) + reg.dynamic_tools.pop(name, None) + # replace() swaps an existing tool in place (no hot-reload guard); + # register_tool() adds a newly-discovered one. + for name, tool in new.items(): + if name in reg.registry: + reg.replace(tool) + else: + reg.register_tool(tool) + + def take_snapshot(self, *_args: Any, **_kwargs: Any) -> Any: + """Disabled; Temporal's event history is the source of truth.""" + raise NotImplementedError(_SNAPSHOT_DISABLED) + + def load_snapshot(self, *_args: Any, **_kwargs: Any) -> Any: + """Disabled; Temporal's event history is the source of truth.""" + raise NotImplementedError(_SNAPSHOT_DISABLED) diff --git a/temporalio/contrib/strands/_temporal_mcp_client.py b/temporalio/contrib/strands/_temporal_mcp_client.py new file mode 100644 index 000000000..bb096956e --- /dev/null +++ b/temporalio/contrib/strands/_temporal_mcp_client.py @@ -0,0 +1,368 @@ +from __future__ import annotations + +import asyncio +from collections.abc import Callable, Sequence +from dataclasses import dataclass, field +from datetime import timedelta +from typing import Any + +from mcp import ClientSession +from mcp.types import PaginatedRequestParams, Tool +from strands.tools import ToolProvider +from strands.tools.mcp import MCPAgentTool, MCPClient +from strands.tools.mcp.mcp_types import MCPToolResult +from strands.types.tools import AgentTool + +from temporalio import activity, workflow +from temporalio.common import Priority, RetryPolicy +from temporalio.workflow import ActivityCancellationType, VersioningIntent + + +@dataclass +class _MCPToolInfo: + name: str + description: str + input_schema: dict[str, Any] + output_schema: dict[str, Any] | None = None + + +@dataclass +class _CallToolArgs: + tool_name: str + arguments: dict[str, Any] = field(default_factory=dict) + tool_use_id: str = "" + + +class TemporalMCPClient(ToolProvider): + """Workflow-side handle to an MCP server registered on the worker. + + The transport factory lives worker-side via + ``StrandsPlugin(mcp_clients={"server": lambda: ...})``. This handle carries + the server name (which selects the registered factory) and the per-call + activity options. Tool discovery runs as the ``{server}-list-tools`` + activity, dispatched from inside the workflow by ``TemporalAgent`` before + each model call. + + ``cache_tools`` controls how often that listing happens. When ``False`` + (the default) the tools are re-listed on every agent turn, so an MCP server + restarted mid-workflow (with tools added, removed, or renamed) is picked up. + When ``True`` the tools are listed once at the beginning of the workflow and + reused for its lifetime. + + Construct once at module level and pass to ``TemporalAgent(tools=[...])`` + inside the workflow. Multiple handles may reference the same server name + with different activity options. + """ + + def __init__( + self, + server: str, + *, + cache_tools: bool = False, + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: Priority = Priority.default, + ) -> None: + """Configure the server name and activity options.""" + self._server = server + self._cache_tools = cache_tools + self._tools: list[AgentTool] = [] + self._fetched = False + self._options: dict[str, Any] = { + "task_queue": task_queue, + "schedule_to_close_timeout": schedule_to_close_timeout, + "schedule_to_start_timeout": schedule_to_start_timeout, + "start_to_close_timeout": start_to_close_timeout, + "heartbeat_timeout": heartbeat_timeout, + "retry_policy": retry_policy, + "cancellation_type": cancellation_type, + "versioning_intent": versioning_intent, + "summary": summary, + "priority": priority, + } + + @property + def server(self) -> str: + """MCP server name used as the activity prefix.""" + return self._server + + async def load_tools(self, **_kwargs: Any) -> Sequence[AgentTool]: + """Return the tools fetched by the most recent ``_refresh``. + + This must stay free of any ``workflow`` API: Strands invokes it once at + ``Agent`` construction on a separate ``run_async`` thread that has no + workflow runtime. ``TemporalAgent`` populates the tools by calling + ``_refresh`` from a ``BeforeModelCallEvent`` hook before the registry is + first read. + """ + return list(self._tools) + + async def _refresh(self) -> None: + """List the server's tools via the ``{server}-list-tools`` activity. + + Runs on the workflow event loop (dispatched from ``TemporalAgent``'s + hook), so the activity result is recorded in history and replay-safe. + """ + from ._temporal_mcp_tool import TemporalMCPTool + + infos: list[_MCPToolInfo] = await workflow.execute_activity( + f"{self._server}-list-tools", + result_type=list[_MCPToolInfo], + **self._options, + ) + self._tools = [ + TemporalMCPTool(self._server, info, self._options) for info in infos + ] + self._fetched = True + + def add_consumer(self, consumer_id: Any, **_kwargs: Any) -> None: + """No-op; consumer tracking is handled by the underlying MCP client.""" + return None + + def remove_consumer(self, consumer_id: Any, **_kwargs: Any) -> None: + """No-op; consumer tracking is handled by the underlying MCP client.""" + return None + + +# Use the MCP session directly instead of MCPClient's background-thread +# helpers. Those helpers route calls through cross-loop futures that are +# unreliable on Python 3.10 when invoked from Temporal's async worker/activity +# event loops. +async def _paginate_list_tools(session: ClientSession) -> list[Tool]: + tools: list[Tool] = [] + pagination_token = None + while True: + page = await session.list_tools( + params=PaginatedRequestParams(cursor=pagination_token) + if pagination_token is not None + else None + ) + tools.extend(page.tools) + pagination_token = page.nextCursor + if pagination_token is None: + return tools + + +def _tool_infos(client: MCPClient, tools: Sequence[Tool]) -> list[_MCPToolInfo]: + """Apply the client's tool filters and project to serializable records.""" + infos: list[_MCPToolInfo] = [] + for tool in tools: + if client._prefix: + agent_tool = MCPAgentTool( + tool, client, name_override=f"{client._prefix}_{tool.name}" + ) + else: + agent_tool = MCPAgentTool(tool, client) + if not client._should_include_tool_with_filters( + agent_tool, client._tool_filters + ): + continue + infos.append( + _MCPToolInfo( + name=tool.name, + description=tool.description or "", + input_schema=tool.inputSchema, + output_schema=tool.outputSchema, + ) + ) + return infos + + +# Default for how long an idle MCP connection stays open before it is +# disconnected. The timer resets on every call that reuses the connection. +# Override per worker via ``StrandsPlugin(mcp_connection_idle_timeout=...)``. +_MCP_CONNECTION_IDLE = timedelta(minutes=5) + +# Server name -> live connection held open in the activity worker process. +# Activities run in the worker process , so this module state is shared across activity invocations on the worker +_CONNECTIONS: dict[str, _ConnectionRecord] = {} + + +class _ConnectionRecord: + """A single MCP session held open by a dedicated owner task. + + The MCP transport and ``ClientSession`` are anyio context managers whose + cancel scope is bound to the task that enters them, so they must be entered + and exited in the same task. ``_run`` owns that task for the connection's + whole lifetime; ``call_tool`` activities on the same event loop invoke + ``session.call_tool`` directly (MCP multiplexes concurrent requests by id). + """ + + def __init__( + self, + server: str, + client_factory: Callable[[], MCPClient], + idle_timeout: timedelta, + ) -> None: + loop = asyncio.get_running_loop() + self._server = server + self._idle_timeout = idle_timeout + self._stop = asyncio.Event() + self._ready: asyncio.Future[tuple[MCPClient, ClientSession]] = ( + loop.create_future() + ) + self._idle_handle: asyncio.TimerHandle | None = None + self._idle_task: asyncio.Task[None] | None = None + self._inflight = 0 + self._owner = asyncio.create_task(self._run(client_factory)) + + async def _run(self, client_factory: Callable[[], MCPClient]) -> None: + client = client_factory() + try: + async with client._transport_callable() as (read_stream, write_stream, *_): + async with ClientSession( + read_stream, + write_stream, + elicitation_callback=client._elicitation_callback, + ) as session: + await session.initialize() + self._ready.set_result((client, session)) + await self._stop.wait() + except BaseException as err: + # A failed connect should not be cached; drop it so the next call + # retries instead of awaiting a permanently rejected future. + if not self._ready.done(): + self._ready.set_exception(err) + _CONNECTIONS.pop(self._server, None) + raise + + def acquire(self) -> None: + """Mark a call in flight; pause idle eviction while calls are active.""" + self._inflight += 1 + if self._idle_handle is not None: + self._idle_handle.cancel() + self._idle_handle = None + + def release(self) -> None: + """Mark a call done; arm idle eviction once no calls remain in flight.""" + self._inflight -= 1 + # Only the record still cached under this server arms a timer; a record + # already evicted or never cached must not schedule one, or it could + # later evict a different, healthy connection for the same server. + if self._inflight == 0 and _CONNECTIONS.get(self._server) is self: + loop = asyncio.get_running_loop() + self._idle_handle = loop.call_later( + self._idle_timeout.total_seconds(), self._on_idle + ) + + def _on_idle(self) -> None: + self._idle_task = asyncio.ensure_future(self._maybe_evict()) + + async def _maybe_evict(self) -> None: + # A call may have acquired the connection between the timer firing and + # this task running; only evict if it is still idle. + if self._inflight == 0: + await _evict_connection(self._server) + + async def aclose(self) -> None: + """Signal the owner task to exit its context managers and wait for it.""" + if self._idle_handle is not None: + self._idle_handle.cancel() + self._idle_handle = None + self._stop.set() + try: + await self._owner + except BaseException: + pass + + async def session(self) -> tuple[MCPClient, ClientSession]: + """Return the live client and session, or raise the connect failure.""" + return await self._ready + + +async def get_connection( + server: str, client_factory: Callable[[], MCPClient], idle_timeout: timedelta +) -> tuple[MCPClient, ClientSession, _ConnectionRecord]: + """Return the cached session for ``server``, opening one lazily if needed. + + Concurrent first-callers dedupe onto a single connect handshake by awaiting + the same record. The returned record is acquired; the caller must + ``release()`` it once the call completes so idle eviction can resume. + """ + record = _CONNECTIONS.get(server) + if record is None: + record = _ConnectionRecord(server, client_factory, idle_timeout) + _CONNECTIONS[server] = record + record.acquire() + try: + client, session = await record.session() + except BaseException: + record.release() + raise + return client, session, record + + +async def _evict_connection(server: str) -> None: + record = _CONNECTIONS.pop(server, None) + if record is not None: + await record.aclose() + + +def build_call_tool_activity( + server: str, + client_factory: Callable[[], MCPClient], + idle_timeout: timedelta | None = None, +) -> Callable: + """Return the per-server ``{server}-call-tool`` activity for registration. + + Reuses a worker-process MCP session opened lazily through ``client_factory``. + Idle connections are disconnected after ``idle_timeout`` (defaults to + ``_MCP_CONNECTION_IDLE``). + """ + idle = idle_timeout if idle_timeout is not None else _MCP_CONNECTION_IDLE + + @activity.defn(name=f"{server}-call-tool") + async def call_tool(args: _CallToolArgs) -> MCPToolResult: + try: + client, session, record = await get_connection(server, client_factory, idle) + except Exception as err: + # Connecting failed; map to a tool error result like a call would. + return client_factory()._handle_tool_execution_error(args.tool_use_id, err) + try: + result = await session.call_tool(args.tool_name, args.arguments) + return client._handle_tool_result(args.tool_use_id, result) + except Exception as err: + # The session may be broken; drop it so the next call reconnects. + await _evict_connection(server) + return client._handle_tool_execution_error(args.tool_use_id, err) + finally: + # No more in-flight call on this connection; let idle eviction + # resume (no-op if the connection was just evicted above). + record.release() + + return call_tool + + +def build_list_tools_activity( + server: str, + client_factory: Callable[[], MCPClient], + idle_timeout: timedelta | None = None, +) -> Callable: + """Return the per-server ``{server}-list-tools`` activity for registration. + + Lists the server's tools (applying the client's tool filters) and reuses + the same lazily-opened, idle-evicted worker-process MCP session as + ``{server}-call-tool``. + """ + idle = idle_timeout if idle_timeout is not None else _MCP_CONNECTION_IDLE + + @activity.defn(name=f"{server}-list-tools") + async def list_tools() -> list[_MCPToolInfo]: + client, session, record = await get_connection(server, client_factory, idle) + try: + return _tool_infos(client, await _paginate_list_tools(session)) + except Exception: + # The session may be broken; drop it so the next call reconnects. + await _evict_connection(server) + raise + finally: + record.release() + + return list_tools diff --git a/temporalio/contrib/strands/_temporal_mcp_tool.py b/temporalio/contrib/strands/_temporal_mcp_tool.py new file mode 100644 index 000000000..885b1a7e2 --- /dev/null +++ b/temporalio/contrib/strands/_temporal_mcp_tool.py @@ -0,0 +1,65 @@ +from typing import Any + +from strands.types._events import ToolResultEvent +from strands.types.tools import AgentTool, ToolGenerator, ToolResult, ToolSpec, ToolUse + +from temporalio import workflow + +from ._temporal_mcp_client import _CallToolArgs, _MCPToolInfo + + +class TemporalMCPTool(AgentTool): + """Workflow-side stub for a single MCP tool; dispatches to an activity.""" + + def __init__( + self, + server: str, + info: _MCPToolInfo, + options: dict[str, Any], + ) -> None: + """Bind this tool to a server, its cached info, and activity options.""" + super().__init__() + self._server = server + self._info = info + self._options = options + + @property + def tool_name(self) -> str: + """Name of the underlying MCP tool.""" + return self._info.name + + @property + def tool_spec(self) -> ToolSpec: + """Strands ToolSpec built from the cached MCP tool info.""" + spec: ToolSpec = { + "name": self._info.name, + "description": self._info.description + or f"Tool which performs {self._info.name}", + "inputSchema": {"json": self._info.input_schema}, + } + if self._info.output_schema: + spec["outputSchema"] = {"json": self._info.output_schema} + return spec + + @property + def tool_type(self) -> str: + """Tool kind identifier used by Strands.""" + return "temporal_mcp" + + async def stream( + self, + tool_use: ToolUse, + invocation_state: dict[str, Any], + **kwargs: Any, + ) -> ToolGenerator: + """Execute the tool by dispatching to the per-server call-tool activity.""" + result: ToolResult = await workflow.execute_activity( + f"{self._server}-call-tool", + _CallToolArgs( + tool_name=self._info.name, + arguments=tool_use["input"], + tool_use_id=tool_use["toolUseId"], + ), + **self._options, + ) + yield ToolResultEvent(result) diff --git a/temporalio/contrib/strands/_temporal_model.py b/temporalio/contrib/strands/_temporal_model.py new file mode 100644 index 000000000..29e5c63a2 --- /dev/null +++ b/temporalio/contrib/strands/_temporal_model.py @@ -0,0 +1,148 @@ +import json +from collections.abc import AsyncIterable +from datetime import timedelta +from typing import Any + +from strands.models import Model +from strands.types.content import Messages, SystemContentBlock +from strands.types.streaming import StreamEvent +from strands.types.tools import ToolChoice, ToolSpec + +from temporalio import workflow +from temporalio.common import Priority, RetryPolicy +from temporalio.workflow import ActivityCancellationType, VersioningIntent + +from ._model_activity import ( + ModelActivity, + _InvokeModelInput, + _StreamingInvokeModelInput, +) + + +def _filter_serializable(state: dict[str, Any]) -> dict[str, Any]: + """Keep invocation_state entries that JSON-serialize; drop the rest with a debug log.""" + clean: dict[str, Any] = {} + dropped: list[str] = [] + for key, value in state.items(): + try: + json.dumps(value) + except (TypeError, ValueError): + dropped.append(key) + continue + clean[key] = value + if dropped: + workflow.logger.debug( + f"Dropping non-serializable invocation_state keys: {dropped}" + ) + return clean + + +class TemporalModel(Model): + """A Strands ``Model`` that runs ``stream()`` as a Temporal activity. + + ``model_name`` selects which factory the plugin will invoke worker-side; it + must match a key in ``StrandsPlugin(models={...})``. Construction of this + ``TemporalModel`` itself does no I/O, so it is safe to instantiate at + module level. + + When ``streaming_topic`` is set, each ``StreamEvent`` is also published to + the named topic on the workflow's + :class:`temporalio.contrib.workflow_streams.WorkflowStream` for external + consumers. + """ + + def __init__( + self, + model_name: str | None = None, + *, + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: Priority = Priority.default, + streaming_topic: str | None = None, + streaming_batch_interval: timedelta = timedelta(milliseconds=100), + ) -> None: + """Configure the model name, activity options, and streaming settings.""" + self._model_name = model_name + self._streaming_topic = streaming_topic + self._streaming_batch_interval = streaming_batch_interval + self._options: dict[str, Any] = { + "task_queue": task_queue, + "schedule_to_close_timeout": schedule_to_close_timeout, + "schedule_to_start_timeout": schedule_to_start_timeout, + "start_to_close_timeout": start_to_close_timeout, + "heartbeat_timeout": heartbeat_timeout, + "retry_policy": retry_policy, + "cancellation_type": cancellation_type, + "versioning_intent": versioning_intent, + "summary": summary, + "priority": priority, + } + + def update_config(self, **_model_config: Any) -> None: + """No-op; the real model is configured worker-side via the plugin's factories.""" + return None + + def get_config(self) -> dict[str, Any]: + """Return an empty config; configuration lives on the worker-side model.""" + return {} + + def structured_output(self, *_args: Any, **_kwargs: Any) -> Any: + """Not supported; use ``TemporalAgent(structured_output_model=...)`` instead.""" + raise NotImplementedError( + "TemporalModel.structured_output is not supported. Use " + "TemporalAgent(structured_output_model=...) which routes structured " + "output through stream() via the structured_output_tool." + ) + + async def stream( + self, + messages: Messages, + tool_specs: list[ToolSpec] | None = None, + system_prompt: str | None = None, + *, + tool_choice: ToolChoice | None = None, + system_prompt_content: list[SystemContentBlock] | None = None, + invocation_state: dict[str, Any] | None = None, + **kwargs: Any, + ) -> AsyncIterable[StreamEvent]: + """Run the model via the registered Temporal activity and yield events.""" + clean_state = _filter_serializable(invocation_state) if invocation_state else {} + if self._streaming_topic is not None: + events = await workflow.execute_activity_method( + ModelActivity.invoke_model_streaming, + _StreamingInvokeModelInput( + model_name=self._model_name, + messages=messages, + invocation_state=clean_state, + tool_specs=tool_specs, + system_prompt=system_prompt, + tool_choice=tool_choice, + system_prompt_content=system_prompt_content, + streaming_topic=self._streaming_topic, + streaming_batch_interval_seconds=self._streaming_batch_interval.total_seconds(), + ), + **self._options, + ) + else: + events = await workflow.execute_activity_method( + ModelActivity.invoke_model, + _InvokeModelInput( + model_name=self._model_name, + messages=messages, + invocation_state=clean_state, + tool_specs=tool_specs, + system_prompt=system_prompt, + tool_choice=tool_choice, + system_prompt_content=system_prompt_content, + ), + **self._options, + ) + for event in events: + yield event diff --git a/temporalio/contrib/strands/workflow.py b/temporalio/contrib/strands/workflow.py new file mode 100644 index 000000000..98dab1cbc --- /dev/null +++ b/temporalio/contrib/strands/workflow.py @@ -0,0 +1,103 @@ +"""Helpers for wiring Temporal activities into Strands' agent and hook surfaces. + +Both ``activity_as_tool`` and ``activity_as_hook`` produce workflow-side objects +that dispatch user activities via :func:`temporalio.workflow.execute_activity`, +so the I/O actually happens off the workflow. +""" + +from collections.abc import Callable +from datetime import timedelta +from typing import Any, TypeVar + +from strands.hooks import BaseHookEvent, HookCallback +from strands.types.tools import AgentTool + +from temporalio import workflow +from temporalio.common import Priority, RetryPolicy +from temporalio.workflow import ActivityCancellationType, VersioningIntent + +from ._temporal_activity_tool import TemporalActivityTool + + +def activity_as_tool( + activity_fn: Callable, + *, + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: Priority = Priority.default, +) -> AgentTool: + """Wrap a Temporal activity as a Strands tool. + + ``activity_fn`` must be decorated by ``@activity.defn``. All keyword + arguments are forwarded to ``workflow.execute_activity``. + """ + options: dict[str, Any] = { + "task_queue": task_queue, + "schedule_to_close_timeout": schedule_to_close_timeout, + "schedule_to_start_timeout": schedule_to_start_timeout, + "start_to_close_timeout": start_to_close_timeout, + "heartbeat_timeout": heartbeat_timeout, + "retry_policy": retry_policy, + "cancellation_type": cancellation_type, + "activity_id": activity_id, + "versioning_intent": versioning_intent, + "summary": summary, + "priority": priority, + } + return TemporalActivityTool(activity_fn, options) + + +TEvent = TypeVar("TEvent", bound=BaseHookEvent) + + +def activity_as_hook( + activity_fn: Callable, + *, + activity_input: Callable[[TEvent], Any], + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: Priority = Priority.default, +) -> HookCallback[TEvent]: + """Wrap a Temporal activity as a Strands hook callback. + + The returned coroutine, when registered with ``HookRegistry.add_callback``, + dispatches ``activity_fn`` as a Temporal activity each time the associated + event fires. ``activity_input`` is called with the event to produce a + serializable activity input — events themselves are not serializable, since + they hold references to the ``Agent`` and other workflow-bound objects. + All other keyword arguments are forwarded to ``workflow.execute_activity``. + """ + options: dict[str, Any] = { + "task_queue": task_queue, + "schedule_to_close_timeout": schedule_to_close_timeout, + "schedule_to_start_timeout": schedule_to_start_timeout, + "start_to_close_timeout": start_to_close_timeout, + "heartbeat_timeout": heartbeat_timeout, + "retry_policy": retry_policy, + "cancellation_type": cancellation_type, + "activity_id": activity_id, + "versioning_intent": versioning_intent, + "summary": summary, + "priority": priority, + } + + async def callback(event: TEvent) -> None: + await workflow.execute_activity(activity_fn, activity_input(event), **options) + + return callback diff --git a/temporalio/contrib/workflow_streams/README.md b/temporalio/contrib/workflow_streams/README.md new file mode 100644 index 000000000..2fb4f9485 --- /dev/null +++ b/temporalio/contrib/workflow_streams/README.md @@ -0,0 +1,33 @@ +# Temporal Workflow Streams + +> ⚠️ **This package is currently at an experimental release stage.** ⚠️ + +**Workflow Streams** is a Temporal Python SDK contrib library that gives a +Workflow a durable, offset-addressed event channel for keeping outside +observers updated on the progress of the Workflow and its Activities. +Typical uses include driving a UI for a long-running AI agent, surfacing +status during in-flight payment or order processing, and reporting progress +from data pipelines. It is not designed for ultra-low-latency applications +such as real-time voice; per-roundtrip latency is around 100ms, and cost +scales with durable batches rather than tokens. + +Under the hood the stream is built directly on Temporal's existing +message-passing primitives: Signals carry publishes, Updates serve +long-poll subscriptions, and a Query exposes the current global offset. +The library packages the boilerplate that turns those primitives into +a usable stream: batching to amortize per-event overhead, deduplication +for exactly-once delivery, topic filtering, and continue-as-new helpers +that hand stream state across Workflow runs. + +## Documentation + +📖 **The full guide lives in the Temporal documentation site:** +**[Workflow Streams — Python SDK](https://docs.temporal.io/develop/python/workflows/workflow-streams)** + +It covers installation, enabling streaming on a Workflow, publishing from +Workflows and Activities, subscribing, continue-as-new, delivery semantics, +codec and payload encoding, architecture, and caveats — with runnable code +snippets throughout. + +For runnable end-to-end examples, see the +[Workflow Streams samples](https://github.com/temporalio/samples-python/tree/main/workflow-streams). diff --git a/temporalio/contrib/workflow_streams/__init__.py b/temporalio/contrib/workflow_streams/__init__.py new file mode 100644 index 000000000..41f670f0c --- /dev/null +++ b/temporalio/contrib/workflow_streams/__init__.py @@ -0,0 +1,43 @@ +"""Workflow Streams for Temporal workflows. + +.. warning:: + This package is experimental and may change in future versions. + +The Workflow Streams contrib library gives a workflow a durable, +offset-addressed event channel built from Signals and polling Updates +with an SSE bridge. Cost scales with durable batches, not tokens. +Latency is around 100ms per roundtrip; not for ultra-low-latency voice. + +See :py:class:`WorkflowStream` for the workflow-side stream object and +:py:class:`WorkflowStreamClient` for the external client interface. +""" + +from temporalio.contrib.workflow_streams._client import WorkflowStreamClient +from temporalio.contrib.workflow_streams._stream import WorkflowStream +from temporalio.contrib.workflow_streams._topic_handle import ( + TopicHandle, + WorkflowTopicHandle, +) +from temporalio.contrib.workflow_streams._types import ( + PollInput, + PollResult, + PublishEntry, + PublisherState, + PublishInput, + WorkflowStreamItem, + WorkflowStreamState, +) + +__all__ = [ + "PollInput", + "PollResult", + "PublishEntry", + "PublishInput", + "PublisherState", + "TopicHandle", + "WorkflowStream", + "WorkflowStreamClient", + "WorkflowStreamItem", + "WorkflowStreamState", + "WorkflowTopicHandle", +] diff --git a/temporalio/contrib/workflow_streams/_client.py b/temporalio/contrib/workflow_streams/_client.py new file mode 100644 index 000000000..605bf3f03 --- /dev/null +++ b/temporalio/contrib/workflow_streams/_client.py @@ -0,0 +1,670 @@ +"""External-side client for Workflow Streams. + +Used by activities, starters, and any code with a workflow handle to +publish messages and subscribe to topics on a workflow that hosts a +:class:`WorkflowStream`. + +Each published value is turned into a :class:`Payload` via the client's +sync payload converter. The **codec chain** (e.g. encryption, compression) +is **not** run per item — it runs once at the envelope +level when Temporal's SDK encodes the ``__temporal_workflow_stream_publish`` +signal args and the ``__temporal_workflow_stream_poll`` update result. +Running the codec per item as well would double-encrypt / double-compress, +because the envelope path covers the items again. The per-item +``Payload`` still carries the encoding metadata (``encoding: json/plain``, +``messageType``, etc.) required by ``subscribe(result_type=T)`` on the +consumer side. +""" + +from __future__ import annotations + +import asyncio +import time +import uuid +from collections.abc import AsyncIterator +from datetime import timedelta +from typing import Any, TypeVar, overload + +from typing_extensions import Self + +from temporalio import activity +from temporalio.api.common.v1 import Payload +from temporalio.client import ( + Client, + WorkflowExecutionDescription, + WorkflowExecutionStatus, + WorkflowHandle, + WorkflowUpdateFailedError, + WorkflowUpdateRPCTimeoutOrCancelledError, + WorkflowUpdateStage, +) +from temporalio.converter import DataConverter, PayloadConverter +from temporalio.service import RPCError, RPCStatusCode + +from ._topic_handle import TopicHandle +from ._types import ( + STREAM_DRAINING_ERROR_TYPE, + TRUNCATED_OFFSET_ERROR_TYPE, + PollInput, + PollResult, + PublishEntry, + PublishInput, + WorkflowStreamItem, + _decode_payload, + _encode_payload, +) + +T = TypeVar("T") + + +class WorkflowStreamClient: + """Client for publishing to and subscribing from a workflow stream. + + .. warning:: + This class is experimental and may change in future versions. + + Create via :py:meth:`create` (explicit client + workflow id), + :py:meth:`from_within_activity` (infer both from the current activity + context), or by passing a handle directly to the constructor. + + For publishing, bind a typed topic handle and use the client as + an async context manager to get automatic batching:: + + client = WorkflowStreamClient.create(temporal_client, workflow_id) + events = client.topic("events", type=MyEvent) + async with client: + events.publish(my_event) + events.publish(another_event, force_flush=True) + ... # more publishing + # Buffer is flushed automatically on context manager exit. + + For subscribing:: + + client = WorkflowStreamClient.create(temporal_client, workflow_id) + async for item in client.subscribe(["events"], result_type=MyEvent): + process(item.data) + """ + + def __init__( + self, + handle: WorkflowHandle[Any, Any], + *, + client: Client | None = None, + batch_interval: timedelta = timedelta(seconds=2), + max_batch_size: int | None = None, + max_retry_duration: timedelta = timedelta(seconds=600), + ) -> None: + """Create a stream client from a workflow handle. + + Prefer :py:meth:`create` — it enables continue-as-new following + in ``subscribe()`` and supplies the :class:`Client` needed to + reach the data converter chain. + + Args: + handle: Workflow handle to the workflow hosting the stream. + client: Temporal client whose payload converter will be used + to turn published values into ``Payload`` objects and to + decode subscriptions when ``result_type`` is set. The + codec chain is **not** applied per item (doing so would + double-encrypt — see module docstring). If ``None``, the + default payload converter is used. + batch_interval: Interval between automatic flushes. + max_batch_size: Auto-flush when buffer reaches this size. + max_retry_duration: Maximum time to retry a failed flush + before raising TimeoutError. Must be less than the + workflow's ``publisher_ttl`` (default 15 minutes) to + preserve exactly-once delivery. Default: 10 minutes. + """ + self._handle: WorkflowHandle[Any, Any] = handle + self._client: Client | None = client + self._workflow_id = handle.id + self._batch_interval = batch_interval + self._max_batch_size = max_batch_size + self._max_retry_duration = max_retry_duration + self._buffer: list[tuple[str, Any]] = [] + self._flush_event = asyncio.Event() + self._flush_task: asyncio.Task[None] | None = None + self._flush_lock = asyncio.Lock() + self._publisher_id: str = uuid.uuid4().hex[:16] + self._sequence: int = 0 + self._pending: list[PublishEntry] | None = None + self._pending_seq: int = 0 + self._pending_since: float | None = None + self._topic_types: dict[str, type[Any]] = {} + # Run id the most recent poll's update was admitted to. Captured before + # waiting for the outcome so a mid-poll continue-as-new can be detected by + # describing that specific run. None until the first poll is admitted. + self._polled_run_id: str | None = None + + @classmethod + def create( + cls, + client: Client, + workflow_id: str, + *, + batch_interval: timedelta = timedelta(seconds=2), + max_batch_size: int | None = None, + max_retry_duration: timedelta = timedelta(seconds=600), + ) -> WorkflowStreamClient: + """Create a stream client from a Temporal client and workflow ID. + + Use this when the caller has an explicit ``Client`` and + ``workflow_id`` in hand (starters, BFFs, other workflows' + activities). For code running inside an activity that targets + its own parent workflow, see :py:meth:`from_within_activity`. + + A client created through this method follows continue-as-new + chains in ``subscribe()`` and uses the client's payload + converter for per-item ``Payload`` construction. + + Args: + client: Temporal client. + workflow_id: ID of the workflow hosting the stream. + batch_interval: Interval between automatic flushes. + max_batch_size: Auto-flush when buffer reaches this size. + max_retry_duration: Maximum time to retry a failed flush + before raising TimeoutError. Default: 10 minutes. + """ + handle = client.get_workflow_handle(workflow_id) + return cls( + handle, + client=client, + batch_interval=batch_interval, + max_batch_size=max_batch_size, + max_retry_duration=max_retry_duration, + ) + + @classmethod + def from_within_activity( + cls, + *, + batch_interval: timedelta = timedelta(seconds=2), + max_batch_size: int | None = None, + max_retry_duration: timedelta = timedelta(seconds=600), + ) -> WorkflowStreamClient: + """Create a stream client targeting the current activity's parent workflow. + + Must be called from within an activity that was scheduled by a + workflow. The Temporal client and parent workflow id are taken + from the activity context. + + Standalone activities — those started directly via + :py:meth:`temporalio.client.Client.start_activity` rather than + from a workflow — have no parent workflow, so this method + raises. Use :py:meth:`create` from a standalone activity, + passing ``activity.client()`` and the target workflow id + explicitly (typically threaded through the activity's input). + + Args: + batch_interval: Interval between automatic flushes. + max_batch_size: Auto-flush when buffer reaches this size. + max_retry_duration: Maximum time to retry a failed flush + before raising TimeoutError. Default: 10 minutes. + """ + info = activity.info() + workflow_id = info.workflow_id + if workflow_id is None: + raise RuntimeError( + "from_within_activity requires an activity scheduled by a workflow; " + "this activity has no parent workflow. From a standalone " + "activity, use WorkflowStreamClient.create(activity.client(), " + "workflow_id) with the target workflow id passed in explicitly." + ) + return cls.create( + activity.client(), + workflow_id, + batch_interval=batch_interval, + max_batch_size=max_batch_size, + max_retry_duration=max_retry_duration, + ) + + async def __aenter__(self) -> Self: + """Start the background flusher task.""" + self._flush_task = asyncio.create_task(self._run_flusher()) + return self + + async def __aexit__(self, *_exc: object) -> None: + """Stop the flusher and flush any remaining buffered entries.""" + if self._flush_task: + self._flush_task.cancel() + try: + await self._flush_task + except asyncio.CancelledError: + pass + self._flush_task = None + # Drain both pending and buffer. A single _flush() processes + # either pending OR buffer, not both — so if the flusher was + # cancelled mid-signal (pending set) while the producer added + # more items (buffer non-empty), a single final flush would + # orphan the buffer. + while self._pending is not None or self._buffer: + await self._flush() + + def _publish_to_topic( + self, topic: str, value: Any, *, force_flush: bool = False + ) -> None: + """Internal publish path used by :class:`TopicHandle`. + + Not part of the public API — call + :meth:`TopicHandle.publish` instead. + """ + self._buffer.append((topic, value)) + if force_flush or ( + self._max_batch_size is not None + and len(self._buffer) >= self._max_batch_size + ): + self._flush_event.set() + + @overload + def topic(self, name: str) -> TopicHandle[Any]: ... + @overload + def topic(self, name: str, *, type: type[T]) -> TopicHandle[T]: ... + + def topic( + self, name: str, *, type: type[T] | None = None + ) -> TopicHandle[T] | TopicHandle[Any]: + """Return a typed handle for publishing to and subscribing from ``name``. + + The handle records the topic name and value type so call sites + do not have to repeat them. Each :class:`WorkflowStreamClient` + instance binds a topic name to exactly one type: a second call + with an unequal type raises ``RuntimeError``. Repeating the + same call with the same type is idempotent and returns an + equivalent handle. + + Type uniformity is checked only on this client instance — it + does not coordinate across processes. The check uses Python + equality on the type object; subtype and union-superset + relationships are not recognized. + + Omitting ``type`` (or passing ``type=typing.Any``) is the + documented escape hatch for heterogeneous topics or + dynamic-topic forwarders: the handle accepts any value, and + subscribers receive the converter's default decoded value. + Pre-built ``Payload`` values can be passed to + :meth:`TopicHandle.publish` regardless of the bound type + (zero-copy fast path) — there is no need to bind the topic to + ``Payload`` itself, and doing so would break the subscribe + path (use ``result_type=RawValue`` on + :meth:`WorkflowStreamClient.subscribe` if you need raw + payloads on a subscriber). + + Args: + name: Topic name. + type: Value type bound to this handle. Used as the + ``result_type`` when subscribing through the handle. + Defaults to ``typing.Any`` (heterogeneous topic). + + Returns: + :class:`TopicHandle` bound to ``name`` and the resolved + type. + + Raises: + RuntimeError: If ``name`` is already bound on this client + to a different type. + """ + bound: Any = Any if type is None else type + if bound is Payload: + raise RuntimeError( + "Cannot bind a topic to type=Payload: the payload converter " + "has no Payload decode path, so TopicHandle.subscribe would " + "fail. Pre-built Payload values can be passed to " + "TopicHandle.publish on any-typed handle (zero-copy fast " + "path); omit type (or pass type=typing.Any) for " + "heterogeneous topics, and subscribe via " + "WorkflowStreamClient.subscribe with result_type=RawValue " + "when raw payloads are needed." + ) + existing = self._topic_types.get(name) + if existing is not None and existing != bound: + raise RuntimeError( + f"Topic {name!r} is already bound to type {existing!r} on this " + f"client; refusing to rebind to {bound!r}. Use a single type " + f"per topic, or omit type (=typing.Any) for heterogeneous topics." + ) + self._topic_types[name] = bound + return TopicHandle(self, name, bound) + + async def flush(self) -> None: + """Flush buffered (and pending) items and wait for server confirmation. + + Returns once the items buffered at call time have been signaled to + the workflow and acknowledged by the server. Returns immediately + if there is nothing to send. + + This is in addition to the declarative ``force_flush=True`` on + :py:meth:`TopicHandle.publish` and to the automatic flush on + context-manager exit. Use this when you need a synchronization + point — proof that prior publications have reached the + server — at a moment that does not naturally correspond to a + specific event. + + Safe to call concurrently with topic-handle publishes and with + the background flusher: the flush lock serializes signal sends. + Items added concurrently after entry may piggyback on this + flush or be deferred to a subsequent one. + + Raises: + TimeoutError: If a pending batch from a prior failure cannot + be sent within ``max_retry_duration``. The pending batch + is dropped; subsequent publications use a fresh sequence. + """ + while self._pending is not None or self._buffer: + await self._flush() + + def _payload_converter(self) -> PayloadConverter: + """Return the sync payload converter for per-item encode/decode. + + Uses the configured client's payload converter when available; + otherwise falls back to the default. The codec chain + (e.g. encryption, compression) is intentionally not + invoked here — it runs once at the envelope level when the + signal/update goes over the wire. See module docstring. + """ + if self._client is not None: + return self._client.data_converter.payload_converter + return DataConverter.default.payload_converter + + def _encode_buffer(self, entries: list[tuple[str, Any]]) -> list[PublishEntry]: + """Convert buffered (topic, value) pairs to wire entries. + + Non-Payload values go through the sync payload converter so the + resulting ``Payload`` carries encoding metadata for + ``result_type=`` decode on the consumer side. Pre-built + Payloads bypass conversion. + """ + converter = self._payload_converter() + out: list[PublishEntry] = [] + for topic, value in entries: + if isinstance(value, Payload): + payload = value + else: + payload = converter.to_payloads([value])[0] + out.append(PublishEntry(topic=topic, data=_encode_payload(payload))) + return out + + async def _flush(self) -> None: + """Send buffered or pending messages to the workflow via signal. + + On failure, the pending batch and sequence are kept for retry. + Only advances the confirmed sequence on success. + """ + async with self._flush_lock: + if self._pending is not None: + # Retry path: check max_retry_duration + if ( + self._pending_since is not None + and time.monotonic() - self._pending_since + > self._max_retry_duration.total_seconds() + ): + # Advance confirmed sequence so the next batch gets + # a fresh sequence number. Without this, the next + # batch reuses pending_seq, which the workflow may + # have already accepted — causing silent dedup + # (data loss). See DropPendingFixed / + # SequenceFreshness in the design doc. + self._sequence = self._pending_seq + self._pending = None + self._pending_seq = 0 + self._pending_since = None + raise TimeoutError( + f"Flush retry exceeded max_retry_duration " + f"({self._max_retry_duration}). Pending batch dropped. " + f"If the signal was delivered, items are in the log. " + f"If not, they are lost." + ) + batch = self._pending + seq = self._pending_seq + elif self._buffer: + # New batch path. Encode before clearing the buffer so + # a payload-converter exception leaves the items in + # place for inspection or retry rather than silently + # dropping them. + batch = self._encode_buffer(self._buffer) + self._buffer = [] + seq = self._sequence + 1 + self._pending = batch + self._pending_seq = seq + self._pending_since = time.monotonic() + else: + return + + try: + # If the SDK ever exposes request_id on signal() and the + # server dedups it across CAN, pinning + # request_id=f"{publisher_id}:{seq}" here lets the + # workflow-side dedup go away. See DESIGN §"Replace + # workflow-side dedup with server-side request_id". + await self._handle.signal( + "__temporal_workflow_stream_publish", + PublishInput( + items=batch, + publisher_id=self._publisher_id, + sequence=seq, + ), + ) + # Success: advance confirmed sequence, clear pending + self._sequence = seq + self._pending = None + self._pending_seq = 0 + self._pending_since = None + except Exception: + # Pending stays set for retry on the next _flush() call + raise + + async def _run_flusher(self) -> None: + """Background task: wait for timer OR force_flush wakeup, then flush.""" + while True: + try: + await asyncio.wait_for( + self._flush_event.wait(), + timeout=self._batch_interval.total_seconds(), + ) + except asyncio.TimeoutError: + pass + self._flush_event.clear() + await self._flush() + + @overload + def subscribe( + self, + topics: str | list[str] | None = ..., + from_offset: int = ..., + *, + result_type: type[T], + poll_cooldown: timedelta = ..., + ) -> AsyncIterator[WorkflowStreamItem[T]]: ... + @overload + def subscribe( + self, + topics: str | list[str] | None = ..., + from_offset: int = ..., + *, + result_type: None = None, + poll_cooldown: timedelta = ..., + ) -> AsyncIterator[WorkflowStreamItem[Any]]: ... + + async def subscribe( + self, + topics: str | list[str] | None = None, + from_offset: int = 0, + *, + result_type: type | None = None, + poll_cooldown: timedelta = timedelta(milliseconds=100), + ) -> AsyncIterator[WorkflowStreamItem[Any]]: + """Async iterator that polls for new items. + + Automatically follows continue-as-new chains when the client + was created via :py:meth:`create`. + + Args: + topics: Topic filter. A single topic name, a list of topic + names, or None. None or empty list means all topics. + from_offset: Global offset to start reading from. + result_type: Optional target type. Each yielded + :class:`WorkflowStreamItem` has its ``data`` decoded via + the client's sync payload converter. When omitted, the + converter's default ``Any`` decoding is used (for the + stock JSON converter that means a Python primitive, + ``dict``, or ``list``). Pass + ``result_type=temporalio.common.RawValue`` for an + opaque ``RawValue`` wrapping the original + ``Payload`` — useful for heterogeneous topics where + the caller dispatches on ``Payload.metadata`` or wants + to forward the bytes without decoding. + poll_cooldown: Minimum interval between polls when caught + up (backlogs always drain at full speed). Defaults to + 100ms. Avoid ``timedelta(0)``: an idle subscriber + busy-loops, and each poll grows workflow history toward + its limit. Use 0 only in tests. + + Yields: + :class:`WorkflowStreamItem` for each matching item. + """ + if result_type is Payload: + raise RuntimeError( + "Cannot subscribe with result_type=Payload: the payload " + "converter has no Payload decode path. Omit result_type " + "for default decoding, or pass result_type=RawValue to " + "receive a RawValue wrapping the raw Payload." + ) + topic_filter: list[str] + if topics is None: + topic_filter = [] + elif isinstance(topics, str): + topic_filter = [topics] + else: + topic_filter = topics + offset = from_offset + while True: + try: + # Wait only for ACCEPTED so the handle (and the run id it was + # admitted to) is available before we block on the outcome; if + # the run continues-as-new mid-poll, result() fails but we still + # know which run to inspect. + handle = await self._handle.start_update( + "__temporal_workflow_stream_poll", + PollInput(topics=topic_filter, from_offset=offset), + wait_for_stage=WorkflowUpdateStage.ACCEPTED, + result_type=PollResult, + ) + self._polled_run_id = handle.workflow_run_id + result: PollResult = await handle.result() + except asyncio.CancelledError: + return + except WorkflowUpdateFailedError as e: + cause_type = getattr(e.cause, "type", None) + if cause_type == TRUNCATED_OFFSET_ERROR_TYPE: + # Subscriber fell behind truncation. Retry from + # offset 0 which the stream treats as "from the + # beginning of whatever exists" (i.e., from + # base_offset). + offset = 0 + continue + if cause_type == STREAM_DRAINING_ERROR_TYPE: + # Workflow is detaching for continue-as-new. Back off and + # retry; the poll lands on the successor run once the + # rollover completes. + cooldown_secs = poll_cooldown.total_seconds() + if cooldown_secs > 0: + await asyncio.sleep(cooldown_secs) + continue + if cause_type == "AcceptedUpdateCompletedWorkflow": + # Workflow returned (or continued-as-new) before + # this poll's update completed. Either follow the + # chain or exit cleanly. + if await self._follow_continue_as_new(): + continue + return + raise + except WorkflowUpdateRPCTimeoutOrCancelledError: + if await self._follow_continue_as_new(): + continue + return + except RPCError as e: + # Workflow may have completed between polls; subscribe + # exits cleanly on terminal status so callers don't + # have to wrap the iterator in error handling for the + # normal end-of-stream case. + if e.status != RPCStatusCode.NOT_FOUND: + raise + if await self._follow_continue_as_new(): + continue + if await self._workflow_in_terminal_state(): + return + raise + converter = self._payload_converter() + for wire_item in result.items: + payload = _decode_payload(wire_item.data) + data: Any = ( + converter.from_payload(payload) + if result_type is None + else converter.from_payload(payload, result_type) + ) + yield WorkflowStreamItem( + topic=wire_item.topic, + data=data, + offset=wire_item.offset, + ) + offset = result.next_offset + cooldown_secs = poll_cooldown.total_seconds() + if not result.more_ready and cooldown_secs > 0: + await asyncio.sleep(cooldown_secs) + + async def _describe_polled_run(self) -> WorkflowExecutionDescription: + """Describe the specific run the most recent poll was admitted to. + + Describing that run (rather than the latest) is what lets a + continue-as-new be detected: a rolled-over run is closed with status + CONTINUED_AS_NEW, whereas the latest run would report RUNNING. Falls + back to the latest run when no run id has been captured yet, or when no + client is available to target a specific run. + """ + if self._client is not None: + return await self._client.get_workflow_handle( + self._workflow_id, run_id=self._polled_run_id + ).describe() + return await self._handle.describe() + + async def _follow_continue_as_new(self) -> bool: + """Check if the polled run continued-as-new and re-target the handle. + + Returns True if the handle was updated (caller should retry). The + successor run id is not needed — re-targeting to an unpinned handle + makes the next poll address the latest (successor) run. + """ + if self._client is None: + return False + try: + desc = await self._describe_polled_run() + except Exception: + return False + if desc.status == WorkflowExecutionStatus.CONTINUED_AS_NEW: + self._handle = self._client.get_workflow_handle(self._workflow_id) + return True + return False + + async def _workflow_in_terminal_state(self) -> bool: + """Return True if the polled run has reached a terminal state. + + Used by ``subscribe()`` to distinguish "workflow finished — + stream is done" from "wrong workflow id" when a poll RPC + returns NOT_FOUND. + """ + try: + desc = await self._describe_polled_run() + except Exception: + return False + return desc.status in ( + WorkflowExecutionStatus.COMPLETED, + WorkflowExecutionStatus.FAILED, + WorkflowExecutionStatus.CANCELED, + WorkflowExecutionStatus.TERMINATED, + WorkflowExecutionStatus.TIMED_OUT, + ) + + async def get_offset(self) -> int: + """Query the current global offset (base_offset + log length).""" + return await self._handle.query( + "__temporal_workflow_stream_offset", result_type=int + ) diff --git a/temporalio/contrib/workflow_streams/_stream.py b/temporalio/contrib/workflow_streams/_stream.py new file mode 100644 index 000000000..ae8608c3b --- /dev/null +++ b/temporalio/contrib/workflow_streams/_stream.py @@ -0,0 +1,477 @@ +"""Workflow-side stream object for Workflow Streams. + +Instantiate :class:`WorkflowStream` once from your workflow's ``@workflow.init`` +method. The constructor registers the stream signal, update, and query +handlers on the current workflow via +:func:`temporalio.workflow.set_signal_handler`, +:func:`temporalio.workflow.set_update_handler`, and +:func:`temporalio.workflow.set_query_handler`. + +For workflows that support continue-as-new, include a +``WorkflowStreamState | None`` field on the workflow input and pass it as +``prior_state`` — it is ``None`` on fresh starts and carries accumulated +state on continue-as-new. + +Workflow-side and client-side topic handles +(:meth:`WorkflowTopicHandle.publish` and +:meth:`TopicHandle.publish`) both use the synchronous payload +converter for per-item ``Payload`` construction. The codec chain +(e.g. encryption, compression) is **not** run per item on either +side — it runs once at the envelope level when Temporal's SDK +encodes the signal/update that carries the batch. Running it per +item as well would double-encrypt, because every signal arg +already goes through the client's ``DataConverter.encode`` at +dispatch time. +""" + +from __future__ import annotations + +import sys +from collections.abc import Sequence +from datetime import timedelta +from typing import Any, Callable, NoReturn, TypeVar, overload + +from temporalio import workflow +from temporalio.api.common.v1 import Payload +from temporalio.exceptions import ApplicationError + +from ._topic_handle import WorkflowTopicHandle +from ._types import ( + STREAM_DRAINING_ERROR_TYPE, + TRUNCATED_OFFSET_ERROR_TYPE, + PollInput, + PollResult, + PublisherState, + PublishInput, + WorkflowStreamItem, + WorkflowStreamState, + _decode_payload, + _encode_payload, + _WorkflowStreamWireItem, +) + +_PUBLISH_SIGNAL = "__temporal_workflow_stream_publish" +_POLL_UPDATE = "__temporal_workflow_stream_poll" +_OFFSET_QUERY = "__temporal_workflow_stream_offset" + +_MAX_POLL_RESPONSE_BYTES = 1_000_000 + +T = TypeVar("T") + + +def _payload_wire_size(payload: Payload, topic: str) -> int: + """Approximate poll-response contribution of a single item. + + Wire form is ``_WorkflowStreamWireItem(topic, base64(proto(Payload)), offset)``. + Base64 inflates by ~4/3; we use the serialized length as a + conservative approximation. + """ + return (payload.ByteSize() * 4 + 2) // 3 + len(topic) + + +class WorkflowStream: + """Workflow-side stream object — append-only log with publish/poll handlers. + + .. warning:: + This class is experimental and may change in future versions. + + Construct once from ``@workflow.init``; the constructor registers + the stream signal, update, and query handlers on the current + workflow. Raises :class:`RuntimeError` if a ``WorkflowStream`` has + already been registered on the workflow. + + Registered handlers: + + - ``__temporal_workflow_stream_publish`` signal — external publish with dedup + - ``__temporal_workflow_stream_poll`` update — long-poll subscription + - ``__temporal_workflow_stream_offset`` query — current log length + + Note: + Because the publish handler is registered dynamically from + ``__init__``, on the activation where the stream is + constructed the publish signal can be buffered until after + class-level signal/update handlers are scheduled. Define + such handlers as ``async`` and ``await asyncio.sleep(0)`` + before reading stream state, so the publish signal is + processed first. + """ + + def __init__(self, prior_state: WorkflowStreamState | None = None) -> None: + """Initialize stream state and register workflow handlers. + + Must be called directly from the workflow's ``@workflow.init`` + method. Calls made from ``@workflow.run``, helper methods, or + signal/update/query handlers raise :class:`RuntimeError`. + + The check inspects the immediate caller's frame and requires the + function name to be ``__init__``. + + Args: + prior_state: State carried from a previous run via + :meth:`get_state` through continue-as-new, or ``None`` + on first start. + + Raises: + RuntimeError: If not called directly from a method named + ``__init__``, or if the stream signal handler is + already registered on this workflow (i.e., + ``WorkflowStream`` was instantiated twice). + + Note: + When carrying state across continue-as-new, type the + carrying field as ``WorkflowStreamState | None``, not + ``Any``. The default data converter deserializes ``Any`` + fields as plain dicts, which silently strips the + ``WorkflowStreamState`` type and breaks the new run. + """ + caller = sys._getframe(1) + caller_name = caller.f_code.co_name + if caller_name != "__init__": + raise RuntimeError( + "WorkflowStream must be constructed directly from the workflow's " + f"@workflow.init method, not from {caller_name!r}." + ) + if workflow.get_signal_handler(_PUBLISH_SIGNAL) is not None: + raise RuntimeError( + "WorkflowStream is already registered on this workflow. " + "Construct WorkflowStream(...) at most once from @workflow.init." + ) + + if prior_state is not None: + self._log: list[WorkflowStreamItem[Payload]] = [ + WorkflowStreamItem(topic=item.topic, data=_decode_payload(item.data)) + for item in prior_state.log + ] + self._base_offset: int = prior_state.base_offset + self._publishers: dict[str, PublisherState] = { + pid: PublisherState(sequence=ps.sequence, last_seen=ps.last_seen) + for pid, ps in prior_state.publishers.items() + } + else: + self._log = [] + self._base_offset = 0 + self._publishers = {} + self._detaching: bool = False + self._topic_types: dict[str, type[Any]] = {} + + workflow.set_signal_handler(_PUBLISH_SIGNAL, self._on_publish) + workflow.set_update_handler( + _POLL_UPDATE, self._on_poll, validator=self._validate_poll + ) + workflow.set_query_handler(_OFFSET_QUERY, self._on_offset) + + def _publish_to_topic(self, topic: str, value: Any) -> None: + """Internal publish path used by :class:`WorkflowTopicHandle`. + + Not part of the public API — call + :meth:`WorkflowTopicHandle.publish` instead. + """ + if isinstance(value, Payload): + payload = value + else: + payload = workflow.payload_converter().to_payloads([value])[0] + self._log.append(WorkflowStreamItem(topic=topic, data=payload)) + + @overload + def topic(self, name: str) -> WorkflowTopicHandle[Any]: ... + @overload + def topic(self, name: str, *, type: type[T]) -> WorkflowTopicHandle[T]: ... + + def topic( + self, name: str, *, type: type[T] | None = None + ) -> WorkflowTopicHandle[T] | WorkflowTopicHandle[Any]: + """Return a typed handle for publishing to ``name`` from this workflow. + + The handle records the topic name and value type so call sites + do not have to repeat them. Each :class:`WorkflowStream` + instance binds a topic name to exactly one type: a second call + with an unequal type raises ``RuntimeError``. Repeating the + same call with the same type is idempotent and returns an + equivalent handle. + + Type uniformity is checked only on this stream instance — it + does not coordinate across publishers (other workflows, + activities, external clients). The check uses Python equality + on the type object; subtype and union-superset relationships + are not recognized. + + Omitting ``type`` (or passing ``type=typing.Any``) is the + documented escape hatch for heterogeneous topics. Pre-built + ``Payload`` values can be passed to + :meth:`WorkflowTopicHandle.publish` regardless of the bound + type (zero-copy fast path) — there is no need to bind the + topic to ``Payload`` itself. + + Args: + name: Topic name. + type: Value type bound to this handle. Defaults to + ``typing.Any`` (heterogeneous topic). + + Returns: + :class:`WorkflowTopicHandle` bound to ``name`` and the + resolved type. + + Raises: + RuntimeError: If ``name`` is already bound on this stream + to a different type. + """ + bound: Any = Any if type is None else type + if bound is Payload: + raise RuntimeError( + "Cannot bind a topic to type=Payload. Pre-built Payload " + "values can be passed to WorkflowTopicHandle.publish on " + "any-typed handle (zero-copy fast path); omit type (or " + "pass type=typing.Any) for heterogeneous topics." + ) + existing = self._topic_types.get(name) + if existing is not None and existing != bound: + raise RuntimeError( + f"Topic {name!r} is already bound to type {existing!r} on this " + f"workflow stream; refusing to rebind to {bound!r}. Use a " + f"single type per topic, or omit type (=typing.Any) for " + f"heterogeneous topics." + ) + self._topic_types[name] = bound + return WorkflowTopicHandle(self, name, bound) + + def get_state( + self, *, publisher_ttl: timedelta = timedelta(seconds=900) + ) -> WorkflowStreamState: + """Return a serializable snapshot of stream state for continue-as-new. + + Drops dedup state for publishers idle longer than + ``publisher_ttl``. The TTL must exceed the + ``max_retry_duration`` of any client that may still be + retrying a failed flush. + + Args: + publisher_ttl: Duration after which an idle publisher's + dedup state is dropped. Default 15 minutes. + """ + now = workflow.now() + + active_publishers = { + pid: ps + for pid, ps in self._publishers.items() + if now - ps.last_seen < publisher_ttl + } + + return WorkflowStreamState( + log=[ + _WorkflowStreamWireItem( + topic=item.topic, data=_encode_payload(item.data) + ) + for item in self._log + ], + base_offset=self._base_offset, + publishers=active_publishers, + ) + + def detach_pollers(self) -> None: + """Release waiting pollers and reject new poll updates. + + After this call the stream's ``__temporal_workflow_stream_poll`` + update handler releases its in-flight subscribers on this run: + each waiting poll returns its current item batch (often empty) + so the consumer can either follow continue-as-new or stop, and + new polls are rejected at the validator. Publishes still land + in the in-memory log and ``get_state`` / ``continue_as_new`` + remain valid — the stream is being held open just long enough + to snapshot state and hand off to the next run. + + Call this before + ``await workflow.wait_condition(workflow.all_handlers_finished)`` + and ``workflow.continue_as_new()``. + """ + self._detaching = True + + async def continue_as_new( + self, + build_args: Callable[[WorkflowStreamState], Sequence[Any]], + *, + publisher_ttl: timedelta = timedelta(seconds=900), + ) -> NoReturn: + """Detach pollers, wait for handlers, continue-as-new with built args. + + Replaces this three-line recipe for the common case where the + only continue-as-new parameter that varies is ``args``: + + .. code-block:: python + + self.stream.detach_pollers() + await workflow.wait_condition(workflow.all_handlers_finished) + workflow.continue_as_new(args=...) + + ``build_args`` is invoked *after* pollers have been detached, + with the post-detach :class:`WorkflowStreamState` as its single + argument. The caller threads that state into whatever input + dataclass the workflow expects: + + .. code-block:: python + + await self.stream.continue_as_new(lambda state: [WorkflowInput( + items_processed=self.items_processed, + stream_state=state, + )]) + + Workflows that need to override other CAN parameters + (``task_queue``, ``retry_policy``, ``run_timeout``, etc.) should + keep using the explicit ``detach_pollers`` / ``wait_condition`` / + ``workflow.continue_as_new(...)`` recipe. + + Args: + build_args: Callable that receives the post-detach stream + state and returns the positional ``args`` for the new + run. + publisher_ttl: Forwarded to :meth:`get_state`. + + Does not return; ``workflow.continue_as_new`` raises an internal + exception that the SDK uses to close the run. + """ + self.detach_pollers() + await workflow.wait_condition(workflow.all_handlers_finished) + workflow.continue_as_new( + args=build_args(self.get_state(publisher_ttl=publisher_ttl)), + ) + + def truncate(self, up_to_offset: int) -> None: + """Discard log entries before ``up_to_offset``. + + After truncation, polls requesting an offset before the new + base will receive an ApplicationError. All global offsets + remain monotonic. + + Raises ApplicationError (not ValueError) when ``up_to_offset`` + is past the end of the log so that callers invoking this from + an update handler surface it as an update failure rather than + a workflow-task poison pill. + + Args: + up_to_offset: The global offset to truncate up to + (exclusive). Entries at offsets + ``[base_offset, up_to_offset)`` are discarded. + """ + log_index = up_to_offset - self._base_offset + if log_index <= 0: + return + if log_index > len(self._log): + raise ApplicationError( + f"Cannot truncate to offset {up_to_offset}: " + f"valid range is [{self._base_offset}, {self._base_offset + len(self._log)})", + type="TruncateOutOfRange", + ) + self._log = self._log[log_index:] + self._base_offset = up_to_offset + + def _on_publish(self, payload: PublishInput) -> None: + """Receive publications from external clients (activities, starters). + + Deduplicates using (publisher_id, sequence). If publisher_id is + set and the sequence is <= the last seen sequence for that + publisher, the entire batch is dropped as a duplicate. Batches + are atomic: the dedup decision applies to the whole batch, not + individual items. + + This block is a polyfill for missing server-side ``request_id`` + dedup across continue-as-new. If the SDK ever exposes + ``request_id`` on signals and the server dedups it across CAN, + this branch and the ``_publishers`` state become redundant. See + DESIGN §"Replace workflow-side dedup with server-side + request_id" for the migration plan. + """ + if payload.publisher_id: + existing = self._publishers.get(payload.publisher_id) + if existing is not None and payload.sequence <= existing.sequence: + return + self._publishers[payload.publisher_id] = PublisherState( + sequence=payload.sequence, + last_seen=workflow.now(), + ) + for entry in payload.items: + self._log.append( + WorkflowStreamItem(topic=entry.topic, data=_decode_payload(entry.data)) + ) + + async def _on_poll(self, payload: PollInput) -> PollResult: + """Long-poll: block until new items available or detaching, then return.""" + # Re-evaluate the predicate against current ``_base_offset`` on + # every iteration: a ``truncate()`` between this poll's arrival + # and the wait firing changes ``log_offset`` underneath us, so + # capturing it as a local would freeze the wait against stale + # state and the poll would only return when the long-poll RPC + # times out. + await workflow.wait_condition( + lambda: ( + payload.from_offset < self._base_offset + or len(self._log) > payload.from_offset - self._base_offset + or self._detaching + ), + ) + log_offset = payload.from_offset - self._base_offset + if log_offset < 0: + if payload.from_offset == 0: + # "From the beginning" — start at whatever is available. + log_offset = 0 + else: + # Subscriber had a specific position that's been + # truncated. ApplicationError fails this update (client + # gets the error) without crashing the workflow task — + # avoids a poison pill during replay. + raise ApplicationError( + f"Requested offset {payload.from_offset} has been truncated. " + f"Current base offset is {self._base_offset}.", + type=TRUNCATED_OFFSET_ERROR_TYPE, + ) + all_new = self._log[log_offset:] + if payload.topics: + topic_set = set(payload.topics) + candidates = [ + (self._base_offset + log_offset + i, item) + for i, item in enumerate(all_new) + if item.topic in topic_set + ] + else: + candidates = [ + (self._base_offset + log_offset + i, item) + for i, item in enumerate(all_new) + ] + # Cap response size to ~1MB wire bytes. + wire_items: list[_WorkflowStreamWireItem] = [] + size = 0 + more_ready = False + next_offset = self._base_offset + len(self._log) + for off, item in candidates: + item_size = _payload_wire_size(item.data, item.topic) + if size + item_size > _MAX_POLL_RESPONSE_BYTES and wire_items: + # Resume from this item on the next poll. + next_offset = off + more_ready = True + break + size += item_size + wire_items.append( + _WorkflowStreamWireItem( + topic=item.topic, data=_encode_payload(item.data), offset=off + ) + ) + return PollResult( + items=wire_items, + next_offset=next_offset, + more_ready=more_ready, + ) + + def _validate_poll(self, _payload: PollInput) -> None: + """Reject new polls when pollers are detached for continue-as-new. + + Uses the well-known ``StreamDraining`` type so a subscriber recognizes + the rollover-in-progress and retries until its poll lands on the + successor run, rather than surfacing the rejection as an error. + """ + if self._detaching: + raise ApplicationError( + "Workflow pollers are detached for continue-as-new", + type=STREAM_DRAINING_ERROR_TYPE, + ) + + def _on_offset(self) -> int: + """Return the current global offset (base_offset + log length).""" + return self._base_offset + len(self._log) diff --git a/temporalio/contrib/workflow_streams/_topic_handle.py b/temporalio/contrib/workflow_streams/_topic_handle.py new file mode 100644 index 000000000..3b94e226f --- /dev/null +++ b/temporalio/contrib/workflow_streams/_topic_handle.py @@ -0,0 +1,164 @@ +"""Typed topic handles for Workflow Streams. + +A topic handle is a thin typed view over an underlying publisher. It +carries the topic name and the value type ``T`` so call sites do not +have to repeat them on every publish, and so cross-language SDKs can +mirror the binding cleanly. + +Type-uniformity is enforced per publisher instance: each +:class:`WorkflowStreamClient` (or :class:`WorkflowStream`) maps a topic +name to exactly one bound ``T``. Re-binding the same name to an +unequal type raises ``RuntimeError``. The check uses Python equality +on the type object — primitives, dataclasses, generic aliases, and +unions all compare structurally — and intentionally does not attempt +to recognize subtype or union-superset relationships. +""" + +from __future__ import annotations + +from collections.abc import AsyncIterator +from datetime import timedelta +from typing import TYPE_CHECKING, Generic, TypeVar + +from temporalio.api.common.v1 import Payload + +from ._types import WorkflowStreamItem + +if TYPE_CHECKING: + from ._client import WorkflowStreamClient + from ._stream import WorkflowStream + +T = TypeVar("T") + + +class TopicHandle(Generic[T]): + """Client-side handle for publishing to and subscribing from a single topic. + + .. warning:: + This class is experimental and may change in future versions. + + Constructed via :meth:`WorkflowStreamClient.topic`. Publishes share + the underlying client's batching, dedup, and codec path; this + object holds only the topic name and bound type. + """ + + def __init__( + self, + client: WorkflowStreamClient, + name: str, + type: type[T], + ) -> None: + """Bind the handle to a client, topic name, and type. + + Prefer :meth:`WorkflowStreamClient.topic` over calling this + directly; the factory is what records the per-client type + binding and rejects conflicts. + """ + self._client = client + self._name = name + self._type = type + + @property + def name(self) -> str: + """The topic name this handle is bound to.""" + return self._name + + @property + def type(self) -> type[T]: + """The value type this handle is bound to.""" + return self._type + + def publish(self, value: T | Payload, *, force_flush: bool = False) -> None: + """Buffer ``value`` for publishing on this topic. + + Equivalent to the underlying client's publish path; the value + flows through the same buffer, batch interval, and dedup + sequence. + + Args: + value: Value to publish. Goes through the client's sync + payload converter at flush time. A pre-built + :class:`temporalio.api.common.v1.Payload` bypasses + conversion (zero-copy fast path), regardless of the + handle's bound type. + force_flush: If True, wake the flusher to send immediately + (fire-and-forget — does not block the caller). + """ + self._client._publish_to_topic(self._name, value, force_flush=force_flush) + + async def subscribe( + self, + from_offset: int = 0, + *, + poll_cooldown: timedelta = timedelta(milliseconds=100), + ) -> AsyncIterator[WorkflowStreamItem[T]]: + """Async iterator over items on this topic, decoded as ``T``. + + For raw ``Payload`` access, or any other decode type that + differs from the handle's bound ``T``, use + :meth:`WorkflowStreamClient.subscribe` directly with an + explicit ``result_type`` (typically + :class:`temporalio.common.RawValue`). The handle's bound + type intentionally cannot be ``Payload`` — the converter has + no Payload decode path. + + Args: + from_offset: Global offset to start reading from. + poll_cooldown: Minimum interval between polls when there + are no new items. + """ + async for item in self._client.subscribe( + [self._name], + from_offset=from_offset, + result_type=self._type, + poll_cooldown=poll_cooldown, + ): + yield item + + +class WorkflowTopicHandle(Generic[T]): + """Workflow-side handle for publishing to a single topic. + + .. warning:: + This class is experimental and may change in future versions. + + Constructed via :meth:`WorkflowStream.topic`. Has no + ``subscribe`` — workflows do not consume their own stream. + """ + + def __init__( + self, + stream: WorkflowStream, + name: str, + type: type[T], + ) -> None: + """Bind the handle to a stream, topic name, and type. + + Prefer :meth:`WorkflowStream.topic` over calling this directly; + the factory is what records the per-stream type binding and + rejects conflicts. + """ + self._stream = stream + self._name = name + self._type = type + + @property + def name(self) -> str: + """The topic name this handle is bound to.""" + return self._name + + @property + def type(self) -> type[T]: + """The value type this handle is bound to.""" + return self._type + + def publish(self, value: T | Payload) -> None: + """Append ``value`` to the workflow stream on this topic. + + Args: + value: Value to publish. Goes through the workflow's sync + payload converter. A pre-built + :class:`temporalio.api.common.v1.Payload` bypasses + conversion, regardless of the handle's bound type. + """ + self._stream._publish_to_topic(self._name, value) diff --git a/temporalio/contrib/workflow_streams/_types.py b/temporalio/contrib/workflow_streams/_types.py new file mode 100644 index 000000000..a58cf75ae --- /dev/null +++ b/temporalio/contrib/workflow_streams/_types.py @@ -0,0 +1,178 @@ +"""Shared data types for the Workflow Streams contrib module. + +The user-facing ``data`` fields on :class:`WorkflowStreamItem` are +:class:`temporalio.api.common.v1.Payload`. Per-item values are converted to +``Payload`` by the payload converter at publish time, and the resulting +bytes/metadata are preserved per item so subscribers can decode with +``subscribe(result_type=T)``. The codec chain (e.g. encryption, compression) +applies once at the outer signal/update envelope level — not separately to each +embedded item — so codec behavior is symmetric between workflow-side and +client-side publishing. + +The wire representation (``PublishEntry``, ``_WorkflowStreamWireItem``) uses +base64-encoded ``Payload.SerializeToString()`` bytes because the default JSON +payload converter cannot serialize a ``Payload`` embedded inside a dataclass +(it only special-cases top-level Payloads on signal/update args). +""" + +from __future__ import annotations + +import base64 +from dataclasses import dataclass, field +from datetime import datetime +from typing import Generic, TypeVar + +from temporalio.api.common.v1 import Payload + +T = TypeVar("T") + +# Well-known ``ApplicationError.type`` values the stream workflow uses to reject +# polls, and which ``WorkflowStreamClient.subscribe`` recognizes to drive retry +# behavior. Defined here so the raise sites (``_stream.py``) and the handling +# sites (``_client.py``) cannot diverge. +STREAM_DRAINING_ERROR_TYPE = "StreamDraining" +TRUNCATED_OFFSET_ERROR_TYPE = "TruncatedOffset" + + +# basedpyright flags _-prefixed module-level functions as unused even when +# sibling modules import them (_stream.py, _client.py). Vanilla pyright does +# not. Suppressions below are required for `poe lint`. +def _encode_payload(payload: Payload) -> str: # pyright: ignore[reportUnusedFunction] + """Wire format: base64(Payload.SerializeToString()).""" + return base64.b64encode(payload.SerializeToString()).decode("ascii") + + +def _decode_payload(wire: str) -> Payload: # pyright: ignore[reportUnusedFunction] + """Inverse of :func:`_encode_payload`.""" + payload = Payload() + payload.ParseFromString(base64.b64decode(wire)) + return payload + + +@dataclass +class WorkflowStreamItem(Generic[T]): + """A single item in the workflow stream's log. + + .. warning:: + This class is experimental and may change in future versions. + + The ``data`` field carries the decoded value produced by + :meth:`WorkflowStreamClient.subscribe`. The generic parameter ``T`` + matches the ``result_type`` passed to ``subscribe``: an instance of + ``T`` when ``result_type=T``, the converter's default ``Any`` + decoding when ``result_type`` is omitted, or a + :class:`temporalio.common.RawValue` wrapping the original + ``Payload`` when ``result_type=RawValue``. + + The ``offset`` field is populated at poll time from the item's + position in the global log. + """ + + topic: str + data: T + offset: int = 0 + + +@dataclass +class PublishEntry: + """A single entry to publish via signal (wire type). + + .. warning:: + This class is experimental and may change in future versions. + + ``data`` is base64-encoded ``Payload.SerializeToString()`` output — + see module docstring for why a nested ``Payload`` cannot be used + directly. + """ + + topic: str + data: str + + +@dataclass +class PublishInput: + """Signal payload: batch of entries to publish. + + .. warning:: + This class is experimental and may change in future versions. + + Includes publisher_id and sequence to ensure exactly-once delivery. + """ + + items: list[PublishEntry] = field(default_factory=list) + publisher_id: str = "" + sequence: int = 0 + + +@dataclass +class PollInput: + """Update payload: request to poll for new items. + + .. warning:: + This class is experimental and may change in future versions. + """ + + topics: list[str] = field(default_factory=list) + from_offset: int = 0 + + +@dataclass +class _WorkflowStreamWireItem: + """Wire representation of a WorkflowStreamItem (base64 of serialized Payload).""" + + topic: str + data: str + offset: int = 0 + + +@dataclass +class PollResult: + """Update response: items matching the poll request. + + .. warning:: + This class is experimental and may change in future versions. + + ``items`` use the wire representation. When ``more_ready`` is True, + the response was truncated to stay within size limits and the + subscriber should poll again immediately rather than applying a + cooldown delay. + """ + + items: list[_WorkflowStreamWireItem] = field(default_factory=list) + next_offset: int = 0 + more_ready: bool = False + + +@dataclass +class PublisherState: + """Per-publisher dedup state. + + .. warning:: + This class is experimental and may change in future versions. + + Tracks the last accepted ``sequence`` and the ``workflow.now()`` at + which it was accepted, used together for at-least-once dedup and + TTL-based pruning at continue-as-new time. + """ + + sequence: int + last_seen: datetime + + +@dataclass +class WorkflowStreamState: + """Serializable snapshot of stream state for continue-as-new. + + .. warning:: + This class is experimental and may change in future versions. + + The containing workflow input must type the field as + ``WorkflowStreamState | None``, not ``Any``, so the default data converter + can reconstruct the dataclass from JSON. + + Log items use the wire representation for serialization stability. + """ + + log: list[_WorkflowStreamWireItem] = field(default_factory=list) + base_offset: int = 0 + publishers: dict[str, PublisherState] = field(default_factory=dict) diff --git a/temporalio/converter.py b/temporalio/converter.py deleted file mode 100644 index 190fda0e6..000000000 --- a/temporalio/converter.py +++ /dev/null @@ -1,1724 +0,0 @@ -"""Base converter and implementations for data conversion.""" - -from __future__ import annotations - -import collections -import collections.abc -import dataclasses -import inspect -import json -import sys -import traceback -import uuid -import warnings -from abc import ABC, abstractmethod -from dataclasses import dataclass -from datetime import datetime -from enum import IntEnum -from itertools import zip_longest -from logging import getLogger -from typing import ( - Any, - Awaitable, - Callable, - ClassVar, - Dict, - List, - Literal, - Mapping, - NewType, - Optional, - Sequence, - Tuple, - Type, - TypeVar, - Union, - get_type_hints, - overload, -) - -import google.protobuf.duration_pb2 -import google.protobuf.json_format -import google.protobuf.message -import google.protobuf.symbol_database -import nexusrpc -import typing_extensions - -import temporalio.api.common.v1 -import temporalio.api.enums.v1 -import temporalio.api.failure.v1 -import temporalio.api.sdk.v1 -import temporalio.common -import temporalio.exceptions -import temporalio.types - -if sys.version_info < (3, 11): - # Python's datetime.fromisoformat doesn't support certain formats pre-3.11 - from dateutil import parser # type: ignore -# StrEnum is available in 3.11+ -if sys.version_info >= (3, 11): - from enum import StrEnum - -if sys.version_info >= (3, 10): - from types import UnionType - -logger = getLogger(__name__) - - -class PayloadConverter(ABC): - """Base payload converter to/from multiple payloads/values.""" - - default: ClassVar[PayloadConverter] - """Default payload converter.""" - - @abstractmethod - def to_payloads( - self, values: Sequence[Any] - ) -> List[temporalio.api.common.v1.Payload]: - """Encode values into payloads. - - Implementers are expected to just return the payload for - :py:class:`temporalio.common.RawValue`. - - Args: - values: Values to be converted. - - Returns: - Converted payloads. Note, this does not have to be the same number - as values given, but must be at least one and cannot be more than - was given. - - Raises: - Exception: Any issue during conversion. - """ - raise NotImplementedError - - @abstractmethod - def from_payloads( - self, - payloads: Sequence[temporalio.api.common.v1.Payload], - type_hints: Optional[List[Type]] = None, - ) -> List[Any]: - """Decode payloads into values. - - Implementers are expected to treat a type hint of - :py:class:`temporalio.common.RawValue` as just the raw value. - - Args: - payloads: Payloads to convert to Python values. - type_hints: Types that are expected if any. This may not have any - types if there are no annotations on the target. If this is - present, it must have the exact same length as payloads even if - the values are just "object". - - Returns: - Collection of Python values. Note, this does not have to be the same - number as values given, but at least one must be present. - - Raises: - Exception: Any issue during conversion. - """ - raise NotImplementedError - - def to_payloads_wrapper( - self, values: Sequence[Any] - ) -> temporalio.api.common.v1.Payloads: - """:py:meth:`to_payloads` for the - :py:class:`temporalio.api.common.v1.Payloads` wrapper. - """ - return temporalio.api.common.v1.Payloads(payloads=self.to_payloads(values)) - - def from_payloads_wrapper( - self, payloads: Optional[temporalio.api.common.v1.Payloads] - ) -> List[Any]: - """:py:meth:`from_payloads` for the - :py:class:`temporalio.api.common.v1.Payloads` wrapper. - """ - if not payloads or not payloads.payloads: - return [] - return self.from_payloads(payloads.payloads) - - def to_payload(self, value: Any) -> temporalio.api.common.v1.Payload: - """Convert a single value to a payload. - - This is a shortcut for :py:meth:`to_payloads` with a single-item list - and result. - - Args: - value: Value to convert to a single payload. - - Returns: - Single converted payload. - """ - return self.to_payloads([value])[0] - - @overload - def from_payload(self, payload: temporalio.api.common.v1.Payload) -> Any: ... - - @overload - def from_payload( - self, - payload: temporalio.api.common.v1.Payload, - type_hint: Type[temporalio.types.AnyType], - ) -> temporalio.types.AnyType: ... - - def from_payload( - self, - payload: temporalio.api.common.v1.Payload, - type_hint: Optional[Type] = None, - ) -> Any: - """Convert a single payload to a value. - - This is a shortcut for :py:meth:`from_payloads` with a single-item list - and result. - - Args: - payload: Payload to convert to value. - type_hint: Optional type hint to say which type to convert to. - - Returns: - Single converted value. - """ - return self.from_payloads([payload], [type_hint] if type_hint else None)[0] - - -class EncodingPayloadConverter(ABC): - """Base converter to/from single payload/value with a known encoding for use in CompositePayloadConverter.""" - - @property - @abstractmethod - def encoding(self) -> str: - """Encoding for the payload this converter works with.""" - raise NotImplementedError - - @abstractmethod - def to_payload(self, value: Any) -> Optional[temporalio.api.common.v1.Payload]: - """Encode a single value to a payload or None. - - Args: - value: Value to be converted. - - Returns: - Payload of the value or None if unable to convert. - - Raises: - TypeError: Value is not the expected type. - ValueError: Value is of the expected type but otherwise incorrect. - RuntimeError: General error during encoding. - """ - raise NotImplementedError - - @abstractmethod - def from_payload( - self, - payload: temporalio.api.common.v1.Payload, - type_hint: Optional[Type] = None, - ) -> Any: - """Decode a single payload to a Python value or raise exception. - - Args: - payload: Payload to convert to Python value. - type_hint: Type that is expected if any. This may not have a type if - there are no annotations on the target. - - Return: - The decoded value from the payload. Since the encoding is checked by - the caller, this should raise an exception if the payload cannot be - converted. - - Raises: - RuntimeError: General error during decoding. - """ - raise NotImplementedError - - -class CompositePayloadConverter(PayloadConverter): - """Composite payload converter that delegates to a list of encoding payload converters. - - Encoding/decoding are attempted on each payload converter successively until - it succeeds. - - Attributes: - converters: List of payload converters to delegate to, in order. - """ - - converters: Mapping[bytes, EncodingPayloadConverter] - - def __init__(self, *converters: EncodingPayloadConverter) -> None: - """Initializes the data converter. - - Args: - converters: Payload converters to delegate to, in order. - """ - # Insertion order preserved here since Python 3.7 - self.converters = {c.encoding.encode(): c for c in converters} - - def to_payloads( - self, values: Sequence[Any] - ) -> List[temporalio.api.common.v1.Payload]: - """Encode values trying each converter. - - See base class. Always returns the same number of payloads as values. - - Raises: - RuntimeError: No known converter - """ - payloads = [] - for index, value in enumerate(values): - # We intentionally attempt these serially just in case a stateful - # converter may rely on the previous values - payload = None - # RawValue should just pass through - if isinstance(value, temporalio.common.RawValue): - payload = value.payload - else: - for converter in self.converters.values(): - payload = converter.to_payload(value) - if payload is not None: - break - if payload is None: - raise RuntimeError( - f"Value at index {index} of type {type(value)} has no known converter" - ) - payloads.append(payload) - return payloads - - def from_payloads( - self, - payloads: Sequence[temporalio.api.common.v1.Payload], - type_hints: Optional[List[Type]] = None, - ) -> List[Any]: - """Decode values trying each converter. - - See base class. Always returns the same number of values as payloads. - - Raises: - KeyError: Unknown payload encoding - RuntimeError: Error during decode - """ - values = [] - type_hints = type_hints or [] - for index, (payload, type_hint) in enumerate(zip_longest(payloads, type_hints)): - # Raw value should just wrap - if type_hint == temporalio.common.RawValue: - values.append(temporalio.common.RawValue(payload)) - continue - encoding = payload.metadata.get("encoding", b"") - converter = self.converters.get(encoding) - if converter is None: - raise KeyError(f"Unknown payload encoding {encoding.decode()}") - try: - values.append(converter.from_payload(payload, type_hint)) - except RuntimeError as err: - raise RuntimeError( - f"Payload at index {index} with encoding {encoding.decode()} could not be converted" - ) from err - return values - - -class DefaultPayloadConverter(CompositePayloadConverter): - """Default payload converter compatible with other Temporal SDKs. - - This handles None, bytes, all protobuf message types, and any type that - :py:func:`json.dump` accepts. A singleton instance of this is available at - :py:attr:`PayloadConverter.default`. - """ - - default_encoding_payload_converters: Tuple[EncodingPayloadConverter, ...] - """Default set of encoding payload converters the default payload converter - uses. - """ - - def __init__(self) -> None: - """Create a default payload converter.""" - super().__init__(*DefaultPayloadConverter.default_encoding_payload_converters) - - -class BinaryNullPayloadConverter(EncodingPayloadConverter): - """Converter for 'binary/null' payloads supporting None values.""" - - @property - def encoding(self) -> str: - """See base class.""" - return "binary/null" - - def to_payload(self, value: Any) -> Optional[temporalio.api.common.v1.Payload]: - """See base class.""" - if value is None: - return temporalio.api.common.v1.Payload( - metadata={"encoding": self.encoding.encode()} - ) - return None - - def from_payload( - self, - payload: temporalio.api.common.v1.Payload, - type_hint: Optional[Type] = None, - ) -> Any: - """See base class.""" - if len(payload.data) > 0: - raise RuntimeError("Expected empty data set for binary/null") - return None - - -class BinaryPlainPayloadConverter(EncodingPayloadConverter): - """Converter for 'binary/plain' payloads supporting bytes values.""" - - @property - def encoding(self) -> str: - """See base class.""" - return "binary/plain" - - def to_payload(self, value: Any) -> Optional[temporalio.api.common.v1.Payload]: - """See base class.""" - if isinstance(value, bytes): - return temporalio.api.common.v1.Payload( - metadata={"encoding": self.encoding.encode()}, data=value - ) - return None - - def from_payload( - self, - payload: temporalio.api.common.v1.Payload, - type_hint: Optional[Type] = None, - ) -> Any: - """See base class.""" - return payload.data - - -_sym_db = google.protobuf.symbol_database.Default() - - -class JSONProtoPayloadConverter(EncodingPayloadConverter): - """Converter for 'json/protobuf' payloads supporting protobuf Message values.""" - - def __init__(self, ignore_unknown_fields: bool = False): - """Initialize a JSON proto converter. - - Args: - ignore_unknown_fields: Determines whether converter should error if - unknown fields are detected - """ - super().__init__() - self._ignore_unknown_fields = ignore_unknown_fields - - @property - def encoding(self) -> str: - """See base class.""" - return "json/protobuf" - - def to_payload(self, value: Any) -> Optional[temporalio.api.common.v1.Payload]: - """See base class.""" - if ( - isinstance(value, google.protobuf.message.Message) - and value.DESCRIPTOR is not None - ): - # We have to convert to dict then to JSON because MessageToJson does - # not have a compact option removing spaces and newlines - json_str = json.dumps( - google.protobuf.json_format.MessageToDict(value), - separators=(",", ":"), - sort_keys=True, - ) - return temporalio.api.common.v1.Payload( - metadata={ - "encoding": self.encoding.encode(), - "messageType": value.DESCRIPTOR.full_name.encode(), - }, - data=json_str.encode(), - ) - return None - - def from_payload( - self, - payload: temporalio.api.common.v1.Payload, - type_hint: Optional[Type] = None, - ) -> Any: - """See base class.""" - message_type = payload.metadata.get("messageType", b"").decode() - try: - value = _sym_db.GetSymbol(message_type)() - return google.protobuf.json_format.Parse( - payload.data, - value, - ignore_unknown_fields=self._ignore_unknown_fields, - ) - except KeyError as err: - raise RuntimeError(f"Unknown Protobuf type {message_type}") from err - except google.protobuf.json_format.ParseError as err: - raise RuntimeError("Failed parsing") from err - - -class BinaryProtoPayloadConverter(EncodingPayloadConverter): - """Converter for 'binary/protobuf' payloads supporting protobuf Message values.""" - - @property - def encoding(self) -> str: - """See base class.""" - return "binary/protobuf" - - def to_payload(self, value: Any) -> Optional[temporalio.api.common.v1.Payload]: - """See base class.""" - if ( - isinstance(value, google.protobuf.message.Message) - and value.DESCRIPTOR is not None - ): - return temporalio.api.common.v1.Payload( - metadata={ - "encoding": self.encoding.encode(), - "messageType": value.DESCRIPTOR.full_name.encode(), - }, - data=value.SerializeToString(), - ) - return None - - def from_payload( - self, - payload: temporalio.api.common.v1.Payload, - type_hint: Optional[Type] = None, - ) -> Any: - """See base class.""" - message_type = payload.metadata.get("messageType", b"").decode() - try: - value = _sym_db.GetSymbol(message_type)() - value.ParseFromString(payload.data) - return value - except KeyError as err: - raise RuntimeError(f"Unknown Protobuf type {message_type}") from err - except google.protobuf.message.DecodeError as err: - raise RuntimeError("Failed parsing") from err - - -class AdvancedJSONEncoder(json.JSONEncoder): - """Advanced JSON encoder. - - This encoder supports dataclasses and all iterables as lists. - - It also uses Pydantic v1's "dict" methods if available on the object, - but this is deprecated. Pydantic users should upgrade to v2 and use - temporalio.contrib.pydantic.pydantic_data_converter. - """ - - def default(self, o: Any) -> Any: - """Override JSON encoding default. - - See :py:meth:`json.JSONEncoder.default`. - """ - # Datetime support - if isinstance(o, datetime): - return o.isoformat() - # Dataclass support - if dataclasses.is_dataclass(o) and not isinstance(o, type): - return dataclasses.asdict(o) - # Support for Pydantic v1's dict method - dict_fn = getattr(o, "dict", None) - if callable(dict_fn): - return dict_fn() - # Support for non-list iterables like set - if not isinstance(o, list) and isinstance(o, collections.abc.Iterable): - return list(o) - # Support for UUID - if isinstance(o, uuid.UUID): - return str(o) - return super().default(o) - - -class JSONPlainPayloadConverter(EncodingPayloadConverter): - """Converter for 'json/plain' payloads supporting common Python values. - - For encoding, this supports all values that :py:func:`json.dump` supports - and by default adds extra encoding support for dataclasses, classes with - ``dict()`` methods, and all iterables. - - For decoding, this uses type hints to attempt to rebuild the type from the - type hint. - """ - - _encoder: Optional[Type[json.JSONEncoder]] - _decoder: Optional[Type[json.JSONDecoder]] - _encoding: str - - def __init__( - self, - *, - encoder: Optional[Type[json.JSONEncoder]] = AdvancedJSONEncoder, - decoder: Optional[Type[json.JSONDecoder]] = None, - encoding: str = "json/plain", - custom_type_converters: Sequence[JSONTypeConverter] = [], - ) -> None: - """Initialize a JSON data converter. - - Args: - encoder: Custom encoder class object to use. - decoder: Custom decoder class object to use. - encoding: Encoding name to use. - custom_type_converters: Set of custom type converters that are used - when converting from a payload to type-hinted values. - """ - super().__init__() - self._encoder = encoder - self._decoder = decoder - self._encoding = encoding - self._custom_type_converters = custom_type_converters - - @property - def encoding(self) -> str: - """See base class.""" - return self._encoding - - def to_payload(self, value: Any) -> Optional[temporalio.api.common.v1.Payload]: - """See base class.""" - # Check for Pydantic v1 - if hasattr(value, "parse_obj"): - warnings.warn( - "If you're using Pydantic v2, use temporalio.contrib.pydantic.pydantic_data_converter. " - "If you're using Pydantic v1 and cannot upgrade, refer to https://github.com/temporalio/samples-python/tree/main/pydantic_converter_v1 for better v1 support." - ) - # We let JSON conversion errors be thrown to caller - return temporalio.api.common.v1.Payload( - metadata={"encoding": self._encoding.encode()}, - data=json.dumps( - value, cls=self._encoder, separators=(",", ":"), sort_keys=True - ).encode(), - ) - - def from_payload( - self, - payload: temporalio.api.common.v1.Payload, - type_hint: Optional[Type] = None, - ) -> Any: - """See base class.""" - try: - obj = json.loads(payload.data, cls=self._decoder) - if type_hint: - obj = value_to_type(type_hint, obj, self._custom_type_converters) - return obj - except json.JSONDecodeError as err: - raise RuntimeError("Failed parsing") from err - - -_JSONTypeConverterUnhandled = NewType("_JSONTypeConverterUnhandled", object) - - -class JSONTypeConverter(ABC): - """Converter for converting an object from Python :py:func:`json.loads` - result (e.g. scalar, list, or dict) to a known type. - """ - - Unhandled = _JSONTypeConverterUnhandled(object()) - """Sentinel value that must be used as the result of - :py:meth:`to_typed_value` to say the given type is not handled by this - converter.""" - - @abstractmethod - def to_typed_value( - self, hint: Type, value: Any - ) -> Union[Optional[Any], _JSONTypeConverterUnhandled]: - """Convert the given value to a type based on the given hint. - - Args: - hint: Type hint to use to help in converting the value. - value: Value as returned by :py:func:`json.loads`. Usually a scalar, - list, or dict. - - Returns: - The converted value or :py:attr:`Unhandled` if this converter does - not handle this situation. - """ - raise NotImplementedError - - -class PayloadCodec(ABC): - """Codec for encoding/decoding to/from bytes. - - Commonly used for compression or encryption. - """ - - @abstractmethod - async def encode( - self, payloads: Sequence[temporalio.api.common.v1.Payload] - ) -> List[temporalio.api.common.v1.Payload]: - """Encode the given payloads. - - Args: - payloads: Payloads to encode. This value should not be mutated. - - Returns: - Encoded payloads. Note, this does not have to be the same number as - payloads given, but must be at least one and cannot be more than was - given. - """ - raise NotImplementedError - - @abstractmethod - async def decode( - self, payloads: Sequence[temporalio.api.common.v1.Payload] - ) -> List[temporalio.api.common.v1.Payload]: - """Decode the given payloads. - - Args: - payloads: Payloads to decode. This value should not be mutated. - - Returns: - Decoded payloads. Note, this does not have to be the same number as - payloads given, but must be at least one and cannot be more than was - given. - """ - raise NotImplementedError - - async def encode_wrapper(self, payloads: temporalio.api.common.v1.Payloads) -> None: - """:py:meth:`encode` for the - :py:class:`temporalio.api.common.v1.Payloads` wrapper. - - This replaces the payloads within the wrapper. - """ - new_payloads = await self.encode(payloads.payloads) - del payloads.payloads[:] - # TODO(cretz): Copy too expensive? - payloads.payloads.extend(new_payloads) - - async def decode_wrapper(self, payloads: temporalio.api.common.v1.Payloads) -> None: - """:py:meth:`decode` for the - :py:class:`temporalio.api.common.v1.Payloads` wrapper. - - This replaces the payloads within. - """ - new_payloads = await self.decode(payloads.payloads) - del payloads.payloads[:] - # TODO(cretz): Copy too expensive? - payloads.payloads.extend(new_payloads) - - async def encode_failure(self, failure: temporalio.api.failure.v1.Failure) -> None: - """Encode payloads of a failure.""" - await self._apply_to_failure_payloads(failure, self.encode_wrapper) - - async def decode_failure(self, failure: temporalio.api.failure.v1.Failure) -> None: - """Decode payloads of a failure.""" - await self._apply_to_failure_payloads(failure, self.decode_wrapper) - - async def _apply_to_failure_payloads( - self, - failure: temporalio.api.failure.v1.Failure, - cb: Callable[[temporalio.api.common.v1.Payloads], Awaitable[None]], - ) -> None: - if failure.HasField("encoded_attributes"): - # Wrap in payloads and merge back - payloads = temporalio.api.common.v1.Payloads( - payloads=[failure.encoded_attributes] - ) - await cb(payloads) - failure.encoded_attributes.CopyFrom(payloads.payloads[0]) - if failure.HasField( - "application_failure_info" - ) and failure.application_failure_info.HasField("details"): - await cb(failure.application_failure_info.details) - elif failure.HasField( - "timeout_failure_info" - ) and failure.timeout_failure_info.HasField("last_heartbeat_details"): - await cb(failure.timeout_failure_info.last_heartbeat_details) - elif failure.HasField( - "canceled_failure_info" - ) and failure.canceled_failure_info.HasField("details"): - await cb(failure.canceled_failure_info.details) - elif failure.HasField( - "reset_workflow_failure_info" - ) and failure.reset_workflow_failure_info.HasField("last_heartbeat_details"): - await cb(failure.reset_workflow_failure_info.last_heartbeat_details) - if failure.HasField("cause"): - await self._apply_to_failure_payloads(failure.cause, cb) - - -class FailureConverter(ABC): - """Base failure converter to/from errors. - - Note, for workflow exceptions, :py:attr:`to_failure` is only invoked if the - exception is an instance of :py:class:`temporalio.exceptions.FailureError`. - Users should extend :py:class:`temporalio.exceptions.ApplicationError` if - they want a custom workflow exception to work with this class. - """ - - default: ClassVar[FailureConverter] - """Default failure converter.""" - - @abstractmethod - def to_failure( - self, - exception: BaseException, - payload_converter: PayloadConverter, - failure: temporalio.api.failure.v1.Failure, - ) -> None: - """Convert the given exception to a Temporal failure. - - Users should make sure not to alter the ``exception`` input. - - Args: - exception: The exception to convert. - payload_converter: The payload converter to use if needed. - failure: The failure to update with error information. - """ - raise NotImplementedError - - @abstractmethod - def from_failure( - self, - failure: temporalio.api.failure.v1.Failure, - payload_converter: PayloadConverter, - ) -> BaseException: - """Convert the given Temporal failure to an exception. - - Users should make sure not to alter the ``failure`` input. - - Args: - failure: The failure to convert. - payload_converter: The payload converter to use if needed. - - Returns: - Converted error. - """ - raise NotImplementedError - - -class DefaultFailureConverter(FailureConverter): - """Default failure converter. - - A singleton instance of this is available at - :py:attr:`FailureConverter.default`. - """ - - def __init__(self, *, encode_common_attributes: bool = False) -> None: - """Create the default failure converter. - - Args: - encode_common_attributes: If ``True``, the message and stack trace - of the failure will be moved into the encoded attribute section - of the failure which can be encoded with a codec. - """ - super().__init__() - self._encode_common_attributes = encode_common_attributes - - def to_failure( - self, - exception: BaseException, - payload_converter: PayloadConverter, - failure: temporalio.api.failure.v1.Failure, - ) -> None: - """See base class.""" - # If already a failure error, use that - if isinstance(exception, temporalio.exceptions.FailureError): - self._error_to_failure(exception, payload_converter, failure) - elif isinstance(exception, nexusrpc.HandlerError): - self._nexus_handler_error_to_failure(exception, payload_converter, failure) - else: - # Convert to failure error - failure_error = temporalio.exceptions.ApplicationError( - str(exception), type=exception.__class__.__name__ - ) - failure_error.__traceback__ = exception.__traceback__ - failure_error.__cause__ = exception.__cause__ - self._error_to_failure(failure_error, payload_converter, failure) - # Encode common attributes if requested - if self._encode_common_attributes: - # Move message and stack trace to encoded attribute payload - failure.encoded_attributes.CopyFrom( - payload_converter.to_payloads( - [{"message": failure.message, "stack_trace": failure.stack_trace}] - )[0] - ) - failure.message = "Encoded failure" - failure.stack_trace = "" - - def _error_to_failure( - self, - error: temporalio.exceptions.FailureError, - payload_converter: PayloadConverter, - failure: temporalio.api.failure.v1.Failure, - ) -> None: - # If there is an underlying proto already, just use that - if error.failure: - failure.CopyFrom(error.failure) - return - - # Set message, stack, and cause. Obtaining cause follows rules from - # https://docs.python.org/3/library/exceptions.html#exception-context - failure.message = error.message - if error.__traceback__: - failure.stack_trace = "\n".join(traceback.format_tb(error.__traceback__)) - if error.__cause__: - self.to_failure(error.__cause__, payload_converter, failure.cause) - elif not error.__suppress_context__ and error.__context__: - self.to_failure(error.__context__, payload_converter, failure.cause) - - # Set specific subclass values - if isinstance(error, temporalio.exceptions.ApplicationError): - failure.application_failure_info.SetInParent() - if error.type: - failure.application_failure_info.type = error.type - failure.application_failure_info.non_retryable = error.non_retryable - if error.details: - failure.application_failure_info.details.CopyFrom( - payload_converter.to_payloads_wrapper(error.details) - ) - if error.next_retry_delay: - failure.application_failure_info.next_retry_delay.FromTimedelta( - error.next_retry_delay - ) - if error.category: - failure.application_failure_info.category = ( - temporalio.api.enums.v1.ApplicationErrorCategory.ValueType( - error.category - ) - ) - elif isinstance(error, temporalio.exceptions.TimeoutError): - failure.timeout_failure_info.SetInParent() - failure.timeout_failure_info.timeout_type = ( - temporalio.api.enums.v1.TimeoutType.ValueType(error.type or 0) - ) - if error.last_heartbeat_details: - failure.timeout_failure_info.last_heartbeat_details.CopyFrom( - payload_converter.to_payloads_wrapper(error.last_heartbeat_details) - ) - elif isinstance(error, temporalio.exceptions.CancelledError): - failure.canceled_failure_info.SetInParent() - if error.details: - failure.canceled_failure_info.details.CopyFrom( - payload_converter.to_payloads_wrapper(error.details) - ) - elif isinstance(error, temporalio.exceptions.TerminatedError): - failure.terminated_failure_info.SetInParent() - elif isinstance(error, temporalio.exceptions.ServerError): - failure.server_failure_info.SetInParent() - failure.server_failure_info.non_retryable = error.non_retryable - elif isinstance(error, temporalio.exceptions.ActivityError): - failure.activity_failure_info.SetInParent() - failure.activity_failure_info.scheduled_event_id = error.scheduled_event_id - failure.activity_failure_info.started_event_id = error.started_event_id - failure.activity_failure_info.identity = error.identity - failure.activity_failure_info.activity_type.name = error.activity_type - failure.activity_failure_info.activity_id = error.activity_id - failure.activity_failure_info.retry_state = ( - temporalio.api.enums.v1.RetryState.ValueType(error.retry_state or 0) - ) - elif isinstance(error, temporalio.exceptions.ChildWorkflowError): - failure.child_workflow_execution_failure_info.SetInParent() - failure.child_workflow_execution_failure_info.namespace = error.namespace - failure.child_workflow_execution_failure_info.workflow_execution.workflow_id = error.workflow_id - failure.child_workflow_execution_failure_info.workflow_execution.run_id = ( - error.run_id - ) - failure.child_workflow_execution_failure_info.workflow_type.name = ( - error.workflow_type - ) - failure.child_workflow_execution_failure_info.initiated_event_id = ( - error.initiated_event_id - ) - failure.child_workflow_execution_failure_info.started_event_id = ( - error.started_event_id - ) - failure.child_workflow_execution_failure_info.retry_state = ( - temporalio.api.enums.v1.RetryState.ValueType(error.retry_state or 0) - ) - # TODO(nexus-preview): missing test coverage - elif isinstance(error, temporalio.exceptions.NexusOperationError): - failure.nexus_operation_execution_failure_info.SetInParent() - failure.nexus_operation_execution_failure_info.scheduled_event_id = ( - error.scheduled_event_id - ) - failure.nexus_operation_execution_failure_info.endpoint = error.endpoint - failure.nexus_operation_execution_failure_info.service = error.service - failure.nexus_operation_execution_failure_info.operation = error.operation - failure.nexus_operation_execution_failure_info.operation_token = ( - error.operation_token - ) - - def _nexus_handler_error_to_failure( - self, - error: nexusrpc.HandlerError, - payload_converter: PayloadConverter, - failure: temporalio.api.failure.v1.Failure, - ) -> None: - failure.message = str(error) - if error.__traceback__: - failure.stack_trace = "\n".join(traceback.format_tb(error.__traceback__)) - if error.__cause__: - self.to_failure(error.__cause__, payload_converter, failure.cause) - failure.nexus_handler_failure_info.SetInParent() - failure.nexus_handler_failure_info.type = error.type.name - failure.nexus_handler_failure_info.retry_behavior = temporalio.api.enums.v1.NexusHandlerErrorRetryBehavior.ValueType( - temporalio.api.enums.v1.NexusHandlerErrorRetryBehavior.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_RETRYABLE - if error.retryable_override is True - else temporalio.api.enums.v1.NexusHandlerErrorRetryBehavior.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_NON_RETRYABLE - if error.retryable_override is False - else temporalio.api.enums.v1.NexusHandlerErrorRetryBehavior.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_UNSPECIFIED - ) - - def from_failure( - self, - failure: temporalio.api.failure.v1.Failure, - payload_converter: PayloadConverter, - ) -> BaseException: - """See base class.""" - # If encoded attributes are present and have the fields we expect, - # extract them - if failure.HasField("encoded_attributes"): - # Clone the failure to not mutate the incoming failure - new_failure = temporalio.api.failure.v1.Failure() - new_failure.CopyFrom(failure) - failure = new_failure - try: - encoded_attributes: Dict[str, Any] = payload_converter.from_payloads( - [failure.encoded_attributes] - )[0] - if isinstance(encoded_attributes, dict): - message = encoded_attributes.get("message") - if isinstance(message, str): - failure.message = message - stack_trace = encoded_attributes.get("stack_trace") - if isinstance(stack_trace, str): - failure.stack_trace = stack_trace - except: - pass - - err: Union[temporalio.exceptions.FailureError, nexusrpc.HandlerError] - if failure.HasField("application_failure_info"): - app_info = failure.application_failure_info - err = temporalio.exceptions.ApplicationError( - failure.message or "Application error", - *payload_converter.from_payloads_wrapper(app_info.details), - type=app_info.type or None, - non_retryable=app_info.non_retryable, - next_retry_delay=app_info.next_retry_delay.ToTimedelta(), - category=temporalio.exceptions.ApplicationErrorCategory( - int(app_info.category) - ), - ) - elif failure.HasField("timeout_failure_info"): - timeout_info = failure.timeout_failure_info - err = temporalio.exceptions.TimeoutError( - failure.message or "Timeout", - type=temporalio.exceptions.TimeoutType(int(timeout_info.timeout_type)) - if timeout_info.timeout_type - else None, - last_heartbeat_details=payload_converter.from_payloads_wrapper( - timeout_info.last_heartbeat_details - ), - ) - elif failure.HasField("canceled_failure_info"): - cancel_info = failure.canceled_failure_info - err = temporalio.exceptions.CancelledError( - failure.message or "Cancelled", - *payload_converter.from_payloads_wrapper(cancel_info.details), - ) - elif failure.HasField("terminated_failure_info"): - err = temporalio.exceptions.TerminatedError(failure.message or "Terminated") - elif failure.HasField("server_failure_info"): - server_info = failure.server_failure_info - err = temporalio.exceptions.ServerError( - failure.message or "Server error", - non_retryable=server_info.non_retryable, - ) - elif failure.HasField("activity_failure_info"): - act_info = failure.activity_failure_info - err = temporalio.exceptions.ActivityError( - failure.message or "Activity error", - scheduled_event_id=act_info.scheduled_event_id, - started_event_id=act_info.started_event_id, - identity=act_info.identity, - activity_type=act_info.activity_type.name, - activity_id=act_info.activity_id, - retry_state=temporalio.exceptions.RetryState(int(act_info.retry_state)) - if act_info.retry_state - else None, - ) - elif failure.HasField("child_workflow_execution_failure_info"): - child_info = failure.child_workflow_execution_failure_info - err = temporalio.exceptions.ChildWorkflowError( - failure.message or "Child workflow error", - namespace=child_info.namespace, - workflow_id=child_info.workflow_execution.workflow_id, - run_id=child_info.workflow_execution.run_id, - workflow_type=child_info.workflow_type.name, - initiated_event_id=child_info.initiated_event_id, - started_event_id=child_info.started_event_id, - retry_state=temporalio.exceptions.RetryState( - int(child_info.retry_state) - ) - if child_info.retry_state - else None, - ) - elif failure.HasField("nexus_handler_failure_info"): - nexus_handler_failure_info = failure.nexus_handler_failure_info - try: - _type = nexusrpc.HandlerErrorType[nexus_handler_failure_info.type] - except KeyError: - logger.warning( - f"Unknown Nexus HandlerErrorType: {nexus_handler_failure_info.type}" - ) - _type = nexusrpc.HandlerErrorType.INTERNAL - retryable_override = ( - True - if ( - nexus_handler_failure_info.retry_behavior - == temporalio.api.enums.v1.NexusHandlerErrorRetryBehavior.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_RETRYABLE - ) - else False - if ( - nexus_handler_failure_info.retry_behavior - == temporalio.api.enums.v1.NexusHandlerErrorRetryBehavior.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_NON_RETRYABLE - ) - else None - ) - err = nexusrpc.HandlerError( - failure.message or "Nexus handler error", - type=_type, - retryable_override=retryable_override, - ) - elif failure.HasField("nexus_operation_execution_failure_info"): - nexus_op_failure_info = failure.nexus_operation_execution_failure_info - err = temporalio.exceptions.NexusOperationError( - failure.message or "Nexus operation error", - scheduled_event_id=nexus_op_failure_info.scheduled_event_id, - endpoint=nexus_op_failure_info.endpoint, - service=nexus_op_failure_info.service, - operation=nexus_op_failure_info.operation, - operation_token=nexus_op_failure_info.operation_token, - ) - else: - err = temporalio.exceptions.FailureError(failure.message or "Failure error") - if isinstance(err, temporalio.exceptions.FailureError): - err._failure = failure - if failure.HasField("cause"): - err.__cause__ = self.from_failure(failure.cause, payload_converter) - return err - - -class DefaultFailureConverterWithEncodedAttributes(DefaultFailureConverter): - """Implementation of :py:class:`DefaultFailureConverter` which moves message - and stack trace to encoded attributes subject to a codec. - """ - - def __init__(self) -> None: - """Create a default failure converter with encoded attributes.""" - super().__init__(encode_common_attributes=True) - - -@dataclass(frozen=True) -class DataConverter: - """Data converter for converting and encoding payloads to/from Python values. - - This combines :py:class:`PayloadConverter` which converts values with - :py:class:`PayloadCodec` which encodes bytes. - """ - - payload_converter_class: Type[PayloadConverter] = DefaultPayloadConverter - """Class to instantiate for payload conversion.""" - - payload_codec: Optional[PayloadCodec] = None - """Optional codec for encoding payload bytes.""" - - failure_converter_class: Type[FailureConverter] = DefaultFailureConverter - """Class to instantiate for failure conversion.""" - - payload_converter: PayloadConverter = dataclasses.field(init=False) - """Payload converter created from the :py:attr:`payload_converter_class`.""" - - failure_converter: FailureConverter = dataclasses.field(init=False) - """Failure converter created from the :py:attr:`failure_converter_class`.""" - - default: ClassVar[DataConverter] - """Singleton default data converter.""" - - def __post_init__(self) -> None: # noqa: D105 - object.__setattr__(self, "payload_converter", self.payload_converter_class()) - object.__setattr__(self, "failure_converter", self.failure_converter_class()) - - async def encode( - self, values: Sequence[Any] - ) -> List[temporalio.api.common.v1.Payload]: - """Encode values into payloads. - - First converts values to payloads then encodes payloads using codec. - - Args: - values: Values to be converted and encoded. - - Returns: - Converted and encoded payloads. Note, this does not have to be the - same number as values given, but must be at least one and cannot be - more than was given. - """ - payloads = self.payload_converter.to_payloads(values) - if self.payload_codec: - payloads = await self.payload_codec.encode(payloads) - return payloads - - async def decode( - self, - payloads: Sequence[temporalio.api.common.v1.Payload], - type_hints: Optional[List[Type]] = None, - ) -> List[Any]: - """Decode payloads into values. - - First decodes payloads using codec then converts payloads to values. - - Args: - payloads: Payloads to be decoded and converted. - - Returns: - Decoded and converted values. - """ - if self.payload_codec: - payloads = await self.payload_codec.decode(payloads) - return self.payload_converter.from_payloads(payloads, type_hints) - - async def encode_wrapper( - self, values: Sequence[Any] - ) -> temporalio.api.common.v1.Payloads: - """:py:meth:`encode` for the - :py:class:`temporalio.api.common.v1.Payloads` wrapper. - """ - return temporalio.api.common.v1.Payloads(payloads=(await self.encode(values))) - - async def decode_wrapper( - self, - payloads: Optional[temporalio.api.common.v1.Payloads], - type_hints: Optional[List[Type]] = None, - ) -> List[Any]: - """:py:meth:`decode` for the - :py:class:`temporalio.api.common.v1.Payloads` wrapper. - """ - if not payloads or not payloads.payloads: - return [] - return await self.decode(payloads.payloads, type_hints) - - async def encode_failure( - self, exception: BaseException, failure: temporalio.api.failure.v1.Failure - ) -> None: - """Convert and encode failure.""" - self.failure_converter.to_failure(exception, self.payload_converter, failure) - if self.payload_codec: - await self.payload_codec.encode_failure(failure) - - async def decode_failure( - self, failure: temporalio.api.failure.v1.Failure - ) -> BaseException: - """Decode and convert failure.""" - if self.payload_codec: - await self.payload_codec.decode_failure(failure) - return self.failure_converter.from_failure(failure, self.payload_converter) - - -DefaultPayloadConverter.default_encoding_payload_converters = ( - BinaryNullPayloadConverter(), - BinaryPlainPayloadConverter(), - JSONProtoPayloadConverter(), - BinaryProtoPayloadConverter(), - JSONPlainPayloadConverter(), # JSON Plain needs to remain in last because it throws on unknown types -) - -DataConverter.default = DataConverter() - -PayloadConverter.default = DataConverter.default.payload_converter - -FailureConverter.default = DataConverter.default.failure_converter - - -def default() -> DataConverter: - """Default data converter. - - .. deprecated:: - Use :py:meth:`DataConverter.default` instead. - """ - return DataConverter.default - - -def encode_search_attributes( - attributes: Union[ - temporalio.common.SearchAttributes, temporalio.common.TypedSearchAttributes - ], - api: temporalio.api.common.v1.SearchAttributes, -) -> None: - """Convert search attributes into an API message. - - Args: - attributes: Search attributes to convert. The dictionary form of this is - DEPRECATED. - api: API message to set converted attributes on. - """ - if isinstance(attributes, temporalio.common.TypedSearchAttributes): - for typed_k, typed_v in attributes: - api.indexed_fields[typed_k.name].CopyFrom( - encode_typed_search_attribute_value(typed_k, typed_v) - ) - return - elif not attributes: - return - for k, v in attributes.items(): - api.indexed_fields[k].CopyFrom(encode_search_attribute_values(v)) - - -def encode_typed_search_attribute_value( - key: temporalio.common.SearchAttributeKey[ - temporalio.common.SearchAttributeValueType - ], - value: Optional[temporalio.common.SearchAttributeValue], -) -> temporalio.api.common.v1.Payload: - """Convert typed search attribute value into a payload. - - Args: - key: Key for the value. - value: Value to convert. - - Returns: - Payload for the value. - """ - # For server search attributes to work properly, we cannot set the metadata - # type when we set null - if value is None: - return default().payload_converter.to_payload(None) - if not isinstance(value, key.origin_value_type): - raise TypeError( - f"Value of type {value} not suitable for indexed value type {key.indexed_value_type}" - ) - # datetime needs to be in isoformat - if isinstance(value, datetime): - value = value.isoformat() - # We'll do an extra sanity check for keyword list and check every value - if isinstance(value, Sequence): - for v in value: - if not isinstance(v, str): - raise TypeError("All values of a keyword list must be strings") - # Convert value - payload = default().payload_converter.to_payload(value) - # Set metadata type - payload.metadata["type"] = key._metadata_type.encode() - return payload - - -def encode_search_attribute_values( - vals: temporalio.common.SearchAttributeValues, -) -> temporalio.api.common.v1.Payload: - """Convert search attribute values into a payload. - - .. deprecated:: - Use typed search attributes instead. - - Args: - vals: List of values to convert. - """ - if not isinstance(vals, list): - raise TypeError("Search attribute values must be lists") - # Confirm all types are the same - val_type: Optional[Type] = None - # Convert dates to strings - safe_vals = [] - for v in vals: - if isinstance(v, datetime): - if v.tzinfo is None: - raise ValueError( - "Timezone must be present on all search attribute dates" - ) - v = v.isoformat() - elif not isinstance(v, (str, int, float, bool)): - raise TypeError( - f"Search attribute value of type {type(v).__name__} not one of str, int, float, bool, or datetime" - ) - elif val_type and type(v) is not val_type: - raise TypeError( - "Search attribute values must have the same type for the same key" - ) - elif not val_type: - val_type = type(v) - safe_vals.append(v) - return default().payload_converter.to_payloads([safe_vals])[0] - - -def _encode_maybe_typed_search_attributes( - non_typed_attributes: Optional[temporalio.common.SearchAttributes], - typed_attributes: Optional[temporalio.common.TypedSearchAttributes], - api: temporalio.api.common.v1.SearchAttributes, -) -> None: - if non_typed_attributes: - if typed_attributes and typed_attributes.search_attributes: - raise ValueError( - "Cannot provide both deprecated search attributes and typed search attributes" - ) - encode_search_attributes(non_typed_attributes, api) - elif typed_attributes and typed_attributes.search_attributes: - encode_search_attributes(typed_attributes, api) - - -def _get_iso_datetime_parser() -> Callable[[str], datetime]: - """Isolates system version check and returns relevant datetime passer - - Returns: - A callable to parse date strings into datetimes. - """ - if sys.version_info >= (3, 11): - return datetime.fromisoformat # noqa - else: - # Isolate import for py > 3.11, as dependency only installed for < 3.11 - return parser.isoparse - - -def decode_search_attributes( - api: temporalio.api.common.v1.SearchAttributes, -) -> temporalio.common.SearchAttributes: - """Decode API search attributes to values. - - .. deprecated:: - Use typed search attributes instead. - - Args: - api: API message with search attribute values to convert. - - Returns: - Converted search attribute values (new mapping every time). - """ - conv = default().payload_converter - ret = {} - for k, v in api.indexed_fields.items(): - val = conv.from_payloads([v])[0] - # If a value did not come back as a list, make it a single-item list - if not isinstance(val, list): - val = [val] - # Convert each item to datetime if necessary - if v.metadata.get("type") == b"Datetime": - parser = _get_iso_datetime_parser() - val = [parser(v) for v in val] - ret[k] = val - return ret - - -def decode_typed_search_attributes( - api: temporalio.api.common.v1.SearchAttributes, -) -> temporalio.common.TypedSearchAttributes: - """Decode API search attributes to typed search attributes. - - Args: - api: API message with search attribute values to convert. - - Returns: - Typed search attribute collection (new object every time). - """ - conv = default().payload_converter - pairs: List[temporalio.common.SearchAttributePair] = [] - for k, v in api.indexed_fields.items(): - # We want the "type" metadata, but if it is not present or an unknown - # type, we will just ignore - metadata_type = v.metadata.get("type") - if not metadata_type: - continue - key = temporalio.common.SearchAttributeKey._from_metadata_type( - k, metadata_type.decode() - ) - if not key: - continue - val = conv.from_payload(v) - # If the value is a list but the type is not keyword list, pull out - # single item or consider this an invalid value and ignore - if ( - key.indexed_value_type - != temporalio.common.SearchAttributeIndexedValueType.KEYWORD_LIST - and isinstance(val, list) - ): - if len(val) != 1: - continue - val = val[0] - if ( - key.indexed_value_type - == temporalio.common.SearchAttributeIndexedValueType.DATETIME - ): - parser = _get_iso_datetime_parser() - # We will let this throw - val = parser(val) - # If the value isn't the right type, we need to ignore - if isinstance(val, key.origin_value_type): - pairs.append(temporalio.common.SearchAttributePair(key, val)) - return temporalio.common.TypedSearchAttributes(pairs) - - -def _decode_search_attribute_value( - payload: temporalio.api.common.v1.Payload, -) -> temporalio.common.SearchAttributeValue: - val = default().payload_converter.from_payload(payload) - if isinstance(val, str) and payload.metadata.get("type") == b"Datetime": - val = _get_iso_datetime_parser()(val) - return val # type: ignore - - -def value_to_type( - hint: Type, - value: Any, - custom_converters: Sequence[JSONTypeConverter] = [], -) -> Any: - """Convert a given value to the given type hint. - - This is used internally to convert a raw JSON loaded value to a specific - type hint. - - Args: - hint: Type hint to convert the value to. - value: Raw value (e.g. primitive, dict, or list) to convert from. - custom_converters: Set of custom converters to try before doing default - conversion. Converters are tried in order and the first value that - is not :py:attr:`JSONTypeConverter.Unhandled` will be returned from - this function instead of doing default behavior. - - Returns: - Converted value. - - Raises: - TypeError: Unable to convert to the given hint. - """ - # Try custom converters - for conv in custom_converters: - ret = conv.to_typed_value(hint, value) - if ret is not JSONTypeConverter.Unhandled: - return ret - - # Any or primitives - if hint is Any: - return value - elif hint is datetime: - if isinstance(value, str): - try: - return _get_iso_datetime_parser()(value) - except ValueError as err: - raise TypeError(f"Failed parsing datetime string: {value}") from err - elif isinstance(value, datetime): - return value - raise TypeError(f"Expected datetime or ISO8601 string, got {type(value)}") - elif hint is int or hint is float: - if not isinstance(value, (int, float)): - raise TypeError(f"Expected value to be int|float, was {type(value)}") - return hint(value) - elif hint is bool: - if not isinstance(value, bool): - raise TypeError(f"Expected value to be bool, was {type(value)}") - return bool(value) - elif hint is str: - if not isinstance(value, str): - raise TypeError(f"Expected value to be str, was {type(value)}") - return str(value) - elif hint is bytes: - if not isinstance(value, (str, bytes, list)): - raise TypeError(f"Expected value to be bytes, was {type(value)}") - # In some other SDKs, this is serialized as a base64 string, but in - # Python this is a numeric array. - return bytes(value) # type: ignore - elif hint is type(None): - if value is not None: - raise TypeError(f"Expected None, got value of type {type(value)}") - return None - - # NewType. Note we cannot simply check isinstance NewType here because it's - # only been a class since 3.10. Instead we'll just check for the presence - # of a supertype. - supertype = getattr(hint, "__supertype__", None) - if supertype: - return value_to_type(supertype, value, custom_converters) - - # Load origin for other checks - origin = getattr(hint, "__origin__", hint) - type_args: Tuple = getattr(hint, "__args__", ()) - - # Literal - if origin is Literal or origin is typing_extensions.Literal: - if value not in type_args: - raise TypeError(f"Value {value} not in literal values {type_args}") - return value - - is_union = origin is Union - if sys.version_info >= (3, 10): - is_union = is_union or isinstance(origin, UnionType) - - # Union - if is_union: - # Try each one. Note, Optional is just a union w/ none. - for arg in type_args: - try: - return value_to_type(arg, value, custom_converters) - except Exception: - pass - raise TypeError(f"Failed converting to {hint} from {value}") - - # Mapping - if inspect.isclass(origin) and issubclass(origin, collections.abc.Mapping): - if not isinstance(value, collections.abc.Mapping): - raise TypeError(f"Expected {hint}, value was {type(value)}") - ret_dict = {} - # If there are required or optional keys that means we are a TypedDict - # and therefore can extract per-key types - per_key_types: Optional[Dict[str, Type]] = None - if getattr(origin, "__required_keys__", None) or getattr( - origin, "__optional_keys__", None - ): - per_key_types = get_type_hints(origin) - key_type = ( - type_args[0] - if len(type_args) > 0 - and type_args[0] is not Any - and not isinstance(type_args[0], TypeVar) - else None - ) - value_type = ( - type_args[1] - if len(type_args) > 1 - and type_args[1] is not Any - and not isinstance(type_args[1], TypeVar) - else None - ) - # Convert each key/value - for key, value in value.items(): - this_value_type = value_type - if per_key_types: - # TODO(cretz): Strict mode would fail an unknown key - this_value_type = per_key_types.get(key) - - if key_type: - # This function is used only by JSONPlainPayloadConverter. When - # serializing to JSON, Python supports key types str, int, float, bool, - # and None, serializing all to string representations. We now attempt to - # use the provided type annotation to recover the original value with its - # original type. - try: - if isinstance(key, str): - if key_type is int or key_type is float: - key = key_type(key) - elif key_type is bool: - key = {"true": True, "false": False}[key] - elif key_type is type(None): - key = {"null": None}[key] - - # Can't call isinstance if key_type is a newtype - is_newtype = getattr(key_type, "__supertype__", None) - if is_newtype or not isinstance(key, key_type): - key = value_to_type(key_type, key, custom_converters) - except Exception as err: - raise TypeError( - f"Failed converting key {repr(key)} to type {key_type} in mapping {hint}" - ) from err - - if this_value_type: - try: - value = value_to_type(this_value_type, value, custom_converters) - except Exception as err: - raise TypeError( - f"Failed converting value for key {repr(key)} in mapping {hint}" - ) from err - ret_dict[key] = value - # If there are per-key types, it's a typed dict and we want to attempt - # instantiation to get its validation - if per_key_types: - ret_dict = hint(**ret_dict) - return ret_dict - - # Dataclass - if dataclasses.is_dataclass(hint): - if not isinstance(value, dict): - raise TypeError( - f"Cannot convert to dataclass {hint}, value is {type(value)} not dict" - ) - # Obtain dataclass fields and check that all dict fields are there and - # that no required fields are missing. Unknown fields are silently - # ignored. - fields = dataclasses.fields(hint) - field_hints = get_type_hints(hint) - field_values = {} - for field in fields: - field_value = value.get(field.name, dataclasses.MISSING) - # We do not check whether field is required here. Rather, we let the - # attempted instantiation of the dataclass raise if a field is - # missing - if field_value is not dataclasses.MISSING: - try: - field_values[field.name] = value_to_type( - field_hints[field.name], field_value, custom_converters - ) - except Exception as err: - raise TypeError( - f"Failed converting field {field.name} on dataclass {hint}" - ) from err - # Simply instantiate the dataclass. This will fail as expected when - # missing required fields. - # TODO(cretz): Want way to convert snake case to camel case? - return hint(**field_values) - - # Pydantic model instance - # Pydantic users should use Pydantic v2 with - # temporalio.contrib.pydantic.pydantic_data_converter, in which case a - # pydantic model instance will have been handled by the custom_converters at - # the start of this function. We retain the following for backwards - # compatibility with pydantic v1 users, but this is deprecated. - parse_obj_attr = inspect.getattr_static(hint, "parse_obj", None) - if isinstance(parse_obj_attr, classmethod) or isinstance( - parse_obj_attr, staticmethod - ): - if not isinstance(value, dict): - raise TypeError( - f"Cannot convert to {hint}, value is {type(value)} not dict" - ) - return getattr(hint, "parse_obj")(value) - - # IntEnum - if inspect.isclass(hint) and issubclass(hint, IntEnum): - if not isinstance(value, int): - raise TypeError( - f"Cannot convert to enum {hint}, value not an integer, value is {type(value)}" - ) - return hint(value) - - # StrEnum, available in 3.11+ - if sys.version_info >= (3, 11): - if inspect.isclass(hint) and issubclass(hint, StrEnum): - if not isinstance(value, str): - raise TypeError( - f"Cannot convert to enum {hint}, value not a string, value is {type(value)}" - ) - return hint(value) - - # UUID - if inspect.isclass(hint) and issubclass(hint, uuid.UUID): - return hint(value) - - # Iterable. We intentionally put this last as it catches several others. - if inspect.isclass(origin) and issubclass(origin, collections.abc.Iterable): - if not isinstance(value, collections.abc.Iterable): - raise TypeError(f"Expected {hint}, value was {type(value)}") - ret_list = [] - # If there is no type arg, just return value as is - if not type_args or ( - len(type_args) == 1 - and (isinstance(type_args[0], TypeVar) or type_args[0] is Ellipsis) - ): - ret_list = list(value) - else: - # Otherwise convert - for i, item in enumerate(value): - # Non-tuples use first type arg, tuples use arg set or one - # before ellipsis if that's set - if origin is not tuple: - arg_type = type_args[0] - elif len(type_args) > i and type_args[i] is not Ellipsis: - arg_type = type_args[i] - elif type_args[-1] is Ellipsis: - # Ellipsis means use the second to last one - arg_type = type_args[-2] # type: ignore - else: - raise TypeError( - f"Type {hint} only expecting {len(type_args)} values, got at least {i + 1}" - ) - try: - ret_list.append(value_to_type(arg_type, item, custom_converters)) - except Exception as err: - raise TypeError(f"Failed converting {hint} index {i}") from err - # If tuple, set, or deque convert back to that type - if origin is tuple: - return tuple(ret_list) - elif origin is set: - return set(ret_list) - elif origin is collections.deque: - return collections.deque(ret_list) - return ret_list - - raise TypeError(f"Unserializable type during conversion: {hint}") diff --git a/temporalio/converter/__init__.py b/temporalio/converter/__init__.py new file mode 100644 index 000000000..ebd2b8396 --- /dev/null +++ b/temporalio/converter/__init__.py @@ -0,0 +1,99 @@ +"""Base converter and implementations for data conversion.""" + +from temporalio.converter._data_converter import ( + DataConverter, + default, +) +from temporalio.converter._extstore import ( + ExternalStorage, + StorageDriver, + StorageDriverActivityInfo, + StorageDriverClaim, + StorageDriverRetrieveContext, + StorageDriverStoreContext, + StorageDriverWorkflowInfo, + StorageWarning, +) +from temporalio.converter._failure_converter import ( + DefaultFailureConverter, + DefaultFailureConverterWithEncodedAttributes, + FailureConverter, +) +from temporalio.converter._payload_codec import PayloadCodec +from temporalio.converter._payload_converter import ( + AdvancedJSONEncoder, + BinaryNullPayloadConverter, + BinaryPlainPayloadConverter, + BinaryProtoPayloadConverter, + CompositePayloadConverter, + DefaultPayloadConverter, + EncodingPayloadConverter, + JSONPlainPayloadConverter, + JSONProtoPayloadConverter, + JSONTypeConverter, + JSONTypeConverterUnhandled, + PayloadConverter, + TransferTypeConverter, + transfer_type_convertible, + value_to_type, +) +from temporalio.converter._search_attributes import ( + decode_search_attributes, + decode_typed_search_attributes, + encode_search_attribute_values, + encode_search_attributes, + encode_typed_search_attribute_value, +) +from temporalio.converter._serialization_context import ( + ActivitySerializationContext, + SerializationContext, + WithSerializationContext, + WorkflowSerializationContext, +) + +__all__ = [ + "ActivitySerializationContext", + "ExternalStorage", + "StorageDriver", + "StorageDriverActivityInfo", + "StorageDriverClaim", + "StorageDriverRetrieveContext", + "StorageDriverStoreContext", + "StorageDriverWorkflowInfo", + "StorageWarning", + "AdvancedJSONEncoder", + "BinaryNullPayloadConverter", + "BinaryPlainPayloadConverter", + "BinaryProtoPayloadConverter", + "CompositePayloadConverter", + "TransferTypeConverter", + "DataConverter", + "DefaultFailureConverter", + "DefaultFailureConverterWithEncodedAttributes", + "DefaultPayloadConverter", + "EncodingPayloadConverter", + "FailureConverter", + "JSONPlainPayloadConverter", + "JSONProtoPayloadConverter", + "JSONTypeConverter", + "JSONTypeConverterUnhandled", + "PayloadCodec", + "PayloadConverter", + "SerializationContext", + "WithSerializationContext", + "WorkflowSerializationContext", + "transfer_type_convertible", + "decode_search_attributes", + "decode_typed_search_attributes", + "default", + "encode_search_attribute_values", + "encode_search_attributes", + "encode_typed_search_attribute_value", + "value_to_type", +] + +DataConverter.default = DataConverter() + +PayloadConverter.default = DataConverter.default.payload_converter + +FailureConverter.default = DataConverter.default.failure_converter diff --git a/temporalio/converter/_data_converter.py b/temporalio/converter/_data_converter.py new file mode 100644 index 000000000..8604ea196 --- /dev/null +++ b/temporalio/converter/_data_converter.py @@ -0,0 +1,360 @@ +"""DataConverter: the top-level data conversion orchestrator.""" + +from __future__ import annotations + +import dataclasses +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from logging import getLogger +from typing import TYPE_CHECKING, Any, ClassVar + +from typing_extensions import Self + +import temporalio.api.common.v1 +import temporalio.api.failure.v1 +import temporalio.common +from temporalio.converter._extstore import ( + _REFERENCE_ENCODING, + _REFERENCE_MESSAGE_TYPE, + ExternalStorage, + StorageDriverStoreContext, +) +from temporalio.converter._failure_converter import ( + FailureConverter, +) +from temporalio.converter._payload_codec import ( + PayloadCodec, + _apply_to_failure_payloads, +) +from temporalio.converter._payload_converter import ( + PayloadConverter, + _TemporalTransferTypePayloadConverter, +) +from temporalio.converter._serialization_context import ( + SerializationContext, + WithSerializationContext, +) + + +def _is_reference_payload(p: temporalio.api.common.v1.Payload) -> bool: + """Return True if *p* is an external-storage reference payload.""" + return p.metadata.get("encoding") == _REFERENCE_ENCODING or ( + p.metadata.get("encoding") == b"json/protobuf" + and p.metadata.get("messageType") == _REFERENCE_MESSAGE_TYPE + ) + + +# Import defaults from public API to avoid pydoctor cross-reference issues +if TYPE_CHECKING: + from temporalio.converter import DefaultFailureConverter, DefaultPayloadConverter +else: + # Import from private modules for runtime to avoid circular imports + from temporalio.converter._failure_converter import DefaultFailureConverter + from temporalio.converter._payload_converter import DefaultPayloadConverter + +logger = getLogger("temporalio.converter") + + +@dataclass(frozen=True) +class DataConverter(WithSerializationContext): + """Data converter for converting and encoding payloads to/from Python values. + + This combines :py:class:`PayloadConverter` which converts values with + :py:class:`PayloadCodec` which encodes bytes. + """ + + payload_converter_class: type[PayloadConverter] = DefaultPayloadConverter + """Class to instantiate for payload conversion.""" + + payload_codec: PayloadCodec | None = None + """Optional codec for encoding payload bytes.""" + + failure_converter_class: type[FailureConverter] = DefaultFailureConverter + """Class to instantiate for failure conversion.""" + + payload_converter: PayloadConverter = dataclasses.field(init=False) + """Payload converter created from the :py:attr:`payload_converter_class`.""" + + failure_converter: FailureConverter = dataclasses.field(init=False) + """Failure converter created from the :py:attr:`failure_converter_class`.""" + + external_storage: ExternalStorage | None = None + """Options for external storage. If None, external storage is disabled. + + .. warning:: + This API is experimental. + """ + + default: ClassVar[DataConverter] + """Singleton default data converter.""" + + def __post_init__(self) -> None: # noqa: D105 + object.__setattr__(self, "payload_converter", self._new_payload_converter()) + object.__setattr__(self, "failure_converter", self.failure_converter_class()) + + def _new_payload_converter(self) -> PayloadConverter: + """Create a payload converter instance with SDK transfer type hooks enabled.""" + return _TemporalTransferTypePayloadConverter.wrap( + self.payload_converter_class() + ) + + async def encode( + self, values: Sequence[Any] + ) -> list[temporalio.api.common.v1.Payload]: + """Encode values into payloads. + + First converts values to payloads then encodes payloads using codec. + + Args: + values: Values to be converted and encoded. + + Returns: + Converted and encoded payloads. Note, this does not have to be the + same number as values given, but must be at least one and cannot be + more than was given. + """ + payloads = self.payload_converter.to_payloads(values) + payloads = await self._encode_payload_sequence(payloads) + payloads = await self._external_store_payload_sequence(payloads) + return payloads + + async def decode( + self, + payloads: Sequence[temporalio.api.common.v1.Payload], + type_hints: list[type] | None = None, + ) -> list[Any]: + """Decode payloads into values. + + First decodes payloads using codec then converts payloads to values. + + Args: + payloads: Payloads to be decoded and converted. + + Returns: + Decoded and converted values. + """ + payloads = await self._external_retrieve_payload_sequence(payloads) + payloads = await self._decode_payload_sequence(payloads) + return self.payload_converter.from_payloads(payloads, type_hints) + + async def encode_wrapper( + self, values: Sequence[Any] + ) -> temporalio.api.common.v1.Payloads: + """:py:meth:`encode` for the + :py:class:`temporalio.api.common.v1.Payloads` wrapper. + """ + return temporalio.api.common.v1.Payloads(payloads=(await self.encode(values))) + + async def decode_wrapper( + self, + payloads: temporalio.api.common.v1.Payloads | None, + type_hints: list[type] | None = None, + ) -> list[Any]: + """:py:meth:`decode` for the + :py:class:`temporalio.api.common.v1.Payloads` wrapper. + """ + if not payloads or not payloads.payloads: + return [] + return await self.decode(payloads.payloads, type_hints) + + async def encode_failure( + self, exception: BaseException, failure: temporalio.api.failure.v1.Failure + ) -> None: + """Convert and encode failure.""" + self.failure_converter.to_failure(exception, self.payload_converter, failure) + await _apply_to_failure_payloads(failure, self._transform_outbound_payloads) + + async def decode_failure( + self, failure: temporalio.api.failure.v1.Failure + ) -> BaseException: + """Decode and convert failure.""" + await _apply_to_failure_payloads(failure, self._transform_inbound_payloads) + return self.failure_converter.from_failure(failure, self.payload_converter) + + def with_context(self, context: SerializationContext) -> Self: + """Return an instance with context set on the component converters.""" + payload_converter = self.payload_converter + payload_codec = self.payload_codec + failure_converter = self.failure_converter + external_storage = self.external_storage + if isinstance(payload_converter, WithSerializationContext): + payload_converter = payload_converter.with_context(context) + if isinstance(payload_codec, WithSerializationContext): + payload_codec = payload_codec.with_context(context) + if isinstance(failure_converter, WithSerializationContext): + failure_converter = failure_converter.with_context(context) + if isinstance(external_storage, WithSerializationContext): + external_storage = external_storage.with_context(context) + if all( + new is orig + for new, orig in [ + (payload_converter, self.payload_converter), + (payload_codec, self.payload_codec), + (failure_converter, self.failure_converter), + (external_storage, self.external_storage), + ] + ): + return self + cloned = dataclasses.replace(self) + object.__setattr__(cloned, "payload_converter", payload_converter) + object.__setattr__(cloned, "payload_codec", payload_codec) + object.__setattr__(cloned, "failure_converter", failure_converter) + object.__setattr__(cloned, "external_storage", external_storage) + return cloned + + def _with_store_context( + self, store_ctx: StorageDriverStoreContext + ) -> DataConverter: + """Return an instance with ``store_ctx`` bound into :attr:`external_storage`.""" + if self.external_storage is None: + return self + return dataclasses.replace( + self, + external_storage=self.external_storage._with_store_context(store_ctx), + ) + + def _with_contexts( + self, + serialization_ctx: SerializationContext, + store_ctx: StorageDriverStoreContext, + ) -> DataConverter: + """Return an instance with both serialization and store contexts applied.""" + return self.with_context(serialization_ctx)._with_store_context(store_ctx) + + async def _decode_memo( + self, + source: temporalio.api.common.v1.Memo, + ) -> Mapping[str, Any]: + mapping: dict[str, Any] = {} + for k, v in source.fields.items(): + mapping[k] = (await self.decode([v]))[0] + return mapping + + async def _decode_memo_field( + self, + source: temporalio.api.common.v1.Memo, + key: str, + default: Any, + type_hint: type | None, + ) -> dict[str, Any]: + payload = source.fields.get(key) + if not payload: + if default is temporalio.common._arg_unset: + raise KeyError(f"Memo does not have a value for key {key}") + return default + return (await self.decode([payload], [type_hint] if type_hint else None))[0] + + async def _encode_memo( + self, source: Mapping[str, Any] + ) -> temporalio.api.common.v1.Memo: + memo = temporalio.api.common.v1.Memo() + await self._encode_memo_existing(source, memo) + return memo + + async def _encode_memo_existing( + self, source: Mapping[str, Any], memo: temporalio.api.common.v1.Memo + ): + for k, v in source.items(): + payload = v + if not isinstance(v, temporalio.api.common.v1.Payload): + payload = (await self.encode([v]))[0] + memo.fields[k].CopyFrom(payload) + + async def _transform_outbound_payload( + self, payload: temporalio.api.common.v1.Payload + ) -> temporalio.api.common.v1.Payload: + if self.payload_codec: + payload = (await self.payload_codec.encode([payload]))[0] + if self.external_storage: + payload = await self.external_storage._store_payload(payload) + return payload + + async def _transform_outbound_payloads( + self, payloads: temporalio.api.common.v1.Payloads + ): + if self.payload_codec: + await self.payload_codec.encode_wrapper(payloads) + if self.external_storage: + await self.external_storage._store_payloads(payloads) + + async def _transform_inbound_payload( + self, payload: temporalio.api.common.v1.Payload + ) -> temporalio.api.common.v1.Payload: + if self.external_storage: + payload = await self.external_storage._retrieve_payload(payload) + if self.payload_codec: + payload = (await self.payload_codec.decode([payload]))[0] + return payload + + async def _transform_inbound_payloads( + self, payloads: temporalio.api.common.v1.Payloads + ): + if self.external_storage: + await self.external_storage._retrieve_payloads(payloads) + else: + if any(_is_reference_payload(p) for p in payloads.payloads): + raise RuntimeError( + "[TMPRL1105] Detected externally stored payload(s) but external storage is not configured." + ) + if self.payload_codec: + await self.payload_codec.decode_wrapper(payloads) + + async def _encode_payload_sequence( + self, payloads: Sequence[temporalio.api.common.v1.Payload] + ) -> list[temporalio.api.common.v1.Payload]: + """Codec encode only.""" + encoded_payloads = list(payloads) + if self.payload_codec: + encoded_payloads = await self.payload_codec.encode(encoded_payloads) + return encoded_payloads + + async def _external_store_payload_sequence( + self, payloads: Sequence[temporalio.api.common.v1.Payload] + ) -> list[temporalio.api.common.v1.Payload]: + """External storage store, then validate payload limits.""" + stored_payloads = list(payloads) + if self.external_storage: + stored_payloads = await self.external_storage._store_payload_sequence( + stored_payloads + ) + return stored_payloads + + async def _external_retrieve_payload_sequence( + self, payloads: Sequence[temporalio.api.common.v1.Payload] + ) -> list[temporalio.api.common.v1.Payload]: + """External storage retrieve only.""" + retrieved_payloads = list(payloads) + if self.external_storage: + retrieved_payloads = await self.external_storage._retrieve_payload_sequence( + retrieved_payloads + ) + else: + if any(_is_reference_payload(p) for p in retrieved_payloads): + raise RuntimeError( + "[TMPRL1105] Detected externally stored payload(s) but external storage is not configured." + ) + return retrieved_payloads + + async def _decode_payload_sequence( + self, payloads: Sequence[temporalio.api.common.v1.Payload] + ) -> list[temporalio.api.common.v1.Payload]: + """Codec decode only.""" + decoded_payloads = list(payloads) + if self.payload_codec: + decoded_payloads = await self.payload_codec.decode(decoded_payloads) + return decoded_payloads + + # Temporary shortcircuit detection while the _decode_* methods may no-op if + # a payload codec is not configured. Remove once those paths have more to them. + @property + def _decode_payload_has_effect(self) -> bool: + return self.payload_codec is not None or self.external_storage is not None + + +def default() -> DataConverter: + """Default data converter. + + .. deprecated:: + Use :py:meth:`DataConverter.default` instead. + """ + return DataConverter.default diff --git a/temporalio/converter/_extstore.py b/temporalio/converter/_extstore.py new file mode 100644 index 000000000..a946b2c0f --- /dev/null +++ b/temporalio/converter/_extstore.py @@ -0,0 +1,600 @@ +"""External payload storage support for offloading payloads to external storage +systems. +""" + +from __future__ import annotations + +import asyncio +import contextlib +import contextvars +import dataclasses +import time +from abc import ABC, abstractmethod +from collections.abc import Callable, Coroutine, Generator, Mapping, Sequence +from dataclasses import dataclass +from datetime import timedelta +from typing import Any, ClassVar, TypeVar + +from typing_extensions import Self + +from temporalio.api.common.v1 import Payload, Payloads +from temporalio.api.sdk.v1.external_storage_pb2 import ExternalStorageReference +from temporalio.converter._payload_converter import ( + JSONPlainPayloadConverter, + JSONProtoPayloadConverter, +) + +_T = TypeVar("_T") + +_REFERENCE_ENCODING = b"json/external-storage-reference" +_REFERENCE_MESSAGE_TYPE = ExternalStorageReference.DESCRIPTOR.full_name.encode() + + +@dataclass +class StorageOperationMetrics: + """Accumulates metrics from external storage operations.""" + + payload_count: int = 0 + """Number of payloads stored or retrieved externally.""" + + total_size: int = 0 + """Total size in bytes of externally stored/retrieved payloads.""" + + total_duration: timedelta = dataclasses.field(default_factory=timedelta) + """Wall-clock time spent on external storage operations.""" + + driver_names: set[str] = dataclasses.field(default_factory=set) + """Names of the drivers that participated in the operations.""" + + def record_batch( + self, count: int, size: int, duration: timedelta, driver_names: set[str] + ) -> None: + """Record metrics from a batch of storage operations.""" + self.payload_count += count + self.total_size += size + self.total_duration += duration + self.driver_names.update(driver_names) + + @contextlib.contextmanager + def track(self) -> Generator[Self, None, None]: + """Set this instance as the current metrics context and reset on exit.""" + token = _current_storage_metrics.set(self) + try: + yield self + finally: + _current_storage_metrics.reset(token) + + +_current_storage_metrics: contextvars.ContextVar[StorageOperationMetrics | None] = ( + contextvars.ContextVar("_current_storage_metrics", default=None) +) + + +async def _gather_cancel_on_error( + coros: Sequence[Coroutine[Any, Any, _T]], +) -> list[_T]: + """Run coroutines concurrently; cancel all remaining tasks if any one fails.""" + tasks = [asyncio.create_task(c) for c in coros] + try: + return await asyncio.gather(*tasks) + except BaseException: + for task in tasks: + task.cancel() + await asyncio.gather(*tasks, return_exceptions=True) + raise + + +@dataclass(frozen=True) +class StorageDriverClaim: + """A driver-defined reference to an externally-stored payload that can be used to + retrieve it. + + .. warning:: + This API is experimental. + """ + + claim_data: Mapping[str, str] + """Driver-defined data for identifying and retrieving an externally stored + payload. + """ + + +@dataclass(frozen=True, kw_only=True) +class StorageDriverWorkflowInfo: + """Workflow identity information for external storage operations. + + .. warning:: + This API is experimental. + """ + + namespace: str + """The namespace of the workflow execution.""" + + id: str | None = None + """The workflow ID.""" + + run_id: str | None = None + """The workflow run ID, if available.""" + + type: str | None = None + """The workflow type name, if available.""" + + +@dataclass(frozen=True, kw_only=True) +class StorageDriverActivityInfo: + """Activity identity information for external storage operations. + + .. warning:: + This API is experimental. + """ + + namespace: str + """The namespace of the activity execution.""" + + id: str | None = None + """The activity ID.""" + + run_id: str | None = None + """The activity run ID (only for standalone activities).""" + + type: str | None = None + """The activity type name, if available.""" + + +@dataclass(frozen=True) +class StorageDriverStoreContext: + """Context passed to :meth:`StorageDriver.store` and ``driver_selector`` calls. + + .. warning:: + This API is experimental. + """ + + target: StorageDriverActivityInfo | StorageDriverWorkflowInfo | None = None + """The workflow or activity for which this payload is being stored. + + For payloads being stored on behalf of an explicit target (e.g. a child + workflow being started, an activity being scheduled, an external workflow + being signaled), this is that target's identity. When no explicit target + exists the current execution context (workflow or activity) is used as the + target instead.""" + + +@dataclass(frozen=True) +class StorageDriverRetrieveContext: + """Context passed to :meth:`StorageDriver.retrieve` calls. + + .. warning:: + This API is experimental. + """ + + +class StorageDriver(ABC): + """Base driver for storing and retrieve payloads from external storage systems. + + .. warning:: + This API is experimental. + """ + + @abstractmethod + def name(self) -> str: + """Returns the name of this driver instance. A driver may allow + its name to be parameterized at construction time so that multiple + instances of the same driver class can coexist in + :attr:`ExternalStorage.drivers` with distinct names. + """ + raise NotImplementedError + + def type(self) -> str: + """Returns the type of the storage driver. This string should be + the same across all instantiations of the same driver class. This + allows the equivalent driver implementation in different languages + to be named the same. + + Defaults to the class name. Subclasses may override this to return a + stable, language-agnostic identifier. + """ + return type(self).__name__ + + @abstractmethod + async def store( + self, + context: StorageDriverStoreContext, + payloads: Sequence[Payload], + ) -> list[StorageDriverClaim]: + """Stores payloads in external storage and returns a + :class:`StorageDriverClaim` for each one. The returned list must be the + same length as ``payloads``. + """ + raise NotImplementedError + + @abstractmethod + async def retrieve( + self, + context: StorageDriverRetrieveContext, + claims: Sequence[StorageDriverClaim], + ) -> list[Payload]: + """Retrieves payloads from external storage for the given + :class:`StorageDriverClaim` list. The returned list must be the same + length as ``claims``. + """ + raise NotImplementedError + + +class StorageWarning(RuntimeWarning): + """Warning for external storage issues. + + .. warning:: + This API is experimental. + """ + + +@dataclass(frozen=True) +class _StorageReference: + """Legacy external storage reference used only on the retrieval path as a + fallback for in-flight workflows that were written before the + ExternalStorageReference proto was introduced. + """ + + driver_name: str + driver_claim: StorageDriverClaim + + +@dataclass(frozen=True) +class ExternalStorage: + """Configuration for external storage behavior. + + .. warning:: + This API is experimental. + """ + + drivers: Sequence[StorageDriver] + """Drivers available for storing and retrieving payloads. At least one + driver must be provided. If more than one driver is registered, + :attr:`driver_selector` must also be set. + + Drivers in this list are looked up by :meth:`StorageDriver.name` during + retrieval, so each driver must have a unique name. + """ + + driver_selector: ( + Callable[[StorageDriverStoreContext, Payload], StorageDriver | None] | None + ) = None + """Controls which driver stores a given payload. A callable that returns the + driver instance to use, or ``None`` to leave the payload stored inline. + The returned driver must be one of the instances registered in + :attr:`drivers`. + + Required when more than one driver is registered. When ``None`` and only + one driver is registered, that driver is used for all store operations. + """ + + payload_size_threshold: int = 256 * 1024 + """Minimum payload size in bytes before external storage is considered. + Defaults to 256 KiB. Must be greater than or equal to zero. + """ + + _driver_map: dict[str, StorageDriver] = dataclasses.field( + init=False, repr=False, compare=False + ) + """Name-keyed index of :attr:`drivers`, built at construction time. Used + for retrieval lookups. + """ + + _store_context: StorageDriverStoreContext = dataclasses.field( + default=StorageDriverStoreContext(target=None), + init=False, + repr=False, + compare=False, + ) + """Store context bound to this instance via :meth:`_with_store_context`.""" + + _claim_converter: ClassVar[JSONProtoPayloadConverter] = JSONProtoPayloadConverter() + _legacy_claim_converter: ClassVar[JSONPlainPayloadConverter] = ( + JSONPlainPayloadConverter(encoding=_REFERENCE_ENCODING.decode()) + ) + + def __post_init__(self) -> None: + """Validate drivers and build the internal name-keyed driver map. + + Raises :exc:`ValueError` if no drivers are provided, if + :attr:`payload_size_threshold` is less than zero, if more than one + driver is registered without a :attr:`driver_selector`, or if any two + drivers share the same name. + """ + if not self.drivers: + raise ValueError( + "ExternalStorage.drivers must contain at least one driver." + ) + if self.payload_size_threshold < 0: + raise ValueError( + "ExternalStorage.payload_size_threshold must be greater than or equal to zero." + ) + if len(self.drivers) > 1 and self.driver_selector is None: + raise ValueError( + "ExternalStorage.driver_selector must be specified if multiple drivers are registered." + ) + driver_map: dict[str, StorageDriver] = {} + for driver in self.drivers: + name = driver.name() + if name in driver_map: + raise ValueError( + f"ExternalStorage.drivers contains multiple drivers with name '{name}'. " + "Each driver must have a unique name." + ) + driver_map[name] = driver + object.__setattr__(self, "_driver_map", driver_map) + + def _select_driver( + self, context: StorageDriverStoreContext, payload: Payload + ) -> StorageDriver | None: + """Returns the driver to use for this payload, or None to pass through.""" + if payload.ByteSize() < self.payload_size_threshold: + return None + selector = self.driver_selector + if selector is None: + return self.drivers[0] if self.drivers else None + driver = selector(context, payload) + if driver is None: + return None + registered = self._driver_map.get(driver.name()) + if registered is not driver: + raise ValueError( + f"Driver '{driver.name()}' returned by driver_selector is not registered in ExternalStorage.drivers" + ) + return driver + + def _get_driver_by_name(self, name: str) -> StorageDriver: + """Looks up a driver by name, raising :class:`ValueError` if not found.""" + driver = self._driver_map.get(name) + if driver is None: + raise ValueError(f"No driver found with name '{name}'") + return driver + + def _with_store_context(self, ctx: StorageDriverStoreContext) -> ExternalStorage: + """Return a copy of this instance with ``ctx`` bound as the store context.""" + result = dataclasses.replace(self) + object.__setattr__(result, "_store_context", ctx) + return result + + async def _store_payload(self, payload: Payload) -> Payload: + start_time = time.monotonic() + + driver = self._select_driver(self._store_context, payload) + if driver is None: + return payload + + claims = await driver.store(self._store_context, [payload]) + + self._validate_claim_length(claims, expected=1, driver=driver) + + external_size = payload.ByteSize() + reference = ExternalStorageReference( + driver_name=driver.name(), + claim_data=claims[0].claim_data, + ) + reference_payload = self._claim_converter.to_payload(reference) + if reference_payload is None: + raise ValueError( + f"Failed to serialize storage reference for driver '{driver.name()}'" + ) + reference_payload.external_payloads.add().size_bytes = external_size + + ExternalStorage._record_metrics(1, external_size, start_time, {driver.name()}) + + return reference_payload + + async def _store_payloads(self, payloads: Payloads): + stored_payloads = await self._store_payload_sequence(payloads.payloads) + for i, payload in enumerate(stored_payloads): + payloads.payloads[i].CopyFrom(payload) + + async def _store_payload_sequence( + self, + payloads: Sequence[Payload], + ) -> list[Payload]: + if len(payloads) == 1: + return [await self._store_payload(payloads[0])] + + start_time = time.monotonic() + + results = list(payloads) + + to_store: list[tuple[int, Payload, StorageDriver]] = [] + for index, payload in enumerate(payloads): + driver = self._select_driver(self._store_context, payload) + if driver is None: + continue + to_store.append((index, payload, driver)) + + if not to_store: + return results + + driver_groups: dict[StorageDriver, list[tuple[int, Payload]]] = {} + for orig_index, payload, driver in to_store: + driver_groups.setdefault(driver, []).append((orig_index, payload)) + + driver_group_list = list(driver_groups.items()) + + all_claims = await _gather_cancel_on_error( + [ + driver.store(self._store_context, [p for _, p in indexed_payloads]) + for driver, indexed_payloads in driver_group_list + ] + ) + + external_count = 0 + external_size = 0 + driver_names: set[str] = set() + for (driver, indexed_payloads), claims in zip(driver_group_list, all_claims): + indices = [idx for idx, _ in indexed_payloads] + sizes = [p.ByteSize() for _, p in indexed_payloads] + + self._validate_claim_length(claims, expected=len(indices), driver=driver) + + for i, claim in enumerate(claims): + reference = ExternalStorageReference( + driver_name=driver.name(), + claim_data=claim.claim_data, + ) + reference_payload = self._claim_converter.to_payload(reference) + if reference_payload is None: + raise ValueError( + f"Failed to serialize storage reference for driver '{driver.name()}'" + ) + reference_payload.external_payloads.add().size_bytes = sizes[i] + results[indices[i]] = reference_payload + external_size += sizes[i] + + external_count += len(claims) + driver_names.add(driver.name()) + + ExternalStorage._record_metrics( + external_count, external_size, start_time, driver_names + ) + + return results + + def _decode_reference(self, payload: Payload) -> ExternalStorageReference | None: + """Decode an external storage reference from a payload.""" + encoding = payload.metadata.get("encoding", b"") + if encoding == _REFERENCE_ENCODING: + legacy = self._legacy_claim_converter.from_payload( + payload, _StorageReference + ) + if not isinstance(legacy, _StorageReference): + return None + return ExternalStorageReference( + driver_name=legacy.driver_name, + claim_data=legacy.driver_claim.claim_data, + ) + if not ( + encoding == b"json/protobuf" + and payload.metadata.get("messageType") == _REFERENCE_MESSAGE_TYPE + ): + return None + ref = self._claim_converter.from_payload(payload, ExternalStorageReference) + return ref if isinstance(ref, ExternalStorageReference) else None + + async def _retrieve_payload(self, payload: Payload) -> Payload: + ref = self._decode_reference(payload) + if ref is None: + return payload + + start_time = time.monotonic() + driver = self._get_driver_by_name(ref.driver_name) + context = StorageDriverRetrieveContext() + claim = StorageDriverClaim(claim_data=dict(ref.claim_data)) + + stored_payloads = await driver.retrieve(context, [claim]) + + self._validate_payload_length(stored_payloads, expected=1, driver=driver) + + stored_payload = stored_payloads[0] + + ExternalStorage._record_metrics( + 1, stored_payload.ByteSize(), start_time, {driver.name()} + ) + + return stored_payload + + async def _retrieve_payloads(self, payloads: Payloads): + stored_payloads = await self._retrieve_payload_sequence(payloads.payloads) + for i, payload in enumerate(stored_payloads): + payloads.payloads[i].CopyFrom(payload) + + async def _retrieve_payload_sequence( + self, + payloads: Sequence[Payload], + ) -> list[Payload]: + if len(payloads) == 1: + return [await self._retrieve_payload(payloads[0])] + + start_time = time.monotonic() + + results = list(payloads) + + driver_claims: dict[StorageDriver, list[tuple[int, StorageDriverClaim]]] = {} + for index, payload in enumerate(payloads): + ref = self._decode_reference(payload) + if ref is None: + continue + driver = self._get_driver_by_name(ref.driver_name) + claim = StorageDriverClaim(claim_data=dict(ref.claim_data)) + driver_claims.setdefault(driver, []).append((index, claim)) + + if not driver_claims: + return results + + context = StorageDriverRetrieveContext() + stored_by_index: dict[int, Payload] = {} + + driver_claim_list = list(driver_claims.items()) + + all_stored = await _gather_cancel_on_error( + [ + driver.retrieve(context, [claim for _, claim in indexed_claims]) + for driver, indexed_claims in driver_claim_list + ] + ) + + external_count = 0 + external_size = 0 + driver_names: set[str] = set() + for (driver, indexed_claims), stored_payloads in zip( + driver_claim_list, all_stored + ): + indices = [idx for idx, _ in indexed_claims] + + self._validate_payload_length( + stored_payloads, + expected=len(indexed_claims), + driver=driver, + ) + + for idx, stored_payload in zip(indices, stored_payloads): + stored_by_index[idx] = stored_payload + external_size += stored_payload.ByteSize() + + external_count += len(stored_payloads) + driver_names.add(driver.name()) + + retrieve_indices = sorted(stored_by_index.keys()) + stored_list = [stored_by_index[idx] for idx in retrieve_indices] + + for i, retrieved_payload in enumerate(stored_list): + results[retrieve_indices[i]] = retrieved_payload + + ExternalStorage._record_metrics( + external_count, external_size, start_time, driver_names + ) + + return results + + def _validate_claim_length( + self, claims: Sequence[StorageDriverClaim], expected: int, driver: StorageDriver + ) -> None: + if len(claims) != expected: + raise ValueError( + f"Driver '{driver.name()}' returned {len(claims)} claims, expected {expected}", + ) + + def _validate_payload_length( + self, payloads: Sequence[Payload], expected: int, driver: StorageDriver + ) -> None: + if len(payloads) != expected: + raise ValueError( + f"Driver '{driver.name()}' returned {len(payloads)} payloads, expected {expected}", + ) + + @staticmethod + def _record_metrics( + count: int, size: int, start_time: float, driver_names: set[str] + ): + metrics = _current_storage_metrics.get() + if metrics is not None: + metrics.record_batch( + count, + size, + timedelta(seconds=time.monotonic() - start_time), + driver_names, + ) diff --git a/temporalio/converter/_failure_converter.py b/temporalio/converter/_failure_converter.py new file mode 100644 index 000000000..848dbc038 --- /dev/null +++ b/temporalio/converter/_failure_converter.py @@ -0,0 +1,467 @@ +"""Failure converters for converting exceptions to/from Temporal Failure protos.""" + +from __future__ import annotations + +import dataclasses +import json +import traceback +from abc import ABC, abstractmethod +from logging import getLogger +from typing import Any, ClassVar + +import google.protobuf.json_format +import nexusrpc + +import temporalio.api.common.v1 +import temporalio.api.enums.v1 +import temporalio.api.failure.v1 +import temporalio.exceptions +from temporalio.converter._payload_converter import PayloadConverter + +logger = getLogger("temporalio.converter") + +_TEMPORAL_FAILURE_PROTO_TYPE = "temporal.api.failure.v1.Failure" + + +class FailureConverter(ABC): + """Base failure converter to/from errors. + + Note, for workflow exceptions, :py:attr:`to_failure` is only invoked if the + exception is an instance of :py:class:`temporalio.exceptions.FailureError`. + Users should extend :py:class:`temporalio.exceptions.ApplicationError` if + they want a custom workflow exception to work with this class. + """ + + default: ClassVar[FailureConverter] + """Default failure converter.""" + + @abstractmethod + def to_failure( + self, + exception: BaseException, + payload_converter: PayloadConverter, + failure: temporalio.api.failure.v1.Failure, + ) -> None: + """Convert the given exception to a Temporal failure. + + Users should make sure not to alter the ``exception`` input. + + Args: + exception: The exception to convert. + payload_converter: The payload converter to use if needed. + failure: The failure to update with error information. + """ + raise NotImplementedError + + @abstractmethod + def from_failure( + self, + failure: temporalio.api.failure.v1.Failure, + payload_converter: PayloadConverter, + ) -> BaseException: + """Convert the given Temporal failure to an exception. + + Users should make sure not to alter the ``failure`` input. + + Args: + failure: The failure to convert. + payload_converter: The payload converter to use if needed. + + Returns: + Converted error. + """ + raise NotImplementedError + + +class DefaultFailureConverter(FailureConverter): + """Default failure converter. + + A singleton instance of this is available at + :py:attr:`FailureConverter.default`. + """ + + def __init__(self, *, encode_common_attributes: bool = False) -> None: + """Create the default failure converter. + + Args: + encode_common_attributes: If ``True``, the message and stack trace + of the failure will be moved into the encoded attribute section + of the failure which can be encoded with a codec. + """ + super().__init__() + self._encode_common_attributes = encode_common_attributes + + def to_failure( + self, + exception: BaseException, + payload_converter: PayloadConverter, + failure: temporalio.api.failure.v1.Failure, + ) -> None: + """See base class.""" + # If already a failure error, use that + if isinstance(exception, temporalio.exceptions.FailureError): + self._error_to_failure(exception, payload_converter, failure) + elif isinstance(exception, nexusrpc.HandlerError): + self._nexus_handler_error_to_failure(exception, payload_converter, failure) + else: + # Convert to failure error + failure_error = temporalio.exceptions.ApplicationError( + str(exception), + type=exception.__class__.__name__, + ) + failure_error.__traceback__ = exception.__traceback__ + failure_error.__cause__ = exception.__cause__ + self._error_to_failure(failure_error, payload_converter, failure) + # Encode common attributes if requested + if self._encode_common_attributes: + # Move message and stack trace to encoded attribute payload + failure.encoded_attributes.CopyFrom( + payload_converter.to_payloads( + [{"message": failure.message, "stack_trace": failure.stack_trace}] + )[0] + ) + failure.message = "Encoded failure" + failure.stack_trace = "" + + def _error_to_failure( + self, + error: temporalio.exceptions.FailureError, + payload_converter: PayloadConverter, + failure: temporalio.api.failure.v1.Failure, + ) -> None: + # If there is an underlying proto already, just use that + if error.failure: + failure.CopyFrom(error.failure) + return + + # Set message, stack, and cause. Obtaining cause follows rules from + # https://docs.python.org/3/library/exceptions.html#exception-context + failure.message = error.message + if error.__traceback__: + failure.stack_trace = "\n".join(traceback.format_tb(error.__traceback__)) + if error.__cause__: + self.to_failure(error.__cause__, payload_converter, failure.cause) + elif not error.__suppress_context__ and error.__context__: + self.to_failure(error.__context__, payload_converter, failure.cause) + + # Set specific subclass values + if isinstance(error, temporalio.exceptions.ApplicationError): + failure.application_failure_info.SetInParent() + if error.type: + failure.application_failure_info.type = error.type + failure.application_failure_info.non_retryable = error.non_retryable + if error.details: + failure.application_failure_info.details.CopyFrom( + payload_converter.to_payloads_wrapper(error.details) + ) + if error.next_retry_delay: + failure.application_failure_info.next_retry_delay.FromTimedelta( + error.next_retry_delay + ) + if error.category: + failure.application_failure_info.category = ( + temporalio.api.enums.v1.ApplicationErrorCategory.ValueType( + error.category + ) + ) + elif isinstance(error, temporalio.exceptions.TimeoutError): + failure.timeout_failure_info.SetInParent() + failure.timeout_failure_info.timeout_type = ( + temporalio.api.enums.v1.TimeoutType.ValueType(error.type or 0) + ) + if error.last_heartbeat_details: + failure.timeout_failure_info.last_heartbeat_details.CopyFrom( + payload_converter.to_payloads_wrapper(error.last_heartbeat_details) + ) + elif isinstance(error, temporalio.exceptions.CancelledError): + failure.canceled_failure_info.SetInParent() + if error.details: + failure.canceled_failure_info.details.CopyFrom( + payload_converter.to_payloads_wrapper(error.details) + ) + elif isinstance(error, temporalio.exceptions.TerminatedError): + failure.terminated_failure_info.SetInParent() + elif isinstance(error, temporalio.exceptions.ServerError): + failure.server_failure_info.SetInParent() + failure.server_failure_info.non_retryable = error.non_retryable + elif isinstance(error, temporalio.exceptions.ActivityError): + failure.activity_failure_info.SetInParent() + failure.activity_failure_info.scheduled_event_id = error.scheduled_event_id + failure.activity_failure_info.started_event_id = error.started_event_id + failure.activity_failure_info.identity = error.identity + failure.activity_failure_info.activity_type.name = error.activity_type + failure.activity_failure_info.activity_id = error.activity_id + failure.activity_failure_info.retry_state = ( + temporalio.api.enums.v1.RetryState.ValueType(error.retry_state or 0) + ) + elif isinstance(error, temporalio.exceptions.ChildWorkflowError): + failure.child_workflow_execution_failure_info.SetInParent() + failure.child_workflow_execution_failure_info.namespace = error.namespace + failure.child_workflow_execution_failure_info.workflow_execution.workflow_id = error.workflow_id + failure.child_workflow_execution_failure_info.workflow_execution.run_id = ( + error.run_id + ) + failure.child_workflow_execution_failure_info.workflow_type.name = ( + error.workflow_type + ) + failure.child_workflow_execution_failure_info.initiated_event_id = ( + error.initiated_event_id + ) + failure.child_workflow_execution_failure_info.started_event_id = ( + error.started_event_id + ) + failure.child_workflow_execution_failure_info.retry_state = ( + temporalio.api.enums.v1.RetryState.ValueType(error.retry_state or 0) + ) + elif isinstance(error, temporalio.exceptions.NexusOperationError): + failure.nexus_operation_execution_failure_info.SetInParent() + failure.nexus_operation_execution_failure_info.scheduled_event_id = ( + error.scheduled_event_id + ) + failure.nexus_operation_execution_failure_info.endpoint = error.endpoint + failure.nexus_operation_execution_failure_info.service = error.service + failure.nexus_operation_execution_failure_info.operation = error.operation + failure.nexus_operation_execution_failure_info.operation_token = ( + error.operation_token + ) + + def _nexus_handler_error_to_failure( + self, + error: nexusrpc.HandlerError, + payload_converter: PayloadConverter, + failure: temporalio.api.failure.v1.Failure, + ) -> None: + if error.original_failure: + self._nexus_failure_to_temporal_failure( + error.original_failure, error.retryable, failure + ) + else: + failure.message = error.message + if stack_trace := error.stack_trace: + failure.stack_trace = stack_trace + elif tb := error.__traceback__: + failure.stack_trace = "\n".join(traceback.format_tb(tb)) + if error.__cause__: + self.to_failure(error.__cause__, payload_converter, failure.cause) + failure.nexus_handler_failure_info.SetInParent() + failure.nexus_handler_failure_info.type = error.type.name + failure.nexus_handler_failure_info.retry_behavior = temporalio.api.enums.v1.NexusHandlerErrorRetryBehavior.ValueType( + temporalio.api.enums.v1.NexusHandlerErrorRetryBehavior.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_RETRYABLE + if error.retryable_override is True + else temporalio.api.enums.v1.NexusHandlerErrorRetryBehavior.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_NON_RETRYABLE + if error.retryable_override is False + else temporalio.api.enums.v1.NexusHandlerErrorRetryBehavior.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_UNSPECIFIED + ) + + def _temporal_failure_to_nexus_failure( + self, failure: temporalio.api.failure.v1.Failure + ) -> nexusrpc.Failure: + message, failure.message = failure.message, "" + stack_trace, failure.stack_trace = failure.stack_trace, "" + failure_dict = google.protobuf.json_format.MessageToDict(failure) + failure.message = message + failure.stack_trace = stack_trace + return nexusrpc.Failure( + message=message, + stack_trace=stack_trace, + metadata={ + "type": _TEMPORAL_FAILURE_PROTO_TYPE, + }, + details=failure_dict, + ) + + def _nexus_failure_to_temporal_failure( + self, + failure: nexusrpc.Failure, + retryable: bool, + temporal_failure: temporalio.api.failure.v1.Failure, + ) -> None: + if ( + failure.metadata + and failure.metadata.get("type") == _TEMPORAL_FAILURE_PROTO_TYPE + ): + google.protobuf.json_format.ParseDict( + dict(failure.details or {}), temporal_failure + ) + else: + temporal_failure.application_failure_info.SetInParent() + temporal_failure.application_failure_info.type = "NexusFailure" + temporal_failure.application_failure_info.non_retryable = not retryable + temporal_failure.application_failure_info.details.SetInParent() + temporal_failure.application_failure_info.details.payloads.append( + temporalio.api.common.v1.Payload( + metadata={"encoding": b"json/plain"}, + data=json.dumps( + dataclasses.replace(failure, message=""), separators=(",", ":") + ).encode("utf-8"), + ) + ) + + temporal_failure.message = failure.message + temporal_failure.stack_trace = failure.stack_trace or "" + + def from_failure( + self, + failure: temporalio.api.failure.v1.Failure, + payload_converter: PayloadConverter, + ) -> BaseException: + """See base class.""" + # If encoded attributes are present and have the fields we expect, + # extract them + if failure.HasField("encoded_attributes"): + # Clone the failure to not mutate the incoming failure + new_failure = temporalio.api.failure.v1.Failure() + new_failure.CopyFrom(failure) + failure = new_failure + try: + encoded_attributes: dict[str, Any] = payload_converter.from_payloads( + [failure.encoded_attributes] + )[0] + if isinstance(encoded_attributes, dict): + message = encoded_attributes.get("message") + if isinstance(message, str): + failure.message = message + stack_trace = encoded_attributes.get("stack_trace") + if isinstance(stack_trace, str): + failure.stack_trace = stack_trace + except: + pass + + err: temporalio.exceptions.FailureError | nexusrpc.HandlerError + match failure.WhichOneof("failure_info"): + case "application_failure_info": + app_info = failure.application_failure_info + err = temporalio.exceptions.ApplicationError( + failure.message or "Application error", + *payload_converter.from_payloads_wrapper(app_info.details), + type=app_info.type or None, + non_retryable=app_info.non_retryable, + next_retry_delay=app_info.next_retry_delay.ToTimedelta(), + category=temporalio.exceptions.ApplicationErrorCategory( + int(app_info.category) + ), + ) + + case "timeout_failure_info": + timeout_info = failure.timeout_failure_info + err = temporalio.exceptions.TimeoutError( + failure.message or "Timeout", + type=temporalio.exceptions.TimeoutType( + int(timeout_info.timeout_type) + ) + if timeout_info.timeout_type + else None, + last_heartbeat_details=payload_converter.from_payloads_wrapper( + timeout_info.last_heartbeat_details + ), + ) + + case "canceled_failure_info": + cancel_info = failure.canceled_failure_info + err = temporalio.exceptions.CancelledError( + failure.message or "Cancelled", + *payload_converter.from_payloads_wrapper(cancel_info.details), + ) + case "terminated_failure_info": + err = temporalio.exceptions.TerminatedError( + failure.message or "Terminated" + ) + + case "server_failure_info": + server_info = failure.server_failure_info + err = temporalio.exceptions.ServerError( + failure.message or "Server error", + non_retryable=server_info.non_retryable, + ) + + case "activity_failure_info": + act_info = failure.activity_failure_info + err = temporalio.exceptions.ActivityError( + failure.message or "Activity error", + scheduled_event_id=act_info.scheduled_event_id, + started_event_id=act_info.started_event_id, + identity=act_info.identity, + activity_type=act_info.activity_type.name, + activity_id=act_info.activity_id, + retry_state=temporalio.exceptions.RetryState( + int(act_info.retry_state) + ) + if act_info.retry_state + else None, + ) + + case "child_workflow_execution_failure_info": + child_info = failure.child_workflow_execution_failure_info + err = temporalio.exceptions.ChildWorkflowError( + failure.message or "Child workflow error", + namespace=child_info.namespace, + workflow_id=child_info.workflow_execution.workflow_id, + run_id=child_info.workflow_execution.run_id, + workflow_type=child_info.workflow_type.name, + initiated_event_id=child_info.initiated_event_id, + started_event_id=child_info.started_event_id, + retry_state=temporalio.exceptions.RetryState( + int(child_info.retry_state) + ) + if child_info.retry_state + else None, + ) + + case "nexus_handler_failure_info": + nexus_handler_failure_info = failure.nexus_handler_failure_info + try: + _type = nexusrpc.HandlerErrorType[nexus_handler_failure_info.type] + except KeyError: + logger.warning( + f"Unknown Nexus HandlerErrorType: {nexus_handler_failure_info.type}" + ) + _type = nexusrpc.HandlerErrorType.INTERNAL + + retryable_override: bool | None + match nexus_handler_failure_info.retry_behavior: + case temporalio.api.enums.v1.NexusHandlerErrorRetryBehavior.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_RETRYABLE: + retryable_override = True + case temporalio.api.enums.v1.NexusHandlerErrorRetryBehavior.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_NON_RETRYABLE: + retryable_override = False + case _: + retryable_override = None + + err = nexusrpc.HandlerError( + failure.message or "Nexus handler error", + type=_type, + retryable_override=retryable_override, + stack_trace=failure.stack_trace if failure.stack_trace else None, + original_failure=self._temporal_failure_to_nexus_failure(failure), + ) + + case "nexus_operation_execution_failure_info": + nexus_op_failure_info = failure.nexus_operation_execution_failure_info + err = temporalio.exceptions.NexusOperationError( + failure.message or "Nexus operation error", + scheduled_event_id=nexus_op_failure_info.scheduled_event_id, + endpoint=nexus_op_failure_info.endpoint, + service=nexus_op_failure_info.service, + operation=nexus_op_failure_info.operation, + operation_token=nexus_op_failure_info.operation_token, + ) + + case "reset_workflow_failure_info" | None: + err = temporalio.exceptions.FailureError( + failure.message or "Failure error", + ) + + if isinstance(err, temporalio.exceptions.FailureError): + err._failure = failure + if failure.HasField("cause"): + err.__cause__ = self.from_failure(failure.cause, payload_converter) + return err + + +class DefaultFailureConverterWithEncodedAttributes(DefaultFailureConverter): + """Implementation of :py:class:`DefaultFailureConverter` which moves message + and stack trace to encoded attributes subject to a codec. + """ + + def __init__(self) -> None: + """Create a default failure converter with encoded attributes.""" + super().__init__(encode_common_attributes=True) diff --git a/temporalio/converter/_payload_codec.py b/temporalio/converter/_payload_codec.py new file mode 100644 index 000000000..93f689509 --- /dev/null +++ b/temporalio/converter/_payload_codec.py @@ -0,0 +1,115 @@ +"""PayloadCodec and failure payload traversal.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Awaitable, Callable, Sequence + +import temporalio.api.common.v1 +import temporalio.api.failure.v1 + + +class PayloadCodec(ABC): + """Codec for encoding/decoding to/from bytes. + + Commonly used for compression or encryption. + """ + + @abstractmethod + async def encode( + self, payloads: Sequence[temporalio.api.common.v1.Payload] + ) -> list[temporalio.api.common.v1.Payload]: + """Encode the given payloads. + + Args: + payloads: Payloads to encode. This value should not be mutated. + + Returns: + Encoded payloads. Note, this does not have to be the same number as + payloads given, but must be at least one and cannot be more than was + given. + """ + raise NotImplementedError + + @abstractmethod + async def decode( + self, payloads: Sequence[temporalio.api.common.v1.Payload] + ) -> list[temporalio.api.common.v1.Payload]: + """Decode the given payloads. + + Args: + payloads: Payloads to decode. This value should not be mutated. + + Returns: + Decoded payloads. Note, this does not have to be the same number as + payloads given, but must be at least one and cannot be more than was + given. + """ + raise NotImplementedError + + async def encode_wrapper(self, payloads: temporalio.api.common.v1.Payloads) -> None: + """:py:meth:`encode` for the + :py:class:`temporalio.api.common.v1.Payloads` wrapper. + + This replaces the payloads within the wrapper. + """ + new_payloads = await self.encode(payloads.payloads) + del payloads.payloads[:] + # TODO(cretz): Copy too expensive? + payloads.payloads.extend(new_payloads) + + async def decode_wrapper(self, payloads: temporalio.api.common.v1.Payloads) -> None: + """:py:meth:`decode` for the + :py:class:`temporalio.api.common.v1.Payloads` wrapper. + + This replaces the payloads within. + """ + new_payloads = await self.decode(payloads.payloads) + del payloads.payloads[:] + # TODO(cretz): Copy too expensive? + payloads.payloads.extend(new_payloads) + + async def encode_failure(self, failure: temporalio.api.failure.v1.Failure) -> None: + """Encode payloads of a failure. Intended as a helper method, not for overriding. + It is not guaranteed that all failures will be encoded with this method rather + than encoding the underlying payloads. + """ + await _apply_to_failure_payloads(failure, self.encode_wrapper) + + async def decode_failure(self, failure: temporalio.api.failure.v1.Failure) -> None: + """Decode payloads of a failure. Intended as a helper method, not for overriding. + It is not guaranteed that all failures will be decoded with this method rather + than decoding the underlying payloads. + """ + await _apply_to_failure_payloads(failure, self.decode_wrapper) + + +async def _apply_to_failure_payloads( + failure: temporalio.api.failure.v1.Failure, + cb: Callable[[temporalio.api.common.v1.Payloads], Awaitable[None]], +) -> None: + if failure.HasField("encoded_attributes"): + # Wrap in payloads and merge back + payloads = temporalio.api.common.v1.Payloads( + payloads=[failure.encoded_attributes] + ) + await cb(payloads) + failure.encoded_attributes.CopyFrom(payloads.payloads[0]) + if failure.HasField( + "application_failure_info" + ) and failure.application_failure_info.HasField("details"): + await cb(failure.application_failure_info.details) + elif failure.HasField( + "timeout_failure_info" + ) and failure.timeout_failure_info.HasField("last_heartbeat_details"): + await cb(failure.timeout_failure_info.last_heartbeat_details) + elif failure.HasField( + "canceled_failure_info" + ) and failure.canceled_failure_info.HasField("details"): + await cb(failure.canceled_failure_info.details) + elif failure.HasField( + "reset_workflow_failure_info" + ) and failure.reset_workflow_failure_info.HasField("last_heartbeat_details"): + await cb(failure.reset_workflow_failure_info.last_heartbeat_details) + if failure.HasField("cause"): + await _apply_to_failure_payloads(failure.cause, cb) diff --git a/temporalio/converter/_payload_converter.py b/temporalio/converter/_payload_converter.py new file mode 100644 index 000000000..a8bc35e28 --- /dev/null +++ b/temporalio/converter/_payload_converter.py @@ -0,0 +1,1101 @@ +"""Payload converter types and implementations for data conversion.""" + +from __future__ import annotations + +import collections +import collections.abc +import dataclasses +import functools +import inspect +import json +import sys +import typing +import uuid +import warnings +from abc import ABC, abstractmethod +from collections.abc import Callable, Mapping, Sequence +from datetime import datetime +from enum import IntEnum +from itertools import zip_longest +from types import UnionType +from typing import ( + Any, + ClassVar, + Generic, + Literal, + NewType, + TypeVar, + get_type_hints, + overload, +) + +import google.protobuf.json_format +import google.protobuf.message +import google.protobuf.symbol_database +import typing_extensions +from typing_extensions import Self + +import temporalio.api.common.v1 +import temporalio.common +import temporalio.types + +if sys.version_info < (3, 11): + # Python's datetime.fromisoformat doesn't support certain formats pre-3.11 + from dateutil import parser # type: ignore +# StrEnum is available in 3.11+ +if sys.version_info >= (3, 11): + from enum import StrEnum # type: ignore[reportUnreachable] + +from temporalio.converter._serialization_context import ( + SerializationContext, + WithSerializationContext, +) + +_sym_db = google.protobuf.symbol_database.Default() +ValueT = TypeVar("ValueT") +TransferTypeT = TypeVar("TransferTypeT") +_TRANSFER_TYPE_CONVERTER_ATTR = "__temporal_transfer_type_converter" + + +class TransferTypeConverter(Generic[ValueT, TransferTypeT], ABC): + """Converter between a user-facing value and a transfer type value. + + .. warning:: + This API is experimental and subject to change. + """ + + transfer_type: type[TransferTypeT] | None = None + """Optional type hint for the transfer type to use when decoding payloads. + + .. warning:: + This API is experimental and subject to change. + """ + + @abstractmethod + def to_transfer_type(self, value: ValueT) -> TransferTypeT: + """Convert a user-facing value to its transfer type value. + + .. warning:: + This API is experimental and subject to change. + """ + raise NotImplementedError + + @abstractmethod + def from_transfer_type( + self, value: TransferTypeT, type_hint: type[ValueT] + ) -> ValueT: + """Convert a transfer type value to its user-facing value. + + ``type_hint`` is the requested user-facing type, including concrete + generic arguments. + + .. warning:: + This API is experimental and subject to change. + """ + raise NotImplementedError + + +class _TransferTypeConvertibleDecorator(Generic[ValueT, TransferTypeT]): + def __init__( + self, converter_type: type[TransferTypeConverter[ValueT, TransferTypeT]] + ) -> None: + self._converter_type = converter_type + + def __call__(self, cls: type[ValueT]) -> type[ValueT]: + if hasattr(cls, _TRANSFER_TYPE_CONVERTER_ATTR): + raise TypeError("class already has a transfer type converter") + setattr(cls, _TRANSFER_TYPE_CONVERTER_ATTR, self._converter_type()) + return cls + + +def transfer_type_convertible( + converter_type: type[TransferTypeConverter[ValueT, TransferTypeT]], +) -> _TransferTypeConvertibleDecorator[ValueT, TransferTypeT]: + """Decorate a class with a transfer type converter class. + + .. warning:: + This API is experimental and subject to change. + """ + return _TransferTypeConvertibleDecorator(converter_type) + + +def _get_transfer_type_converter( + value_type: object, +) -> TransferTypeConverter[Any, Any] | None: + converter = getattr(value_type, _TRANSFER_TYPE_CONVERTER_ATTR, None) + if isinstance(converter, TransferTypeConverter): + return converter + return None + + +class PayloadConverter(ABC): + """Base payload converter to/from multiple payloads/values.""" + + default: ClassVar[PayloadConverter] + """Default payload converter.""" + + @abstractmethod + def to_payloads( + self, values: Sequence[Any] + ) -> list[temporalio.api.common.v1.Payload]: + """Encode values into payloads. + + Implementers are expected to just return the payload for + :py:class:`temporalio.common.RawValue`. + + Args: + values: Values to be converted. + + Returns: + Converted payloads. Note, this does not have to be the same number + as values given, but must be at least one and cannot be more than + was given. + + Raises: + Exception: Any issue during conversion. + """ + raise NotImplementedError + + @abstractmethod + def from_payloads( + self, + payloads: Sequence[temporalio.api.common.v1.Payload], + type_hints: list[type] | None = None, + ) -> list[Any]: + """Decode payloads into values. + + Implementers are expected to treat a type hint of + :py:class:`temporalio.common.RawValue` as just the raw value. + + Args: + payloads: Payloads to convert to Python values. + type_hints: Types that are expected if any. This may not have any + types if there are no annotations on the target. If this is + present, it must have the exact same length as payloads even if + the values are just "object". + + Returns: + Collection of Python values. Note, this does not have to be the same + number as values given, but at least one must be present. + + Raises: + Exception: Any issue during conversion. + """ + raise NotImplementedError + + def to_payloads_wrapper( + self, values: Sequence[Any] + ) -> temporalio.api.common.v1.Payloads: + """:py:meth:`to_payloads` for the + :py:class:`temporalio.api.common.v1.Payloads` wrapper. + """ + return temporalio.api.common.v1.Payloads(payloads=self.to_payloads(values)) + + def from_payloads_wrapper( + self, payloads: temporalio.api.common.v1.Payloads | None + ) -> list[Any]: + """:py:meth:`from_payloads` for the + :py:class:`temporalio.api.common.v1.Payloads` wrapper. + """ + if not payloads or not payloads.payloads: + return [] + return self.from_payloads(payloads.payloads) + + def to_payload(self, value: Any) -> temporalio.api.common.v1.Payload: + """Convert a single value to a payload. + + This is a shortcut for :py:meth:`to_payloads` with a single-item list + and result. + + Args: + value: Value to convert to a single payload. + + Returns: + Single converted payload. + """ + return self.to_payloads([value])[0] + + @overload + def from_payload(self, payload: temporalio.api.common.v1.Payload) -> Any: ... + + @overload + def from_payload( + self, + payload: temporalio.api.common.v1.Payload, + type_hint: type[temporalio.types.AnyType], + ) -> temporalio.types.AnyType: ... + + def from_payload( + self, + payload: temporalio.api.common.v1.Payload, + type_hint: type | None = None, + ) -> Any: + """Convert a single payload to a value. + + This is a shortcut for :py:meth:`from_payloads` with a single-item list + and result. + + Args: + payload: Payload to convert to value. + type_hint: Optional type hint to say which type to convert to. + + Returns: + Single converted value. + """ + return self.from_payloads([payload], [type_hint] if type_hint else None)[0] + + +class EncodingPayloadConverter(ABC): + """Base converter to/from single payload/value with a known encoding for use in CompositePayloadConverter.""" + + @property + @abstractmethod + def encoding(self) -> str: + """Encoding for the payload this converter works with.""" + raise NotImplementedError + + @abstractmethod + def to_payload(self, value: Any) -> temporalio.api.common.v1.Payload | None: + """Encode a single value to a payload or None. + + Args: + value: Value to be converted. + + Returns: + Payload of the value or None if unable to convert. + + Raises: + TypeError: Value is not the expected type. + ValueError: Value is of the expected type but otherwise incorrect. + RuntimeError: General error during encoding. + """ + raise NotImplementedError + + @abstractmethod + def from_payload( + self, + payload: temporalio.api.common.v1.Payload, + type_hint: type | None = None, + ) -> Any: + """Decode a single payload to a Python value or raise exception. + + Args: + payload: Payload to convert to Python value. + type_hint: Type that is expected if any. This may not have a type if + there are no annotations on the target. + + Return: + The decoded value from the payload. Since the encoding is checked by + the caller, this should raise an exception if the payload cannot be + converted. + + Raises: + RuntimeError: General error during decoding. + """ + raise NotImplementedError + + +class CompositePayloadConverter(PayloadConverter, WithSerializationContext): + """Composite payload converter that delegates to a list of encoding payload converters. + + Encoding/decoding are attempted on each payload converter successively until + it succeeds. + + Attributes: + converters: List of payload converters to delegate to, in order. + """ + + converters: Mapping[bytes, EncodingPayloadConverter] + + def __init__(self, *converters: EncodingPayloadConverter) -> None: + """Initializes the data converter. + + Args: + converters: Payload converters to delegate to, in order. + """ + self._set_converters(*converters) + + def _set_converters(self, *converters: EncodingPayloadConverter) -> None: + self.converters = {c.encoding.encode(): c for c in converters} + + def to_payloads( + self, values: Sequence[Any] + ) -> list[temporalio.api.common.v1.Payload]: + """Encode values trying each converter. + + See base class. Always returns the same number of payloads as values. + + Raises: + RuntimeError: No known converter + """ + payloads = [] + for index, value in enumerate(values): + # We intentionally attempt these serially just in case a stateful + # converter may rely on the previous values + payload = None + # RawValue should just pass through + if isinstance(value, temporalio.common.RawValue): + payload = value.payload + else: + for converter in self.converters.values(): + payload = converter.to_payload(value) + if payload is not None: + break + if payload is None: + raise RuntimeError( + f"Value at index {index} of type {type(value)} has no known converter" + ) + payloads.append(payload) + return payloads + + def from_payloads( + self, + payloads: Sequence[temporalio.api.common.v1.Payload], + type_hints: list[type] | None = None, + ) -> list[Any]: + """Decode values trying each converter. + + See base class. Always returns the same number of values as payloads. + + Raises: + KeyError: Unknown payload encoding + RuntimeError: Error during decode + """ + values = [] + type_hints = type_hints or [] + for index, (payload, type_hint) in enumerate(zip_longest(payloads, type_hints)): + # Raw value should just wrap + if type_hint == temporalio.common.RawValue: + values.append(temporalio.common.RawValue(payload)) + continue + encoding = payload.metadata.get("encoding", b"") + converter = self.converters.get(encoding) + if converter is None: + raise KeyError(f"Unknown payload encoding {encoding.decode()}") + try: + values.append(converter.from_payload(payload, type_hint)) + except RuntimeError as err: + raise RuntimeError( + f"Payload at index {index} with encoding {encoding.decode()} could not be converted" + ) from err + return values + + def with_context(self, context: SerializationContext) -> Self: + """Return a new instance with context set on the component converters. + + If none of the component converters returned new instances, return self. + """ + converters = self.get_converters_with_context(context) + if converters is None: + return self + new_instance = type(self)() # Must have a nullary constructor + new_instance._set_converters(*converters) + return new_instance + + def get_converters_with_context( + self, context: SerializationContext + ) -> list[EncodingPayloadConverter] | None: + """Return converter instances with context set. + + If no converter uses context, return None. + """ + if not self._any_converter_takes_context: + return None + converters: list[EncodingPayloadConverter] = [] + any_with_context = False + for c in self.converters.values(): + if isinstance(c, WithSerializationContext): + converters.append(c.with_context(context)) + any_with_context |= converters[-1] is not c + else: + converters.append(c) + + return converters if any_with_context else None + + @functools.cached_property + def _any_converter_takes_context(self) -> bool: + return any( + isinstance(c, WithSerializationContext) for c in self.converters.values() + ) + + +class DefaultPayloadConverter(CompositePayloadConverter): + """Default payload converter compatible with other Temporal SDKs. + + This handles None, bytes, all protobuf message types, and any type that + :py:func:`json.dump` accepts. A singleton instance of this is available at + :py:attr:`PayloadConverter.default`. + """ + + default_encoding_payload_converters: tuple[EncodingPayloadConverter, ...] + """Default set of encoding payload converters the default payload converter + uses. + """ + + def __init__(self) -> None: + """Create a default payload converter.""" + super().__init__(*DefaultPayloadConverter.default_encoding_payload_converters) + + +class BinaryNullPayloadConverter(EncodingPayloadConverter): + """Converter for 'binary/null' payloads supporting None values.""" + + @property + def encoding(self) -> str: + """See base class.""" + return "binary/null" + + def to_payload(self, value: Any) -> temporalio.api.common.v1.Payload | None: + """See base class.""" + if value is None: + return temporalio.api.common.v1.Payload( + metadata={"encoding": self.encoding.encode()} + ) + return None + + def from_payload( + self, + payload: temporalio.api.common.v1.Payload, + type_hint: type | None = None, + ) -> Any: + """See base class.""" + if len(payload.data) > 0: + raise RuntimeError("Expected empty data set for binary/null") + return None + + +class BinaryPlainPayloadConverter(EncodingPayloadConverter): + """Converter for 'binary/plain' payloads supporting bytes values.""" + + @property + def encoding(self) -> str: + """See base class.""" + return "binary/plain" + + def to_payload(self, value: Any) -> temporalio.api.common.v1.Payload | None: + """See base class.""" + if isinstance(value, bytes): + return temporalio.api.common.v1.Payload( + metadata={"encoding": self.encoding.encode()}, data=value + ) + return None + + def from_payload( + self, + payload: temporalio.api.common.v1.Payload, + type_hint: type | None = None, + ) -> Any: + """See base class.""" + return payload.data + + +class JSONProtoPayloadConverter(EncodingPayloadConverter): + """Converter for 'json/protobuf' payloads supporting protobuf Message values.""" + + def __init__(self, ignore_unknown_fields: bool = False): + """Initialize a JSON proto converter. + + Args: + ignore_unknown_fields: Determines whether converter should error if + unknown fields are detected + """ + super().__init__() + self._ignore_unknown_fields = ignore_unknown_fields + + @property + def encoding(self) -> str: + """See base class.""" + return "json/protobuf" + + def to_payload(self, value: Any) -> temporalio.api.common.v1.Payload | None: + """See base class.""" + if ( + isinstance(value, google.protobuf.message.Message) + and value.DESCRIPTOR is not None # type:ignore[reportUnnecessaryComparison] + ): + # We have to convert to dict then to JSON because MessageToJson does + # not have a compact option removing spaces and newlines + json_str = json.dumps( + google.protobuf.json_format.MessageToDict(value), + separators=(",", ":"), + sort_keys=True, + ) + return temporalio.api.common.v1.Payload( + metadata={ + "encoding": self.encoding.encode(), + "messageType": value.DESCRIPTOR.full_name.encode(), + }, + data=json_str.encode(), + ) + return None + + def from_payload( + self, + payload: temporalio.api.common.v1.Payload, + type_hint: type | None = None, + ) -> Any: + """See base class.""" + message_type = payload.metadata.get("messageType", b"").decode() + try: + value = _sym_db.GetSymbol(message_type)() + return google.protobuf.json_format.Parse( + payload.data, + value, + ignore_unknown_fields=self._ignore_unknown_fields, + ) + except KeyError as err: + raise RuntimeError(f"Unknown Protobuf type {message_type}") from err + except google.protobuf.json_format.ParseError as err: + raise RuntimeError("Failed parsing") from err + + +class BinaryProtoPayloadConverter(EncodingPayloadConverter): + """Converter for 'binary/protobuf' payloads supporting protobuf Message values.""" + + @property + def encoding(self) -> str: + """See base class.""" + return "binary/protobuf" + + def to_payload(self, value: Any) -> temporalio.api.common.v1.Payload | None: + """See base class.""" + if ( + isinstance(value, google.protobuf.message.Message) + and value.DESCRIPTOR is not None # type:ignore[reportUnnecessaryComparison] + ): + return temporalio.api.common.v1.Payload( + metadata={ + "encoding": self.encoding.encode(), + "messageType": value.DESCRIPTOR.full_name.encode(), + }, + data=value.SerializeToString(), + ) + return None + + def from_payload( + self, + payload: temporalio.api.common.v1.Payload, + type_hint: type | None = None, + ) -> Any: + """See base class.""" + message_type = payload.metadata.get("messageType", b"").decode() + try: + value = _sym_db.GetSymbol(message_type)() + value.ParseFromString(payload.data) + return value + except KeyError as err: + raise RuntimeError(f"Unknown Protobuf type {message_type}") from err + except google.protobuf.message.DecodeError as err: + raise RuntimeError("Failed parsing") from err + + +class _TemporalTransferTypePayloadConverter(PayloadConverter, WithSerializationContext): + """Payload converter wrapper for registered Temporal transfer type converters. + + Values with a registered transfer type converter are first converted to their + transfer type value, then encoded by the wrapped payload converter. When + decoding to a type with a registered transfer type converter, the wrapped + converter first decodes the payload to the transfer type value and this wrapper + constructs the requested user-facing type from it. + """ + + _inner_payload_converter: PayloadConverter + + def __init__(self, inner_payload_converter: PayloadConverter) -> None: + """Create a Temporal transfer type payload converter.""" + self._inner_payload_converter = inner_payload_converter + + @staticmethod + def wrap(payload_converter: PayloadConverter) -> PayloadConverter: + """Wrap a payload converter unless it is already wrapped.""" + if isinstance(payload_converter, _TemporalTransferTypePayloadConverter): + return payload_converter + return _TemporalTransferTypePayloadConverter(payload_converter) + + def to_payloads( + self, values: Sequence[Any] + ) -> list[temporalio.api.common.v1.Payload]: + """See base class.""" + transfer_type_values: list[Any] = [] + for value in values: + converter = _get_transfer_type_converter(type(value)) + if converter is not None: + value = converter.to_transfer_type(value) + transfer_type_values.append(value) + return self._inner_payload_converter.to_payloads(transfer_type_values) + + def from_payloads( + self, + payloads: Sequence[temporalio.api.common.v1.Payload], + type_hints: list[type] | None = None, + ) -> list[Any]: + """See base class.""" + if type_hints is None: + return self._inner_payload_converter.from_payloads(payloads, None) + converters = [ + _get_transfer_type_converter(type_hint) for type_hint in type_hints + ] + inner_type_hints = [ + converter.transfer_type if converter is not None else type_hint + for converter, type_hint in zip(converters, type_hints) + ] + values = self._inner_payload_converter.from_payloads( + payloads, typing.cast("list[type]", inner_type_hints) + ) + return [ + converter.from_transfer_type(value, type_hint) + if converter is not None + else value + for value, converter, type_hint in zip(values, converters, type_hints) + ] + + def with_context(self, context: SerializationContext) -> Self: + """Return a new instance with context set on the inner converter.""" + if not isinstance(self._inner_payload_converter, WithSerializationContext): + return self + inner_payload_converter = self._inner_payload_converter.with_context(context) + if inner_payload_converter is self._inner_payload_converter: + return self + return type(self)(inner_payload_converter) + + +class AdvancedJSONEncoder(json.JSONEncoder): + """Advanced JSON encoder. + + This encoder supports dataclasses and all iterables as lists. + + It also uses Pydantic v1's "dict" methods if available on the object, + but this is deprecated. Pydantic users should upgrade to v2 and use + temporalio.contrib.pydantic.pydantic_data_converter. + """ + + def default(self, o: Any) -> Any: + """Override JSON encoding default. + + See :py:meth:`json.JSONEncoder.default`. + """ + # Datetime support + if isinstance(o, datetime): + return o.isoformat() + # Dataclass support + if dataclasses.is_dataclass(o) and not isinstance(o, type): + return dataclasses.asdict(o) + # Support for Pydantic v1's dict method + dict_fn = getattr(o, "dict", None) + if callable(dict_fn): + return dict_fn() + # Support for non-list iterables like set + if not isinstance(o, list) and isinstance(o, collections.abc.Iterable): + return list(o) + # Support for UUID + if isinstance(o, uuid.UUID): + return str(o) + return super().default(o) + + +JSONTypeConverterUnhandled = NewType("JSONTypeConverterUnhandled", object) +"""Type of :py:attr:`JSONTypeConverter.Unhandled`.""" + +_JSONTypeConverterUnhandled = JSONTypeConverterUnhandled + + +class JSONTypeConverter(ABC): + """Converter for converting an object from Python :py:func:`json.loads` + result (e.g. scalar, list, or dict) to a known type. + """ + + Unhandled: ClassVar[JSONTypeConverterUnhandled] = JSONTypeConverterUnhandled( + object() + ) + """Sentinel value that must be used as the result of + :py:meth:`to_typed_value` to say the given type is not handled by this + converter.""" + + @abstractmethod + def to_typed_value( + self, hint: type, value: Any + ) -> Any | None | JSONTypeConverterUnhandled: + """Convert the given value to a type based on the given hint. + + Args: + hint: Type hint to use to help in converting the value. + value: Value as returned by :py:func:`json.loads`. Usually a scalar, + list, or dict. + + Returns: + The converted value or :py:attr:`Unhandled` if this converter does + not handle this situation. + """ + raise NotImplementedError + + +class JSONPlainPayloadConverter(EncodingPayloadConverter): + """Converter for 'json/plain' payloads supporting common Python values. + + For encoding, this supports all values that :py:func:`json.dump` supports + and by default adds extra encoding support for dataclasses, classes with + ``dict()`` methods, and all iterables. + + For decoding, this uses type hints to attempt to rebuild the type from the + type hint. + """ + + _encoder: type[json.JSONEncoder] | None + _decoder: type[json.JSONDecoder] | None + _encoding: str + + def __init__( + self, + *, + encoder: type[json.JSONEncoder] | None = AdvancedJSONEncoder, + decoder: type[json.JSONDecoder] | None = None, + encoding: str = "json/plain", + custom_type_converters: Sequence[JSONTypeConverter] = [], + ) -> None: + """Initialize a JSON data converter. + + Args: + encoder: Custom encoder class object to use. + decoder: Custom decoder class object to use. + encoding: Encoding name to use. + custom_type_converters: Set of custom type converters that are used + when converting from a payload to type-hinted values. + """ + super().__init__() + self._encoder = encoder + self._decoder = decoder + self._encoding = encoding + self._custom_type_converters = custom_type_converters + + @property + def encoding(self) -> str: + """See base class.""" + return self._encoding + + def to_payload(self, value: Any) -> temporalio.api.common.v1.Payload | None: + """See base class.""" + # Check for Pydantic v1 + if hasattr(value, "parse_obj"): + warnings.warn( + "If you're using Pydantic v2, use temporalio.contrib.pydantic.pydantic_data_converter. " + "If you're using Pydantic v1 and cannot upgrade, refer to https://github.com/temporalio/samples-python/tree/main/pydantic_converter_v1 for better v1 support." + ) + # We let JSON conversion errors be thrown to caller + return temporalio.api.common.v1.Payload( + metadata={"encoding": self._encoding.encode()}, + data=json.dumps( + value, cls=self._encoder, separators=(",", ":"), sort_keys=True + ).encode(), + ) + + def from_payload( + self, + payload: temporalio.api.common.v1.Payload, + type_hint: type | None = None, + ) -> Any: + """See base class.""" + try: + obj = json.loads(payload.data, cls=self._decoder) + if type_hint: + obj = value_to_type(type_hint, obj, self._custom_type_converters) + return obj + except json.JSONDecodeError as err: + raise RuntimeError("Failed parsing") from err + + +def _get_iso_datetime_parser() -> Callable[[str], datetime]: + """Isolates system version check and returns relevant datetime passer + + Returns: + A callable to parse date strings into datetimes. + """ + if sys.version_info >= (3, 11): + return datetime.fromisoformat # type:ignore[reportUnreachable] # noqa + else: + # Isolate import for py > 3.11, as dependency only installed for < 3.11 + return parser.isoparse # type:ignore[reportUnreachable] + + +def value_to_type( + hint: type, + value: Any, + custom_converters: Sequence[JSONTypeConverter] = [], +) -> Any: + """Convert a given value to the given type hint. + + This is used internally to convert a raw JSON loaded value to a specific + type hint. + + Args: + hint: Type hint to convert the value to. + value: Raw value (e.g. primitive, dict, or list) to convert from. + custom_converters: Set of custom converters to try before doing default + conversion. Converters are tried in order and the first value that + is not :py:attr:`JSONTypeConverter.Unhandled` will be returned from + this function instead of doing default behavior. + + Returns: + Converted value. + + Raises: + TypeError: Unable to convert to the given hint. + """ + # Try custom converters + for conv in custom_converters: + ret = conv.to_typed_value(hint, value) + if ret is not JSONTypeConverter.Unhandled: + return ret + + # Any or primitives + if hint is Any: + return value + elif hint is datetime: + if isinstance(value, str): + try: + return _get_iso_datetime_parser()(value) + except ValueError as err: + raise TypeError(f"Failed parsing datetime string: {value}") from err + elif isinstance(value, datetime): + return value + raise TypeError(f"Expected datetime or ISO8601 string, got {type(value)}") + elif hint is int or hint is float: + if not isinstance(value, (int, float)): + raise TypeError(f"Expected value to be int|float, was {type(value)}") + return hint(value) + elif hint is bool: + if not isinstance(value, bool): + raise TypeError(f"Expected value to be bool, was {type(value)}") + return bool(value) + elif hint is str: + if not isinstance(value, str): + raise TypeError(f"Expected value to be str, was {type(value)}") + return str(value) + elif hint is bytes: + if not isinstance(value, (str, bytes, list)): + raise TypeError(f"Expected value to be bytes, was {type(value)}") + # In some other SDKs, this is serialized as a base64 string, but in + # Python this is a numeric array. + return bytes(value) # type: ignore + elif hint is type(None): + if value is not None: + raise TypeError(f"Expected None, got value of type {type(value)}") + return None + + # NewType. Note we cannot simply check isinstance NewType here because it's + # only been a class since 3.10. Instead we'll just check for the presence + # of a supertype. + supertype = getattr(hint, "__supertype__", None) + if supertype: + return value_to_type(supertype, value, custom_converters) + + # Load origin for other checks + origin = getattr(hint, "__origin__", hint) + type_args: tuple = getattr(hint, "__args__", ()) + + # Literal + if origin is Literal or origin is typing_extensions.Literal: + if value not in type_args: + raise TypeError(f"Value {value} not in literal values {type_args}") + return value + + is_union = origin is typing.Union # type:ignore[reportDeprecated] + is_union = is_union or isinstance(origin, UnionType) + + # Union + if is_union: + # Try each one. Note, Optional is just a union w/ none. + for arg in type_args: + try: + return value_to_type(arg, value, custom_converters) + except Exception: + pass + raise TypeError(f"Failed converting to {hint} from {value}") + + # Mapping + if inspect.isclass(origin) and issubclass(origin, collections.abc.Mapping): + if not isinstance(value, collections.abc.Mapping): + raise TypeError(f"Expected {hint}, value was {type(value)}") + ret_dict = {} + # If there are required or optional keys that means we are a TypedDict + # and therefore can extract per-key types + per_key_types: dict[str, type] | None = None + if getattr(origin, "__required_keys__", None) or getattr( + origin, "__optional_keys__", None + ): + per_key_types = get_type_hints(origin) + key_type = ( + type_args[0] + if len(type_args) > 0 + and type_args[0] is not Any + and not isinstance(type_args[0], TypeVar) + else None + ) + value_type = ( + type_args[1] + if len(type_args) > 1 + and type_args[1] is not Any + and not isinstance(type_args[1], TypeVar) + else None + ) + # Convert each key/value + for key, value in value.items(): + this_value_type = value_type + if per_key_types: + # TODO(cretz): Strict mode would fail an unknown key + this_value_type = per_key_types.get(key) + + if key_type: + # This function is used only by JSONPlainPayloadConverter. When + # serializing to JSON, Python supports key types str, int, float, bool, + # and None, serializing all to string representations. We now attempt to + # use the provided type annotation to recover the original value with its + # original type. + try: + if isinstance(key, str): + if key_type is int or key_type is float: + key = key_type(key) + elif key_type is bool: + key = {"true": True, "false": False}[key] + elif key_type is type(None): + key = {"null": None}[key] + + if not isinstance(key_type, type) or not isinstance(key, key_type): + key = value_to_type(key_type, key, custom_converters) + except Exception as err: + raise TypeError( + f"Failed converting key {repr(key)} to type {key_type} in mapping {hint}" + ) from err + + if this_value_type: + try: + value = value_to_type(this_value_type, value, custom_converters) + except Exception as err: + raise TypeError( + f"Failed converting value for key {repr(key)} in mapping {hint}" + ) from err + ret_dict[key] = value + # If there are per-key types, it's a typed dict and we want to attempt + # instantiation to get its validation + if per_key_types: + ret_dict = hint(**ret_dict) + return ret_dict + + # Dataclass + if dataclasses.is_dataclass(hint): + if not isinstance(value, dict): + raise TypeError( + f"Cannot convert to dataclass {hint}, value is {type(value)} not dict" + ) + # Obtain dataclass fields and check that all dict fields are there and + # that no required fields are missing. Unknown fields are silently + # ignored. + fields = dataclasses.fields(hint) + field_hints = get_type_hints(hint) + field_values = {} + for field in fields: + field_value = value.get(field.name, dataclasses.MISSING) + # We do not check whether field is required here. Rather, we let the + # attempted instantiation of the dataclass raise if a field is + # missing + if field_value is not dataclasses.MISSING: + try: + field_values[field.name] = value_to_type( + field_hints[field.name], field_value, custom_converters + ) + except Exception as err: + raise TypeError( + f"Failed converting field {field.name} on dataclass {hint}" + ) from err + # Simply instantiate the dataclass. This will fail as expected when + # missing required fields. + # TODO(cretz): Want way to convert snake case to camel case? + return hint(**field_values) + + # Pydantic model instance + # Pydantic users should use Pydantic v2 with + # temporalio.contrib.pydantic.pydantic_data_converter, in which case a + # pydantic model instance will have been handled by the custom_converters at + # the start of this function. We retain the following for backwards + # compatibility with pydantic v1 users, but this is deprecated. + parse_obj_attr = inspect.getattr_static(hint, "parse_obj", None) + if isinstance(parse_obj_attr, classmethod) or isinstance( + parse_obj_attr, staticmethod + ): + if not isinstance(value, dict): + raise TypeError( + f"Cannot convert to {hint}, value is {type(value)} not dict" + ) + return getattr(hint, "parse_obj")(value) + + # IntEnum + if inspect.isclass(hint) and issubclass(hint, IntEnum): + if not isinstance(value, int): + raise TypeError( + f"Cannot convert to enum {hint}, value not an integer, value is {type(value)}" + ) + return hint(value) + + # StrEnum, available in 3.11+ + if sys.version_info >= (3, 11): + if inspect.isclass(hint) and issubclass(hint, StrEnum): # type:ignore[reportUnreachable] + if not isinstance(value, str): + raise TypeError( + f"Cannot convert to enum {hint}, value not a string, value is {type(value)}" + ) + return hint(value) + + # UUID + if inspect.isclass(hint) and issubclass(hint, uuid.UUID): + return hint(value) + + # Iterable. We intentionally put this last as it catches several others. + if inspect.isclass(origin) and issubclass(origin, collections.abc.Iterable): + if not isinstance(value, collections.abc.Iterable): + raise TypeError(f"Expected {hint}, value was {type(value)}") + ret_list = [] + # If there is no type arg, just return value as is + if not type_args or ( + len(type_args) == 1 + and (isinstance(type_args[0], TypeVar) or type_args[0] is Ellipsis) + ): + ret_list = list(value) + else: + # Otherwise convert + for i, item in enumerate(value): + # Non-tuples use first type arg, tuples use arg set or one + # before ellipsis if that's set + if origin is not tuple: + arg_type = type_args[0] + elif len(type_args) > i and type_args[i] is not Ellipsis: + arg_type = type_args[i] + elif type_args[-1] is Ellipsis: + # Ellipsis means use the second to last one + arg_type = type_args[-2] # type: ignore + else: + raise TypeError( + f"Type {hint} only expecting {len(type_args)} values, got at least {i + 1}" + ) + try: + ret_list.append(value_to_type(arg_type, item, custom_converters)) + except Exception as err: + raise TypeError(f"Failed converting {hint} index {i}") from err + # If tuple, set, or deque convert back to that type + if origin is tuple: + return tuple(ret_list) + elif origin is set: + return set(ret_list) + elif origin is collections.deque: + return collections.deque(ret_list) + return ret_list + + raise TypeError(f"Unserializable type during conversion: {hint}") + + +# Set up after all converter classes are defined to avoid forward-reference issues. +DefaultPayloadConverter.default_encoding_payload_converters = ( + BinaryNullPayloadConverter(), + BinaryPlainPayloadConverter(), + JSONProtoPayloadConverter(), + BinaryProtoPayloadConverter(), + JSONPlainPayloadConverter(), # JSON Plain needs to remain last because it throws on unknown types +) diff --git a/temporalio/converter/_search_attributes.py b/temporalio/converter/_search_attributes.py new file mode 100644 index 000000000..4ec154d6f --- /dev/null +++ b/temporalio/converter/_search_attributes.py @@ -0,0 +1,213 @@ +"""Utilities for encoding and decoding Temporal search attributes.""" + +from __future__ import annotations + +from collections.abc import Sequence +from datetime import datetime + +import temporalio.api.common.v1 +import temporalio.common +from temporalio.converter._data_converter import default +from temporalio.converter._payload_converter import _get_iso_datetime_parser + + +def encode_search_attributes( + attributes: ( + temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes + ), + api: temporalio.api.common.v1.SearchAttributes, +) -> None: + """Convert search attributes into an API message. + + Args: + attributes: Search attributes to convert. The dictionary form of this is + DEPRECATED. + api: API message to set converted attributes on. + """ + if isinstance(attributes, temporalio.common.TypedSearchAttributes): + for typed_k, typed_v in attributes: + api.indexed_fields[typed_k.name].CopyFrom( + encode_typed_search_attribute_value(typed_k, typed_v) + ) + return + elif not attributes: + return + for k, v in attributes.items(): + api.indexed_fields[k].CopyFrom(encode_search_attribute_values(v)) + + +def encode_typed_search_attribute_value( + key: temporalio.common.SearchAttributeKey[ + temporalio.common.SearchAttributeValueType + ], + value: temporalio.common.SearchAttributeValue | None, +) -> temporalio.api.common.v1.Payload: + """Convert typed search attribute value into a payload. + + Args: + key: Key for the value. + value: Value to convert. + + Returns: + Payload for the value. + """ + # For server search attributes to work properly, we cannot set the metadata + # type when we set null + if value is None: + return default().payload_converter.to_payload(None) + if not isinstance(value, key.origin_value_type): + raise TypeError( + f"Value of type {value} not suitable for indexed value type {key.indexed_value_type}" + ) + # datetime needs to be in isoformat + if isinstance(value, datetime): + value = value.isoformat() + # We'll do an extra sanity check for keyword list and check every value + if isinstance(value, Sequence): + for v in value: + if not isinstance(v, str): + raise TypeError("All values of a keyword list must be strings") + # Convert value + payload = default().payload_converter.to_payload(value) + # Set metadata type + payload.metadata["type"] = key._metadata_type.encode() + return payload + + +def encode_search_attribute_values( + vals: temporalio.common.SearchAttributeValues, +) -> temporalio.api.common.v1.Payload: + """Convert search attribute values into a payload. + + .. deprecated:: + Use typed search attributes instead. + + Args: + vals: List of values to convert. + """ + if not isinstance(vals, list): + raise TypeError("Search attribute values must be lists") # type:ignore[reportUnreachable] + # Confirm all types are the same + val_type: type | None = None + # Convert dates to strings + safe_vals = [] + for v in vals: + if isinstance(v, datetime): + if v.tzinfo is None: + raise ValueError( + "Timezone must be present on all search attribute dates" + ) + v = v.isoformat() + elif not isinstance(v, (str, int, float, bool)): + raise TypeError( + f"Search attribute value of type {type(v).__name__} not one of str, int, float, bool, or datetime" + ) + elif val_type and type(v) is not val_type: + raise TypeError( + "Search attribute values must have the same type for the same key" + ) + elif not val_type: + val_type = type(v) + safe_vals.append(v) + return default().payload_converter.to_payloads([safe_vals])[0] + + +def _encode_maybe_typed_search_attributes( # type:ignore[reportUnusedFunction] + non_typed_attributes: temporalio.common.SearchAttributes | None, + typed_attributes: temporalio.common.TypedSearchAttributes | None, + api: temporalio.api.common.v1.SearchAttributes, +) -> None: + if non_typed_attributes: + if typed_attributes and typed_attributes.search_attributes: + raise ValueError( + "Cannot provide both deprecated search attributes and typed search attributes" + ) + encode_search_attributes(non_typed_attributes, api) + elif typed_attributes and typed_attributes.search_attributes: + encode_search_attributes(typed_attributes, api) + + +def decode_search_attributes( + api: temporalio.api.common.v1.SearchAttributes, +) -> temporalio.common.SearchAttributes: + """Decode API search attributes to values. + + .. deprecated:: + Use typed search attributes instead. + + Args: + api: API message with search attribute values to convert. + + Returns: + Converted search attribute values (new mapping every time). + """ + conv = default().payload_converter + ret = {} + for k, v in api.indexed_fields.items(): + val = conv.from_payloads([v])[0] + # If a value did not come back as a list, make it a single-item list + if not isinstance(val, list): + val = [val] + # Convert each item to datetime if necessary + if v.metadata.get("type") == b"Datetime": + parser = _get_iso_datetime_parser() + val = [parser(v) for v in val] + ret[k] = val + return ret + + +def decode_typed_search_attributes( + api: temporalio.api.common.v1.SearchAttributes, +) -> temporalio.common.TypedSearchAttributes: + """Decode API search attributes to typed search attributes. + + Args: + api: API message with search attribute values to convert. + + Returns: + Typed search attribute collection (new object every time). + """ + conv = default().payload_converter + pairs: list[temporalio.common.SearchAttributePair] = [] + for k, v in api.indexed_fields.items(): + # We want the "type" metadata, but if it is not present or an unknown + # type, we will just ignore + metadata_type = v.metadata.get("type") + if not metadata_type: + continue + key = temporalio.common.SearchAttributeKey._from_metadata_type( + k, metadata_type.decode() + ) + if not key: + continue + val = conv.from_payload(v) + # If the value is a list but the type is not keyword list, pull out + # single item or consider this an invalid value and ignore + if ( + key.indexed_value_type + != temporalio.common.SearchAttributeIndexedValueType.KEYWORD_LIST + and isinstance(val, list) + ): + if len(val) != 1: + continue + val = val[0] + if ( + key.indexed_value_type + == temporalio.common.SearchAttributeIndexedValueType.DATETIME + ): + parser = _get_iso_datetime_parser() + # We will let this throw + val = parser(val) + # If the value isn't the right type, we need to ignore + if isinstance(val, key.origin_value_type): + pairs.append(temporalio.common.SearchAttributePair(key, val)) + return temporalio.common.TypedSearchAttributes(pairs) + + +def _decode_search_attribute_value( # type:ignore[reportUnusedFunction] + payload: temporalio.api.common.v1.Payload, +) -> temporalio.common.SearchAttributeValue: + val = default().payload_converter.from_payload(payload) + if isinstance(val, str) and payload.metadata.get("type") == b"Datetime": + val = _get_iso_datetime_parser()(val) + return val # type: ignore diff --git a/temporalio/converter/_serialization_context.py b/temporalio/converter/_serialization_context.py new file mode 100644 index 000000000..73a4a7104 --- /dev/null +++ b/temporalio/converter/_serialization_context.py @@ -0,0 +1,121 @@ +"""Serialization context types for data conversion.""" + +from __future__ import annotations + +from abc import ABC +from dataclasses import dataclass + +from typing_extensions import Self + + +class SerializationContext(ABC): + """Base serialization context. + + Provides contextual information during serialization and deserialization operations. + + Examples: + In client code, when starting a workflow, or sending a signal/update/query to a workflow, + or receiving the result of an update/query, or handling an exception from a workflow, the + context type is :py:class:`WorkflowSerializationContext` and the workflow ID set of the + target workflow will be set in the context. + + In workflow code, when operating on a payload being sent/received to/from a child workflow, + or handling an exception from a child workflow, the context type is + :py:class:`WorkflowSerializationContext` and the workflow ID is that of the child workflow, + not of the currently executing (i.e. parent) workflow. + + In workflow code, when operating on a payload to be sent/received to/from an activity, the + context type is :py:class:`ActivitySerializationContext` and the workflow ID is that of the + currently-executing workflow. ActivitySerializationContext is also set on data converter + operations in the activity context. + """ + + pass + + +@dataclass(frozen=True) +class WorkflowSerializationContext(SerializationContext): + """Serialization context for workflows. + + See :py:class:`SerializationContext` for more details. + """ + + namespace: str + """The namespace the workflow is running in.""" + + workflow_id: str + """The ID of the workflow. + + Note that this is the ID of the workflow of which the payload being operated on is an input or + output. Note also that when creating/describing schedules, this may be the workflow ID prefix + as configured, not the final workflow ID when the workflow is created by the schedule. + """ + + +@dataclass(frozen=True) +class ActivitySerializationContext(SerializationContext): + """Serialization context for activities. + + See :py:class:`SerializationContext` for more details. + """ + + namespace: str + """Workflow/activity namespace.""" + + activity_id: str | None + """Activity ID. Optional if this is an activity started from a workflow.""" + + activity_type: str | None + """Activity type. + + .. deprecated:: + This value may not be set in some bidirectional situations, it should + not be relied on. + """ + + activity_task_queue: str | None + """Activity task queue. + + .. deprecated:: + This value may not be set in some bidirectional situations, it should + not be relied on. + """ + + workflow_id: str | None + """Workflow ID. Only set if this is an activity started from a workflow. + + Note, when creating/describing schedules, this may be the workflow ID prefix as + configured, not the final workflow ID when the workflow is created by the schedule.""" + + workflow_type: str | None + """Workflow type if this is an activity started from a workflow.""" + + is_local: bool + """Whether the activity is a local activity started from a workflow.""" + + +class WithSerializationContext(ABC): + """Interface for classes that can use serialization context. + + The following classes may implement this interface: + - :py:class:`PayloadConverter` + - :py:class:`PayloadCodec` + - :py:class:`FailureConverter` + - :py:class:`EncodingPayloadConverter` + + During data converter operations (encoding/decoding, serialization/deserialization, and failure + conversion), instances of classes implementing this interface will be replaced by the result of + calling with_context(context). This allows overridden methods (encode/decode, + to_payload/from_payload, etc) to use the context. + """ + + def with_context(self, context: SerializationContext) -> Self: # type: ignore[reportUnusedParameter] + """Return a copy of this object configured to use the given context. + + Args: + context: The serialization context to use. + + Returns: + A new instance configured with the context. + """ + raise NotImplementedError() diff --git a/temporalio/envconfig.py b/temporalio/envconfig.py index ff68474cb..ad413b9a9 100644 --- a/temporalio/envconfig.py +++ b/temporalio/envconfig.py @@ -6,25 +6,26 @@ from __future__ import annotations +from collections.abc import Mapping from dataclasses import dataclass, field from pathlib import Path -from typing import Any, Dict, Literal, Mapping, Optional, Union, cast +from typing import Any, Literal, TypeAlias, cast -from typing_extensions import TypeAlias, TypedDict +from typing_extensions import Self, TypedDict import temporalio.service from temporalio.bridge.temporal_sdk_bridge import envconfig as _bridge_envconfig -DataSource: TypeAlias = Union[ - Path, str, bytes -] # str represents a file contents, bytes represents raw data +DataSource: TypeAlias = ( + Path | str | bytes +) # str represents a file contents, bytes represents raw data # We define typed dictionaries for what these configs look like as TOML. class ClientConfigTLSDict(TypedDict, total=False): """Dictionary representation of TLS config for TOML.""" - disabled: bool + disabled: bool | None server_name: str server_ca_cert: Mapping[str, str] client_cert: Mapping[str, str] @@ -41,7 +42,7 @@ class ClientConfigProfileDict(TypedDict, total=False): grpc_meta: Mapping[str, str] -def _from_dict_to_source(d: Optional[Mapping[str, Any]]) -> Optional[DataSource]: +def _from_dict_to_source(d: Mapping[str, Any] | None) -> DataSource | None: if not d: return None if "data" in d: @@ -52,8 +53,8 @@ def _from_dict_to_source(d: Optional[Mapping[str, Any]]) -> Optional[DataSource] def _source_to_dict( - source: Optional[DataSource], -) -> Optional[Mapping[str, str]]: + source: DataSource | None, +) -> Mapping[str, str] | None: if isinstance(source, Path): return {"path": str(source)} if isinstance(source, str): @@ -64,10 +65,10 @@ def _source_to_dict( def _source_to_path_and_data( - source: Optional[DataSource], -) -> tuple[Optional[str], Optional[bytes]]: - path: Optional[str] = None - data: Optional[bytes] = None + source: DataSource | None, +) -> tuple[str | None, bytes | None]: + path: str | None = None + data: bytes | None = None if isinstance(source, Path): path = str(source) elif isinstance(source, str): @@ -75,14 +76,14 @@ def _source_to_path_and_data( elif isinstance(source, bytes): data = source elif source is not None: - raise TypeError( + raise TypeError( # type: ignore[reportUnreachable] "config_source must be one of pathlib.Path, str, bytes, or None, " f"but got {type(source).__name__}" ) return path, data -def _read_source(source: Optional[DataSource]) -> Optional[bytes]: +def _read_source(source: DataSource | None) -> bytes | None: if source is None: return None if isinstance(source, Path): @@ -92,41 +93,37 @@ def _read_source(source: Optional[DataSource]) -> Optional[bytes]: return source.encode("utf-8") if isinstance(source, bytes): return source - raise TypeError( + raise TypeError( # type: ignore[reportUnreachable] f"Source must be one of pathlib.Path, str, or bytes, but got {type(source).__name__}" ) @dataclass(frozen=True) class ClientConfigTLS: - """TLS configuration as specified as part of client configuration + """TLS configuration as specified as part of client configuration""" - .. warning:: - Experimental API. - """ - - disabled: bool = False - """If true, TLS is explicitly disabled.""" - server_name: Optional[str] = None + disabled: bool | None = None + """If True, TLS is explicitly disabled. If False, TLS is explicitly enabled. If None, TLS behavior was not configured.""" + server_name: str | None = None """SNI override.""" - server_root_ca_cert: Optional[DataSource] = None + server_root_ca_cert: DataSource | None = None """Server CA certificate source.""" - client_cert: Optional[DataSource] = None + client_cert: DataSource | None = None """Client certificate source.""" - client_private_key: Optional[DataSource] = None + client_private_key: DataSource | None = None """Client key source.""" def to_dict(self) -> ClientConfigTLSDict: """Convert to a dictionary that can be used for TOML serialization.""" d: ClientConfigTLSDict = {} - if self.disabled: + if self.disabled is not None: d["disabled"] = self.disabled if self.server_name is not None: d["server_name"] = self.server_name def set_source( key: Literal["server_ca_cert", "client_cert", "client_key"], - source: Optional[DataSource], + source: DataSource | None, ): if source is not None and (val := _source_to_dict(source)): d[key] = val @@ -136,9 +133,9 @@ def set_source( set_source("client_key", self.client_private_key) return d - def to_connect_tls_config(self) -> Union[bool, temporalio.service.TLSConfig]: + def to_connect_tls_config(self) -> bool | temporalio.service.TLSConfig: """Create a `temporalio.service.TLSConfig` from this profile.""" - if self.disabled: + if self.disabled is True: return False return temporalio.service.TLSConfig( @@ -148,13 +145,13 @@ def to_connect_tls_config(self) -> Union[bool, temporalio.service.TLSConfig]: client_private_key=_read_source(self.client_private_key), ) - @staticmethod - def from_dict(d: Optional[ClientConfigTLSDict]) -> Optional[ClientConfigTLS]: + @classmethod + def from_dict(cls, d: ClientConfigTLSDict | None) -> Self | None: """Create a ClientConfigTLS from a dictionary.""" if not d: return None - return ClientConfigTLS( - disabled=d.get("disabled", False), + return cls( + disabled=d.get("disabled"), server_name=d.get("server_name"), # Note: Bridge uses snake_case, but TOML uses kebab-case which is # converted to snake_case. Core has server_ca_cert, client_key. @@ -167,15 +164,12 @@ def from_dict(d: Optional[ClientConfigTLSDict]) -> Optional[ClientConfigTLS]: class ClientConnectConfig(TypedDict, total=False): """Arguments for `temporalio.client.Client.connect` that are configurable via environment configuration. - - .. warning:: - Experimental API. """ target_host: str namespace: str api_key: str - tls: Union[bool, temporalio.service.TLSConfig] + tls: bool | temporalio.service.TLSConfig rpc_metadata: Mapping[str, str] @@ -184,28 +178,25 @@ class ClientConfigProfile: """Represents a client configuration profile. This class holds the configuration as loaded from a file or environment. - See `to_connect_config` to transform the profile to `ClientConnectConfig`, + See `to_client_connect_config` to transform the profile to `ClientConnectConfig`, which can be used to create a client. - - .. warning:: - Experimental API. """ - address: Optional[str] = None + address: str | None = None """Client address.""" - namespace: Optional[str] = None + namespace: str | None = None """Client namespace.""" - api_key: Optional[str] = None + api_key: str | None = None """Client API key.""" - tls: Optional[ClientConfigTLS] = None + tls: ClientConfigTLS | None = None """TLS configuration.""" grpc_meta: Mapping[str, str] = field(default_factory=dict) """gRPC metadata.""" - @staticmethod - def from_dict(d: ClientConfigProfileDict) -> ClientConfigProfile: + @classmethod + def from_dict(cls, d: ClientConfigProfileDict) -> Self: """Create a ClientConfigProfile from a dictionary.""" - return ClientConfigProfile( + return cls( address=d.get("address"), namespace=d.get("namespace"), api_key=d.get("api_key"), @@ -231,30 +222,33 @@ def to_dict(self) -> ClientConfigProfileDict: def to_client_connect_config(self) -> ClientConnectConfig: """Create a `ClientConnectConfig` from this profile.""" # Only include non-None values - config: Dict[str, Any] = {} + config: dict[str, Any] = {} if self.address: config["target_host"] = self.address if self.namespace is not None: config["namespace"] = self.namespace if self.api_key is not None: config["api_key"] = self.api_key + # Enable TLS with default TLS options + config["tls"] = True if self.tls is not None: + # Use specified TLS options config["tls"] = self.tls.to_connect_tls_config() if self.grpc_meta: config["rpc_metadata"] = self.grpc_meta # Cast to ClientConnectConfig - this is safe because we've only included non-None values - return cast(ClientConnectConfig, config) + return cast(ClientConnectConfig, config) # type: ignore[reportInvalidCast] @staticmethod def load( - profile: Optional[str] = None, + profile: str | None = None, *, - config_source: Optional[DataSource] = None, + config_source: DataSource | None = None, disable_file: bool = False, disable_env: bool = False, config_file_strict: bool = False, - override_env_vars: Optional[Mapping[str, str]] = None, + override_env_vars: Mapping[str, str] | None = None, ) -> ClientConfigProfile: """Load a single client profile from given sources, applying env overrides. @@ -301,11 +295,8 @@ class ClientConfig: """Client configuration loaded from TOML and environment variables. This contains a mapping of profile names to client profiles. Use - `ClientConfigProfile.to_connect_config` to create a `ClientConnectConfig` - from a profile. See `load_profile` to load an individual profile. - - .. warning:: - Experimental API. + `ClientConfigProfile.to_client_connect_config` to create a `ClientConnectConfig` + from a profile. See `ClientConfigProfile.load` to load an individual profile. """ profiles: Mapping[str, ClientConfigProfile] @@ -315,14 +306,15 @@ def to_dict(self) -> Mapping[str, ClientConfigProfileDict]: """Convert to a dictionary that can be used for TOML serialization.""" return {k: v.to_dict() for k, v in self.profiles.items()} - @staticmethod + @classmethod def from_dict( + cls, d: Mapping[str, Mapping[str, Any]], - ) -> ClientConfig: + ) -> Self: """Create a ClientConfig from a dictionary.""" # We must cast the inner dictionary because the source is often a plain # Mapping[str, Any] from the bridge or other sources. - return ClientConfig( + return cls( profiles={ k: ClientConfigProfile.from_dict(cast(ClientConfigProfileDict, v)) for k, v in d.items() @@ -332,10 +324,9 @@ def from_dict( @staticmethod def load( *, - config_source: Optional[DataSource] = None, - disable_file: bool = False, + config_source: DataSource | None = None, config_file_strict: bool = False, - override_env_vars: Optional[Mapping[str, str]] = None, + override_env_vars: Mapping[str, str] | None = None, ) -> ClientConfig: """Load all client profiles from given sources. @@ -348,8 +339,6 @@ def load( config_source: If present, this is used as the configuration source instead of default file locations. This can be a path to the file or the string/byte contents of the file. - disable_file: If true, file loading is disabled. This is only used - when ``config_source`` is not present. config_file_strict: If true, will TOML file parsing will error on unrecognized keys. override_env_vars: The environment variables to use for locating the @@ -364,7 +353,6 @@ def load( loaded_profiles = _bridge_envconfig.load_client_config( path=path, data=data, - disable_file=disable_file, config_file_strict=config_file_strict, env_vars=override_env_vars, ) @@ -372,13 +360,13 @@ def load( @staticmethod def load_client_connect_config( - profile: Optional[str] = None, + profile: str | None = None, *, - config_file: Optional[str] = None, + config_file: str | None = None, disable_file: bool = False, disable_env: bool = False, config_file_strict: bool = False, - override_env_vars: Optional[Mapping[str, str]] = None, + override_env_vars: Mapping[str, str] | None = None, ) -> ClientConnectConfig: """Load a single client profile and convert to connect config. @@ -406,7 +394,7 @@ def load_client_connect_config( TypedDict of keyword arguments for :py:meth:`temporalio.client.Client.connect`. """ - config_source: Optional[DataSource] = None + config_source: DataSource | None = None if config_file and not disable_file: config_source = Path(config_file) diff --git a/temporalio/exceptions.py b/temporalio/exceptions.py index 74afb7ea7..43a2e1bad 100644 --- a/temporalio/exceptions.py +++ b/temporalio/exceptions.py @@ -1,11 +1,11 @@ """Common Temporal exceptions.""" import asyncio +from collections.abc import Sequence from datetime import timedelta from enum import IntEnum -from typing import Any, Optional, Sequence, Tuple +from typing import Any -import temporalio.api.common.v1 import temporalio.api.enums.v1 import temporalio.api.failure.v1 @@ -14,7 +14,7 @@ class TemporalError(Exception): """Base for all Temporal exceptions.""" @property - def cause(self) -> Optional[BaseException]: + def cause(self) -> BaseException | None: """Cause of the exception. This is the same as ``Exception.__cause__``. @@ -29,8 +29,8 @@ def __init__( self, message: str, *, - failure: Optional[temporalio.api.failure.v1.Failure] = None, - exc_args: Optional[Tuple] = None, + failure: temporalio.api.failure.v1.Failure | None = None, + exc_args: tuple | None = None, ) -> None: """Initialize a failure error.""" if exc_args is None: @@ -45,7 +45,7 @@ def message(self) -> str: return self._message @property - def failure(self) -> Optional[temporalio.api.failure.v1.Failure]: + def failure(self) -> temporalio.api.failure.v1.Failure | None: """Underlying protobuf failure object.""" return self._failure @@ -61,7 +61,7 @@ class WorkflowAlreadyStartedError(FailureError): """ def __init__( - self, workflow_id: str, workflow_type: str, *, run_id: Optional[str] = None + self, workflow_id: str, workflow_type: str, *, run_id: str | None = None ) -> None: """Initialize a workflow already started error.""" super().__init__("Workflow execution already started") @@ -70,6 +70,44 @@ def __init__( self.run_id = run_id +class ActivityAlreadyStartedError(FailureError): + """Thrown by a client when an activity execution has already started. + + Attributes: + activity_id: ID of the already-started activity. + activity_type: Activity type name of the already-started activity. + run_id: Run ID of the already-started activity if this was raised by the + client. + """ + + def __init__( + self, activity_id: str, activity_type: str, *, run_id: str | None = None + ) -> None: + """Initialize an activity already started error.""" + super().__init__("Activity execution already started") + self.activity_id = activity_id + self.activity_type = activity_type + self.run_id = run_id + + +class NexusOperationAlreadyStartedError(FailureError): + """Thrown by a client when a Nexus operation execution has already started. + + .. warning:: + This API is experimental and unstable. + + Attributes: + operation_id: ID of the already-started operation. + run_id: Run ID of the already-started operation if available. + """ + + def __init__(self, operation_id: str, *, run_id: str | None = None) -> None: + """Initialize a Nexus operation already started error.""" + super().__init__("Nexus operation execution already started") + self.operation_id = operation_id + self.run_id = run_id + + class ApplicationErrorCategory(IntEnum): """Severity category for your application error. Maps to corresponding client-side logging/metrics behaviors""" @@ -90,9 +128,9 @@ def __init__( self, message: str, *details: Any, - type: Optional[str] = None, + type: str | None = None, non_retryable: bool = False, - next_retry_delay: Optional[timedelta] = None, + next_retry_delay: timedelta | None = None, category: ApplicationErrorCategory = ApplicationErrorCategory.UNSPECIFIED, ) -> None: """Initialize an application error.""" @@ -113,7 +151,7 @@ def details(self) -> Sequence[Any]: return self._details @property - def type(self) -> Optional[str]: + def type(self) -> str | None: """General error type.""" return self._type @@ -128,7 +166,7 @@ def non_retryable(self) -> bool: return self._non_retryable @property - def next_retry_delay(self) -> Optional[timedelta]: + def next_retry_delay(self) -> timedelta | None: """Delay before the next activity retry attempt. User activity code may set this when raising ApplicationError to specify @@ -192,7 +230,7 @@ def __init__( self, message: str, *, - type: Optional[TimeoutType], + type: TimeoutType | None, last_heartbeat_details: Sequence[Any], ) -> None: """Initialize a timeout error.""" @@ -201,7 +239,7 @@ def __init__( self._last_heartbeat_details = last_heartbeat_details @property - def type(self) -> Optional[TimeoutType]: + def type(self) -> TimeoutType | None: """Type of timeout error.""" return self._type @@ -259,7 +297,7 @@ def __init__( identity: str, activity_type: str, activity_id: str, - retry_state: Optional[RetryState], + retry_state: RetryState | None, ) -> None: """Initialize an activity error.""" super().__init__(message) @@ -296,7 +334,7 @@ def activity_id(self) -> str: return self._activity_id @property - def retry_state(self) -> Optional[RetryState]: + def retry_state(self) -> RetryState | None: """Retry state for this error.""" return self._retry_state @@ -314,7 +352,7 @@ def __init__( workflow_type: str, initiated_event_id: int, started_event_id: int, - retry_state: Optional[RetryState], + retry_state: RetryState | None, ) -> None: """Initialize a child workflow error.""" super().__init__(message) @@ -357,7 +395,7 @@ def started_event_id(self) -> int: return self._started_event_id @property - def retry_state(self) -> Optional[RetryState]: + def retry_state(self) -> RetryState | None: """Retry state for this error.""" return self._retry_state diff --git a/temporalio/nexus/__init__.py b/temporalio/nexus/__init__.py index de9164716..3abc9b0f2 100644 --- a/temporalio/nexus/__init__.py +++ b/temporalio/nexus/__init__.py @@ -1,20 +1,61 @@ """Temporal Nexus support -.. warning:: - Nexus APIs are experimental and unstable. - See https://github.com/temporalio/sdk-python/tree/main#nexus """ -from ._decorators import workflow_run_operation as workflow_run_operation -from ._operation_context import Info as Info -from ._operation_context import LoggerAdapter as LoggerAdapter -from ._operation_context import NexusCallback as NexusCallback +from ._decorators import ( + TemporalOperationStartHandlerFunc, + temporal_operation, + workflow_run_operation, +) from ._operation_context import ( - WorkflowRunOperationContext as WorkflowRunOperationContext, + Info, + LoggerAdapter, + NexusCallback, + TemporalCancelOperationContext, + TemporalStartOperationContext, + WorkflowRunOperationContext, + client, + in_operation, + info, + is_worker_shutdown, + logger, + metric_meter, + wait_for_worker_shutdown, + wait_for_worker_shutdown_sync, +) +from ._operation_handlers import ( + CancelActivityOptions, + CancelUpdateWorkflowOptions, + CancelWorkflowRunOptions, + TemporalOperationHandler, +) +from ._temporal_client import TemporalNexusClient, TemporalOperationResult +from ._token import WorkflowHandle + +__all__ = ( + "workflow_run_operation", + "CancelActivityOptions", + "CancelWorkflowRunOptions", + "CancelUpdateWorkflowOptions", + "Info", + "LoggerAdapter", + "NexusCallback", + "WorkflowRunOperationContext", + "TemporalCancelOperationContext", + "TemporalStartOperationContext", + "client", + "in_operation", + "info", + "is_worker_shutdown", + "logger", + "metric_meter", + "wait_for_worker_shutdown", + "wait_for_worker_shutdown_sync", + "WorkflowHandle", + "TemporalNexusClient", + "TemporalOperationStartHandlerFunc", + "TemporalOperationHandler", + "TemporalOperationResult", + "temporal_operation", ) -from ._operation_context import client as client -from ._operation_context import in_operation as in_operation -from ._operation_context import info as info -from ._operation_context import logger as logger -from ._token import WorkflowHandle as WorkflowHandle diff --git a/temporalio/nexus/_decorators.py b/temporalio/nexus/_decorators.py index 28c625816..2dd2b3554 100644 --- a/temporalio/nexus/_decorators.py +++ b/temporalio/nexus/_decorators.py @@ -1,11 +1,8 @@ from __future__ import annotations +from collections.abc import Awaitable, Callable from typing import ( - Awaitable, - Callable, - Optional, - TypeVar, - Union, + TypeAlias, overload, ) @@ -15,27 +12,40 @@ OperationHandler, StartOperationContext, ) +from typing_extensions import override -from ._operation_context import WorkflowRunOperationContext -from ._operation_handlers import WorkflowRunOperationHandler +from temporalio.nexus._temporal_client import ( + TemporalNexusClient, + TemporalOperationResult, +) +from temporalio.types import NexusServiceType + +from ._operation_context import ( + TemporalStartOperationContext, + WorkflowRunOperationContext, +) +from ._operation_handlers import ( + TemporalOperationHandler, + WorkflowRunOperationHandler, +) from ._token import WorkflowHandle from ._util import ( get_callable_name, + get_temporal_operation_start_method_input_and_output_type_annotations, get_workflow_run_start_method_input_and_output_type_annotations, + is_async_callable, set_operation_factory, ) -ServiceHandlerT = TypeVar("ServiceHandlerT") - @overload def workflow_run_operation( start: Callable[ - [ServiceHandlerT, WorkflowRunOperationContext, InputT], + [NexusServiceType, WorkflowRunOperationContext, InputT], Awaitable[WorkflowHandle[OutputT]], ], ) -> Callable[ - [ServiceHandlerT, WorkflowRunOperationContext, InputT], + [NexusServiceType, WorkflowRunOperationContext, InputT], Awaitable[WorkflowHandle[OutputT]], ]: ... @@ -43,57 +53,58 @@ def workflow_run_operation( @overload def workflow_run_operation( *, - name: Optional[str] = None, + name: str | None = None, ) -> Callable[ [ Callable[ - [ServiceHandlerT, WorkflowRunOperationContext, InputT], + [NexusServiceType, WorkflowRunOperationContext, InputT], Awaitable[WorkflowHandle[OutputT]], ] ], Callable[ - [ServiceHandlerT, WorkflowRunOperationContext, InputT], + [NexusServiceType, WorkflowRunOperationContext, InputT], Awaitable[WorkflowHandle[OutputT]], ], ]: ... def workflow_run_operation( - start: Optional[ + start: None + | ( Callable[ - [ServiceHandlerT, WorkflowRunOperationContext, InputT], + [NexusServiceType, WorkflowRunOperationContext, InputT], Awaitable[WorkflowHandle[OutputT]], ] - ] = None, + ) = None, *, - name: Optional[str] = None, -) -> Union[ + name: str | None = None, +) -> ( Callable[ - [ServiceHandlerT, WorkflowRunOperationContext, InputT], + [NexusServiceType, WorkflowRunOperationContext, InputT], Awaitable[WorkflowHandle[OutputT]], - ], - Callable[ + ] + | Callable[ [ Callable[ - [ServiceHandlerT, WorkflowRunOperationContext, InputT], + [NexusServiceType, WorkflowRunOperationContext, InputT], Awaitable[WorkflowHandle[OutputT]], ] ], Callable[ - [ServiceHandlerT, WorkflowRunOperationContext, InputT], + [NexusServiceType, WorkflowRunOperationContext, InputT], Awaitable[WorkflowHandle[OutputT]], ], - ], -]: + ] +): """Decorator marking a method as the start method for a workflow-backed operation.""" def decorator( start: Callable[ - [ServiceHandlerT, WorkflowRunOperationContext, InputT], + [NexusServiceType, WorkflowRunOperationContext, InputT], Awaitable[WorkflowHandle[OutputT]], ], ) -> Callable[ - [ServiceHandlerT, WorkflowRunOperationContext, InputT], + [NexusServiceType, WorkflowRunOperationContext, InputT], Awaitable[WorkflowHandle[OutputT]], ]: ( @@ -102,7 +113,7 @@ def decorator( ) = get_workflow_run_start_method_input_and_output_type_annotations(start) def operation_handler_factory( - self: ServiceHandlerT, + self: NexusServiceType, ) -> OperationHandler[InputT, OutputT]: async def _start( ctx: StartOperationContext, input: InputT @@ -117,15 +128,117 @@ async def _start( return WorkflowRunOperationHandler(_start) method_name = get_callable_name(start) - nexusrpc.set_operation_definition( - operation_handler_factory, - nexusrpc.Operation( - name=name or method_name, - method_name=method_name, - input_type=input_type, - output_type=output_type, - ), + op = nexusrpc.Operation( + name=name or method_name, + input_type=input_type, + output_type=output_type, + ) + op.method_name = method_name + nexusrpc.set_operation(operation_handler_factory, op) + + set_operation_factory(start, operation_handler_factory) + return start + + if start is None: + return decorator + + return decorator(start) + + +TemporalOperationStartHandlerFunc: TypeAlias = Callable[ + [ + NexusServiceType, + TemporalStartOperationContext, + TemporalNexusClient, + InputT, + ], + Awaitable[TemporalOperationResult[OutputT]], +] + + +@overload +def temporal_operation( + start: TemporalOperationStartHandlerFunc[NexusServiceType, InputT, OutputT], +) -> TemporalOperationStartHandlerFunc[NexusServiceType, InputT, OutputT]: ... + + +@overload +def temporal_operation( + *, + name: str | None = None, +) -> Callable[ + [TemporalOperationStartHandlerFunc[NexusServiceType, InputT, OutputT]], + TemporalOperationStartHandlerFunc[NexusServiceType, InputT, OutputT], +]: ... + + +def temporal_operation( + start: None + | TemporalOperationStartHandlerFunc[NexusServiceType, InputT, OutputT] = None, + *, + name: str | None = None, +) -> ( + TemporalOperationStartHandlerFunc[NexusServiceType, InputT, OutputT] + | Callable[ + [TemporalOperationStartHandlerFunc[NexusServiceType, InputT, OutputT]], + TemporalOperationStartHandlerFunc[NexusServiceType, InputT, OutputT], + ] +): + """Decorator marking a method as the start method for an operation that interacts with Temporal. + + .. warning:: + This API is experimental and unstable. + """ + + def decorator( + start: TemporalOperationStartHandlerFunc[NexusServiceType, InputT, OutputT], + ) -> TemporalOperationStartHandlerFunc[NexusServiceType, InputT, OutputT]: + if not is_async_callable(start): + raise RuntimeError( + f"{start} is not an `async def` method. " + "@temporal_operation must decorate an `async def` start method." + ) + ( + input_type, + output_type, + ) = get_temporal_operation_start_method_input_and_output_type_annotations(start) + + def operation_handler_factory( + self: NexusServiceType, + ) -> OperationHandler[InputT, OutputT]: + async def _start( + ctx: TemporalStartOperationContext, + client: TemporalNexusClient, + input: InputT, + ) -> TemporalOperationResult[OutputT]: + return await start( + self, + ctx, + client, + input, + ) + + class _TemporalOperationHandler(TemporalOperationHandler): + @override + async def start_operation( + self, + ctx: TemporalStartOperationContext, + client: TemporalNexusClient, + input: InputT, + ) -> TemporalOperationResult[OutputT]: + return await _start(ctx, client, input) + + _TemporalOperationHandler.start_operation.__doc__ = start.__doc__ + return _TemporalOperationHandler() + + method_name = get_callable_name(start) + op = nexusrpc.Operation( + name=name or method_name, + input_type=input_type, + output_type=output_type, ) + op.method_name = method_name + nexusrpc.set_operation(operation_handler_factory, op) set_operation_factory(start, operation_handler_factory) return start diff --git a/temporalio/nexus/_link_conversion.py b/temporalio/nexus/_link_conversion.py index a13c2d149..e3ef3988b 100644 --- a/temporalio/nexus/_link_conversion.py +++ b/temporalio/nexus/_link_conversion.py @@ -3,10 +3,10 @@ import logging import re import urllib.parse +from enum import Enum from typing import ( TYPE_CHECKING, Any, - Optional, ) import nexusrpc @@ -19,15 +19,38 @@ logger = logging.getLogger(__name__) -_LINK_URL_PATH_REGEX = re.compile( - r"^/namespaces/(?P[^/]+)/workflows/(?P[^/]+)/(?P[^/]+)/history$" +_NEXUS_OPERATION_LINK_URL_PATH_REGEX = re.compile( + r"^/namespaces/(?P[^/]+)/nexus-operations/(?P[^/]+)/(?P[^/]*)/details$" ) + +_ACTIVITY_LINK_URL_PATH_REGEX = re.compile( + r"^/namespaces/(?P[^/]+)/activities/(?P[^/]+)/(?P[^/]+)/details$" +) + +_WORKFLOW_LINK_URL_PATH_REGEX = re.compile( + r"^/namespaces/(?P[^/]+)/workflows/(?P[^/]+)/(?P[^/]+)(?P/history)?$" +) + + +class _LinkType(str, Enum): + WORKFLOW_EVENT = temporalio.api.common.v1.Link.WorkflowEvent.DESCRIPTOR.full_name + WORKFLOW = temporalio.api.common.v1.Link.Workflow.DESCRIPTOR.full_name + NEXUS_OPERATION = temporalio.api.common.v1.Link.NexusOperation.DESCRIPTOR.full_name + ACTIVITY = temporalio.api.common.v1.Link.Activity.DESCRIPTOR.full_name + + LINK_EVENT_ID_PARAM_NAME = "eventID" LINK_EVENT_TYPE_PARAM_NAME = "eventType" +LINK_REQUEST_ID_PARAM_NAME = "requestID" +LINK_REFERENCE_TYPE_PARAM_NAME = "referenceType" +LINK_REASON_PARAM_NAME = "reason" + +EVENT_REFERENCE_TYPE = "EventReference" +REQUEST_ID_REFERENCE_TYPE = "RequestIdReference" def workflow_execution_started_event_link_from_workflow_handle( - handle: temporalio.client.WorkflowHandle[Any, Any], + handle: temporalio.client.WorkflowHandle[Any, Any], request_id: str ) -> temporalio.api.common.v1.Link.WorkflowEvent: """Create a WorkflowEvent link corresponding to a started workflow""" if handle.first_execution_run_id is None: @@ -35,18 +58,73 @@ def workflow_execution_started_event_link_from_workflow_handle( f"Workflow handle {handle} has no first execution run ID. " f"Cannot create WorkflowExecutionStarted event link." ) + return temporalio.api.common.v1.Link.WorkflowEvent( namespace=handle._client.namespace, workflow_id=handle.id, run_id=handle.first_execution_run_id, - event_ref=temporalio.api.common.v1.Link.WorkflowEvent.EventReference( - event_id=1, + request_id_ref=temporalio.api.common.v1.Link.WorkflowEvent.RequestIdReference( + request_id=request_id, event_type=temporalio.api.enums.v1.EventType.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED, ), - # TODO(nexus-preview): RequestIdReference ) +def nexus_link_to_temporal_link( + nexus_link: nexusrpc.Link, +) -> temporalio.api.common.v1.Link | None: + """Convert a nexusrpc link into a Temporal API Link. + + Returns None when the Nexus link type is invalid or unknown. + """ + try: + link_type = _LinkType(nexus_link.type) + except ValueError: + logger.warning(f"Invalid Nexus link: unknown link type {nexus_link}") + return None + + match link_type: + case _LinkType.WORKFLOW_EVENT: + return nexus_link_to_workflow_event_link(nexus_link) + + case _LinkType.WORKFLOW: + return nexus_link_to_workflow_link(nexus_link) + + case _LinkType.NEXUS_OPERATION: + return nexus_link_to_nexus_operation_link(nexus_link) + + case _LinkType.ACTIVITY: + return nexus_link_to_activity_link(nexus_link) + + +def temporal_link_to_nexus_link( + temporal_link: temporalio.api.common.v1.Link, +) -> nexusrpc.Link | None: + """Convert a Temporal API Link into a nexusrpc link. + + Returns None when the Temporal link variant is missing. + """ + match temporal_link.WhichOneof("variant"): + case "workflow_event": + return workflow_event_to_nexus_link(temporal_link.workflow_event) + + case "workflow": + return workflow_to_nexus_link(temporal_link.workflow) + + case "nexus_operation": + return nexus_operation_to_nexus_link(temporal_link.nexus_operation) + + case "activity": + return activity_link_to_nexus_link(temporal_link.activity) + + case "batch_job": + raise NotImplementedError("batch_job links are not supported") + + case None: + logger.warning("Invalid Temporal link: missing variant") + return None + + def workflow_event_to_nexus_link( workflow_event: temporalio.api.common.v1.Link.WorkflowEvent, ) -> nexusrpc.Link: @@ -55,47 +133,246 @@ def workflow_event_to_nexus_link( Used when propagating links from a StartWorkflow response to a Nexus start operation response. """ - scheme = "temporal" - namespace = urllib.parse.quote(workflow_event.namespace) - workflow_id = urllib.parse.quote(workflow_event.workflow_id) - run_id = urllib.parse.quote(workflow_event.run_id) - path = f"/namespaces/{namespace}/workflows/{workflow_id}/{run_id}/history" - query_params = _event_reference_to_query_params(workflow_event.event_ref) + query_params = None + match workflow_event.WhichOneof("reference"): + case "event_ref": + query_params = _event_reference_to_query_params(workflow_event.event_ref) + case "request_id_ref": + query_params = _request_id_reference_to_query_params( + workflow_event.request_id_ref + ) + case _: + pass + return nexusrpc.Link( - url=urllib.parse.urlunparse((scheme, "", path, "", query_params, "")), - type=workflow_event.DESCRIPTOR.full_name, + url=_workflow_nexus_url( + workflow_event.namespace, + workflow_event.workflow_id, + workflow_event.run_id, + history=True, + query_params=query_params, + ), + type=_LinkType.WORKFLOW_EVENT.value, ) -def nexus_link_to_workflow_event( +def workflow_to_nexus_link( + workflow: temporalio.api.common.v1.Link.Workflow, +) -> nexusrpc.Link: + """Convert a Workflow link into a nexusrpc link.""" + query_params = "" + if workflow.reason: + query_params = urllib.parse.urlencode( + { + LINK_REASON_PARAM_NAME: workflow.reason, + }, + ) + + return nexusrpc.Link( + url=_workflow_nexus_url( + workflow.namespace, + workflow.workflow_id, + workflow.run_id, + history=False, + query_params=query_params, + ), + type=_LinkType.WORKFLOW.value, + ) + + +def nexus_operation_to_nexus_link( + op_link: temporalio.api.common.v1.Link.NexusOperation, +) -> nexusrpc.Link: + """Convert a NexusOperation link into a nexusrpc link + + Used when propagating links from a StartNexusOperation response to a Nexus start operation + response. + """ + namespace = urllib.parse.quote(op_link.namespace, safe="") + operation_id = urllib.parse.quote(op_link.operation_id, safe="") + run_id = urllib.parse.quote(op_link.run_id, safe="") + path = f"/namespaces/{namespace}/nexus-operations/{operation_id}/{run_id}/details" + + return nexusrpc.Link( + url=_temporal_nexus_url(path), + type=_LinkType.NEXUS_OPERATION.value, + ) + + +def activity_link_to_nexus_link( + activity: temporalio.api.common.v1.Link.Activity, +) -> nexusrpc.Link: + """Convert an Activity link into a nexusrpc link.""" + namespace = urllib.parse.quote(activity.namespace, safe="") + activity_id = urllib.parse.quote(activity.activity_id, safe="") + run_id = urllib.parse.quote(activity.run_id, safe="") + path = f"/namespaces/{namespace}/activities/{activity_id}/{run_id}/details" + return nexusrpc.Link(url=_temporal_nexus_url(path), type=_LinkType.ACTIVITY.value) + + +def _workflow_nexus_url( + namespace: str, + workflow_id: str, + run_id: str, + *, + history: bool, + query_params: str | None = "", +) -> str: + namespace = urllib.parse.quote(namespace, safe="") + workflow_id = urllib.parse.quote(workflow_id, safe="") + run_id = urllib.parse.quote(run_id, safe="") + path = f"/namespaces/{namespace}/workflows/{workflow_id}/{run_id}" + if history: + path += "/history" + return _temporal_nexus_url(path, query_params=query_params) + + +def _temporal_nexus_url(path: str, *, query_params: str | None = "") -> str: + # urllib will omit '//' from the url if netloc is empty so we add the scheme manually + return f"temporal://{urllib.parse.urlunparse(('', '', path, '', query_params or '', ''))}" + + +def _parse_workflow_nexus_url( + link: nexusrpc.Link, *, history: bool +) -> tuple[dict[str, str], dict[str, list[str]]] | None: + url = urllib.parse.urlparse(link.url) + match = _WORKFLOW_LINK_URL_PATH_REGEX.match(url.path) + if not match or bool(match.group("history")) != history: + expected_suffix = "/history" if history else "" + logger.warning( + f"Invalid Nexus link: {link}. Expected path to match " + f"/namespaces/{{namespace}}/workflows/{{workflow_id}}/{{run_id}}{expected_suffix}" + ) + return None + + groups = { + name: urllib.parse.unquote(value) + for name, value in match.groupdict().items() + if name != "history" and value is not None + } + return groups, urllib.parse.parse_qs(url.query) + + +def _optional_single_query_param( + query_params: dict[str, list[str]], param_name: str +) -> str: + match query_params.get(param_name): + case [param]: + return param + case [] | None: + return "" + case _: + raise ValueError(f"Expected {param_name} to have at most 1 value") + + +def nexus_link_to_workflow_event_link( link: nexusrpc.Link, -) -> Optional[temporalio.api.common.v1.Link.WorkflowEvent]: - """Convert a nexus link into a WorkflowEvent link +) -> temporalio.api.common.v1.Link | None: + """Convert a nexus link into a Temporal WorkflowEvent link This is used when propagating links from a Nexus start operation request to a StartWorklow request. """ - url = urllib.parse.urlparse(link.url) - match = _LINK_URL_PATH_REGEX.match(url.path) - if not match: + parsed = _parse_workflow_nexus_url(link, history=True) + if parsed is None: + return None + groups, query_params = parsed + try: + request_id_ref = None + event_ref = None + match query_params.get(LINK_REFERENCE_TYPE_PARAM_NAME): + case ["EventReference"]: + event_ref = _query_params_to_event_reference(query_params) + case ["RequestIdReference"]: + request_id_ref = _query_params_to_request_id_reference(query_params) + case _: + raise ValueError( + f"Invalid Nexus link: {link}. Expected {LINK_REFERENCE_TYPE_PARAM_NAME} to be '{EVENT_REFERENCE_TYPE}' or '{REQUEST_ID_REFERENCE_TYPE}'" + ) + + except ValueError as err: logger.warning( - f"Invalid Nexus link: {link}. Expected path to match {_LINK_URL_PATH_REGEX.pattern}" + f"Failed to parse event reference from Nexus link URL query parameters: {link} ({err})" ) return None + + workflow_event_link = temporalio.api.common.v1.Link.WorkflowEvent( + namespace=groups["namespace"], + workflow_id=groups["workflow_id"], + run_id=groups["run_id"], + event_ref=event_ref, + request_id_ref=request_id_ref, + ) + return temporalio.api.common.v1.Link(workflow_event=workflow_event_link) + + +def nexus_link_to_workflow_link( + link: nexusrpc.Link, +) -> temporalio.api.common.v1.Link | None: + """Convert a nexus link into a Temporal Workflow link.""" + parsed = _parse_workflow_nexus_url(link, history=False) + if parsed is None: + return None + groups, query_params = parsed try: - event_ref = _query_params_to_event_reference(url.query) + reason = _optional_single_query_param(query_params, LINK_REASON_PARAM_NAME) except ValueError as err: + logger.warning(f"Invalid Nexus link: {link}. {err}") + return None + + workflow_link = temporalio.api.common.v1.Link.Workflow( + namespace=groups["namespace"], + workflow_id=groups["workflow_id"], + run_id=groups["run_id"], + reason=reason, + ) + return temporalio.api.common.v1.Link(workflow=workflow_link) + + +def nexus_link_to_nexus_operation_link( + nexus_link: nexusrpc.Link, +) -> temporalio.api.common.v1.Link | None: + """Convert a nexus link into a Temporal NexusOperation link + + This is used when propagating links from a Nexus start operation request to a + StartNexusOperation request. + """ + url = urllib.parse.urlparse(nexus_link.url) + match = _NEXUS_OPERATION_LINK_URL_PATH_REGEX.match(url.path) + if not match: logger.warning( - f"Failed to parse event reference from Nexus link URL query parameters: {link} ({err})" + f"Invalid Nexus link: {nexus_link}. Expected path to match {_NEXUS_OPERATION_LINK_URL_PATH_REGEX.pattern}" ) return None groups = match.groupdict() - return temporalio.api.common.v1.Link.WorkflowEvent( + nexus_op_link = temporalio.api.common.v1.Link.NexusOperation( namespace=urllib.parse.unquote(groups["namespace"]), - workflow_id=urllib.parse.unquote(groups["workflow_id"]), + operation_id=urllib.parse.unquote(groups["operation_id"]), run_id=urllib.parse.unquote(groups["run_id"]), - event_ref=event_ref, + ) + return temporalio.api.common.v1.Link(nexus_operation=nexus_op_link) + + +def nexus_link_to_activity_link( + nexus_link: nexusrpc.Link, +) -> temporalio.api.common.v1.Link | None: + """Convert a Nexus Activity link into a Temporal Activity link.""" + match = _ACTIVITY_LINK_URL_PATH_REGEX.match( + urllib.parse.urlparse(nexus_link.url).path + ) + if not match: + logger.warning( + f"Invalid Nexus link: {nexus_link}. Expected path to match {_ACTIVITY_LINK_URL_PATH_REGEX.pattern}" + ) + return None + groups = match.groupdict() + return temporalio.api.common.v1.Link( + activity=temporalio.api.common.v1.Link.Activity( + namespace=urllib.parse.unquote(groups["namespace"]), + activity_id=urllib.parse.unquote(groups["activity_id"]), + run_id=urllib.parse.unquote(groups["run_id"]), + ) ) @@ -109,36 +386,58 @@ def _event_reference_to_query_params( ) return urllib.parse.urlencode( { - "eventID": event_ref.event_id, - "eventType": event_type_name, - "referenceType": "EventReference", + LINK_EVENT_ID_PARAM_NAME: event_ref.event_id, + LINK_EVENT_TYPE_PARAM_NAME: event_type_name, + LINK_REFERENCE_TYPE_PARAM_NAME: EVENT_REFERENCE_TYPE, } ) +def _request_id_reference_to_query_params( + request_id_ref: temporalio.api.common.v1.Link.WorkflowEvent.RequestIdReference, +) -> str: + params = { + LINK_REFERENCE_TYPE_PARAM_NAME: REQUEST_ID_REFERENCE_TYPE, + } + + if request_id_ref.request_id: + params[LINK_REQUEST_ID_PARAM_NAME] = request_id_ref.request_id + + event_type_name = temporalio.api.enums.v1.EventType.Name(request_id_ref.event_type) + if event_type_name.startswith("EVENT_TYPE_"): + event_type_name = _event_type_constant_case_to_pascal_case( + event_type_name.removeprefix("EVENT_TYPE_") + ) + params[LINK_EVENT_TYPE_PARAM_NAME] = event_type_name + + return urllib.parse.urlencode(params) + + def _query_params_to_event_reference( - raw_query_params: str, + query_params: dict[str, list[str]], ) -> temporalio.api.common.v1.Link.WorkflowEvent.EventReference: """Return an EventReference from the query params or raise ValueError.""" - query_params = urllib.parse.parse_qs(raw_query_params) - - [reference_type] = query_params.get("referenceType") or [""] - if reference_type != "EventReference": + [reference_type] = query_params.get(LINK_REFERENCE_TYPE_PARAM_NAME) or [""] + if reference_type != EVENT_REFERENCE_TYPE: raise ValueError( f"Expected Nexus link URL query parameter referenceType to be EventReference but got: {reference_type}" ) + # event type - [raw_event_type_name] = query_params.get(LINK_EVENT_TYPE_PARAM_NAME) or [""] - if not raw_event_type_name: - raise ValueError(f"query params do not contain event type: {query_params}") - if raw_event_type_name.startswith("EVENT_TYPE_"): - event_type_name = raw_event_type_name - elif re.match("[A-Z][a-z]", raw_event_type_name): - event_type_name = "EVENT_TYPE_" + _event_type_pascal_case_to_constant_case( - raw_event_type_name - ) - else: - raise ValueError(f"Invalid event type name: {raw_event_type_name}") + match query_params.get(LINK_EVENT_TYPE_PARAM_NAME): + case None: + raise ValueError(f"query params do not contain event type: {query_params}") + + case [raw_event_type_name] if raw_event_type_name.startswith("EVENT_TYPE_"): + event_type_name = raw_event_type_name + + case [raw_event_type_name] if re.match("[A-Z][a-z]", raw_event_type_name): + event_type_name = "EVENT_TYPE_" + _event_type_pascal_case_to_constant_case( + raw_event_type_name + ) + + case raw_event_type_name: + raise ValueError(f"Invalid event type name: {raw_event_type_name}") # event id event_id = 0 @@ -155,6 +454,34 @@ def _query_params_to_event_reference( ) +def _query_params_to_request_id_reference( + query_params: dict[str, list[str]], +) -> temporalio.api.common.v1.Link.WorkflowEvent.RequestIdReference: + """Return an EventReference from the query params or raise ValueError.""" + # event type + match query_params.get(LINK_EVENT_TYPE_PARAM_NAME): + case None: + raise ValueError(f"query params do not contain event type: {query_params}") + + case [raw_event_type_name] if raw_event_type_name.startswith("EVENT_TYPE_"): + event_type_name = raw_event_type_name + + case [raw_event_type_name] if re.match("[A-Z][a-z]", raw_event_type_name): + event_type_name = "EVENT_TYPE_" + _event_type_pascal_case_to_constant_case( + raw_event_type_name + ) + + case raw_event_type_name: + raise ValueError(f"Invalid event type name: {raw_event_type_name}") + + [request_id] = query_params.get(LINK_REQUEST_ID_PARAM_NAME, [""]) + + return temporalio.api.common.v1.Link.WorkflowEvent.RequestIdReference( + request_id=request_id, + event_type=temporalio.api.enums.v1.EventType.Value(event_type_name), + ) + + def _event_type_constant_case_to_pascal_case(s: str) -> str: """Convert a CONSTANT_CASE string to PascalCase. diff --git a/temporalio/nexus/_operation_context.py b/temporalio/nexus/_operation_context.py index 515b5e814..54f8a987d 100644 --- a/temporalio/nexus/_operation_context.py +++ b/temporalio/nexus/_operation_context.py @@ -2,7 +2,14 @@ import dataclasses import logging -from collections.abc import Awaitable, Mapping, MutableMapping, Sequence +from collections.abc import ( + Awaitable, + Callable, + Generator, + Mapping, + MutableMapping, + Sequence, +) from contextlib import contextmanager from contextvars import ContextVar from dataclasses import dataclass @@ -10,21 +17,23 @@ from typing import ( TYPE_CHECKING, Any, - Callable, - Generator, - Optional, - Union, + Concatenate, + Generic, + TypeVar, overload, ) -from nexusrpc.handler import CancelOperationContext, StartOperationContext -from typing_extensions import Concatenate +import nexusrpc +from nexusrpc.handler import ( + CancelOperationContext, + OperationContext, + StartOperationContext, +) +from typing_extensions import Self import temporalio.api.common.v1 import temporalio.api.workflowservice.v1 import temporalio.common -from temporalio.nexus import _link_conversion -from temporalio.nexus._token import WorkflowHandle from temporalio.types import ( MethodAsyncNoParam, MethodAsyncSingleParam, @@ -34,6 +43,13 @@ SelfType, ) +from ._link_conversion import ( + nexus_link_to_temporal_link, + temporal_link_to_nexus_link, + workflow_execution_started_event_link_from_workflow_handle, +) +from ._token import OperationToken, OperationTokenType, WorkflowHandle + if TYPE_CHECKING: import temporalio.client @@ -49,12 +65,12 @@ ContextVar("temporal-cancel-operation-context") ) -# A Nexus start handler might start zero or more workflows as usual using a Temporal client. In -# addition, it may start one "nexus-backing" workflow, using -# WorkflowRunOperationContext.start_workflow. This context is active while the latter is being done. +# A Nexus start handler might start zero or more async Temporal actions as usual using a Temporal client. In +# addition, it may start one "nexus-backing" async Temporal action, using +# WorkflowRunOperationContext.start_workflow or methods from TemporalNexusClient. This context is active while the latter is being done. # It is thus a narrower context than _temporal_start_operation_context. -_temporal_nexus_backing_workflow_start_context: ContextVar[bool] = ContextVar( - "temporal-nexus-backing-workflow-start-context" +_temporal_nexus_backing_start_context: ContextVar[bool] = ContextVar( + "temporal-nexus-backing-start-context" ) @@ -62,12 +78,15 @@ class Info: """Information about the running Nexus operation. - .. warning:: - This API is experimental and unstable. - Retrieved inside a Nexus operation handler via :py:func:`info`. """ + endpoint: str + """The endpoint this Nexus request was addressed to.""" + + namespace: str + """The namespace of the worker handling this Nexus operation.""" + task_queue: str """The task queue of the worker handling this Nexus operation.""" @@ -87,8 +106,51 @@ def client() -> temporalio.client.Client: return _temporal_context().client +def metric_meter() -> temporalio.common.MetricMeter: + """Get the metric meter for the current Nexus operation.""" + return _temporal_context().metric_meter + + +def is_worker_shutdown() -> bool: + """Whether shutdown has been invoked on the worker. + + Returns: + True if shutdown has been called on the worker, False otherwise. + + Raises: + RuntimeError: When not in a Nexus operation. + """ + return _temporal_context()._worker_shutdown_event.is_set() + + +async def wait_for_worker_shutdown() -> None: + """Asynchronously wait for shutdown to be called on the worker. + + Raises: + RuntimeError: When not in a Nexus operation. + """ + await _temporal_context()._worker_shutdown_event.wait() + + +def wait_for_worker_shutdown_sync(timeout: timedelta | float | None = None) -> None: + """Synchronously block while waiting for shutdown to be called on the worker. + + This is essentially a wrapper around :py:meth:`threading.Event.wait`. + + Args: + timeout: Max amount of time to wait for shutdown to be called on the + worker. + + Raises: + RuntimeError: When not in a Nexus operation. + """ + _temporal_context()._worker_shutdown_event.wait_sync( + timeout.total_seconds() if isinstance(timeout, timedelta) else timeout + ) + + def _temporal_context() -> ( - Union[_TemporalStartOperationContext, _TemporalCancelOperationContext] + _TemporalStartOperationContext | _TemporalCancelOperationContext ): ctx = _try_temporal_context() if ctx is None: @@ -97,7 +159,7 @@ def _temporal_context() -> ( def _try_temporal_context() -> ( - Optional[Union[_TemporalStartOperationContext, _TemporalCancelOperationContext]] + _TemporalStartOperationContext | _TemporalCancelOperationContext | None ): start_ctx = _temporal_start_operation_context.get(None) cancel_ctx = _temporal_cancel_operation_context.get(None) @@ -106,31 +168,58 @@ def _try_temporal_context() -> ( return start_ctx or cancel_ctx +def _try_start_operation_context() -> _TemporalStartOperationContext | None: # pyright: ignore[reportUnusedFunction] + """Return the active Nexus start-operation context, if any.""" + return _temporal_start_operation_context.get(None) + + @contextmanager -def _nexus_backing_workflow_start_context() -> Generator[None, None, None]: - token = _temporal_nexus_backing_workflow_start_context.set(True) +def _nexus_backing_start_context() -> Generator[None]: + token = _temporal_nexus_backing_start_context.set(True) try: yield finally: - _temporal_nexus_backing_workflow_start_context.reset(token) + _temporal_nexus_backing_start_context.reset(token) -def _in_nexus_backing_workflow_start_context() -> bool: - return _temporal_nexus_backing_workflow_start_context.get(False) +def _in_nexus_backing_start_context() -> bool: # type:ignore[reportUnusedClass] + return _temporal_nexus_backing_start_context.get(False) -@dataclass -class _TemporalStartOperationContext: - """Context for a Nexus start operation being handled by a Temporal Nexus Worker.""" +_OperationCtxT = TypeVar("_OperationCtxT", bound=OperationContext) - nexus_context: StartOperationContext - """Nexus-specific start operation context.""" + +@dataclass(kw_only=True) +class _TemporalOperationCtx(Generic[_OperationCtxT]): + client: temporalio.client.Client + """The Temporal client in use by the worker handling the current Nexus operation.""" info: Callable[[], Info] """Temporal information about the running Nexus operation.""" - client: temporalio.client.Client - """The Temporal client in use by the worker handling this Nexus operation.""" + nexus_context: _OperationCtxT + """Nexus-specific start operation context.""" + + _runtime_metric_meter: temporalio.common.MetricMeter + _worker_shutdown_event: temporalio.common._CompositeEvent + _metric_meter: temporalio.common.MetricMeter | None = None + + @property + def metric_meter(self) -> temporalio.common.MetricMeter: + if not self._metric_meter: + self._metric_meter = self._runtime_metric_meter.with_additional_attributes( + { + "nexus_service": self.nexus_context.service, + "nexus_operation": self.nexus_context.operation, + "task_queue": self.info().task_queue, + } + ) + return self._metric_meter + + +@dataclass +class _TemporalStartOperationContext(_TemporalOperationCtx[StartOperationContext]): + """Context for a Nexus start operation being handled by a Temporal Nexus Worker.""" @classmethod def get(cls) -> _TemporalStartOperationContext: @@ -142,67 +231,91 @@ def get(cls) -> _TemporalStartOperationContext: def set(self) -> None: _temporal_start_operation_context.set(self) - def _get_callbacks( - self, - ) -> list[temporalio.client.Callback]: + def _get_callbacks(self, token: str) -> list[temporalio.client.Callback]: ctx = self.nexus_context + callback_headers = {**ctx.callback_headers, "nexus-operation-token": token} return ( [ NexusCallback( url=ctx.callback_url, - headers=ctx.callback_headers, + headers=callback_headers, ) ] if ctx.callback_url else [] ) - def _get_workflow_event_links( - self, - ) -> list[temporalio.api.common.v1.Link.WorkflowEvent]: - event_links = [] + def _get_request_links(self) -> list[temporalio.api.common.v1.Link]: + """Request links to attach to RPCs the operation handler issues. + + These are the inbound Nexus task links. When the operation handler signals, + signal-with-starts, or starts a workflow, these links are added to the request's + ``links`` field so the callee's history event links back to whatever scheduled this + Nexus operation. + """ + links: list[temporalio.api.common.v1.Link] = [] for inbound_link in self.nexus_context.inbound_links: - if link := _link_conversion.nexus_link_to_workflow_event(inbound_link): - event_links.append(link) - return event_links + if link := nexus_link_to_temporal_link(inbound_link): + links.append(link) + return links - def _add_outbound_links( + def _add_start_workflow_response_link( self, workflow_handle: temporalio.client.WorkflowHandle[Any, Any] ): - # If links were not sent in StartWorkflowExecutionResponse then construct them. - wf_event_links: list[temporalio.api.common.v1.Link.WorkflowEvent] = [] - try: - if isinstance( - workflow_handle._start_workflow_response, - temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse, - ): - if workflow_handle._start_workflow_response.HasField("link"): - if link := workflow_handle._start_workflow_response.link: - if link.HasField("workflow_event"): - wf_event_links.append(link.workflow_event) - if not wf_event_links: - wf_event_links = [ - _link_conversion.workflow_execution_started_event_link_from_workflow_handle( - workflow_handle + response = workflow_handle._start_workflow_response + + nexus_link: nexusrpc.Link | None = None + if isinstance( + response, temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse + ): + if response.HasField("link"): + nexus_link = temporal_link_to_nexus_link(response.link) + else: + # If a link was not sent in response then construct it. + link = temporalio.api.common.v1.Link( + workflow_event=workflow_execution_started_event_link_from_workflow_handle( + workflow_handle, + self.nexus_context.request_id, ) - ] - self.nexus_context.outbound_links.extend( - _link_conversion.workflow_event_to_nexus_link(link) - for link in wf_event_links - ) + ) + nexus_link = temporal_link_to_nexus_link(link) + + elif isinstance( + response, + temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse, + ): + # Server >= 1.31 with EnableCHASMSignalBacklinks returns signal_link pointing at + # the WorkflowExecutionSignaled event; older servers leave it unset. + if response.HasField("signal_link"): + nexus_link = temporal_link_to_nexus_link(response.signal_link) + + try: + if nexus_link is not None: + self.nexus_context.outbound_links.append(nexus_link) except Exception as e: logger.warning( - f"Failed to create WorkflowExecutionStarted event links for workflow {workflow_handle}: {e}" + f"Failed to create event links for workflow {workflow_handle}: {e}" ) - return workflow_handle + def _add_response_link(self, link: temporalio.api.common.v1.Link | None) -> None: + """Append a response link returned by an RPC the operation handler issued. -class WorkflowRunOperationContext(StartOperationContext): - """Context received by a workflow run operation. + ``link`` is the ``common.v1.Link`` returned on a signal, signal-with-start, or start + response (or ``None`` against a server that did not return one). When present, it is + converted to a Nexus link and added to the operation's outbound links. - .. warning:: - This API is experimental and unstable. - """ + This is only safe to call from the single thread/task that runs the operation handler. + """ + if link is not None: + try: + if response_link := temporal_link_to_nexus_link(link): + self.nexus_context.outbound_links.append(response_link) + except Exception as e: + logger.warning(f"Failed to create Nexus link from Temporal link: {e}") + + +class WorkflowRunOperationContext(StartOperationContext): + """Context received by a workflow run operation.""" def __init__(self, *args: Any, **kwargs: Any) -> None: """Initialize the workflow run operation context.""" @@ -217,6 +330,11 @@ def _from_start_operation_context( **{f.name: getattr(ctx, f.name) for f in dataclasses.fields(ctx)}, ) + @property + def metric_meter(self) -> temporalio.common.MetricMeter: + """The metric meter""" + return self._temporal_context.metric_meter + # Overload for no-param workflow @overload async def start_workflow( @@ -224,31 +342,29 @@ async def start_workflow( workflow: MethodAsyncNoParam[SelfType, ReturnType], *, id: str, - task_queue: Optional[str] = None, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, + task_queue: str | None = None, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, + retry_policy: temporalio.common.RetryPolicy | None = None, cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.TypedSearchAttributes, - temporalio.common.SearchAttributes, - ] - ] = None, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - start_delay: Optional[timedelta] = None, - start_signal: Optional[str] = None, + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, start_signal_args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, request_eager_start: bool = False, priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: Optional[temporalio.common.VersioningOverride] = None, + versioning_override: temporalio.common.VersioningOverride | None = None, ) -> WorkflowHandle[ReturnType]: ... # Overload for single-param workflow @@ -259,31 +375,29 @@ async def start_workflow( arg: ParamType, *, id: str, - task_queue: Optional[str] = None, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, + task_queue: str | None = None, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, + retry_policy: temporalio.common.RetryPolicy | None = None, cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.TypedSearchAttributes, - temporalio.common.SearchAttributes, - ] - ] = None, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - start_delay: Optional[timedelta] = None, - start_signal: Optional[str] = None, + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, start_signal_args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, request_eager_start: bool = False, priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: Optional[temporalio.common.VersioningOverride] = None, + versioning_override: temporalio.common.VersioningOverride | None = None, ) -> WorkflowHandle[ReturnType]: ... # Overload for multi-param workflow @@ -296,31 +410,29 @@ async def start_workflow( *, args: Sequence[Any], id: str, - task_queue: Optional[str] = None, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, + task_queue: str | None = None, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, + retry_policy: temporalio.common.RetryPolicy | None = None, cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.TypedSearchAttributes, - temporalio.common.SearchAttributes, - ] - ] = None, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - start_delay: Optional[timedelta] = None, - start_signal: Optional[str] = None, + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, start_signal_args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, request_eager_start: bool = False, priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: Optional[temporalio.common.VersioningOverride] = None, + versioning_override: temporalio.common.VersioningOverride | None = None, ) -> WorkflowHandle[ReturnType]: ... # Overload for string-name workflow @@ -332,67 +444,63 @@ async def start_workflow( *, args: Sequence[Any] = [], id: str, - task_queue: Optional[str] = None, - result_type: Optional[type[ReturnType]] = None, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, + task_queue: str | None = None, + result_type: type[ReturnType] | None = None, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, + retry_policy: temporalio.common.RetryPolicy | None = None, cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.TypedSearchAttributes, - temporalio.common.SearchAttributes, - ] - ] = None, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - start_delay: Optional[timedelta] = None, - start_signal: Optional[str] = None, + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, start_signal_args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, request_eager_start: bool = False, priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: Optional[temporalio.common.VersioningOverride] = None, + versioning_override: temporalio.common.VersioningOverride | None = None, ) -> WorkflowHandle[ReturnType]: ... async def start_workflow( self, - workflow: Union[str, Callable[..., Awaitable[ReturnType]]], + workflow: str | Callable[..., Awaitable[ReturnType]], arg: Any = temporalio.common._arg_unset, *, args: Sequence[Any] = [], id: str, - task_queue: Optional[str] = None, - result_type: Optional[type] = None, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, + task_queue: str | None = None, + result_type: type | None = None, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, + retry_policy: temporalio.common.RetryPolicy | None = None, cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.TypedSearchAttributes, - temporalio.common.SearchAttributes, - ] - ] = None, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - start_delay: Optional[timedelta] = None, - start_signal: Optional[str] = None, + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, start_signal_args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, request_eager_start: bool = False, priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: Optional[temporalio.common.VersioningOverride] = None, + versioning_override: temporalio.common.VersioningOverride | None = None, ) -> WorkflowHandle[ReturnType]: """Start a workflow that will deliver the result of the Nexus operation. @@ -419,59 +527,39 @@ async def start_workflow( Nexus caller is itself a workflow, this means that the workflow in the caller namespace web UI will contain links to the started workflow, and vice versa. """ - # We must pass nexus_completion_callbacks, workflow_event_links, and request_id, - # but these are deliberately not exposed in overloads, hence the type-check - # violation. - - # Here we are starting a "nexus-backing" workflow. That means that the StartWorkflow request - # contains nexus-specific data such as a completion callback (used by the handler server - # namespace to deliver the result to the caller namespace when the workflow reaches a - # terminal state) and inbound links to the caller workflow (attached to history events of - # the workflow started in the handler namespace, and displayed in the UI). - with _nexus_backing_workflow_start_context(): - wf_handle = await self._temporal_context.client.start_workflow( # type: ignore - workflow=workflow, - arg=arg, - args=args, - id=id, - task_queue=task_queue or self._temporal_context.info().task_queue, - result_type=result_type, - execution_timeout=execution_timeout, - run_timeout=run_timeout, - task_timeout=task_timeout, - id_reuse_policy=id_reuse_policy, - id_conflict_policy=id_conflict_policy, - retry_policy=retry_policy, - cron_schedule=cron_schedule, - memo=memo, - search_attributes=search_attributes, - static_summary=static_summary, - static_details=static_details, - start_delay=start_delay, - start_signal=start_signal, - start_signal_args=start_signal_args, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - request_eager_start=request_eager_start, - priority=priority, - versioning_override=versioning_override, - callbacks=self._temporal_context._get_callbacks(), - workflow_event_links=self._temporal_context._get_workflow_event_links(), - request_id=self._temporal_context.nexus_context.request_id, - ) - - self._temporal_context._add_outbound_links(wf_handle) - - return WorkflowHandle[ReturnType]._unsafe_from_client_workflow_handle(wf_handle) + return await _start_nexus_backing_workflow( + temporal_context=self._temporal_context, + workflow=workflow, + arg=arg, + args=args, + id=id, + task_queue=task_queue, + result_type=result_type, + execution_timeout=execution_timeout, + run_timeout=run_timeout, + task_timeout=task_timeout, + id_reuse_policy=id_reuse_policy, + id_conflict_policy=id_conflict_policy, + retry_policy=retry_policy, + cron_schedule=cron_schedule, + memo=memo, + search_attributes=search_attributes, + static_summary=static_summary, + static_details=static_details, + start_delay=start_delay, + start_signal=start_signal, + start_signal_args=start_signal_args, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + request_eager_start=request_eager_start, + priority=priority, + versioning_override=versioning_override, + ) @dataclass(frozen=True) class NexusCallback: - """Nexus callback to attach to events such as workflow completion. - - .. warning:: - This API is experimental and unstable. - """ + """Nexus callback to attach to events such as workflow completion.""" url: str """Callback URL.""" @@ -480,19 +568,10 @@ class NexusCallback: """Header to attach to callback request.""" -@dataclass(frozen=True) -class _TemporalCancelOperationContext: +@dataclass +class _TemporalCancelOperationContext(_TemporalOperationCtx[CancelOperationContext]): """Context for a Nexus cancel operation being handled by a Temporal Nexus Worker.""" - nexus_context: CancelOperationContext - """Nexus-specific cancel operation context.""" - - info: Callable[[], Info] - """Temporal information about the running Nexus cancel operation.""" - - client: temporalio.client.Client - """The Temporal client in use by the worker handling the current Nexus operation.""" - @classmethod def get(cls) -> _TemporalCancelOperationContext: ctx = _temporal_cancel_operation_context.get(None) @@ -504,10 +583,38 @@ def set(self) -> None: _temporal_cancel_operation_context.set(self) +class TemporalStartOperationContext(StartOperationContext): + """Context received by a Temporal Nexus operation when it is started. + + .. warning:: + This API is experimental and unstable. + """ + + @classmethod + def _from_start_operation_context(cls, ctx: StartOperationContext) -> Self: + return cls( + **{f.name: getattr(ctx, f.name) for f in dataclasses.fields(ctx)}, + ) + + +class TemporalCancelOperationContext(CancelOperationContext): + """Context received by a Temporal Nexus operation when it is canceled. + + .. warning:: + This API is experimental and unstable. + """ + + @classmethod + def _from_cancel_operation_context(cls, ctx: CancelOperationContext) -> Self: + return cls( + **{f.name: getattr(ctx, f.name) for f in dataclasses.fields(ctx)}, + ) + + class LoggerAdapter(logging.LoggerAdapter): """Logger adapter that adds Nexus operation context information.""" - def __init__(self, logger: logging.Logger, extra: Optional[Mapping[str, Any]]): + def __init__(self, logger: logging.Logger, extra: Mapping[str, Any] | None): """Initialize the logger adapter.""" super().__init__(logger, extra or {}) @@ -526,3 +633,188 @@ def process( logger = LoggerAdapter(logging.getLogger("temporalio.nexus"), None) """Logger that emits additional data describing the current Nexus operation.""" + + +async def _start_nexus_backing_workflow( + temporal_context: _TemporalStartOperationContext, + workflow: str | Callable[..., Awaitable[ReturnType]], + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str | None = None, + result_type: type | None = None, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + request_eager_start: bool = False, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, +) -> WorkflowHandle[ReturnType]: + # We must pass nexus_completion_callbacks, links, and request_id, + # but these are deliberately not exposed in overloads, hence the type-check + # violation. + + # Here we are starting a "nexus-backing" workflow. That means that the StartWorkflow request + # contains nexus-specific data such as a completion callback (used by the handler server + # namespace to deliver the result to the caller namespace when the workflow reaches a + # terminal state) and inbound links to the caller workflow (attached to history events of + # the workflow started in the handler namespace, and displayed in the UI). + with _nexus_backing_start_context(): + token = OperationToken( + type=OperationTokenType.WORKFLOW, + namespace=temporal_context.client.namespace, + workflow_id=id, + ).encode() + wf_handle = await temporal_context.client.start_workflow( # type: ignore + workflow=workflow, + arg=arg, + args=args, + id=id, + task_queue=task_queue or temporal_context.info().task_queue, + result_type=result_type, + execution_timeout=execution_timeout, + run_timeout=run_timeout, + task_timeout=task_timeout, + id_reuse_policy=id_reuse_policy, + id_conflict_policy=id_conflict_policy, + retry_policy=retry_policy, + cron_schedule=cron_schedule, + memo=memo, + search_attributes=search_attributes, + static_summary=static_summary, + static_details=static_details, + start_delay=start_delay, + start_signal=start_signal, + start_signal_args=start_signal_args, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + request_eager_start=request_eager_start, + priority=priority, + versioning_override=versioning_override, + callbacks=temporal_context._get_callbacks(token), + links=temporal_context._get_request_links(), + request_id=temporal_context.nexus_context.request_id, + ) + + return WorkflowHandle[ReturnType]._unsafe_from_client_workflow_handle(wf_handle) + + +async def _start_nexus_operation_workflow_update( # pyright: ignore[reportUnusedFunction] + *, + temporal_context: _TemporalStartOperationContext, + workflow_id: str, + update: str | Callable, + arg: Any = temporalio.common._arg_unset, + args: Sequence[Any] = [], + update_id: str | None = None, + result_type: type | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + run_id: str | None = None, + first_execution_run_id: str | None = None, +) -> temporalio.client.WorkflowUpdateHandle[Any]: + # Default update ID to the Nexus request ID for retry-safety (matches sdk-go). + update_id = update_id or temporal_context.nexus_context.request_id + # This token is different from the actual token returned to the caller + # because return token will have the run_id that is unknowable before + # making the call. If run_id is passed, then it will be the same + token = OperationToken( + type=OperationTokenType.UPDATE_WORKFLOW, + namespace=temporal_context.client.namespace, + workflow_id=workflow_id, + update_id=update_id, + run_id=run_id, + ).encode() + workflow_handle = temporal_context.client.get_workflow_handle( + workflow_id, run_id=run_id, first_execution_run_id=first_execution_run_id + ) + return await workflow_handle._start_update( + update, + arg, + args=args, + wait_for_stage=temporalio.client.WorkflowUpdateStage.ACCEPTED, # hardcoded as nexus only supports async updates + id=update_id, + result_type=result_type, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + callbacks=temporal_context._get_callbacks(token), + links=temporal_context._get_request_links(), + request_id=temporal_context.nexus_context.request_id, + ) + + +def _apply_nexus_context_to_start_activity_request( # pyright: ignore[reportUnusedFunction] + req: temporalio.api.workflowservice.v1.StartActivityExecutionRequest, +) -> None: + """Apply the current Nexus operation context to an activity start request. + + This is a no-op outside a Nexus operation context. Within one, it attaches + the Nexus request ID and configures conflict handling to preserve the Nexus + metadata. Inbound links are attached to the completion callback when the + activity backs the operation and to the request otherwise. + """ + nexus_ctx = _try_start_operation_context() + if nexus_ctx is not None: + req.on_conflict_options.attach_request_id = True + req.on_conflict_options.attach_completion_callbacks = True + req.on_conflict_options.attach_links = True + + req.request_id = nexus_ctx.nexus_context.request_id + request_links = nexus_ctx._get_request_links() + + if _in_nexus_backing_start_context(): + callbacks = nexus_ctx._get_callbacks( + OperationToken( + type=OperationTokenType.ACTIVITY, + namespace=nexus_ctx.client.namespace, + activity_id=req.activity_id, + ).encode() + ) + req.completion_callbacks.extend( + temporalio.api.common.v1.Callback( + nexus=temporalio.api.common.v1.Callback.Nexus( + url=callback.url, + header=callback.headers, + ), + links=request_links, + ) + for callback in callbacks + ) + else: + req.links.extend(request_links) + + +def _apply_start_activity_response_to_nexus_context( # pyright: ignore[reportUnusedFunction] + activity_id: str, + resp: temporalio.api.workflowservice.v1.StartActivityExecutionResponse, +): + nexus_ctx = _try_start_operation_context() + if nexus_ctx is not None: + if resp.HasField("link"): + response_link = resp.link + else: + response_link = temporalio.api.common.v1.Link( + activity=temporalio.api.common.v1.Link.Activity( + namespace=nexus_ctx.client.namespace, + activity_id=activity_id, + run_id=resp.run_id, + ) + ) + nexus_ctx._add_response_link(response_link) diff --git a/temporalio/nexus/_operation_handlers.py b/temporalio/nexus/_operation_handlers.py index a73c3eb50..36c44e96c 100644 --- a/temporalio/nexus/_operation_handlers.py +++ b/temporalio/nexus/_operation_handlers.py @@ -1,31 +1,36 @@ from __future__ import annotations -from typing import ( - Any, - Awaitable, - Callable, -) +from abc import ABC, abstractmethod +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Any from nexusrpc import ( HandlerError, HandlerErrorType, InputT, - OperationInfo, OutputT, ) from nexusrpc.handler import ( CancelOperationContext, - FetchOperationInfoContext, - FetchOperationResultContext, OperationHandler, StartOperationContext, StartOperationResultAsync, + StartOperationResultSync, ) +import temporalio.nexus from temporalio.nexus._operation_context import ( + TemporalCancelOperationContext, + TemporalStartOperationContext, _temporal_cancel_operation_context, ) -from temporalio.nexus._token import WorkflowHandle +from temporalio.nexus._temporal_client import ( + TemporalNexusClient, + TemporalOperationResult, + _TemporalNexusClient, +) +from temporalio.nexus._token import OperationToken, OperationTokenType, WorkflowHandle from ._util import ( is_async_callable, @@ -81,22 +86,6 @@ async def cancel(self, ctx: CancelOperationContext, token: str) -> None: """Cancel the operation, by cancelling the workflow.""" await _cancel_workflow(token) - async def fetch_info( - self, ctx: FetchOperationInfoContext, token: str - ) -> OperationInfo: - """Fetch operation info (not supported for Temporal Nexus operations).""" - raise NotImplementedError( - "Temporal Nexus operation handlers do not support fetching operation info." - ) - - async def fetch_result( - self, ctx: FetchOperationResultContext, token: str - ) -> OutputT: - """Fetch operation result (not supported for Temporal Nexus operations).""" - raise NotImplementedError( - "Temporal Nexus operation handlers do not support fetching the operation result." - ) - async def _cancel_workflow( token: str, @@ -110,7 +99,7 @@ async def _cancel_workflow( Args: token: The token of the workflow to cancel. kwargs: Additional keyword arguments - to pass to the workflow cancel method. + to pass to the workflow cancel method. """ try: nexus_workflow_handle = WorkflowHandle[Any].from_token(token) @@ -132,3 +121,175 @@ async def _cancel_workflow( type=HandlerErrorType.NOT_FOUND, ) from err await client_workflow_handle.cancel(**kwargs) + + +@dataclass(frozen=True) +class CancelWorkflowRunOptions: + """Options for cancelling the workflow backing a Nexus operation. + + These options are built by :py:class:`TemporalOperationHandler` and passed to + :py:meth:`TemporalOperationHandler.cancel_workflow_run`. + + .. warning:: + This API is experimental and unstable. + """ + + workflow_id: str + """The ID of the workflow to cancel.""" + + +@dataclass(frozen=True) +class CancelUpdateWorkflowOptions: + """Options for cancelling the workflow update backing a Nexus operation. + + These options are built by :py:class:`TemporalOperationHandler` and passed to + :py:meth:`TemporalOperationHandler.cancel_workflow_update`. + + .. warning:: + This API is experimental and unstable. + """ + + workflow_id: str + """The ID of the workflow where the update is running.""" + update_id: str + """The ID of the update to cancel.""" + run_id: str + """The workflow runID that accepted the update.""" + + +@dataclass(frozen=True) +class CancelActivityOptions: + """Options for cancelling the activity backing a Nexus operation.""" + + activity_id: str + """The activity ID to cancel.""" + run_id: str + """The run ID of the activity to cancel.""" + + +class TemporalOperationHandler(OperationHandler[InputT, OutputT], ABC): + """Operation handler for Nexus operations that interact with Temporal. + Implementations override the start_operation method. + + .. warning:: + This API is experimental and unstable. + """ + + @abstractmethod + async def start_operation( + self, + ctx: TemporalStartOperationContext, + client: TemporalNexusClient, + input: InputT, + ) -> TemporalOperationResult[OutputT]: + """Start the Temporal-backed Nexus operation.""" + ... + + async def start( + self, ctx: StartOperationContext, input: InputT + ) -> StartOperationResultSync[OutputT] | StartOperationResultAsync: + """Start the Nexus operation using a Nexus-aware Temporal client. + + .. warning:: + This API is experimental and unstable. + """ + nexus_client = _TemporalNexusClient() + start_ctx = TemporalStartOperationContext._from_start_operation_context(ctx) + result = await self.start_operation(start_ctx, nexus_client, input) + return result._to_nexus_result() + + async def cancel(self, ctx: CancelOperationContext, token: str) -> None: + """Cancel a Nexus operation using its operation token. + + .. warning:: + This API is experimental and unstable. + """ + try: + operation_token = OperationToken.decode(token) + except Exception as err: + raise HandlerError( + "Unable to decode operation token to cancel", + type=HandlerErrorType.INTERNAL, + retryable_override=False, + ) from err + + cancel_ctx = TemporalCancelOperationContext._from_cancel_operation_context(ctx) + match operation_token.type: + case OperationTokenType.WORKFLOW: + assert operation_token.workflow_id is not None + options = CancelWorkflowRunOptions( + workflow_id=operation_token.workflow_id + ) + await self.cancel_workflow_run(cancel_ctx, options) + case OperationTokenType.UPDATE_WORKFLOW: + assert operation_token.workflow_id is not None + assert operation_token.update_id is not None + assert operation_token.run_id is not None + cancel_options = CancelUpdateWorkflowOptions( + workflow_id=operation_token.workflow_id, + update_id=operation_token.update_id, + run_id=operation_token.run_id, + ) + await self.cancel_workflow_update(cancel_ctx, cancel_options) + case OperationTokenType.ACTIVITY: + assert operation_token.activity_id is not None + if not operation_token.run_id: + raise HandlerError( + "Expected operation token of type ACTIVITY to have a valid run id.", + type=HandlerErrorType.INTERNAL, + retryable_override=False, + ) + + await self.cancel_activity( + cancel_ctx, + CancelActivityOptions( + activity_id=operation_token.activity_id, + run_id=operation_token.run_id, + ), + ) + + async def cancel_workflow_run( + self, + ctx: TemporalCancelOperationContext, # pyright: ignore[reportUnusedParameter] + options: CancelWorkflowRunOptions, + ) -> None: + """Cancels the workflow backing the Nexus operation. + + .. warning:: + This API is experimental and unstable. + """ + workflow_handle = temporalio.nexus.client().get_workflow_handle( + options.workflow_id + ) + await workflow_handle.cancel() + + async def cancel_workflow_update( + self, + ctx: TemporalCancelOperationContext, # pyright: ignore[reportUnusedParameter] + options: CancelUpdateWorkflowOptions, # pyright: ignore[reportUnusedParameter] + ) -> None: + """Cancels the Workflow Update triggered by the Nexus operation. There is no native way to cancel an accepted Update, so cancellation depends on the Update handler itself. + Override this method and coordinate with the specific Update handler to trigger a cancellation. + + + .. warning:: + This API is experimental and unstable. + """ + raise HandlerError( + """ + There is no native way to cancel an accepted Update, so cancellation depends on the Update handler itself. + Override this method and coordinate with the specific Update handler to trigger a cancellation. + """, + type=HandlerErrorType.NOT_IMPLEMENTED, + ) + + async def cancel_activity( + self, + ctx: TemporalCancelOperationContext, # pyright: ignore[reportUnusedParameter] + options: CancelActivityOptions, + ) -> None: + """Requests cancellation of the standalone activity backing the operation.""" + activity_handle = temporalio.nexus.client().get_activity_handle( + options.activity_id, run_id=options.run_id + ) + await activity_handle.cancel() diff --git a/temporalio/nexus/_temporal_client.py b/temporalio/nexus/_temporal_client.py new file mode 100644 index 000000000..393da2fae --- /dev/null +++ b/temporalio/nexus/_temporal_client.py @@ -0,0 +1,794 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Awaitable, Callable, Iterator, Mapping, Sequence +from contextlib import contextmanager +from dataclasses import dataclass +from datetime import timedelta +from typing import ( + TYPE_CHECKING, + Any, + Concatenate, + Generic, + TypeVar, + cast, + overload, +) + +from nexusrpc import HandlerError, HandlerErrorType, OperationError, OperationErrorState +from nexusrpc.handler import StartOperationResultAsync, StartOperationResultSync +from typing_extensions import Self + +import temporalio.common +from temporalio.nexus._operation_context import ( + _nexus_backing_start_context, + _start_nexus_backing_workflow, + _start_nexus_operation_workflow_update, + _TemporalStartOperationContext, +) +from temporalio.nexus._token import OperationToken, OperationTokenType +from temporalio.types import ( + CallableAsyncNoParam, + CallableAsyncSingleParam, + CallableSyncNoParam, + CallableSyncSingleParam, + MethodAsyncNoParam, + MethodAsyncSingleParam, + MultiParamSpec, + ParamType, + ReturnType, + SelfType, +) + +if TYPE_CHECKING: + import temporalio.client + import temporalio.workflow + + +_ResultT = TypeVar("_ResultT") + + +@dataclass(frozen=True) +class TemporalOperationResult(Generic[_ResultT]): + """Unified result: sync value or async token. + + .. warning:: + This API is experimental and unstable. + """ + + value: _ResultT | object = temporalio.common._arg_unset + token: str | None = None + + def __post_init__(self) -> None: + """Validate that the result represents exactly one completion mode.""" + has_value = self.value is not temporalio.common._arg_unset + has_token = self.token is not None + if has_value == has_token: + raise ValueError( + "TemporalOperationResult must have exactly one of value or token set." + ) + if has_token and (not isinstance(self.token, str) or not self.token): + raise ValueError( + "TemporalOperationResult token must be a non-empty string." + ) + + @classmethod + def sync(cls, value: _ResultT) -> Self: + """Create a result that completes the Nexus operation synchronously.""" + return cls(value=value) + + @classmethod + def async_token(cls, token: str) -> Self: + """Create a result that completes the Nexus operation asynchronously.""" + return cls(token=token) + + def _to_nexus_result( + self, + ) -> StartOperationResultSync[_ResultT] | StartOperationResultAsync: + if self.token is not None: + return StartOperationResultAsync(self.token) + elif self.value is not temporalio.common._arg_unset: + return StartOperationResultSync(cast(_ResultT, self.value)) + else: + raise RuntimeError( + "Invalid TemporalOperationResult. Neither token nor value are set." + ) + + +class TemporalNexusClient(ABC): + """Nexus-aware wrapper around a Temporal Client. + + .. warning:: + This API is experimental and unstable. + """ + + @property + @abstractmethod + def client(self) -> temporalio.client.Client: + """The underlying Temporal Client + + .. warning:: + This API is experimental and unstable. + """ + ... + + # Overload for no-param workflow + @overload + async def start_workflow( + self, + workflow: MethodAsyncNoParam[SelfType, ReturnType], + *, + id: str, + task_queue: str | None = None, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + request_eager_start: bool = False, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> TemporalOperationResult[ReturnType]: ... + + # Overload for single-param workflow + @overload + async def start_workflow( + self, + workflow: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], + arg: ParamType, + *, + id: str, + task_queue: str | None = None, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + request_eager_start: bool = False, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> TemporalOperationResult[ReturnType]: ... + + # Overload for multi-param workflow + @overload + async def start_workflow( + self, + workflow: Callable[ + Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType] + ], + *, + args: Sequence[Any], + id: str, + task_queue: str | None = None, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + request_eager_start: bool = False, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> TemporalOperationResult[ReturnType]: ... + + # Overload for string-name workflow + @overload + async def start_workflow( + self, + workflow: str, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str | None = None, + result_type: type[ReturnType] | None = None, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + request_eager_start: bool = False, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> TemporalOperationResult[ReturnType]: ... + + @abstractmethod + async def start_workflow( + self, + workflow: str | Callable[..., Awaitable[ReturnType]], + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str | None = None, + result_type: type | None = None, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + request_eager_start: bool = False, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> TemporalOperationResult[ReturnType]: + """Start a workflow as the backing asynchronous Nexus operation. + + .. warning:: + This API is experimental and unstable. + """ + ... + + # Overload for no-param update + @overload + async def start_workflow_update( + self, + workflow_id: str, + update: temporalio.workflow.UpdateMethodMultiParam[[Any], ReturnType], + *, + update_id: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + run_id: str | None = None, + first_execution_run_id: str | None = None, + ) -> TemporalOperationResult[ReturnType]: ... + + # Overload for single-param update + @overload + async def start_workflow_update( + self, + workflow_id: str, + update: temporalio.workflow.UpdateMethodMultiParam[ + [Any, ParamType], ReturnType + ], + arg: ParamType, + *, + update_id: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + run_id: str | None = None, + first_execution_run_id: str | None = None, + ) -> TemporalOperationResult[ReturnType]: ... + + # Overload for multi-param update + @overload + async def start_workflow_update( + self, + workflow_id: str, + update: temporalio.workflow.UpdateMethodMultiParam[MultiParamSpec, ReturnType], + *, + args: MultiParamSpec.args, # type: ignore + update_id: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + run_id: str | None = None, + first_execution_run_id: str | None = None, + ) -> TemporalOperationResult[ReturnType]: ... + + # Overload for string-name update + @overload + async def start_workflow_update( + self, + workflow_id: str, + update: str, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + update_id: str | None = None, + result_type: type[ReturnType] | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + run_id: str | None = None, + first_execution_run_id: str | None = None, + ) -> TemporalOperationResult[ReturnType]: ... + + @abstractmethod + async def start_workflow_update( + self, + workflow_id: str, + update: str | Callable, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + update_id: str | None = None, + result_type: type | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + run_id: str | None = None, + first_execution_run_id: str | None = None, + ) -> TemporalOperationResult[Any]: + """Start a Workflow Update-backed Nexus Operation. + + .. warning:: + This API is experimental and unstable. + """ + ... + + # async no-param activity + @overload + async def start_activity( + self, + activity: CallableAsyncNoParam[ReturnType], + *, + id: str, + task_queue: str | None = None, + schedule_to_start_timeout: timedelta | None = None, + schedule_to_close_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + start_delay: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> TemporalOperationResult[ReturnType]: ... + + # sync no-param activity + @overload + async def start_activity( + self, + activity: CallableSyncNoParam[ReturnType], + *, + id: str, + task_queue: str | None = None, + schedule_to_start_timeout: timedelta | None = None, + schedule_to_close_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + start_delay: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> TemporalOperationResult[ReturnType]: ... + + # async single-param activity + @overload + async def start_activity( + self, + activity: CallableAsyncSingleParam[ParamType, ReturnType], + arg: ParamType, + *, + id: str, + task_queue: str | None = None, + schedule_to_start_timeout: timedelta | None = None, + schedule_to_close_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + start_delay: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> TemporalOperationResult[ReturnType]: ... + + # sync single-param activity + @overload + async def start_activity( + self, + activity: CallableSyncSingleParam[ParamType, ReturnType], + arg: ParamType, + *, + id: str, + task_queue: str | None = None, + schedule_to_start_timeout: timedelta | None = None, + schedule_to_close_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + start_delay: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> TemporalOperationResult[ReturnType]: ... + + # async multi-param activity + @overload + async def start_activity( + self, + activity: Callable[..., Awaitable[ReturnType]], + *, + args: Sequence[Any], + id: str, + task_queue: str | None = None, + schedule_to_start_timeout: timedelta | None = None, + schedule_to_close_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + start_delay: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> TemporalOperationResult[ReturnType]: ... + + # sync multi-param activity + @overload + async def start_activity( + self, + activity: Callable[..., ReturnType], + *, + args: Sequence[Any], + id: str, + task_queue: str | None = None, + schedule_to_start_timeout: timedelta | None = None, + schedule_to_close_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + start_delay: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> TemporalOperationResult[ReturnType]: ... + + # string-name activity + @overload + async def start_activity( + self, + activity: str, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str | None = None, + result_type: type[ReturnType] | None = None, + schedule_to_start_timeout: timedelta | None = None, + schedule_to_close_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + start_delay: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> TemporalOperationResult[ReturnType]: ... + + @abstractmethod + async def start_activity( + self, + activity: ( + str | Callable[..., Awaitable[ReturnType]] | Callable[..., ReturnType] + ), + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str | None = None, + result_type: type | None = None, + schedule_to_start_timeout: timedelta | None = None, + schedule_to_close_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + start_delay: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> TemporalOperationResult[ReturnType]: + """Start a standalone activity that will deliver the Nexus operation result. + + If ``task_queue`` is not specified, the Nexus worker's task queue is used. + See :py:meth:`temporalio.client.Client.start_activity` for all other arguments. + """ + ... + + +class _TemporalNexusClient(TemporalNexusClient): # pyright: ignore[reportUnusedClass] + """Nexus-aware wrapper around a Temporal Client. + + .. warning:: + This API is experimental and unstable. + """ + + def __init__(self) -> None: + """Initialize the client wrapper from the active Nexus operation context.""" + self._temporal_context = _TemporalStartOperationContext.get() + self._started_async = False + + @property + def client(self) -> temporalio.client.Client: + """Return the Temporal client for the active Nexus operation.""" + return self._temporal_context.client + + @contextmanager + def _reserve_async_start(self) -> Iterator[None]: + if self._started_async: + raise HandlerError( + "Only one async operation can be started per operation handler invocation. Use TemporalNexusClient.client for additional workflow interactions", + type=HandlerErrorType.BAD_REQUEST, + ) + + # Reserve the started flag before sending to prevent concurrent starts + self._started_async = True + try: + yield + except BaseException: + self._started_async = False + raise + + async def start_workflow( + self, + workflow: str | Callable[..., Awaitable[ReturnType]], + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str | None = None, + result_type: type | None = None, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + request_eager_start: bool = False, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> TemporalOperationResult[ReturnType]: + """Start a workflow as the backing asynchronous Nexus operation.""" + with self._reserve_async_start(): + wf_handle = await _start_nexus_backing_workflow( + temporal_context=self._temporal_context, + workflow=workflow, + arg=arg, + args=args, + id=id, + task_queue=task_queue, + result_type=result_type, + execution_timeout=execution_timeout, + run_timeout=run_timeout, + task_timeout=task_timeout, + id_reuse_policy=id_reuse_policy, + id_conflict_policy=id_conflict_policy, + retry_policy=retry_policy, + cron_schedule=cron_schedule, + memo=memo, + search_attributes=search_attributes, + static_summary=static_summary, + static_details=static_details, + start_delay=start_delay, + start_signal=start_signal, + start_signal_args=start_signal_args, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + request_eager_start=request_eager_start, + priority=priority, + versioning_override=versioning_override, + ) + + return TemporalOperationResult.async_token(wf_handle.to_token()) + + async def start_workflow_update( + self, + workflow_id: str, + update: str | Callable, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + update_id: str | None = None, + result_type: type | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + run_id: str | None = None, + first_execution_run_id: str | None = None, + ) -> TemporalOperationResult[Any]: + """Start a Workflow Update-backed Nexus Operation.""" + if not self._temporal_context.nexus_context.callback_url: + raise HandlerError( + "callback URL is required for a workflow update Nexus operation", + type=HandlerErrorType.BAD_REQUEST, + ) + with self._reserve_async_start(): + update_handle = await _start_nexus_operation_workflow_update( + temporal_context=self._temporal_context, + workflow_id=workflow_id, + update=update, + arg=arg, + args=args, + update_id=update_id, + result_type=result_type, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + run_id=run_id, + first_execution_run_id=first_execution_run_id, + ) + # If the update has already completed, return the result synchronously + if update_handle._known_outcome is not None: + try: + result = await update_handle.result() + except temporalio.client.WorkflowUpdateFailedError as err: + raise OperationError( + str(err), state=OperationErrorState.FAILED + ) from err + return TemporalOperationResult.sync(result) + token = OperationToken( + type=OperationTokenType.UPDATE_WORKFLOW, + namespace=update_handle._client.namespace, + workflow_id=update_handle.workflow_id, + update_id=update_handle.id, + run_id=update_handle.workflow_run_id, + ).encode() + return TemporalOperationResult.async_token(token) + + async def start_activity( + self, + activity: ( + str | Callable[..., Awaitable[ReturnType]] | Callable[..., ReturnType] + ), + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str | None = None, + result_type: type | None = None, + schedule_to_start_timeout: timedelta | None = None, + schedule_to_close_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + start_delay: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> TemporalOperationResult[ReturnType]: + """Start a standalone activity that will deliver the Nexus operation result. + + If ``task_queue`` is not specified, the Nexus worker's task queue is used. + See :py:meth:`temporalio.client.Client.start_activity` for all other arguments. + """ + with self._reserve_async_start(): + # Here we are starting a "nexus-backing" standalone activity. The start request + # carries the Nexus completion callback so the activity result is delivered to + # the Nexus caller when the activity reaches a terminal state. + + with _nexus_backing_start_context(): + activity_handle: temporalio.client.ActivityHandle[ + ReturnType + ] = await self._temporal_context.client.start_activity( + activity=activity, # type: ignore + arg=arg, + args=args, + id=id, + task_queue=task_queue or self._temporal_context.info().task_queue, + result_type=result_type, + schedule_to_start_timeout=schedule_to_start_timeout, + schedule_to_close_timeout=schedule_to_close_timeout, + start_to_close_timeout=start_to_close_timeout, + heartbeat_timeout=heartbeat_timeout, + start_delay=start_delay, + id_reuse_policy=id_reuse_policy, + id_conflict_policy=id_conflict_policy, + retry_policy=retry_policy, + search_attributes=search_attributes, + summary=summary, + priority=priority, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + + activity_token = OperationToken( + type=OperationTokenType.ACTIVITY, + namespace=self._temporal_context.client.namespace, + activity_id=activity_handle.id, + run_id=activity_handle.run_id, + ) + + return TemporalOperationResult.async_token(activity_token.encode()) diff --git a/temporalio/nexus/_token.py b/temporalio/nexus/_token.py index 999e33767..a7c732f2a 100644 --- a/temporalio/nexus/_token.py +++ b/temporalio/nexus/_token.py @@ -3,24 +3,162 @@ import base64 import json from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Generic, Literal, Optional +from enum import IntEnum +from typing import TYPE_CHECKING, Any, Generic from nexusrpc import OutputT +from typing_extensions import Self + + +class OperationTokenType(IntEnum): + """Type discriminator for Nexus operation tokens.""" + + WORKFLOW = 1 + ACTIVITY = 2 + UPDATE_WORKFLOW = 3 -OperationTokenType = Literal[1] -OPERATION_TOKEN_TYPE_WORKFLOW: OperationTokenType = 1 if TYPE_CHECKING: import temporalio.client +@dataclass(frozen=True, kw_only=True) +class OperationToken: + """Serializable token identifying a Nexus operation target.""" + + version: int | None = None + type: OperationTokenType + namespace: str + workflow_id: str | None = None + activity_id: str | None = None + run_id: str | None = None + update_id: str | None = None + + def encode(self) -> str: + """Convert handle to a base64url-encoded token string.""" + token_details: dict[str, Any] = { + "t": self.type, + "ns": self.namespace, + } + if self.workflow_id is not None: + token_details["wid"] = self.workflow_id + if self.activity_id is not None: + token_details["aid"] = self.activity_id + if self.version is not None: + token_details["v"] = self.version + if self.run_id is not None: + token_details["rid"] = self.run_id + if self.update_id is not None: + token_details["uid"] = self.update_id + return _base64url_encode_no_padding( + json.dumps( + token_details, + separators=(",", ":"), + ).encode("utf-8") + ) + + @classmethod + def decode(cls, token: str) -> Self: + """Decodes and validates a token from its base64url-encoded string representation.""" + if not token: + raise TypeError("invalid token: token is empty") + try: + decoded_bytes = _base64url_decode_no_padding(token) + except Exception as err: + raise TypeError("failed to decode token as base64url") from err + try: + token_details = json.loads(decoded_bytes.decode("utf-8")) + except Exception as err: + raise TypeError("failed to unmarshal operation token") from err + + if not isinstance(token_details, dict): + raise TypeError(f"invalid token: expected dict, got {type(token_details)}") + + raw_token_type = token_details.get("t") + if not isinstance(raw_token_type, int): + raise TypeError( + f"invalid token: expected token type to be an int, got {type(raw_token_type)}" + ) + + try: + token_type = OperationTokenType(raw_token_type) + except ValueError as err: + raise TypeError( + f"invalid token: unknown token type, got {raw_token_type}.", + f"Valid values: {', '.join([f'{t.value} ({t.name})' for t in OperationTokenType])}", + ) from err + + version = token_details.get("v") + if version is not None and not isinstance(version, int): + raise TypeError( + f"invalid token: expected version to be an int or null, got {type(version)}" + ) + + workflow_id = token_details.get("wid") + if workflow_id is not None and not isinstance(workflow_id, str): + raise TypeError( + f"invalid token: expected workflow id to be a string, got {type(workflow_id)}" + ) + + if ( + token_type == OperationTokenType.WORKFLOW + or token_type == OperationTokenType.UPDATE_WORKFLOW + ): + if not workflow_id: + raise TypeError( + f"invalid token: expected non-empty workflow id for token type `{token_type.name}`" + ) + + activity_id = token_details.get("aid") + if activity_id is not None and not isinstance(activity_id, str): + raise TypeError( + f"invalid token: expected activity id to be a string, got {type(activity_id)}" + ) + + if token_type == OperationTokenType.ACTIVITY and not activity_id: + raise TypeError( + f"invalid token: expected non-empty activity id for token type `{token_type.name}`" + ) + + update_id = token_details.get("uid") + if not isinstance(update_id, str | None): + raise TypeError( + f"invalid token: expected update_id to be a string or None, got {type(update_id)}" + ) + + if token_type == OperationTokenType.UPDATE_WORKFLOW and not update_id: + raise TypeError( + "invalid token: expected non-empty update id for token type `UPDATE_WORKFLOW`" + ) + + namespace = token_details.get("ns") + if not isinstance(namespace, str): + # Allow empty string for ns, but it must be present and a string + raise TypeError( + f"invalid token: expected namespace to be a string, got {type(namespace)}" + ) + + run_id = token_details.get("rid") + if not isinstance(run_id, str | None): + raise TypeError( + f"invalid token: expected run_id to be a string or None, got {type(run_id)}" + ) + + return cls( + type=OperationTokenType(token_type), + namespace=namespace, + workflow_id=workflow_id, + activity_id=activity_id, + run_id=run_id, + version=version, + update_id=update_id, + ) + + @dataclass(frozen=True) class WorkflowHandle(Generic[OutputT]): """A handle to a workflow that is backing a Nexus operation. - .. warning:: - This API is experimental and unstable. - Do not instantiate this directly. Use :py:func:`temporalio.nexus.WorkflowRunOperationContext.start_workflow` to create a handle. @@ -30,12 +168,12 @@ class WorkflowHandle(Generic[OutputT]): workflow_id: str # Version of the token. Treated as v1 if missing. This field is not included in the # serialized token; it's only used to reject newer token versions on load. - version: Optional[int] = None + version: int | None = None def _to_client_workflow_handle( self, client: temporalio.client.Client, - result_type: Optional[type[OutputT]] = None, + result_type: type[OutputT] | None = None, ) -> temporalio.client.WorkflowHandle[Any, OutputT]: """Create a :py:class:`temporalio.client.WorkflowHandle` from the token.""" if client.namespace != self.namespace: @@ -45,8 +183,6 @@ def _to_client_workflow_handle( ) return client.get_workflow_handle(self.workflow_id, result_type=result_type) - # TODO(nexus-preview): The return type here should be dictated by the input workflow - # handle type. @classmethod def _unsafe_from_client_workflow_handle( cls, workflow_handle: temporalio.client.WorkflowHandle[Any, OutputT] @@ -64,65 +200,33 @@ def _unsafe_from_client_workflow_handle( def to_token(self) -> str: """Convert handle to a base64url-encoded token string.""" - return _base64url_encode_no_padding( - json.dumps( - { - "t": OPERATION_TOKEN_TYPE_WORKFLOW, - "ns": self.namespace, - "wid": self.workflow_id, - }, - separators=(",", ":"), - ).encode("utf-8") - ) + return OperationToken( + type=OperationTokenType.WORKFLOW, + namespace=self.namespace, + workflow_id=self.workflow_id, + ).encode() @classmethod def from_token(cls, token: str) -> WorkflowHandle[OutputT]: """Decodes and validates a token from its base64url-encoded string representation.""" - if not token: - raise TypeError("invalid workflow token: token is empty") - try: - decoded_bytes = _base64url_decode_no_padding(token) - except Exception as err: - raise TypeError("failed to decode token as base64url") from err - try: - workflow_operation_token = json.loads(decoded_bytes.decode("utf-8")) - except Exception as err: - raise TypeError("failed to unmarshal workflow operation token") from err - - if not isinstance(workflow_operation_token, dict): + op_token = OperationToken.decode(token) + if op_token.type != OperationTokenType.WORKFLOW: raise TypeError( - f"invalid workflow token: expected dict, got {type(workflow_operation_token)}" + f"invalid workflow token type: {op_token.type}, expected: {OperationTokenType.WORKFLOW}" ) - token_type = workflow_operation_token.get("t") - if token_type != OPERATION_TOKEN_TYPE_WORKFLOW: - raise TypeError( - f"invalid workflow token type: {token_type}, expected: {OPERATION_TOKEN_TYPE_WORKFLOW}" - ) + if not op_token.workflow_id: + raise TypeError("invalid workflow token: missing workflow id") - version = workflow_operation_token.get("v") - if version is not None and version != 0: + if op_token.version is not None and op_token.version != 0: raise TypeError( "invalid workflow token: 'v' field, if present, must be 0 or null/absent" ) - workflow_id = workflow_operation_token.get("wid") - if not workflow_id or not isinstance(workflow_id, str): - raise TypeError( - "invalid workflow token: missing, empty, or non-string workflow ID (wid)" - ) - - namespace = workflow_operation_token.get("ns") - if namespace is None or not isinstance(namespace, str): - # Allow empty string for ns, but it must be present and a string - raise TypeError( - "invalid workflow token: missing or non-string namespace (ns)" - ) - return cls( - namespace=namespace, - workflow_id=workflow_id, - version=version, + namespace=op_token.namespace, + workflow_id=op_token.workflow_id, + version=op_token.version, ) diff --git a/temporalio/nexus/_util.py b/temporalio/nexus/_util.py index ef005d0c4..f129dda2b 100644 --- a/temporalio/nexus/_util.py +++ b/temporalio/nexus/_util.py @@ -4,13 +4,9 @@ import inspect import typing import warnings +from collections.abc import Awaitable, Callable from typing import ( Any, - Awaitable, - Callable, - Optional, - Type, - TypeVar, ) import nexusrpc @@ -19,36 +15,91 @@ OutputT, ) -from temporalio.nexus._operation_context import WorkflowRunOperationContext +from temporalio.nexus._operation_context import ( + TemporalStartOperationContext, + WorkflowRunOperationContext, +) +from temporalio.nexus._temporal_client import ( + TemporalNexusClient, + TemporalOperationResult, +) +from temporalio.types import NexusServiceType from ._token import ( WorkflowHandle as WorkflowHandle, ) -ServiceHandlerT = TypeVar("ServiceHandlerT") - def get_workflow_run_start_method_input_and_output_type_annotations( start: Callable[ - [ServiceHandlerT, WorkflowRunOperationContext, InputT], + [NexusServiceType, WorkflowRunOperationContext, InputT], Awaitable[WorkflowHandle[OutputT]], ], ) -> tuple[ - Optional[Type[InputT]], - Optional[Type[OutputT]], + type[InputT] | None, + type[OutputT] | None, ]: """Return operation input and output types. - `start` must be a type-annotated start method that returns a + ``start`` must be a type-annotated start method that returns a :py:class:`temporalio.nexus.WorkflowHandle`. """ - input_type, output_type = _get_start_method_input_and_output_type_annotations(start) + return _get_wrapped_start_method_input_and_output_type_annotations( + start, + expected_param_types=(WorkflowRunOperationContext,), + expected_return_origin=WorkflowHandle, + ) + + +def get_temporal_operation_start_method_input_and_output_type_annotations( + start: Callable[ + [ + NexusServiceType, + TemporalStartOperationContext, + TemporalNexusClient, + InputT, + ], + Awaitable[TemporalOperationResult[OutputT]], + ], +) -> tuple[ + type[InputT] | None, + type[OutputT] | None, +]: + """Return operation input and output types. + + ``start`` must be a type-annotated start method that returns a + :py:class:`temporalio.nexus.TemporalOperationResult`. + """ + return _get_wrapped_start_method_input_and_output_type_annotations( + start, + expected_param_types=( + TemporalStartOperationContext, + TemporalNexusClient, + ), + expected_return_origin=TemporalOperationResult, + ) + + +def _get_wrapped_start_method_input_and_output_type_annotations( + start: Callable[..., Any], + *, + expected_param_types: tuple[type[Any], ...], + expected_return_origin: type[Any], +) -> tuple[ + type[Any] | None, + type[Any] | None, +]: + input_type, output_type = _get_start_method_input_and_output_type_annotations( + start, + expected_param_types=expected_param_types, + ) origin_type = typing.get_origin(output_type) if not origin_type: output_type = None - elif not issubclass(origin_type, WorkflowHandle): + elif not issubclass(origin_type, expected_return_origin): warnings.warn( - f"Expected return type of {start.__name__} to be a subclass of WorkflowHandle, " + f"Expected return type of {start.__name__} to be a subclass of " + f"{expected_return_origin.__name__}, " f"but is {output_type}" ) output_type = None @@ -68,13 +119,12 @@ def get_workflow_run_start_method_input_and_output_type_annotations( def _get_start_method_input_and_output_type_annotations( - start: Callable[ - [ServiceHandlerT, WorkflowRunOperationContext, InputT], - Awaitable[WorkflowHandle[OutputT]], - ], + start: Callable[..., Any], + *, + expected_param_types: tuple[type[Any], ...], ) -> tuple[ - Optional[Type[InputT]], - Optional[Type[OutputT]], + type[Any] | None, + type[Any] | None, ]: try: type_annotations = typing.get_type_hints(start) @@ -84,23 +134,28 @@ def _get_start_method_input_and_output_type_annotations( ) return None, None output_type = type_annotations.pop("return", None) + expected_parameter_count = len(expected_param_types) + 1 - if len(type_annotations) != 2: + if len(type_annotations) != expected_parameter_count: suffix = f": {type_annotations}" if type_annotations else "" warnings.warn( - f"Expected decorated start method {start} to have exactly 2 " - f"type-annotated parameters (ctx and input), but it has {len(type_annotations)}" + f"Expected decorated start method {start} to have exactly " + f"{expected_parameter_count} type-annotated parameters, " + f"but it has {len(type_annotations)}" f"{suffix}." ) input_type = None else: - ctx_type, input_type = type_annotations.values() - if not issubclass(ctx_type, WorkflowRunOperationContext): - warnings.warn( - f"Expected first parameter of {start} to be an instance of " - f"WorkflowRunOperationContext, but is {ctx_type}." - ) - input_type = None + *param_types, input_type = type_annotations.values() + for index, (param_type, expected_param_type) in enumerate( + zip(param_types, expected_param_types), start=1 + ): + if not issubclass(expected_param_type, param_type): + warnings.warn( + f"Expected parameter {index} of {start} to be an instance of " + f"{expected_param_type.__name__}, but is {param_type}." + ) + input_type = None return input_type, output_type @@ -122,19 +177,19 @@ def get_callable_name(fn: Callable[..., Any]) -> str: def get_operation_factory( obj: Any, ) -> tuple[ - Optional[Callable[[Any], Any]], - Optional[nexusrpc.Operation[Any, Any]], + Callable[[Any], Any] | None, + nexusrpc.Operation[Any, Any] | None, ]: - """Return the :py:class:`Operation` for the object along with the factory function. + """Return the :py:class:`nexusrpc.Operation` for the object along with the factory function. ``obj`` should be a decorated operation start method. """ - op_defn = nexusrpc.get_operation_definition(obj) + op_defn = nexusrpc.get_operation(obj) if op_defn: factory = obj else: if factory := getattr(obj, "__nexus_operation_factory__", None): - op_defn = nexusrpc.get_operation_definition(factory) + op_defn = nexusrpc.get_operation(factory) if not isinstance(op_defn, nexusrpc.Operation): return None, None return factory, op_defn @@ -145,7 +200,7 @@ def set_operation_factory( obj: Any, operation_factory: Callable[[Any], Any], ) -> None: - """Set the :py:class:`OperationHandler` factory for this object. + """Set the :py:class:`nexusrpc.handler.OperationHandler` factory for this object. ``obj`` should be an operation start method. """ @@ -158,7 +213,7 @@ def set_operation_factory( # # This file is licensed under the MIT License. def is_async_callable(obj: Any) -> bool: - """Return True if `obj` is an async callable. + """Return True if ``obj`` is an async callable. Supports partials of async callable class instances. """ diff --git a/temporalio/nexus/system/__init__.py b/temporalio/nexus/system/__init__.py new file mode 100644 index 000000000..b3d4d2d6f --- /dev/null +++ b/temporalio/nexus/system/__init__.py @@ -0,0 +1,151 @@ +"""System Nexus operation helpers. + +.. warning:: + This API is experimental and subject to change. +""" + +from __future__ import annotations + +import contextlib +import contextvars +from collections.abc import Iterator, Sequence +from typing import Any + +import temporalio.api.common.v1 +import temporalio.common +import temporalio.converter +from temporalio.bridge._visitor_functions import VisitorFunctions +from temporalio.converter import BinaryProtoPayloadConverter, CompositePayloadConverter +from temporalio.converter._payload_converter import ( + _TemporalTransferTypePayloadConverter, +) + +TEMPORAL_SYSTEM_ENDPOINT = "__temporal_system" +_user_payload_converter: contextvars.ContextVar[ + temporalio.converter.PayloadConverter | None +] = contextvars.ContextVar("temporal-system-nexus-user-payload-converter", default=None) +_SYSTEM_PAYLOAD_METADATA_KEY = "__temporal_system_payload" +_SYSTEM_PAYLOAD_METADATA_VALUE = b"true" + + +@contextlib.contextmanager +def _user_payload_converter_context( + payload_converter: temporalio.converter.PayloadConverter, +) -> Iterator[None]: + """Set the user payload converter for system Nexus model conversion.""" + token = _user_payload_converter.set(payload_converter) + try: + yield + finally: + _user_payload_converter.reset(token) + + +def _current_user_payload_converter() -> temporalio.converter.PayloadConverter: # pyright: ignore[reportUnusedFunction] + """Return the active user payload converter for system Nexus model conversion.""" + payload_converter = _user_payload_converter.get() + if payload_converter is None: + raise RuntimeError("System Nexus user payload converter context is not active") + return payload_converter + + +class _SystemNexusOuterPayloadConverter(CompositePayloadConverter): + """Payload converter for system Nexus outer proto envelopes.""" + + def __init__(self) -> None: + """Create a payload converter for system Nexus outer envelopes.""" + super().__init__(BinaryProtoPayloadConverter()) + + def to_payloads( + self, values: Sequence[Any] + ) -> list[temporalio.api.common.v1.Payload]: + """See base class.""" + payloads = super().to_payloads(values) + for value, payload in zip(values, payloads): + if isinstance(value, temporalio.common.RawValue): + continue + payload.metadata[_SYSTEM_PAYLOAD_METADATA_KEY] = ( + _SYSTEM_PAYLOAD_METADATA_VALUE + ) + return payloads + + +class _SystemNexusPayloadConverter(temporalio.converter.PayloadConverter): + """Payload converter for system Nexus outer envelopes.""" + + _user_payload_converter: temporalio.converter.PayloadConverter + _outer_payload_converter: temporalio.converter.PayloadConverter + + def __init__( + self, user_payload_converter: temporalio.converter.PayloadConverter + ) -> None: + """Create a payload converter for system Nexus outer envelopes.""" + self._user_payload_converter = user_payload_converter + self._outer_payload_converter = _TemporalTransferTypePayloadConverter.wrap( + _SystemNexusOuterPayloadConverter() + ) + + def to_payloads( + self, values: Sequence[Any] + ) -> list[temporalio.api.common.v1.Payload]: + """See base class.""" + with _user_payload_converter_context(self._user_payload_converter): + return self._outer_payload_converter.to_payloads(values) + + def from_payloads( + self, + payloads: Sequence[temporalio.api.common.v1.Payload], + type_hints: list[type] | None = None, + ) -> list[Any]: + """See base class.""" + with _user_payload_converter_context(self._user_payload_converter): + return self._outer_payload_converter.from_payloads(payloads, type_hints) + + +def is_system_endpoint(endpoint: str) -> bool: + """Return whether a Nexus endpoint is the Temporal system endpoint. + + .. warning:: + This API is experimental and subject to change. + """ + return endpoint == TEMPORAL_SYSTEM_ENDPOINT + + +def _is_system_payload(payload: temporalio.api.common.v1.Payload) -> bool: + return ( + payload.metadata.get(_SYSTEM_PAYLOAD_METADATA_KEY) + == _SYSTEM_PAYLOAD_METADATA_VALUE + ) + + +async def maybe_visit_payload( + payload: temporalio.api.common.v1.Payload, + visitor_functions: VisitorFunctions, + skip_search_attributes: bool, +) -> temporalio.api.common.v1.Payload | None: + """Visit nested payloads if the payload is a Temporal system Nexus envelope.""" + if not _is_system_payload(payload): + return None + + payload_converter = _SystemNexusOuterPayloadConverter() + value = payload_converter.from_payload(payload) + from temporalio.bridge._visitor import PayloadVisitor + + payload_visitor = PayloadVisitor(skip_search_attributes=skip_search_attributes) + checkpoint = visitor_functions.checkpoint() + await payload_visitor.visit(visitor_functions, value) + if checkpoint is not None: + await visitor_functions.drain_since(checkpoint) + return payload_converter.to_payload(value) + + +def _get_payload_converter( # pyright: ignore[reportUnusedFunction] + user_payload_converter: temporalio.converter.PayloadConverter, +) -> temporalio.converter.PayloadConverter: + """Return the fixed payload converter for system Nexus outer envelopes.""" + return _SystemNexusPayloadConverter(user_payload_converter) + + +__all__ = [ + "TEMPORAL_SYSTEM_ENDPOINT", + "is_system_endpoint", +] diff --git a/temporalio/nexus/system/workflow_service/__init__.py b/temporalio/nexus/system/workflow_service/__init__.py new file mode 100644 index 000000000..7c24fa125 --- /dev/null +++ b/temporalio/nexus/system/workflow_service/__init__.py @@ -0,0 +1,18 @@ +# Generated by nex-gen. DO NOT EDIT! + +from __future__ import annotations + +from . import service as _service +from .operations.signal_with_start_workflow import signal_with_start_workflow + +__all__ = [ + "signal_with_start_workflow", +] + + +__nexus_operation_registry__ = { + ( + "temporal.api.workflowservice.v1.WorkflowService", + "SignalWithStartWorkflowExecution", + ): _service.WorkflowService.signal_with_start_workflow, +} diff --git a/temporalio/nexus/system/workflow_service/_resources/__init__.py b/temporalio/nexus/system/workflow_service/_resources/__init__.py new file mode 100644 index 000000000..373efbd33 --- /dev/null +++ b/temporalio/nexus/system/workflow_service/_resources/__init__.py @@ -0,0 +1,5 @@ +# Generated by nex-gen. DO NOT EDIT! + +from __future__ import annotations + +__all__ = [] diff --git a/temporalio/nexus/system/workflow_service/_support/__init__.py b/temporalio/nexus/system/workflow_service/_support/__init__.py new file mode 100644 index 000000000..530c33e80 --- /dev/null +++ b/temporalio/nexus/system/workflow_service/_support/__init__.py @@ -0,0 +1,5 @@ +# Generated by nex-gen. DO NOT EDIT! + +from __future__ import annotations + +from .nex_gen_support import * # noqa: F401,F403 diff --git a/temporalio/nexus/system/workflow_service/_support/nex_gen_support.py b/temporalio/nexus/system/workflow_service/_support/nex_gen_support.py new file mode 100644 index 000000000..58b51e263 --- /dev/null +++ b/temporalio/nexus/system/workflow_service/_support/nex_gen_support.py @@ -0,0 +1,195 @@ +import collections.abc +import typing +from datetime import timedelta + +import google.protobuf.duration_pb2 + +import temporalio.api.common.v1.message_pb2 as common_pb2 +import temporalio.api.enums.v1.workflow_pb2 as workflow_enums_pb2 +import temporalio.api.taskqueue.v1.message_pb2 as taskqueue_pb2 +import temporalio.api.workflow.v1 +import temporalio.common +import temporalio.converter + + +def retry_policy_from_proto( + proto: common_pb2.RetryPolicy, +) -> temporalio.common.RetryPolicy: + return temporalio.common.RetryPolicy.from_proto(proto) + + +def retry_policy_to_proto( + retry_policy: temporalio.common.RetryPolicy, +) -> common_pb2.RetryPolicy: + proto = common_pb2.RetryPolicy() + retry_policy.apply_to_proto(proto) + return proto + + +def workflow_function_name( + value: str | collections.abc.Callable[..., collections.abc.Awaitable[object]], +) -> str: + from temporalio.workflow import _Definition # pyright: ignore[reportPrivateUsage] + + name, _result_type = _Definition.get_name_and_result_type(value) + return name + + +def signal_function_to_proto( + value: str | collections.abc.Callable[..., typing.Any], +) -> str: + from temporalio.workflow import ( + _SignalDefinition, # pyright: ignore[reportPrivateUsage] + ) + + return _SignalDefinition.must_name_from_fn_or_str(value) # pyright: ignore[reportUnknownMemberType] + + +def workflow_type_to_proto( + workflow_type: str + | collections.abc.Callable[..., collections.abc.Awaitable[object]], +) -> common_pb2.WorkflowType: + return common_pb2.WorkflowType(name=workflow_function_name(workflow_type)) + + +def task_queue_from_proto( + proto: taskqueue_pb2.TaskQueue, +) -> str: + return proto.name + + +def task_queue_to_proto( + task_queue: str, +) -> taskqueue_pb2.TaskQueue: + return taskqueue_pb2.TaskQueue(name=task_queue) + + +def workflow_namespace() -> str: + from temporalio.workflow import info + + return info().namespace + + +def payloads_to_proto( + values: collections.abc.Sequence[typing.Any], +) -> common_pb2.Payloads: + from temporalio.workflow import payload_converter + + return payload_converter().to_payloads_wrapper(values) + + +def _clone_payload(payload: common_pb2.Payload) -> common_pb2.Payload: + clone = common_pb2.Payload() + clone.CopyFrom(payload) + return clone + + +def _value_to_payload(value: object | common_pb2.Payload) -> common_pb2.Payload: + if isinstance(value, common_pb2.Payload): + return _clone_payload(value) + from temporalio.workflow import payload_converter + + payloads = payload_converter().to_payloads_wrapper([value]) + return _clone_payload(payloads.payloads[0]) + + +def _payload_to_value(payload: common_pb2.Payload) -> object: + wrapper = common_pb2.Payloads() + wrapper.payloads.add().CopyFrom(payload) + from temporalio.workflow import payload_converter + + return typing.cast( + object, + payload_converter().from_payloads_wrapper(wrapper)[0], + ) + + +def payload_from_proto( + proto: common_pb2.Payload, +) -> object: + return _payload_to_value(proto) + + +def payload_to_proto( + payload: object, +) -> common_pb2.Payload: + return _value_to_payload(payload) + + +def memo_from_proto( + proto: common_pb2.Memo, +) -> collections.abc.Mapping[str, object]: + return {key: _payload_to_value(value) for key, value in proto.fields.items()} + + +def memo_to_proto( + memo: collections.abc.Mapping[str, object], +) -> common_pb2.Memo: + message = common_pb2.Memo() + for key, value in memo.items(): + message.fields[key].CopyFrom(_value_to_payload(value)) + return message + + +def duration_from_proto(proto: google.protobuf.duration_pb2.Duration) -> timedelta: + return proto.ToTimedelta() + + +def duration_to_proto( + duration: timedelta, +) -> google.protobuf.duration_pb2.Duration: + proto = google.protobuf.duration_pb2.Duration() + proto.FromTimedelta(duration) + return proto + + +def workflow_id_reuse_policy_from_proto( + policy: workflow_enums_pb2.WorkflowIdReusePolicy.ValueType, +) -> temporalio.common.WorkflowIDReusePolicy: + return temporalio.common.WorkflowIDReusePolicy(int(policy)) + + +def workflow_id_reuse_policy_to_proto( + policy: temporalio.common.WorkflowIDReusePolicy, +) -> workflow_enums_pb2.WorkflowIdReusePolicy.ValueType: + return typing.cast(workflow_enums_pb2.WorkflowIdReusePolicy.ValueType, int(policy)) + + +def workflow_id_conflict_policy_from_proto( + policy: workflow_enums_pb2.WorkflowIdConflictPolicy.ValueType, +) -> temporalio.common.WorkflowIDConflictPolicy: + return temporalio.common.WorkflowIDConflictPolicy(int(policy)) + + +def workflow_id_conflict_policy_to_proto( + policy: temporalio.common.WorkflowIDConflictPolicy, +) -> workflow_enums_pb2.WorkflowIdConflictPolicy.ValueType: + return typing.cast( + workflow_enums_pb2.WorkflowIdConflictPolicy.ValueType, int(policy) + ) + + +def search_attributes_to_proto( + search_attributes: temporalio.common.TypedSearchAttributes, +) -> common_pb2.SearchAttributes: + proto = common_pb2.SearchAttributes() + temporalio.converter.encode_search_attributes(search_attributes, proto) + return proto + + +def priority_from_proto( + proto: common_pb2.Priority, +) -> temporalio.common.Priority: + return temporalio.common.Priority._from_proto(proto) # pyright: ignore[reportPrivateUsage] + + +def priority_to_proto( + priority: temporalio.common.Priority, +) -> common_pb2.Priority: + return priority._to_proto() # pyright: ignore[reportPrivateUsage] + + +def versioning_override_to_proto( + versioning_override: temporalio.common.VersioningOverride, +) -> temporalio.api.workflow.v1.VersioningOverride: + return versioning_override._to_proto() # pyright: ignore[reportPrivateUsage] diff --git a/temporalio/nexus/system/workflow_service/models.py b/temporalio/nexus/system/workflow_service/models.py new file mode 100644 index 000000000..05e1e3088 --- /dev/null +++ b/temporalio/nexus/system/workflow_service/models.py @@ -0,0 +1,141 @@ +# Generated by nex-gen. DO NOT EDIT! + +from __future__ import annotations + +import collections.abc +import dataclasses +import datetime +import typing + +import temporalio.api.sdk.v1.user_metadata_pb2 +import temporalio.api.workflowservice.v1.request_response_pb2 +import temporalio.common + +from ._support import ( + duration_to_proto, + memo_to_proto, + payload_from_proto, + payload_to_proto, + payloads_to_proto, + priority_to_proto, + retry_policy_to_proto, + search_attributes_to_proto, + signal_function_to_proto, + task_queue_to_proto, + versioning_override_to_proto, + workflow_id_conflict_policy_to_proto, + workflow_id_reuse_policy_to_proto, + workflow_namespace, + workflow_type_to_proto, +) + + +@dataclasses.dataclass(slots=True, kw_only=True) +class SignalWithStartWorkflowRequest: + """ + .. warning:: + This API is experimental and subject to change. + """ + + workflow: str | collections.abc.Callable[..., collections.abc.Awaitable[object]] + args: list[typing.Any] | None = None + id: str + task_queue: str + signal: str | collections.abc.Callable[..., None | collections.abc.Awaitable[None]] + signal_args: list[typing.Any] | None = None + execution_timeout: datetime.timedelta | None = None + run_timeout: datetime.timedelta | None = None + task_timeout: datetime.timedelta | None = None + request_id: str | None = None + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = ( + temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE + ) + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy | None = None + retry_policy: temporalio.common.RetryPolicy | None = None + cron_schedule: str | None = None + memo: collections.abc.Mapping[str, typing.Any] | None = None + search_attributes: temporalio.common.TypedSearchAttributes | None = None + priority: temporalio.common.Priority | None = None + versioning_override: temporalio.common.VersioningOverride | None = None + start_delay: datetime.timedelta | None = None + user_metadata: UserMetadata | None = None + + def to_proto( + self, + ) -> temporalio.api.workflowservice.v1.request_response_pb2.SignalWithStartWorkflowExecutionRequest: + message = temporalio.api.workflowservice.v1.request_response_pb2.SignalWithStartWorkflowExecutionRequest() + message.workflow_type.CopyFrom(workflow_type_to_proto(self.workflow)) + if self.args is not None: + message.input.CopyFrom(payloads_to_proto(self.args)) + message.workflow_id = self.id + message.task_queue.CopyFrom(task_queue_to_proto(self.task_queue)) + message.signal_name = signal_function_to_proto(self.signal) + if self.signal_args is not None: + message.signal_input.CopyFrom(payloads_to_proto(self.signal_args)) + if self.execution_timeout is not None: + message.workflow_execution_timeout.CopyFrom( + duration_to_proto(self.execution_timeout) + ) + if self.run_timeout is not None: + message.workflow_run_timeout.CopyFrom(duration_to_proto(self.run_timeout)) + if self.task_timeout is not None: + message.workflow_task_timeout.CopyFrom(duration_to_proto(self.task_timeout)) + if self.request_id is not None: + message.request_id = self.request_id + message.workflow_id_reuse_policy = workflow_id_reuse_policy_to_proto( + self.id_reuse_policy + ) + if self.id_conflict_policy is not None: + message.workflow_id_conflict_policy = workflow_id_conflict_policy_to_proto( + self.id_conflict_policy + ) + if self.retry_policy is not None: + message.retry_policy.CopyFrom(retry_policy_to_proto(self.retry_policy)) + if self.cron_schedule is not None: + message.cron_schedule = self.cron_schedule + if self.memo is not None: + message.memo.CopyFrom(memo_to_proto(self.memo)) + if self.search_attributes is not None: + message.search_attributes.CopyFrom( + search_attributes_to_proto(self.search_attributes) + ) + if self.priority is not None: + message.priority.CopyFrom(priority_to_proto(self.priority)) + if self.versioning_override is not None: + message.versioning_override.CopyFrom( + versioning_override_to_proto(self.versioning_override) + ) + if self.start_delay is not None: + message.workflow_start_delay.CopyFrom(duration_to_proto(self.start_delay)) + if self.user_metadata is not None: + message.user_metadata.CopyFrom(self.user_metadata.to_proto()) + message.namespace = workflow_namespace() + return message + + +@dataclasses.dataclass(slots=True) +class UserMetadata: + static_summary: typing.Any | None = None + static_details: typing.Any | None = None + + @classmethod + def from_proto( + cls, + proto: temporalio.api.sdk.v1.user_metadata_pb2.UserMetadata, + ) -> UserMetadata: + return cls( + static_summary=payload_from_proto(proto.summary) + if proto.HasField("summary") + else None, + static_details=payload_from_proto(proto.details) + if proto.HasField("details") + else None, + ) + + def to_proto(self) -> temporalio.api.sdk.v1.user_metadata_pb2.UserMetadata: + message = temporalio.api.sdk.v1.user_metadata_pb2.UserMetadata() + if self.static_summary is not None: + message.summary.CopyFrom(payload_to_proto(self.static_summary)) + if self.static_details is not None: + message.details.CopyFrom(payload_to_proto(self.static_details)) + return message diff --git a/temporalio/nexus/system/workflow_service/operations/__init__.py b/temporalio/nexus/system/workflow_service/operations/__init__.py new file mode 100644 index 000000000..67c9cc56b --- /dev/null +++ b/temporalio/nexus/system/workflow_service/operations/__init__.py @@ -0,0 +1,3 @@ +# Generated by nex-gen. DO NOT EDIT! + +from __future__ import annotations diff --git a/temporalio/nexus/system/workflow_service/operations/signal_with_start_workflow.py b/temporalio/nexus/system/workflow_service/operations/signal_with_start_workflow.py new file mode 100644 index 000000000..0865e5a88 --- /dev/null +++ b/temporalio/nexus/system/workflow_service/operations/signal_with_start_workflow.py @@ -0,0 +1,674 @@ +# Generated by nex-gen. DO NOT EDIT! + +from __future__ import annotations + +import collections.abc +import datetime +import typing + +import typing_extensions + +import temporalio.api.workflowservice.v1.request_response_pb2 +import temporalio.common + +if typing.TYPE_CHECKING: + from temporalio.workflow import ExternalWorkflowHandle + +from ..models import ( + SignalWithStartWorkflowRequest, + UserMetadata, +) + +SelfType = typing.TypeVar("SelfType") +SignalArg = typing.TypeVar("SignalArg") +WorkflowResult = typing.TypeVar("WorkflowResult") +WorkflowArgs = typing_extensions.TypeVarTuple("WorkflowArgs") + + +async def _signal_with_start_workflow( + request: SignalWithStartWorkflowRequest, +) -> ExternalWorkflowHandle[typing.Any]: + from temporalio.workflow import ( + create_nexus_client, + get_external_workflow_handle, + ) + + request_proto = request.to_proto() + nexus_client = create_nexus_client( + service="temporal.api.workflowservice.v1.WorkflowService", + endpoint="__temporal_system", + ) + handle = await nexus_client.start_operation( + operation="SignalWithStartWorkflowExecution", + input=request_proto, + output_type=temporalio.api.workflowservice.v1.request_response_pb2.SignalWithStartWorkflowExecutionResponse, + ) + result = await handle + return get_external_workflow_handle(request.id, run_id=result.run_id) + + +# Overload case: +# - workflow name with positional workflow arguments +# - signal name with optional list-form signal arguments +@typing.overload +async def signal_with_start_workflow( + workflow: str, + *args: object, + id: str, + task_queue: str, + signal: str, + signal_args: list[typing.Any] | None = ..., + execution_timeout: datetime.timedelta | None = ..., + run_timeout: datetime.timedelta | None = ..., + task_timeout: datetime.timedelta | None = ..., + request_id: str | None = ..., + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = ..., + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy | None = ..., + retry_policy: temporalio.common.RetryPolicy | None = ..., + cron_schedule: str | None = ..., + memo: collections.abc.Mapping[str, typing.Any] | None = ..., + search_attributes: temporalio.common.TypedSearchAttributes | None = ..., + priority: temporalio.common.Priority | None = ..., + versioning_override: temporalio.common.VersioningOverride | None = ..., + start_delay: datetime.timedelta | None = ..., + static_summary: str | None = ..., + static_details: str | None = ..., +) -> ExternalWorkflowHandle[object]: ... + + +# Overload case: +# - workflow name with optional list-form workflow arguments +# - signal name with optional list-form signal arguments +@typing.overload +async def signal_with_start_workflow( + workflow: str, + *, + args: list[typing.Any] | None = ..., + id: str, + task_queue: str, + signal: str, + signal_args: list[typing.Any] | None = ..., + execution_timeout: datetime.timedelta | None = ..., + run_timeout: datetime.timedelta | None = ..., + task_timeout: datetime.timedelta | None = ..., + request_id: str | None = ..., + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = ..., + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy | None = ..., + retry_policy: temporalio.common.RetryPolicy | None = ..., + cron_schedule: str | None = ..., + memo: collections.abc.Mapping[str, typing.Any] | None = ..., + search_attributes: temporalio.common.TypedSearchAttributes | None = ..., + priority: temporalio.common.Priority | None = ..., + versioning_override: temporalio.common.VersioningOverride | None = ..., + start_delay: datetime.timedelta | None = ..., + static_summary: str | None = ..., + static_details: str | None = ..., +) -> ExternalWorkflowHandle[object]: ... + + +# Overload case: +# - workflow method callable with typed positional workflow arguments +# - signal name with optional list-form signal arguments +@typing.overload +async def signal_with_start_workflow( + workflow: collections.abc.Callable[ + [SelfType, typing_extensions.Unpack[WorkflowArgs]], + collections.abc.Awaitable[WorkflowResult], + ], + *args: typing_extensions.Unpack[WorkflowArgs], + id: str, + task_queue: str, + signal: str, + signal_args: list[typing.Any] | None = ..., + execution_timeout: datetime.timedelta | None = ..., + run_timeout: datetime.timedelta | None = ..., + task_timeout: datetime.timedelta | None = ..., + request_id: str | None = ..., + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = ..., + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy | None = ..., + retry_policy: temporalio.common.RetryPolicy | None = ..., + cron_schedule: str | None = ..., + memo: collections.abc.Mapping[str, typing.Any] | None = ..., + search_attributes: temporalio.common.TypedSearchAttributes | None = ..., + priority: temporalio.common.Priority | None = ..., + versioning_override: temporalio.common.VersioningOverride | None = ..., + start_delay: datetime.timedelta | None = ..., + static_summary: str | None = ..., + static_details: str | None = ..., +) -> ExternalWorkflowHandle[SelfType]: ... + + +# Overload case: +# - workflow method callable with list-form workflow arguments +# - signal name with optional list-form signal arguments +@typing.overload +async def signal_with_start_workflow( + workflow: collections.abc.Callable[ + [SelfType, typing_extensions.Unpack[WorkflowArgs]], + collections.abc.Awaitable[WorkflowResult], + ], + *, + args: list[typing.Any], + id: str, + task_queue: str, + signal: str, + signal_args: list[typing.Any] | None = ..., + execution_timeout: datetime.timedelta | None = ..., + run_timeout: datetime.timedelta | None = ..., + task_timeout: datetime.timedelta | None = ..., + request_id: str | None = ..., + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = ..., + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy | None = ..., + retry_policy: temporalio.common.RetryPolicy | None = ..., + cron_schedule: str | None = ..., + memo: collections.abc.Mapping[str, typing.Any] | None = ..., + search_attributes: temporalio.common.TypedSearchAttributes | None = ..., + priority: temporalio.common.Priority | None = ..., + versioning_override: temporalio.common.VersioningOverride | None = ..., + start_delay: datetime.timedelta | None = ..., + static_summary: str | None = ..., + static_details: str | None = ..., +) -> ExternalWorkflowHandle[SelfType]: ... + + +# Overload case: +# - workflow name with positional workflow arguments +# - signal method callable with no signal arguments +@typing.overload +async def signal_with_start_workflow( + workflow: str, + *args: object, + id: str, + task_queue: str, + signal: collections.abc.Callable[ + [SelfType], None | collections.abc.Awaitable[None] + ], + execution_timeout: datetime.timedelta | None = ..., + run_timeout: datetime.timedelta | None = ..., + task_timeout: datetime.timedelta | None = ..., + request_id: str | None = ..., + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = ..., + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy | None = ..., + retry_policy: temporalio.common.RetryPolicy | None = ..., + cron_schedule: str | None = ..., + memo: collections.abc.Mapping[str, typing.Any] | None = ..., + search_attributes: temporalio.common.TypedSearchAttributes | None = ..., + priority: temporalio.common.Priority | None = ..., + versioning_override: temporalio.common.VersioningOverride | None = ..., + start_delay: datetime.timedelta | None = ..., + static_summary: str | None = ..., + static_details: str | None = ..., +) -> ExternalWorkflowHandle[SelfType]: ... + + +# Overload case: +# - workflow name with optional list-form workflow arguments +# - signal method callable with no signal arguments +@typing.overload +async def signal_with_start_workflow( + workflow: str, + *, + args: list[typing.Any] | None = ..., + id: str, + task_queue: str, + signal: collections.abc.Callable[ + [SelfType], None | collections.abc.Awaitable[None] + ], + execution_timeout: datetime.timedelta | None = ..., + run_timeout: datetime.timedelta | None = ..., + task_timeout: datetime.timedelta | None = ..., + request_id: str | None = ..., + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = ..., + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy | None = ..., + retry_policy: temporalio.common.RetryPolicy | None = ..., + cron_schedule: str | None = ..., + memo: collections.abc.Mapping[str, typing.Any] | None = ..., + search_attributes: temporalio.common.TypedSearchAttributes | None = ..., + priority: temporalio.common.Priority | None = ..., + versioning_override: temporalio.common.VersioningOverride | None = ..., + start_delay: datetime.timedelta | None = ..., + static_summary: str | None = ..., + static_details: str | None = ..., +) -> ExternalWorkflowHandle[SelfType]: ... + + +# Overload case: +# - workflow method callable with typed positional workflow arguments +# - signal method callable with no signal arguments +@typing.overload +async def signal_with_start_workflow( + workflow: collections.abc.Callable[ + [SelfType, typing_extensions.Unpack[WorkflowArgs]], + collections.abc.Awaitable[WorkflowResult], + ], + *args: typing_extensions.Unpack[WorkflowArgs], + id: str, + task_queue: str, + signal: collections.abc.Callable[ + [SelfType], None | collections.abc.Awaitable[None] + ], + execution_timeout: datetime.timedelta | None = ..., + run_timeout: datetime.timedelta | None = ..., + task_timeout: datetime.timedelta | None = ..., + request_id: str | None = ..., + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = ..., + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy | None = ..., + retry_policy: temporalio.common.RetryPolicy | None = ..., + cron_schedule: str | None = ..., + memo: collections.abc.Mapping[str, typing.Any] | None = ..., + search_attributes: temporalio.common.TypedSearchAttributes | None = ..., + priority: temporalio.common.Priority | None = ..., + versioning_override: temporalio.common.VersioningOverride | None = ..., + start_delay: datetime.timedelta | None = ..., + static_summary: str | None = ..., + static_details: str | None = ..., +) -> ExternalWorkflowHandle[SelfType]: ... + + +# Overload case: +# - workflow method callable with list-form workflow arguments +# - signal method callable with no signal arguments +@typing.overload +async def signal_with_start_workflow( + workflow: collections.abc.Callable[ + [SelfType, typing_extensions.Unpack[WorkflowArgs]], + collections.abc.Awaitable[WorkflowResult], + ], + *, + args: list[typing.Any], + id: str, + task_queue: str, + signal: collections.abc.Callable[ + [SelfType], None | collections.abc.Awaitable[None] + ], + execution_timeout: datetime.timedelta | None = ..., + run_timeout: datetime.timedelta | None = ..., + task_timeout: datetime.timedelta | None = ..., + request_id: str | None = ..., + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = ..., + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy | None = ..., + retry_policy: temporalio.common.RetryPolicy | None = ..., + cron_schedule: str | None = ..., + memo: collections.abc.Mapping[str, typing.Any] | None = ..., + search_attributes: temporalio.common.TypedSearchAttributes | None = ..., + priority: temporalio.common.Priority | None = ..., + versioning_override: temporalio.common.VersioningOverride | None = ..., + start_delay: datetime.timedelta | None = ..., + static_summary: str | None = ..., + static_details: str | None = ..., +) -> ExternalWorkflowHandle[SelfType]: ... + + +# Overload case: +# - workflow name with positional workflow arguments +# - signal method callable with a typed single signal arguments +@typing.overload +async def signal_with_start_workflow( + workflow: str, + *args: object, + id: str, + task_queue: str, + signal: collections.abc.Callable[ + [SelfType, SignalArg], None | collections.abc.Awaitable[None] + ], + signal_args: SignalArg, + execution_timeout: datetime.timedelta | None = ..., + run_timeout: datetime.timedelta | None = ..., + task_timeout: datetime.timedelta | None = ..., + request_id: str | None = ..., + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = ..., + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy | None = ..., + retry_policy: temporalio.common.RetryPolicy | None = ..., + cron_schedule: str | None = ..., + memo: collections.abc.Mapping[str, typing.Any] | None = ..., + search_attributes: temporalio.common.TypedSearchAttributes | None = ..., + priority: temporalio.common.Priority | None = ..., + versioning_override: temporalio.common.VersioningOverride | None = ..., + start_delay: datetime.timedelta | None = ..., + static_summary: str | None = ..., + static_details: str | None = ..., +) -> ExternalWorkflowHandle[SelfType]: ... + + +# Overload case: +# - workflow name with optional list-form workflow arguments +# - signal method callable with a typed single signal arguments +@typing.overload +async def signal_with_start_workflow( + workflow: str, + *, + args: list[typing.Any] | None = ..., + id: str, + task_queue: str, + signal: collections.abc.Callable[ + [SelfType, SignalArg], None | collections.abc.Awaitable[None] + ], + signal_args: SignalArg, + execution_timeout: datetime.timedelta | None = ..., + run_timeout: datetime.timedelta | None = ..., + task_timeout: datetime.timedelta | None = ..., + request_id: str | None = ..., + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = ..., + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy | None = ..., + retry_policy: temporalio.common.RetryPolicy | None = ..., + cron_schedule: str | None = ..., + memo: collections.abc.Mapping[str, typing.Any] | None = ..., + search_attributes: temporalio.common.TypedSearchAttributes | None = ..., + priority: temporalio.common.Priority | None = ..., + versioning_override: temporalio.common.VersioningOverride | None = ..., + start_delay: datetime.timedelta | None = ..., + static_summary: str | None = ..., + static_details: str | None = ..., +) -> ExternalWorkflowHandle[SelfType]: ... + + +# Overload case: +# - workflow method callable with typed positional workflow arguments +# - signal method callable with a typed single signal arguments +@typing.overload +async def signal_with_start_workflow( + workflow: collections.abc.Callable[ + [SelfType, typing_extensions.Unpack[WorkflowArgs]], + collections.abc.Awaitable[WorkflowResult], + ], + *args: typing_extensions.Unpack[WorkflowArgs], + id: str, + task_queue: str, + signal: collections.abc.Callable[ + [SelfType, SignalArg], None | collections.abc.Awaitable[None] + ], + signal_args: SignalArg, + execution_timeout: datetime.timedelta | None = ..., + run_timeout: datetime.timedelta | None = ..., + task_timeout: datetime.timedelta | None = ..., + request_id: str | None = ..., + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = ..., + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy | None = ..., + retry_policy: temporalio.common.RetryPolicy | None = ..., + cron_schedule: str | None = ..., + memo: collections.abc.Mapping[str, typing.Any] | None = ..., + search_attributes: temporalio.common.TypedSearchAttributes | None = ..., + priority: temporalio.common.Priority | None = ..., + versioning_override: temporalio.common.VersioningOverride | None = ..., + start_delay: datetime.timedelta | None = ..., + static_summary: str | None = ..., + static_details: str | None = ..., +) -> ExternalWorkflowHandle[SelfType]: ... + + +# Overload case: +# - workflow method callable with list-form workflow arguments +# - signal method callable with a typed single signal arguments +@typing.overload +async def signal_with_start_workflow( + workflow: collections.abc.Callable[ + [SelfType, typing_extensions.Unpack[WorkflowArgs]], + collections.abc.Awaitable[WorkflowResult], + ], + *, + args: list[typing.Any], + id: str, + task_queue: str, + signal: collections.abc.Callable[ + [SelfType, SignalArg], None | collections.abc.Awaitable[None] + ], + signal_args: SignalArg, + execution_timeout: datetime.timedelta | None = ..., + run_timeout: datetime.timedelta | None = ..., + task_timeout: datetime.timedelta | None = ..., + request_id: str | None = ..., + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = ..., + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy | None = ..., + retry_policy: temporalio.common.RetryPolicy | None = ..., + cron_schedule: str | None = ..., + memo: collections.abc.Mapping[str, typing.Any] | None = ..., + search_attributes: temporalio.common.TypedSearchAttributes | None = ..., + priority: temporalio.common.Priority | None = ..., + versioning_override: temporalio.common.VersioningOverride | None = ..., + start_delay: datetime.timedelta | None = ..., + static_summary: str | None = ..., + static_details: str | None = ..., +) -> ExternalWorkflowHandle[SelfType]: ... + + +# Overload case: +# - workflow name with positional workflow arguments +# - signal callable with list-form signal arguments +@typing.overload +async def signal_with_start_workflow( + workflow: str, + *args: object, + id: str, + task_queue: str, + signal: collections.abc.Callable[..., None | collections.abc.Awaitable[None]], + signal_args: list[typing.Any], + execution_timeout: datetime.timedelta | None = ..., + run_timeout: datetime.timedelta | None = ..., + task_timeout: datetime.timedelta | None = ..., + request_id: str | None = ..., + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = ..., + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy | None = ..., + retry_policy: temporalio.common.RetryPolicy | None = ..., + cron_schedule: str | None = ..., + memo: collections.abc.Mapping[str, typing.Any] | None = ..., + search_attributes: temporalio.common.TypedSearchAttributes | None = ..., + priority: temporalio.common.Priority | None = ..., + versioning_override: temporalio.common.VersioningOverride | None = ..., + start_delay: datetime.timedelta | None = ..., + static_summary: str | None = ..., + static_details: str | None = ..., +) -> ExternalWorkflowHandle[object]: ... + + +# Overload case: +# - workflow name with optional list-form workflow arguments +# - signal callable with list-form signal arguments +@typing.overload +async def signal_with_start_workflow( + workflow: str, + *, + args: list[typing.Any] | None = ..., + id: str, + task_queue: str, + signal: collections.abc.Callable[..., None | collections.abc.Awaitable[None]], + signal_args: list[typing.Any], + execution_timeout: datetime.timedelta | None = ..., + run_timeout: datetime.timedelta | None = ..., + task_timeout: datetime.timedelta | None = ..., + request_id: str | None = ..., + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = ..., + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy | None = ..., + retry_policy: temporalio.common.RetryPolicy | None = ..., + cron_schedule: str | None = ..., + memo: collections.abc.Mapping[str, typing.Any] | None = ..., + search_attributes: temporalio.common.TypedSearchAttributes | None = ..., + priority: temporalio.common.Priority | None = ..., + versioning_override: temporalio.common.VersioningOverride | None = ..., + start_delay: datetime.timedelta | None = ..., + static_summary: str | None = ..., + static_details: str | None = ..., +) -> ExternalWorkflowHandle[object]: ... + + +# Overload case: +# - workflow method callable with typed positional workflow arguments +# - signal callable with list-form signal arguments +@typing.overload +async def signal_with_start_workflow( + workflow: collections.abc.Callable[ + [SelfType, typing_extensions.Unpack[WorkflowArgs]], + collections.abc.Awaitable[WorkflowResult], + ], + *args: typing_extensions.Unpack[WorkflowArgs], + id: str, + task_queue: str, + signal: collections.abc.Callable[..., None | collections.abc.Awaitable[None]], + signal_args: list[typing.Any], + execution_timeout: datetime.timedelta | None = ..., + run_timeout: datetime.timedelta | None = ..., + task_timeout: datetime.timedelta | None = ..., + request_id: str | None = ..., + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = ..., + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy | None = ..., + retry_policy: temporalio.common.RetryPolicy | None = ..., + cron_schedule: str | None = ..., + memo: collections.abc.Mapping[str, typing.Any] | None = ..., + search_attributes: temporalio.common.TypedSearchAttributes | None = ..., + priority: temporalio.common.Priority | None = ..., + versioning_override: temporalio.common.VersioningOverride | None = ..., + start_delay: datetime.timedelta | None = ..., + static_summary: str | None = ..., + static_details: str | None = ..., +) -> ExternalWorkflowHandle[SelfType]: ... + + +# Overload case: +# - workflow method callable with list-form workflow arguments +# - signal callable with list-form signal arguments +@typing.overload +async def signal_with_start_workflow( + workflow: collections.abc.Callable[ + [SelfType, typing_extensions.Unpack[WorkflowArgs]], + collections.abc.Awaitable[WorkflowResult], + ], + *, + args: list[typing.Any], + id: str, + task_queue: str, + signal: collections.abc.Callable[..., None | collections.abc.Awaitable[None]], + signal_args: list[typing.Any], + execution_timeout: datetime.timedelta | None = ..., + run_timeout: datetime.timedelta | None = ..., + task_timeout: datetime.timedelta | None = ..., + request_id: str | None = ..., + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = ..., + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy | None = ..., + retry_policy: temporalio.common.RetryPolicy | None = ..., + cron_schedule: str | None = ..., + memo: collections.abc.Mapping[str, typing.Any] | None = ..., + search_attributes: temporalio.common.TypedSearchAttributes | None = ..., + priority: temporalio.common.Priority | None = ..., + versioning_override: temporalio.common.VersioningOverride | None = ..., + start_delay: datetime.timedelta | None = ..., + static_summary: str | None = ..., + static_details: str | None = ..., +) -> ExternalWorkflowHandle[SelfType]: ... + + +async def signal_with_start_workflow( + workflow: str | collections.abc.Callable[..., collections.abc.Awaitable[object]], + *positional_args: object, + args: list[typing.Any] | None = None, + id: str, + task_queue: str, + signal: str | collections.abc.Callable[..., None | collections.abc.Awaitable[None]], + signal_args: object | list[typing.Any] | None = None, + execution_timeout: datetime.timedelta | None = None, + run_timeout: datetime.timedelta | None = None, + task_timeout: datetime.timedelta | None = None, + request_id: str | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = ( + temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE + ), + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str | None = None, + memo: collections.abc.Mapping[str, typing.Any] | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + priority: temporalio.common.Priority | None = None, + versioning_override: temporalio.common.VersioningOverride | None = None, + start_delay: datetime.timedelta | None = None, + static_summary: str | None = None, + static_details: str | None = None, +) -> ExternalWorkflowHandle[typing.Any]: + """Signal a workflow, starting it first if needed. + + .. warning:: + This API is experimental and subject to change. + + Args: + workflow: Workflow type name or callable identifying the workflow to start. + positional_args: Positional arguments for workflow. Cannot be set if args is + set. + args: List-form arguments for workflow. Cannot be set if positional_args are + set. For typed workflow callables, list contents are not statically + typechecked; pass workflow arguments positionally for precise typechecking. + id: Unique identifier for the workflow execution. + task_queue: Task queue to run the workflow on. + signal: Signal name or callable to send with the start request. + signal_args: Argument value, or list of argument values, for signal. For typed + single-argument signals, scalar signal_args values are statically + typechecked. List-form signal_args values are not precisely typechecked. To + pass a single signal argument that is itself a list, wrap it in another + list; otherwise the list is interpreted as multiple signal arguments. + execution_timeout: Total workflow execution timeout, including retries and + continue-as-new. + run_timeout: Timeout of a single workflow run. + task_timeout: Timeout of a single workflow task. + request_id: Request ID used to deduplicate workflow start requests. + id_reuse_policy: Behavior when a closed workflow with the same ID exists. + Default is allow-duplicate. + id_conflict_policy: Behavior when a workflow is currently running with the same + ID. Set to use-existing for idempotent deduplication on workflow ID. Cannot + be set if id-reuse-policy is terminate-if-running. + retry_policy: Retry policy for the workflow. + cron_schedule: Cron schedule for recurring workflow executions. See + https://docs.temporal.io/cron-job. + memo: Memo for the workflow. + search_attributes: Typed search attributes for the workflow. + priority: Priority of the workflow execution. + versioning_override: Override for workflow versioning behavior. + start_delay: Amount of time to wait before starting the workflow. This does not + work with cron-schedule. + static_summary: Single-line fixed summary for the workflow execution that may + appear in UI and CLI. This can be in single-line Temporal Markdown format. + static_details: General fixed details for the workflow execution that may appear + in UI and CLI. This can be in Temporal Markdown format and can span multiple + lines. This value is fixed on the workflow execution and cannot be updated. + + Returns: + A workflow handle to the started workflow. + """ + normalized_signal_args: list[typing.Any] | None + if signal_args is None: + normalized_signal_args = None + elif isinstance(signal_args, list): + normalized_signal_args = typing.cast(list[typing.Any], signal_args) + else: + normalized_signal_args = [signal_args] + if positional_args and args is not None: + raise TypeError("cannot specify both positional arguments and args") + normalized_args: list[typing.Any] | None = ( + list(positional_args) if positional_args else args + ) + user_metadata = ( + None + if static_summary is None and static_details is None + else UserMetadata( + static_summary=static_summary, + static_details=static_details, + ) + ) + request = SignalWithStartWorkflowRequest( + workflow=workflow, + args=normalized_args, + id=id, + task_queue=task_queue, + signal=signal, + signal_args=normalized_signal_args, + execution_timeout=execution_timeout, + run_timeout=run_timeout, + task_timeout=task_timeout, + request_id=request_id, + id_reuse_policy=id_reuse_policy, + id_conflict_policy=id_conflict_policy, + retry_policy=retry_policy, + cron_schedule=cron_schedule, + memo=memo, + search_attributes=search_attributes, + priority=priority, + versioning_override=versioning_override, + start_delay=start_delay, + user_metadata=user_metadata, + ) + return await _signal_with_start_workflow(request) diff --git a/temporalio/nexus/system/workflow_service/service.py b/temporalio/nexus/system/workflow_service/service.py new file mode 100644 index 000000000..7ce5849ca --- /dev/null +++ b/temporalio/nexus/system/workflow_service/service.py @@ -0,0 +1,21 @@ +# Generated by nex-gen. DO NOT EDIT! + +from __future__ import annotations + +from nexusrpc import Operation, service + +import temporalio.api.workflowservice.v1.request_response_pb2 + + +@service(name="temporal.api.workflowservice.v1.WorkflowService") +class WorkflowService: + """ + .. warning:: + This API is experimental and subject to change. + """ + + # .. warning:: This API is experimental and subject to change. + signal_with_start_workflow: Operation[ + temporalio.api.workflowservice.v1.request_response_pb2.SignalWithStartWorkflowExecutionRequest, + temporalio.api.workflowservice.v1.request_response_pb2.SignalWithStartWorkflowExecutionResponse, + ] = Operation(name="SignalWithStartWorkflowExecution") diff --git a/temporalio/plugin.py b/temporalio/plugin.py new file mode 100644 index 000000000..79d372cfa --- /dev/null +++ b/temporalio/plugin.py @@ -0,0 +1,280 @@ +"""Plugin module for Temporal SDK. + +This module provides plugin functionality that allows customization of both client +and worker behavior in the Temporal SDK through configurable parameters. +""" + +from collections.abc import AsyncIterator, Awaitable, Callable, Sequence +from contextlib import AbstractAsyncContextManager, asynccontextmanager +from typing import ( + Any, + TypeAlias, + TypeVar, + cast, +) + +import temporalio.client +import temporalio.converter +import temporalio.worker +from temporalio.client import ClientConfig, WorkflowHistory +from temporalio.service import ConnectConfig, ServiceClient +from temporalio.worker import ( + Replayer, + ReplayerConfig, + Worker, + WorkerConfig, + WorkflowReplayResult, + WorkflowRunner, +) + +T = TypeVar("T") + +PluginParameter: TypeAlias = None | T | Callable[[T | None], T] + + +class SimplePlugin(temporalio.client.Plugin, temporalio.worker.Plugin): + """A simple plugin definition which has a limited set of configurations but makes it easier to produce + a plugin which needs to configure them. + """ + + def __init__( + self, + name: str, + *, + data_converter: PluginParameter[temporalio.converter.DataConverter] = None, + interceptors: Sequence[ + temporalio.client.Interceptor | temporalio.worker.Interceptor + ] + | None = None, + activities: PluginParameter[Sequence[Callable]] = None, + nexus_service_handlers: PluginParameter[Sequence[Any]] = None, + workflows: PluginParameter[Sequence[type]] = None, + workflow_runner: PluginParameter[WorkflowRunner] = None, + workflow_failure_exception_types: PluginParameter[ + Sequence[type[BaseException]] + ] = None, + run_context: Callable[[], AbstractAsyncContextManager[None]] | None = None, + ) -> None: + """Create a simple plugin with configurable parameters. Each of the parameters will be applied to any + component for which they are applicable. All arguments are optional, and all but run_context can also + be callables for more complex modification. See the type PluginParameter above. + For details on each argument, see below. + + Args: + name: The name of the plugin. + data_converter: Data converter for serialization, or callable to customize existing one. + Applied to the Client and Replayer. + interceptors: Interceptors to append. + Client interceptors are applied to the Client, worker interceptors are applied + to the Worker and Replayer. Interceptors that implement both interfaces will + be applied to both, with exactly one instance used per worker to avoid duplication. + activities: Activity functions to append, or callable to customize existing ones. + Applied to the Worker. + nexus_service_handlers: Nexus service handlers to append, or callable to customize existing ones. + Applied to the Worker. + workflows: Workflow classes to append, or callable to customize existing ones. + Applied to the Worker and Replayer. + workflow_runner: Workflow runner, or callable to customize existing one. + Applied to the Worker and Replayer. + workflow_failure_exception_types: Exception types for workflow failures to append, + or callable to customize existing ones. Applied to the Worker and Replayer. + run_context: A place to run custom code to wrap around the Worker (or Replayer) execution. + Specifically, it's an async context manager producer. Applied to the Worker and Replayer. + + Returns: + A configured Plugin instance. + """ + self._name = name + self.data_converter = data_converter + self.interceptors = interceptors + self.activities = activities + self.nexus_service_handlers = nexus_service_handlers + self.workflows = workflows + self.workflow_runner = workflow_runner + self.workflow_failure_exception_types = workflow_failure_exception_types + self.run_context = run_context + + def name(self) -> str: + """See base class.""" + return self._name + + def configure_client(self, config: ClientConfig) -> ClientConfig: + """See base class.""" + data_converter = _resolve_parameter( + config.get("data_converter"), self.data_converter + ) + if data_converter: + config["data_converter"] = data_converter + + # Resolve the combined interceptors first, then filter to client ones + all_interceptors = _resolve_append_parameter( + cast( + Sequence[temporalio.client.Interceptor | temporalio.worker.Interceptor] + | None, + config.get("interceptors"), + ), + self.interceptors, + ) + if all_interceptors is not None: + client_interceptors = [ + interceptor + for interceptor in all_interceptors + if isinstance(interceptor, temporalio.client.Interceptor) + ] + config["interceptors"] = client_interceptors + + return config + + async def connect_service_client( + self, + config: ConnectConfig, + next: Callable[[ConnectConfig], Awaitable[ServiceClient]], + ) -> temporalio.service.ServiceClient: + """See base class.""" + return await next(config) + + def configure_worker(self, config: WorkerConfig) -> WorkerConfig: + """See base class.""" + activities = _resolve_append_parameter( + config.get("activities"), self.activities + ) + if activities: + config["activities"] = activities + + nexus_service_handlers = _resolve_append_parameter( + config.get("nexus_service_handlers"), self.nexus_service_handlers + ) + if nexus_service_handlers is not None: + config["nexus_service_handlers"] = nexus_service_handlers + + workflows = _resolve_append_parameter(config.get("workflows"), self.workflows) + if workflows is not None: + config["workflows"] = workflows + + workflow_runner = _resolve_parameter( + config.get("workflow_runner"), self.workflow_runner + ) + if workflow_runner: + config["workflow_runner"] = workflow_runner + + if self.interceptors is not None: + client_interceptors_list = ( + config["client"].config(active_config=True).get("interceptors", []) # type:ignore[reportTypedDictNotRequiredAccess] + ) + + # Exclude any already registered interceptors and client only interceptors + worker_interceptors = [ + interceptor + for interceptor in self.interceptors + if isinstance(interceptor, temporalio.worker.Interceptor) + and interceptor not in client_interceptors_list + ] + + provided_interceptors = _resolve_append_parameter( + config.get("interceptors"), worker_interceptors + ) + if provided_interceptors is not None: + config["interceptors"] = provided_interceptors + + failure_exception_types = _resolve_append_parameter( + config.get("workflow_failure_exception_types"), + self.workflow_failure_exception_types, + ) + if failure_exception_types is not None: + config["workflow_failure_exception_types"] = failure_exception_types + + return config + + def configure_replayer(self, config: ReplayerConfig) -> ReplayerConfig: + """See base class.""" + data_converter = _resolve_parameter( + config.get("data_converter"), self.data_converter + ) + if data_converter: + config["data_converter"] = data_converter + + workflows = _resolve_append_parameter(config.get("workflows"), self.workflows) + if workflows is not None: + config["workflows"] = workflows + + workflow_runner = _resolve_parameter( + config.get("workflow_runner"), self.workflow_runner + ) + if workflow_runner: + config["workflow_runner"] = workflow_runner + + all_interceptors = _resolve_append_parameter( + cast( + Sequence[temporalio.client.Interceptor | temporalio.worker.Interceptor] + | None, + config.get("interceptors"), + ), + self.interceptors, + ) + if all_interceptors is not None: + worker_interceptors = [ + interceptor + for interceptor in all_interceptors + if isinstance(interceptor, temporalio.worker.Interceptor) + ] + config["interceptors"] = worker_interceptors + + failure_exception_types = _resolve_append_parameter( + config.get("workflow_failure_exception_types"), + self.workflow_failure_exception_types, + ) + if failure_exception_types is not None: + config["workflow_failure_exception_types"] = failure_exception_types + + return config + + async def run_worker( + self, worker: Worker, next: Callable[[Worker], Awaitable[None]] + ) -> None: + """See base class.""" + if self.run_context: + async with self.run_context(): + await next(worker) + else: + await next(worker) + + @asynccontextmanager + async def run_replayer( + self, + replayer: Replayer, + histories: AsyncIterator[WorkflowHistory], + next: Callable[ + [Replayer, AsyncIterator[WorkflowHistory]], + AbstractAsyncContextManager[AsyncIterator[WorkflowReplayResult]], + ], + ) -> AsyncIterator[AsyncIterator[WorkflowReplayResult]]: + """See base class.""" + if self.run_context: + async with self.run_context(): + async with next(replayer, histories) as results: + yield results + else: + async with next(replayer, histories) as results: + yield results + + +def _resolve_parameter(existing: T | None, parameter: PluginParameter[T]) -> T | None: + if parameter is None: + return existing + elif callable(parameter): + return cast(Callable[[T | None], T | None], parameter)(existing) + else: + return parameter + + +def _resolve_append_parameter( + existing: Sequence[T] | None, parameter: PluginParameter[Sequence[T]] +) -> Sequence[T] | None: + if parameter is None: + return existing + elif callable(parameter): + return cast(Callable[[Sequence[T] | None], Sequence[T] | None], parameter)( + existing + ) + else: + return list(existing or []) + list(parameter) diff --git a/temporalio/runtime.py b/temporalio/runtime.py index 84b683941..fc526ca2b 100644 --- a/temporalio/runtime.py +++ b/temporalio/runtime.py @@ -4,18 +4,15 @@ import logging import time +from collections.abc import Mapping, Sequence from dataclasses import dataclass, field from datetime import timedelta from enum import Enum from typing import ( ClassVar, Generic, - Mapping, NewType, - Optional, - Sequence, TypeVar, - Union, ) from typing_extensions import Protocol, Self @@ -24,22 +21,60 @@ import temporalio.bridge.runtime import temporalio.common -_default_runtime: Optional[Runtime] = None + +class _RuntimeRef: + def __init__( + self, + ) -> None: + self._default_runtime: Runtime | None = None + self._prevent_default = False + + def default(self) -> Runtime: + if not self._default_runtime: + if self._prevent_default: + raise RuntimeError( + "Cannot create default Runtime after Runtime.prevent_default has been called" + ) + self._default_runtime = Runtime(telemetry=TelemetryConfig()) + return self._default_runtime + + def prevent_default(self): + if self._default_runtime: + raise RuntimeError( + "Runtime.prevent_default called after default runtime has been created or set" + ) + self._prevent_default = True + + def set_default( + self, runtime: Runtime, *, error_if_already_set: bool = True + ) -> None: + if self._default_runtime and error_if_already_set: + raise RuntimeError("Runtime default already set") + + self._default_runtime = runtime + + +_runtime_ref: _RuntimeRef = _RuntimeRef() class Runtime: """Runtime for Temporal Python SDK. - Users are encouraged to use :py:meth:`default`. It can be set with + Most users are encouraged to use :py:meth:`default`. It can be set with :py:meth:`set_default`. Every time a new runtime is created, a new internal thread pool is created. - Runtimes do not work across forks. + Runtimes do not work across forks. Advanced users should consider using + :py:meth:`prevent_default` and :py:meth:`set_default` to ensure each + fork creates it's own runtime. + """ - @staticmethod - def default() -> Runtime: - """Get the default runtime, creating if not already created. + @classmethod + def default(cls) -> Runtime: + """Get the default runtime, creating if not already created. If :py:meth:`prevent_default` + is called before this method it will raise a RuntimeError instead of creating a default + runtime. If the default runtime needs to be different, it should be done with :py:meth:`set_default` before this is called or ever used. @@ -47,10 +82,20 @@ def default() -> Runtime: Returns: The default runtime. """ - global _default_runtime - if not _default_runtime: - _default_runtime = Runtime(telemetry=TelemetryConfig()) - return _default_runtime + global _runtime_ref + return _runtime_ref.default() + + @classmethod + def prevent_default(cls): + """Prevent :py:meth:`default` from lazily creating a :py:class:`Runtime`. + + Raises a RuntimeError if a default :py:class:`Runtime` has already been created. + + Explicitly setting a default runtime with :py:meth:`set_default` bypasses this setting and + future calls to :py:meth:`default` will return the provided runtime. + """ + global _runtime_ref + _runtime_ref.prevent_default() @staticmethod def set_default(runtime: Runtime, *, error_if_already_set: bool = True) -> None: @@ -65,19 +110,45 @@ def set_default(runtime: Runtime, *, error_if_already_set: bool = True) -> None: error_if_already_set: If True and default is already set, this will raise a RuntimeError. """ - global _default_runtime - if _default_runtime and error_if_already_set: - raise RuntimeError("Runtime default already set") - _default_runtime = runtime + global _runtime_ref + _runtime_ref.set_default(runtime, error_if_already_set=error_if_already_set) - def __init__(self, *, telemetry: TelemetryConfig) -> None: - """Create a default runtime with the given telemetry config. + def __init__( + self, + *, + telemetry: TelemetryConfig, + worker_heartbeat_interval: timedelta | None = timedelta(seconds=60), + disable_environment_info: bool = False, + ) -> None: + """Create a runtime with the provided configuration. Each new runtime creates a new internal thread pool, so use sparingly. + + Args: + telemetry: Telemetry configuration when not supplying + ``runtime_options``. + worker_heartbeat_interval: Interval for worker heartbeats. ``None`` + disables heartbeating. Interval must be between 1s and 60s. + disable_environment_info: Whether to omit runtime, hosting, and + platform information from worker heartbeats. + + Raises: + ValueError: If both ```runtime_options`` is a negative value. """ - self._core_runtime = temporalio.bridge.runtime.Runtime( - telemetry=telemetry._to_bridge_config() + if worker_heartbeat_interval is None: + heartbeat_millis = None + else: + if worker_heartbeat_interval <= timedelta(0): + raise ValueError("worker_heartbeat_interval must be positive") + heartbeat_millis = int(worker_heartbeat_interval.total_seconds() * 1000) + + runtime_options = temporalio.bridge.runtime.RuntimeOptions( + telemetry=telemetry._to_bridge_config(), + worker_heartbeat_interval_millis=heartbeat_millis, + disable_environment_info=disable_environment_info, ) + + self._core_runtime = temporalio.bridge.runtime.Runtime(options=runtime_options) if isinstance(telemetry.metrics, MetricBuffer): telemetry.metrics._runtime = self core_meter = temporalio.bridge.metric.MetricMeter.create(self._core_runtime) @@ -112,17 +183,26 @@ def formatted(self) -> str: """Return a formatted form of this filter.""" # We intentionally aren't using __str__ or __format__ so they can keep # their original dataclass impls - return f"{self.other_level},temporal_sdk_core={self.core_level},temporal_client={self.core_level},temporal_sdk={self.core_level}" + targets = [ + "temporalio_common", + "temporalio_sdk_core", + "temporalio_client", + "temporalio_sdk", + "temporal_sdk_bridge", + ] + parts = [self.other_level] + parts.extend(f"{target}={self.core_level}" for target in targets) + return ",".join(parts) @dataclass(frozen=True) class LoggingConfig: """Configuration for runtime logging.""" - filter: Union[TelemetryFilter, str] + filter: TelemetryFilter | str """Filter for logging. Can use :py:class:`TelemetryFilter` or raw string.""" - forwarding: Optional[LogForwardingConfig] = None + forwarding: LogForwardingConfig | None = None """If present, Core logger messages will be forwarded to a Python logger. See the :py:class:`LogForwardingConfig` docs for more info. """ @@ -241,16 +321,40 @@ class OpenTelemetryMetricTemporality(Enum): @dataclass(frozen=True) class OpenTelemetryConfig: - """Configuration for OpenTelemetry collector.""" + """Configuration for OpenTelemetry collector. + + Attributes: + url: URL of the OpenTelemetry collector endpoint (e.g. + ``"http://localhost:4317"`` for gRPC or + ``"http://localhost:4318/v1/metrics"`` for HTTP). + headers: Optional headers to include with each export request. + Useful for authentication tokens or routing metadata. + metric_periodicity: How often metrics are exported to the collector. + Defaults to 1s (set by sdk-core) when ``None``. + metric_temporality: Whether metrics are exported as cumulative + or delta values. Defaults to ``CUMULATIVE``. + durations_as_seconds: If ``True``, export duration metrics as + floating-point seconds instead of integer milliseconds. + Defaults to ``False``. + http: If ``True``, use HTTP/protobuf transport instead of gRPC. + When enabled, the ``url`` should point to the HTTP endpoint + (e.g. ``"http://localhost:4318/v1/metrics"``). + Defaults to ``False`` (gRPC). + histogram_bucket_overrides: Override the default histogram bucket + boundaries for specific metrics. Keys are metric names and + values are sequences of bucket boundaries (e.g. + ``{"workflow_task_schedule_to_start_latency": [0.01, 0.05, 0.1, 0.5, 1.0, 5.0]}``). + """ url: str - headers: Optional[Mapping[str, str]] = None - metric_periodicity: Optional[timedelta] = None + headers: Mapping[str, str] | None = None + metric_periodicity: timedelta | None = None metric_temporality: OpenTelemetryMetricTemporality = ( OpenTelemetryMetricTemporality.CUMULATIVE ) durations_as_seconds: bool = False http: bool = False + histogram_bucket_overrides: Mapping[str, Sequence[float]] | None = None def _to_bridge_config(self) -> temporalio.bridge.runtime.OpenTelemetryConfig: return temporalio.bridge.runtime.OpenTelemetryConfig( @@ -266,18 +370,40 @@ def _to_bridge_config(self) -> temporalio.bridge.runtime.OpenTelemetryConfig: ), durations_as_seconds=self.durations_as_seconds, http=self.http, + histogram_bucket_overrides=self.histogram_bucket_overrides, ) @dataclass(frozen=True) class PrometheusConfig: - """Configuration for Prometheus metrics endpoint.""" + """Configuration for Prometheus metrics endpoint. + + Starts an HTTP server on the given address that exposes a ``/metrics`` + endpoint for Prometheus scraping. + + Attributes: + bind_address: Address to bind the metrics HTTP server to (e.g. + ``"0.0.0.0:9000"`` or ``"127.0.0.1:9090"``). Prometheus + will scrape ``http:///metrics``. + counters_total_suffix: If ``True``, append ``_total`` suffix to + counter metric names, following the OpenMetrics convention. + Defaults to ``False``. + unit_suffix: If ``True``, append unit suffixes (e.g. ``_seconds``, + ``_bytes``) to metric names. Defaults to ``False``. + durations_as_seconds: If ``True``, report duration metrics as + floating-point seconds instead of integer milliseconds. + Defaults to ``False``. + histogram_bucket_overrides: Override the default histogram bucket + boundaries for specific metrics. Keys are metric names and + values are sequences of bucket boundaries (e.g. + ``{"workflow_task_schedule_to_start_latency": [0.01, 0.05, 0.1, 0.5, 1.0, 5.0]}``). + """ bind_address: str counters_total_suffix: bool = False unit_suffix: bool = False durations_as_seconds: bool = False - histogram_bucket_overrides: Optional[Mapping[str, Sequence[float]]] = None + histogram_bucket_overrides: Mapping[str, Sequence[float]] | None = None def _to_bridge_config(self) -> temporalio.bridge.runtime.PrometheusConfig: return temporalio.bridge.runtime.PrometheusConfig( @@ -327,7 +453,7 @@ def __init__( duration_format: Which duration format to use. """ self._buffer_size = buffer_size - self._runtime: Optional[Runtime] = None + self._runtime: Runtime | None = None self._durations_as_seconds = ( duration_format == MetricBufferDurationFormat.SECONDS ) @@ -353,10 +479,10 @@ def retrieve_updates(self) -> Sequence[BufferedMetricUpdate]: class TelemetryConfig: """Configuration for Core telemetry.""" - logging: Optional[LoggingConfig] = LoggingConfig.default + logging: LoggingConfig | None = LoggingConfig.default """Logging configuration.""" - metrics: Optional[Union[OpenTelemetryConfig, PrometheusConfig, MetricBuffer]] = None + metrics: OpenTelemetryConfig | PrometheusConfig | MetricBuffer | None = None """Metrics configuration or buffer.""" global_tags: Mapping[str, str] = field(default_factory=dict) @@ -365,7 +491,7 @@ class TelemetryConfig: attach_service_name: bool = True """Whether to put the service_name on every metric.""" - metric_prefix: Optional[str] = None + metric_prefix: str | None = None """Prefix to put on every Temporal metric. If unset, defaults to ``temporal_``.""" @@ -419,12 +545,12 @@ def name(self) -> str: ... @property - def description(self) -> Optional[str]: + def description(self) -> str | None: """Get the description of the metric if any.""" ... @property - def unit(self) -> Optional[str]: + def unit(self) -> str | None: """Get the unit of the metric if any.""" ... @@ -454,7 +580,7 @@ def metric(self) -> BufferedMetric: ... @property - def value(self) -> Union[int, float]: + def value(self) -> int | float: """Value for the update. For counters this is a delta, for gauges and histograms this is just the @@ -487,7 +613,7 @@ def __init__( self._core_attrs = core_attrs def create_counter( - self, name: str, description: Optional[str] = None, unit: Optional[str] = None + self, name: str, description: str | None = None, unit: str | None = None ) -> temporalio.common.MetricCounter: return _MetricCounter( name, @@ -500,7 +626,7 @@ def create_counter( ) def create_histogram( - self, name: str, description: Optional[str] = None, unit: Optional[str] = None + self, name: str, description: str | None = None, unit: str | None = None ) -> temporalio.common.MetricHistogram: return _MetricHistogram( name, @@ -513,7 +639,7 @@ def create_histogram( ) def create_histogram_float( - self, name: str, description: Optional[str] = None, unit: Optional[str] = None + self, name: str, description: str | None = None, unit: str | None = None ) -> temporalio.common.MetricHistogramFloat: return _MetricHistogramFloat( name, @@ -526,7 +652,7 @@ def create_histogram_float( ) def create_histogram_timedelta( - self, name: str, description: Optional[str] = None, unit: Optional[str] = None + self, name: str, description: str | None = None, unit: str | None = None ) -> temporalio.common.MetricHistogramTimedelta: return _MetricHistogramTimedelta( name, @@ -539,7 +665,7 @@ def create_histogram_timedelta( ) def create_gauge( - self, name: str, description: Optional[str] = None, unit: Optional[str] = None + self, name: str, description: str | None = None, unit: str | None = None ) -> temporalio.common.MetricGauge: return _MetricGauge( name, @@ -552,7 +678,7 @@ def create_gauge( ) def create_gauge_float( - self, name: str, description: Optional[str] = None, unit: Optional[str] = None + self, name: str, description: str | None = None, unit: str | None = None ) -> temporalio.common.MetricGaugeFloat: return _MetricGaugeFloat( name, @@ -580,8 +706,8 @@ class _MetricCommon(temporalio.common.MetricCommon, Generic[_CoreMetricType]): def __init__( self, name: str, - description: Optional[str], - unit: Optional[str], + description: str | None, + unit: str | None, core_metric: _CoreMetricType, core_attrs: temporalio.bridge.metric.MetricAttributes, ) -> None: @@ -596,11 +722,11 @@ def name(self) -> str: return self._name @property - def description(self) -> Optional[str]: + def description(self) -> str | None: return self._description @property - def unit(self) -> Optional[str]: + def unit(self) -> str | None: return self._unit def with_additional_attributes( @@ -622,7 +748,7 @@ class _MetricCounter( def add( self, value: int, - additional_attributes: Optional[temporalio.common.MetricAttributes] = None, + additional_attributes: temporalio.common.MetricAttributes | None = None, ) -> None: if value < 0: raise ValueError("Metric value cannot be negative") @@ -639,7 +765,7 @@ class _MetricHistogram( def record( self, value: int, - additional_attributes: Optional[temporalio.common.MetricAttributes] = None, + additional_attributes: temporalio.common.MetricAttributes | None = None, ) -> None: if value < 0: raise ValueError("Metric value cannot be negative") @@ -656,7 +782,7 @@ class _MetricHistogramFloat( def record( self, value: float, - additional_attributes: Optional[temporalio.common.MetricAttributes] = None, + additional_attributes: temporalio.common.MetricAttributes | None = None, ) -> None: if value < 0: raise ValueError("Metric value cannot be negative") @@ -673,7 +799,7 @@ class _MetricHistogramTimedelta( def record( self, value: timedelta, - additional_attributes: Optional[temporalio.common.MetricAttributes] = None, + additional_attributes: temporalio.common.MetricAttributes | None = None, ) -> None: if value.days < 0: raise ValueError("Metric value cannot be negative") @@ -694,7 +820,7 @@ class _MetricGauge( def set( self, value: int, - additional_attributes: Optional[temporalio.common.MetricAttributes] = None, + additional_attributes: temporalio.common.MetricAttributes | None = None, ) -> None: if value < 0: raise ValueError("Metric value cannot be negative") @@ -711,7 +837,7 @@ class _MetricGaugeFloat( def set( self, value: float, - additional_attributes: Optional[temporalio.common.MetricAttributes] = None, + additional_attributes: temporalio.common.MetricAttributes | None = None, ) -> None: if value < 0: raise ValueError("Metric value cannot be negative") diff --git a/temporalio/service.py b/temporalio/service.py index 189df6ba2..130b5d295 100644 --- a/temporalio/service.py +++ b/temporalio/service.py @@ -8,25 +8,23 @@ import socket import warnings from abc import ABC, abstractmethod +from collections.abc import Mapping from dataclasses import dataclass, field from datetime import timedelta from enum import IntEnum -from typing import ClassVar, Generic, Mapping, Optional, Tuple, Type, TypeVar, Union +from typing import ClassVar, TypeVar -import google.protobuf.empty_pb2 import google.protobuf.message -import temporalio.api.cloud.cloudservice.v1 import temporalio.api.common.v1 -import temporalio.api.operatorservice.v1 -import temporalio.api.testservice.v1 -import temporalio.api.workflowservice.v1 import temporalio.bridge.client import temporalio.bridge.proto.health.v1 +import temporalio.bridge.services_generated import temporalio.exceptions import temporalio.runtime +from temporalio.bridge.client import RPCError as BridgeRPCError -__version__ = "1.17.0" +__version__ = "1.31.0" ServiceRequest = TypeVar("ServiceRequest", bound=google.protobuf.message.Message) ServiceResponse = TypeVar("ServiceResponse", bound=google.protobuf.message.Message) @@ -41,28 +39,44 @@ class TLSConfig: """TLS configuration for connecting to Temporal server.""" - server_root_ca_cert: Optional[bytes] = None + server_root_ca_cert: bytes | None = None """Root CA to validate the server certificate against.""" - domain: Optional[str] = None - """TLS domain.""" + domain: str | None = None + """SNI host and HTTP/2 authority override, and the default name the server + certificate is verified against (see + :py:attr:`verification_server_name`).""" - client_cert: Optional[bytes] = None + client_cert: bytes | None = None """Client certificate for mTLS. This must be combined with :py:attr:`client_private_key`.""" - client_private_key: Optional[bytes] = None + client_private_key: bytes | None = None """Client private key for mTLS. This must be combined with :py:attr:`client_cert`.""" + verification_server_name: str | None = None + """Name to verify the server certificate against, instead of the + :py:attr:`domain` / connected host. + + Unlike :py:attr:`domain`, this does not change the TLS SNI or HTTP/2 + authority values, which continue to follow the connected host (or + :py:attr:`domain` if set). Use this when the server's certificate does not + carry the name being dialed, e.g. when the connection traverses an + SNI-inspecting proxy that must be able to resolve the SNI value. + + Requires :py:attr:`server_root_ca_cert`; the system root store is not + consulted when this is set.""" + def _to_bridge_config(self) -> temporalio.bridge.client.ClientTlsConfig: return temporalio.bridge.client.ClientTlsConfig( server_root_ca_cert=self.server_root_ca_cert, domain=self.domain, client_cert=self.client_cert, client_private_key=self.client_private_key, + verification_server_name=self.verification_server_name, ) @@ -78,7 +92,7 @@ class RetryConfig: """Backoff multiplier.""" max_interval_millis: int = 5000 """Maximum backoff interval.""" - max_elapsed_time_millis: Optional[int] = 10000 + max_elapsed_time_millis: int | None = 10000 """Maximum total time.""" max_retries: int = 10 """Maximum number of retries.""" @@ -122,7 +136,7 @@ class HttpConnectProxyConfig: target_host: str """Target host:port for the HTTP CONNECT proxy.""" - basic_auth: Optional[Tuple[str, str]] = None + basic_auth: tuple[str, str] | None = None """Basic auth for the HTTP CONNECT proxy if any as a user/pass tuple.""" def _to_bridge_config( @@ -134,20 +148,97 @@ def _to_bridge_config( ) +@dataclass(frozen=True) +class DnsLoadBalancingConfig: + """DNS load balancing configuration for client connections. + + When enabled, Core periodically re-resolves the target host's DNS records + and round-robins requests across the resolved addresses. Cannot be used + together with :py:class:`HttpConnectProxyConfig` -- DNS load balancing is + silently disabled when an HTTP CONNECT proxy is configured. + """ + + resolution_interval_millis: int = 30000 + """How often to re-resolve DNS, in milliseconds.""" + default: ClassVar[DnsLoadBalancingConfig] + """Default DNS load balancing config.""" + + def _to_bridge_config( + self, + ) -> temporalio.bridge.client.ClientDnsLoadBalancingConfig: + return temporalio.bridge.client.ClientDnsLoadBalancingConfig( + resolution_interval_millis=self.resolution_interval_millis, + ) + + +DnsLoadBalancingConfig.default = DnsLoadBalancingConfig() + + +class GrpcCompression(ABC): + """Transport-level gRPC compression mode. + + This is a base type for concrete compression modes. Current modes are + available as singleton constants on this class. + """ + + NONE: ClassVar[GrpcCompression] + """Do not compress gRPC requests or advertise support for compressed responses.""" + + GZIP: ClassVar[GrpcCompression] + """Gzip-compress gRPC requests and accept gzip-compressed responses.""" + + @abstractmethod + def _to_bridge_config(self) -> str: + raise NotImplementedError + + +@dataclass(frozen=True) +class _NoGrpcCompression(GrpcCompression): + def _to_bridge_config(self) -> str: + return "none" + + +@dataclass(frozen=True) +class _GzipGrpcCompression(GrpcCompression): + def _to_bridge_config(self) -> str: + return "gzip" + + +GrpcCompression.NONE = _NoGrpcCompression() +GrpcCompression.GZIP = _GzipGrpcCompression() + + +@dataclass(frozen=True) +class PayloadLimitsConfig: + """Warning thresholds for outbound payload/memo sizes.""" + + # Defaults mirror the Temporal server's `limit.blobSize.warn` (512 KiB) and `limit.memoSize.warn` + # (2 KiB) dynamic-config defaults, so the SDK warns at the same sizes the server would. + payloads_warn_size: int = 512 * 1024 + """Warning threshold, in bytes, for the size of an outbound payload-bearing field. Set to 0 to + disable.""" + + memo_warn_size: int = 2 * 1024 + """Warning threshold, in bytes, for outbound memo size. Set to 0 to disable.""" + + @dataclass class ConnectConfig: """Config for connecting to the server.""" target_host: str - api_key: Optional[str] = None - tls: Union[bool, TLSConfig] = False - retry_config: Optional[RetryConfig] = None - keep_alive_config: Optional[KeepAliveConfig] = KeepAliveConfig.default - rpc_metadata: Mapping[str, str] = field(default_factory=dict) + api_key: str | None = None + tls: bool | TLSConfig | None = None + retry_config: RetryConfig | None = None + keep_alive_config: KeepAliveConfig | None = KeepAliveConfig.default + rpc_metadata: Mapping[str, str | bytes] = field(default_factory=dict) identity: str = "" lazy: bool = False - runtime: Optional[temporalio.runtime.Runtime] = None - http_connect_proxy_config: Optional[HttpConnectProxyConfig] = None + runtime: temporalio.runtime.Runtime | None = None + http_connect_proxy_config: HttpConnectProxyConfig | None = None + dns_load_balancing_config: DnsLoadBalancingConfig | None = None + grpc_compression: GrpcCompression = GrpcCompression.GZIP + payload_limits: PayloadLimitsConfig = field(default_factory=PayloadLimitsConfig) def __post_init__(self) -> None: """Set extra defaults on unset properties.""" @@ -159,7 +250,7 @@ def _to_bridge_config(self) -> temporalio.bridge.client.ClientConfig: # past so we'll leave it for only one more version with a warning. # Otherwise we'll prepend the scheme. target_url: str - tls_config: Optional[temporalio.bridge.client.ClientTlsConfig] + tls_config: temporalio.bridge.client.ClientTlsConfig | None if "://" in self.target_host: warnings.warn( "Target host as URL with scheme no longer supported. This will be an error in future versions." @@ -176,6 +267,10 @@ def _to_bridge_config(self) -> temporalio.bridge.client.ClientConfig: elif self.tls: target_url = f"https://{self.target_host}" tls_config = TLSConfig()._to_bridge_config() + # Enable TLS by default when API key is provided and tls not explicitly set + elif self.tls is None and self.api_key is not None: + target_url = f"https://{self.target_host}" + tls_config = TLSConfig()._to_bridge_config() else: target_url = f"http://{self.target_host}" tls_config = None @@ -201,6 +296,14 @@ def _to_bridge_config(self) -> temporalio.bridge.client.ClientConfig: if self.http_connect_proxy_config else None ), + dns_load_balancing_config=( + self.dns_load_balancing_config._to_bridge_config() + if self.dns_load_balancing_config + else None + ), + grpc_compression=self.grpc_compression._to_bridge_config(), + payloads_warn_size=self.payload_limits.payloads_warn_size, + memo_warn_size=self.payload_limits.memo_warn_size, ) @@ -220,38 +323,31 @@ def __init__(self, config: ConnectConfig) -> None: self.operator_service = OperatorService(self) self.cloud_service = CloudService(self) self.test_service = TestService(self) - self._check_health_call = self._new_call( - "check", - temporalio.bridge.proto.health.v1.HealthCheckRequest, - temporalio.bridge.proto.health.v1.HealthCheckResponse, - service="health", - ) + self.health_service = HealthService(self) async def check_health( self, *, service: str = "temporal.api.workflowservice.v1.WorkflowService", retry: bool = False, - metadata: Mapping[str, str] = {}, - timeout: Optional[timedelta] = None, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, ) -> bool: - """Check whether the WorkflowService is up. - - In addition to accepting which service to check health on, this accepts - some of the same parameters as other RPC calls. See - :py:meth:`ServiceCall.__call__`. + """Check whether the provided service is up. If no service is specified, + the WorkflowService is used. Returns: True when available, false if the server is running but the service is unavailable (rare), or raises an error if server/service cannot be reached. """ - resp = await self._check_health_call( + resp = await self.health_service.check( temporalio.bridge.proto.health.v1.HealthCheckRequest(service=service), retry=retry, metadata=metadata, timeout=timeout, ) + return ( resp.status == temporalio.bridge.proto.health.v1.HealthCheckResponse.ServingStatus.SERVING @@ -264,12 +360,12 @@ def worker_service_client(self) -> _BridgeServiceClient: raise NotImplementedError @abstractmethod - def update_rpc_metadata(self, metadata: Mapping[str, str]) -> None: + def update_rpc_metadata(self, metadata: Mapping[str, str | bytes]) -> None: """Update service client's RPC metadata.""" raise NotImplementedError @abstractmethod - def update_api_key(self, api_key: Optional[str]) -> None: + def update_api_key(self, api_key: str | None) -> None: """Update service client's API key.""" raise NotImplementedError @@ -278,1012 +374,34 @@ async def _rpc_call( self, rpc: str, req: google.protobuf.message.Message, - resp_type: Type[ServiceResponse], + resp_type: type[ServiceResponse], *, service: str, retry: bool, - metadata: Mapping[str, str], - timeout: Optional[timedelta], + metadata: Mapping[str, str | bytes], + timeout: timedelta | None, ) -> ServiceResponse: raise NotImplementedError - def _new_call( - self, - name: str, - req_type: Type[ServiceRequest], - resp_type: Type[ServiceResponse], - *, - service: str = "workflow", - ) -> ServiceCall[ServiceRequest, ServiceResponse]: - return ServiceCall(self, name, req_type, resp_type, service) - -class WorkflowService: +class WorkflowService(temporalio.bridge.services_generated.WorkflowService): """Client to the Temporal server's workflow service.""" - def __init__(self, client: ServiceClient) -> None: - """Initialize the workflow service.""" - wsv1 = temporalio.api.workflowservice.v1 - self.count_workflow_executions = client._new_call( - "count_workflow_executions", - wsv1.CountWorkflowExecutionsRequest, - wsv1.CountWorkflowExecutionsResponse, - ) - self.create_schedule = client._new_call( - "create_schedule", - wsv1.CreateScheduleRequest, - wsv1.CreateScheduleResponse, - ) - self.create_workflow_rule = client._new_call( - "create_workflow_rule", - wsv1.CreateWorkflowRuleRequest, - wsv1.CreateWorkflowRuleRequest, - ) - self.delete_schedule = client._new_call( - "delete_schedule", - wsv1.DeleteScheduleRequest, - wsv1.DeleteScheduleResponse, - ) - self.delete_worker_deployment = client._new_call( - "delete_worker_deployment", - wsv1.DeleteWorkerDeploymentRequest, - wsv1.DeleteWorkerDeploymentResponse, - ) - self.delete_worker_deployment_version = client._new_call( - "delete_worker_deployment_version", - wsv1.DeleteWorkerDeploymentVersionRequest, - wsv1.DeleteWorkerDeploymentVersionResponse, - ) - self.delete_workflow_execution = client._new_call( - "delete_workflow_execution", - wsv1.DeleteWorkflowExecutionRequest, - wsv1.DeleteWorkflowExecutionResponse, - ) - self.delete_workflow_rule = client._new_call( - "delete_workflow_rule", - wsv1.DeleteWorkflowRuleRequest, - wsv1.DeleteWorkflowRuleResponse, - ) - self.describe_batch_operation = client._new_call( - "describe_batch_operation", - wsv1.DescribeBatchOperationRequest, - wsv1.DescribeBatchOperationResponse, - ) - self.describe_deployment = client._new_call( - "describe_deployment", - wsv1.DescribeDeploymentRequest, - wsv1.DescribeDeploymentResponse, - ) - self.deprecate_namespace = client._new_call( - "deprecate_namespace", - wsv1.DeprecateNamespaceRequest, - wsv1.DeprecateNamespaceResponse, - ) - self.describe_namespace = client._new_call( - "describe_namespace", - wsv1.DescribeNamespaceRequest, - wsv1.DescribeNamespaceResponse, - ) - self.describe_schedule = client._new_call( - "describe_schedule", - wsv1.DescribeScheduleRequest, - wsv1.DescribeScheduleResponse, - ) - self.describe_task_queue = client._new_call( - "describe_task_queue", - wsv1.DescribeTaskQueueRequest, - wsv1.DescribeTaskQueueResponse, - ) - self.describe_worker_deployment = client._new_call( - "describe_worker_deployment", - wsv1.DescribeWorkerDeploymentRequest, - wsv1.DescribeWorkerDeploymentResponse, - ) - self.describe_worker_deployment_version = client._new_call( - "describe_worker_deployment_version", - wsv1.DescribeWorkerDeploymentVersionRequest, - wsv1.DescribeWorkerDeploymentVersionResponse, - ) - self.describe_workflow_execution = client._new_call( - "describe_workflow_execution", - wsv1.DescribeWorkflowExecutionRequest, - wsv1.DescribeWorkflowExecutionResponse, - ) - self.describe_workflow_rule = client._new_call( - "describe_workflow_rule", - wsv1.DescribeWorkflowRuleRequest, - wsv1.DescribeWorkflowRuleResponse, - ) - self.execute_multi_operation = client._new_call( - "execute_multi_operation", - wsv1.ExecuteMultiOperationRequest, - wsv1.ExecuteMultiOperationResponse, - ) - self.fetch_worker_config = client._new_call( - "fetch_worker_config", - wsv1.FetchWorkerConfigRequest, - wsv1.FetchWorkerConfigResponse, - ) - self.get_cluster_info = client._new_call( - "get_cluster_info", - wsv1.GetClusterInfoRequest, - wsv1.GetClusterInfoResponse, - ) - self.get_current_deployment = client._new_call( - "get_current_deployment", - wsv1.GetCurrentDeploymentRequest, - wsv1.GetCurrentDeploymentResponse, - ) - self.get_deployment_reachability = client._new_call( - "get_deployment_reachability", - wsv1.GetDeploymentReachabilityRequest, - wsv1.GetDeploymentReachabilityResponse, - ) - self.get_search_attributes = client._new_call( - "get_search_attributes", - wsv1.GetSearchAttributesRequest, - wsv1.GetSearchAttributesResponse, - ) - self.get_system_info = client._new_call( - "get_system_info", - wsv1.GetSystemInfoRequest, - wsv1.GetSystemInfoResponse, - ) - self.get_worker_build_id_compatibility = client._new_call( - "get_worker_build_id_compatibility", - wsv1.GetWorkerBuildIdCompatibilityRequest, - wsv1.GetWorkerBuildIdCompatibilityResponse, - ) - self.get_worker_task_reachability = client._new_call( - "get_worker_task_reachability", - wsv1.GetWorkerTaskReachabilityRequest, - wsv1.GetWorkerTaskReachabilityResponse, - ) - self.get_worker_versioning_rules = client._new_call( - "get_worker_versioning_rules", - wsv1.GetWorkerVersioningRulesRequest, - wsv1.GetWorkerVersioningRulesResponse, - ) - self.get_workflow_execution_history = client._new_call( - "get_workflow_execution_history", - wsv1.GetWorkflowExecutionHistoryRequest, - wsv1.GetWorkflowExecutionHistoryResponse, - ) - self.get_workflow_execution_history_reverse = client._new_call( - "get_workflow_execution_history_reverse", - wsv1.GetWorkflowExecutionHistoryReverseRequest, - wsv1.GetWorkflowExecutionHistoryReverseResponse, - ) - self.list_archived_workflow_executions = client._new_call( - "list_archived_workflow_executions", - wsv1.ListArchivedWorkflowExecutionsRequest, - wsv1.ListArchivedWorkflowExecutionsResponse, - ) - self.list_batch_operations = client._new_call( - "list_batch_operations", - wsv1.ListBatchOperationsRequest, - wsv1.ListBatchOperationsResponse, - ) - self.list_closed_workflow_executions = client._new_call( - "list_closed_workflow_executions", - wsv1.ListClosedWorkflowExecutionsRequest, - wsv1.ListClosedWorkflowExecutionsResponse, - ) - self.list_deployments = client._new_call( - "list_deployments", - wsv1.ListDeploymentsRequest, - wsv1.ListDeploymentsResponse, - ) - self.list_namespaces = client._new_call( - "list_namespaces", - wsv1.ListNamespacesRequest, - wsv1.ListNamespacesResponse, - ) - self.list_open_workflow_executions = client._new_call( - "list_open_workflow_executions", - wsv1.ListOpenWorkflowExecutionsRequest, - wsv1.ListOpenWorkflowExecutionsResponse, - ) - self.list_schedule_matching_times = client._new_call( - "list_schedule_matching_times", - wsv1.ListScheduleMatchingTimesRequest, - wsv1.ListScheduleMatchingTimesResponse, - ) - self.list_schedules = client._new_call( - "list_schedules", - wsv1.ListSchedulesRequest, - wsv1.ListSchedulesResponse, - ) - self.list_task_queue_partitions = client._new_call( - "list_task_queue_partitions", - wsv1.ListTaskQueuePartitionsRequest, - wsv1.ListTaskQueuePartitionsResponse, - ) - self.list_worker_deployments = client._new_call( - "list_worker_deployments", - wsv1.ListWorkerDeploymentsRequest, - wsv1.ListWorkerDeploymentsResponse, - ) - self.list_workflow_executions = client._new_call( - "list_workflow_executions", - wsv1.ListWorkflowExecutionsRequest, - wsv1.ListWorkflowExecutionsResponse, - ) - self.list_workers = client._new_call( - "list_workers", - wsv1.ListWorkersRequest, - wsv1.ListWorkersResponse, - ) - self.list_workflow_rules = client._new_call( - "list_workflow_rules", - wsv1.ListWorkflowRulesRequest, - wsv1.ListWorkflowRulesResponse, - ) - self.patch_schedule = client._new_call( - "patch_schedule", - wsv1.PatchScheduleRequest, - wsv1.PatchScheduleResponse, - ) - self.pause_activity = client._new_call( - "pause_activity", - wsv1.PauseActivityRequest, - wsv1.PauseActivityResponse, - ) - self.poll_activity_task_queue = client._new_call( - "poll_activity_task_queue", - wsv1.PollActivityTaskQueueRequest, - wsv1.PollActivityTaskQueueResponse, - ) - self.poll_nexus_task_queue = client._new_call( - "poll_nexus_task_queue", - wsv1.PollNexusTaskQueueRequest, - wsv1.PollNexusTaskQueueResponse, - ) - self.poll_workflow_execution_update = client._new_call( - "poll_workflow_execution_update", - wsv1.PollWorkflowExecutionUpdateRequest, - wsv1.PollWorkflowExecutionUpdateResponse, - ) - self.poll_workflow_task_queue = client._new_call( - "poll_workflow_task_queue", - wsv1.PollWorkflowTaskQueueRequest, - wsv1.PollWorkflowTaskQueueResponse, - ) - self.query_workflow = client._new_call( - "query_workflow", - wsv1.QueryWorkflowRequest, - wsv1.QueryWorkflowResponse, - ) - self.record_activity_task_heartbeat = client._new_call( - "record_activity_task_heartbeat", - wsv1.RecordActivityTaskHeartbeatRequest, - wsv1.RecordActivityTaskHeartbeatResponse, - ) - self.record_activity_task_heartbeat_by_id = client._new_call( - "record_activity_task_heartbeat_by_id", - wsv1.RecordActivityTaskHeartbeatByIdRequest, - wsv1.RecordActivityTaskHeartbeatByIdResponse, - ) - self.record_worker_heartbeat = client._new_call( - "record_worker_heartbeat", - wsv1.RecordWorkerHeartbeatRequest, - wsv1.RecordWorkerHeartbeatResponse, - ) - self.register_namespace = client._new_call( - "register_namespace", - wsv1.RegisterNamespaceRequest, - wsv1.RegisterNamespaceResponse, - ) - self.request_cancel_workflow_execution = client._new_call( - "request_cancel_workflow_execution", - wsv1.RequestCancelWorkflowExecutionRequest, - wsv1.RequestCancelWorkflowExecutionResponse, - ) - self.reset_activity = client._new_call( - "reset_activity", - wsv1.ResetActivityRequest, - wsv1.ResetActivityResponse, - ) - self.reset_sticky_task_queue = client._new_call( - "reset_sticky_task_queue", - wsv1.ResetStickyTaskQueueRequest, - wsv1.ResetStickyTaskQueueResponse, - ) - self.reset_workflow_execution = client._new_call( - "reset_workflow_execution", - wsv1.ResetWorkflowExecutionRequest, - wsv1.ResetWorkflowExecutionResponse, - ) - self.respond_activity_task_canceled = client._new_call( - "respond_activity_task_canceled", - wsv1.RespondActivityTaskCanceledRequest, - wsv1.RespondActivityTaskCanceledResponse, - ) - self.respond_activity_task_canceled_by_id = client._new_call( - "respond_activity_task_canceled_by_id", - wsv1.RespondActivityTaskCanceledByIdRequest, - wsv1.RespondActivityTaskCanceledByIdResponse, - ) - self.respond_activity_task_completed = client._new_call( - "respond_activity_task_completed", - wsv1.RespondActivityTaskCompletedRequest, - wsv1.RespondActivityTaskCompletedResponse, - ) - self.respond_activity_task_completed_by_id = client._new_call( - "respond_activity_task_completed_by_id", - wsv1.RespondActivityTaskCompletedByIdRequest, - wsv1.RespondActivityTaskCompletedByIdResponse, - ) - self.respond_activity_task_failed = client._new_call( - "respond_activity_task_failed", - wsv1.RespondActivityTaskFailedRequest, - wsv1.RespondActivityTaskFailedResponse, - ) - self.respond_activity_task_failed_by_id = client._new_call( - "respond_activity_task_failed_by_id", - wsv1.RespondActivityTaskFailedByIdRequest, - wsv1.RespondActivityTaskFailedByIdResponse, - ) - self.respond_nexus_task_completed = client._new_call( - "respond_nexus_task_completed", - wsv1.RespondNexusTaskCompletedRequest, - wsv1.RespondNexusTaskCompletedResponse, - ) - self.respond_nexus_task_failed = client._new_call( - "respond_nexus_task_failed", - wsv1.RespondNexusTaskFailedRequest, - wsv1.RespondNexusTaskFailedResponse, - ) - self.respond_query_task_completed = client._new_call( - "respond_query_task_completed", - wsv1.RespondQueryTaskCompletedRequest, - wsv1.RespondQueryTaskCompletedResponse, - ) - self.respond_workflow_task_completed = client._new_call( - "respond_workflow_task_completed", - wsv1.RespondWorkflowTaskCompletedRequest, - wsv1.RespondWorkflowTaskCompletedResponse, - ) - self.respond_workflow_task_failed = client._new_call( - "respond_workflow_task_failed", - wsv1.RespondWorkflowTaskFailedRequest, - wsv1.RespondWorkflowTaskFailedResponse, - ) - self.scan_workflow_executions = client._new_call( - "scan_workflow_executions", - wsv1.ScanWorkflowExecutionsRequest, - wsv1.ScanWorkflowExecutionsResponse, - ) - self.set_current_deployment = client._new_call( - "set_current_deployment", - wsv1.SetCurrentDeploymentRequest, - wsv1.SetCurrentDeploymentResponse, - ) - self.set_worker_deployment_current_version = client._new_call( - "set_worker_deployment_current_version", - wsv1.SetWorkerDeploymentCurrentVersionRequest, - wsv1.SetWorkerDeploymentCurrentVersionResponse, - ) - self.set_worker_deployment_ramping_version = client._new_call( - "set_worker_deployment_ramping_version", - wsv1.SetWorkerDeploymentRampingVersionRequest, - wsv1.SetWorkerDeploymentRampingVersionResponse, - ) - self.shutdown_worker = client._new_call( - "shutdown_worker", - wsv1.ShutdownWorkerRequest, - wsv1.ShutdownWorkerResponse, - ) - self.signal_with_start_workflow_execution = client._new_call( - "signal_with_start_workflow_execution", - wsv1.SignalWithStartWorkflowExecutionRequest, - wsv1.SignalWithStartWorkflowExecutionResponse, - ) - self.signal_workflow_execution = client._new_call( - "signal_workflow_execution", - wsv1.SignalWorkflowExecutionRequest, - wsv1.SignalWorkflowExecutionResponse, - ) - self.start_batch_operation = client._new_call( - "start_batch_operation", - wsv1.StartBatchOperationRequest, - wsv1.StartBatchOperationResponse, - ) - self.start_workflow_execution = client._new_call( - "start_workflow_execution", - wsv1.StartWorkflowExecutionRequest, - wsv1.StartWorkflowExecutionResponse, - ) - self.stop_batch_operation = client._new_call( - "stop_batch_operation", - wsv1.StopBatchOperationRequest, - wsv1.StopBatchOperationResponse, - ) - self.terminate_workflow_execution = client._new_call( - "terminate_workflow_execution", - wsv1.TerminateWorkflowExecutionRequest, - wsv1.TerminateWorkflowExecutionResponse, - ) - self.trigger_workflow_rule = client._new_call( - "trigger_workflow_rule", - wsv1.TriggerWorkflowRuleRequest, - wsv1.TriggerWorkflowRuleResponse, - ) - self.unpause_activity = client._new_call( - "unpause_activity", - wsv1.UnpauseActivityRequest, - wsv1.UnpauseActivityResponse, - ) - self.update_activity_options = client._new_call( - "update_activity_options", - wsv1.UpdateActivityOptionsRequest, - wsv1.UpdateActivityOptionsResponse, - ) - self.update_namespace = client._new_call( - "update_namespace", - wsv1.UpdateNamespaceRequest, - wsv1.UpdateNamespaceResponse, - ) - self.update_schedule = client._new_call( - "update_schedule", - wsv1.UpdateScheduleRequest, - wsv1.UpdateScheduleResponse, - ) - self.update_task_queue_config = client._new_call( - "update_task_queue_config", - wsv1.UpdateTaskQueueConfigRequest, - wsv1.UpdateTaskQueueConfigResponse, - ) - self.update_worker_config = client._new_call( - "update_worker_config", - wsv1.UpdateWorkerConfigRequest, - wsv1.UpdateWorkerConfigResponse, - ) - self.update_worker_deployment_version_metadata = client._new_call( - "update_worker_deployment_version_metadata", - wsv1.UpdateWorkerDeploymentVersionMetadataRequest, - wsv1.UpdateWorkerDeploymentVersionMetadataResponse, - ) - self.update_worker_build_id_compatibility = client._new_call( - "update_worker_build_id_compatibility", - wsv1.UpdateWorkerBuildIdCompatibilityRequest, - wsv1.UpdateWorkerBuildIdCompatibilityResponse, - ) - self.update_worker_versioning_rules = client._new_call( - "update_worker_versioning_rules", - wsv1.UpdateWorkerVersioningRulesRequest, - wsv1.UpdateWorkerVersioningRulesResponse, - ) - self.update_workflow_execution = client._new_call( - "update_workflow_execution", - wsv1.UpdateWorkflowExecutionRequest, - wsv1.UpdateWorkflowExecutionResponse, - ) - self.update_workflow_execution_options = client._new_call( - "update_workflow_execution_options", - wsv1.UpdateWorkflowExecutionOptionsRequest, - wsv1.UpdateWorkflowExecutionOptionsResponse, - ) - -class OperatorService: +class OperatorService(temporalio.bridge.services_generated.OperatorService): """Client to the Temporal server's operator service.""" - def __init__(self, client: ServiceClient) -> None: - """Initialize the operator service.""" - osv1 = temporalio.api.operatorservice.v1 - self.add_or_update_remote_cluster = client._new_call( - "add_or_update_remote_cluster", - osv1.AddOrUpdateRemoteClusterRequest, - osv1.AddOrUpdateRemoteClusterResponse, - service="operator", - ) - self.add_search_attributes = client._new_call( - "add_search_attributes", - osv1.AddSearchAttributesRequest, - osv1.AddSearchAttributesResponse, - service="operator", - ) - self.create_nexus_endpoint = client._new_call( - "create_nexus_endpoint", - osv1.CreateNexusEndpointRequest, - osv1.CreateNexusEndpointResponse, - service="operator", - ) - self.delete_nexus_endpoint = client._new_call( - "delete_nexus_endpoint", - osv1.DeleteNexusEndpointRequest, - osv1.DeleteNexusEndpointResponse, - service="operator", - ) - self.delete_namespace = client._new_call( - "delete_namespace", - osv1.DeleteNamespaceRequest, - osv1.DeleteNamespaceResponse, - service="operator", - ) - self.get_nexus_endpoint = client._new_call( - "get_nexus_endpoint", - osv1.GetNexusEndpointRequest, - osv1.GetNexusEndpointResponse, - service="operator", - ) - self.list_clusters = client._new_call( - "list_clusters", - osv1.ListClustersRequest, - osv1.ListClustersResponse, - service="operator", - ) - self.list_nexus_endpoints = client._new_call( - "list_nexus_endpoints", - osv1.ListNexusEndpointsRequest, - osv1.ListNexusEndpointsResponse, - service="operator", - ) - self.list_search_attributes = client._new_call( - "list_search_attributes", - osv1.ListSearchAttributesRequest, - osv1.ListSearchAttributesResponse, - service="operator", - ) - self.remove_remote_cluster = client._new_call( - "remove_remote_cluster", - osv1.RemoveRemoteClusterRequest, - osv1.RemoveRemoteClusterResponse, - service="operator", - ) - self.remove_search_attributes = client._new_call( - "remove_search_attributes", - osv1.RemoveSearchAttributesRequest, - osv1.RemoveSearchAttributesResponse, - service="operator", - ) - self.update_nexus_endpoint = client._new_call( - "update_nexus_endpoint", - osv1.UpdateNexusEndpointRequest, - osv1.UpdateNexusEndpointResponse, - service="operator", - ) - -class CloudService: +class CloudService(temporalio.bridge.services_generated.CloudService): """Client to the Temporal server's cloud service.""" - def __init__(self, client: ServiceClient) -> None: - """Initialize the cloud service.""" - clv1 = temporalio.api.cloud.cloudservice.v1 - self.add_namespace_region = client._new_call( - "add_namespace_region", - clv1.AddNamespaceRegionRequest, - clv1.AddNamespaceRegionResponse, - service="cloud", - ) - self.add_user_group_member = client._new_call( - "add_user_group_member", - clv1.AddUserGroupMemberRequest, - clv1.AddUserGroupMemberResponse, - service="cloud", - ) - self.create_api_key = client._new_call( - "create_api_key", - clv1.CreateApiKeyRequest, - clv1.CreateApiKeyResponse, - service="cloud", - ) - self.create_connectivity_rule = client._new_call( - "create_connectivity_rule", - clv1.CreateConnectivityRuleRequest, - clv1.CreateConnectivityRuleResponse, - service="cloud", - ) - self.create_namespace = client._new_call( - "create_namespace", - clv1.CreateNamespaceRequest, - clv1.CreateNamespaceResponse, - service="cloud", - ) - self.create_namespace_export_sink = client._new_call( - "create_namespace_export_sink", - clv1.CreateNamespaceExportSinkRequest, - clv1.CreateNamespaceExportSinkResponse, - service="cloud", - ) - self.create_nexus_endpoint = client._new_call( - "create_nexus_endpoint", - clv1.CreateNexusEndpointRequest, - clv1.CreateNexusEndpointResponse, - service="cloud", - ) - self.create_service_account = client._new_call( - "create_service_account", - clv1.CreateServiceAccountRequest, - clv1.CreateServiceAccountResponse, - service="cloud", - ) - self.create_user_group = client._new_call( - "create_user_group", - clv1.CreateUserGroupRequest, - clv1.CreateUserGroupResponse, - service="cloud", - ) - self.create_user = client._new_call( - "create_user", - clv1.CreateUserRequest, - clv1.CreateUserResponse, - service="cloud", - ) - self.delete_api_key = client._new_call( - "delete_api_key", - clv1.DeleteApiKeyRequest, - clv1.DeleteApiKeyResponse, - service="cloud", - ) - self.delete_connectivity_rule = client._new_call( - "delete_connectivity_rule", - clv1.DeleteConnectivityRuleRequest, - clv1.DeleteConnectivityRuleResponse, - service="cloud", - ) - self.delete_namespace = client._new_call( - "delete_namespace", - clv1.DeleteNamespaceRequest, - clv1.DeleteNamespaceResponse, - service="cloud", - ) - self.delete_namespace_export_sink = client._new_call( - "delete_namespace_export_sink", - clv1.DeleteNamespaceExportSinkRequest, - clv1.DeleteNamespaceExportSinkResponse, - service="cloud", - ) - self.delete_namespace_region = client._new_call( - "delete_namespace_region", - clv1.DeleteNamespaceRegionRequest, - clv1.DeleteNamespaceRegionResponse, - service="cloud", - ) - self.delete_nexus_endpoint = client._new_call( - "delete_nexus_endpoint", - clv1.DeleteNexusEndpointRequest, - clv1.DeleteNexusEndpointResponse, - service="cloud", - ) - self.delete_service_account = client._new_call( - "delete_service_account", - clv1.DeleteServiceAccountRequest, - clv1.DeleteServiceAccountResponse, - service="cloud", - ) - self.delete_user_group = client._new_call( - "delete_user_group", - clv1.DeleteUserGroupRequest, - clv1.DeleteUserGroupResponse, - service="cloud", - ) - self.delete_user = client._new_call( - "delete_user", - clv1.DeleteUserRequest, - clv1.DeleteUserResponse, - service="cloud", - ) - self.failover_namespace_region = client._new_call( - "failover_namespace_region", - clv1.FailoverNamespaceRegionRequest, - clv1.FailoverNamespaceRegionResponse, - service="cloud", - ) - self.get_account = client._new_call( - "get_account", - clv1.GetAccountRequest, - clv1.GetAccountResponse, - service="cloud", - ) - self.get_api_key = client._new_call( - "get_api_key", - clv1.GetApiKeyRequest, - clv1.GetApiKeyResponse, - service="cloud", - ) - self.get_api_keys = client._new_call( - "get_api_keys", - clv1.GetApiKeysRequest, - clv1.GetApiKeysResponse, - service="cloud", - ) - self.get_async_operation = client._new_call( - "get_async_operation", - clv1.GetAsyncOperationRequest, - clv1.GetAsyncOperationResponse, - service="cloud", - ) - self.get_connectivity_rule = client._new_call( - "get_connectivity_rule", - clv1.GetConnectivityRuleRequest, - clv1.GetConnectivityRuleResponse, - service="cloud", - ) - self.get_connectivity_rules = client._new_call( - "get_connectivity_rules", - clv1.GetConnectivityRulesRequest, - clv1.GetConnectivityRulesResponse, - service="cloud", - ) - self.get_namespace = client._new_call( - "get_namespace", - clv1.GetNamespaceRequest, - clv1.GetNamespaceResponse, - service="cloud", - ) - self.get_namespaces = client._new_call( - "get_namespaces", - clv1.GetNamespacesRequest, - clv1.GetNamespacesResponse, - service="cloud", - ) - self.get_namespace_export_sink = client._new_call( - "get_namespace_export_sink", - clv1.GetNamespaceExportSinkRequest, - clv1.GetNamespaceExportSinkResponse, - service="cloud", - ) - self.get_namespace_export_sinks = client._new_call( - "get_namespace_export_sinks", - clv1.GetNamespaceExportSinksRequest, - clv1.GetNamespaceExportSinksResponse, - service="cloud", - ) - self.get_nexus_endpoint = client._new_call( - "get_nexus_endpoint", - clv1.GetNexusEndpointRequest, - clv1.GetNexusEndpointResponse, - service="cloud", - ) - self.get_nexus_endpoints = client._new_call( - "get_nexus_endpoints", - clv1.GetNexusEndpointsRequest, - clv1.GetNexusEndpointsResponse, - service="cloud", - ) - self.get_region = client._new_call( - "get_region", - clv1.GetRegionRequest, - clv1.GetRegionResponse, - service="cloud", - ) - self.get_regions = client._new_call( - "get_regions", - clv1.GetRegionsRequest, - clv1.GetRegionsResponse, - service="cloud", - ) - self.get_service_account = client._new_call( - "get_service_account", - clv1.GetServiceAccountRequest, - clv1.GetServiceAccountResponse, - service="cloud", - ) - self.get_service_accounts = client._new_call( - "get_service_accounts", - clv1.GetServiceAccountsRequest, - clv1.GetServiceAccountsResponse, - service="cloud", - ) - self.get_usage = client._new_call( - "get_usage", - clv1.GetUsageRequest, - clv1.GetUsageResponse, - service="cloud", - ) - self.get_user_group = client._new_call( - "get_user_group", - clv1.GetUserGroupRequest, - clv1.GetUserGroupResponse, - service="cloud", - ) - self.get_user_group_members = client._new_call( - "get_user_group_members", - clv1.GetUserGroupMembersRequest, - clv1.GetUserGroupMembersResponse, - service="cloud", - ) - self.get_user_groups = client._new_call( - "get_user_groups", - clv1.GetUserGroupsRequest, - clv1.GetUserGroupsResponse, - service="cloud", - ) - self.get_user = client._new_call( - "get_user", - clv1.GetUserRequest, - clv1.GetUserResponse, - service="cloud", - ) - self.get_users = client._new_call( - "get_users", - clv1.GetUsersRequest, - clv1.GetUsersResponse, - service="cloud", - ) - self.remove_user_group_member = client._new_call( - "remove_user_group_member", - clv1.RemoveUserGroupMemberRequest, - clv1.RemoveUserGroupMemberResponse, - service="cloud", - ) - self.rename_custom_search_attribute = client._new_call( - "rename_custom_search_attribute", - clv1.RenameCustomSearchAttributeRequest, - clv1.RenameCustomSearchAttributeResponse, - service="cloud", - ) - self.set_user_group_namespace_access = client._new_call( - "set_user_group_namespace_access", - clv1.SetUserGroupNamespaceAccessRequest, - clv1.SetUserGroupNamespaceAccessResponse, - service="cloud", - ) - self.set_user_namespace_access = client._new_call( - "set_user_namespace_access", - clv1.SetUserNamespaceAccessRequest, - clv1.SetUserNamespaceAccessResponse, - service="cloud", - ) - self.update_account = client._new_call( - "update_account", - clv1.UpdateAccountRequest, - clv1.UpdateAccountResponse, - service="cloud", - ) - self.update_api_key = client._new_call( - "update_api_key", - clv1.UpdateApiKeyRequest, - clv1.UpdateApiKeyResponse, - service="cloud", - ) - self.update_namespace = client._new_call( - "update_namespace", - clv1.UpdateNamespaceRequest, - clv1.UpdateNamespaceResponse, - service="cloud", - ) - self.update_namespace_export_sink = client._new_call( - "update_namespace_export_sink", - clv1.UpdateNamespaceExportSinkRequest, - clv1.UpdateNamespaceExportSinkResponse, - service="cloud", - ) - self.update_namespace_tags = client._new_call( - "update_namespace_tags", - clv1.UpdateNamespaceTagsRequest, - clv1.UpdateNamespaceTagsResponse, - service="cloud", - ) - self.update_nexus_endpoint = client._new_call( - "update_nexus_endpoint", - clv1.UpdateNexusEndpointRequest, - clv1.UpdateNexusEndpointResponse, - service="cloud", - ) - self.update_service_account = client._new_call( - "update_service_account", - clv1.UpdateServiceAccountRequest, - clv1.UpdateServiceAccountResponse, - service="cloud", - ) - self.update_user_group = client._new_call( - "update_user_group", - clv1.UpdateUserGroupRequest, - clv1.UpdateUserGroupResponse, - service="cloud", - ) - self.update_user = client._new_call( - "update_user", - clv1.UpdateUserRequest, - clv1.UpdateUserResponse, - service="cloud", - ) - self.validate_namespace_export_sink = client._new_call( - "validate_namespace_export_sink", - clv1.ValidateNamespaceExportSinkRequest, - clv1.ValidateNamespaceExportSinkResponse, - service="cloud", - ) - -class TestService: +class TestService(temporalio.bridge.services_generated.TestService): """Client to the Temporal test server's test service.""" - def __init__(self, client: ServiceClient) -> None: - """Initialize the test service.""" - tsv1 = temporalio.api.testservice.v1 - self.get_current_time = client._new_call( - "get_current_time", - google.protobuf.empty_pb2.Empty, - tsv1.GetCurrentTimeResponse, - service="test", - ) - self.lock_time_skipping = client._new_call( - "lock_time_skipping", - tsv1.LockTimeSkippingRequest, - tsv1.LockTimeSkippingResponse, - service="test", - ) - self.sleep_until = client._new_call( - "sleep_until", - tsv1.SleepUntilRequest, - tsv1.SleepResponse, - service="test", - ) - self.sleep = client._new_call( - "sleep", - tsv1.SleepRequest, - tsv1.SleepResponse, - service="test", - ) - self.unlock_time_skipping_with_sleep = client._new_call( - "unlock_time_skipping_with_sleep", - tsv1.SleepRequest, - tsv1.SleepResponse, - service="test", - ) - self.unlock_time_skipping = client._new_call( - "unlock_time_skipping", - tsv1.UnlockTimeSkippingRequest, - tsv1.UnlockTimeSkippingResponse, - service="test", - ) - -class ServiceCall(Generic[ServiceRequest, ServiceResponse]): - """Callable RPC method for services.""" - - def __init__( - self, - service_client: ServiceClient, - name: str, - req_type: Type[ServiceRequest], - resp_type: Type[ServiceResponse], - service: str, - ) -> None: - """Initialize the service call.""" - self.service_client = service_client - self.name = name - self.req_type = req_type - self.resp_type = resp_type - self.service = service - - async def __call__( - self, - req: ServiceRequest, - *, - retry: bool = False, - metadata: Mapping[str, str] = {}, - timeout: Optional[timedelta] = None, - ) -> ServiceResponse: - """Invoke underlying client with the given request. - - Args: - req: Request for the call. - retry: If true, will use retry config to retry failed calls. - metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - timeout: Optional RPC deadline to set for the RPC call. - - Returns: - RPC response. - - Raises: - RPCError: Any RPC error that occurs during the call. - """ - return await self.service_client._rpc_call( - self.name, - req, - self.resp_type, - service=self.service, - retry=retry, - metadata=metadata, - timeout=timeout, - ) +class HealthService(temporalio.bridge.services_generated.HealthService): + """Client to the Temporal server's health service.""" class _BridgeServiceClient(ServiceClient): @@ -1298,10 +416,16 @@ async def connect(config: ConnectConfig) -> _BridgeServiceClient: def __init__(self, config: ConnectConfig) -> None: super().__init__(config) self._bridge_config = config._to_bridge_config() - self._bridge_client: Optional[temporalio.bridge.client.Client] = None + self._bridge_client: temporalio.bridge.client.Client | None = None self._bridge_client_connect_lock = asyncio.Lock() async def _connected_client(self) -> temporalio.bridge.client.Client: + # Fast path avoids touching the lock once connected. This keeps the + # lock off the per-RPC hot path so it never binds to (or is contended + # across) an event loop, letting a connected client be reused from any + # loop. + if self._bridge_client is not None: + return self._bridge_client async with self._bridge_client_connect_lock: if not self._bridge_client: runtime = self.config.runtime or temporalio.runtime.Runtime.default() @@ -1316,7 +440,7 @@ def worker_service_client(self) -> _BridgeServiceClient: """Underlying service client.""" return self - def update_rpc_metadata(self, metadata: Mapping[str, str]) -> None: + def update_rpc_metadata(self, metadata: Mapping[str, str | bytes]) -> None: """Update Core client metadata.""" # Mutate the bridge config and then only mutate the running client # metadata if already connected @@ -1324,7 +448,7 @@ def update_rpc_metadata(self, metadata: Mapping[str, str]) -> None: if self._bridge_client: self._bridge_client.update_metadata(metadata) - def update_api_key(self, api_key: Optional[str]) -> None: + def update_api_key(self, api_key: str | None) -> None: """Update Core client API key.""" # Mutate the bridge config and then only mutate the running client # metadata if already connected @@ -1336,12 +460,12 @@ async def _rpc_call( self, rpc: str, req: google.protobuf.message.Message, - resp_type: Type[ServiceResponse], + resp_type: type[ServiceResponse], *, service: str, retry: bool, - metadata: Mapping[str, str], - timeout: Optional[timedelta], + metadata: Mapping[str, str | bytes], + timeout: timedelta | None, ) -> ServiceResponse: global LOG_PROTOS if LOG_PROTOS: @@ -1360,7 +484,7 @@ async def _rpc_call( if LOG_PROTOS: logger.debug("Service %s response from %s: %s", service, rpc, resp) return resp - except temporalio.bridge.client.RPCError as err: + except BridgeRPCError as err: # Intentionally swallowing the cause instead of using "from" status, message, details = err.args raise RPCError(message, RPCStatusCode(status), details) @@ -1399,7 +523,7 @@ def __init__( self._message = message self._status = status self._raw_grpc_status = raw_grpc_status - self._grpc_status: Optional[temporalio.api.common.v1.GrpcStatus] = None + self._grpc_status: temporalio.api.common.v1.GrpcStatus | None = None @property def message(self) -> str: diff --git a/temporalio/testing/_activity.py b/temporalio/testing/_activity.py index d1441b012..6f0a9ab5d 100644 --- a/temporalio/testing/_activity.py +++ b/temporalio/testing/_activity.py @@ -5,9 +5,10 @@ import asyncio import inspect import threading +from collections.abc import Callable from contextlib import contextmanager from datetime import datetime, timedelta, timezone -from typing import Any, Callable, Optional, Set, TypeVar +from typing import Any, TypeVar from typing_extensions import ParamSpec @@ -21,28 +22,6 @@ _Params = ParamSpec("_Params") _Return = TypeVar("_Return") -_utc_zero = datetime.fromtimestamp(0).replace(tzinfo=timezone.utc) -_default_info = temporalio.activity.Info( - activity_id="test", - activity_type="unknown", - attempt=1, - current_attempt_scheduled_time=_utc_zero, - heartbeat_details=[], - heartbeat_timeout=None, - is_local=False, - schedule_to_close_timeout=timedelta(seconds=1), - scheduled_time=_utc_zero, - start_to_close_timeout=timedelta(seconds=1), - started_time=_utc_zero, - task_queue="test", - task_token=b"test", - workflow_id="test", - workflow_namespace="default", - workflow_run_id="test-run", - workflow_type="test", - priority=temporalio.common.Priority.default, -) - class ActivityEnvironment: """Activity environment for testing activities. @@ -53,7 +32,8 @@ class ActivityEnvironment: Attributes: info: The info that is returned from :py:func:`temporalio.activity.info` - function. + function. To customize, use :py:meth:`default_info` with + :py:func:`dataclasses.replace` to modify fields. on_heartbeat: Function called on each heartbeat invocation by the activity. payload_converter: Payload converter set on the activity context. This @@ -64,9 +44,9 @@ class ActivityEnvironment: take effect. Default is noop. """ - def __init__(self, client: Optional[Client] = None) -> None: + def __init__(self, client: Client | None = None) -> None: """Create an ActivityEnvironment for running activity code.""" - self.info = _default_info + self.info = ActivityEnvironment.default_info() self.on_heartbeat: Callable[..., None] = lambda *args: None self.payload_converter = ( temporalio.converter.DataConverter.default.payload_converter @@ -74,12 +54,44 @@ def __init__(self, client: Optional[Client] = None) -> None: self.metric_meter = temporalio.common.MetricMeter.noop self._cancelled = False self._worker_shutdown = False - self._activities: Set[_Activity] = set() + self._activities: set[_Activity] = set() self._client = client self._cancellation_details = ( temporalio.activity._ActivityCancellationDetailsHolder() ) + @staticmethod + def default_info() -> temporalio.activity.Info: + """Get the default activity info used for testing. + + Returns a new default Info instance that can be modified using + :py:func:`dataclasses.replace` before assigning to the info attribute. + """ + utc_zero = datetime.fromtimestamp(0).replace(tzinfo=timezone.utc) + return temporalio.activity.Info( + activity_id="test", + activity_type="unknown", + attempt=1, + current_attempt_scheduled_time=utc_zero, + heartbeat_details=[], + heartbeat_timeout=None, + is_local=False, + namespace="default", + schedule_to_close_timeout=timedelta(seconds=1), + scheduled_time=utc_zero, + start_to_close_timeout=timedelta(seconds=1), + started_time=utc_zero, + task_queue="test", + task_token=b"test", + workflow_id="test", + workflow_namespace="default", + workflow_run_id="test-run", + workflow_type="test", + priority=temporalio.common.Priority.default, + retry_policy=None, + activity_run_id=None, + ) + def cancel( self, cancellation_details: temporalio.activity.ActivityCancellationDetails = temporalio.activity.ActivityCancellationDetails( @@ -138,16 +150,16 @@ def __init__( self, env: ActivityEnvironment, fn: Callable, - client: Optional[Client], + client: Client | None, ) -> None: self.env = env self.fn = fn self.is_async = inspect.iscoroutinefunction(fn) or inspect.iscoroutinefunction( fn.__call__ # type: ignore ) - self.cancel_thread_raiser: Optional[ + self.cancel_thread_raiser: None | ( temporalio.worker._activity._ThreadExceptionRaiser - ] = None + ) = None if not self.is_async: # If there is a definition and they disable thread raising, don't # set @@ -160,11 +172,11 @@ def __init__( self.context = temporalio.activity._Context( info=lambda: env.info, heartbeat=lambda *args: env.on_heartbeat(*args), - cancelled_event=temporalio.activity._CompositeEvent( + cancelled_event=temporalio.common._CompositeEvent( thread_event=threading.Event(), async_event=asyncio.Event() if self.is_async else None, ), - worker_shutdown_event=temporalio.activity._CompositeEvent( + worker_shutdown_event=temporalio.common._CompositeEvent( thread_event=threading.Event(), async_event=asyncio.Event() if self.is_async else None, ), @@ -178,9 +190,9 @@ def __init__( client=client if self.is_async else None, cancellation_details=env._cancellation_details, ) - self.task: Optional[asyncio.Task] = None + self.task: asyncio.Task | None = None - def run(self, *args, **kwargs) -> Any: + def run(self, *args: Any, **kwargs: Any) -> Any: if self.cancel_thread_raiser: thread_id = threading.current_thread().ident if thread_id is not None: diff --git a/temporalio/testing/_workflow.py b/temporalio/testing/_workflow.py index d0eda5580..3ab07e5aa 100644 --- a/temporalio/testing/_workflow.py +++ b/temporalio/testing/_workflow.py @@ -4,23 +4,19 @@ import asyncio import logging +from collections.abc import AsyncIterator, Iterator, Mapping, Sequence from contextlib import asynccontextmanager, contextmanager from datetime import datetime, timedelta, timezone from typing import ( Any, - AsyncIterator, - Iterator, - List, - Mapping, - Optional, - Sequence, - Type, - Union, cast, ) import google.protobuf.empty_pb2 +from typing_extensions import Self +import temporalio.api.nexus.v1 +import temporalio.api.operatorservice.v1 import temporalio.api.testservice.v1 import temporalio.bridge.testing import temporalio.client @@ -29,7 +25,6 @@ import temporalio.exceptions import temporalio.runtime import temporalio.service -import temporalio.types import temporalio.worker logger = logging.getLogger(__name__) @@ -54,8 +49,8 @@ class WorkflowEnvironment: to have ``assert`` failures fail the workflow with the assertion error. """ - @staticmethod - def from_client(client: temporalio.client.Client) -> WorkflowEnvironment: + @classmethod + def from_client(cls, client: temporalio.client.Client) -> Self: """Create a workflow environment from the given client. :py:attr:`supports_time_skipping` will always return ``False`` for this @@ -69,37 +64,36 @@ def from_client(client: temporalio.client.Client) -> WorkflowEnvironment: The workflow environment that runs against the given client. """ # Add the assertion interceptor - return WorkflowEnvironment( - _client_with_interceptors(client, _AssertionErrorInterceptor()) - ) + return cls(_client_with_interceptors(client, _AssertionErrorInterceptor())) - @staticmethod + @classmethod async def start_local( + cls, *, namespace: str = "default", data_converter: temporalio.converter.DataConverter = temporalio.converter.DataConverter.default, interceptors: Sequence[temporalio.client.Interceptor] = [], plugins: Sequence[temporalio.client.Plugin] = [], - default_workflow_query_reject_condition: Optional[ - temporalio.common.QueryRejectCondition - ] = None, - retry_config: Optional[temporalio.client.RetryConfig] = None, - rpc_metadata: Mapping[str, str] = {}, - identity: Optional[str] = None, - tls: bool | temporalio.client.TLSConfig = False, + default_workflow_query_reject_condition: None + | (temporalio.common.QueryRejectCondition) = None, + retry_config: temporalio.service.RetryConfig | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + identity: str | None = None, + tls: bool | temporalio.service.TLSConfig = False, ip: str = "127.0.0.1", - port: Optional[int] = None, - download_dest_dir: Optional[str] = None, + port: int | None = None, + download_dest_dir: str | None = None, ui: bool = False, - runtime: Optional[temporalio.runtime.Runtime] = None, + runtime: temporalio.runtime.Runtime | None = None, search_attributes: Sequence[temporalio.common.SearchAttributeKey] = (), - dev_server_existing_path: Optional[str] = None, - dev_server_database_filename: Optional[str] = None, + dev_server_existing_path: str | None = None, + dev_server_database_filename: str | None = None, dev_server_log_format: str = "pretty", - dev_server_log_level: Optional[str] = "warn", + dev_server_log_level: str | None = "warn", dev_server_download_version: str = "default", dev_server_extra_args: Sequence[str] = [], - dev_server_download_ttl: Optional[timedelta] = None, + dev_server_download_ttl: timedelta | None = None, + ui_port: int | None = None, ) -> WorkflowEnvironment: """Start a full Temporal server locally, downloading if necessary. @@ -158,6 +152,7 @@ async def start_local( dev_server_extra_args: Extra arguments for the CLI binary. dev_server_download_ttl: TTL for the downloaded CLI binary. If unset, it will be cached indefinitely. + ui_port: UI port to use if UI is enabled. Returns: The started CLI dev server workflow environment. @@ -182,6 +177,7 @@ async def start_local( new_args.append(f"{attr.name}={attr._metadata_type}") new_args += dev_server_extra_args dev_server_extra_args = new_args + # Start CLI dev server runtime = runtime or temporalio.runtime.Runtime.default() download_ttl_ms = None @@ -200,12 +196,14 @@ async def start_local( port=port, database_filename=dev_server_database_filename, ui=ui, + ui_port=ui_port, log_format=dev_server_log_format, log_level=dev_server_log_level, extra_args=dev_server_extra_args, download_ttl_ms=download_ttl_ms, ), ) + # If we can't connect to the server, we should shut it down try: return _EphemeralServerWorkflowEnvironment( @@ -228,31 +226,31 @@ async def start_local( try: await server.shutdown() except: - logger.warn( + logger.warning( "Failed stopping local server on client connection failure", exc_info=True, ) raise - @staticmethod + @classmethod async def start_time_skipping( + cls, *, data_converter: temporalio.converter.DataConverter = temporalio.converter.DataConverter.default, interceptors: Sequence[temporalio.client.Interceptor] = [], plugins: Sequence[temporalio.client.Plugin] = [], - default_workflow_query_reject_condition: Optional[ - temporalio.common.QueryRejectCondition - ] = None, - retry_config: Optional[temporalio.client.RetryConfig] = None, - rpc_metadata: Mapping[str, str] = {}, - identity: Optional[str] = None, - port: Optional[int] = None, - download_dest_dir: Optional[str] = None, - runtime: Optional[temporalio.runtime.Runtime] = None, - test_server_existing_path: Optional[str] = None, + default_workflow_query_reject_condition: None + | (temporalio.common.QueryRejectCondition) = None, + retry_config: temporalio.service.RetryConfig | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + identity: str | None = None, + port: int | None = None, + download_dest_dir: str | None = None, + runtime: temporalio.runtime.Runtime | None = None, + test_server_existing_path: str | None = None, test_server_download_version: str = "default", test_server_extra_args: Sequence[str] = [], - test_server_download_ttl: Optional[timedelta] = None, + test_server_download_ttl: timedelta | None = None, ) -> WorkflowEnvironment: """Start a time skipping workflow environment. @@ -348,7 +346,7 @@ async def start_time_skipping( try: await server.shutdown() except: - logger.warn( + logger.warning( "Failed stopping test server on client connection failure", exc_info=True, ) @@ -357,7 +355,8 @@ async def start_time_skipping( def __init__(self, client: temporalio.client.Client) -> None: """Create a workflow environment from a client. - Most users would use a static method instead. + Most users would use a factory methods instead. + """ self._client = client @@ -365,7 +364,7 @@ async def __aenter__(self) -> WorkflowEnvironment: """Noop for ``async with`` support.""" return self - async def __aexit__(self, *args) -> None: + async def __aexit__(self, *args: Any) -> None: """For ``async with`` support to just call :py:meth:`shutdown`.""" await self.shutdown() @@ -374,11 +373,32 @@ def client(self) -> temporalio.client.Client: """Client to this environment.""" return self._client + async def connect_client(self, **kwargs: Any) -> temporalio.client.Client: + """Create another client connected to this environment. + + Namespace and connection credentials from this environment's client are + used by default. + Keyword arguments are forwarded to :py:meth:`temporalio.client.Client.connect` + and override those defaults. + """ + config = self.client.service_client.config + connect_kwargs: dict[str, Any] = { + "namespace": self.client.namespace, + "api_key": config.api_key, + "tls": config.tls, + "rpc_metadata": config.rpc_metadata, + "runtime": config.runtime or temporalio.runtime.Runtime.default(), + } + connect_kwargs.update(kwargs) + return await temporalio.client.Client.connect( + config.target_host, **connect_kwargs + ) + async def shutdown(self) -> None: """Shut down this environment.""" pass - async def sleep(self, duration: Union[timedelta, float]) -> None: + async def sleep(self, duration: timedelta | float) -> None: """Sleep in this environment. This awaits a regular :py:func:`asyncio.sleep` in regular environments, @@ -404,6 +424,48 @@ def supports_time_skipping(self) -> bool: """Whether this environment supports time skipping.""" return False + async def create_nexus_endpoint( + self, endpoint_name: str, task_queue: str + ) -> temporalio.api.nexus.v1.Endpoint: + """Create a Nexus endpoint with the given name and task queue. + + Args: + endpoint_name: The name of the Nexus endpoint to create. + task_queue: The task queue to associate with the endpoint. + + Returns: + The created Nexus endpoint. + """ + response = await self._client.operator_service.create_nexus_endpoint( + temporalio.api.operatorservice.v1.CreateNexusEndpointRequest( + spec=temporalio.api.nexus.v1.EndpointSpec( + name=endpoint_name, + target=temporalio.api.nexus.v1.EndpointTarget( + worker=temporalio.api.nexus.v1.EndpointTarget.Worker( + namespace=self._client.namespace, + task_queue=task_queue, + ) + ), + ) + ) + ) + return response.endpoint + + async def delete_nexus_endpoint( + self, endpoint: temporalio.api.nexus.v1.Endpoint + ) -> None: + """Delete a Nexus endpoint. + + Args: + endpoint: The Nexus endpoint to delete. + """ + await self._client.operator_service.delete_nexus_endpoint( + temporalio.api.operatorservice.v1.DeleteNexusEndpointRequest( + id=endpoint.id, + version=endpoint.version, + ) + ) + @contextmanager def auto_time_skipping_disabled(self) -> Iterator[None]: """Disable any automatic time skipping if this is a time-skipping @@ -430,7 +492,7 @@ def __init__( # Add assertion interceptor to client and if time skipping is supported, # add time skipping interceptor self._supports_time_skipping = server.has_test_service - interceptors: List[temporalio.client.Interceptor] = [ + interceptors: list[temporalio.client.Interceptor] = [ _AssertionErrorInterceptor() ] if self._supports_time_skipping: @@ -442,7 +504,7 @@ def __init__( async def shutdown(self) -> None: await self._server.shutdown() - async def sleep(self, duration: Union[timedelta, float]) -> None: + async def sleep(self, duration: timedelta | float) -> None: # Use regular sleep if no time skipping if not self._supports_time_skipping: return await super().sleep(duration) @@ -508,7 +570,7 @@ class _AssertionErrorInterceptor( ): def workflow_interceptor_class( self, input: temporalio.worker.WorkflowInterceptorClassInput - ) -> Optional[Type[temporalio.worker.WorkflowInboundInterceptor]]: + ) -> type[temporalio.worker.WorkflowInboundInterceptor] | None: return _AssertionErrorWorkflowInboundInterceptor @@ -573,8 +635,8 @@ async def result( self, *, follow_runs: bool = True, - rpc_metadata: Mapping[str, str] = {}, - rpc_timeout: Optional[timedelta] = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, ) -> Any: async with self.env.time_skipping_unlocked(): return await super().result( diff --git a/temporalio/types.py b/temporalio/types.py index 331c9596e..f90863d3e 100644 --- a/temporalio/types.py +++ b/temporalio/types.py @@ -1,24 +1,26 @@ """Advanced types.""" -from typing import Any, Awaitable, Callable, Type, TypeVar, Union +from collections.abc import Awaitable, Callable +from typing import Any, TypeVar from typing_extensions import ParamSpec, Protocol AnyType = TypeVar("AnyType") -ClassType = TypeVar("ClassType", bound=Type) +ClassType = TypeVar("ClassType", bound=type) SelfType = TypeVar("SelfType") ParamType = TypeVar("ParamType") ReturnType = TypeVar("ReturnType", covariant=True) LocalReturnType = TypeVar("LocalReturnType", covariant=True) +NexusServiceType = TypeVar("NexusServiceType") CallableType = TypeVar("CallableType", bound=Callable[..., Any]) CallableAsyncType = TypeVar("CallableAsyncType", bound=Callable[..., Awaitable[Any]]) CallableSyncOrAsyncType = TypeVar( "CallableSyncOrAsyncType", - bound=Callable[..., Union[Any, Awaitable[Any]]], + bound=Callable[..., Any | Awaitable[Any]], ) CallableSyncOrAsyncReturnNoneType = TypeVar( "CallableSyncOrAsyncReturnNoneType", - bound=Callable[..., Union[None, Awaitable[None]]], + bound=Callable[..., None | Awaitable[None]], ) MultiParamSpec = ParamSpec("MultiParamSpec") @@ -105,7 +107,7 @@ class MethodSyncOrAsyncNoParam(Protocol[ProtocolSelfType, ProtocolReturnType]): def __call__( self, __self: ProtocolSelfType - ) -> Union[ProtocolReturnType, Awaitable[ProtocolReturnType]]: + ) -> ProtocolReturnType | Awaitable[ProtocolReturnType]: """Generic callable type callback.""" ... @@ -117,7 +119,7 @@ class MethodSyncOrAsyncSingleParam( def __call__( self, __self: ProtocolSelfType, __param: ProtocolParamType, / - ) -> Union[ProtocolReturnType, Awaitable[ProtocolReturnType]]: + ) -> ProtocolReturnType | Awaitable[ProtocolReturnType]: """Generic callable type callback.""" ... diff --git a/temporalio/worker/__init__.py b/temporalio/worker/__init__.py index 08686dcb3..4f6efe68c 100644 --- a/temporalio/worker/__init__.py +++ b/temporalio/worker/__init__.py @@ -1,16 +1,20 @@ """Worker for processing Temporal workflows and/or activities.""" +from ..common import WorkerDeploymentVersion from ._activity import SharedHeartbeatSender, SharedStateManager from ._interceptor import ( ActivityInboundInterceptor, ActivityOutboundInterceptor, ContinueAsNewInput, ExecuteActivityInput, + ExecuteNexusOperationCancelInput, + ExecuteNexusOperationStartInput, ExecuteWorkflowInput, HandleQueryInput, HandleSignalInput, HandleUpdateInput, Interceptor, + NexusOperationInboundInterceptor, SignalChildWorkflowInput, SignalExternalWorkflowInput, StartActivityInput, @@ -33,6 +37,7 @@ CustomSlotSupplier, FixedSizeSlotSupplier, LocalActivitySlotInfo, + NexusSlotInfo, ResourceBasedSlotConfig, ResourceBasedSlotSupplier, ResourceBasedTunerConfig, @@ -51,9 +56,9 @@ Worker, WorkerConfig, WorkerDeploymentConfig, - WorkerDeploymentVersion, ) from ._workflow_instance import ( + PatchActivationInput, UnsandboxedWorkflowRunner, WorkflowInstance, WorkflowInstanceDetails, @@ -73,12 +78,14 @@ "PollerBehavior", "PollerBehaviorSimpleMaximum", "PollerBehaviorAutoscaling", + "PatchActivationInput", # Interceptor base classes "Interceptor", "ActivityInboundInterceptor", "ActivityOutboundInterceptor", "WorkflowInboundInterceptor", "WorkflowOutboundInterceptor", + "NexusOperationInboundInterceptor", "Plugin", # Interceptor input "ContinueAsNewInput", @@ -94,6 +101,8 @@ "StartLocalActivityInput", "StartNexusOperationInput", "WorkflowInterceptorClassInput", + "ExecuteNexusOperationStartInput", + "ExecuteNexusOperationCancelInput", # Advanced activity classes "SharedStateManager", "SharedHeartbeatSender", @@ -117,4 +126,5 @@ "SlotReleaseContext", "SlotReserveContext", "WorkflowSlotInfo", + "NexusSlotInfo", ] diff --git a/temporalio/worker/_activity.py b/temporalio/worker/_activity.py index 19e406e6f..ded3047fc 100644 --- a/temporalio/worker/_activity.py +++ b/temporalio/worker/_activity.py @@ -14,16 +14,13 @@ import threading import warnings from abc import ABC, abstractmethod -from collections.abc import Iterator, Sequence +from collections.abc import Callable, Iterator, Sequence from contextlib import contextmanager from dataclasses import dataclass, field from datetime import datetime, timedelta, timezone from typing import ( Any, - Callable, NoReturn, - Optional, - Union, ) import google.protobuf.duration_pb2 @@ -36,6 +33,11 @@ import temporalio.common import temporalio.converter import temporalio.exceptions +from temporalio.converter import ( + StorageDriverActivityInfo, + StorageDriverStoreContext, + StorageDriverWorkflowInfo, +) from ._interceptor import ( ActivityInboundInterceptor, @@ -54,8 +56,8 @@ def __init__( bridge_worker: Callable[[], temporalio.bridge.worker.Worker], task_queue: str, activities: Sequence[Callable], - activity_executor: Optional[concurrent.futures.Executor], - shared_state_manager: Optional[SharedStateManager], + activity_executor: concurrent.futures.Executor | None, + shared_state_manager: SharedStateManager | None, data_converter: temporalio.converter.DataConverter, interceptors: Sequence[Interceptor], metric_meter: temporalio.common.MetricMeter, @@ -73,15 +75,13 @@ def __init__( self._encode_headers = encode_headers self._fail_worker_exception_queue: asyncio.Queue[Exception] = asyncio.Queue() # Lazily created on first activity - self._worker_shutdown_event: Optional[temporalio.activity._CompositeEvent] = ( - None - ) + self._worker_shutdown_event: temporalio.common._CompositeEvent | None = None self._seen_sync_activity = False self._client = client # Validate and build activity dict self._activities: dict[str, temporalio.activity._Definition] = {} - self._dynamic_activity: Optional[temporalio.activity._Definition] = None + self._dynamic_activity: temporalio.activity._Definition | None = None for activity in activities: # Get definition defn = temporalio.activity._Definition.must_from_callable(activity) @@ -164,7 +164,6 @@ async def raise_from_exception_queue() -> NoReturn: ) self._running_activities[task.task_token] = activity elif task.HasField("cancel"): - # TODO(nexus-prerelease): does the task get removed from running_activities? self._handle_cancel_activity_task(task.task_token, task.cancel) else: raise RuntimeError(f"Unrecognized activity task: {task}") @@ -195,9 +194,6 @@ async def drain_poll_queue(self) -> None: # Only call this after run()/drain_poll_queue() have returned. This will not # raise an exception. - # TODO(nexus-preview): based on the comment above it looks like the intention may have been to use - # return_exceptions=True. Change this for nexus and activity and change call sites to consume entire - # stream and then raise first exception async def wait_all_completed(self) -> None: running_tasks = [v.task for v in self._running_activities.values() if v.task] if running_tasks: @@ -246,20 +242,42 @@ async def _heartbeat_async( task_token: bytes, ) -> None: # Drain the queue, only taking the last value to actually heartbeat - details: Optional[Sequence[Any]] = None + details: Sequence[Any] | None = None while not activity.pending_heartbeats.empty(): details = activity.pending_heartbeats.get_nowait() if details is None: return + data_converter = self._data_converter + if activity.info: + context = temporalio.converter.ActivitySerializationContext( + namespace=activity.info.namespace, + workflow_id=activity.info.workflow_id, + workflow_type=activity.info.workflow_type, + activity_type=activity.info.activity_type, + activity_id=activity.info.activity_id, + activity_task_queue=self._task_queue, + is_local=activity.info.is_local, + ) + data_converter = data_converter._with_contexts( + context, + StorageDriverStoreContext( + target=StorageDriverActivityInfo( + id=activity.info.activity_id, + type=activity.info.activity_type, + run_id=activity.info.activity_run_id, + namespace=activity.info.namespace, + ), + ), + ) + # Perform the heartbeat try: heartbeat = temporalio.bridge.proto.ActivityHeartbeat( # type: ignore[reportAttributeAccessIssue] task_token=task_token ) if details: - # Convert to core payloads - heartbeat.details.extend(await self._data_converter.encode(details)) + heartbeat.details.extend(await data_converter.encode(details)) logger.debug("Recording heartbeat with details %s", details) self._bridge_worker().record_activity_heartbeat(heartbeat) except Exception as err: @@ -293,9 +311,47 @@ async def _handle_start_activity_task( completion = temporalio.bridge.proto.ActivityTaskCompletion( # type: ignore[reportAttributeAccessIssue] task_token=task_token ) + # Create serialization context for the activity + context = temporalio.converter.ActivitySerializationContext( + namespace=start.workflow_namespace or self._client.namespace, + workflow_id=start.workflow_execution.workflow_id, + workflow_type=start.workflow_type, + activity_type=start.activity_type, + activity_id=start.activity_id, + activity_task_queue=self._task_queue, + is_local=start.is_local, + ) + data_converter = self._data_converter.with_context(context) + + # Build store context for external storage + ns = start.workflow_namespace or self._client.namespace + # Store context is set for the full activity task lifetime (input + # decode, execution, result/failure encode). Each activity task runs + # in its own coroutine so the value won't leak to other tasks. + started_by_workflow = bool(start.workflow_execution.workflow_id) + store_target: StorageDriverWorkflowInfo | StorageDriverActivityInfo + if started_by_workflow: + store_target = StorageDriverWorkflowInfo( + id=start.workflow_execution.workflow_id or None, + type=start.workflow_type or None, + run_id=start.workflow_execution.run_id or None, + namespace=ns, + ) + else: + store_target = StorageDriverActivityInfo( + id=start.activity_id or None, + type=start.activity_type or None, + run_id=start.run_id or None, + namespace=ns, + ) + data_converter = self._data_converter._with_contexts( + context, StorageDriverStoreContext(target=store_target) + ) try: - result = await self._execute_activity(start, running_activity, task_token) - [payload] = await self._data_converter.encode([result]) + result = await self._execute_activity( + start, running_activity, task_token, data_converter + ) + [payload] = await data_converter.encode([result]) completion.result.completed.result.CopyFrom(payload) except BaseException as err: try: @@ -305,7 +361,10 @@ async def _handle_start_activity_task( elif ( isinstance( err, - (asyncio.CancelledError, temporalio.exceptions.CancelledError), + ( + asyncio.CancelledError, + temporalio.exceptions.CancelledError, + ), ) and running_activity.cancelled_due_to_heartbeat_error ): @@ -313,13 +372,16 @@ async def _handle_start_activity_task( temporalio.activity.logger.warning( f"Completing as failure during heartbeat with error of type {type(err)}: {err}", ) - await self._data_converter.encode_failure( + await data_converter.encode_failure( err, completion.result.failed.failure ) elif ( isinstance( err, - (asyncio.CancelledError, temporalio.exceptions.CancelledError), + ( + asyncio.CancelledError, + temporalio.exceptions.CancelledError, + ), ) and running_activity.cancellation_details.details and running_activity.cancellation_details.details.paused @@ -327,7 +389,7 @@ async def _handle_start_activity_task( temporalio.activity.logger.warning( "Completing as failure due to unhandled cancel error produced by activity pause", ) - await self._data_converter.encode_failure( + await data_converter.encode_failure( temporalio.exceptions.ApplicationError( type="ActivityPause", message="Unhandled activity cancel error produced by activity pause", @@ -337,7 +399,10 @@ async def _handle_start_activity_task( elif ( isinstance( err, - (asyncio.CancelledError, temporalio.exceptions.CancelledError), + ( + asyncio.CancelledError, + temporalio.exceptions.CancelledError, + ), ) and running_activity.cancellation_details.details and running_activity.cancellation_details.details.reset @@ -345,7 +410,7 @@ async def _handle_start_activity_task( temporalio.activity.logger.warning( "Completing as failure due to unhandled cancel error produced by activity reset", ) - await self._data_converter.encode_failure( + await data_converter.encode_failure( temporalio.exceptions.ApplicationError( type="ActivityReset", message="Unhandled activity cancel error produced by activity reset", @@ -355,12 +420,15 @@ async def _handle_start_activity_task( elif ( isinstance( err, - (asyncio.CancelledError, temporalio.exceptions.CancelledError), + ( + asyncio.CancelledError, + temporalio.exceptions.CancelledError, + ), ) and running_activity.cancelled_by_request ): temporalio.activity.logger.debug("Completing as cancelled") - await self._data_converter.encode_failure( + await data_converter.encode_failure( # TODO(cretz): Should use some other message? temporalio.exceptions.CancelledError("Cancelled"), completion.result.cancelled.failure, @@ -386,7 +454,7 @@ async def _handle_start_activity_task( exc_info=True, extra={"__temporal_error_identifier": "ActivityFailure"}, ) - await self._data_converter.encode_failure( + await data_converter.encode_failure( err, completion.result.failed.failure ) # For broken executors, we have to fail the entire worker @@ -428,6 +496,7 @@ async def _execute_activity( start: temporalio.bridge.proto.activity_task.Start, # type: ignore[reportAttributeAccessIssue] running_activity: _RunningActivity, task_token: bytes, + data_converter: temporalio.converter.DataConverter, ) -> Any: """Invoke the user's activity function. @@ -445,7 +514,7 @@ async def _execute_activity( # Create the worker shutdown event if not created if not self._worker_shutdown_event: - self._worker_shutdown_event = temporalio.activity._CompositeEvent( + self._worker_shutdown_event = temporalio.common._CompositeEvent( thread_event=threading.Event(), async_event=asyncio.Event() ) @@ -458,7 +527,7 @@ async def _execute_activity( if isinstance( self._activity_executor, concurrent.futures.ThreadPoolExecutor ): - running_activity.cancelled_event = temporalio.activity._CompositeEvent( + running_activity.cancelled_event = temporalio.common._CompositeEvent( thread_event=threading.Event(), # No async event async_event=None, @@ -470,7 +539,7 @@ async def _execute_activity( manager = self._shared_state_manager # Pre-checked on worker init assert manager - running_activity.cancelled_event = temporalio.activity._CompositeEvent( + running_activity.cancelled_event = temporalio.common._CompositeEvent( thread_event=manager.new_event(), # No async event async_event=None, @@ -484,7 +553,7 @@ async def _execute_activity( self._seen_sync_activity = True else: # We have to set the async form of events - running_activity.cancelled_event = temporalio.activity._CompositeEvent( + running_activity.cancelled_event = temporalio.common._CompositeEvent( thread_event=threading.Event(), async_event=asyncio.Event(), ) @@ -501,9 +570,7 @@ async def _execute_activity( args = ( [] if not start.input - else await self._data_converter.decode( - start.input, type_hints=arg_types - ) + else await data_converter.decode(start.input, type_hints=arg_types) ) except Exception as err: raise temporalio.exceptions.ApplicationError( @@ -519,7 +586,7 @@ async def _execute_activity( heartbeat_details = ( [] if not start.heartbeat_details - else await self._data_converter.decode(start.heartbeat_details) + else await data_converter.decode(start.heartbeat_details) ) except Exception as err: raise temporalio.exceptions.ApplicationError( @@ -527,6 +594,7 @@ async def _execute_activity( ) from err # Build info + started_by_workflow = bool(start.workflow_execution.workflow_id) info = temporalio.activity.Info( activity_id=start.activity_id, activity_type=start.activity_type, @@ -539,6 +607,7 @@ async def _execute_activity( if start.HasField("heartbeat_timeout") else None, is_local=start.is_local, + namespace=start.workflow_namespace or self._client.namespace, schedule_to_close_timeout=_proto_to_non_zero_timedelta( start.schedule_to_close_timeout ) @@ -553,19 +622,24 @@ async def _execute_activity( started_time=_proto_to_datetime(start.started_time), task_queue=self._task_queue, task_token=task_token, - workflow_id=start.workflow_execution.workflow_id, - workflow_namespace=start.workflow_namespace, - workflow_run_id=start.workflow_execution.run_id, - workflow_type=start.workflow_type, + workflow_id=start.workflow_execution.workflow_id or None, + workflow_namespace=start.workflow_namespace or None, + workflow_run_id=start.workflow_execution.run_id or None, + workflow_type=start.workflow_type or None, priority=temporalio.common.Priority._from_proto(start.priority), + retry_policy=temporalio.common.RetryPolicy.from_proto(start.retry_policy) + if start.HasField("retry_policy") + else None, + activity_run_id=getattr(start, "run_id", None) + if not started_by_workflow + else None, ) - if self._encode_headers and self._data_converter.payload_codec is not None: + if self._encode_headers: for payload in start.header_fields.values(): - new_payload = ( - await self._data_converter.payload_codec.decode([payload]) - )[0] - payload.CopyFrom(new_payload) + payload.CopyFrom( + await data_converter._transform_inbound_payload(payload) + ) running_activity.info = info input = ExecuteActivityInput( @@ -588,7 +662,7 @@ async def _execute_activity( if not running_activity.cancel_thread_raiser else running_activity.cancel_thread_raiser.shielded ), - payload_converter_class_or_instance=self._data_converter.payload_converter, + payload_converter_class_or_instance=data_converter.payload_converter, runtime_metric_meter=None if sync_non_threaded else self._metric_meter, client=self._client if not running_activity.sync else None, cancellation_details=running_activity.cancellation_details, @@ -606,7 +680,7 @@ async def _execute_activity( impl.init(_ActivityOutboundImpl(self, running_activity.info)) return await impl.execute_activity(input) - def assert_activity_valid(self, activity) -> None: + def assert_activity_valid(self, activity: str) -> None: if self._dynamic_activity: return activity_def = self._activities.get(activity) @@ -622,15 +696,15 @@ def assert_activity_valid(self, activity) -> None: class _RunningActivity: pending_heartbeats: asyncio.Queue[Sequence[Any]] # Most of these optional values are set before use - info: Optional[temporalio.activity.Info] = None - task: Optional[asyncio.Task] = None - cancelled_event: Optional[temporalio.activity._CompositeEvent] = None - last_heartbeat_task: Optional[asyncio.Task] = None - cancel_thread_raiser: Optional[_ThreadExceptionRaiser] = None + info: temporalio.activity.Info | None = None + task: asyncio.Task | None = None + cancelled_event: temporalio.common._CompositeEvent | None = None + last_heartbeat_task: asyncio.Task | None = None + cancel_thread_raiser: _ThreadExceptionRaiser | None = None sync: bool = False done: bool = False cancelled_by_request: bool = False - cancelled_due_to_heartbeat_error: Optional[Exception] = None + cancelled_due_to_heartbeat_error: Exception | None = None cancellation_details: temporalio.activity._ActivityCancellationDetailsHolder = ( field(default_factory=temporalio.activity._ActivityCancellationDetailsHolder) ) @@ -639,7 +713,7 @@ def cancel( self, *, cancelled_by_request: bool = False, - cancelled_due_to_heartbeat_error: Optional[Exception] = None, + cancelled_due_to_heartbeat_error: Exception | None = None, ) -> None: self.cancelled_by_request = cancelled_by_request self.cancelled_due_to_heartbeat_error = cancelled_due_to_heartbeat_error @@ -653,21 +727,34 @@ def cancel( ) # If not sync and there's a task, cancel it if not self.sync and self.task: - # TODO(cretz): Check that Python >= 3.9 and set msg? - self.task.cancel() + self.task.cancel("Activity cancelled") class _ThreadExceptionRaiser: def __init__(self) -> None: self._lock = threading.Lock() - self._thread_id: Optional[int] = None - self._pending_exception: Optional[type[Exception]] = None + self._thread_id: int | None = None + self._pending_exception: type[Exception] | None = None self._shield_depth = 0 def set_thread_id(self, thread_id: int) -> None: with self._lock: self._thread_id = thread_id + @contextmanager + def active_thread(self) -> Iterator[None]: + thread_id = threading.current_thread().ident + if thread_id is not None: + self.set_thread_id(thread_id) + try: + yield None + finally: + if thread_id is not None: + with self._lock: + if self._thread_id == thread_id: + self._thread_id = None + self._pending_exception = None + def raise_in_thread(self, exc_type: type[Exception]) -> None: with self._lock: self._pending_exception = exc_type @@ -747,8 +834,8 @@ async def heartbeat_with_context(*details: Any) -> None: # For heartbeats, we use the existing heartbeat callable for thread # pool executors or a multiprocessing queue for others - heartbeat: Union[Callable[..., None], SharedHeartbeatSender] = ctx.heartbeat - shared_manager: Optional[SharedStateManager] = None + heartbeat: Callable[..., None] | SharedHeartbeatSender = ctx.heartbeat + shared_manager: SharedStateManager | None = None if not isinstance(input.executor, concurrent.futures.ThreadPoolExecutor): # Should always be present in worker, pre-checked on init shared_manager = self._worker._shared_state_manager @@ -818,24 +905,20 @@ def heartbeat(self, *details: Any) -> None: # This has to be defined at the top-level to be picklable for process executors def _execute_sync_activity( info: temporalio.activity.Info, - heartbeat: Union[Callable[..., None], SharedHeartbeatSender], + heartbeat: Callable[..., None] | SharedHeartbeatSender, # This is only set for threaded activities - cancel_thread_raiser: Optional[_ThreadExceptionRaiser], + cancel_thread_raiser: _ThreadExceptionRaiser | None, cancelled_event: threading.Event, worker_shutdown_event: threading.Event, - payload_converter_class_or_instance: Union[ - type[temporalio.converter.PayloadConverter], - temporalio.converter.PayloadConverter, - ], - runtime_metric_meter: Optional[temporalio.common.MetricMeter], + payload_converter_class_or_instance: ( + type[temporalio.converter.PayloadConverter] + | temporalio.converter.PayloadConverter + ), + runtime_metric_meter: temporalio.common.MetricMeter | None, cancellation_details: temporalio.activity._ActivityCancellationDetailsHolder, fn: Callable[..., Any], *args: Any, ) -> Any: - if cancel_thread_raiser: - thread_id = threading.current_thread().ident - if thread_id is not None: - cancel_thread_raiser.set_thread_id(thread_id) if isinstance(heartbeat, SharedHeartbeatSender): def heartbeat_fn(*details: Any) -> None: @@ -846,10 +929,10 @@ def heartbeat_fn(*details: Any) -> None: temporalio.activity._Context( info=lambda: info, heartbeat=heartbeat_fn, - cancelled_event=temporalio.activity._CompositeEvent( + cancelled_event=temporalio.common._CompositeEvent( thread_event=cancelled_event, async_event=None ), - worker_shutdown_event=temporalio.activity._CompositeEvent( + worker_shutdown_event=temporalio.common._CompositeEvent( thread_event=worker_shutdown_event, async_event=None ), shield_thread_cancel_exception=( @@ -861,7 +944,11 @@ def heartbeat_fn(*details: Any) -> None: cancellation_details=cancellation_details, ) ) - return fn(*args) + if not cancel_thread_raiser: + return fn(*args) + else: + with cancel_thread_raiser.active_thread(): + return fn(*args) class SharedStateManager(ABC): @@ -876,7 +963,7 @@ class SharedStateManager(ABC): @staticmethod def create_from_multiprocessing( mgr: multiprocessing.managers.SyncManager, - queue_poller_executor: Optional[concurrent.futures.Executor] = None, + queue_poller_executor: concurrent.futures.Executor | None = None, ) -> SharedStateManager: """Create a shared state manager from a multiprocessing manager. @@ -1032,7 +1119,7 @@ def _proto_to_datetime( def _proto_to_non_zero_timedelta( dur: google.protobuf.duration_pb2.Duration, -) -> Optional[timedelta]: +) -> timedelta | None: if dur.nanos == 0 and dur.seconds == 0: return None return dur.ToTimedelta() diff --git a/temporalio/worker/_command_aware_visitor.py b/temporalio/worker/_command_aware_visitor.py new file mode 100644 index 000000000..500fc4db5 --- /dev/null +++ b/temporalio/worker/_command_aware_visitor.py @@ -0,0 +1,194 @@ +"""Visitor that sets command context during payload traversal.""" + +import contextvars +from collections.abc import Iterator +from contextlib import contextmanager +from dataclasses import dataclass + +from temporalio.api.enums.v1.command_type_pb2 import CommandType +from temporalio.bridge._visitor import PayloadVisitor +from temporalio.bridge._visitor_functions import VisitorFunctions +from temporalio.bridge.proto.workflow_activation.workflow_activation_pb2 import ( + ResolveActivity, + ResolveChildWorkflowExecution, + ResolveChildWorkflowExecutionStart, + ResolveNexusOperation, + ResolveNexusOperationStart, + ResolveRequestCancelExternalWorkflow, + ResolveSignalExternalWorkflow, +) +from temporalio.bridge.proto.workflow_commands.workflow_commands_pb2 import ( + CompleteWorkflowExecution, + ScheduleActivity, + ScheduleLocalActivity, + ScheduleNexusOperation, + SignalExternalWorkflowExecution, + StartChildWorkflowExecution, +) + + +@dataclass(frozen=True) +class CommandInfo: + """Information identifying a specific command instance.""" + + command_type: CommandType.ValueType + command_seq: int + + +current_command_info: contextvars.ContextVar[CommandInfo | None] = ( + contextvars.ContextVar("current_command_info", default=None) +) + + +class CommandAwarePayloadVisitor(PayloadVisitor): + """Payload visitor that sets command context during traversal. + + Override methods are explicitly defined for workflow commands and + activation jobs that have both a 'seq' field and payloads to visit. + """ + + def __init__( + self, + *, + skip_search_attributes: bool = False, + skip_headers: bool = False, + concurrency_limit: int = 1, + ) -> None: + """Creates a new command-aware payload visitor. + + Args: + skip_search_attributes: If True, search attributes are not visited. + skip_headers: If True, headers are not visited. + concurrency_limit: Maximum number of payload visits that may run + concurrently during a single call to visit(). Defaults to 1. + """ + super().__init__( + skip_search_attributes=skip_search_attributes, + skip_headers=skip_headers, + concurrency_limit=concurrency_limit, + ) + + # Workflow commands with payloads + async def _visit_coresdk_workflow_commands_CompleteWorkflowExecution( + self, fs: VisitorFunctions, o: CompleteWorkflowExecution + ) -> None: + with current_command(CommandType.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, 0): + await super()._visit_coresdk_workflow_commands_CompleteWorkflowExecution( + fs, o + ) + + async def _visit_coresdk_workflow_commands_ScheduleActivity( + self, fs: VisitorFunctions, o: ScheduleActivity + ) -> None: + with current_command(CommandType.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, o.seq): + await super()._visit_coresdk_workflow_commands_ScheduleActivity(fs, o) + + async def _visit_coresdk_workflow_commands_ScheduleLocalActivity( + self, fs: VisitorFunctions, o: ScheduleLocalActivity + ) -> None: + with current_command(CommandType.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, o.seq): + await super()._visit_coresdk_workflow_commands_ScheduleLocalActivity(fs, o) + + async def _visit_coresdk_workflow_commands_StartChildWorkflowExecution( + self, fs: VisitorFunctions, o: StartChildWorkflowExecution + ) -> None: + with current_command( + CommandType.COMMAND_TYPE_START_CHILD_WORKFLOW_EXECUTION, o.seq + ): + await super()._visit_coresdk_workflow_commands_StartChildWorkflowExecution( + fs, o + ) + + async def _visit_coresdk_workflow_commands_SignalExternalWorkflowExecution( + self, fs: VisitorFunctions, o: SignalExternalWorkflowExecution + ) -> None: + with current_command( + CommandType.COMMAND_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION, o.seq + ): + await super()._visit_coresdk_workflow_commands_SignalExternalWorkflowExecution( + fs, o + ) + + async def _visit_coresdk_workflow_commands_ScheduleNexusOperation( + self, fs: VisitorFunctions, o: ScheduleNexusOperation + ) -> None: + with current_command(CommandType.COMMAND_TYPE_SCHEDULE_NEXUS_OPERATION, o.seq): + await super()._visit_coresdk_workflow_commands_ScheduleNexusOperation(fs, o) + + # Workflow activation jobs with payloads + async def _visit_coresdk_workflow_activation_ResolveActivity( + self, fs: VisitorFunctions, o: ResolveActivity + ) -> None: + with current_command(CommandType.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, o.seq): + await super()._visit_coresdk_workflow_activation_ResolveActivity(fs, o) + + async def _visit_coresdk_workflow_activation_ResolveChildWorkflowExecutionStart( + self, fs: VisitorFunctions, o: ResolveChildWorkflowExecutionStart + ) -> None: + with current_command( + CommandType.COMMAND_TYPE_START_CHILD_WORKFLOW_EXECUTION, o.seq + ): + await super()._visit_coresdk_workflow_activation_ResolveChildWorkflowExecutionStart( + fs, o + ) + + async def _visit_coresdk_workflow_activation_ResolveChildWorkflowExecution( + self, fs: VisitorFunctions, o: ResolveChildWorkflowExecution + ) -> None: + with current_command( + CommandType.COMMAND_TYPE_START_CHILD_WORKFLOW_EXECUTION, o.seq + ): + await super()._visit_coresdk_workflow_activation_ResolveChildWorkflowExecution( + fs, o + ) + + async def _visit_coresdk_workflow_activation_ResolveSignalExternalWorkflow( + self, fs: VisitorFunctions, o: ResolveSignalExternalWorkflow + ) -> None: + with current_command( + CommandType.COMMAND_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION, o.seq + ): + await super()._visit_coresdk_workflow_activation_ResolveSignalExternalWorkflow( + fs, o + ) + + async def _visit_coresdk_workflow_activation_ResolveRequestCancelExternalWorkflow( + self, fs: VisitorFunctions, o: ResolveRequestCancelExternalWorkflow + ) -> None: + with current_command( + CommandType.COMMAND_TYPE_REQUEST_CANCEL_EXTERNAL_WORKFLOW_EXECUTION, o.seq + ): + await super()._visit_coresdk_workflow_activation_ResolveRequestCancelExternalWorkflow( + fs, o + ) + + async def _visit_coresdk_workflow_activation_ResolveNexusOperationStart( + self, fs: VisitorFunctions, o: ResolveNexusOperationStart + ) -> None: + with current_command(CommandType.COMMAND_TYPE_SCHEDULE_NEXUS_OPERATION, o.seq): + await super()._visit_coresdk_workflow_activation_ResolveNexusOperationStart( + fs, o + ) + + async def _visit_coresdk_workflow_activation_ResolveNexusOperation( + self, fs: VisitorFunctions, o: ResolveNexusOperation + ) -> None: + with current_command(CommandType.COMMAND_TYPE_SCHEDULE_NEXUS_OPERATION, o.seq): + await super()._visit_coresdk_workflow_activation_ResolveNexusOperation( + fs, o + ) + + +@contextmanager +def current_command( + command_type: CommandType.ValueType, command_seq: int +) -> Iterator[None]: + """Context manager for setting command info.""" + token = current_command_info.set( + CommandInfo(command_type=command_type, command_seq=command_seq) + ) + try: + yield + finally: + if token: + current_command_info.reset(token) diff --git a/temporalio/worker/_debugger.py b/temporalio/worker/_debugger.py new file mode 100644 index 000000000..6f64da5c4 --- /dev/null +++ b/temporalio/worker/_debugger.py @@ -0,0 +1,158 @@ +from __future__ import annotations + +import dataclasses +import sys +from types import FrameType, TracebackType + +import temporalio.workflow +from temporalio.worker.workflow_sandbox._runner import SandboxedWorkflowRunner + +from ._workflow_instance import WorkflowRunner + +__all__ = [ + "_install_workflow_breakpoint_hook", + "_relax_sandbox_for_debugger", + "_temporal_workflow_breakpoint_hook", +] + +_ORIGINAL_BREAKPOINTHOOK = sys.breakpointhook + + +def _build_workflow_pdb_class() -> type: + """Build a Pdb subclass that suspends sandbox restrictions during the REPL. + + pdb's cmdloop touches ``readline.get_completer`` and other + sandbox-restricted internals each time it interacts with the user; we + bracket each interaction with ``_sandbox_unrestricted.value = True`` and + restore the previous value afterwards. Outside the REPL the sandbox + stays intact. + + ``pdb`` is imported lazily because it's a debug-only dependency that + pulls in ``cmd``/``bdb``/``linecache``; no reason to pay that cost at + worker import time. + """ + import pdb + + from temporalio.workflow._sandbox import _sandbox_unrestricted + + class _WorkflowPdb(pdb.Pdb): + # The `interaction` signature differs across Python versions: 3.10-3.12 + # typeshed names the second parameter `traceback: TracebackType | None`, + # while 3.13+ renames it `tb_or_exc` and widens the type to include + # `BaseException`. No single signature satisfies both stubs, so we + # suppress the override check. + def interaction( # type: ignore[override] + self, + frame: FrameType | None, + tb_or_exc: TracebackType | BaseException | None, + ) -> None: + prev = getattr(_sandbox_unrestricted, "value", False) + _sandbox_unrestricted.value = True + try: + super().interaction(frame, tb_or_exc) # type: ignore[arg-type] + finally: + _sandbox_unrestricted.value = prev + + # Override `q`/`quit`/`exit`/EOF (Ctrl-D) to behave like `continue`. + # Default pdb raises `BdbQuit`, which propagates as an uncaught + # exception out of workflow.run, fails the workflow task, and + # triggers a server retry storm during teardown. For a debug + # session the user almost always wants "stop debugging and let the + # workflow finish" — that's `continue`. Users who truly want to + # abort can Ctrl-C the outer shell. + def do_quit(self, arg: str) -> bool | None: + self.message( + "[Temporal] 'q'/Ctrl-D continues the workflow. " + "Ctrl-C the outer shell to abort." + ) + return self.do_continue(arg) + + do_q = do_exit = do_quit + do_EOF = do_quit + + return _WorkflowPdb + + +def _temporal_workflow_breakpoint_hook(*args: object, **kwargs: object) -> object: + """``sys.breakpointhook`` that handles ``breakpoint()`` inside workflows. + + Only installed when ``debug_mode`` is enabled on the Worker. From inside + a workflow activation: drops the user into a custom Pdb at the workflow's + own frame, with sandbox restrictions suspended during the REPL. From + anywhere else (test code, helpers, etc.): delegates to whatever hook was + previously installed. + """ + if not temporalio.workflow.in_workflow(): + # Not inside a workflow activation — let pytest's wrapper, ipdb, or + # whatever else is configured handle it. + return _ORIGINAL_BREAKPOINTHOOK(*args, **kwargs) + # Inside a workflow: drop the user into pdb at the caller's frame (the + # workflow's `run` method, where breakpoint() was actually written) rather + # than landing inside this hook. Bypassing the configured breakpoint hook + # also avoids pytest's pdb wrapper, which assumes a test-code context and + # touches sandbox-restricted internals during its terminal-writer setup. + # `sandbox_unrestricted()` lifts member checks for the duration of the + # REPL so pdb's own initialization (readline, etc.) isn't blocked. + # `skip` tells pdb not to stop in our hook frame or the contextlib + # plumbing — without it pdb's first step lands at the `with` teardown + # instead of the user's next workflow line. + caller_frame = sys._getframe(1) + with temporalio.workflow.unsafe.sandbox_unrestricted(): + pdb_cls = _build_workflow_pdb_class() + pdb_cls( + skip=[ + "temporalio.worker._debugger", + "temporalio.workflow._sandbox", + "contextlib", + ] + ).set_trace(caller_frame) + return None + + +def _install_workflow_breakpoint_hook() -> None: + """Set ``sys.breakpointhook`` to the workflow hook if it isn't already.""" + if sys.breakpointhook is not _temporal_workflow_breakpoint_hook: + sys.breakpointhook = _temporal_workflow_breakpoint_hook + + +def _relax_sandbox_for_debugger(workflow_runner: WorkflowRunner) -> WorkflowRunner: + """Allow ``breakpoint()`` past the sandbox so it can reach the worker hook. + + The sandbox flags ``breakpoint`` as non-deterministic by default; without + this relaxation the call raises before our breakpoint hook can run. + Once inside the hook, the hook itself enters ``sandbox_unrestricted()`` + for the duration of the debugger session, so pdb's internals (readline, + os.environ, etc.) aren't blocked either — without permanently dropping + sandbox checks for the rest of workflow execution. + """ + if not isinstance(workflow_runner, SandboxedWorkflowRunner): + return workflow_runner + + restrictions = workflow_runner.restrictions + invalid = restrictions.invalid_module_members + builtins_matcher = invalid.children.get("__builtins__") + if builtins_matcher is None: + return workflow_runner + + # `breakpoint` may sit either in `children` (as a leaf matcher with a + # custom error message) or in `use` (the legacy flat form). Strip from + # whichever shape is present. + has_child = "breakpoint" in builtins_matcher.children + has_use = "breakpoint" in builtins_matcher.use + if not (has_child or has_use): + return workflow_runner + + new_children = { + k: v for k, v in builtins_matcher.children.items() if k != "breakpoint" + } + new_use = set(builtins_matcher.use) - {"breakpoint"} + new_builtins = dataclasses.replace( + builtins_matcher, children=new_children, use=new_use + ) + new_invalid = dataclasses.replace( + invalid, children={**invalid.children, "__builtins__": new_builtins} + ) + new_restrictions = dataclasses.replace( + restrictions, invalid_module_members=new_invalid + ) + return dataclasses.replace(workflow_runner, restrictions=new_restrictions) diff --git a/temporalio/worker/_interceptor.py b/temporalio/worker/_interceptor.py index 7119b0665..4acf3c5d1 100644 --- a/temporalio/worker/_interceptor.py +++ b/temporalio/worker/_interceptor.py @@ -3,22 +3,16 @@ from __future__ import annotations import concurrent.futures -from collections.abc import Callable, Mapping, MutableMapping +from collections.abc import Awaitable, Callable, Mapping, MutableMapping, Sequence from dataclasses import dataclass from datetime import timedelta from typing import ( Any, - Awaitable, Generic, - List, NoReturn, - Optional, - Sequence, - Type, - Union, ) -import nexusrpc.handler +import nexusrpc from nexusrpc import InputT, OutputT import temporalio.activity @@ -27,7 +21,7 @@ import temporalio.nexus import temporalio.nexus._util import temporalio.workflow -from temporalio.workflow import VersioningIntent +from temporalio.workflow import ContinueAsNewVersioningBehavior, VersioningIntent class Interceptor: @@ -51,8 +45,9 @@ def intercept_activity( return next def workflow_interceptor_class( - self, input: WorkflowInterceptorClassInput - ) -> Optional[Type[WorkflowInboundInterceptor]]: + self, + input: WorkflowInterceptorClassInput, # type:ignore[reportUnusedParameter] + ) -> type[WorkflowInboundInterceptor] | None: """Class that will be instantiated and used to intercept workflows. This method is called on workflow start. The class must have the same @@ -68,6 +63,20 @@ def workflow_interceptor_class( """ return None + def intercept_nexus_operation( + self, next: NexusOperationInboundInterceptor + ) -> NexusOperationInboundInterceptor: + """Method called for intercepting a Nexus operation. + + Args: + next: The underlying inbound this interceptor + should delegate to. + + Returns: + The new interceptor that should be used for the Nexus operation. + """ + return next + @dataclass(frozen=True) class WorkflowInterceptorClassInput: @@ -91,7 +100,7 @@ class ExecuteActivityInput: fn: Callable[..., Any] args: Sequence[Any] - executor: Optional[concurrent.futures.Executor] + executor: concurrent.futures.Executor | None headers: Mapping[str, temporalio.api.common.v1.Payload] @@ -151,29 +160,29 @@ def heartbeat(self, *details: Any) -> None: class ContinueAsNewInput: """Input for :py:meth:`WorkflowOutboundInterceptor.continue_as_new`.""" - workflow: Optional[str] + workflow: str | None args: Sequence[Any] - task_queue: Optional[str] - run_timeout: Optional[timedelta] - task_timeout: Optional[timedelta] - retry_policy: Optional[temporalio.common.RetryPolicy] - memo: Optional[Mapping[str, Any]] - search_attributes: Optional[ - Union[ - temporalio.common.SearchAttributes, temporalio.common.TypedSearchAttributes - ] - ] + task_queue: str | None + run_timeout: timedelta | None + task_timeout: timedelta | None + backoff_start_interval: timedelta | None + retry_policy: temporalio.common.RetryPolicy | None + memo: Mapping[str, Any] | None + search_attributes: None | ( + temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes + ) headers: Mapping[str, temporalio.api.common.v1.Payload] - versioning_intent: Optional[VersioningIntent] + versioning_intent: VersioningIntent | None + initial_versioning_behavior: ContinueAsNewVersioningBehavior | None # The types may be absent - arg_types: Optional[List[Type]] + arg_types: list[type] | None @dataclass class ExecuteWorkflowInput: """Input for :py:meth:`WorkflowInboundInterceptor.execute_workflow`.""" - type: Type + type: type # Note, this is an unbound method run_fn: Callable[..., Awaitable[Any]] args: Sequence[Any] @@ -229,7 +238,7 @@ class SignalExternalWorkflowInput: args: Sequence[Any] namespace: str workflow_id: str - workflow_run_id: Optional[str] + workflow_run_id: str | None headers: Mapping[str, temporalio.api.common.v1.Payload] @@ -239,22 +248,22 @@ class StartActivityInput: activity: str args: Sequence[Any] - activity_id: Optional[str] - task_queue: Optional[str] - schedule_to_close_timeout: Optional[timedelta] - schedule_to_start_timeout: Optional[timedelta] - start_to_close_timeout: Optional[timedelta] - heartbeat_timeout: Optional[timedelta] - retry_policy: Optional[temporalio.common.RetryPolicy] + activity_id: str | None + task_queue: str | None + schedule_to_close_timeout: timedelta | None + schedule_to_start_timeout: timedelta | None + start_to_close_timeout: timedelta | None + heartbeat_timeout: timedelta | None + retry_policy: temporalio.common.RetryPolicy | None cancellation_type: temporalio.workflow.ActivityCancellationType headers: Mapping[str, temporalio.api.common.v1.Payload] disable_eager_execution: bool - versioning_intent: Optional[VersioningIntent] - summary: Optional[str] + versioning_intent: VersioningIntent | None + summary: str | None priority: temporalio.common.Priority # The types may be absent - arg_types: Optional[List[Type]] - ret_type: Optional[Type] + arg_types: list[type] | None + ret_type: type | None @dataclass @@ -264,29 +273,27 @@ class StartChildWorkflowInput: workflow: str args: Sequence[Any] id: str - task_queue: Optional[str] + task_queue: str | None cancellation_type: temporalio.workflow.ChildWorkflowCancellationType parent_close_policy: temporalio.workflow.ParentClosePolicy - execution_timeout: Optional[timedelta] - run_timeout: Optional[timedelta] - task_timeout: Optional[timedelta] + execution_timeout: timedelta | None + run_timeout: timedelta | None + task_timeout: timedelta | None id_reuse_policy: temporalio.common.WorkflowIDReusePolicy - retry_policy: Optional[temporalio.common.RetryPolicy] + retry_policy: temporalio.common.RetryPolicy | None cron_schedule: str - memo: Optional[Mapping[str, Any]] - search_attributes: Optional[ - Union[ - temporalio.common.SearchAttributes, temporalio.common.TypedSearchAttributes - ] - ] + memo: Mapping[str, Any] | None + search_attributes: None | ( + temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes + ) headers: Mapping[str, temporalio.api.common.v1.Payload] - versioning_intent: Optional[VersioningIntent] - static_summary: Optional[str] - static_details: Optional[str] + versioning_intent: VersioningIntent | None + static_summary: str | None + static_details: str | None priority: temporalio.common.Priority # The types may be absent - arg_types: Optional[List[Type]] - ret_type: Optional[Type] + arg_types: list[type] | None + ret_type: type | None @dataclass @@ -295,12 +302,15 @@ class StartNexusOperationInput(Generic[InputT, OutputT]): endpoint: str service: str - operation: Union[nexusrpc.Operation[InputT, OutputT], str, Callable[..., Any]] + operation: nexusrpc.Operation[InputT, OutputT] | str | Callable[..., Any] input: InputT - schedule_to_close_timeout: Optional[timedelta] + schedule_to_close_timeout: timedelta | None + schedule_to_start_timeout: timedelta | None + start_to_close_timeout: timedelta | None cancellation_type: temporalio.workflow.NexusOperationCancellationType - headers: Optional[Mapping[str, str]] - output_type: Optional[Type[OutputT]] = None + headers: Mapping[str, str] | None + summary: str | None + output_type: type[OutputT] | None = None def __post_init__(self) -> None: """Initialize operation-specific attributes after dataclass creation.""" @@ -344,19 +354,19 @@ class StartLocalActivityInput: activity: str args: Sequence[Any] - activity_id: Optional[str] - schedule_to_close_timeout: Optional[timedelta] - schedule_to_start_timeout: Optional[timedelta] - start_to_close_timeout: Optional[timedelta] - retry_policy: Optional[temporalio.common.RetryPolicy] - local_retry_threshold: Optional[timedelta] + activity_id: str | None + schedule_to_close_timeout: timedelta | None + schedule_to_start_timeout: timedelta | None + start_to_close_timeout: timedelta | None + retry_policy: temporalio.common.RetryPolicy | None + local_retry_threshold: timedelta | None cancellation_type: temporalio.workflow.ActivityCancellationType headers: Mapping[str, temporalio.api.common.v1.Payload] - summary: Optional[str] + summary: str | None # The types may be absent - arg_types: Optional[List[Type]] - ret_type: Optional[Type] + arg_types: list[type] | None + ret_type: type | None class WorkflowInboundInterceptor: @@ -468,5 +478,52 @@ def start_local_activity( async def start_nexus_operation( self, input: StartNexusOperationInput[InputT, OutputT] ) -> temporalio.workflow.NexusOperationHandle[OutputT]: - """Called for every :py:func:`temporalio.workflow.start_nexus_operation` call.""" + """Called for every :py:func:`temporalio.workflow.NexusClient.start_operation` call.""" return await self.next.start_nexus_operation(input) + + +@dataclass +class ExecuteNexusOperationStartInput: + """Input for :pyt:meth:`NexusOperationInboundInterceptor.start_operation""" + + ctx: nexusrpc.handler.StartOperationContext + input: Any + + +@dataclass +class ExecuteNexusOperationCancelInput: + """Input for :pyt:meth:`NexusOperationInboundInterceptor.cancel_operation""" + + ctx: nexusrpc.handler.CancelOperationContext + token: str + + +class NexusOperationInboundInterceptor: + """Inbound interceptor to wrap Nexus operation starting and cancelling. + + This should be extended by any Nexus operation inbound interceptors. + """ + + def __init__(self, next: NexusOperationInboundInterceptor) -> None: + """Create the inbound interceptor. + + Args: + next: The next interceptor in the chain. The default implementation + of all calls is to delegate to the next interceptor. + """ + self.next = next + + async def execute_nexus_operation_start( + self, input: ExecuteNexusOperationStartInput + ) -> ( + nexusrpc.handler.StartOperationResultSync[Any] + | nexusrpc.handler.StartOperationResultAsync + ): + """Called to start a Nexus operation""" + return await self.next.execute_nexus_operation_start(input) + + async def execute_nexus_operation_cancel( + self, input: ExecuteNexusOperationCancelInput + ) -> None: + """Called to cancel an in progress Nexus operation""" + return await self.next.execute_nexus_operation_cancel(input) diff --git a/temporalio/worker/_nexus.py b/temporalio/worker/_nexus.py index 72b47187f..131e50862 100644 --- a/temporalio/worker/_nexus.py +++ b/temporalio/worker/_nexus.py @@ -4,27 +4,25 @@ import asyncio import concurrent.futures -import json +import contextvars +import threading +from collections.abc import Awaitable, Callable, Mapping, Sequence from dataclasses import dataclass +from datetime import datetime, timezone +from functools import reduce from typing import ( Any, - Callable, - Mapping, NoReturn, - Optional, - Sequence, - Type, - Union, + ParamSpec, + TypeVar, + cast, ) -import google.protobuf.json_format import nexusrpc.handler from nexusrpc import LazyValue from nexusrpc.handler import CancelOperationContext, Handler, StartOperationContext import temporalio.api.common.v1 -import temporalio.api.enums.v1 -import temporalio.api.failure.v1 import temporalio.api.nexus.v1 import temporalio.bridge.proto.nexus import temporalio.bridge.worker @@ -32,41 +30,72 @@ import temporalio.common import temporalio.converter import temporalio.nexus -from temporalio.exceptions import ApplicationError, WorkflowAlreadyStartedError +from temporalio.bridge._visitor import PayloadVisitor +from temporalio.bridge._visitor_functions import PayloadSequence, VisitorFunctions +from temporalio.bridge.worker import PollShutdownError +from temporalio.exceptions import ( + ApplicationError, + CancelledError, + FailureError, + WorkflowAlreadyStartedError, +) from temporalio.nexus import Info, logger from temporalio.service import RPCError, RPCStatusCode -from ._interceptor import Interceptor +from ._interceptor import ( + ExecuteNexusOperationCancelInput, + ExecuteNexusOperationStartInput, + Interceptor, + NexusOperationInboundInterceptor, +) _TEMPORAL_FAILURE_PROTO_TYPE = "temporal.api.failure.v1.Failure" -class _NexusWorker: +@dataclass +class _RunningNexusTask: + task: asyncio.Task[Any] + cancellation: _NexusTaskCancellation + + def cancel(self, reason: str): + self.cancellation.cancel(reason) + self.task.cancel() + + +class _NexusWorker: # type:ignore[reportUnusedClass] def __init__( self, *, bridge_worker: Callable[[], temporalio.bridge.worker.Worker], client: temporalio.client.Client, + namespace: str, task_queue: str, service_handlers: Sequence[Any], data_converter: temporalio.converter.DataConverter, interceptors: Sequence[Interceptor], metric_meter: temporalio.common.MetricMeter, - executor: Optional[concurrent.futures.Executor], + executor: concurrent.futures.ThreadPoolExecutor | None, ) -> None: - # TODO: make it possible to query task queue of bridge worker instead of passing - # unused task_queue into _NexusWorker, _ActivityWorker, etc? self._bridge_worker = bridge_worker self._client = client + self._namespace = namespace self._task_queue = task_queue - self._handler = Handler(service_handlers, executor) - self._data_converter = data_converter - # TODO(nexus-preview): interceptors - self._interceptors = interceptors - # TODO(nexus-preview): metric_meter + self._metric_meter = metric_meter - self._running_tasks: dict[bytes, asyncio.Task[Any]] = {} + middleware = _NexusMiddlewareForInterceptors(interceptors) + + # If an executor is provided, we wrap the executor with one that will + # copy the contextvars.Context to the thread on submit + handler_executor = _ContextPropagatingExecutor(executor) if executor else None + self._handler = Handler( + service_handlers, handler_executor, middleware=[middleware] + ) + + self._data_converter = data_converter + + self._running_tasks: dict[bytes, _RunningNexusTask] = {} self._fail_worker_exception_queue: asyncio.Queue[Exception] = asyncio.Queue() + self._worker_shutdown_event: temporalio.common._CompositeEvent | None = None async def run(self) -> None: """Continually poll for Nexus tasks and dispatch to handlers.""" @@ -89,22 +118,43 @@ async def raise_from_exception_queue() -> NoReturn: if nexus_task.HasField("task"): task = nexus_task.task + request_deadline = ( + nexus_task.request_deadline.ToDatetime().replace( + tzinfo=timezone.utc + ) + if nexus_task.HasField("request_deadline") + else None + ) if task.request.HasField("start_operation"): - self._running_tasks[task.task_token] = asyncio.create_task( + task_cancellation = _NexusTaskCancellation() + start_op_task = asyncio.create_task( self._handle_start_operation_task( - task.task_token, - task.request.start_operation, - dict(task.request.header), + task_token=task.task_token, + start_request=task.request.start_operation, + headers=dict(task.request.header), + task_cancellation=task_cancellation, + request_deadline=request_deadline, + endpoint=nexus_task.endpoint, ) ) + self._running_tasks[task.task_token] = _RunningNexusTask( + start_op_task, task_cancellation + ) elif task.request.HasField("cancel_operation"): - self._running_tasks[task.task_token] = asyncio.create_task( + task_cancellation = _NexusTaskCancellation() + cancel_op_task = asyncio.create_task( self._handle_cancel_operation_task( - task.task_token, - task.request.cancel_operation, - dict(task.request.header), + task_token=task.task_token, + request=task.request.cancel_operation, + headers=dict(task.request.header), + task_cancellation=task_cancellation, + request_deadline=request_deadline, + endpoint=nexus_task.endpoint, ) ) + self._running_tasks[task.task_token] = _RunningNexusTask( + cancel_op_task, task_cancellation + ) else: raise NotImplementedError( f"Invalid Nexus task request: {task.request}" @@ -113,8 +163,12 @@ async def raise_from_exception_queue() -> NoReturn: if running_task := self._running_tasks.get( nexus_task.cancel_task.task_token ): - # TODO(nexus-prerelease): when do we remove the entry from _running_operations? - running_task.cancel() + reason = ( + temporalio.bridge.proto.nexus.NexusTaskCancelReason.Name( + nexus_task.cancel_task.reason + ) + ) + running_task.cancel(reason) else: logger.debug( f"Received cancel_task but no running task exists for " @@ -123,13 +177,17 @@ async def raise_from_exception_queue() -> NoReturn: else: raise NotImplementedError(f"Invalid Nexus task: {nexus_task}") - except temporalio.bridge.worker.PollShutdownError: + except PollShutdownError: exception_task.cancel() return except Exception as err: raise RuntimeError("Nexus worker failed") from err + def notify_shutdown(self) -> None: + if self._worker_shutdown_event: + self._worker_shutdown_event.set() + # Only call this if run() raised an error async def drain_poll_queue(self) -> None: while True: @@ -141,13 +199,37 @@ async def drain_poll_queue(self) -> None: ) completion.error.failure.message = "Worker shutting down" await self._bridge_worker().complete_nexus_task(completion) - except temporalio.bridge.worker.PollShutdownError: + except PollShutdownError: return # Only call this after run()/drain_poll_queue() have returned. This will not # raise an exception. async def wait_all_completed(self) -> None: - await asyncio.gather(*self._running_tasks.values(), return_exceptions=True) + running_tasks = [ + running_task.task for running_task in self._running_tasks.values() + ] + await asyncio.gather(*running_tasks, return_exceptions=True) + + # Task completion should never be dropped in case of cancellation. + # The Rust future in core must complete for shutdown to happen without + # hanging. + async def _complete_task( + self, completion: temporalio.bridge.proto.nexus.NexusTaskCompletion + ): + await asyncio.shield(self._bridge_worker().complete_nexus_task(completion)) + + async def _encode_completion( + self, completion: temporalio.bridge.proto.nexus.NexusTaskCompletion + ) -> None: + """Apply the payload codec then external storage to the completion's payloads.""" + dc = self._data_converter + await PayloadVisitor(skip_search_attributes=True, skip_headers=True).visit( + _PayloadTransformVisitor(dc._encode_payload_sequence), completion + ) + await PayloadVisitor(skip_search_attributes=True).visit( + _PayloadTransformVisitor(dc._external_store_payload_sequence), + completion, + ) # TODO(nexus-preview): stack trace pruning. See sdk-typescript NexusHandler.execute # "Any call up to this function and including this one will be trimmed out of stack traces."" @@ -157,43 +239,68 @@ async def _handle_cancel_operation_task( task_token: bytes, request: temporalio.api.nexus.v1.CancelOperationRequest, headers: Mapping[str, str], + task_cancellation: nexusrpc.handler.OperationTaskCancellation, + request_deadline: datetime | None, + endpoint: str, ) -> None: """Handle a cancel operation task. Attempt to execute the user cancel_operation method. Handle errors and send the task completion. """ + # Create the worker shutdown event if not created + if not self._worker_shutdown_event: + self._worker_shutdown_event = temporalio.common._CompositeEvent( + thread_event=threading.Event(), async_event=asyncio.Event() + ) # TODO(nexus-prerelease): headers ctx = CancelOperationContext( service=request.service, operation=request.operation, headers=headers, + task_cancellation=task_cancellation, + request_deadline=request_deadline, ) temporalio.nexus._operation_context._TemporalCancelOperationContext( - info=lambda: Info(task_queue=self._task_queue), + info=lambda: Info( + endpoint=endpoint, + namespace=self._namespace, + task_queue=self._task_queue, + ), nexus_context=ctx, client=self._client, + _runtime_metric_meter=self._metric_meter, + _worker_shutdown_event=self._worker_shutdown_event, ).set() try: try: await self._handler.cancel_operation(ctx, request.operation_token) - except BaseException as err: - logger.warning("Failed to execute Nexus cancel operation method") completion = temporalio.bridge.proto.nexus.NexusTaskCompletion( task_token=task_token, - error=await self._handler_error_to_proto( - _exception_to_handler_error(err) + completed=temporalio.api.nexus.v1.Response( + cancel_operation=temporalio.api.nexus.v1.CancelOperationResponse() ), ) - else: + # No-op but keeps the cancel covered if it ever carries a payload. + await self._encode_completion(completion) + except asyncio.CancelledError: completion = temporalio.bridge.proto.nexus.NexusTaskCompletion( task_token=task_token, - completed=temporalio.api.nexus.v1.Response( - cancel_operation=temporalio.api.nexus.v1.CancelOperationResponse() - ), + ack_cancel=task_cancellation.is_cancelled(), ) - - await self._bridge_worker().complete_nexus_task(completion) + except BaseException as err: + logger.warning("Failed to execute Nexus cancel operation method") + handler_error = _exception_to_handler_error(err) + completion = temporalio.bridge.proto.nexus.NexusTaskCompletion( + task_token=task_token, + ) + self._data_converter.failure_converter.to_failure( + handler_error, + self._data_converter.payload_converter, + completion.failure, + ) + await self._encode_completion(completion) + await self._complete_task(completion) except Exception: logger.exception("Failed to send Nexus task completion") finally: @@ -209,6 +316,9 @@ async def _handle_start_operation_task( task_token: bytes, start_request: temporalio.api.nexus.v1.StartOperationRequest, headers: Mapping[str, str], + task_cancellation: nexusrpc.handler.OperationTaskCancellation, + request_deadline: datetime | None, + endpoint: str, ) -> None: """Handle a start operation task. @@ -217,25 +327,42 @@ async def _handle_start_operation_task( """ try: try: - start_response = await self._start_operation(start_request, headers) - except BaseException as err: - logger.warning("Failed to execute Nexus start operation method") - completion = temporalio.bridge.proto.nexus.NexusTaskCompletion( - task_token=task_token, - error=await self._handler_error_to_proto( - _exception_to_handler_error(err) - ), + start_response = await self._start_operation( + start_request, + headers, + task_cancellation, + request_deadline, + endpoint, ) - if isinstance(err, concurrent.futures.BrokenExecutor): - self._fail_worker_exception_queue.put_nowait(err) - else: completion = temporalio.bridge.proto.nexus.NexusTaskCompletion( task_token=task_token, completed=temporalio.api.nexus.v1.Response( start_operation=start_response ), ) - await self._bridge_worker().complete_nexus_task(completion) + await self._encode_completion(completion) + except asyncio.CancelledError: + completion = temporalio.bridge.proto.nexus.NexusTaskCompletion( + task_token=task_token, + ack_cancel=task_cancellation.is_cancelled(), + ) + except BaseException as err: + logger.warning("Failed to execute Nexus start operation method") + completion = temporalio.bridge.proto.nexus.NexusTaskCompletion( + task_token=task_token, + ) + handler_error = _exception_to_handler_error(err) + self._data_converter.failure_converter.to_failure( + handler_error, + self._data_converter.payload_converter, + completion.failure, + ) + + if isinstance(err, concurrent.futures.BrokenExecutor): + self._fail_worker_exception_queue.put_nowait(err) + await self._encode_completion(completion) + + await self._complete_task(completion) except Exception: logger.exception("Failed to send Nexus task completion") finally: @@ -250,6 +377,9 @@ async def _start_operation( self, start_request: temporalio.api.nexus.v1.StartOperationRequest, headers: Mapping[str, str], + cancellation: nexusrpc.handler.OperationTaskCancellation, + request_deadline: datetime | None, + endpoint: str, ) -> temporalio.api.nexus.v1.StartOperationResponse: """Invoke the Nexus handler's start_operation method and construct the StartOperationResponse. @@ -257,6 +387,11 @@ async def _start_operation( All other exceptions are handled by a caller of this function. """ + # Create the worker shutdown event if not created + if not self._worker_shutdown_event: + self._worker_shutdown_event = temporalio.common._CompositeEvent( + thread_event=threading.Event(), async_event=asyncio.Event() + ) ctx = StartOperationContext( service=start_request.service, operation=start_request.operation, @@ -268,11 +403,19 @@ async def _start_operation( for link in start_request.links ], callback_headers=dict(start_request.callback_header), + task_cancellation=cancellation, + request_deadline=request_deadline, ) temporalio.nexus._operation_context._TemporalStartOperationContext( nexus_context=ctx, client=self._client, - info=lambda: Info(task_queue=self._task_queue), + info=lambda: Info( + endpoint=endpoint, + namespace=self._namespace, + task_queue=self._task_queue, + ), + _runtime_metric_meter=self._metric_meter, + _worker_shutdown_event=self._worker_shutdown_event, ).set() input = LazyValue( serializer=_DummyPayloadSerializer( @@ -296,7 +439,9 @@ async def _start_operation( ) ) elif isinstance(result, nexusrpc.handler.StartOperationResultSync): - [payload] = await self._data_converter.encode([result.value]) + [payload] = self._data_converter.payload_converter.to_payloads( + [result.value] + ) return temporalio.api.nexus.v1.StartOperationResponse( sync_success=temporalio.api.nexus.v1.StartOperationResponse.Sync( payload=payload, @@ -312,81 +457,52 @@ async def _start_operation( ) ) except nexusrpc.OperationError as err: - return temporalio.api.nexus.v1.StartOperationResponse( - operation_error=await self._operation_error_to_proto(err), - ) - - async def _nexus_error_to_nexus_failure_proto( - self, - error: Union[nexusrpc.HandlerError, nexusrpc.OperationError], - ) -> temporalio.api.nexus.v1.Failure: - """Serialize ``error`` as a Nexus Failure proto. - - The Nexus Failure represents the top-level error. If there is a cause chain - attached to the exception, then serialize it as the ``details``. - - Notice that any stack trace attached to ``error`` itself is not included in the - result. - - See https://github.com/nexus-rpc/api/blob/main/SPEC.md#failure - """ - if cause := error.__cause__: + # Convert OperationError to a Temporal failure try: - failure = temporalio.api.failure.v1.Failure() - await self._data_converter.encode_failure(cause, failure) - # Following other SDKs, we move the message from the first item - # in the details chain to the top level nexus.v1.Failure - # message. In Go and Java this particularly makes sense since - # their constructors are controlled such that the nexus - # exception itself does not have its own message. However, in - # Python, nexusrpc.HandlerError and nexusrpc.OperationError have - # their own error messages and stack traces, independent of any - # cause exception they may have, and this must be propagated to - # the caller. See _exception_to_handler_error for how we address - # this by injecting an additional error into the cause chain - # before the current function is called. - failure_dict = google.protobuf.json_format.MessageToDict(failure) - return temporalio.api.nexus.v1.Failure( - message=failure_dict.pop("message", str(error)), - metadata={"type": _TEMPORAL_FAILURE_PROTO_TYPE}, - details=json.dumps( - failure_dict, - separators=(",", ":"), - ).encode("utf-8"), + match err.state: + case nexusrpc.OperationErrorState.CANCELED: + raise CancelledError(err.message) from err.__cause__ + case nexusrpc.OperationErrorState.FAILED: + raise ApplicationError( + message=err.message, + type="OperationError", + non_retryable=True, + ) from err.__cause__ + except FailureError as new_err: + response = temporalio.api.nexus.v1.StartOperationResponse() + self._data_converter.failure_converter.to_failure( + new_err, + self._data_converter.payload_converter, + response.failure, ) - except BaseException: - logger.exception("Failed to serialize cause chain of nexus exception") - return temporalio.api.nexus.v1.Failure( - message=str(error), - metadata={}, - details=b"", - ) + return response + - async def _operation_error_to_proto( +class _PayloadTransformVisitor(VisitorFunctions): + """Adapts a payload-sequence transform for use with :class:`PayloadVisitor`.""" + + def __init__( self, - err: nexusrpc.OperationError, - ) -> temporalio.api.nexus.v1.UnsuccessfulOperationError: - return temporalio.api.nexus.v1.UnsuccessfulOperationError( - operation_state=err.state.value, - failure=await self._nexus_error_to_nexus_failure_proto(err), - ) + f: Callable[ + [Sequence[temporalio.api.common.v1.Payload]], + Awaitable[list[temporalio.api.common.v1.Payload]], + ], + ) -> None: + self._f = f - async def _handler_error_to_proto( - self, handler_error: nexusrpc.HandlerError - ) -> temporalio.api.nexus.v1.HandlerError: - """Serialize ``handler_error`` as a Nexus HandlerError proto.""" - retry_behavior = ( - temporalio.api.enums.v1.NexusHandlerErrorRetryBehavior.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_RETRYABLE - if handler_error.retryable_override is True - else temporalio.api.enums.v1.NexusHandlerErrorRetryBehavior.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_NON_RETRYABLE - if handler_error.retryable_override is False - else temporalio.api.enums.v1.NexusHandlerErrorRetryBehavior.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_UNSPECIFIED - ) - return temporalio.api.nexus.v1.HandlerError( - error_type=handler_error.type.value, - failure=await self._nexus_error_to_nexus_failure_proto(handler_error), - retry_behavior=retry_behavior, - ) + async def visit_payload(self, payload: temporalio.api.common.v1.Payload) -> None: + new_payload = (await self._f([payload]))[0] + if new_payload is not payload: + payload.CopyFrom(new_payload) + + async def visit_payloads(self, payloads: PayloadSequence) -> None: + if len(payloads) == 0: + return + new_payloads = await self._f(payloads) + if new_payloads is payloads: + return + del payloads[:] + payloads.extend(new_payloads) @dataclass @@ -394,54 +510,65 @@ class _DummyPayloadSerializer: data_converter: temporalio.converter.DataConverter payload: temporalio.api.common.v1.Payload - async def serialize(self, value: Any) -> nexusrpc.Content: + async def serialize(self, value: Any) -> nexusrpc.Content: # type:ignore[reportUnusedParameter] raise NotImplementedError( "The serialize method of the Serializer is not used by handlers" ) async def deserialize( self, - content: nexusrpc.Content, - as_type: Optional[Type[Any]] = None, + content: nexusrpc.Content, # type:ignore[reportUnusedParameter] + as_type: type[Any] | None = None, ) -> Any: + dc = self.data_converter + # The visitor mutates in place, so work on a copy to leave the request + # payload untouched. + payload = temporalio.api.common.v1.Payload() + payload.CopyFrom(self.payload) + try: + await PayloadVisitor(skip_search_attributes=True).visit( + _PayloadTransformVisitor(dc._external_retrieve_payload_sequence), + payload, + ) + except Exception as err: + raise nexusrpc.HandlerError( + "Failed to retrieve Nexus operation input from external storage", + type=nexusrpc.HandlerErrorType.INTERNAL, + retryable_override=True, + ) from err + + try: + await PayloadVisitor(skip_search_attributes=True, skip_headers=True).visit( + _PayloadTransformVisitor(dc._decode_payload_sequence), payload + ) + except Exception as err: + raise nexusrpc.HandlerError( + "Payload codec failed to decode Nexus operation input", + type=nexusrpc.HandlerErrorType.INTERNAL, + ) from err + try: - [input] = await self.data_converter.decode( - [self.payload], + [input] = dc.payload_converter.from_payloads( + [payload], type_hints=[as_type] if as_type else None, ) return input except Exception as err: raise nexusrpc.HandlerError( - "Data converter failed to decode Nexus operation input", + "Payload converter failed to decode Nexus operation input", type=nexusrpc.HandlerErrorType.BAD_REQUEST, retryable_override=False, ) from err -# TODO(nexus-prerelease): tests for this function def _exception_to_handler_error(err: BaseException) -> nexusrpc.HandlerError: # Based on sdk-typescript's convertKnownErrors: # https://github.com/temporalio/sdk-typescript/blob/nexus/packages/worker/src/nexus.ts if isinstance(err, nexusrpc.HandlerError): - # Insert an ApplicationError at the head of the cause chain to hold the - # HandlerError's message and traceback. We do this because - # _nexus_error_to_nexus_failure_proto moves the message at the head of - # the cause chain to be the top-level nexus.Failure message. Therefore, - # if we did not do this, then the HandlerError's own message and - # traceback would be lost. (This hoisting behavior makes sense for Go - # and Java since they control construction of HandlerError such that it - # does not have its own message or stack trace.) - handler_err = err - err = ApplicationError( - message=str(handler_err), - non_retryable=not handler_err.retryable, - ) - err.__traceback__ = handler_err.__traceback__ - err.__cause__ = handler_err.__cause__ + return err elif isinstance(err, ApplicationError): handler_err = nexusrpc.HandlerError( - # TODO(nexus-preview): confirm what we want as message here - err.message, + message="Handler failed with non-retryable application error", type=nexusrpc.HandlerErrorType.INTERNAL, retryable_override=not err.non_retryable, ) @@ -513,7 +640,122 @@ def _exception_to_handler_error(err: BaseException) -> nexusrpc.HandlerError: ) else: handler_err = nexusrpc.HandlerError( - str(err), type=nexusrpc.HandlerErrorType.INTERNAL + "Internal handler error", type=nexusrpc.HandlerErrorType.INTERNAL ) handler_err.__cause__ = err return handler_err + + +class _NexusTaskCancellation(nexusrpc.handler.OperationTaskCancellation): + def __init__(self): + self._thread_evt = threading.Event() + self._async_evt = asyncio.Event() + self._lock = threading.Lock() + self._reason: str | None = None + + def is_cancelled(self) -> bool: + return self._thread_evt.is_set() + + def cancellation_reason(self) -> str | None: + with self._lock: + return self._reason + + def wait_until_cancelled_sync(self, timeout: float | None = None) -> bool: + return self._thread_evt.wait(timeout) + + async def wait_until_cancelled(self) -> None: + await self._async_evt.wait() + + def cancel(self, reason: str) -> bool: + with self._lock: + if self._thread_evt.is_set(): + return False + self._reason = reason + self._thread_evt.set() + self._async_evt.set() + return True + + +class _NexusOperationHandlerForInterceptor( + nexusrpc.handler.MiddlewareSafeOperationHandler +): + def __init__(self, next_interceptor: NexusOperationInboundInterceptor): + self._next_interceptor = next_interceptor + + async def start( + self, ctx: nexusrpc.handler.StartOperationContext, input: Any + ) -> ( + nexusrpc.handler.StartOperationResultSync[Any] + | nexusrpc.handler.StartOperationResultAsync + ): + return await self._next_interceptor.execute_nexus_operation_start( + ExecuteNexusOperationStartInput(ctx, input) + ) + + async def cancel( + self, ctx: nexusrpc.handler.CancelOperationContext, token: str + ) -> None: + return await self._next_interceptor.execute_nexus_operation_cancel( + ExecuteNexusOperationCancelInput(ctx, token) + ) + + +class _NexusOperationInboundInterceptorImpl(NexusOperationInboundInterceptor): + def __init__(self, handler: nexusrpc.handler.MiddlewareSafeOperationHandler): # pyright: ignore[reportMissingSuperCall] + self._handler = handler + + async def execute_nexus_operation_start( + self, input: ExecuteNexusOperationStartInput + ) -> ( + nexusrpc.handler.StartOperationResultSync[Any] + | nexusrpc.handler.StartOperationResultAsync + ): + return await self._handler.start(input.ctx, input.input) + + async def execute_nexus_operation_cancel( + self, input: ExecuteNexusOperationCancelInput + ) -> None: + return await self._handler.cancel(input.ctx, input.token) + + +class _NexusMiddlewareForInterceptors(nexusrpc.handler.OperationHandlerMiddleware): + def __init__(self, interceptors: Sequence[Interceptor]) -> None: + self._interceptors = interceptors + + def intercept( + self, + ctx: nexusrpc.handler.OperationContext, + next: nexusrpc.handler.MiddlewareSafeOperationHandler, + ) -> nexusrpc.handler.MiddlewareSafeOperationHandler: + inbound = reduce( + lambda impl, _next: _next.intercept_nexus_operation(impl), + reversed(self._interceptors), + cast( + NexusOperationInboundInterceptor, + _NexusOperationInboundInterceptorImpl(next), + ), + ) + + return _NexusOperationHandlerForInterceptor(inbound) + + +_P = ParamSpec("_P") +_T = TypeVar("_T") + + +class _ContextPropagatingExecutor(concurrent.futures.Executor): + def __init__(self, executor: concurrent.futures.ThreadPoolExecutor) -> None: + self._executor = executor + + def submit( + self, fn: Callable[_P, _T], /, *args: _P.args, **kwargs: _P.kwargs + ) -> concurrent.futures.Future[_T]: + ctx = contextvars.copy_context() + + def wrapped(*a: _P.args, **k: _P.kwargs) -> _T: + return ctx.run(fn, *a, **k) + + return self._executor.submit(wrapped, *args, **kwargs) + + def shutdown(self, wait: bool = True, *, cancel_futures: bool = False) -> None: + return self._executor.shutdown(wait=wait, cancel_futures=cancel_futures) diff --git a/temporalio/worker/_plugin.py b/temporalio/worker/_plugin.py index 0e696a2dd..969b1d370 100644 --- a/temporalio/worker/_plugin.py +++ b/temporalio/worker/_plugin.py @@ -1,8 +1,9 @@ from __future__ import annotations import abc +from collections.abc import AsyncIterator, Awaitable, Callable from contextlib import AbstractAsyncContextManager -from typing import TYPE_CHECKING, AsyncIterator +from typing import TYPE_CHECKING from temporalio.client import WorkflowHistory @@ -34,19 +35,6 @@ def name(self) -> str: """ return type(self).__module__ + "." + type(self).__qualname__ - @abc.abstractmethod - def init_worker_plugin(self, next: Plugin) -> None: - """Initialize this plugin in the plugin chain. - - This method sets up the chain of responsibility pattern by providing a reference - to the next plugin in the chain. It is called during worker creation to build - the plugin chain. Implementations should store this reference and call the corresponding method - of the next plugin on method calls. - - Args: - next: The next plugin in the chain to delegate to. - """ - @abc.abstractmethod def configure_worker(self, config: WorkerConfig) -> WorkerConfig: """Hook called when creating a worker to allow modification of configuration. @@ -64,7 +52,9 @@ def configure_worker(self, config: WorkerConfig) -> WorkerConfig: """ @abc.abstractmethod - async def run_worker(self, worker: Worker) -> None: + async def run_worker( + self, worker: Worker, next: Callable[[Worker], Awaitable[None]] + ) -> None: """Hook called when running a worker to allow interception of execution. This method is called when the worker is started and allows plugins to @@ -73,6 +63,7 @@ async def run_worker(self, worker: Worker) -> None: Args: worker: The worker instance to run. + next: Callable to continue the worker execution. """ @abc.abstractmethod @@ -94,26 +85,9 @@ def run_replayer( self, replayer: Replayer, histories: AsyncIterator[WorkflowHistory], + next: Callable[ + [Replayer, AsyncIterator[WorkflowHistory]], + AbstractAsyncContextManager[AsyncIterator[WorkflowReplayResult]], + ], ) -> AbstractAsyncContextManager[AsyncIterator[WorkflowReplayResult]]: """Hook called when running a replayer to allow interception of execution.""" - - -class _RootPlugin(Plugin): - def init_worker_plugin(self, next: Plugin) -> None: - raise NotImplementedError() - - def configure_worker(self, config: WorkerConfig) -> WorkerConfig: - return config - - def configure_replayer(self, config: ReplayerConfig) -> ReplayerConfig: - return config - - async def run_worker(self, worker: Worker) -> None: - await worker._run() - - def run_replayer( - self, - replayer: Replayer, - histories: AsyncIterator[WorkflowHistory], - ) -> AbstractAsyncContextManager[AsyncIterator[WorkflowReplayResult]]: - return replayer._workflow_replay_iterator(histories) diff --git a/temporalio/worker/_replayer.py b/temporalio/worker/_replayer.py index 240429bf7..b3eb1a4d1 100644 --- a/temporalio/worker/_replayer.py +++ b/temporalio/worker/_replayer.py @@ -5,9 +5,9 @@ import asyncio import concurrent.futures import logging +from collections.abc import AsyncIterator, Mapping, Sequence from contextlib import AbstractAsyncContextManager, asynccontextmanager from dataclasses import dataclass -from typing import AsyncIterator, Dict, Mapping, Optional, Sequence, Type from typing_extensions import TypedDict @@ -17,14 +17,19 @@ import temporalio.client import temporalio.converter import temporalio.runtime +import temporalio.worker import temporalio.workflow from ..common import HeaderCodecBehavior from ._interceptor import Interceptor -from ._plugin import _RootPlugin from ._worker import load_default_build_id from ._workflow import _WorkflowWorker -from ._workflow_instance import UnsandboxedWorkflowRunner, WorkflowRunner +from ._workflow_instance import ( + _DEFAULT_ENABLED_WORKFLOW_LOGIC_FLAGS, + UnsandboxedWorkflowRunner, + WorkflowRunner, + _WorkflowLogicFlag, +) from .workflow_sandbox import SandboxedWorkflowRunner logger = logging.getLogger(__name__) @@ -36,19 +41,19 @@ class Replayer: def __init__( self, *, - workflows: Sequence[Type], - workflow_task_executor: Optional[concurrent.futures.ThreadPoolExecutor] = None, + workflows: Sequence[type], + workflow_task_executor: concurrent.futures.ThreadPoolExecutor | None = None, workflow_runner: WorkflowRunner = SandboxedWorkflowRunner(), unsandboxed_workflow_runner: WorkflowRunner = UnsandboxedWorkflowRunner(), namespace: str = "ReplayNamespace", data_converter: temporalio.converter.DataConverter = temporalio.converter.DataConverter.default, interceptors: Sequence[Interceptor] = [], plugins: Sequence[temporalio.worker.Plugin] = [], - build_id: Optional[str] = None, - identity: Optional[str] = None, - workflow_failure_exception_types: Sequence[Type[BaseException]] = [], + build_id: str | None = None, + identity: str | None = None, + workflow_failure_exception_types: Sequence[type[BaseException]] = [], debug_mode: bool = False, - runtime: Optional[temporalio.runtime.Runtime] = None, + runtime: temporalio.runtime.Runtime | None = None, disable_safe_workflow_eviction: bool = False, header_codec_behavior: HeaderCodecBehavior = HeaderCodecBehavior.NO_CODEC, ) -> None: @@ -82,27 +87,38 @@ def __init__( disable_safe_workflow_eviction=disable_safe_workflow_eviction, header_codec_behavior=header_codec_behavior, ) + self._initial_config = self._config.copy() + self._default_workflow_logic_flags = set(_DEFAULT_ENABLED_WORKFLOW_LOGIC_FLAGS) # Apply plugin configuration - root_plugin: temporalio.worker.Plugin = _RootPlugin() - for plugin in reversed(plugins): - plugin.init_worker_plugin(root_plugin) - root_plugin = plugin - self._config = root_plugin.configure_replayer(self._config) - self._plugin = root_plugin + self.plugins = plugins + for plugin in plugins: + self._config = plugin.configure_replayer(self._config) # Validate workflows after plugin configuration - if not self._config["workflows"]: + if not self._config.get("workflows"): raise ValueError("At least one workflow must be specified") - def config(self) -> ReplayerConfig: + def _set_default_workflow_logic_flag( + self, flag: _WorkflowLogicFlag, *, enabled: bool + ) -> None: + if enabled: + self._default_workflow_logic_flags.add(flag) + else: + self._default_workflow_logic_flags.discard(flag) + + def config(self, *, active_config: bool = False) -> ReplayerConfig: """Config, as a dictionary, used to create this replayer. + Args: + active_config: If true, return the modified configuration in use rather than the initial one + provided to the client. + Returns: Configuration, shallow-copied. """ - config = self._config.copy() - config["workflows"] = list(config["workflows"]) + config = self._config.copy() if active_config else self._initial_config.copy() + config["workflows"] = list(config.get("workflows", [])) return config async def replay_workflow( @@ -116,7 +132,7 @@ async def replay_workflow( Args: history: The history to replay. Can be fetched directly, or use :py:meth:`temporalio.client.WorkflowHistory.from_json` to parse - a history downloaded via ``tctl`` or the web UI. + a history downloaded via ``Temporal CLI`` or the web UI. raise_on_replay_failure: If ``True`` (the default), this will raise a :py:attr:`WorkflowReplayResult.replay_failure` if it is present. @@ -153,7 +169,7 @@ async def replay_workflows( Aggregated results. """ async with self.workflow_replay_iterator(histories) as replay_iterator: - replay_failures: Dict[str, Exception] = {} + replay_failures: dict[str, Exception] = {} async for result in replay_iterator: if result.replay_failure: if raise_on_replay_failure: @@ -176,19 +192,32 @@ def workflow_replay_iterator( An async iterator that returns replayed workflow results as they are replayed. """ - return self._plugin.run_replayer(self, histories) + + def make_lambda(plugin, next): # type: ignore[reportMissingParameterType] + return lambda r, hs: plugin.run_replayer(r, hs, next) + + next_function = lambda r, hs: r._workflow_replay_iterator(hs) + for plugin in reversed(self.plugins): + next_function = make_lambda(plugin, next_function) + + return next_function(self, histories) @asynccontextmanager async def _workflow_replay_iterator( self, histories: AsyncIterator[temporalio.client.WorkflowHistory] ) -> AsyncIterator[AsyncIterator[WorkflowReplayResult]]: + # Initialize variables to avoid unbound variable errors + pusher = None + workflow_worker_task = None + bridge_worker_scope = None + try: - last_replay_failure: Optional[Exception] + last_replay_failure: Exception | None last_replay_complete = asyncio.Event() # Create eviction hook def on_eviction_hook( - run_id: str, + _run_id: str, remove_job: temporalio.bridge.proto.workflow_activation.RemoveFromCache, ) -> None: nonlocal last_replay_failure @@ -214,39 +243,65 @@ def on_eviction_hook( # Create worker referencing bridge worker bridge_worker: temporalio.bridge.worker.Worker - task_queue = f"replay-{self._config['build_id']}" - runtime = self._config["runtime"] or temporalio.runtime.Runtime.default() + task_queue = f"replay-{self._config.get('build_id')}" + runtime = ( + self._config.get("runtime") or temporalio.runtime.Runtime.default() + ) + data_converter = ( + self._config.get("data_converter") + or temporalio.converter.DataConverter.default + ) workflow_worker = _WorkflowWorker( bridge_worker=lambda: bridge_worker, - namespace=self._config["namespace"], + namespace=self._config.get("namespace", "ReplayNamespace"), task_queue=task_queue, - workflows=self._config["workflows"], - workflow_task_executor=self._config["workflow_task_executor"], + workflows=self._config.get("workflows", []), + workflow_task_executor=self._config.get("workflow_task_executor"), max_concurrent_workflow_tasks=5, - workflow_runner=self._config["workflow_runner"], - unsandboxed_workflow_runner=self._config["unsandboxed_workflow_runner"], - data_converter=self._config["data_converter"], - interceptors=self._config["interceptors"], - workflow_failure_exception_types=self._config[ - "workflow_failure_exception_types" - ], - debug_mode=self._config["debug_mode"], + workflow_runner=self._config.get("workflow_runner") + or SandboxedWorkflowRunner(), + unsandboxed_workflow_runner=self._config.get( + "unsandboxed_workflow_runner" + ) + or UnsandboxedWorkflowRunner(), + data_converter=data_converter, + interceptors=self._config.get("interceptors", []), + workflow_failure_exception_types=self._config.get( + "workflow_failure_exception_types", [] + ), + patch_activation_callback=None, + debug_mode=self._config.get("debug_mode", False), metric_meter=runtime.metric_meter, on_eviction_hook=on_eviction_hook, disable_eager_activity_execution=False, - disable_safe_eviction=self._config["disable_safe_workflow_eviction"], + disable_safe_eviction=self._config.get( + "disable_safe_workflow_eviction", False + ), should_enforce_versioning_behavior=False, assert_local_activity_valid=lambda a: None, - encode_headers=self._config["header_codec_behavior"] + encode_headers=self._config.get( + "header_codec_behavior", HeaderCodecBehavior.NO_CODEC + ) != HeaderCodecBehavior.NO_CODEC, + max_workflow_task_external_storage_concurrency=1, + default_workflow_logic_flags=frozenset( + self._default_workflow_logic_flags + ), ) + external_storage = data_converter.external_storage + storage_driver_types = ( + {driver.type() for driver in external_storage.drivers} + if external_storage + else set() + ) + # Create bridge worker bridge_worker, pusher = temporalio.bridge.worker.Worker.for_replay( runtime._core_runtime, temporalio.bridge.worker.WorkerConfig( - namespace=self._config["namespace"], + namespace=self._config.get("namespace", "ReplayNamespace"), task_queue=task_queue, - identity_override=self._config["identity"], + identity_override=self._config.get("identity"), # Need to tell core whether we want to consider all # non-determinism exceptions as workflow fail, and whether we do # per workflow type @@ -264,17 +319,28 @@ def on_eviction_hook( local_activity_slot_supplier=temporalio.bridge.worker.FixedSizeSlotSupplier( 1 ), + nexus_slot_supplier=temporalio.bridge.worker.FixedSizeSlotSupplier( + 1 + ), ), nonsticky_to_sticky_poll_ratio=1, no_remote_activities=True, + disable_payload_error_limit=True, + task_types=temporalio.bridge.worker.WorkerTaskTypes( + enable_workflows=True, + enable_local_activities=False, + enable_remote_activities=False, + enable_nexus=False, + ), sticky_queue_schedule_to_start_timeout_millis=1000, max_heartbeat_throttle_interval_millis=1000, default_heartbeat_throttle_interval_millis=1000, max_activities_per_second=None, max_task_queue_activities_per_second=None, + max_eager_activity_reservations_per_workflow_task=3, graceful_shutdown_period_millis=0, versioning_strategy=temporalio.bridge.worker.WorkerVersioningStrategyNone( - build_id_no_versioning=self._config["build_id"] + build_id_no_versioning=self._config.get("build_id") or load_default_build_id(), ), workflow_task_poller_behavior=temporalio.bridge.worker.PollerBehaviorSimpleMaximum( @@ -286,8 +352,12 @@ def on_eviction_hook( nexus_task_poller_behavior=temporalio.bridge.worker.PollerBehaviorSimpleMaximum( 1 ), + plugins=[plugin.name() for plugin in self.plugins], + storage_drivers=storage_driver_types, ), ) + bridge_worker_scope = bridge_worker + # Start worker workflow_worker_task = asyncio.create_task(workflow_worker.run()) @@ -328,18 +398,20 @@ async def replay_iterator() -> AsyncIterator[WorkflowReplayResult]: yield replay_iterator() finally: # Close the pusher - pusher.close() + if pusher is not None: + pusher.close() # If the workflow worker task is not done, wait for it try: - if not workflow_worker_task.done(): + if workflow_worker_task is not None and not workflow_worker_task.done(): await workflow_worker_task except Exception: logger.warning("Failed to shutdown worker", exc_info=True) finally: # We must shutdown here try: - bridge_worker.initiate_shutdown() - await bridge_worker.finalize_shutdown() + if bridge_worker_scope is not None: + bridge_worker_scope.initiate_shutdown() + await bridge_worker_scope.finalize_shutdown() except Exception: logger.warning("Failed to finalize shutdown", exc_info=True) @@ -347,18 +419,18 @@ async def replay_iterator() -> AsyncIterator[WorkflowReplayResult]: class ReplayerConfig(TypedDict, total=False): """TypedDict of config originally passed to :py:class:`Replayer`.""" - workflows: Sequence[Type] - workflow_task_executor: Optional[concurrent.futures.ThreadPoolExecutor] + workflows: Sequence[type] + workflow_task_executor: concurrent.futures.ThreadPoolExecutor | None workflow_runner: WorkflowRunner unsandboxed_workflow_runner: WorkflowRunner namespace: str data_converter: temporalio.converter.DataConverter interceptors: Sequence[Interceptor] - build_id: Optional[str] - identity: Optional[str] - workflow_failure_exception_types: Sequence[Type[BaseException]] + build_id: str | None + identity: str | None + workflow_failure_exception_types: Sequence[type[BaseException]] debug_mode: bool - runtime: Optional[temporalio.runtime.Runtime] + runtime: temporalio.runtime.Runtime | None disable_safe_workflow_eviction: bool header_codec_behavior: HeaderCodecBehavior @@ -370,7 +442,7 @@ class WorkflowReplayResult: history: temporalio.client.WorkflowHistory """History originally passed for this workflow replay.""" - replay_failure: Optional[Exception] + replay_failure: Exception | None """Failure during replay if any. This does not mean your workflow exited by raising an error, but rather that diff --git a/temporalio/worker/_tuning.py b/temporalio/worker/_tuning.py index b74f79d5e..39ab8343a 100644 --- a/temporalio/worker/_tuning.py +++ b/temporalio/worker/_tuning.py @@ -1,16 +1,18 @@ +from __future__ import annotations + import asyncio import logging from abc import ABC, abstractmethod +from collections.abc import Callable from dataclasses import dataclass from datetime import timedelta -from typing import Any, Callable, Literal, Optional, Protocol, Union, runtime_checkable - -from typing_extensions import TypeAlias +from typing import Any, Literal, Protocol, TypeAlias, runtime_checkable import temporalio.bridge.worker +from temporalio.bridge.worker import BridgeCustomSlotSupplier from temporalio.common import WorkerDeploymentVersion -_DEFAULT_RESOURCE_ACTIVITY_MAX = 500 +_DEFAULT_RESOURCE_SLOTS_MAX = 500 logger = logging.getLogger(__name__) @@ -25,11 +27,7 @@ class FixedSizeSlotSupplier: @dataclass(frozen=True) class ResourceBasedTunerConfig: - """Options for a :py:class:`ResourceBasedTuner` or a :py:class:`ResourceBasedSlotSupplier`. - - .. warning:: - The resource based tuner is currently experimental. - """ + """Options for a :py:class:`ResourceBasedSlotSupplier`.""" target_memory_usage: float """A value between 0 and 1 that represents the target (system) memory usage. It's not recommended @@ -42,18 +40,14 @@ class ResourceBasedTunerConfig: @dataclass(frozen=True) class ResourceBasedSlotConfig: - """Options for a specific slot type being used with a :py:class:`ResourceBasedSlotSupplier`. - - .. warning:: - The resource based tuner is currently experimental. - """ + """Options for a specific slot type being used with a :py:class:`ResourceBasedSlotSupplier`.""" - minimum_slots: Optional[int] = None + minimum_slots: int | None = None """Amount of slots that will be issued regardless of any other checks. Defaults to 5 for workflows and 1 for activities.""" - maximum_slots: Optional[int] = None + maximum_slots: int | None = None """Maximum amount of slots permitted. Defaults to 500.""" - ramp_throttle: Optional[timedelta] = None + ramp_throttle: timedelta | None = None """Minimum time we will wait (after passing the minimum slots number) between handing out new slots in milliseconds. Defaults to 0 for workflows and 50ms for activities. @@ -63,25 +57,18 @@ class ResourceBasedSlotConfig: @dataclass(frozen=True) class ResourceBasedSlotSupplier: - """A slot supplier that will dynamically adjust the number of slots based on resource usage. - - .. warning:: - The resource based tuner is currently experimental. - """ + """A slot supplier that will dynamically adjust the number of slots based on resource usage.""" slot_config: ResourceBasedSlotConfig tuner_config: ResourceBasedTunerConfig """Options for the tuner that will be used to adjust the number of slots. When used with a - :py:class:`CompositeTuner`, all resource-based slot suppliers must use the same tuner options.""" + :py:class:`_CompositeTuner`, all resource-based slot suppliers must use the same tuner options.""" class SlotPermit: """A permit to use a slot for a workflow/activity/local activity task. You can inherit from this class to add your own data to the permit. - - .. warning:: - Custom slot suppliers are currently experimental. """ pass @@ -89,11 +76,7 @@ class SlotPermit: # WARNING: This must match Rust worker::SlotReserveCtx class SlotReserveContext(Protocol): - """Context for reserving a slot from a :py:class:`CustomSlotSupplier`. - - .. warning:: - Custom slot suppliers are currently experimental. - """ + """Context for reserving a slot from a :py:class:`CustomSlotSupplier`.""" slot_type: Literal["workflow", "activity", "local-activity"] """The type of slot trying to be reserved. Always one of "workflow", "activity", or "local-activity".""" @@ -107,7 +90,7 @@ class SlotReserveContext(Protocol): .. warning:: Deprecated, use :py:attr:`worker_deployment_version` instead. """ - worker_deployment_version: Optional[WorkerDeploymentVersion] + worker_deployment_version: WorkerDeploymentVersion | None """The deployment version of the worker that is requesting the reservation, if any.""" is_sticky: bool """True iff this is a reservation for a sticky poll for a workflow task.""" @@ -116,11 +99,7 @@ class SlotReserveContext(Protocol): # WARNING: This must match Rust worker::WorkflowSlotInfo @runtime_checkable class WorkflowSlotInfo(Protocol): - """Info about a workflow task slot usage. - - .. warning:: - Custom slot suppliers are currently experimental. - """ + """Info about a workflow task slot usage.""" workflow_type: str is_sticky: bool @@ -129,11 +108,7 @@ class WorkflowSlotInfo(Protocol): # WARNING: This must match Rust worker::ActivitySlotInfo @runtime_checkable class ActivitySlotInfo(Protocol): - """Info about an activity task slot usage. - - .. warning:: - Custom slot suppliers are currently experimental. - """ + """Info about an activity task slot usage.""" activity_type: str @@ -141,26 +116,29 @@ class ActivitySlotInfo(Protocol): # WARNING: This must match Rust worker::LocalActivitySlotInfo @runtime_checkable class LocalActivitySlotInfo(Protocol): - """Info about a local activity task slot usage. - - .. warning:: - Custom slot suppliers are currently experimental. - """ + """Info about a local activity task slot usage.""" activity_type: str -SlotInfo: TypeAlias = Union[WorkflowSlotInfo, ActivitySlotInfo, LocalActivitySlotInfo] +# WARNING: This must match Rust worker::NexusSlotInfo +@runtime_checkable +class NexusSlotInfo(Protocol): + """Info about a nexus task slot usage.""" + + service: str + operation: str + + +SlotInfo: TypeAlias = ( + WorkflowSlotInfo | ActivitySlotInfo | LocalActivitySlotInfo | NexusSlotInfo +) # WARNING: This must match Rust worker::SlotMarkUsedCtx @dataclass(frozen=True) class SlotMarkUsedContext(Protocol): - """Context for marking a slot used from a :py:class:`CustomSlotSupplier`. - - .. warning:: - Custom slot suppliers are currently experimental. - """ + """Context for marking a slot used from a :py:class:`CustomSlotSupplier`.""" slot_info: SlotInfo """Info about the task that will be using the slot.""" @@ -171,24 +149,16 @@ class SlotMarkUsedContext(Protocol): # WARNING: This must match Rust worker::SlotReleaseCtx @dataclass(frozen=True) class SlotReleaseContext: - """Context for releasing a slot from a :py:class:`CustomSlotSupplier`. + """Context for releasing a slot from a :py:class:`CustomSlotSupplier`.""" - .. warning:: - Custom slot suppliers are currently experimental. - """ - - slot_info: Optional[SlotInfo] + slot_info: SlotInfo | None """Info about the task that will be using the slot. May be None if the slot was never used.""" permit: SlotPermit """The permit that was issued when the slot was reserved.""" class CustomSlotSupplier(ABC): - """This class can be implemented to provide custom slot supplier behavior. - - .. warning:: - Custom slot suppliers are currently experimental. - """ + """This class can be implemented to provide custom slot supplier behavior.""" @abstractmethod async def reserve_slot(self, ctx: SlotReserveContext) -> SlotPermit: @@ -213,7 +183,7 @@ async def reserve_slot(self, ctx: SlotReserveContext) -> SlotPermit: ... @abstractmethod - def try_reserve_slot(self, ctx: SlotReserveContext) -> Optional[SlotPermit]: + def try_reserve_slot(self, ctx: SlotReserveContext) -> SlotPermit | None: """This function is called when trying to reserve slots for "eager" workflow and activity tasks. Eager tasks are those which are returned as a result of completing a workflow task, rather than from polling. Your implementation must not block, and if a slot is available, return a permit @@ -251,9 +221,9 @@ def release_slot(self, ctx: SlotReleaseContext) -> None: ... -SlotSupplier: TypeAlias = Union[ - FixedSizeSlotSupplier, ResourceBasedSlotSupplier, CustomSlotSupplier -] +SlotSupplier: TypeAlias = ( + FixedSizeSlotSupplier | ResourceBasedSlotSupplier | CustomSlotSupplier +) class _BridgeSlotSupplierWrapper: @@ -276,7 +246,7 @@ async def reserve_slot( # Error needs to be re-thrown here so the rust code will loop raise - def try_reserve_slot(self, ctx: SlotReserveContext) -> Optional[SlotPermit]: + def try_reserve_slot(self, ctx: SlotReserveContext) -> SlotPermit | None: try: return self._supplier.try_reserve_slot(ctx) except Exception: @@ -303,13 +273,14 @@ def mark_slot_used(self, ctx: SlotMarkUsedContext) -> None: def _to_bridge_slot_supplier( - slot_supplier: SlotSupplier, kind: Literal["workflow", "activity", "local_activity"] + slot_supplier: SlotSupplier, + kind: Literal["workflow", "activity", "local_activity", "nexus"], ) -> temporalio.bridge.worker.SlotSupplier: if isinstance(slot_supplier, FixedSizeSlotSupplier): return temporalio.bridge.worker.FixedSizeSlotSupplier(slot_supplier.num_slots) elif isinstance(slot_supplier, ResourceBasedSlotSupplier): min_slots = 5 if kind == "workflow" else 1 - max_slots = _DEFAULT_RESOURCE_ACTIVITY_MAX + max_slots = _DEFAULT_RESOURCE_SLOTS_MAX ramp_throttle = ( timedelta(seconds=0) if kind == "workflow" else timedelta(milliseconds=50) ) @@ -329,25 +300,25 @@ def _to_bridge_slot_supplier( ), ) elif isinstance(slot_supplier, CustomSlotSupplier): - return temporalio.bridge.worker.BridgeCustomSlotSupplier( - _BridgeSlotSupplierWrapper(slot_supplier) - ) + return BridgeCustomSlotSupplier(_BridgeSlotSupplierWrapper(slot_supplier)) else: - raise TypeError(f"Unknown slot supplier type: {slot_supplier}") + raise TypeError(f"Unknown slot supplier type: {slot_supplier}") # type:ignore[reportUnreachable] class WorkerTuner(ABC): """WorkerTuners allow for the dynamic customization of some aspects of worker configuration""" - @staticmethod + @classmethod def create_resource_based( + cls, *, target_memory_usage: float, target_cpu_usage: float, - workflow_config: Optional[ResourceBasedSlotConfig] = None, - activity_config: Optional[ResourceBasedSlotConfig] = None, - local_activity_config: Optional[ResourceBasedSlotConfig] = None, - ) -> "WorkerTuner": + workflow_config: ResourceBasedSlotConfig | None = None, + activity_config: ResourceBasedSlotConfig | None = None, + local_activity_config: ResourceBasedSlotConfig | None = None, + nexus_config: ResourceBasedSlotConfig | None = None, + ) -> WorkerTuner: """Create a resource-based tuner with the provided options.""" resource_cfg = ResourceBasedTunerConfig(target_memory_usage, target_cpu_usage) wf = ResourceBasedSlotSupplier( @@ -359,40 +330,53 @@ def create_resource_based( local_act = ResourceBasedSlotSupplier( local_activity_config or ResourceBasedSlotConfig(), resource_cfg ) + nexus = ResourceBasedSlotSupplier( + nexus_config or ResourceBasedSlotConfig(), resource_cfg + ) return _CompositeTuner( wf, act, local_act, + nexus, ) - @staticmethod + @classmethod def create_fixed( + cls, *, - workflow_slots: Optional[int], - activity_slots: Optional[int], - local_activity_slots: Optional[int], - ) -> "WorkerTuner": - """Create a fixed-size tuner with the provided number of slots. Any unspecified slots will default to 100.""" + workflow_slots: int | None = None, + activity_slots: int | None = None, + local_activity_slots: int | None = None, + nexus_slots: int | None = None, + ) -> WorkerTuner: + """Create a fixed-size tuner with the provided number of slots. + + Any unspecified slot numbers will default to 100. + """ return _CompositeTuner( FixedSizeSlotSupplier(workflow_slots if workflow_slots else 100), FixedSizeSlotSupplier(activity_slots if activity_slots else 100), FixedSizeSlotSupplier( local_activity_slots if local_activity_slots else 100 ), + FixedSizeSlotSupplier(nexus_slots if nexus_slots else 100), ) - @staticmethod + @classmethod def create_composite( + cls, *, workflow_supplier: SlotSupplier, activity_supplier: SlotSupplier, local_activity_supplier: SlotSupplier, - ) -> "WorkerTuner": + nexus_supplier: SlotSupplier, + ) -> WorkerTuner: """Create a tuner composed of the provided slot suppliers.""" return _CompositeTuner( workflow_supplier, activity_supplier, local_activity_supplier, + nexus_supplier, ) @abstractmethod @@ -407,6 +391,10 @@ def _get_activity_task_slot_supplier(self) -> SlotSupplier: def _get_local_activity_task_slot_supplier(self) -> SlotSupplier: raise NotImplementedError + @abstractmethod + def _get_nexus_slot_supplier(self) -> SlotSupplier: + raise NotImplementedError + def _to_bridge_tuner(self) -> temporalio.bridge.worker.TunerHolder: return temporalio.bridge.worker.TunerHolder( _to_bridge_slot_supplier( @@ -418,14 +406,25 @@ def _to_bridge_tuner(self) -> temporalio.bridge.worker.TunerHolder: _to_bridge_slot_supplier( self._get_local_activity_task_slot_supplier(), "local_activity" ), + _to_bridge_slot_supplier(self._get_nexus_slot_supplier(), "nexus"), ) - def _get_activities_max(self) -> Optional[int]: - ss = self._get_activity_task_slot_supplier() - if isinstance(ss, FixedSizeSlotSupplier): - return ss.num_slots - elif isinstance(ss, ResourceBasedSlotSupplier): - return ss.slot_config.maximum_slots or _DEFAULT_RESOURCE_ACTIVITY_MAX + def _get_activities_max(self) -> int | None: + return WorkerTuner._get_slot_supplier_max( + self._get_activity_task_slot_supplier() + ) + + def _get_nexus_tasks_max(self) -> int | None: + return WorkerTuner._get_slot_supplier_max(self._get_nexus_slot_supplier()) + + @staticmethod + def _get_slot_supplier_max(slot_supplier: SlotSupplier) -> int | None: + if isinstance(slot_supplier, FixedSizeSlotSupplier): + return slot_supplier.num_slots + elif isinstance(slot_supplier, ResourceBasedSlotSupplier): + return ( + slot_supplier.slot_config.maximum_slots or _DEFAULT_RESOURCE_SLOTS_MAX + ) return None @@ -436,6 +435,7 @@ class _CompositeTuner(WorkerTuner): workflow_slot_supplier: SlotSupplier activity_slot_supplier: SlotSupplier local_activity_slot_supplier: SlotSupplier + nexus_slot_supplier: SlotSupplier def _get_workflow_task_slot_supplier(self) -> SlotSupplier: return self.workflow_slot_supplier @@ -445,3 +445,6 @@ def _get_activity_task_slot_supplier(self) -> SlotSupplier: def _get_local_activity_task_slot_supplier(self) -> SlotSupplier: return self.local_activity_slot_supplier + + def _get_nexus_slot_supplier(self) -> SlotSupplier: + return self.nexus_slot_supplier diff --git a/temporalio/worker/_worker.py b/temporalio/worker/_worker.py index f93848496..60f824c4d 100644 --- a/temporalio/worker/_worker.py +++ b/temporalio/worker/_worker.py @@ -8,21 +8,16 @@ import logging import sys import warnings +from collections.abc import Awaitable, Callable, Sequence from dataclasses import dataclass from datetime import timedelta from typing import ( Any, - Awaitable, - Callable, - List, - Optional, - Sequence, - Type, - Union, + TypeAlias, cast, ) -from typing_extensions import TypeAlias, TypedDict +from typing_extensions import TypedDict import temporalio.bridge.worker import temporalio.client @@ -38,10 +33,18 @@ from ._activity import SharedStateManager, _ActivityWorker from ._interceptor import Interceptor from ._nexus import _NexusWorker -from ._plugin import Plugin, _RootPlugin +from ._plugin import Plugin from ._tuning import WorkerTuner -from ._workflow import _WorkflowWorker -from ._workflow_instance import UnsandboxedWorkflowRunner, WorkflowRunner +from ._workflow import ( + _DEFAULT_WORKFLOW_TASK_EXTERNAL_STORAGE_CONCURRENCY, + _WorkflowWorker, +) +from ._workflow_instance import ( + PatchActivationInput, + UnsandboxedWorkflowRunner, + WorkflowRunner, + _WorkflowLogicFlag, +) from .workflow_sandbox import SandboxedWorkflowRunner logger = logging.getLogger(__name__) @@ -83,10 +86,7 @@ def _to_bridge(self) -> temporalio.bridge.worker.PollerBehavior: ) -PollerBehavior: TypeAlias = Union[ - PollerBehaviorSimpleMaximum, - PollerBehaviorAutoscaling, -] +PollerBehavior: TypeAlias = PollerBehaviorSimpleMaximum | PollerBehaviorAutoscaling class Worker: @@ -105,39 +105,42 @@ def __init__( task_queue: str, activities: Sequence[Callable] = [], nexus_service_handlers: Sequence[Any] = [], - workflows: Sequence[Type] = [], - activity_executor: Optional[concurrent.futures.Executor] = None, - workflow_task_executor: Optional[concurrent.futures.ThreadPoolExecutor] = None, - nexus_task_executor: Optional[concurrent.futures.Executor] = None, + workflows: Sequence[type] = [], + activity_executor: concurrent.futures.Executor | None = None, + workflow_task_executor: concurrent.futures.ThreadPoolExecutor | None = None, + nexus_task_executor: concurrent.futures.ThreadPoolExecutor | None = None, workflow_runner: WorkflowRunner = SandboxedWorkflowRunner(), unsandboxed_workflow_runner: WorkflowRunner = UnsandboxedWorkflowRunner(), plugins: Sequence[Plugin] = [], interceptors: Sequence[Interceptor] = [], - build_id: Optional[str] = None, - identity: Optional[str] = None, + build_id: str | None = None, + identity: str | None = None, max_cached_workflows: int = 1000, - max_concurrent_workflow_tasks: Optional[int] = None, - max_concurrent_activities: Optional[int] = None, - max_concurrent_local_activities: Optional[int] = None, - tuner: Optional[WorkerTuner] = None, - max_concurrent_workflow_task_polls: Optional[int] = None, + max_concurrent_workflow_tasks: int | None = None, + max_concurrent_activities: int | None = None, + max_concurrent_local_activities: int | None = None, + max_concurrent_nexus_tasks: int | None = None, + tuner: WorkerTuner | None = None, + max_concurrent_workflow_task_polls: int | None = None, nonsticky_to_sticky_poll_ratio: float = 0.2, - max_concurrent_activity_task_polls: Optional[int] = None, + max_concurrent_activity_task_polls: int | None = None, no_remote_activities: bool = False, sticky_queue_schedule_to_start_timeout: timedelta = timedelta(seconds=10), max_heartbeat_throttle_interval: timedelta = timedelta(seconds=60), default_heartbeat_throttle_interval: timedelta = timedelta(seconds=30), - max_activities_per_second: Optional[float] = None, - max_task_queue_activities_per_second: Optional[float] = None, + max_activities_per_second: float | None = None, + max_task_queue_activities_per_second: float | None = None, + max_eager_activity_reservations_per_workflow_task: int = 3, graceful_shutdown_timeout: timedelta = timedelta(), - workflow_failure_exception_types: Sequence[Type[BaseException]] = [], - shared_state_manager: Optional[SharedStateManager] = None, + workflow_failure_exception_types: Sequence[type[BaseException]] = [], + shared_state_manager: SharedStateManager | None = None, debug_mode: bool = False, disable_eager_activity_execution: bool = False, - on_fatal_error: Optional[Callable[[BaseException], Awaitable[None]]] = None, + on_fatal_error: Callable[[BaseException], Awaitable[None]] | None = None, use_worker_versioning: bool = False, disable_safe_workflow_eviction: bool = False, - deployment_config: Optional[WorkerDeploymentConfig] = None, + deployment_config: WorkerDeploymentConfig | None = None, + patch_activation_callback: Callable[[PatchActivationInput], bool] | None = None, workflow_task_poller_behavior: PollerBehavior = PollerBehaviorSimpleMaximum( maximum=5 ), @@ -147,6 +150,8 @@ def __init__( nexus_task_poller_behavior: PollerBehavior = PollerBehaviorSimpleMaximum( maximum=5 ), + disable_payload_error_limit: bool = False, + max_workflow_task_external_storage_concurrency: int = _DEFAULT_WORKFLOW_TASK_EXTERNAL_STORAGE_CONCURRENCY, ) -> None: """Create a worker to process workflows and/or activities. @@ -161,10 +166,7 @@ def __init__( :py:func:`@activity.defn`. Activities may be async functions or non-async functions. nexus_service_handlers: Instances of Nexus service handler classes - decorated with :py:func:`@nexusrpc.handler.service_handler`. - - .. warning:: - This parameter is experimental and unstable. + decorated with :py:func:`@nexusrpc.handler.service_handler`. workflows: Workflow classes decorated with :py:func:`@workflow.defn`. activity_executor: Concurrent executor to use for non-async @@ -186,11 +188,7 @@ def __init__( the worker is shut down. nexus_task_executor: Executor to use for non-async Nexus operations. This is required if any operation start methods - are non-`async def`. :py:class:`concurrent.futures.ThreadPoolExecutor` - is recommended. - - .. warning:: - This parameter is experimental and unstable. + are non-``async def``. workflow_runner: Runner for workflows. unsandboxed_workflow_runner: Runner for workflows that opt-out of sandboxing. @@ -202,9 +200,11 @@ def __init__( interceptors already on the client that also implement :py:class:`Interceptor` are prepended to this list and should not be explicitly given here. - build_id: Unique identifier for the current runtime. This is best - set as a hash of all code and should change only when code does. - If unset, a best-effort identifier is generated. + build_id: A unique identifier for the current runtime, ideally provided as a + representation of the complete source code. If not explicitly set, the system + automatically generates a best-effort identifier by traversing and computing + hashes of all modules in the codebase. In very large codebases this automatic + process may significantly increase initialization time. Exclusive with `deployment_config`. WARNING: Deprecated. Use `deployment_config` instead. identity: Identity for this worker client. If unset, the client @@ -214,18 +214,19 @@ def __init__( max_concurrent_workflow_tasks: Maximum allowed number of workflow tasks that will ever be given to this worker at one time. Mutually exclusive with ``tuner``. Must be set to at least two if ``max_cached_workflows`` is nonzero. - max_concurrent_activities: Maximum number of activity tasks that - will ever be given to the activity worker concurrently. Mutually exclusive with ``tuner``. + max_concurrent_activities: Maximum number of activity tasks that will ever be given to + the activity worker concurrently. Mutually exclusive with ``tuner``. max_concurrent_local_activities: Maximum number of local activity - tasks that will ever be given to the activity worker concurrently. Mutually exclusive with ``tuner``. + tasks that will ever be given to the activity worker concurrently. Mutually + exclusive with ``tuner``. + max_concurrent_nexus_tasks: Maximum number of Nexus tasks that will ever be given to + the Nexus worker concurrently. Mutually exclusive with ``tuner``. tuner: Provide a custom :py:class:`WorkerTuner`. Mutually exclusive with the - ``max_concurrent_workflow_tasks``, ``max_concurrent_activities``, and - ``max_concurrent_local_activities`` arguments. + ``max_concurrent_workflow_tasks``, ``max_concurrent_activities``, + ``max_concurrent_local_activities``, and ``max_concurrent_nexus_tasks`` arguments. Defaults to fixed-size 100 slots for each slot kind if unset and none of the max_* arguments are provided. - - WARNING: This argument is experimental max_concurrent_workflow_task_polls: Maximum number of concurrent poll workflow task requests we will perform at a time on this worker's task queue. Must be set to at least two if ``max_cached_workflows`` is nonzero. @@ -268,6 +269,11 @@ def __init__( poll request. If multiple workers on the same queue have different values set, they will thrash with the last poller winning. + max_eager_activity_reservations_per_workflow_task: Maximum number of + activity slots that may be reserved for eager execution when + completing a workflow task. The default is 3 and the value must + be positive. To disable eager activity execution, set + ``disable_eager_activity_execution`` to ``True``. graceful_shutdown_timeout: Amount of time after shutdown is called that activities are given to complete before their tasks are cancelled. @@ -294,14 +300,14 @@ def __init__( on_fatal_error: An async function that can handle a failure before the worker shutdown commences. This cannot stop the shutdown and any exception raised is logged and ignored. - use_worker_versioning: If true, the `build_id` argument must be + use_worker_versioning: If true, the ``build_id`` argument must be specified, and this worker opts into the worker versioning feature. This ensures it only receives workflow tasks for workflows which it claims to be compatible with. For more information, see https://docs.temporal.io/workers#worker-versioning. - Exclusive with `deployment_config`. - WARNING: Deprecated. Use `deployment_config` instead. + Exclusive with ``deployment_config``. + WARNING: Deprecated. Use ``deployment_config`` instead. disable_safe_workflow_eviction: If true, instead of letting the workflow collect its tasks properly, the worker will simply let the Python garbage collector collect the tasks. WARNING: Users @@ -309,8 +315,14 @@ def __init__( throw ``GeneratorExit`` in coroutines causing them to wake up in different threads and run ``finally`` and other code in the wrong workflow environment. - deployment_config: Deployment config for the worker. Exclusive with `build_id` and - `use_worker_versioning`. + deployment_config: Deployment config for the worker. Exclusive with ``build_id`` and + ``use_worker_versioning``. + WARNING: This is an experimental feature and may change in the future. + patch_activation_callback: Callback to decide whether the first non-replay + call to :py:func:`workflow.patched` for a + patch ID should activate that patch. The callback receives a + :py:class:`PatchActivationInput` and must return ``True`` to activate the + patch or ``False`` to leave it inactive. WARNING: This is an experimental feature and may change in the future. workflow_task_poller_behavior: Specify the behavior of workflow task polling. Defaults to a 5-poller maximum. @@ -318,6 +330,21 @@ def __init__( Defaults to a 5-poller maximum. nexus_task_poller_behavior: Specify the behavior of Nexus task polling. Defaults to a 5-poller maximum. + disable_payload_error_limit: If true, payload and memo error limit checks + are disabled in the worker, allowing payloads and memos that are above + the server error limit to be submitted to the Temporal server. If false, + the worker will validate the size before submitting to the Temporal server, + and cause a task failure if the size limit is exceeded. The default is False. + See https://docs.temporal.io/troubleshooting/blob-size-limit-error for more + details. + max_workflow_task_external_storage_concurrency: Maximum number of + external storage payload operations (store/retrieve) that may run + concurrently within a single workflow task activation. + Defaults to 3. Adjust this value based on your workload's needs. + Please report any issues you encounter with this setting or if you + feel the default should be changed. + WARNING: This setting is experimental. + """ config = WorkerConfig( client=client, @@ -330,6 +357,7 @@ def __init__( nexus_task_executor=nexus_task_executor, workflow_runner=workflow_runner, unsandboxed_workflow_runner=unsandboxed_workflow_runner, + plugins=plugins, interceptors=interceptors, build_id=build_id, identity=identity, @@ -337,6 +365,7 @@ def __init__( max_concurrent_workflow_tasks=max_concurrent_workflow_tasks, max_concurrent_activities=max_concurrent_activities, max_concurrent_local_activities=max_concurrent_local_activities, + max_concurrent_nexus_tasks=max_concurrent_nexus_tasks, tuner=tuner, max_concurrent_workflow_task_polls=max_concurrent_workflow_task_polls, nonsticky_to_sticky_poll_ratio=nonsticky_to_sticky_poll_ratio, @@ -347,6 +376,7 @@ def __init__( default_heartbeat_throttle_interval=default_heartbeat_throttle_interval, max_activities_per_second=max_activities_per_second, max_task_queue_activities_per_second=max_task_queue_activities_per_second, + max_eager_activity_reservations_per_workflow_task=max_eager_activity_reservations_per_workflow_task, graceful_shutdown_timeout=graceful_shutdown_timeout, workflow_failure_exception_types=workflow_failure_exception_types, shared_state_manager=shared_state_manager, @@ -356,13 +386,16 @@ def __init__( use_worker_versioning=use_worker_versioning, disable_safe_workflow_eviction=disable_safe_workflow_eviction, deployment_config=deployment_config, + patch_activation_callback=patch_activation_callback, workflow_task_poller_behavior=workflow_task_poller_behavior, activity_task_poller_behavior=activity_task_poller_behavior, nexus_task_poller_behavior=nexus_task_poller_behavior, + disable_payload_error_limit=disable_payload_error_limit, + max_workflow_task_external_storage_concurrency=max_workflow_task_external_storage_concurrency, ) plugins_from_client = cast( - List[Plugin], + list[Plugin], [p for p in client.config()["plugins"] if isinstance(p, Plugin)], ) for client_plugin in plugins_from_client: @@ -371,13 +404,11 @@ def __init__( f"The same plugin type {type(client_plugin)} is present from both client and worker. It may run twice and may not be the intended behavior." ) plugins = plugins_from_client + list(plugins) + self._initial_config = config.copy() - root_plugin: Plugin = _RootPlugin() - for plugin in reversed(plugins): - plugin.init_worker_plugin(root_plugin) - root_plugin = plugin - config = root_plugin.configure_worker(config) - self._plugin = root_plugin + self._plugins = plugins + for plugin in plugins: + config = plugin.configure_worker(config) self._init_from_config(client, config) @@ -386,72 +417,83 @@ def _init_from_config(self, client: temporalio.client.Client, config: WorkerConf Client is safe to take separately since it can't be modified by worker plugins. """ self._config = config - - # TODO(nexus-preview): max_concurrent_nexus_tasks / tuner support if not ( - config["activities"] - or config["nexus_service_handlers"] - or config["workflows"] + config.get("activities") + or config.get("nexus_service_handlers") + or config.get("workflows") ): raise ValueError( "At least one activity, Nexus service, or workflow must be specified" ) - if config["use_worker_versioning"] and not config["build_id"]: + if config.get("use_worker_versioning") and not config.get("build_id"): raise ValueError( "build_id must be specified when use_worker_versioning is True" ) - if config["deployment_config"] and ( - config["build_id"] or config["use_worker_versioning"] + if config.get("deployment_config") and ( + config.get("build_id") or config.get("use_worker_versioning") ): raise ValueError( "deployment_config cannot be used with build_id or use_worker_versioning" ) + _deployment_config = config.get("deployment_config") + if ( + _deployment_config is not None + and not _deployment_config.use_worker_versioning + and _deployment_config.default_versioning_behavior + != VersioningBehavior.UNSPECIFIED + ): + raise ValueError( + "default_versioning_behavior must be UNSPECIFIED when use_worker_versioning is False" + ) + max_workflow_task_external_storage_concurrency = config.get( + "max_workflow_task_external_storage_concurrency", + _DEFAULT_WORKFLOW_TASK_EXTERNAL_STORAGE_CONCURRENCY, + ) + if max_workflow_task_external_storage_concurrency < 1: + raise ValueError( + "max_workflow_task_external_storage_concurrency must be positive" + ) + if config.get("max_eager_activity_reservations_per_workflow_task", 3) < 1: + raise ValueError( + "max_eager_activity_reservations_per_workflow_task must be positive; " + "use disable_eager_activity_execution=True to disable eager activity execution" + ) # Prepend applicable client interceptors to the given ones - client_config = config["client"].config() + client_config = config["client"].config(active_config=True) # type: ignore[reportTypedDictNotRequiredAccess] interceptors_from_client = cast( - List[Interceptor], + list[Interceptor], [i for i in client_config["interceptors"] if isinstance(i, Interceptor)], ) - interceptors = interceptors_from_client + list(config["interceptors"]) + interceptors = interceptors_from_client + list(config["interceptors"]) # type: ignore[reportTypedDictNotRequiredAccess] + + # Extract storage drivers from the client's data converter + _ext_storage = client_config["data_converter"].external_storage + self._storage_drivers = list(_ext_storage.drivers) if _ext_storage else [] # Extract the bridge service client - bridge_client = _extract_bridge_client_for_worker(config["client"]) + bridge_client = _extract_bridge_client_for_worker(config["client"]) # type: ignore[reportTypedDictNotRequiredAccess] self._started = False self._shutdown_event = asyncio.Event() self._shutdown_complete_event = asyncio.Event() - self._async_context_inner_task: Optional[asyncio.Task] = None - self._async_context_run_task: Optional[asyncio.Task] = None - self._async_context_run_exception: Optional[BaseException] = None + self._async_context_inner_task: asyncio.Task | None = None + self._async_context_run_task: asyncio.Task | None = None + self._async_context_run_exception: BaseException | None = None - self._activity_worker: Optional[_ActivityWorker] = None + self._activity_worker: _ActivityWorker | None = None self._runtime = ( bridge_client.config.runtime or temporalio.runtime.Runtime.default() ) - if config["activities"]: - # Issue warning here if executor max_workers is lower than max - # concurrent activities. We do this here instead of in - # _ActivityWorker so the stack level is predictable. - max_workers = getattr(config["activity_executor"], "_max_workers", None) - concurrent_activities = config["max_concurrent_activities"] - if config["tuner"] and config["tuner"]._get_activities_max(): - concurrent_activities = config["tuner"]._get_activities_max() - if isinstance(max_workers, int) and max_workers < ( - concurrent_activities or 0 - ): - warnings.warn( - f"Worker max_concurrent_activities is {concurrent_activities} " - + f"but activity_executor's max_workers is only {max_workers}", - stacklevel=2, - ) - + activities = config.get("activities") + if activities: + _warn_if_activity_executor_max_workers_is_inconsistent(config) self._activity_worker = _ActivityWorker( bridge_worker=lambda: self._bridge_worker, - task_queue=config["task_queue"], - activities=config["activities"], - activity_executor=config["activity_executor"], - shared_state_manager=config["shared_state_manager"], + task_queue=config["task_queue"], # type: ignore[reportTypedDictNotRequiredAccess] + activities=activities, + activity_executor=config.get("activity_executor"), + shared_state_manager=config.get("shared_state_manager"), data_converter=client_config["data_converter"], interceptors=interceptors, metric_meter=self._runtime.metric_meter, @@ -460,28 +502,33 @@ def _init_from_config(self, client: temporalio.client.Client, config: WorkerConf client_config["header_codec_behavior"] == HeaderCodecBehavior.CODEC ), ) - self._nexus_worker: Optional[_NexusWorker] = None - if config["nexus_service_handlers"]: + self._nexus_worker: _NexusWorker | None = None + nexus_service_handlers = config.get("nexus_service_handlers") + if nexus_service_handlers: + _warn_if_nexus_task_executor_max_workers_is_inconsistent(config) self._nexus_worker = _NexusWorker( bridge_worker=lambda: self._bridge_worker, - client=config["client"], - task_queue=config["task_queue"], - service_handlers=config["nexus_service_handlers"], + client=config["client"], # type: ignore[reportTypedDictNotRequiredAccess] + namespace=client_config["namespace"], + task_queue=config["task_queue"], # type: ignore[reportTypedDictNotRequiredAccess] + service_handlers=nexus_service_handlers, data_converter=client_config["data_converter"], interceptors=interceptors, metric_meter=self._runtime.metric_meter, - executor=config["nexus_task_executor"], + executor=config.get("nexus_task_executor"), ) - self._workflow_worker: Optional[_WorkflowWorker] = None - if config["workflows"]: + self._workflow_worker: _WorkflowWorker | None = None + workflows = config.get("workflows") + if workflows: + deployment_config = config.get("deployment_config") should_enforce_versioning_behavior = ( - config["deployment_config"] is not None - and config["deployment_config"].use_worker_versioning - and config["deployment_config"].default_versioning_behavior + deployment_config is not None + and deployment_config.use_worker_versioning + and deployment_config.default_versioning_behavior == temporalio.common.VersioningBehavior.UNSPECIFIED ) - def check_activity(activity): + def check_activity(activity: str): if self._activity_worker is None: raise ValueError( f"Activity function {activity} " @@ -491,80 +538,95 @@ def check_activity(activity): self._workflow_worker = _WorkflowWorker( bridge_worker=lambda: self._bridge_worker, - namespace=config["client"].namespace, - task_queue=config["task_queue"], - workflows=config["workflows"], - workflow_task_executor=config["workflow_task_executor"], - max_concurrent_workflow_tasks=config["max_concurrent_workflow_tasks"], - workflow_runner=config["workflow_runner"], - unsandboxed_workflow_runner=config["unsandboxed_workflow_runner"], + namespace=config["client"].namespace, # type: ignore[reportTypedDictNotRequiredAccess] + task_queue=config["task_queue"], # type: ignore[reportTypedDictNotRequiredAccess] + workflows=workflows, + workflow_task_executor=config.get("workflow_task_executor"), + max_concurrent_workflow_tasks=config.get( + "max_concurrent_workflow_tasks" + ), + workflow_runner=config["workflow_runner"], # type: ignore[reportTypedDictNotRequiredAccess] + unsandboxed_workflow_runner=config["unsandboxed_workflow_runner"], # type: ignore[reportTypedDictNotRequiredAccess] data_converter=client_config["data_converter"], interceptors=interceptors, workflow_failure_exception_types=config[ "workflow_failure_exception_types" - ], - debug_mode=config["debug_mode"], + ], # type: ignore[reportTypedDictNotRequiredAccess] + patch_activation_callback=config.get("patch_activation_callback"), + debug_mode=config["debug_mode"], # type: ignore[reportTypedDictNotRequiredAccess] disable_eager_activity_execution=config[ "disable_eager_activity_execution" - ], + ], # type: ignore[reportTypedDictNotRequiredAccess] metric_meter=self._runtime.metric_meter, on_eviction_hook=None, - disable_safe_eviction=config["disable_safe_workflow_eviction"], + disable_safe_eviction=config["disable_safe_workflow_eviction"], # type: ignore[reportTypedDictNotRequiredAccess] should_enforce_versioning_behavior=should_enforce_versioning_behavior, assert_local_activity_valid=check_activity, encode_headers=client_config["header_codec_behavior"] != HeaderCodecBehavior.NO_CODEC, + max_workflow_task_external_storage_concurrency=max_workflow_task_external_storage_concurrency, ) - tuner = config["tuner"] + tuner = config.get("tuner") if tuner is not None: if ( - config["max_concurrent_workflow_tasks"] - or config["max_concurrent_activities"] - or config["max_concurrent_local_activities"] + config.get("max_concurrent_workflow_tasks") + or config.get("max_concurrent_activities") + or config.get("max_concurrent_local_activities") + or config.get("max_concurrent_nexus_tasks") ): raise ValueError( "Cannot specify max_concurrent_workflow_tasks, max_concurrent_activities, " - "or max_concurrent_local_activities when also specifying tuner" + "max_concurrent_local_activities, or max_concurrent_nexus_tasks when also " + "specifying tuner" ) else: tuner = WorkerTuner.create_fixed( - workflow_slots=config["max_concurrent_workflow_tasks"], - activity_slots=config["max_concurrent_activities"], - local_activity_slots=config["max_concurrent_local_activities"], + workflow_slots=config.get("max_concurrent_workflow_tasks"), + activity_slots=config.get("max_concurrent_activities"), + local_activity_slots=config.get("max_concurrent_local_activities"), + nexus_slots=config.get("max_concurrent_nexus_tasks"), ) bridge_tuner = tuner._to_bridge_tuner() versioning_strategy: temporalio.bridge.worker.WorkerVersioningStrategy - if config["deployment_config"]: - versioning_strategy = config[ - "deployment_config" - ]._to_bridge_worker_deployment_options() - elif config["use_worker_versioning"]: - build_id = config["build_id"] or load_default_build_id() + deployment_config = config.get("deployment_config") + if deployment_config: + versioning_strategy = ( + deployment_config._to_bridge_worker_deployment_options() + ) + elif config.get("use_worker_versioning"): + build_id = config.get("build_id") or load_default_build_id() versioning_strategy = ( temporalio.bridge.worker.WorkerVersioningStrategyLegacyBuildIdBased( build_id_with_versioning=build_id ) ) else: - build_id = config["build_id"] or load_default_build_id() + build_id = config.get("build_id") or load_default_build_id() versioning_strategy = temporalio.bridge.worker.WorkerVersioningStrategyNone( build_id_no_versioning=build_id ) - workflow_task_poller_behavior = config["workflow_task_poller_behavior"] - if config["max_concurrent_workflow_task_polls"]: + workflow_task_poller_behavior = config["workflow_task_poller_behavior"] # type: ignore[reportTypedDictNotRequiredAccess] + max_workflow_polls = config.get("max_concurrent_workflow_task_polls") + if max_workflow_polls: workflow_task_poller_behavior = PollerBehaviorSimpleMaximum( - maximum=config["max_concurrent_workflow_task_polls"] + maximum=max_workflow_polls ) - activity_task_poller_behavior = config["activity_task_poller_behavior"] - if config["max_concurrent_activity_task_polls"]: + activity_task_poller_behavior = config["activity_task_poller_behavior"] # type: ignore[reportTypedDictNotRequiredAccess] + max_activity_polls = config.get("max_concurrent_activity_task_polls") + if max_activity_polls: activity_task_poller_behavior = PollerBehaviorSimpleMaximum( - maximum=config["max_concurrent_activity_task_polls"] + maximum=max_activity_polls ) + deduped_plugin_names = list({plugin.name() for plugin in self._plugins}) + deduped_storage_driver_types = { + driver.type() for driver in self._storage_drivers + } + # Create bridge worker last. We have empirically observed that if it is # created before an error is raised from the activity worker # constructor, a deadlock/hang will occur presumably while trying to @@ -575,33 +637,44 @@ def check_activity(activity): self._bridge_worker = temporalio.bridge.worker.Worker.create( bridge_client._bridge_client, temporalio.bridge.worker.WorkerConfig( - namespace=config["client"].namespace, - task_queue=config["task_queue"], - identity_override=config["identity"], - max_cached_workflows=config["max_cached_workflows"], + namespace=config["client"].namespace, # type: ignore[reportTypedDictNotRequiredAccess] + task_queue=config["task_queue"], # type: ignore[reportTypedDictNotRequiredAccess] + identity_override=config.get("identity"), + max_cached_workflows=config["max_cached_workflows"], # type: ignore[reportTypedDictNotRequiredAccess] tuner=bridge_tuner, - nonsticky_to_sticky_poll_ratio=config["nonsticky_to_sticky_poll_ratio"], + nonsticky_to_sticky_poll_ratio=config["nonsticky_to_sticky_poll_ratio"], # type: ignore[reportTypedDictNotRequiredAccess] # We have to disable remote activities if a user asks _or_ if we # are not running an activity worker at all. Otherwise shutdown # will not proceed properly. - no_remote_activities=config["no_remote_activities"] - or not config["activities"], + no_remote_activities=config.get("no_remote_activities") + or not config.get("activities"), + task_types=temporalio.bridge.worker.WorkerTaskTypes( + enable_workflows=self._workflow_worker is not None, + enable_local_activities=self._activity_worker is not None + and self._workflow_worker is not None, + enable_remote_activities=self._activity_worker is not None + and not config.get("no_remote_activities"), + enable_nexus=self._nexus_worker is not None, + ), sticky_queue_schedule_to_start_timeout_millis=int( 1000 - * config["sticky_queue_schedule_to_start_timeout"].total_seconds() + * config["sticky_queue_schedule_to_start_timeout"].total_seconds() # type: ignore[reportTypedDictNotRequiredAccess] ), max_heartbeat_throttle_interval_millis=int( - 1000 * config["max_heartbeat_throttle_interval"].total_seconds() + 1000 * config["max_heartbeat_throttle_interval"].total_seconds() # type: ignore[reportTypedDictNotRequiredAccess] ), default_heartbeat_throttle_interval_millis=int( - 1000 * config["default_heartbeat_throttle_interval"].total_seconds() + 1000 * config["default_heartbeat_throttle_interval"].total_seconds() # type: ignore[reportTypedDictNotRequiredAccess] ), - max_activities_per_second=config["max_activities_per_second"], + max_activities_per_second=config.get("max_activities_per_second"), max_task_queue_activities_per_second=config[ "max_task_queue_activities_per_second" - ], + ], # type: ignore[reportTypedDictNotRequiredAccess] + max_eager_activity_reservations_per_workflow_task=config[ + "max_eager_activity_reservations_per_workflow_task" + ], # type: ignore[reportTypedDictNotRequiredAccess] graceful_shutdown_period_millis=int( - 1000 * config["graceful_shutdown_timeout"].total_seconds() + 1000 * config["graceful_shutdown_timeout"].total_seconds() # type: ignore[reportTypedDictNotRequiredAccess] ), # Need to tell core whether we want to consider all # non-determinism exceptions as workflow fail, and whether we do @@ -618,17 +691,26 @@ def check_activity(activity): activity_task_poller_behavior=activity_task_poller_behavior._to_bridge(), nexus_task_poller_behavior=config[ "nexus_task_poller_behavior" - ]._to_bridge(), + ]._to_bridge(), # type: ignore[reportTypedDictNotRequiredAccess,reportOptionalMemberAccess] + plugins=deduped_plugin_names, + storage_drivers=deduped_storage_driver_types, + disable_payload_error_limit=config.get( + "disable_payload_error_limit", False + ), ), ) - def config(self) -> WorkerConfig: + def config(self, *, active_config: bool = False) -> WorkerConfig: """Config, as a dictionary, used to create this worker. + Args: + active_config: If true, return the modified configuration in use rather than the initial one + provided to the worker. + Returns: Configuration, shallow-copied. """ - config = self._config.copy() + config = self._config.copy() if active_config else self._initial_config.copy() config["activities"] = list(config.get("activities", [])) config["workflows"] = list(config.get("workflows", [])) return config @@ -661,6 +743,25 @@ def client(self, value: temporalio.client.Client) -> None: self._bridge_worker.replace_client(bridge_client._bridge_client) self._config["client"] = value + # Update the activity worker's client reference if activities are configured + if self._activity_worker: + self._activity_worker._client = value + + # Update the nexus worker's client reference if nexus services are configured + if self._nexus_worker: + self._nexus_worker._client = value + + def _set_default_workflow_logic_flag( + self, flag: _WorkflowLogicFlag, *, enabled: bool + ) -> None: + if self._started: + raise RuntimeError( + "Cannot set default workflow logic flags after the worker has started" + ) + if not self._workflow_worker: + raise RuntimeError("Cannot set workflow logic flags without workflows") + self._workflow_worker._set_default_workflow_logic_flag(flag, enabled=enabled) + @property def is_running(self) -> bool: """Whether the worker is running. @@ -697,10 +798,18 @@ async def run(self) -> None: also cancel the shutdown process. Therefore users are encouraged to use explicit shutdown instead. """ - await self._plugin.run_worker(self) + + def make_lambda(plugin: Plugin, next: Callable[[Worker], Awaitable[None]]): + return lambda w: plugin.run_worker(w, next) + + next_function = lambda w: w._run() + for plugin in reversed(self._plugins): + next_function = make_lambda(plugin, next_function) + + await next_function(self) async def _run(self): - # Eagerly validate which will do a namespace check in Core + # Eagerly validate which will do a namespace check in Core. await self._bridge_worker.validate() if self._started: @@ -716,7 +825,7 @@ async def raise_on_shutdown(): pass tasks: dict[ - Union[None, _ActivityWorker, _WorkflowWorker, _NexusWorker], asyncio.Task + None | _ActivityWorker | _WorkflowWorker | _NexusWorker, asyncio.Task ] = {None: asyncio.create_task(raise_on_shutdown())} # Create tasks for workers if self._activity_worker: @@ -772,6 +881,8 @@ async def raise_on_shutdown(): self._activity_worker.notify_shutdown() if self._workflow_worker: self._workflow_worker.notify_shutdown() + if self._nexus_worker: + self._nexus_worker.notify_shutdown() # Wait for all tasks to complete (i.e. for poller loops to stop) await asyncio.wait(tasks.values()) @@ -790,8 +901,6 @@ async def raise_on_shutdown(): if self._nexus_worker: await self._nexus_worker.wait_all_completed() - # TODO(nexus-preview): check that we do all appropriate things for nexus worker that we do for activity worker - # Do final shutdown try: await self._bridge_worker.finalize_shutdown() @@ -846,7 +955,7 @@ async def run(): self._async_context_run_task = asyncio.create_task(run()) return self - async def __aexit__(self, exc_type: Optional[Type[BaseException]], *args) -> None: + async def __aexit__(self, exc_type: type[BaseException] | None, *args: Any) -> None: """Same as :py:meth:`shutdown` for use by ``async with``. Note, this will raise the worker fatal error if one occurred and the @@ -870,49 +979,86 @@ class WorkerConfig(TypedDict, total=False): task_queue: str activities: Sequence[Callable] nexus_service_handlers: Sequence[Any] - workflows: Sequence[Type] - activity_executor: Optional[concurrent.futures.Executor] - workflow_task_executor: Optional[concurrent.futures.ThreadPoolExecutor] - nexus_task_executor: Optional[concurrent.futures.Executor] + workflows: Sequence[type] + activity_executor: concurrent.futures.Executor | None + workflow_task_executor: concurrent.futures.ThreadPoolExecutor | None + nexus_task_executor: concurrent.futures.ThreadPoolExecutor | None workflow_runner: WorkflowRunner unsandboxed_workflow_runner: WorkflowRunner + plugins: Sequence[Plugin] interceptors: Sequence[Interceptor] - build_id: Optional[str] - identity: Optional[str] + build_id: str | None + identity: str | None max_cached_workflows: int - max_concurrent_workflow_tasks: Optional[int] - max_concurrent_activities: Optional[int] - max_concurrent_local_activities: Optional[int] - tuner: Optional[WorkerTuner] - max_concurrent_workflow_task_polls: Optional[int] + max_concurrent_workflow_tasks: int | None + max_concurrent_activities: int | None + max_concurrent_local_activities: int | None + max_concurrent_nexus_tasks: int | None + tuner: WorkerTuner | None + max_concurrent_workflow_task_polls: int | None nonsticky_to_sticky_poll_ratio: float - max_concurrent_activity_task_polls: Optional[int] + max_concurrent_activity_task_polls: int | None no_remote_activities: bool sticky_queue_schedule_to_start_timeout: timedelta max_heartbeat_throttle_interval: timedelta default_heartbeat_throttle_interval: timedelta - max_activities_per_second: Optional[float] - max_task_queue_activities_per_second: Optional[float] + max_activities_per_second: float | None + max_task_queue_activities_per_second: float | None + max_eager_activity_reservations_per_workflow_task: int graceful_shutdown_timeout: timedelta - workflow_failure_exception_types: Sequence[Type[BaseException]] - shared_state_manager: Optional[SharedStateManager] + workflow_failure_exception_types: Sequence[type[BaseException]] + shared_state_manager: SharedStateManager | None debug_mode: bool disable_eager_activity_execution: bool - on_fatal_error: Optional[Callable[[BaseException], Awaitable[None]]] + on_fatal_error: Callable[[BaseException], Awaitable[None]] | None use_worker_versioning: bool disable_safe_workflow_eviction: bool - deployment_config: Optional[WorkerDeploymentConfig] + deployment_config: WorkerDeploymentConfig | None + patch_activation_callback: Callable[[PatchActivationInput], bool] | None workflow_task_poller_behavior: PollerBehavior activity_task_poller_behavior: PollerBehavior nexus_task_poller_behavior: PollerBehavior + disable_payload_error_limit: bool + max_workflow_task_external_storage_concurrency: int + + +def _warn_if_activity_executor_max_workers_is_inconsistent( + config: WorkerConfig, +) -> None: + activity_executor = config.get("activity_executor") + max_workers = getattr(activity_executor, "_max_workers", None) + concurrent_activities = config.get("max_concurrent_activities") + tuner = config.get("tuner") + if tuner and tuner._get_activities_max(): + concurrent_activities = tuner._get_activities_max() + if isinstance(max_workers, int) and max_workers < (concurrent_activities or 0): + warnings.warn( + f"Worker max_concurrent_activities is {concurrent_activities} " + + f"but activity_executor's max_workers is only {max_workers}", + stacklevel=3, + ) + + +def _warn_if_nexus_task_executor_max_workers_is_inconsistent( + config: WorkerConfig, +) -> None: + nexus_task_executor = config.get("nexus_task_executor") + max_workers = getattr(nexus_task_executor, "_max_workers", None) + concurrent_nexus_tasks = config.get("max_concurrent_nexus_tasks") + tuner = config.get("tuner") + if tuner and tuner._get_nexus_tasks_max(): + concurrent_nexus_tasks = tuner._get_nexus_tasks_max() + if isinstance(max_workers, int) and max_workers < (concurrent_nexus_tasks or 0): + warnings.warn( + f"Worker max_concurrent_nexus_tasks is {concurrent_nexus_tasks} " + + f"but nexus_task_executor's max_workers is only {max_workers}", + stacklevel=3, + ) @dataclass class WorkerDeploymentConfig: - """Options for configuring the Worker Versioning feature. - - WARNING: This is an experimental feature and may change in the future. - """ + """Options for configuring the Worker Versioning feature.""" version: WorkerDeploymentVersion use_worker_versioning: bool @@ -931,7 +1077,7 @@ def _to_bridge_worker_deployment_options( ) -_default_build_id: Optional[str] = None +_default_build_id: str | None = None def load_default_build_id(*, memoize: bool = True) -> str: @@ -965,10 +1111,7 @@ def load_default_build_id(*, memoize: bool = True) -> str: # * Using the loader's get_code in rare cases can cause a compile() got_temporal_code = False - if sys.version_info < (3, 9): - m = hashlib.md5() - else: - m = hashlib.md5(usedforsecurity=False) + m = hashlib.md5(usedforsecurity=False) for mod_name in sorted(sys.modules): # Try to read code code = _get_module_code(mod_name) @@ -993,7 +1136,7 @@ def load_default_build_id(*, memoize: bool = True) -> str: return digest -def _get_module_code(mod_name: str) -> Optional[bytes]: +def _get_module_code(mod_name: str) -> bytes | None: # First try the module's loader and if that fails, try __cached__ file try: loader: Any = sys.modules[mod_name].__loader__ @@ -1025,7 +1168,7 @@ def _extract_bridge_client_for_worker( elif hasattr(client.service_client, "worker_service_client"): bridge_client = client.service_client.worker_service_client if not isinstance(bridge_client, temporalio.service._BridgeServiceClient): - raise TypeError( + raise TypeError( # type: ignore[reportUnreachable] "Client's worker_service_client cannot be used for a worker" ) else: diff --git a/temporalio/worker/_workflow.py b/temporalio/worker/_workflow.py index 1e178f015..c031b5653 100644 --- a/temporalio/worker/_workflow.py +++ b/temporalio/worker/_workflow.py @@ -4,47 +4,49 @@ import asyncio import concurrent.futures +import dataclasses import logging import os import sys import threading -from datetime import timezone +import time +from collections.abc import Awaitable, Callable, MutableMapping, Sequence +from dataclasses import dataclass +from datetime import timedelta, timezone from types import TracebackType -from typing import ( - Awaitable, - Callable, - Dict, - List, - MutableMapping, - Optional, - Sequence, - Set, - Type, -) -import temporalio.activity import temporalio.api.common.v1 -import temporalio.bridge.client import temporalio.bridge.proto.workflow_activation import temporalio.bridge.proto.workflow_completion import temporalio.bridge.runtime import temporalio.bridge.worker -import temporalio.client import temporalio.common import temporalio.converter +import temporalio.converter._extstore import temporalio.exceptions import temporalio.workflow - +from temporalio.bridge.worker import PollShutdownError +from temporalio.converter import StorageDriverStoreContext, StorageDriverWorkflowInfo +from temporalio.worker.workflow_sandbox._runner import SandboxedWorkflowRunner + +from . import _command_aware_visitor +from ._debugger import ( + _install_workflow_breakpoint_hook, + _relax_sandbox_for_debugger, +) from ._interceptor import ( Interceptor, WorkflowInboundInterceptor, WorkflowInterceptorClassInput, ) from ._workflow_instance import ( + _DEFAULT_ENABLED_WORKFLOW_LOGIC_FLAGS, + PatchActivationInput, WorkflowInstance, WorkflowInstanceDetails, WorkflowRunner, _WorkflowExternFunctions, + _WorkflowLogicFlag, ) logger = logging.getLogger(__name__) @@ -53,37 +55,56 @@ LOG_PROTOS = False -class _WorkflowWorker: +# Value was chosen abitrarily as a small number that allows some concurrency and prevents +# large numbers of concurrent external storage operations causing resource contention. +# This default limit is per workflow task activation and does not limit the total number +# of concurrent external storage operations across all workflow task activations. +# Advise customers to adjust based on their workload needs and to report issues with the +# value if problems are encountered. This setting is experimental. +_DEFAULT_WORKFLOW_TASK_EXTERNAL_STORAGE_CONCURRENCY: int = 3 + + +class _WorkflowWorker: # type:ignore[reportUnusedClass] def __init__( self, *, bridge_worker: Callable[[], temporalio.bridge.worker.Worker], namespace: str, task_queue: str, - workflows: Sequence[Type], - workflow_task_executor: Optional[concurrent.futures.ThreadPoolExecutor], - max_concurrent_workflow_tasks: Optional[int], + workflows: Sequence[type], + workflow_task_executor: concurrent.futures.ThreadPoolExecutor | None, + max_concurrent_workflow_tasks: int | None, workflow_runner: WorkflowRunner, unsandboxed_workflow_runner: WorkflowRunner, data_converter: temporalio.converter.DataConverter, interceptors: Sequence[Interceptor], - workflow_failure_exception_types: Sequence[Type[BaseException]], + workflow_failure_exception_types: Sequence[type[BaseException]], + patch_activation_callback: Callable[[PatchActivationInput], bool] | None, debug_mode: bool, disable_eager_activity_execution: bool, metric_meter: temporalio.common.MetricMeter, - on_eviction_hook: Optional[ - Callable[ - [str, temporalio.bridge.proto.workflow_activation.RemoveFromCache], None - ] - ], + on_eviction_hook: Callable[ + [str, temporalio.bridge.proto.workflow_activation.RemoveFromCache], None + ] + | None, disable_safe_eviction: bool, should_enforce_versioning_behavior: bool, assert_local_activity_valid: Callable[[str], None], encode_headers: bool, + max_workflow_task_external_storage_concurrency: int, + default_workflow_logic_flags: frozenset[_WorkflowLogicFlag] | None = None, ) -> None: + # Debug mode is enabled if specified or if the TEMPORAL_DEBUG env var is truthy + debug_mode = debug_mode or bool(os.environ.get("TEMPORAL_DEBUG")) + self._bridge_worker = bridge_worker self._namespace = namespace self._task_queue = task_queue + self._default_workflow_logic_flags = set( + _DEFAULT_ENABLED_WORKFLOW_LOGIC_FLAGS + if default_workflow_logic_flags is None + else default_workflow_logic_flags + ) self._workflow_task_executor = ( workflow_task_executor or concurrent.futures.ThreadPoolExecutor( @@ -92,12 +113,31 @@ def __init__( ) ) self._workflow_task_executor_user_provided = workflow_task_executor is not None + + # If debug mode is enabled, ensure that the debugpy (https://github.com/microsoft/debugpy) + # import is added as a passthrough + if debug_mode and isinstance(workflow_runner, SandboxedWorkflowRunner): + workflow_runner = dataclasses.replace( + workflow_runner, + restrictions=workflow_runner.restrictions.with_passthrough_modules( + "_pydevd_bundle" + ), + ) + + # In debug mode, also lift the sandbox restriction on breakpoint() + # and install the workflow-aware breakpoint hook so pdb works in + # workflow code. Outside of debug mode neither happens. + self._debug_mode = debug_mode + if self._debug_mode: + workflow_runner = _relax_sandbox_for_debugger(workflow_runner) + _install_workflow_breakpoint_hook() self._workflow_runner = workflow_runner + self._unsandboxed_workflow_runner = unsandboxed_workflow_runner self._data_converter = data_converter # Build the interceptor classes and collect extern functions self._extern_functions: MutableMapping[str, Callable] = {} - self._interceptor_classes: List[Type[WorkflowInboundInterceptor]] = [] + self._interceptor_classes: list[type[WorkflowInboundInterceptor]] = [] interceptor_class_input = WorkflowInterceptorClassInput( unsafe_extern_functions=self._extern_functions ) @@ -113,18 +153,20 @@ def __init__( ) self._workflow_failure_exception_types = workflow_failure_exception_types - self._running_workflows: Dict[str, _RunningWorkflow] = {} + self._patch_activation_callback = patch_activation_callback + self._running_workflows: dict[str, _RunningWorkflow] = {} self._disable_eager_activity_execution = disable_eager_activity_execution self._on_eviction_hook = on_eviction_hook self._disable_safe_eviction = disable_safe_eviction self._encode_headers = encode_headers - self._throw_after_activation: Optional[Exception] = None - - # If there's a debug mode or a truthy TEMPORAL_DEBUG env var, disable - # deadlock detection, otherwise set to 2 seconds - self._deadlock_timeout_seconds = ( - None if debug_mode or os.environ.get("TEMPORAL_DEBUG") else 2 + self._max_workflow_task_external_storage_concurrency = ( + max_workflow_task_external_storage_concurrency ) + self._throw_after_activation: Exception | None = None + + # If debug mode is enabled, disable deadlock detection + # otherwise set to 2 seconds + self._deadlock_timeout_seconds = None if self._debug_mode else 2 # Keep track of workflows that could not be evicted self._could_not_evict_count = 0 @@ -135,8 +177,8 @@ def __init__( ) # Validate and build workflow dict - self._workflows: Dict[str, temporalio.workflow._Definition] = {} - self._dynamic_workflow: Optional[temporalio.workflow._Definition] = None + self._workflows: dict[str, temporalio.workflow._Definition] = {} + self._dynamic_workflow: temporalio.workflow._Definition | None = None for workflow in workflows: defn = temporalio.workflow._Definition.must_from_class(workflow) # Confirm name unique @@ -185,7 +227,7 @@ async def run(self) -> None: # when done. task = asyncio.create_task(self._handle_activation(act)) setattr(task, "__temporal_task_tag", task_tag) - except temporalio.bridge.worker.PollShutdownError: + except PollShutdownError: pass except Exception as err: raise RuntimeError("Workflow worker failed") from err @@ -223,9 +265,37 @@ async def drain_poll_queue(self) -> None: ) completion.failed.failure.message = "Worker shutting down" await self._bridge_worker().complete_workflow_activation(completion) - except temporalio.bridge.worker.PollShutdownError: + except PollShutdownError: return + async def _activate_inline_for_debug( + self, + loop: asyncio.AbstractEventLoop, + workflow: _RunningWorkflow, + act: temporalio.bridge.proto.workflow_activation.WorkflowActivation, + ) -> temporalio.bridge.proto.workflow_completion.WorkflowActivationCompletion: + # Indirect through call_soon + a future so the activation runs outside + # the dispatch task's __step() context. Python 3.14 refuses to enter a + # task while another on the same thread is mid-step; suspending at the + # await below clears that state so workflow.activate can step its own + # task without collision. + future: asyncio.Future = loop.create_future() + + def run_inline() -> None: + # _run_once clears the running-loop registration on exit; restore + # the main loop so later code sees the right one. + main_loop = asyncio._get_running_loop() + try: + completion = workflow.activate(act) + future.set_result(completion) + except BaseException as e: + future.set_exception(e) + finally: + asyncio._set_running_loop(main_loop) + + loop.call_soon(run_inline) + return await future + async def _handle_activation( self, act: temporalio.bridge.proto.workflow_activation.WorkflowActivation ) -> None: @@ -253,39 +323,78 @@ async def _handle_activation( temporalio.bridge.proto.workflow_completion.WorkflowActivationCompletion() ) completion.successful.SetInParent() + workflow = None + data_converter = self._data_converter + task_start_time = time.monotonic() + download_metrics = temporalio.converter._extstore.StorageOperationMetrics() try: - # Decode the activation if there's a codec and not cache remove job - if self._data_converter.payload_codec: - await temporalio.bridge.worker.decode_activation( - act, - self._data_converter.payload_codec, - decode_headers=self._encode_headers, - ) - if LOG_PROTOS: logger.debug("Received workflow activation:\n%s", act) - # If the workflow is not running yet, create it workflow = self._running_workflows.get(act.run_id) if not workflow: - # Must have a initialize job to create instance if not init_job: raise RuntimeError( "Missing initialize workflow, workflow could have unexpectedly been removed from cache" ) + workflow_id = init_job.workflow_id + else: + workflow_id = workflow.workflow_id + if init_job: + # Should never happen + logger.warning( + "Cache already exists for activation with initialize job" + ) + + workflow_context = temporalio.converter.WorkflowSerializationContext( + namespace=self._namespace, + workflow_id=workflow_id, + ) + data_converter = self._data_converter._with_contexts( + workflow_context, + StorageDriverStoreContext( + target=StorageDriverWorkflowInfo( + id=workflow_id, + run_id=act.run_id, + type=( + workflow.get_info().workflow_type + if workflow + else (init_job.workflow_type if init_job else None) + ), + namespace=self._namespace, + ), + ), + ) + if workflow: + data_converter = _CommandAwareDataConverter.create( + instance=workflow.instance, + context_free_dc=self._data_converter, + workflow_context_dc=data_converter, + workflow_context=workflow_context, + ) + download_metrics = await temporalio.bridge.worker.decode_activation( + act, + data_converter, + decode_headers=self._encode_headers, + storage_concurrency_limit=self._max_workflow_task_external_storage_concurrency, + ) + if not workflow: + assert init_job workflow = _RunningWorkflow( - self._create_workflow_instance(act, init_job) + self._create_workflow_instance(act, init_job), workflow_id ) self._running_workflows[act.run_id] = workflow - elif init_job: - # This should never happen - logger.warning( - "Cache already exists for activation with initialize job" - ) - # Run activation in separate thread so we can check if it's - # deadlocked - if workflow: + if self._debug_mode: + # Inline on the main thread so pdb / breakpoint() can read + # stdin. The loop blocks during the activation — that's the + # intended single-stepping semantic. + completion = await self._activate_inline_for_debug( + asyncio.get_running_loop(), workflow, act + ) + else: + # Run activation in separate thread so we can check if it's + # deadlocked activate_task = asyncio.get_running_loop().run_in_executor( self._workflow_task_executor, workflow.activate, @@ -322,40 +431,74 @@ async def _handle_activation( "Failed handling activation on workflow with run ID %s", act.run_id ) - # Set completion failure - completion.failed.failure.SetInParent() - try: - self._data_converter.failure_converter.to_failure( - err, - self._data_converter.payload_converter, - completion.failed.failure, - ) - except Exception as inner_err: - logger.exception( - "Failed converting activation exception on workflow with run ID %s", - act.run_id, - ) - completion.failed.failure.message = ( - f"Failed converting activation exception: {inner_err}" - ) + if ( + isinstance(err, temporalio.exceptions.ApplicationError) + and err.non_retryable + ): + # Fail the workflow execution terminally rather than failing the task + command = completion.successful.commands.add() + failure = command.fail_workflow_execution.failure + failure.SetInParent() + try: + data_converter.failure_converter.to_failure( + err, + data_converter.payload_converter, + failure, + ) + except Exception as inner_err: + logger.exception( + "Failed converting activation exception on workflow with run ID %s", + act.run_id, + ) + failure.message = ( + f"Failed converting activation exception: {inner_err}" + ) + else: + completion.failed.failure.SetInParent() + try: + data_converter.failure_converter.to_failure( + err, + data_converter.payload_converter, + completion.failed.failure, + ) + except Exception as inner_err: + logger.exception( + "Failed converting activation exception on workflow with run ID %s", + act.run_id, + ) + completion.failed.failure.message = ( + f"Failed converting activation exception: {inner_err}" + ) - # Always set the run ID on the completion completion.run_id = act.run_id - # Encode the completion if there's a codec and not cache remove job - if self._data_converter.payload_codec: - try: - await temporalio.bridge.worker.encode_completion( - completion, - self._data_converter.payload_codec, - encode_headers=self._encode_headers, - ) - except Exception as err: - logger.exception( - "Failed encoding completion on workflow with run ID %s", act.run_id - ) - completion.failed.Clear() - completion.failed.failure.message = f"Failed encoding completion: {err}" + # Encode completion + if workflow: + workflow_context = temporalio.converter.WorkflowSerializationContext( + namespace=self._namespace, + workflow_id=workflow.workflow_id, + ) + data_converter = _CommandAwareDataConverter.create( + instance=workflow.instance, + context_free_dc=self._data_converter, + workflow_context_dc=self._data_converter.with_context(workflow_context), + workflow_context=workflow_context, + ) + + upload_metrics = temporalio.converter._extstore.StorageOperationMetrics() + try: + upload_metrics = await temporalio.bridge.worker.encode_completion( + completion, + data_converter, + encode_headers=self._encode_headers, + storage_concurrency_limit=self._max_workflow_task_external_storage_concurrency, + ) + except Exception as err: + logger.exception( + "Failed encoding completion on workflow with run ID %s", act.run_id + ) + completion.failed.Clear() + completion.failed.failure.message = f"Failed encoding completion: {err}" # Send off completion if LOG_PROTOS: @@ -368,6 +511,84 @@ async def _handle_activation( "Failed completing activation on workflow with run ID %s", act.run_id ) + # Log workflow task duration with external storage metrics + self._log_workflow_task_duration( + act, workflow, task_start_time, download_metrics, upload_metrics + ) + + def _log_workflow_task_duration( + self, + act: temporalio.bridge.proto.workflow_activation.WorkflowActivation, + workflow: _RunningWorkflow | None, + task_start_time: float, + download_metrics: temporalio.converter._extstore.StorageOperationMetrics, + upload_metrics: temporalio.converter._extstore.StorageOperationMetrics, + ) -> None: + task_duration = timedelta(seconds=time.monotonic() - task_start_time) + + def _fmt_duration(td: timedelta) -> str: + secs = td.total_seconds() + if secs >= 1: + return f"{secs:.3f}s" + return f"{secs * 1000:.3f}ms" + + completed_event_id = act.history_length + 1 + _info = workflow.get_info() if workflow is not None else None + attempt = _info.attempt if _info is not None else "unknown" + log_id = f"{act.run_id}:{completed_event_id}:{attempt}" + msg_details, extra = temporalio.workflow._build_log_context( + _info._logger_details() if _info is not None else None, + full_workflow_info=_info, + ) + msg_details["event_id"] = completed_event_id + msg_details["workflow_task_duration"] = _fmt_duration(task_duration) + msg_details["workflow_history_size"] = act.history_size_bytes + extra["event_id"] = completed_event_id + extra["workflow_task_duration"] = task_duration + extra["workflow_history_size"] = act.history_size_bytes + if download_metrics.payload_count > 0: + msg_details["payload_download_count"] = download_metrics.payload_count + msg_details["payload_download_size"] = download_metrics.total_size + msg_details["payload_download_duration"] = _fmt_duration( + download_metrics.total_duration + ) + msg_details["payload_download_drivers"] = sorted( + download_metrics.driver_names + ) + extra["payload_download_count"] = download_metrics.payload_count + extra["payload_download_size"] = download_metrics.total_size + extra["payload_download_duration"] = download_metrics.total_duration + extra["payload_download_drivers"] = sorted(download_metrics.driver_names) + if upload_metrics.payload_count > 0: + msg_details["payload_upload_count"] = upload_metrics.payload_count + msg_details["payload_upload_size"] = upload_metrics.total_size + msg_details["payload_upload_duration"] = _fmt_duration( + upload_metrics.total_duration + ) + msg_details["payload_upload_drivers"] = sorted(upload_metrics.driver_names) + extra["payload_upload_count"] = upload_metrics.payload_count + extra["payload_upload_size"] = upload_metrics.total_size + extra["payload_upload_duration"] = upload_metrics.total_duration + extra["payload_upload_drivers"] = sorted(upload_metrics.driver_names) + if task_duration.total_seconds() > 10: + logger.warning( + f"[TMPRL1104] {log_id} Workflow task exceeded 10 seconds (%s)", + msg_details, + extra=extra, + ) + elif task_duration.total_seconds() > 5: + logger.info( + f"[TMPRL1104] {log_id} Workflow task exceeded 5 seconds (%s)", + msg_details, + extra=extra, + ) + else: + logger.debug( + f"[TMPRL1104] {log_id} Workflow task duration information (%s)", + msg_details, + extra=extra, + ) + async def _handle_cache_eviction( self, act: temporalio.bridge.proto.workflow_activation.WorkflowActivation, @@ -409,25 +630,30 @@ async def _handle_cache_eviction( # swallowed. Any error or timeout of eviction causes us to retry # forever because something in users code is preventing eviction. seen_fail = False - handle_eviction_task: Optional[asyncio.Future] = None + handle_eviction_task: asyncio.Future | None = None while True: try: - # We only create the eviction task if we haven't already or - # it is done. This is because if it already is running and - # timed out, it's still running (and holding on to a - # thread). But if did complete running but failed with - # another error, we want to re-create the task. - if not handle_eviction_task or handle_eviction_task.done(): - handle_eviction_task = ( - asyncio.get_running_loop().run_in_executor( - self._workflow_task_executor, - workflow.activate, - act, + if self._debug_mode: + await self._activate_inline_for_debug( + asyncio.get_running_loop(), workflow, act + ) + else: + # We only create the eviction task if we haven't already or + # it is done. This is because if it already is running and + # timed out, it's still running (and holding on to a + # thread). But if did complete running but failed with + # another error, we want to re-create the task. + if not handle_eviction_task or handle_eviction_task.done(): + handle_eviction_task = ( + asyncio.get_running_loop().run_in_executor( + self._workflow_task_executor, + workflow.activate, + act, + ) ) + await asyncio.wait_for( + handle_eviction_task, self._deadlock_timeout_seconds ) - await asyncio.wait_for( - handle_eviction_task, self._deadlock_timeout_seconds - ) # Break if it succeeds break except BaseException as err: @@ -502,8 +728,8 @@ def _create_workflow_instance( ) # Build info - parent: Optional[temporalio.workflow.ParentInfo] = None - root: Optional[temporalio.workflow.RootInfo] = None + parent: temporalio.workflow.ParentInfo | None = None + root: temporalio.workflow.RootInfo | None = None if init.HasField("parent_workflow_info"): parent = temporalio.workflow.ParentInfo( namespace=init.parent_workflow_info.namespace, @@ -558,7 +784,7 @@ def _create_workflow_instance( # Create instance from details det = WorkflowInstanceDetails( - payload_converter_class=self._data_converter.payload_converter_class, + payload_converter_factory=self._data_converter._new_payload_converter, failure_converter_class=self._data_converter.failure_converter_class, interceptor_classes=self._interceptor_classes, defn=defn, @@ -567,8 +793,10 @@ def _create_workflow_instance( extern_functions=self._extern_functions, disable_eager_activity_execution=self._disable_eager_activity_execution, worker_level_failure_exception_types=self._workflow_failure_exception_types, + patch_activation_callback=self._patch_activation_callback, last_completion_result=init.last_completion_result, last_failure=last_failure, + default_workflow_logic_flags=frozenset(self._default_workflow_logic_flags), ) if defn.sandboxed: return self._workflow_runner.create_instance(det) @@ -581,22 +809,30 @@ def nondeterminism_as_workflow_fail(self) -> bool: for typ in self._workflow_failure_exception_types ) - def nondeterminism_as_workflow_fail_for_types(self) -> Set[str]: - return set( + def _set_default_workflow_logic_flag( + self, flag: _WorkflowLogicFlag, *, enabled: bool + ) -> None: + if enabled: + self._default_workflow_logic_flags.add(flag) + else: + self._default_workflow_logic_flags.discard(flag) + + def nondeterminism_as_workflow_fail_for_types(self) -> set[str]: + return { k for k, v in self._workflows.items() if any( issubclass(temporalio.workflow.NondeterminismError, typ) for typ in v.failure_exception_types ) - ) + } class _DeadlockError(Exception): """Exception class for deadlocks. Contains functionality to swap the default traceback for another.""" - def __init__(self, message: str, replacement_tb: Optional[TracebackType] = None): - """Create a new DeadlockError, with message `message` and optionally a traceback `replacement_tb` to be swapped in later. + def __init__(self, message: str, replacement_tb: TracebackType | None = None): + """Create a new DeadlockError, with message ``message`` and optionally a traceback ``replacement_tb`` to be swapped in later. Args: message: Message to be presented through exception. @@ -616,9 +852,7 @@ def swap_traceback(self) -> None: self._new_tb = None @classmethod - def from_deadlocked_workflow( - cls, workflow: WorkflowInstance, timeout: Optional[int] - ): + def from_deadlocked_workflow(cls, workflow: WorkflowInstance, timeout: int | None): msg = f"[TMPRL1101] Potential deadlock detected: workflow didn't yield within {timeout} second(s)." tid = workflow.get_thread_id() if not tid: @@ -635,7 +869,7 @@ def from_deadlocked_workflow( @staticmethod def _gen_tb_helper( tid: int, - ) -> Optional[TracebackType]: + ) -> TracebackType | None: """Take a thread id and construct a stack trace. Returns: @@ -667,12 +901,20 @@ def _gen_tb_helper( class _RunningWorkflow: - def __init__(self, instance: WorkflowInstance): + def __init__( + self, + instance: WorkflowInstance, + workflow_id: str, + ): self.instance = instance - self.deadlocked_activation_task: Optional[Awaitable] = None + self.workflow_id = workflow_id + self.deadlocked_activation_task: Awaitable | None = None self._deadlock_can_be_interrupted_lock = threading.Lock() self._deadlock_can_be_interrupted = False + def get_info(self) -> temporalio.workflow.Info: + return self.instance.get_info() + def activate( self, act: temporalio.bridge.proto.workflow_activation.WorkflowActivation ) -> temporalio.bridge.proto.workflow_completion.WorkflowActivationCompletion: @@ -698,5 +940,90 @@ def attempt_deadlock_interruption(self) -> None: ) +@dataclass(frozen=True) +class _CommandAwareDataConverter(temporalio.converter.DataConverter): + """Data converter that resolves serialization context per-command. + + Responds to the context variable set by + :py:class:`_command_aware_visitor.CommandAwarePayloadVisitor`. + """ + + _ca_instance: WorkflowInstance = dataclasses.field( + default=None, + repr=False, + compare=False, # type: ignore[assignment] + ) + _ca_context_free_dc: temporalio.converter.DataConverter = dataclasses.field( + default=None, + repr=False, + compare=False, # type: ignore[assignment] + ) + _ca_workflow_context_dc: temporalio.converter.DataConverter = dataclasses.field( + default=None, + repr=False, + compare=False, # type: ignore[assignment] + ) + _ca_workflow_context: temporalio.converter.WorkflowSerializationContext = ( + dataclasses.field( + default=None, + repr=False, + compare=False, # type: ignore[assignment] + ) + ) + + @staticmethod + def create( + instance: WorkflowInstance, + context_free_dc: temporalio.converter.DataConverter, + workflow_context_dc: temporalio.converter.DataConverter, + workflow_context: temporalio.converter.WorkflowSerializationContext, + ) -> _CommandAwareDataConverter: + return _CommandAwareDataConverter( + payload_converter_class=workflow_context_dc.payload_converter_class, + payload_codec=workflow_context_dc.payload_codec, + failure_converter_class=workflow_context_dc.failure_converter_class, + external_storage=workflow_context_dc.external_storage, + _ca_instance=instance, + _ca_context_free_dc=context_free_dc, + _ca_workflow_context_dc=workflow_context_dc, + _ca_workflow_context=workflow_context, + ) + + def _get_current_dc(self) -> temporalio.converter.DataConverter: + context = self._ca_instance.get_serialization_context( + _command_aware_visitor.current_command_info.get(), + ) + if context is None: + return self._ca_context_free_dc + if context == self._ca_workflow_context: + return self._ca_workflow_context_dc + return self._ca_context_free_dc.with_context(context) + + async def _encode_payload_sequence( + self, payloads: Sequence[temporalio.api.common.v1.Payload] + ) -> list[temporalio.api.common.v1.Payload]: + return await self._get_current_dc()._encode_payload_sequence(payloads) + + async def _external_store_payload_sequence( + self, payloads: Sequence[temporalio.api.common.v1.Payload] + ) -> list[temporalio.api.common.v1.Payload]: + command_info = _command_aware_visitor.current_command_info.get() + store_ctx = self._ca_instance.get_external_store_context(command_info) + dc = self._get_current_dc()._with_store_context(store_ctx) + return await dc._external_store_payload_sequence(payloads) + + async def _external_retrieve_payload_sequence( + self, payloads: Sequence[temporalio.api.common.v1.Payload] + ) -> list[temporalio.api.common.v1.Payload]: + return await self._get_current_dc()._external_retrieve_payload_sequence( + payloads + ) + + async def _decode_payload_sequence( + self, payloads: Sequence[temporalio.api.common.v1.Payload] + ) -> list[temporalio.api.common.v1.Payload]: + return await self._get_current_dc()._decode_payload_sequence(payloads) + + class _InterruptDeadlockError(BaseException): pass diff --git a/temporalio/worker/_workflow_instance.py b/temporalio/worker/_workflow_instance.py index 118966b34..d0b10ccae 100644 --- a/temporalio/worker/_workflow_instance.py +++ b/temporalio/worker/_workflow_instance.py @@ -14,38 +14,34 @@ import traceback import warnings from abc import ABC, abstractmethod -from contextlib import contextmanager -from dataclasses import dataclass -from datetime import timedelta -from enum import IntEnum -from typing import ( - Any, +from collections import deque +from collections.abc import ( Awaitable, Callable, Coroutine, - Deque, - Dict, Generator, - Generic, Iterable, Iterator, - List, Mapping, MutableMapping, - NoReturn, - Optional, Sequence, - Set, - Tuple, - Type, +) +from contextlib import contextmanager +from dataclasses import dataclass, field +from datetime import timedelta +from enum import IntEnum +from typing import ( + Any, + Generic, + NoReturn, + TypeAlias, TypeVar, - Union, cast, ) -import nexusrpc.handler +import nexusrpc from nexusrpc import InputT, OutputT -from typing_extensions import Self, TypeAlias, TypedDict +from typing_extensions import Self, TypedDict, TypeVarTuple, Unpack import temporalio.activity import temporalio.api.common.v1 @@ -61,10 +57,13 @@ import temporalio.common import temporalio.converter import temporalio.exceptions +import temporalio.nexus.system import temporalio.workflow +from temporalio.converter import StorageDriverStoreContext, StorageDriverWorkflowInfo from temporalio.service import __version__ from ..api.failure.v1.message_pb2 import Failure +from . import _command_aware_visitor from ._interceptor import ( ContinueAsNewInput, ExecuteWorkflowInput, @@ -87,6 +86,17 @@ LOG_IGNORE_DURING_DELETE = False +async def _shield_await(fut: asyncio.Future[Any]) -> Any: + """Await a future without cancelling it if the awaiting task is cancelled. + + This behaves like ``asyncio.shield(fut)`` but avoids the spurious + "exception in shielded future" error log on Python 3.11+ when the + awaiting task is cancelled and the future eventually fails. + """ + await asyncio.wait([fut]) + return fut.result() + + class WorkflowRunner(ABC): """Abstract runner for workflows that creates workflow instances to run. @@ -120,7 +130,8 @@ def create_instance(self, det: WorkflowInstanceDetails) -> WorkflowInstance: raise NotImplementedError def set_worker_level_failure_exception_types( - self, types: Sequence[Type[BaseException]] + self, + types: Sequence[type[BaseException]], # type:ignore[reportUnusedParameter] ) -> None: """Set worker-level failure exception types that will be used to validate in the sandbox when calling ``prepare_workflow``. @@ -131,21 +142,36 @@ def set_worker_level_failure_exception_types( pass +@dataclass(frozen=True) +class PatchActivationInput: + """Input for the worker patch activation callback.""" + + workflow_info: temporalio.workflow.Info + """Information about the workflow execution calling ``patched``.""" + + patch_id: str + """Patch ID passed to ``patched``.""" + + @dataclass(frozen=True) class WorkflowInstanceDetails: """Immutable details for creating a workflow instance.""" - payload_converter_class: Type[temporalio.converter.PayloadConverter] - failure_converter_class: Type[temporalio.converter.FailureConverter] - interceptor_classes: Sequence[Type[WorkflowInboundInterceptor]] + payload_converter_factory: Callable[[], temporalio.converter.PayloadConverter] + failure_converter_class: type[temporalio.converter.FailureConverter] + interceptor_classes: Sequence[type[WorkflowInboundInterceptor]] defn: temporalio.workflow._Definition info: temporalio.workflow.Info randomness_seed: int extern_functions: Mapping[str, Callable] disable_eager_activity_execution: bool - worker_level_failure_exception_types: Sequence[Type[BaseException]] + worker_level_failure_exception_types: Sequence[type[BaseException]] + patch_activation_callback: Callable[[PatchActivationInput], bool] | None last_completion_result: temporalio.api.common.v1.Payloads - last_failure: Optional[Failure] + last_failure: Failure | None + default_workflow_logic_flags: frozenset[_WorkflowLogicFlag] = field( + default_factory=lambda: _DEFAULT_ENABLED_WORKFLOW_LOGIC_FLAGS + ) class WorkflowInstance(ABC): @@ -168,7 +194,43 @@ def activate( """ raise NotImplementedError - def get_thread_id(self) -> Optional[int]: + @abstractmethod + def get_serialization_context( + self, + command_info: _command_aware_visitor.CommandInfo | None, + ) -> temporalio.converter.SerializationContext | None: + """Return appropriate serialization context. + + Args: + command_info: Optional information identifying the associated command. If set, the payload + codec will have serialization context set appropriately for that command. + + Returns: + The serialization context, or None if no context should be set. + """ + raise NotImplementedError + + @abstractmethod + def get_external_store_context( + self, + command_info: _command_aware_visitor.CommandInfo | None, + ) -> StorageDriverStoreContext: + """Return appropriate store context for external storage operations. + + Args: + command_info: Optional information identifying the associated command. + + Returns: + The store context associated with the command. + """ + raise NotImplementedError + + @abstractmethod + def get_info(self) -> temporalio.workflow.Info: + """Return the workflow info for this instance.""" + raise NotImplementedError + + def get_thread_id(self) -> int | None: """Return the thread identifier that this workflow is running on. Not an abstractmethod because it is not mandatory to implement. Used primarily for getting the frames of a deadlocked thread. @@ -196,7 +258,7 @@ def create_instance(self, det: WorkflowInstanceDetails) -> WorkflowInstance: _T = TypeVar("_T") -_Context: TypeAlias = Dict[str, Any] +_Context: TypeAlias = dict[str, Any] _ExceptionHandler: TypeAlias = Callable[[asyncio.AbstractEventLoop, _Context], Any] @@ -207,57 +269,75 @@ def __init__(self, det: WorkflowInstanceDetails) -> None: # No init for AbstractEventLoop WorkflowInstance.__init__(self) temporalio.workflow._Runtime.__init__(self) - self._payload_converter = det.payload_converter_class() - self._failure_converter = det.failure_converter_class() self._defn = det.defn - self._workflow_input: Optional[ExecuteWorkflowInput] = None + self._workflow_input: ExecuteWorkflowInput | None = None self._info = det.info + self._context_free_payload_converter = det.payload_converter_factory() + self._context_free_failure_converter = det.failure_converter_class() + workflow_context = temporalio.converter.WorkflowSerializationContext( + namespace=det.info.namespace, + workflow_id=det.info.workflow_id, + ) + self._workflow_context_payload_converter = self._payload_converter_with_context( + workflow_context + ) + self._workflow_context_failure_converter = self._failure_converter_with_context( + workflow_context + ) + self._extern_functions = det.extern_functions self._disable_eager_activity_execution = det.disable_eager_activity_execution self._worker_level_failure_exception_types = ( det.worker_level_failure_exception_types ) - self._primary_task: Optional[asyncio.Task[None]] = None + self._patch_activation_callback = det.patch_activation_callback + self._default_workflow_logic_flags = det.default_workflow_logic_flags + self._primary_task: asyncio.Task[None] | None = None + self._cancel_primary_task_pending = False self._time_ns = 0 - self._cancel_requested = False - self._deployment_version_for_current_task: Optional[ + self._cancel_reason: str | None = None + self._deployment_version_for_current_task: None | ( temporalio.bridge.proto.common.WorkerDeploymentVersion - ] = None + ) = None self._current_history_length = 0 self._current_history_size = 0 self._continue_as_new_suggested = False + self._target_worker_deployment_version_changed = False # Lazily loaded - self._untyped_converted_memo: Optional[MutableMapping[str, Any]] = None + self._untyped_converted_memo: MutableMapping[str, Any] | None = None # Handles which are ready to run on the next event loop iteration - self._ready: Deque[asyncio.Handle] = collections.deque() - self._conditions: List[Tuple[Callable[[], bool], asyncio.Future]] = [] + self._ready: deque[asyncio.Handle] = collections.deque() + self._conditions: list[tuple[Callable[[], bool], asyncio.Future]] = [] # Keyed by seq - self._pending_timers: Dict[int, _TimerHandle] = {} - self._pending_activities: Dict[int, _ActivityHandle] = {} - self._pending_child_workflows: Dict[int, _ChildWorkflowHandle] = {} - self._pending_nexus_operations: Dict[int, _NexusOperationHandle] = {} - self._pending_external_signals: Dict[int, asyncio.Future] = {} - self._pending_external_cancels: Dict[int, asyncio.Future] = {} + self._pending_timers: dict[int, _TimerHandle] = {} + self._pending_activities: dict[int, _ActivityHandle] = {} + self._pending_child_workflows: dict[int, _ChildWorkflowHandle] = {} + self._pending_nexus_operations: dict[int, _NexusOperationHandle] = {} + self._pending_external_signals: dict[int, tuple[asyncio.Future, str]] = {} + self._pending_external_cancels: dict[int, tuple[asyncio.Future, str]] = {} # Keyed by type - self._curr_seqs: Dict[str, int] = {} + self._curr_seqs: dict[str, int] = {} # TODO(cretz): Any concerns about not sharing this? Maybe the types I # need to lookup should be done at definition time? - self._exception_handler: Optional[_ExceptionHandler] = None + self._exception_handler: _ExceptionHandler | None = None # The actual instance, instantiated on first _run_once self._object: Any = None self._is_replaying: bool = False self._random = random.Random(det.randomness_seed) + self._current_seed = det.randomness_seed + self._seed_callbacks: list[Callable[[int], None]] = [] self._read_only = False + self._in_query_or_validator = False # Patches we have been notified of and memoized patch responses - self._patches_notified: Set[str] = set() - self._patches_memoized: Dict[str, bool] = {} + self._patches_notified: set[str] = set() + self._patches_memoized: dict[str, bool] = {} # Tasks stored by asyncio are weak references and therefore can get GC'd # which can cause warnings like "Task was destroyed but it is pending!". # So we store the tasks ourselves. # See https://bugs.python.org/issue21163 and others. - self._tasks: Set[asyncio.Task] = set() + self._tasks: set[asyncio.Task] = set() # We maintain signals, queries, and updates on this class since handlers can be # added during workflow execution @@ -270,8 +350,8 @@ def __init__(self, det: WorkflowInstanceDetails) -> None: # signals lack a unique per-invocation identifier, we introduce a sequence number for the # purpose. self._handled_signals_seq = 0 - self._in_progress_signals: Dict[int, HandlerExecution] = {} - self._in_progress_updates: Dict[str, HandlerExecution] = {} + self._in_progress_signals: dict[int, HandlerExecution] = {} + self._in_progress_updates: dict[str, HandlerExecution] = {} # Add stack trace handler # TODO(cretz): Is it ok that this can be forcefully overridden by the @@ -305,8 +385,8 @@ def __init__(self, det: WorkflowInstanceDetails) -> None: ) # Maintain buffered signals for later-added dynamic handlers - self._buffered_signals: Dict[ - str, List[temporalio.bridge.proto.workflow_activation.SignalWorkflow] + self._buffered_signals: dict[ + str, list[temporalio.bridge.proto.workflow_activation.SignalWorkflow] ] = {} # When we evict, we have to mark the workflow as deleting so we don't @@ -314,10 +394,10 @@ def __init__(self, det: WorkflowInstanceDetails) -> None: self._deleting = False # We only create the metric meter lazily - self._metric_meter: Optional[_ReplaySafeMetricMeter] = None + self._metric_meter: _ReplaySafeMetricMeter | None = None # For tracking the thread this workflow is running on (primarily for deadlock situations) - self._current_thread_id: Optional[int] = None + self._current_thread_id: int | None = None # The current details (as opposed to static details on workflow start), returned in the # metadata query @@ -328,12 +408,12 @@ def __init__(self, det: WorkflowInstanceDetails) -> None: # The versioning behavior of this workflow, as established by annotation or by the dynamic # config function. Is only set once upon initialization. - self._versioning_behavior: Optional[temporalio.common.VersioningBehavior] = None + self._versioning_behavior: temporalio.common.VersioningBehavior | None = None # Dynamic failure exception types as overridden by the dynamic config function - self._dynamic_failure_exception_types: Optional[ - Sequence[type[BaseException]] - ] = None + self._dynamic_failure_exception_types: ( + None | (Sequence[type[BaseException]]) + ) = None # Create interceptors. We do this with our runtime on the loop just in # case they want to access info() during init(). This should remain at the end of the constructor so that variables are defined during interceptor creation @@ -353,7 +433,7 @@ def __init__(self, det: WorkflowInstanceDetails) -> None: # Set ourselves on our own loop temporalio.workflow._Runtime.set_on_loop(self, self) - def get_thread_id(self) -> Optional[int]: + def get_thread_id(self) -> int | None: return self._current_thread_id #### Activation functions #### @@ -370,24 +450,30 @@ def activate( ) self._current_completion.successful.SetInParent() - self._current_activation_error: Optional[Exception] = None + self._current_activation_error: Exception | None = None self._deployment_version_for_current_task = ( act.deployment_version_for_current_task ) self._current_history_length = act.history_length self._current_history_size = act.history_size_bytes self._continue_as_new_suggested = act.continue_as_new_suggested + self._target_worker_deployment_version_changed = ( + act.target_worker_deployment_version_changed + ) self._time_ns = act.timestamp.ToNanoseconds() self._is_replaying = act.is_replaying self._current_thread_id = threading.get_ident() self._current_internal_flags = act.available_internal_flags - activation_err: Optional[Exception] = None + self._single_batch_activation = self._workflow_logic_flag_enabled( + _WorkflowLogicFlag.PROCESS_WORKFLOW_ACTIVATION_JOBS_AS_SINGLE_BATCH + ) + activation_err: Exception | None = None try: # Split into job sets with patches, then signals + updates, then # non-queries, then queries start_job = None - job_sets: List[ - List[temporalio.bridge.proto.workflow_activation.WorkflowActivationJob] + job_sets: list[ + list[temporalio.bridge.proto.workflow_activation.WorkflowActivationJob] ] = [[], [], [], []] for job in act.jobs: if job.HasField("notify_has_patch"): @@ -401,21 +487,39 @@ def activate( else: job_sets[3].append(job) + # Core guarantees query-only activations. Fail the workflow task if violated. + assert not job_sets[3] or not any(job_sets[:3]), ( + "Query jobs must not share an activation with non-query jobs. " + "This is an SDK Core bug." + ) + if start_job: self._workflow_input = self._make_workflow_input(start_job) - # Apply every job set, running after each set - for index, job_set in enumerate(job_sets): - if not job_set: - continue - for job in job_set: - # Let errors bubble out of these to the caller to fail the task - self._apply(job) - - # Run one iteration of the loop. We do not allow conditions to - # be checked in patch jobs (first index) or query jobs (last - # index). - self._run_once(check_conditions=index == 1 or index == 2) + if self._single_batch_activation: + # Applying every job before giving workflow tasks a chance to + # run prevents their order in the activation from hiding state + # that arrived in the same workflow task. + for job_set in job_sets: + for job in job_set: + # Let errors bubble out of these to the caller to fail the task + self._apply(job) + if any(job_sets): + self._run_once(check_conditions=bool(job_sets[1] or job_sets[2])) + else: + # Preserve the legacy scheduling order for histories which do + # not contain the single-batch workflow logic flag. + for index, job_set in enumerate(job_sets): + if not job_set: + continue + for job in job_set: + # Let errors bubble out of these to the caller to fail the task + self._apply(job) + + # Run one iteration of the loop. We do not allow conditions to + # be checked in patch jobs (first index) or query jobs (last + # index). + self._run_once(check_conditions=index == 1 or index == 2) except Exception as err: # We want some errors during activation, like those that can happen # during payload conversion, to be able to fail the workflow not the @@ -466,9 +570,9 @@ def activate( # Set completion failure self._current_completion.failed.failure.SetInParent() try: - self._failure_converter.to_failure( + self._workflow_context_failure_converter.to_failure( activation_err, - self._payload_converter, + self._workflow_context_payload_converter, self._current_completion.failed.failure, ) except Exception as inner_err: @@ -480,7 +584,9 @@ def activate( ) self._current_completion.failed.failure.application_failure_info.SetInParent() - def is_completion(command): + def is_completion( + command: temporalio.bridge.proto.workflow_commands.workflow_commands_pb2.WorkflowCommand, + ): return ( command.HasField("complete_workflow_execution") or command.HasField("continue_as_new_workflow_execution") @@ -542,13 +648,16 @@ def _apply( def _apply_cancel_workflow( self, job: temporalio.bridge.proto.workflow_activation.CancelWorkflow ) -> None: - self._cancel_requested = True - # TODO(cretz): Details or cancel message or whatever? + self._cancel_reason = job.reason if self._primary_task: # The primary task may not have started yet and we want to give the # workflow the ability to receive the cancellation, so we must defer # this cancellation to the next iteration of the event loop. self.call_soon(self._primary_task.cancel) + elif self._single_batch_activation: + # Initialization is the only job that creates the primary task, so + # retain a same-activation cancellation until that task exists. + self._cancel_primary_task_pending = True def _apply_do_update( self, job: temporalio.bridge.proto.workflow_activation.DoUpdate @@ -591,7 +700,7 @@ async def run_update() -> None: ) if job.run_validator and defn.validator is not None: - with self._as_read_only(): + with self._as_read_only(in_query_or_validator=True): self._inbound.handle_update_validator(handler_input) # Re-process arguments to avoid any problems caused by user mutation of them during validation args = self._process_handler_args( @@ -610,7 +719,9 @@ async def run_update() -> None: # Run the handler success = await self._inbound.handle_update_handler(handler_input) - result_payloads = self._payload_converter.to_payloads([success]) + result_payloads = self._workflow_context_payload_converter.to_payloads( + [success] + ) if len(result_payloads) != 1: raise ValueError( f"Expected 1 result payload, got {len(result_payloads)}" @@ -642,9 +753,9 @@ async def run_update() -> None: job.protocol_instance_id ) command.update_response.rejected.SetInParent() - self._failure_converter.to_failure( + self._workflow_context_failure_converter.to_failure( err, - self._payload_converter, + self._workflow_context_payload_converter, command.update_response.rejected, ) else: @@ -681,7 +792,7 @@ def _apply_query_workflow( # Wrap entire bunch of work in a task async def run_query() -> None: try: - with self._as_read_only(): + with self._as_read_only(in_query_or_validator=True): # Named query or dynamic defn = self._queries.get(job.query_type) or self._queries.get(None) if not defn: @@ -706,7 +817,9 @@ async def run_query() -> None: headers=job.headers, ) success = await self._inbound.handle_query(input) - result_payloads = self._payload_converter.to_payloads([success]) + result_payloads = ( + self._workflow_context_payload_converter.to_payloads([success]) + ) if len(result_payloads) != 1: raise ValueError( f"Expected 1 result payload, got {len(result_payloads)}" @@ -718,9 +831,9 @@ async def run_query() -> None: try: command = self._add_command() command.respond_to_query.query_id = job.query_id - self._failure_converter.to_failure( + self._workflow_context_failure_converter.to_failure( err, - self._payload_converter, + self._workflow_context_payload_converter, command.respond_to_query.failed, ) except Exception as inner_err: @@ -737,10 +850,9 @@ def _apply_notify_has_patch( self._patches_notified.add(job.patch_id) def _apply_remove_from_cache( - self, job: temporalio.bridge.proto.workflow_activation.RemoveFromCache + self, _job: temporalio.bridge.proto.workflow_activation.RemoveFromCache ) -> None: self._deleting = True - self._cancel_requested = True # We consider eviction to be under replay so that certain code like # logging that avoids replaying doesn't run during eviction either self._is_replaying = True @@ -754,26 +866,42 @@ def _apply_resolve_activity( handle = self._pending_activities.pop(job.seq, None) if not handle: raise RuntimeError(f"Failed finding activity handle for sequence {job.seq}") + activity_context = temporalio.converter.ActivitySerializationContext( + namespace=self._info.namespace, + workflow_id=self._info.workflow_id, + workflow_type=self._info.workflow_type, + activity_type=handle._input.activity, + activity_id=handle._input.activity_id, + activity_task_queue=( + handle._input.task_queue or self._info.task_queue + if isinstance(handle._input, StartActivityInput) + else self._info.task_queue + ), + is_local=isinstance(handle._input, StartLocalActivityInput), + ) + payload_converter = self._payload_converter_with_context(activity_context) + failure_converter = self._failure_converter_with_context(activity_context) if job.result.HasField("completed"): - ret: Optional[Any] = None + ret: Any | None = None if job.result.completed.HasField("result"): ret_types = [handle._input.ret_type] if handle._input.ret_type else None ret_vals = self._convert_payloads( [job.result.completed.result], ret_types, + payload_converter, ) ret = ret_vals[0] handle._resolve_success(ret) elif job.result.HasField("failed"): handle._resolve_failure( - self._failure_converter.from_failure( - job.result.failed.failure, self._payload_converter + failure_converter.from_failure( + job.result.failed.failure, payload_converter ) ) elif job.result.HasField("cancelled"): handle._resolve_failure( - self._failure_converter.from_failure( - job.result.cancelled.failure, self._payload_converter + failure_converter.from_failure( + job.result.cancelled.failure, payload_converter ) ) elif job.result.HasField("backoff"): @@ -790,26 +918,28 @@ def _apply_resolve_child_workflow_execution( raise RuntimeError( f"Failed finding child workflow handle for sequence {job.seq}" ) + if job.result.HasField("completed"): - ret: Optional[Any] = None + ret: Any | None = None if job.result.completed.HasField("result"): ret_types = [handle._input.ret_type] if handle._input.ret_type else None ret_vals = self._convert_payloads( [job.result.completed.result], ret_types, + handle._payload_converter, ) ret = ret_vals[0] handle._resolve_success(ret) elif job.result.HasField("failed"): handle._resolve_failure( - self._failure_converter.from_failure( - job.result.failed.failure, self._payload_converter + handle._failure_converter.from_failure( + job.result.failed.failure, handle._payload_converter ) ) elif job.result.HasField("cancelled"): handle._resolve_failure( - self._failure_converter.from_failure( - job.result.cancelled.failure, self._payload_converter + handle._failure_converter.from_failure( + job.result.cancelled.failure, handle._payload_converter ) ) else: @@ -846,8 +976,8 @@ def _apply_resolve_child_workflow_execution_start( elif job.HasField("cancelled"): self._pending_child_workflows.pop(job.seq) handle._resolve_failure( - self._failure_converter.from_failure( - job.cancelled.failure, self._payload_converter + handle._failure_converter.from_failure( + job.cancelled.failure, handle._payload_converter ) ) else: @@ -874,8 +1004,8 @@ def _apply_resolve_nexus_operation_start( # The nexus operation start failed; no ResolveNexusOperation will follow. self._pending_nexus_operations.pop(job.seq, None) handle._resolve_failure( - self._failure_converter.from_failure( - job.failed, self._payload_converter + handle._failure_converter.from_failure( + job.failed, handle._payload_converter ) ) else: @@ -905,24 +1035,25 @@ def _apply_resolve_nexus_operation( [output] = self._convert_payloads( [result.completed], [handle._input.output_type] if handle._input.output_type else None, + handle._payload_converter, ) handle._resolve_success(output) elif result.HasField("failed"): handle._resolve_failure( - self._failure_converter.from_failure( - result.failed, self._payload_converter + handle._failure_converter.from_failure( + result.failed, handle._payload_converter ) ) elif result.HasField("cancelled"): handle._resolve_failure( - self._failure_converter.from_failure( - result.cancelled, self._payload_converter + handle._failure_converter.from_failure( + result.cancelled, handle._payload_converter ) ) elif result.HasField("timed_out"): handle._resolve_failure( - self._failure_converter.from_failure( - result.timed_out, self._payload_converter + handle._failure_converter.from_failure( + result.timed_out, handle._payload_converter ) ) else: @@ -932,17 +1063,22 @@ def _apply_resolve_request_cancel_external_workflow( self, job: temporalio.bridge.proto.workflow_activation.ResolveRequestCancelExternalWorkflow, ) -> None: - fut = self._pending_external_cancels.pop(job.seq, None) - if not fut: + pending = self._pending_external_cancels.pop(job.seq, None) + if not pending: raise RuntimeError( f"Failed finding pending external cancel for sequence {job.seq}" ) + fut, external_workflow_id = pending # We intentionally let this error if future is already done if job.HasField("failure"): + workflow_context = temporalio.converter.WorkflowSerializationContext( + namespace=self._info.namespace, + workflow_id=external_workflow_id, + ) + payload_converter = self._payload_converter_with_context(workflow_context) + failure_converter = self._failure_converter_with_context(workflow_context) fut.set_exception( - self._failure_converter.from_failure( - job.failure, self._payload_converter - ) + failure_converter.from_failure(job.failure, payload_converter) ) else: fut.set_result(None) @@ -951,17 +1087,22 @@ def _apply_resolve_signal_external_workflow( self, job: temporalio.bridge.proto.workflow_activation.ResolveSignalExternalWorkflow, ) -> None: - fut = self._pending_external_signals.pop(job.seq, None) - if not fut: + pending = self._pending_external_signals.pop(job.seq, None) + if not pending: raise RuntimeError( f"Failed finding pending external signal for sequence {job.seq}" ) + fut, external_workflow_id = pending # We intentionally let this error if future is already done if job.HasField("failure"): + workflow_context = temporalio.converter.WorkflowSerializationContext( + namespace=self._info.namespace, + workflow_id=external_workflow_id, + ) + payload_converter = self._payload_converter_with_context(workflow_context) + failure_converter = self._failure_converter_with_context(workflow_context) fut.set_exception( - self._failure_converter.from_failure( - job.failure, self._payload_converter - ) + failure_converter.from_failure(job.failure, payload_converter) ) else: fut.set_result(None) @@ -977,14 +1118,16 @@ def _apply_signal_workflow( self._process_signal_job(signal_defn, job) def _apply_initialize_workflow( - self, job: temporalio.bridge.proto.workflow_activation.InitializeWorkflow + self, _job: temporalio.bridge.proto.workflow_activation.InitializeWorkflow ) -> None: # Async call to run on the scheduler thread. This will be wrapped in # another function which applies exception handling. async def run_workflow(input: ExecuteWorkflowInput) -> None: try: result = await self._inbound.execute_workflow(input) - result_payloads = self._payload_converter.to_payloads([result]) + result_payloads = self._workflow_context_payload_converter.to_payloads( + [result] + ) if len(result_payloads) != 1: raise ValueError( f"Expected 1 result payload, got {len(result_payloads)}" @@ -1008,11 +1151,22 @@ async def run_workflow(input: ExecuteWorkflowInput) -> None: self._run_top_level_workflow_function(run_workflow(self._workflow_input)), name="run", ) + if self._cancel_primary_task_pending: + self._cancel_primary_task_pending = False + self.call_soon(self._primary_task.cancel) def _apply_update_random_seed( self, job: temporalio.bridge.proto.workflow_activation.UpdateRandomSeed ) -> None: self._random.seed(job.randomness_seed) + self._current_seed = job.randomness_seed + # Notify all registered callbacks + for callback in self._seed_callbacks: + try: + callback(job.randomness_seed) + except Exception: + # Ignore callback errors to avoid disrupting workflow execution + pass def _make_workflow_input( self, init_job: temporalio.bridge.proto.workflow_activation.InitializeWorkflow @@ -1022,7 +1176,10 @@ def _make_workflow_input( if not self._defn.name: # Dynamic is just the raw value for each input value arg_types = [temporalio.common.RawValue] * len(init_job.arguments) - args = self._convert_payloads(init_job.arguments, arg_types) + + args = self._convert_payloads( + init_job.arguments, arg_types, self._workflow_context_payload_converter + ) # Put args in a list if dynamic if not self._defn.name: args = [args] @@ -1044,24 +1201,25 @@ def workflow_all_handlers_finished(self) -> bool: def workflow_continue_as_new( self, *args: Any, - workflow: Union[None, Callable, str], - task_queue: Optional[str], - run_timeout: Optional[timedelta], - task_timeout: Optional[timedelta], - retry_policy: Optional[temporalio.common.RetryPolicy], - memo: Optional[Mapping[str, Any]], - search_attributes: Optional[ - Union[ - temporalio.common.SearchAttributes, - temporalio.common.TypedSearchAttributes, - ] - ], - versioning_intent: Optional[temporalio.workflow.VersioningIntent], + workflow: None | Callable | str, + task_queue: str | None, + run_timeout: timedelta | None, + task_timeout: timedelta | None, + backoff_start_interval: timedelta | None, + retry_policy: temporalio.common.RetryPolicy | None, + memo: Mapping[str, Any] | None, + search_attributes: None + | ( + temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes + ), + versioning_intent: temporalio.workflow.VersioningIntent | None, + initial_versioning_behavior: temporalio.workflow.ContinueAsNewVersioningBehavior + | None, ) -> NoReturn: self._assert_not_read_only("continue as new") # Use definition if callable - name: Optional[str] = None - arg_types: Optional[List[Type]] = None + name: str | None = None + arg_types: list[type] | None = None if isinstance(workflow, str): name = workflow elif callable(workflow): @@ -1069,7 +1227,7 @@ def workflow_continue_as_new( name = defn.name arg_types = defn.arg_types elif workflow is not None: - raise TypeError("Workflow must be None, a string, or callable") + raise TypeError("Workflow must be None, a string, or callable") # type:ignore[reportUnreachable] self._outbound.continue_as_new( ContinueAsNewInput( @@ -1078,16 +1236,19 @@ def workflow_continue_as_new( task_queue=task_queue, run_timeout=run_timeout, task_timeout=task_timeout, + backoff_start_interval=backoff_start_interval, retry_policy=retry_policy, memo=memo, search_attributes=search_attributes, headers={}, arg_types=arg_types, versioning_intent=versioning_intent, + initial_versioning_behavior=initial_versioning_behavior, ) ) - # TODO(cretz): Why can't MyPy infer the above never returns? - raise RuntimeError("Unreachable") + + def workflow_cancellation_reason(self) -> str | None: + return self._cancel_reason def workflow_extern_functions(self) -> Mapping[str, Callable]: return self._extern_functions @@ -1099,7 +1260,7 @@ def workflow_get_current_build_id(self) -> str: def workflow_get_current_deployment_version( self, - ) -> Optional[temporalio.common.WorkerDeploymentVersion]: + ) -> temporalio.common.WorkerDeploymentVersion | None: if not self._deployment_version_for_current_task: return None return temporalio.common.WorkerDeploymentVersion( @@ -1107,6 +1268,9 @@ def workflow_get_current_deployment_version( deployment_name=self._deployment_version_for_current_task.deployment_name, ) + def get_info(self) -> temporalio.workflow.Info: + return self._info + def workflow_get_current_history_length(self) -> int: return self._current_history_length @@ -1114,32 +1278,32 @@ def workflow_get_current_history_size(self) -> int: return self._current_history_size def workflow_get_external_workflow_handle( - self, id: str, *, run_id: Optional[str] + self, id: str, *, run_id: str | None ) -> temporalio.workflow.ExternalWorkflowHandle[Any]: return _ExternalWorkflowHandle(self, id, run_id) - def workflow_get_query_handler(self, name: Optional[str]) -> Optional[Callable]: + def workflow_get_query_handler(self, name: str | None) -> Callable | None: defn = self._queries.get(name) if not defn: return None # Bind if a method return defn.bind_fn(self._object) if defn.is_method else defn.fn - def workflow_get_signal_handler(self, name: Optional[str]) -> Optional[Callable]: + def workflow_get_signal_handler(self, name: str | None) -> Callable | None: defn = self._signals.get(name) if not defn: return None # Bind if a method return defn.bind_fn(self._object) if defn.is_method else defn.fn - def workflow_get_update_handler(self, name: Optional[str]) -> Optional[Callable]: + def workflow_get_update_handler(self, name: str | None) -> Callable | None: defn = self._updates.get(name) if not defn: return None # Bind if a method return defn.bind_fn(self._object) if defn.is_method else defn.fn - def workflow_get_update_validator(self, name: Optional[str]) -> Optional[Callable]: + def workflow_get_update_validator(self, name: str | None) -> Callable | None: defn = self._updates.get(name) or self._updates.get(None) if not defn or not defn.validator: return None @@ -1155,26 +1319,35 @@ def workflow_instance(self) -> Any: def workflow_is_continue_as_new_suggested(self) -> bool: return self._continue_as_new_suggested + def workflow_is_target_worker_deployment_version_changed(self) -> bool: + return self._target_worker_deployment_version_changed + def workflow_is_replaying(self) -> bool: return self._is_replaying + def workflow_is_replaying_history_events(self) -> bool: + return self._is_replaying and not self._in_query_or_validator + + def workflow_is_read_only(self) -> bool: + return self._read_only + def workflow_memo(self) -> Mapping[str, Any]: if self._untyped_converted_memo is None: self._untyped_converted_memo = { - k: self._payload_converter.from_payload(v) + k: self._workflow_context_payload_converter.from_payload(v) for k, v in self._info.raw_memo.items() } return self._untyped_converted_memo def workflow_memo_value( - self, key: str, default: Any, *, type_hint: Optional[Type] + self, key: str, default: Any, *, type_hint: type | None ) -> Any: payload = self._info.raw_memo.get(key) if not payload: if default is temporalio.common._arg_unset: raise KeyError(f"Memo does not have a value for key {key}") return default - return self._payload_converter.from_payload( + return self._workflow_context_payload_converter.from_payload( payload, type_hint, # type: ignore[arg-type] ) @@ -1188,7 +1361,9 @@ def workflow_upsert_memo(self, updates: Mapping[str, Any]) -> None: # Intentionally not checking if memo exists, so that no-op removals show up in history too. removals.append(k) else: - update_payloads[k] = self._payload_converter.to_payload(v) + update_payloads[k] = ( + self._workflow_context_payload_converter.to_payload(v) + ) if not update_payloads and not removals: return @@ -1207,7 +1382,7 @@ def workflow_upsert_memo(self, updates: Mapping[str, Any]) -> None: mut_raw_memo[k] = v if removals: - null_payload = self._payload_converter.to_payload(None) + null_payload = self._workflow_context_payload_converter.to_payload(None) for k in removals: fields[k].CopyFrom(null_payload) mut_raw_memo.pop(k, None) @@ -1215,8 +1390,8 @@ def workflow_upsert_memo(self, updates: Mapping[str, Any]) -> None: # Keeping deserialized memo dict in sync, if exists if self._untyped_converted_memo is not None: for k, v in update_payloads.items(): - self._untyped_converted_memo[k] = self._payload_converter.from_payload( - v + self._untyped_converted_memo[k] = ( + self._workflow_context_payload_converter.from_payload(v) ) for k in removals: self._untyped_converted_memo.pop(k, None) @@ -1245,7 +1420,20 @@ def workflow_patch(self, id: str, *, deprecated: bool) -> bool: if use_patch is not None: return use_patch - use_patch = not self._is_replaying or id in self._patches_notified + # Replay and history markers already determine the branch, and deprecation must + # keep existing patch semantics, so only a genuinely new patch consults the + # callback. + if deprecated or self._is_replaying or id in self._patches_notified: + use_patch = not self._is_replaying or id in self._patches_notified + elif self._patch_activation_callback is not None: + with self._as_read_only(in_query_or_validator=False): + use_patch = self._patch_activation_callback( + PatchActivationInput(workflow_info=self._info, patch_id=id) + ) + if type(use_patch) is not bool: + raise TypeError("Patch activation callback must return true or false") + else: + use_patch = True self._patches_memoized[id] = use_patch if use_patch: command = self._add_command() @@ -1254,14 +1442,14 @@ def workflow_patch(self, id: str, *, deprecated: bool) -> bool: return use_patch def workflow_payload_converter(self) -> temporalio.converter.PayloadConverter: - return self._payload_converter + return self._workflow_context_payload_converter def workflow_random(self) -> random.Random: self._assert_not_read_only("random") return self._random def workflow_set_query_handler( - self, name: Optional[str], handler: Optional[Callable] + self, name: str | None, handler: Callable | None ) -> None: self._assert_not_read_only("set query handler") if handler: @@ -1285,7 +1473,7 @@ def workflow_set_query_handler( self._queries.pop(name, None) def workflow_set_signal_handler( - self, name: Optional[str], handler: Optional[Callable] + self, name: str | None, handler: Callable | None ) -> None: self._assert_not_read_only("set signal handler") if handler: @@ -1313,9 +1501,9 @@ def workflow_set_signal_handler( def workflow_set_update_handler( self, - name: Optional[str], - handler: Optional[Callable], - validator: Optional[Callable], + name: str | None, + handler: Callable | None, + validator: Callable | None, ) -> None: self._assert_not_read_only("set update handler") if handler: @@ -1336,23 +1524,23 @@ def workflow_start_activity( self, activity: Any, *args: Any, - task_queue: Optional[str], - result_type: Optional[Type], - schedule_to_close_timeout: Optional[timedelta], - schedule_to_start_timeout: Optional[timedelta], - start_to_close_timeout: Optional[timedelta], - heartbeat_timeout: Optional[timedelta], - retry_policy: Optional[temporalio.common.RetryPolicy], + task_queue: str | None, + result_type: type | None, + schedule_to_close_timeout: timedelta | None, + schedule_to_start_timeout: timedelta | None, + start_to_close_timeout: timedelta | None, + heartbeat_timeout: timedelta | None, + retry_policy: temporalio.common.RetryPolicy | None, cancellation_type: temporalio.workflow.ActivityCancellationType, - activity_id: Optional[str], - versioning_intent: Optional[temporalio.workflow.VersioningIntent], - summary: Optional[str] = None, + activity_id: str | None, + versioning_intent: temporalio.workflow.VersioningIntent | None, + summary: str | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> temporalio.workflow.ActivityHandle[Any]: self._assert_not_read_only("start activity") # Get activity definition if it's callable name: str - arg_types: Optional[List[Type]] = None + arg_types: list[type] | None = None ret_type = result_type if isinstance(activity, str): name = activity @@ -1394,31 +1582,29 @@ async def workflow_start_child_workflow( workflow: Any, *args: Any, id: str, - task_queue: Optional[str], - result_type: Optional[Type], + task_queue: str | None, + result_type: type | None, cancellation_type: temporalio.workflow.ChildWorkflowCancellationType, parent_close_policy: temporalio.workflow.ParentClosePolicy, - execution_timeout: Optional[timedelta], - run_timeout: Optional[timedelta], - task_timeout: Optional[timedelta], + execution_timeout: timedelta | None, + run_timeout: timedelta | None, + task_timeout: timedelta | None, id_reuse_policy: temporalio.common.WorkflowIDReusePolicy, - retry_policy: Optional[temporalio.common.RetryPolicy], + retry_policy: temporalio.common.RetryPolicy | None, cron_schedule: str, - memo: Optional[Mapping[str, Any]], - search_attributes: Optional[ - Union[ - temporalio.common.SearchAttributes, - temporalio.common.TypedSearchAttributes, - ] - ], - versioning_intent: Optional[temporalio.workflow.VersioningIntent], - static_summary: Optional[str] = None, - static_details: Optional[str] = None, + memo: Mapping[str, Any] | None, + search_attributes: None + | ( + temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes + ), + versioning_intent: temporalio.workflow.VersioningIntent | None, + static_summary: str | None = None, + static_details: str | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> temporalio.workflow.ChildWorkflowHandle[Any, Any]: # Use definition if callable name: str - arg_types: Optional[List[Type]] = None + arg_types: list[type] | None = None ret_type = result_type if isinstance(workflow, str): name = workflow @@ -1462,19 +1648,19 @@ def workflow_start_local_activity( self, activity: Any, *args: Any, - result_type: Optional[Type], - schedule_to_close_timeout: Optional[timedelta], - schedule_to_start_timeout: Optional[timedelta], - start_to_close_timeout: Optional[timedelta], - retry_policy: Optional[temporalio.common.RetryPolicy], - local_retry_threshold: Optional[timedelta], + result_type: type | None, + schedule_to_close_timeout: timedelta | None, + schedule_to_start_timeout: timedelta | None, + start_to_close_timeout: timedelta | None, + retry_policy: temporalio.common.RetryPolicy | None, + local_retry_threshold: timedelta | None, cancellation_type: temporalio.workflow.ActivityCancellationType, - activity_id: Optional[str], - summary: Optional[str], + activity_id: str | None, + summary: str | None, ) -> temporalio.workflow.ActivityHandle[Any]: # Get activity definition if it's callable name: str - arg_types: Optional[List[Type]] = None + arg_types: list[type] | None = None ret_type = result_type if isinstance(activity, str): name = activity @@ -1514,12 +1700,15 @@ async def workflow_start_nexus_operation( self, endpoint: str, service: str, - operation: Union[nexusrpc.Operation[InputT, OutputT], str, Callable[..., Any]], + operation: nexusrpc.Operation[InputT, OutputT] | str | Callable[..., Any], input: Any, - output_type: Optional[Type[OutputT]], - schedule_to_close_timeout: Optional[timedelta], + output_type: type[OutputT] | None, + schedule_to_close_timeout: timedelta | None, + schedule_to_start_timeout: timedelta | None, + start_to_close_timeout: timedelta | None, cancellation_type: temporalio.workflow.NexusOperationCancellationType, - headers: Optional[Mapping[str, str]], + headers: Mapping[str, str] | None, + summary: str | None, ) -> temporalio.workflow.NexusOperationHandle[OutputT]: # start_nexus_operation return await self._outbound.start_nexus_operation( @@ -1530,8 +1719,11 @@ async def workflow_start_nexus_operation( input=input, output_type=output_type, schedule_to_close_timeout=schedule_to_close_timeout, + schedule_to_start_timeout=schedule_to_start_timeout, + start_to_close_timeout=start_to_close_timeout, cancellation_type=cancellation_type, headers=headers, + summary=summary, ) ) @@ -1540,10 +1732,10 @@ def workflow_time_ns(self) -> int: def workflow_upsert_search_attributes( self, - attributes: Union[ - temporalio.common.SearchAttributes, - Sequence[temporalio.common.SearchAttributeUpdate], - ], + attributes: ( + temporalio.common.SearchAttributes + | Sequence[temporalio.common.SearchAttributeUpdate] + ), ) -> None: v = self._add_command().upsert_workflow_search_attributes @@ -1553,7 +1745,7 @@ def workflow_upsert_search_attributes( self._info.search_attributes, ) mut_typed_attrs = cast( - List[temporalio.common.SearchAttributePair], + list[temporalio.common.SearchAttributePair], self._info.typed_search_attributes.search_attributes, ) @@ -1565,7 +1757,7 @@ def workflow_upsert_search_attributes( mut_attrs.update(attributes) for k, vals in attributes.items(): # Add to command - v.search_attributes[k].CopyFrom( + v.search_attributes.indexed_fields[k].CopyFrom( temporalio.converter.encode_search_attribute_values(vals) ) @@ -1606,7 +1798,7 @@ def workflow_upsert_search_attributes( # Update typed and untyped keys, replacing typed as needed for update in attributes: # Set on command (delete is a proper null) - v.search_attributes[update.key.name].CopyFrom( + v.search_attributes.indexed_fields[update.key.name].CopyFrom( temporalio.converter.encode_typed_search_attribute_value( update.key, update.value ) @@ -1626,7 +1818,7 @@ def workflow_upsert_search_attributes( if index is not None: del mut_typed_attrs[index] # Just empty-list the untyped one - mut_attrs[update.key.name] = cast(List[str], []) + mut_attrs[update.key.name] = cast(list[str], []) else: # Update pair = temporalio.common.SearchAttributePair( @@ -1645,20 +1837,23 @@ def workflow_upsert_search_attributes( ) async def workflow_sleep( - self, duration: float, *, summary: Optional[str] = None + self, duration: float, *, summary: str | None = None ) -> None: user_metadata = ( temporalio.api.sdk.v1.UserMetadata( - summary=self._payload_converter.to_payload(summary) + summary=self._workflow_context_payload_converter.to_payload(summary) ) if summary else None ) fut = self.create_future() - self._timer_impl( + timer_handle = self._timer_impl( duration, _TimerOptions(user_metadata=user_metadata), - lambda: fut.set_result(None), + lambda: fut.set_result(None) if not fut.done() else None, + ) + fut.add_done_callback( + lambda f: timer_handle.cancel() if f.cancelled() else None ) await fut @@ -1666,15 +1861,30 @@ async def workflow_wait_condition( self, fn: Callable[[], bool], *, - timeout: Optional[float] = None, - timeout_summary: Optional[str] = None, + timeout: float | None = None, + timeout_summary: str | None = None, ) -> None: self._assert_not_read_only("wait condition") + cancellation_requested_before = self._cancel_reason is not None + + # Some asyncio.wait_for implementations can prefer a ready condition + # result or timeout over task cancellation that becomes ready in the same + # event-loop turn. Only detect a new request so workflows can catch + # cancellation and keep going. + def cancellation_arrived() -> bool: + return ( + self._single_batch_activation + and not cancellation_requested_before + and self._cancel_reason is not None + ) + fut = self.create_future() self._conditions.append((fn, fut)) user_metadata = ( temporalio.api.sdk.v1.UserMetadata( - summary=self._payload_converter.to_payload(timeout_summary) + summary=self._workflow_context_payload_converter.to_payload( + timeout_summary + ) ) if timeout_summary else None @@ -1685,7 +1895,14 @@ async def in_context(): _TimerOptionsCtxVar.set(_TimerOptions(user_metadata=user_metadata)) await asyncio.wait_for(fut, timeout) - await ctxvars.run(in_context) + try: + await ctxvars.run(in_context) + except asyncio.TimeoutError: + if cancellation_arrived(): + raise asyncio.CancelledError() + raise + if cancellation_arrived(): + raise asyncio.CancelledError() def workflow_get_current_details(self) -> str: return self._current_details @@ -1695,9 +1912,9 @@ def workflow_set_current_details(self, details: str): self._current_details = details def workflow_is_failure_exception(self, err: BaseException) -> bool: - # An exception is a failure instead of a task fail if it's already a - # failure error or if it is a timeout error or if it is an instance of - # any of the failure types in the worker or workflow-level setting + # An exception causes the workflow to fail (rather than the task) if it + # is already a failure error, a timeout error, or an instance of any of the + # failure exception types configured at the worker or workflow level. wf_failure_exception_types = self._defn.failure_exception_types if self._dynamic_failure_exception_types is not None: wf_failure_exception_types = self._dynamic_failure_exception_types @@ -1714,9 +1931,7 @@ def workflow_is_failure_exception(self, err: BaseException) -> bool: def workflow_has_last_completion_result(self) -> bool: return len(self._last_completion_result.payloads) > 0 - def workflow_last_completion_result( - self, type_hint: Optional[Type] - ) -> Optional[Any]: + def workflow_last_completion_result(self, type_hint: type | None) -> Any | None: if len(self._last_completion_result.payloads) == 0: return None elif len(self._last_completion_result.payloads) > 1: @@ -1726,32 +1941,40 @@ def workflow_last_completion_result( return None if type_hint is None: - return self._payload_converter.from_payload( + return self._workflow_context_payload_converter.from_payload( self._last_completion_result.payloads[0] ) else: - return self._payload_converter.from_payload( + return self._workflow_context_payload_converter.from_payload( self._last_completion_result.payloads[0], type_hint ) - def workflow_last_failure(self) -> Optional[BaseException]: + def workflow_last_failure(self) -> BaseException | None: if self._last_failure: - return self._failure_converter.from_failure( - self._last_failure, self._payload_converter + return self._workflow_context_failure_converter.from_failure( + self._last_failure, self._workflow_context_payload_converter ) return None + def workflow_random_seed(self) -> int: + return self._current_seed + + def workflow_register_random_seed_callback( + self, callback: Callable[[int], None] + ) -> None: + self._assert_not_read_only("register random seed callback") + self._seed_callbacks.append(callback) + #### Calls from outbound impl #### # These are in alphabetical order and all start with "_outbound_". def _outbound_continue_as_new(self, input: ContinueAsNewInput) -> NoReturn: - # Just throw raise _ContinueAsNewError(self, input) def _outbound_schedule_activity( self, - input: Union[StartActivityInput, StartLocalActivityInput], + input: StartActivityInput | StartLocalActivityInput, ) -> _ActivityHandle: # Validate if not input.start_to_close_timeout and not input.schedule_to_close_timeout: @@ -1768,9 +1991,11 @@ async def run_activity() -> Any: # be marked as unstarted handle._started = True try: - # We have to shield because we don't want the underlying - # result future to be cancelled - return await asyncio.shield(handle._result_fut) + return await self._await_temporal_operation( + handle._result_fut, + lambda _err, command: handle._apply_cancel_command(command), + completed_cancellation_flag=_WorkflowLogicFlag.RAISE_ON_CANCELLING_COMPLETED_ACTIVITY, + ) except _ActivityDoBackoffError as err: # We have to sleep then reschedule. Note this sleep can be # cancelled like any other timer. @@ -1781,21 +2006,6 @@ async def run_activity() -> Any: # We have to put the handle back on the pending activity # dict with its new seq self._pending_activities[handle._seq] = handle - except asyncio.CancelledError: - # If an activity future completes at the same time as a cancellation is being processed, the cancellation would be swallowed - # _WorkflowLogicFlag.RAISE_ON_CANCELLING_COMPLETED_ACTIVITY will correctly reraise the exception - if handle._result_fut.done(): - if ( - not self._is_replaying - or _WorkflowLogicFlag.RAISE_ON_CANCELLING_COMPLETED_ACTIVITY - in self._current_internal_flags - ): - self._current_completion.successful.used_internal_flags.append( - _WorkflowLogicFlag.RAISE_ON_CANCELLING_COMPLETED_ACTIVITY - ) - raise - # Send a cancel request to the activity - handle._apply_cancel_command(self._add_command()) # Create the handle and set as pending handle = _ActivityHandle(self, input, run_activity()) @@ -1806,9 +2016,13 @@ async def run_activity() -> Any: async def _outbound_signal_child_workflow( self, input: SignalChildWorkflowInput ) -> None: - payloads = ( - self._payload_converter.to_payloads(input.args) if input.args else None + payload_converter = self._payload_converter_with_context( + temporalio.converter.WorkflowSerializationContext( + namespace=self._info.namespace, + workflow_id=input.child_workflow_id, + ) ) + payloads = payload_converter.to_payloads(input.args) if input.args else None command = self._add_command() v = command.signal_external_workflow_execution v.child_workflow_id = input.child_workflow_id @@ -1822,9 +2036,13 @@ async def _outbound_signal_child_workflow( async def _outbound_signal_external_workflow( self, input: SignalExternalWorkflowInput ) -> None: - payloads = ( - self._payload_converter.to_payloads(input.args) if input.args else None + payload_converter = self._payload_converter_with_context( + temporalio.converter.WorkflowSerializationContext( + namespace=input.namespace, + workflow_id=input.workflow_id, + ) ) + payloads = payload_converter.to_payloads(input.args) if input.args else None command = self._add_command() v = command.signal_external_workflow_execution v.workflow_execution.namespace = input.namespace @@ -1844,10 +2062,14 @@ async def _outbound_start_child_workflow( handle: _ChildWorkflowHandle # Common code for handling cancel for start and run - def apply_child_cancel_error() -> None: - # Send a cancel request to the child - cancel_command = self._add_command() - handle._apply_cancel_command(cancel_command) + def apply_child_cancel_error( + err: asyncio.CancelledError, + cancel_command: temporalio.bridge.proto.workflow_commands.WorkflowCommand, + ) -> None: + # Send a cancel request to the child, forwarding the msg passed to + # Task.cancel(msg) (if any) as the cancellation reason. + reason = err.args[0] if err.args and isinstance(err.args[0], str) else "" + handle._apply_cancel_command(cancel_command, reason=reason) # If the cancel command is for external workflow, we # have to add a seq and mark it pending if cancel_command.HasField("request_cancel_external_workflow_execution"): @@ -1858,17 +2080,16 @@ def apply_child_cancel_error() -> None: # TODO(cretz): Nothing waits on this future, so how # if at all should we report child-workflow cancel # request failure? - self._pending_external_cancels[cancel_seq] = self.create_future() + self._pending_external_cancels[cancel_seq] = ( + self.create_future(), + input.id, + ) # Function that runs in the handle async def run_child() -> Any: - while True: - try: - # We have to shield because we don't want the future itself - # to be cancelled - return await asyncio.shield(handle._result_fut) - except asyncio.CancelledError: - apply_child_cancel_error() + return await self._await_temporal_operation( + handle._result_fut, apply_child_cancel_error + ) # Create the handle and set as pending handle = _ChildWorkflowHandle( @@ -1878,14 +2099,12 @@ async def run_child() -> Any: self._pending_child_workflows[handle._seq] = handle # Wait on start before returning - while True: - try: - # We have to shield because we don't want the future itself - # to be cancelled - await asyncio.shield(handle._start_fut) - return handle - except asyncio.CancelledError: - apply_child_cancel_error() + await self._await_temporal_operation( + handle._start_fut, + apply_child_cancel_error, + reraise_on_workflow_cancellation=True, + ) + return handle async def _outbound_start_nexus_operation( self, input: StartNexusOperationInput[Any, OutputT] @@ -1906,26 +2125,37 @@ async def _outbound_start_nexus_operation( handle: _NexusOperationHandle[OutputT] async def operation_handle_fn() -> OutputT: - while True: - try: - return cast(OutputT, await asyncio.shield(handle._result_fut)) - except asyncio.CancelledError: - cancel_command = self._add_command() - handle._apply_cancel_command(cancel_command) + return cast( + OutputT, + await self._await_temporal_operation( + handle._result_fut, + lambda _err, command: handle._apply_cancel_command(command), + ), + ) + payload_converter = ( + temporalio.nexus.system._get_payload_converter( + self._workflow_context_payload_converter + ) + if temporalio.nexus.system.is_system_endpoint(input.endpoint) + else self._context_free_payload_converter + ) handle = _NexusOperationHandle( - self, self._next_seq("nexus_operation"), input, operation_handle_fn() + self, + self._next_seq("nexus_operation"), + input, + operation_handle_fn(), + payload_converter, ) handle._apply_schedule_command() self._pending_nexus_operations[handle._seq] = handle - while True: - try: - await asyncio.shield(handle._start_fut) - return handle - except asyncio.CancelledError: - cancel_command = self._add_command() - handle._apply_cancel_command(cancel_command) + await self._await_temporal_operation( + handle._start_fut, + lambda _err, command: handle._apply_cancel_command(command), + reraise_on_workflow_cancellation=True, + ) + return handle #### Miscellaneous helpers #### # These are in alphabetical order. @@ -1934,14 +2164,25 @@ def _add_command(self) -> temporalio.bridge.proto.workflow_commands.WorkflowComm self._assert_not_read_only("add command") return self._current_completion.successful.commands.add() + def _workflow_logic_flag_enabled(self, flag: _WorkflowLogicFlag) -> bool: + if flag in self._current_internal_flags: + return True + if self._is_replaying or flag not in self._default_workflow_logic_flags: + return False + self._current_completion.successful.used_internal_flags.append(flag) + return True + @contextmanager - def _as_read_only(self) -> Iterator[None]: - prev_val = self._read_only + def _as_read_only(self, *, in_query_or_validator: bool) -> Iterator[None]: + prev_read_only = self._read_only + prev_in_query_or_validator = self._in_query_or_validator self._read_only = True + self._in_query_or_validator = in_query_or_validator try: yield None finally: - self._read_only = prev_val + self._read_only = prev_read_only + self._in_query_or_validator = prev_in_query_or_validator def _assert_not_read_only( self, action_attempted: str, *, allow_during_delete: bool = False @@ -1955,6 +2196,56 @@ def _assert_not_read_only( f"While in read-only function, action attempted: {action_attempted}" ) + async def _await_temporal_operation( + self, + fut: asyncio.Future[_T], + apply_cancel: Callable[ + [ + asyncio.CancelledError, + temporalio.bridge.proto.workflow_commands.WorkflowCommand, + ], + None, + ], + *, + completed_cancellation_flag: _WorkflowLogicFlag | None = None, + reraise_on_workflow_cancellation: bool = False, + ) -> _T: + while True: + try: + # Protect the operation's result from task cancellation so a + # Temporal cancellation command can decide its outcome. The + # custom shield also avoids spurious error logs on Python 3.11+. + return await _shield_await(fut) + except asyncio.CancelledError as err: + if fut.done(): + # Retrying the shield after both futures become ready would + # return the result and erase the task cancellation. + if self._single_batch_activation: + raise + if completed_cancellation_flag is not None and ( + not self._is_replaying + or completed_cancellation_flag in self._current_internal_flags + ): + self._current_completion.successful.used_internal_flags.append( + completed_cancellation_flag + ) + raise + + apply_cancel(err, self._add_command()) + + # Clear the cancellation counter on Python 3.11+ so the next + # await does not immediately re-raise CancelledError. + if ( + sys.version_info >= (3, 11) + and (task := asyncio.current_task()) is not None + ): + task.uncancel() # type: ignore[union-attr] + + if reraise_on_workflow_cancellation and ( + self._cancel_reason is not None or self._deleting + ): + raise + async def _cancel_external_workflow( self, # Should not have seq set @@ -1964,8 +2255,9 @@ async def _cancel_external_workflow( done_fut = self.create_future() command.request_cancel_external_workflow_execution.seq = seq - # Set as pending - self._pending_external_cancels[seq] = done_fut + # Set as pending with the target workflow ID for later context use + target_workflow_id = command.request_cancel_external_workflow_execution.workflow_execution.workflow_id + self._pending_external_cancels[seq] = (done_fut, target_workflow_id) # Wait until done (there is no cancelling a cancel request) await done_fut @@ -1979,18 +2271,16 @@ def _check_condition(self, fn: Callable[[], bool], fut: asyncio.Future) -> bool: def _convert_payloads( self, payloads: Sequence[temporalio.api.common.v1.Payload], - types: Optional[List[Type]], - ) -> List[Any]: + types: list[type] | None, + payload_converter: temporalio.converter.PayloadConverter, + ) -> list[Any]: if not payloads: return [] # Only use type hints if they match count if types and len(types) != len(payloads): types = None try: - return self._payload_converter.from_payloads( - payloads, - type_hints=types, - ) + return payload_converter.from_payloads(payloads, type_hints=types) except temporalio.exceptions.FailureError: # Don't wrap payload conversion errors that would fail the workflow raise @@ -1999,11 +2289,184 @@ def _convert_payloads( raise raise RuntimeError("Failed decoding arguments") from err + def _payload_converter_with_context( + self, + context: temporalio.converter.SerializationContext, + ) -> temporalio.converter.PayloadConverter: + """Construct workflow payload converter with the given context. + + This plays a similar role to DataConverter._with_context, but operates on PayloadConverter + only (payload encoding/decoding is done by the worker, outside the workflow sandbox). + """ + payload_converter = self._context_free_payload_converter + if isinstance(payload_converter, temporalio.converter.WithSerializationContext): + payload_converter = payload_converter.with_context(context) + return payload_converter + + def _failure_converter_with_context( + self, + context: temporalio.converter.SerializationContext, + ) -> temporalio.converter.FailureConverter: + """Construct workflow failure converter with the given context. + + This plays a similar role to DataConverter._with_context, but operates on FailureConverter + only (payload encoding/decoding is done by the worker, outside the workflow sandbox). + """ + failure_converter = self._context_free_failure_converter + if isinstance(failure_converter, temporalio.converter.WithSerializationContext): + failure_converter = failure_converter.with_context(context) + return failure_converter + + def get_serialization_context( + self, + command_info: _command_aware_visitor.CommandInfo | None, + ) -> temporalio.converter.SerializationContext | None: + if command_info is None: + # Use payload codec with workflow context by default (i.e. for payloads not associated + # with a pending command) + return temporalio.converter.WorkflowSerializationContext( + namespace=self._info.namespace, + workflow_id=self._info.workflow_id, + ) + + if ( + command_info.command_type + == temporalio.api.enums.v1.command_type_pb2.CommandType.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK + and command_info.command_seq in self._pending_activities + ): + # Use the activity's context + activity_handle = self._pending_activities[command_info.command_seq] + return temporalio.converter.ActivitySerializationContext( + namespace=self._info.namespace, + workflow_id=self._info.workflow_id, + workflow_type=self._info.workflow_type, + activity_type=activity_handle._input.activity, + activity_id=activity_handle._input.activity_id, + activity_task_queue=( + activity_handle._input.task_queue + if isinstance(activity_handle._input, StartActivityInput) + and activity_handle._input.task_queue + else self._info.task_queue + ), + is_local=isinstance(activity_handle._input, StartLocalActivityInput), + ) + + elif ( + command_info.command_type + == temporalio.api.enums.v1.command_type_pb2.CommandType.COMMAND_TYPE_START_CHILD_WORKFLOW_EXECUTION + and command_info.command_seq in self._pending_child_workflows + ): + # Use the child workflow's context + child_wf_handle = self._pending_child_workflows[command_info.command_seq] + return temporalio.converter.WorkflowSerializationContext( + namespace=self._info.namespace, + workflow_id=child_wf_handle._input.id, + ) + + elif ( + command_info.command_type + == temporalio.api.enums.v1.command_type_pb2.CommandType.COMMAND_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION + and command_info.command_seq in self._pending_external_signals + ): + # Use the target workflow's context + _, target_workflow_id = self._pending_external_signals[ + command_info.command_seq + ] + return temporalio.converter.WorkflowSerializationContext( + namespace=self._info.namespace, + workflow_id=target_workflow_id, + ) + + elif ( + command_info.command_type + == temporalio.api.enums.v1.command_type_pb2.CommandType.COMMAND_TYPE_SCHEDULE_NEXUS_OPERATION + and command_info.command_seq in self._pending_nexus_operations + ): + # Use empty context for nexus operations: users will never want to encrypt using a + # key derived from caller workflow context because the caller workflow context is + # not available on the handler side for decryption. + return None + + else: + # Use payload codec with workflow context for all other payloads + return temporalio.converter.WorkflowSerializationContext( + namespace=self._info.namespace, + workflow_id=self._info.workflow_id, + ) + + def get_external_store_context( + self, + command_info: _command_aware_visitor.CommandInfo | None, + ) -> StorageDriverStoreContext: + # The current workflow is the default target for external store + # operations. For commands that target other workflows, those workflows + # are the target for that command's external store operation. For + # workflow activities, the target is the current workflow since the + # activity is bound to the lifetime of the current workflow, the + # activity run information is the same as the current workflow, and + # successfully completed activities are not involved in replay. + # Otherwise, the storage space for a given workflow would be disparate + # if stored under activity information. + current_wf = StorageDriverWorkflowInfo( + id=self._info.workflow_id, + run_id=self._info.run_id, + type=self._info.workflow_type, + namespace=self._info.namespace, + ) + + if command_info is None: + return StorageDriverStoreContext(target=current_wf) + + COMMAND_TYPE = temporalio.api.enums.v1.command_type_pb2.CommandType + + if ( + command_info.command_type + == COMMAND_TYPE.COMMAND_TYPE_START_CHILD_WORKFLOW_EXECUTION + and command_info.command_seq in self._pending_child_workflows + ): + child = self._pending_child_workflows[command_info.command_seq] + return StorageDriverStoreContext( + target=StorageDriverWorkflowInfo( + id=child._input.id, + type=child._input.workflow, + namespace=self._info.namespace, + ), + ) + + elif ( + command_info.command_type + == COMMAND_TYPE.COMMAND_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION + and command_info.command_seq in self._pending_external_signals + ): + _, target_id = self._pending_external_signals[command_info.command_seq] + return StorageDriverStoreContext( + target=StorageDriverWorkflowInfo( + id=target_id, namespace=self._info.namespace + ), + ) + + elif ( + command_info.command_type + == COMMAND_TYPE.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION + and self._info.parent is not None + and self._info.continued_run_id is None + ): + return StorageDriverStoreContext( + target=StorageDriverWorkflowInfo( + id=self._info.parent.workflow_id, + run_id=self._info.parent.run_id, + namespace=self._info.parent.namespace, + ), + ) + + else: + return StorageDriverStoreContext(target=current_wf) + def _instantiate_workflow_object(self) -> Any: if not self._workflow_input: raise RuntimeError("Expected workflow input. This is a Python SDK bug.") - if hasattr(self._defn.cls.__init__, "__temporal_workflow_init"): + if hasattr(self._defn.cls.__init__, "__temporal_workflow_init"): # type:ignore[misc] workflow_instance = self._defn.cls(*self._workflow_input.args) else: workflow_instance = self._defn.cls() @@ -2015,7 +2478,7 @@ def _instantiate_workflow_object(self) -> Any: if self._defn.name is None and self._defn.dynamic_config_fn is not None: dynamic_config = None try: - with self._as_read_only(): + with self._as_read_only(in_query_or_validator=False): dynamic_config = self._defn.dynamic_config_fn(workflow_instance) except Exception as err: logger.exception( @@ -2072,25 +2535,31 @@ def _process_handler_args( self, job_name: str, job_input: Sequence[temporalio.api.common.v1.Payload], - defn_name: Optional[str], - defn_arg_types: Optional[List[Type]], + defn_name: str | None, + defn_arg_types: list[type] | None, defn_dynamic_vararg: bool, - ) -> List[Any]: + ) -> list[Any]: # If dynamic old-style vararg, args become name + varargs of given arg # types. If dynamic new-style raw value sequence, args become name + # seq of raw values. if not defn_name and defn_dynamic_vararg: # Take off the string type hint for conversion arg_types = defn_arg_types[1:] if defn_arg_types else None - return [job_name] + self._convert_payloads(job_input, arg_types) + return [job_name] + self._convert_payloads( + job_input, arg_types, self._workflow_context_payload_converter + ) if not defn_name: return [ job_name, self._convert_payloads( - job_input, [temporalio.common.RawValue] * len(job_input) + job_input, + [temporalio.common.RawValue] * len(job_input), + self._workflow_context_payload_converter, ), ] - return self._convert_payloads(job_input, defn_arg_types) + return self._convert_payloads( + job_input, defn_arg_types, self._workflow_context_payload_converter + ) def _process_signal_job( self, @@ -2107,7 +2576,10 @@ def _process_signal_job( ) except Exception: logger.exception( - f"Failed deserializing signal input for {job.signal_name}, dropping the signal" + f"Failed deserializing signal input for {job.signal_name}" + f" on workflow {self._info.workflow_type} with ID {self._info.workflow_id}" + f" and run ID {self._info.run_id}, dropping the signal", + extra={"temporal_workflow": self._info._logger_details()}, ) return input = HandleSignalInput( @@ -2120,7 +2592,7 @@ def _process_signal_job( job.signal_name, defn.unfinished_policy ) - def done_callback(f): + def done_callback(_f: Any): self._in_progress_signals.pop(id, None) task = self.create_task( @@ -2133,15 +2605,14 @@ def _register_task( self, task: asyncio.Task, *, - name: Optional[str], + name: str | None, ) -> None: self._assert_not_read_only("create task") # Name not supported on older Python versions - if sys.version_info >= (3, 8): - # Put the workflow info at the end of the task name - name = name or task.get_name() - name += f" (workflow: {self._info.workflow_type}, id: {self._info.workflow_id}, run: {self._info.run_id})" - task.set_name(name) + # Put the workflow info at the end of the task name + name = name or task.get_name() + name += f" (workflow: {self._info.workflow_type}, id: {self._info.workflow_id}, run: {self._info.run_id})" + task.set_name(name) # Add to and remove from our own non-weak set instead of relying on # Python's weak set which can collect these too early self._tasks.add(task) @@ -2227,8 +2698,9 @@ async def _run_top_level_workflow_function(self, coro: Awaitable[None]) -> None: # cancel later on will show the workflow as cancelled. But this is # a Temporal limitation in that cancellation is a state not an # event. - if self._cancel_requested and temporalio.exceptions.is_cancelled_exception( - err + if ( + self._cancel_reason is not None + and temporalio.exceptions.is_cancelled_exception(err) ): self._add_command().cancel_workflow_execution.SetInParent() elif self.workflow_is_failure_exception(err): @@ -2243,7 +2715,9 @@ def _set_workflow_failure(self, err: BaseException) -> None: failure = self._add_command().fail_workflow_execution.failure failure.SetInParent() try: - self._failure_converter.to_failure(err, self._payload_converter, failure) + self._workflow_context_failure_converter.to_failure( + err, self._workflow_context_payload_converter, failure + ) except Exception as inner_err: raise ValueError("Failed converting workflow exception") from inner_err @@ -2256,18 +2730,20 @@ async def _signal_external_workflow( done_fut = self.create_future() command.signal_external_workflow_execution.seq = seq - # Set as pending - self._pending_external_signals[seq] = done_fut + target_workflow_id = ( + command.signal_external_workflow_execution.child_workflow_id + or command.signal_external_workflow_execution.workflow_execution.workflow_id + ) + self._pending_external_signals[seq] = (done_fut, target_workflow_id) + + def apply_cancel( + _err: asyncio.CancelledError, + command: temporalio.bridge.proto.workflow_commands.WorkflowCommand, + ) -> None: + command.cancel_signal_workflow.seq = seq # Wait until completed or cancelled - while True: - try: - # We have to shield because we don't want the future itself - # to be cancelled - return await asyncio.shield(done_fut) - except asyncio.CancelledError: - cancel_command = self._add_command() - cancel_command.cancel_signal_workflow.seq = seq + return await self._await_temporal_operation(done_fut, apply_cancel) def _stack_trace(self) -> str: stacks = [] @@ -2293,14 +2769,14 @@ def _enhanced_stack_trace(self) -> temporalio.api.sdk.v1.EnhancedStackTrace: # this is to use `open` with temporalio.workflow.unsafe.sandbox_unrestricted(): - sources: Dict[str, temporalio.api.sdk.v1.StackTraceFileSlice] = dict() - stacks: List[temporalio.api.sdk.v1.StackTrace] = [] + sources: dict[str, temporalio.api.sdk.v1.StackTraceFileSlice] = dict() + stacks: list[temporalio.api.sdk.v1.StackTrace] = [] # future TODO # site package filter list -- we want to filter out traces from Python's internals and our sdk's internals. This is what `internal_code` is for, but right now it's just set to false. for task in list(self._tasks): - locations: List[temporalio.api.sdk.v1.StackTraceFileLocation] = [] + locations: list[temporalio.api.sdk.v1.StackTraceFileLocation] = [] for frame in task.get_stack(): filename = frame.f_code.co_filename @@ -2309,7 +2785,7 @@ def _enhanced_stack_trace(self) -> temporalio.api.sdk.v1.EnhancedStackTrace: if filename not in sources.keys(): try: - with open(filename, "r") as f: + with open(filename) as f: code = f.read() except OSError as ose: code = f"Cannot access code.\n---\n{ose.strerror}" @@ -2382,7 +2858,7 @@ def _timer_impl( options: _TimerOptions, callback: Callable[..., Any], *args: Any, - context: Optional[contextvars.Context] = None, + context: contextvars.Context | None = None, ): self._assert_not_read_only("schedule timer") # Delay must be positive @@ -2409,11 +2885,13 @@ def _timer_handle_cancelled(self, handle: asyncio.TimerHandle) -> None: return handle._apply_cancel_command(self._add_command()) + _Ts = TypeVarTuple("_Ts") + def call_soon( self, - callback: Callable[..., Any], - *args: Any, - context: Optional[contextvars.Context] = None, + callback: Callable[[Unpack[_Ts]], object], + *args: Unpack[_Ts], + context: contextvars.Context | None = None, ) -> asyncio.Handle: # We need to allow this during delete because this is how tasks schedule # entire cancellation calls @@ -2425,9 +2903,9 @@ def call_soon( def call_later( self, delay: float, - callback: Callable[..., Any], - *args: Any, - context: Optional[contextvars.Context] = None, + callback: Callable[[Unpack[_Ts]], object], + *args: Unpack[_Ts], + context: contextvars.Context | None = None, ) -> asyncio.TimerHandle: options = _TimerOptionsCtxVar.get() return self._timer_impl(delay, options, callback, *args, context=context) @@ -2435,9 +2913,9 @@ def call_later( def call_at( self, when: float, - callback: Callable[..., Any], - *args: Any, - context: Optional[contextvars.Context] = None, + callback: Callable[[Unpack[_Ts]], object], + *args: Unpack[_Ts], + context: contextvars.Context | None = None, ) -> asyncio.TimerHandle: # We usually would not support fixed-future-time call (and we didn't # previously), but 3.11 added asyncio.timeout which uses it and 3.12 @@ -2459,10 +2937,10 @@ def create_future(self) -> asyncio.Future[Any]: def create_task( self, - coro: Union[Awaitable[_T], Generator[Any, None, _T]], + coro: Awaitable[_T] | Generator[Any, None, _T], *, - name: Optional[str] = None, - context: Optional[contextvars.Context] = None, + name: str | None = None, + context: contextvars.Context | None = None, ) -> asyncio.Task[_T]: # Context only supported on newer Python versions if sys.version_info >= (3, 11): @@ -2472,13 +2950,13 @@ def create_task( self._register_task(task, name=name) return task - def get_exception_handler(self) -> Optional[_ExceptionHandler]: + def get_exception_handler(self) -> _ExceptionHandler | None: return self._exception_handler def get_task_factory(self) -> None: return None - def set_exception_handler(self, handler: Optional[_ExceptionHandler]) -> None: + def set_exception_handler(self, handler: _ExceptionHandler | None) -> None: self._exception_handler = handler def default_exception_handler(self, context: _Context) -> None: @@ -2561,6 +3039,9 @@ def call_exception_handler(self, context: _Context) -> None: def get_debug(self) -> bool: return False + def is_running(self) -> bool: + return True + class _WorkflowInboundImpl(WorkflowInboundInterceptor): def __init__( # type: ignore @@ -2662,7 +3143,7 @@ def start_local_activity( @dataclass(frozen=True) class _TimerOptions: - user_metadata: Optional[temporalio.api.sdk.v1.UserMetadata] = None + user_metadata: temporalio.api.sdk.v1.UserMetadata | None = None _TimerOptionsCtxVar: contextvars.ContextVar[_TimerOptions] = contextvars.ContextVar( @@ -2675,11 +3156,11 @@ def __init__( self, seq: int, when: float, - options: Optional[_TimerOptions], + options: _TimerOptions | None, callback: Callable[..., Any], args: Sequence[Any], loop: asyncio.AbstractEventLoop, - context: Optional[contextvars.Context], + context: contextvars.Context | None, ) -> None: super().__init__(when, callback, args, loop, context) self._seq = seq @@ -2714,7 +3195,7 @@ class _ActivityHandle(temporalio.workflow.ActivityHandle[Any]): def __init__( self, instance: _WorkflowInstanceImpl, - input: Union[StartActivityInput, StartLocalActivityInput], + input: StartActivityInput | StartLocalActivityInput, fn: Coroutine[Any, Any, Any], ) -> None: super().__init__(fn) @@ -2724,8 +3205,23 @@ def __init__( self._result_fut = instance.create_future() self._started = False instance._register_task(self, name=f"activity: {input.activity}") + self._payload_converter = self._instance._payload_converter_with_context( + temporalio.converter.ActivitySerializationContext( + namespace=self._instance._info.namespace, + workflow_id=self._instance._info.workflow_id, + workflow_type=self._instance._info.workflow_type, + activity_type=self._input.activity, + activity_id=self._input.activity_id, + activity_task_queue=( + self._input.task_queue or self._instance._info.task_queue + if isinstance(self._input, StartActivityInput) + else self._instance._info.task_queue + ), + is_local=isinstance(self._input, StartLocalActivityInput), + ) + ) - def cancel(self, msg: Optional[Any] = None) -> bool: + def cancel(self, msg: Any | None = None) -> bool: # Allow the cancel to go through for the task even if we're deleting, # just don't do any commands if not self._instance._deleting: @@ -2735,9 +3231,6 @@ def cancel(self, msg: Optional[Any] = None) -> bool: # the cancel (i.e. cancelled before started) if not self._started and not self.done(): self._apply_cancel_command(self._instance._add_command()) - # Message not supported in older versions - if sys.version_info < (3, 9): - return super().cancel() return super().cancel(msg) def _resolve_success(self, result: Any) -> None: @@ -2765,23 +3258,22 @@ def _resolve_backoff( def _apply_schedule_command( self, - local_backoff: Optional[ - temporalio.bridge.proto.activity_result.DoBackoff - ] = None, + local_backoff: None + | (temporalio.bridge.proto.activity_result.DoBackoff) = None, ) -> None: # Convert arguments before creating command in case it raises error payloads = ( - self._instance._payload_converter.to_payloads(self._input.args) + self._payload_converter.to_payloads(self._input.args) if self._input.args else None ) command = self._instance._add_command() # TODO(cretz): Why can't MyPy infer this? - v: Union[ - temporalio.bridge.proto.workflow_commands.ScheduleActivity, - temporalio.bridge.proto.workflow_commands.ScheduleLocalActivity, - ] = ( + v: ( + temporalio.bridge.proto.workflow_commands.ScheduleActivity + | temporalio.bridge.proto.workflow_commands.ScheduleLocalActivity + ) = ( command.schedule_local_activity if isinstance(self._input, StartLocalActivityInput) else command.schedule_activity @@ -2807,7 +3299,7 @@ def _apply_schedule_command( self._input.retry_policy.apply_to_proto(v.retry_policy) if self._input.summary: command.user_metadata.summary.CopyFrom( - self._instance._payload_converter.to_payload(self._input.summary) + self._payload_converter.to_payload(self._input.summary) ) v.cancellation_type = cast( temporalio.bridge.proto.workflow_commands.ActivityCancellationType.ValueType, @@ -2871,18 +3363,28 @@ def __init__( self._result_fut: asyncio.Future[Any] = instance.create_future() self._first_execution_run_id = "" instance._register_task(self, name=f"child: {input.workflow}") + workflow_context = temporalio.converter.WorkflowSerializationContext( + namespace=self._instance._info.namespace, + workflow_id=self._input.id, + ) + self._payload_converter = self._instance._payload_converter_with_context( + workflow_context + ) + self._failure_converter = self._instance._failure_converter_with_context( + workflow_context + ) @property def id(self) -> str: return self._input.id @property - def first_execution_run_id(self) -> Optional[str]: + def first_execution_run_id(self) -> str | None: return self._first_execution_run_id async def signal( self, - signal: Union[str, Callable], + signal: str | Callable, arg: Any = temporalio.common._arg_unset, *, args: Sequence[Any] = [], @@ -2921,7 +3423,7 @@ def _resolve_failure(self, err: BaseException) -> None: def _apply_start_command(self) -> None: # Convert arguments before creating command in case it raises error payloads = ( - self._instance._payload_converter.to_payloads(self._input.args) + self._payload_converter.to_payloads(self._input.args) if self._input.args else None ) @@ -2956,12 +3458,10 @@ def _apply_start_command(self) -> None: temporalio.common._apply_headers(self._input.headers, v.headers) if self._input.memo: for k, val in self._input.memo.items(): - v.memo[k].CopyFrom( - self._instance._payload_converter.to_payloads([val])[0] - ) + v.memo[k].CopyFrom(self._payload_converter.to_payloads([val])[0]) if self._input.search_attributes: _encode_search_attributes( - self._input.search_attributes, v.search_attributes + self._input.search_attributes, v.search_attributes.indexed_fields ) v.cancellation_type = cast( temporalio.bridge.proto.child_workflow.ChildWorkflowCancellationType.ValueType, @@ -2971,11 +3471,11 @@ def _apply_start_command(self) -> None: v.versioning_intent = self._input.versioning_intent._to_proto() if self._input.static_summary: command.user_metadata.summary.CopyFrom( - self._instance._payload_converter.to_payload(self._input.static_summary) + self._payload_converter.to_payload(self._input.static_summary) ) if self._input.static_details: command.user_metadata.details.CopyFrom( - self._instance._payload_converter.to_payload(self._input.static_details) + self._payload_converter.to_payload(self._input.static_details) ) if self._input.priority: v.priority.CopyFrom(self._input.priority._to_proto()) @@ -2984,8 +3484,12 @@ def _apply_start_command(self) -> None: def _apply_cancel_command( self, command: temporalio.bridge.proto.workflow_commands.WorkflowCommand, + *, + reason: str = "", ) -> None: - command.cancel_child_workflow_execution.child_workflow_seq = self._seq + v = command.cancel_child_workflow_execution + v.child_workflow_seq = self._seq + v.reason = reason class _ExternalWorkflowHandle(temporalio.workflow.ExternalWorkflowHandle[Any]): @@ -2993,7 +3497,7 @@ def __init__( self, instance: _WorkflowInstanceImpl, id: str, - run_id: Optional[str], + run_id: str | None, ) -> None: super().__init__() self._instance = instance @@ -3005,12 +3509,12 @@ def id(self) -> str: return self._id @property - def run_id(self) -> Optional[str]: + def run_id(self) -> str | None: return self._run_id async def signal( self, - signal: Union[str, Callable], + signal: str | Callable, arg: Any = temporalio.common._arg_unset, *, args: Sequence[Any] = [], @@ -3029,7 +3533,7 @@ async def signal( ) ) - async def cancel(self) -> None: + async def cancel(self, *, reason: str = "") -> None: self._instance._assert_not_read_only("cancel external handle") command = self._instance._add_command() v = command.request_cancel_external_workflow_execution @@ -3037,12 +3541,10 @@ async def cancel(self) -> None: v.workflow_execution.workflow_id = self._id if self._run_id: v.workflow_execution.run_id = self._run_id + v.reason = reason await self._instance._cancel_external_workflow(command) -# TODO(nexus-preview): are we sure we don't want to inherit from asyncio.Task as -# ActivityHandle and ChildWorkflowHandle do? I worry that we should provide .done(), -# .result(), .exception() etc for consistency. class _NexusOperationHandle(temporalio.workflow.NexusOperationHandle[OutputT]): def __init__( self, @@ -3050,16 +3552,19 @@ def __init__( seq: int, input: StartNexusOperationInput[Any, OutputT], fn: Coroutine[Any, Any, OutputT], + payload_converter: temporalio.converter.PayloadConverter, ): self._instance = instance self._seq = seq self._input = input self._task = asyncio.Task(fn) - self._start_fut: asyncio.Future[Optional[str]] = instance.create_future() - self._result_fut: asyncio.Future[Optional[OutputT]] = instance.create_future() + self._start_fut: asyncio.Future[str | None] = instance.create_future() + self._result_fut: asyncio.Future[OutputT | None] = instance.create_future() + self._payload_converter = payload_converter + self._failure_converter = self._instance._context_free_failure_converter @property - def operation_token(self) -> Optional[str]: + def operation_token(self) -> str | None: try: return self._start_fut.result() except BaseException: @@ -3071,7 +3576,7 @@ def __await__(self) -> Generator[Any, Any, OutputT]: def cancel(self) -> bool: return self._task.cancel() - def _resolve_start_success(self, operation_token: Optional[str]) -> None: + def _resolve_start_success(self, operation_token: str | None) -> None: # We intentionally let this error if already done self._start_fut.set_result(operation_token) @@ -3089,7 +3594,7 @@ def _resolve_failure(self, err: BaseException) -> None: self._result_fut.set_result(None) def _apply_schedule_command(self) -> None: - payload = self._instance._payload_converter.to_payload(self._input.input) + payload = self._payload_converter.to_payload(self._input.input) command = self._instance._add_command() v = command.schedule_nexus_operation v.seq = self._seq @@ -3101,6 +3606,12 @@ def _apply_schedule_command(self) -> None: v.schedule_to_close_timeout.FromTimedelta( self._input.schedule_to_close_timeout ) + if self._input.schedule_to_start_timeout is not None: + v.schedule_to_start_timeout.FromTimedelta( + self._input.schedule_to_start_timeout + ) + if self._input.start_to_close_timeout is not None: + v.start_to_close_timeout.FromTimedelta(self._input.start_to_close_timeout) v.cancellation_type = cast( temporalio.bridge.proto.nexus.NexusOperationCancellationType.ValueType, int(self._input.cancellation_type), @@ -3110,6 +3621,11 @@ def _apply_schedule_command(self) -> None: for key, val in self._input.headers.items(): v.nexus_header[key] = val + if self._input.summary: + command.user_metadata.summary.CopyFrom( + self._payload_converter.to_payload(self._input.summary) + ) + def _apply_cancel_command( self, command: temporalio.bridge.proto.workflow_commands.WorkflowCommand, @@ -3128,13 +3644,17 @@ def __init__( def _apply_command(self) -> None: # Convert arguments before creating command in case it raises error payloads = ( - self._instance._payload_converter.to_payloads(self._input.args) + self._instance._workflow_context_payload_converter.to_payloads( + self._input.args + ) if self._input.args else None ) memo_payloads = ( { - k: self._instance._payload_converter.to_payloads([val])[0] + k: self._instance._workflow_context_payload_converter.to_payloads( + [val] + )[0] for k, val in self._input.memo.items() } if self._input.memo @@ -3154,6 +3674,8 @@ def _apply_command(self) -> None: v.workflow_run_timeout.FromTimedelta(self._input.run_timeout) if self._input.task_timeout: v.workflow_task_timeout.FromTimedelta(self._input.task_timeout) + if self._input.backoff_start_interval: + v.backoff_start_interval.FromTimedelta(self._input.backoff_start_interval) if self._input.headers: temporalio.common._apply_headers(self._input.headers, v.headers) if self._input.retry_policy: @@ -3163,16 +3685,21 @@ def _apply_command(self) -> None: v.memo[k].CopyFrom(val) if self._input.search_attributes: _encode_search_attributes( - self._input.search_attributes, v.search_attributes + self._input.search_attributes, v.search_attributes.indexed_fields ) if self._input.versioning_intent: v.versioning_intent = self._input.versioning_intent._to_proto() + if self._input.initial_versioning_behavior: + v.initial_versioning_behavior = cast( + "temporalio.api.enums.v1.ContinueAsNewVersioningBehavior.ValueType", + int(self._input.initial_versioning_behavior), + ) def _encode_search_attributes( - attributes: Union[ - temporalio.common.SearchAttributes, temporalio.common.TypedSearchAttributes - ], + attributes: ( + temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes + ), payloads: Mapping[str, temporalio.api.common.v1.Payload], ) -> None: if isinstance(attributes, temporalio.common.TypedSearchAttributes): @@ -3199,42 +3726,42 @@ def __init__(self, underlying: temporalio.common.MetricMeter) -> None: self._underlying = underlying def create_counter( - self, name: str, description: Optional[str] = None, unit: Optional[str] = None + self, name: str, description: str | None = None, unit: str | None = None ) -> temporalio.common.MetricCounter: return _ReplaySafeMetricCounter( self._underlying.create_counter(name, description, unit) ) def create_histogram( - self, name: str, description: Optional[str] = None, unit: Optional[str] = None + self, name: str, description: str | None = None, unit: str | None = None ) -> temporalio.common.MetricHistogram: return _ReplaySafeMetricHistogram( self._underlying.create_histogram(name, description, unit) ) def create_histogram_float( - self, name: str, description: Optional[str] = None, unit: Optional[str] = None + self, name: str, description: str | None = None, unit: str | None = None ) -> temporalio.common.MetricHistogramFloat: return _ReplaySafeMetricHistogramFloat( self._underlying.create_histogram_float(name, description, unit) ) def create_histogram_timedelta( - self, name: str, description: Optional[str] = None, unit: Optional[str] = None + self, name: str, description: str | None = None, unit: str | None = None ) -> temporalio.common.MetricHistogramTimedelta: return _ReplaySafeMetricHistogramTimedelta( self._underlying.create_histogram_timedelta(name, description, unit) ) def create_gauge( - self, name: str, description: Optional[str] = None, unit: Optional[str] = None + self, name: str, description: str | None = None, unit: str | None = None ) -> temporalio.common.MetricGauge: return _ReplaySafeMetricGauge( self._underlying.create_gauge(name, description, unit) ) def create_gauge_float( - self, name: str, description: Optional[str] = None, unit: Optional[str] = None + self, name: str, description: str | None = None, unit: str | None = None ) -> temporalio.common.MetricGaugeFloat: return _ReplaySafeMetricGaugeFloat( self._underlying.create_gauge_float(name, description, unit) @@ -3260,11 +3787,11 @@ def name(self) -> str: return self._underlying.name @property - def description(self) -> Optional[str]: + def description(self) -> str | None: return self._underlying.description @property - def unit(self) -> Optional[str]: + def unit(self) -> str | None: return self._underlying.unit def with_additional_attributes( @@ -3282,7 +3809,7 @@ class _ReplaySafeMetricCounter( def add( self, value: int, - additional_attributes: Optional[temporalio.common.MetricAttributes] = None, + additional_attributes: temporalio.common.MetricAttributes | None = None, ) -> None: if not temporalio.workflow.unsafe.is_replaying(): self._underlying.add(value, additional_attributes) @@ -3295,7 +3822,7 @@ class _ReplaySafeMetricHistogram( def record( self, value: int, - additional_attributes: Optional[temporalio.common.MetricAttributes] = None, + additional_attributes: temporalio.common.MetricAttributes | None = None, ) -> None: if not temporalio.workflow.unsafe.is_replaying(): self._underlying.record(value, additional_attributes) @@ -3308,7 +3835,7 @@ class _ReplaySafeMetricHistogramFloat( def record( self, value: float, - additional_attributes: Optional[temporalio.common.MetricAttributes] = None, + additional_attributes: temporalio.common.MetricAttributes | None = None, ) -> None: if not temporalio.workflow.unsafe.is_replaying(): self._underlying.record(value, additional_attributes) @@ -3321,7 +3848,7 @@ class _ReplaySafeMetricHistogramTimedelta( def record( self, value: timedelta, - additional_attributes: Optional[temporalio.common.MetricAttributes] = None, + additional_attributes: temporalio.common.MetricAttributes | None = None, ) -> None: if not temporalio.workflow.unsafe.is_replaying(): self._underlying.record(value, additional_attributes) @@ -3334,7 +3861,7 @@ class _ReplaySafeMetricGauge( def set( self, value: int, - additional_attributes: Optional[temporalio.common.MetricAttributes] = None, + additional_attributes: temporalio.common.MetricAttributes | None = None, ) -> None: if not temporalio.workflow.unsafe.is_replaying(): self._underlying.set(value, additional_attributes) @@ -3347,7 +3874,7 @@ class _ReplaySafeMetricGaugeFloat( def set( self, value: float, - additional_attributes: Optional[temporalio.common.MetricAttributes] = None, + additional_attributes: temporalio.common.MetricAttributes | None = None, ) -> None: if not temporalio.workflow.unsafe.is_replaying(): self._underlying.set(value, additional_attributes) @@ -3363,11 +3890,11 @@ class HandlerExecution: name: str unfinished_policy: temporalio.workflow.HandlerUnfinishedPolicy - id: Optional[str] = None + id: str | None = None def _make_unfinished_update_handler_message( - handler_executions: List[HandlerExecution], + handler_executions: list[HandlerExecution], ) -> str: message = """ [TMPRL1102] Workflow finished while update handlers are still running. This may have interrupted work that the @@ -3386,7 +3913,7 @@ def _make_unfinished_update_handler_message( def _make_unfinished_signal_handler_message( - handler_executions: List[HandlerExecution], + handler_executions: list[HandlerExecution], ) -> str: message = """ [TMPRL1102] Workflow finished while signal handlers are still running. This may have interrupted work that the @@ -3409,3 +3936,11 @@ class _WorkflowLogicFlag(IntEnum): """Flags that may be set on task/activation completion to differentiate new from old workflow behavior.""" RAISE_ON_CANCELLING_COMPLETED_ACTIVITY = 1 + PROCESS_WORKFLOW_ACTIVATION_JOBS_AS_SINGLE_BATCH = 2 + + +# TODO: Enable PROCESS_WORKFLOW_ACTIVATION_JOBS_AS_SINGLE_BATCH by default after +# two published SDK releases have recognized flag 2. When enabling it, remove the +# explicit overrides for this flag from tests/worker/test_workflow.py, then remove +# this reminder. +_DEFAULT_ENABLED_WORKFLOW_LOGIC_FLAGS: frozenset[_WorkflowLogicFlag] = frozenset() diff --git a/temporalio/worker/workflow_sandbox/__init__.py b/temporalio/worker/workflow_sandbox/__init__.py index 399e633cc..1bdc62954 100644 --- a/temporalio/worker/workflow_sandbox/__init__.py +++ b/temporalio/worker/workflow_sandbox/__init__.py @@ -58,11 +58,13 @@ RestrictedWorkflowAccessError, SandboxMatcher, SandboxRestrictions, + UnintentionalPassthroughError, ) from ._runner import SandboxedWorkflowRunner __all__ = [ "RestrictedWorkflowAccessError", + "UnintentionalPassthroughError", "SandboxedWorkflowRunner", "SandboxMatcher", "SandboxRestrictions", diff --git a/temporalio/worker/workflow_sandbox/_importer.py b/temporalio/worker/workflow_sandbox/_importer.py index 462bd44c2..1ab0a1dd6 100644 --- a/temporalio/worker/workflow_sandbox/_importer.py +++ b/temporalio/worker/workflow_sandbox/_importer.py @@ -15,20 +15,11 @@ import threading import types import warnings +from collections.abc import Callable, Iterator, Mapping, MutableMapping, Sequence from contextlib import ExitStack, contextmanager from typing import ( Any, - Callable, - Dict, Generic, - Iterator, - List, - Mapping, - MutableMapping, - Optional, - Sequence, - Set, - Tuple, TypeVar, no_type_check, ) @@ -42,6 +33,7 @@ RestrictedWorkflowAccessError, RestrictionContext, SandboxRestrictions, + UnintentionalPassthroughError, ) logger = logging.getLogger(__name__) @@ -66,23 +58,23 @@ def __init__( """Create importer.""" self.restrictions = restrictions self.restriction_context = restriction_context - self.new_modules: Dict[str, types.ModuleType] = { + self.new_modules: dict[str, types.ModuleType] = { "sys": sys, "builtins": builtins, # Even though we don't want to, we have to have __main__ because # stdlib packages like inspect and others expect it to be present "__main__": types.ModuleType("__main__"), } - self.modules_checked_for_restrictions: Set[str] = set() + self.modules_checked_for_restrictions: set[str] = set() self.import_func = self._import if not LOG_TRACE else self._traced_import # Pre-collect restricted builtins - self.restricted_builtins: List[Tuple[str, _ThreadLocalCallable, Callable]] = [] + self.restricted_builtins: list[tuple[str, _ThreadLocalCallable, Callable]] = [] builtin_matcher = restrictions.invalid_module_members.child_matcher( "__builtins__" ) if builtin_matcher: - def restrict_built_in(name: str, orig: Any, *args, **kwargs): + def restrict_built_in(name: str, orig: Any, *args: Any, **kwargs: Any): # Check if restricted against matcher if ( builtin_matcher @@ -91,7 +83,15 @@ def restrict_built_in(name: str, orig: Any, *args, **kwargs): ) and not temporalio.workflow.unsafe.is_sandbox_unrestricted() ): - raise RestrictedWorkflowAccessError(f"__builtins__.{name}") + # If a per-builtin child matcher carries a custom + # leaf_message (e.g. directing the user to debug_mode for + # breakpoint()), surface that instead of the generic + # pass-through-modules advice. + child = builtin_matcher.children.get(name) + override_message = child.leaf_message if child else None + raise RestrictedWorkflowAccessError( + f"__builtins__.{name}", override_message=override_message + ) return orig(*args, **kwargs) for k in dir(builtins): @@ -150,7 +150,9 @@ def applied(self) -> Iterator[None]: try: with _thread_local_sys_modules.applied(sys, "modules", self.new_modules): with _thread_local_import.applied( - builtins, "__import__", self.import_func + builtins, + "__import__", + self.import_func, # type: ignore[reportArgumentType] ): with self._builtins_restricted(): yield None @@ -173,8 +175,8 @@ def _unapplied(self) -> Iterator[None]: def _traced_import( self, name: str, - globals: Optional[Mapping[str, object]] = None, - locals: Optional[Mapping[str, object]] = None, + globals: Mapping[str, object] | None = None, + locals: Mapping[str, object] | None = None, fromlist: Sequence[str] = (), level: int = 0, ) -> types.ModuleType: @@ -189,8 +191,8 @@ def _traced_import( def _import( self, name: str, - globals: Optional[Mapping[str, object]] = None, - locals: Optional[Mapping[str, object]] = None, + globals: Mapping[str, object] | None = None, + locals: Mapping[str, object] | None = None, fromlist: Sequence[str] = (), level: int = 0, ) -> types.ModuleType: @@ -225,6 +227,17 @@ def _import( setattr(sys.modules[parent], child, sys.modules[full_name]) # All children of this module that are on the original sys # modules but not here and are passthrough + else: + # Issue a warning if appropriate + if ( + self.restriction_context.in_activation + and self._is_import_notification_policy_applied( + temporalio.workflow.SandboxImportNotificationPolicy.WARN_ON_DYNAMIC_IMPORT + ) + ): + warnings.warn( + f"Module {full_name} was imported after initial workflow load." + ) # If the module is __temporal_main__ and not already in sys.modules, # we load it from whatever file __main__ was originally in @@ -282,13 +295,36 @@ def module_configured_passthrough(self, name: str) -> bool: break return True - def _maybe_passthrough_module(self, name: str) -> Optional[types.ModuleType]: + def _is_import_notification_policy_applied( + self, policy: temporalio.workflow.SandboxImportNotificationPolicy + ) -> bool: + override_policy = ( + temporalio.workflow.unsafe.current_import_notification_policy_override() + ) + if override_policy: + return policy in override_policy + + return policy in self.restrictions.import_notification_policy + + def _maybe_passthrough_module(self, name: str) -> types.ModuleType | None: # If imports not passed through and all modules are not passed through # and name not in passthrough modules, check parents if ( not temporalio.workflow.unsafe.is_imports_passed_through() and not self.module_configured_passthrough(name) ): + if self._is_import_notification_policy_applied( + temporalio.workflow.SandboxImportNotificationPolicy.RAISE_ON_UNINTENTIONAL_PASSTHROUGH + ): + raise UnintentionalPassthroughError(name) + + if self._is_import_notification_policy_applied( + temporalio.workflow.SandboxImportNotificationPolicy.WARN_ON_UNINTENTIONAL_PASSTHROUGH + ): + warnings.warn( + f"Module {name} was not intentionally passed through to the sandbox." + ) + return None # Do the pass through with self._unapplied(): @@ -301,10 +337,7 @@ def _maybe_passthrough_module(self, name: str) -> Optional[types.ModuleType]: finally: _trace_depth -= 1 - def _maybe_restrict_module( - self, mod: types.ModuleType - ) -> Optional[types.ModuleType]: - """Implements :py:meth:`_Environment.maybe_restrict_module`.""" + def _maybe_restrict_module(self, mod: types.ModuleType) -> types.ModuleType | None: matcher = self.restrictions.invalid_module_members.child_matcher( *mod.__name__.split(".") ) @@ -338,7 +371,7 @@ def _builtins_unrestricted(self) -> Iterator[None]: _thread_local_current = threading.local() @staticmethod - def current_importer() -> Optional[Importer]: + def current_importer() -> Importer | None: """Get the current importer if any.""" return Importer._thread_local_current.__dict__.get("importer") @@ -354,7 +387,7 @@ def __init__(self, orig: _T) -> None: self.applied_counter_lock = threading.Lock() @property - def maybe_current(self) -> Optional[_T]: + def maybe_current(self) -> _T | None: return self.thread_local.__dict__.get("data") @property @@ -412,7 +445,7 @@ def unapplied(self) -> Iterator[None]: class _ThreadLocalSysModules( - _ThreadLocalOverride[Dict[str, types.ModuleType]], + _ThreadLocalOverride[dict[str, types.ModuleType]], MutableMapping[str, types.ModuleType], ): def __contains__(self, key: object) -> bool: @@ -451,28 +484,24 @@ def __setitem__(self, key: str, value: types.ModuleType) -> None: def __or__( self, other: Mapping[str, types.ModuleType] - ) -> Dict[str, types.ModuleType]: - if sys.version_info < (3, 9): - raise NotImplementedError - return self.current.__or__(other) + ) -> dict[str, types.ModuleType]: + return self.current.__or__(other) # type: ignore[operator] def __ior__( self, other: Mapping[str, types.ModuleType] - ) -> Dict[str, types.ModuleType]: - if sys.version_info < (3, 9): - raise NotImplementedError + ) -> dict[str, types.ModuleType]: return self.current.__ior__(other) __ror__ = __or__ - def copy(self) -> Dict[str, types.ModuleType]: + def copy(self) -> dict[str, types.ModuleType]: return self.current.copy() @classmethod - def fromkeys(cls, *args, **kwargs) -> Any: + def fromkeys(cls, *args: Any, **kwargs: Any) -> Any: return dict.fromkeys(*args, **kwargs) - def _lazily_passthrough_if_available(self, key: str) -> Optional[types.ModuleType]: + def _lazily_passthrough_if_available(self, key: str) -> types.ModuleType | None: # We only lazily pass through if it's in orig, lazy not disabled, and # module configured as pass through if ( @@ -499,7 +528,7 @@ def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _T: _thread_local_import = _ThreadLocalCallable(builtins.__import__) -_thread_local_builtins: Dict[str, _ThreadLocalCallable] = {} +_thread_local_builtins: dict[str, _ThreadLocalCallable] = {} def _get_thread_local_builtin(name: str) -> _ThreadLocalCallable: @@ -511,7 +540,7 @@ def _get_thread_local_builtin(name: str) -> _ThreadLocalCallable: def _resolve_module_name( - name: str, globals: Optional[Mapping[str, object]], level: int + name: str, globals: Mapping[str, object] | None, level: int ) -> str: if level == 0: return name @@ -537,7 +566,7 @@ def _calc___package__(globals: Mapping[str, object]) -> str: if package is not None: if spec is not None and package != spec.parent: warnings.warn( - "__package__ != __spec__.parent " f"({package!r} != {spec.parent!r})", + f"__package__ != __spec__.parent ({package!r} != {spec.parent!r})", DeprecationWarning, stacklevel=3, ) diff --git a/temporalio/worker/workflow_sandbox/_in_sandbox.py b/temporalio/worker/workflow_sandbox/_in_sandbox.py index 3091cef1d..d18374899 100644 --- a/temporalio/worker/workflow_sandbox/_in_sandbox.py +++ b/temporalio/worker/workflow_sandbox/_in_sandbox.py @@ -6,12 +6,15 @@ import dataclasses import logging -from typing import Any, Type +from typing import Any import temporalio.bridge.proto.workflow_activation import temporalio.bridge.proto.workflow_completion +import temporalio.converter import temporalio.worker._workflow_instance import temporalio.workflow +from temporalio.converter._extstore import StorageDriverStoreContext +from temporalio.worker import _command_aware_visitor logger = logging.getLogger(__name__) @@ -30,8 +33,8 @@ class InSandbox: def __init__( self, instance_details: temporalio.worker._workflow_instance.WorkflowInstanceDetails, - runner_class: Type[temporalio.worker._workflow_instance.WorkflowRunner], - workflow_class: Type, + runner_class: type[temporalio.worker._workflow_instance.WorkflowRunner], + workflow_class: type, ) -> None: """Create in-sandbox instance.""" _trace("Initializing workflow %s in sandbox", workflow_class) @@ -79,3 +82,17 @@ def activate( ) -> temporalio.bridge.proto.workflow_completion.WorkflowActivationCompletion: """Send activation to this instance.""" return self.instance.activate(act) + + def get_serialization_context( + self, + command_info: _command_aware_visitor.CommandInfo | None, + ) -> temporalio.converter.SerializationContext | None: + """Get serialization context.""" + return self.instance.get_serialization_context(command_info) + + def get_external_store_context( + self, + command_info: _command_aware_visitor.CommandInfo | None, + ) -> StorageDriverStoreContext: + """Get store context for external storage.""" + return self.instance.get_external_store_context(command_info) diff --git a/temporalio/worker/workflow_sandbox/_restrictions.py b/temporalio/worker/workflow_sandbox/_restrictions.py index 3e2bbf643..23774fcd2 100644 --- a/temporalio/worker/workflow_sandbox/_restrictions.py +++ b/temporalio/worker/workflow_sandbox/_restrictions.py @@ -14,21 +14,15 @@ import math import operator import random +import sys import types import warnings +from collections.abc import Callable, Mapping, Sequence from copy import copy, deepcopy from dataclasses import dataclass from typing import ( Any, - Callable, ClassVar, - Dict, - Mapping, - Optional, - Sequence, - Set, - Tuple, - Type, TypeVar, cast, ) @@ -39,8 +33,9 @@ HAVE_PYDANTIC = True except ImportError: - HAVE_PYDANTIC = False + HAVE_PYDANTIC = False # type: ignore[reportConstantRedefinition] +import temporalio.exceptions import temporalio.workflow logger = logging.getLogger(__name__) @@ -61,7 +56,7 @@ class RestrictedWorkflowAccessError(temporalio.workflow.NondeterminismError): """ def __init__( - self, qualified_name: str, *, override_message: Optional[str] = None + self, qualified_name: str, *, override_message: str | None = None ) -> None: """Create restricted workflow access error.""" super().__init__( @@ -81,11 +76,26 @@ def default_message(qualified_name: str) -> str: ) +class UnintentionalPassthroughError(temporalio.exceptions.TemporalError): + """Error that occurs when a workflow unintentionally passes an import to the sandbox when + the import notification policy includes :py:attr:`temporalio.workflow.SandboxImportNotificationPolicy.RAISE_ON_UNINTENTIONAL_PASSTHROUGH`. + + Attributes: + qualified_name: Fully qualified name of what was passed through to the sandbox. + """ + + def __init__(self, qualified_name: str) -> None: + """Create an unintentional passthrough error.""" + super().__init__( + f"Module {qualified_name} was not intentionally passed through to the sandbox." + ) + + @dataclass(frozen=True) class SandboxRestrictions: """Set of restrictions that can be applied to a sandbox.""" - passthrough_modules: Set[str] + passthrough_modules: set[str] """ Modules which pass through because we know they are side-effect free (or the side-effecting pieces are restricted). These modules will not be reloaded, @@ -109,6 +119,13 @@ class methods (including __init__, etc). The check compares the against the fully qualified path to the item. """ + import_notification_policy: temporalio.workflow.SandboxImportNotificationPolicy = ( + temporalio.workflow.SandboxImportNotificationPolicy.WARN_ON_DYNAMIC_IMPORT + ) + """ + The import notification policy to use when an import is triggered during workflow loading or execution. See :py:class:`temporalio.workflow.SandboxImportNotificationPolicy` for options. + """ + passthrough_all_modules: bool = False """ Pass through all modules, do not sandbox any modules. This is the equivalent @@ -129,19 +146,19 @@ class methods (including __init__, etc). The check compares the against the to have been explicitly imported. """ - passthrough_modules_minimum: ClassVar[Set[str]] + passthrough_modules_minimum: ClassVar[set[str]] """Set of modules that must be passed through at the minimum.""" - passthrough_modules_with_temporal: ClassVar[Set[str]] + passthrough_modules_with_temporal: ClassVar[set[str]] """Minimum modules that must be passed through and the Temporal modules.""" - passthrough_modules_maximum: ClassVar[Set[str]] + passthrough_modules_maximum: ClassVar[set[str]] """ All modules that can be passed through. This includes all standard library modules. """ - passthrough_modules_default: ClassVar[Set[str]] + passthrough_modules_default: ClassVar[set[str]] """Same as :py:attr:`passthrough_modules_maximum`.""" invalid_module_members_default: ClassVar[SandboxMatcher] @@ -169,6 +186,12 @@ def with_passthrough_all_modules(self) -> SandboxRestrictions: """ return dataclasses.replace(self, passthrough_all_modules=True) + def with_import_notification_policy( + self, policy: temporalio.workflow.SandboxImportNotificationPolicy + ) -> SandboxRestrictions: + """Create a new restriction set with the given import notification policy as the :py:attr:`import_notification_policy`.""" + return dataclasses.replace(self, import_notification_policy=policy) + # We intentionally use specific fields instead of generic "matcher" callbacks # for optimization reasons. @@ -181,8 +204,8 @@ class SandboxMatcher: instances. """ - @staticmethod - def nested_child(path: Sequence[str], child: SandboxMatcher) -> SandboxMatcher: + @classmethod + def nested_child(cls, path: Sequence[str], child: SandboxMatcher) -> SandboxMatcher: """Create a matcher where the given child is put at the given path. Args: @@ -194,12 +217,12 @@ def nested_child(path: Sequence[str], child: SandboxMatcher) -> SandboxMatcher: """ ret = child for key in reversed(path): - ret = SandboxMatcher(children={key: ret}) + ret = cls(children={key: ret}) return ret - access: Set[str] = frozenset() # type: ignore + access: set[str] = frozenset() # type: ignore """Immutable set of names to match access. - + This is often only used for pass through checks and not member restrictions. If this is used for member restrictions, even importing/accessing the value will fail as opposed to :py:attr:`use` which is for when it is used. @@ -207,9 +230,9 @@ def nested_child(path: Sequence[str], child: SandboxMatcher) -> SandboxMatcher: An string containing a single asterisk can be used to match all. """ - use: Set[str] = frozenset() # type: ignore + use: set[str] = frozenset() # type: ignore """Immutable set of names to match use. - + This is best used for member restrictions on functions/classes because the restriction will not apply to referencing/importing the item, just when it is used. @@ -229,23 +252,23 @@ def nested_child(path: Sequence[str], child: SandboxMatcher) -> SandboxMatcher: time. """ - leaf_message: Optional[str] = None + leaf_message: str | None = None """ Override message to use in error/warning. Defaults to a common message. This is only applicable to leafs, so this must only be set when ``match_self`` is ``True`` and this matcher is on ``children`` of a parent. """ - leaf_warning: Optional[Type[Warning]] = None + leaf_warning: type[Warning] | None = None """ If set, issues a warning instead of raising an error. This is only applicable to leafs, so this must only be set when ``match_self`` is ``True`` and this matcher is on ``children`` of a parent. """ - exclude: Set[str] = frozenset() # type: ignore + exclude: set[str] = frozenset() # type: ignore """Immutable set of names to exclude. - + These override anything that may have been matched elsewhere. """ @@ -273,7 +296,7 @@ def __post_init__(self): def access_matcher( self, context: RestrictionContext, *child_path: str, include_use: bool = False - ) -> Optional[SandboxMatcher]: + ) -> SandboxMatcher | None: """Perform a match check and return matcher. Args: @@ -304,10 +327,12 @@ def access_matcher( if not child_matcher: return None matcher = child_matcher + if not context.is_runtime and matcher.only_runtime: return None if not matcher.match_self: return None + return matcher def match_access( @@ -328,7 +353,7 @@ def match_access( is not None ) - def child_matcher(self, *child_path: str) -> Optional[SandboxMatcher]: + def child_matcher(self, *child_path: str) -> SandboxMatcher | None: """Return a child matcher for the given path. Unlike :py:meth:`match_access`, this will match if in py:attr:`use` in @@ -341,7 +366,7 @@ def child_matcher(self, *child_path: str) -> Optional[SandboxMatcher]: Matcher that can be used to check children. """ # We prefer to avoid recursion - matcher: Optional[SandboxMatcher] = self + matcher: SandboxMatcher | None = self only_runtime = self.only_runtime for v in child_path: # Use all if it matches self, access, _or_ use. Use doesn't match @@ -409,7 +434,7 @@ def with_child_unrestricted(self, *child_path: str) -> SandboxMatcher: assert child_path # If there's only one item in path, make sure not in access, use, or # children. Otherwise, just remove from child. - to_replace: Dict[str, Any] = {} + to_replace: dict[str, Any] = {} if len(child_path) == 1: if child_path[0] in self.access: to_replace["access"] = set(self.access) @@ -488,51 +513,9 @@ def with_child_unrestricted(self, *child_path: str) -> SandboxMatcher: # Due to how Pydantic is importing lazily inside of some classes, we choose # to always pass it through "pydantic", - # OpenAI and OpenAI agent modules in workflows we always want to pass - # through and reference the out-of-sandbox forms - "openai", - "agents", } ) -# sys.stdlib_module_names is only available on 3.10+, so we hardcode here. A -# test will fail if this list doesn't match the latest Python version it was -# generated against, spitting out the expected list. This is a string instead -# of a list of strings due to black wanting to format this to one item each -# line in a list. -_stdlib_module_names = ( - "__future__,_abc,_aix_support,_ast,_asyncio,_bisect,_blake2,_bootsubprocess,_bz2,_codecs," - "_codecs_cn,_codecs_hk,_codecs_iso2022,_codecs_jp,_codecs_kr,_codecs_tw,_collections," - "_collections_abc,_compat_pickle,_compression,_contextvars,_crypt,_csv,_ctypes,_curses," - "_curses_panel,_datetime,_dbm,_decimal,_elementtree,_frozen_importlib,_frozen_importlib_external," - "_functools,_gdbm,_hashlib,_heapq,_imp,_io,_json,_locale,_lsprof,_lzma,_markupbase," - "_md5,_msi,_multibytecodec,_multiprocessing,_opcode,_operator,_osx_support,_overlapped," - "_pickle,_posixshmem,_posixsubprocess,_py_abc,_pydecimal,_pyio,_queue,_random,_scproxy," - "_sha1,_sha256,_sha3,_sha512,_signal,_sitebuiltins,_socket,_sqlite3,_sre,_ssl,_stat," - "_statistics,_string,_strptime,_struct,_symtable,_thread,_threading_local,_tkinter," - "_tokenize,_tracemalloc,_typing,_uuid,_warnings,_weakref,_weakrefset,_winapi,_zoneinfo," - "abc,aifc,antigravity,argparse,array,ast,asynchat,asyncio,asyncore,atexit,audioop," - "base64,bdb,binascii,bisect,builtins,bz2,cProfile,calendar,cgi,cgitb,chunk,cmath,cmd," - "code,codecs,codeop,collections,colorsys,compileall,concurrent,configparser,contextlib," - "contextvars,copy,copyreg,crypt,csv,ctypes,curses,dataclasses,datetime,dbm,decimal," - "difflib,dis,distutils,doctest,email,encodings,ensurepip,enum,errno,faulthandler,fcntl," - "filecmp,fileinput,fnmatch,fractions,ftplib,functools,gc,genericpath,getopt,getpass," - "gettext,glob,graphlib,grp,gzip,hashlib,heapq,hmac,html,http,idlelib,imaplib,imghdr," - "imp,importlib,inspect,io,ipaddress,itertools,json,keyword,lib2to3,linecache,locale," - "logging,lzma,mailbox,mailcap,marshal,math,mimetypes,mmap,modulefinder,msilib,msvcrt," - "multiprocessing,netrc,nis,nntplib,nt,ntpath,nturl2path,numbers,opcode,operator,optparse," - "os,ossaudiodev,pathlib,pdb,pickle,pickletools,pipes,pkgutil,platform,plistlib,poplib," - "posix,posixpath,pprint,profile,pstats,pty,pwd,py_compile,pyclbr,pydoc,pydoc_data," - "pyexpat,queue,quopri,random,re,readline,reprlib,resource,rlcompleter,runpy,sched," - "secrets,select,selectors,shelve,shlex,shutil,signal,site,smtpd,smtplib,sndhdr,socket," - "socketserver,spwd,sqlite3,sre_compile,sre_constants,sre_parse,ssl,stat,statistics," - "string,stringprep,struct,subprocess,sunau,symtable,sys,sysconfig,syslog,tabnanny," - "tarfile,telnetlib,tempfile,termios,textwrap,this,threading,time,timeit,tkinter,token," - "tokenize,tomllib,trace,traceback,tracemalloc,tty,turtle,turtledemo,types,typing,unicodedata," - "unittest,urllib,uu,uuid,venv,warnings,wave,weakref,webbrowser,winreg,winsound,wsgiref," - "xdrlib,xml,xmlrpc,zipapp,zipfile,zipimport,zlib,zoneinfo" -) - SandboxRestrictions.passthrough_modules_maximum = ( SandboxRestrictions.passthrough_modules_with_temporal | { @@ -541,7 +524,7 @@ def with_child_unrestricted(self, *child_path: str) -> SandboxMatcher: # manually setting sys.modules["os.path"]) they have certain child # expectations. v - for v in _stdlib_module_names.split(",") + for v in sys.stdlib_module_names if v != "sys" } ) @@ -551,8 +534,8 @@ def with_child_unrestricted(self, *child_path: str) -> SandboxMatcher: ) -def _public_callables(parent: Any, *, exclude: Set[str] = set()) -> Set[str]: - ret: Set[str] = set() +def _public_callables(parent: Any, *, exclude: set[str] = set()) -> set[str]: + ret: set[str] = set() for name, member in inspect.getmembers(parent): # Name must be public and callable and not in exclude and not a class if ( @@ -568,11 +551,19 @@ def _public_callables(parent: Any, *, exclude: Set[str] = set()) -> Set[str]: SandboxRestrictions.invalid_module_members_default = SandboxMatcher( children={ "__builtins__": SandboxMatcher( - use={ - "breakpoint", - "input", - "open", + children={ + "breakpoint": SandboxMatcher( + match_self=True, + only_runtime=True, + leaf_message=( + "breakpoint() inside workflow code requires " + "debug_mode=True on the Worker (or the " + "TEMPORAL_DEBUG environment variable). Without it, " + "the call cannot reach the debugger." + ), + ), }, + use={"input", "open"}, # Too many things use open() at import time, e.g. pytest's assertion # rewriter only_runtime=True, @@ -646,11 +637,18 @@ def _public_callables(parent: Any, *, exclude: Set[str] = set()) -> Set[str]: # "linecache": SandboxMatcher.all_uses, # Restrict almost everything in OS at runtime "os": SandboxMatcher( + # As of https://github.com/python/cpython/pull/132662 in python 3.14 we have to allow os.path calls + # which may occur during exception tracing. See https://github.com/python/cpython/issues/140228. + children={ + "path": SandboxMatcher.none + if sys.version_info >= (3, 14) + else SandboxMatcher.all + }, access={"name"}, use={"*"}, # As of https://github.com/python/cpython/pull/112097, os.stat # calls are now made when displaying errors - exclude={"stat"}, + exclude={"stat", "path"} if sys.version_info >= (3, 14) else {"stat"}, # Only restricted at runtime only_runtime=True, ), @@ -744,7 +742,8 @@ def _public_callables(parent: Any, *, exclude: Set[str] = set()) -> Set[str]: "monotonic", "monotonic_ns", "perf_counter", - "perf_counter_ns" "process_time", + "perf_counter_ns", + "process_time", "process_time_ns", "sleep", "time", @@ -770,7 +769,9 @@ def _public_callables(parent: Any, *, exclude: Set[str] = set()) -> Set[str]: "urllib": SandboxMatcher( children={"request": SandboxMatcher.all_uses}, ), - "uuid": SandboxMatcher(use={"uuid1", "uuid4"}, only_runtime=True), + # uuid7 only exists in the stdlib on Python 3.14+; matching a + # nonexistent attribute is harmless on older versions + "uuid": SandboxMatcher(use={"uuid1", "uuid4", "uuid7"}, only_runtime=True), "webbrowser": SandboxMatcher.all_uses, "xmlrpc": SandboxMatcher.all_uses, "zipfile": SandboxMatcher( @@ -811,6 +812,7 @@ def unwrap_if_proxied(v: Any) -> Any: def __init__(self) -> None: """Create a restriction context.""" self.is_runtime = False + self.in_activation = False @dataclass @@ -860,27 +862,33 @@ def set_on_proxy(self, v: _RestrictedProxy) -> None: class _RestrictedProxyLookup: + bind_func: Callable[[_RestrictedProxy, Any], Callable[..., Any]] | None + def __init__( self, - access_func: Optional[Callable] = None, + access_func: Callable | None = None, *, - fallback_func: Optional[Callable] = None, - class_value: Optional[Any] = None, + fallback_func: Callable | None = None, + class_value: Any | None = None, is_attr: bool = False, ) -> None: - bind_func: Optional[Callable[[_RestrictedProxy, Any], Callable]] + bind_func: Callable[[_RestrictedProxy, Any], Callable] | None if hasattr(access_func, "__get__"): # A Python function, can be turned into a bound method. - def bind_func(instance: _RestrictedProxy, obj: Any) -> Callable: + def _bind_func(_instance: _RestrictedProxy, obj: Any) -> Callable: return access_func.__get__(obj, type(obj)) # type: ignore + bind_func = _bind_func + elif access_func is not None: # A C function, use partial to bind the first argument. - def bind_func(instance: _RestrictedProxy, obj: Any) -> Callable: + def _bind_func(_instance: _RestrictedProxy, obj: Any) -> Callable: return functools.partial(access_func, obj) # type: ignore + bind_func = _bind_func + else: # Use getattr, which will produce a bound method. bind_func = None @@ -891,12 +899,12 @@ def bind_func(instance: _RestrictedProxy, obj: Any) -> Callable: self.is_attr = is_attr def __set_name__(self, owner: _RestrictedProxy, name: str) -> None: - self.name = name + self.name = name # type: ignore[reportUninitializedInstanceVariable] - def __get__(self, instance: _RestrictedProxy, owner: Optional[Type] = None) -> Any: - if instance is None: - if self.class_value is not None: - return self.class_value + def __get__(self, instance: _RestrictedProxy, owner: type | None = None) -> Any: + if instance is None: # type: ignore[reportUninitializedInstanceVariable] + if self.class_value is not None: # type: ignore[reportUnreachable] + return self.class_value # type: ignore[reportUnreachable] return self @@ -939,20 +947,20 @@ class _RestrictedProxyIOp(_RestrictedProxyLookup): def __init__( self, - access_func: Optional[Callable] = None, + access_func: Callable | None = None, *, - fallback_func: Optional[Callable] = None, + fallback_func: Callable | None = None, ) -> None: super().__init__(access_func, fallback_func=fallback_func) def bind_f(instance: _RestrictedProxy, obj: Any) -> Callable: def i_op(self: Any, other: Any) -> _RestrictedProxy: - f(self, other) # type: ignore + access_func(self, other) # type: ignore return instance return i_op.__get__(obj, type(obj)) # type: ignore - self.bind_f = bind_f + self.bind_func = bind_f _OpF = TypeVar("_OpF", bound=Callable[..., Any]) @@ -967,7 +975,7 @@ def r_op(obj: Any, other: Any) -> Any: return cast(_OpF, r_op) -_do_not_restrict: Tuple[Type, ...] = (bool, int, float, complex, str, bytes, bytearray) +_do_not_restrict: tuple[type, ...] = (bool, int, float, complex, str, bytes, bytearray) if HAVE_PYDANTIC: # The datetime validator in pydantic_core # https://github.com/pydantic/pydantic-core/blob/741961c05847d9e9ee517cd783e24c2b58e5596b/src/input/input_python.rs#L548-L582 @@ -984,7 +992,7 @@ def _is_restrictable(v: Any) -> bool: class _RestrictedProxy: - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args: Any, **kwargs: Any) -> None: # When we instantiate this class, we have the signature of: # __init__( # self, @@ -1032,7 +1040,7 @@ def __setattr__(self, __name: str, __value: Any) -> None: state.assert_child_not_restricted(__name) setattr(state.obj, __name, __value) - def __call__(self, *args, **kwargs) -> _RestrictedProxy: + def __call__(self, *args: Any, **kwargs: Any) -> _RestrictedProxy: state = _RestrictionState.from_proxy(self) _trace("__call__ on %s", state.name) state.assert_child_not_restricted("__call__") diff --git a/temporalio/worker/workflow_sandbox/_runner.py b/temporalio/worker/workflow_sandbox/_runner.py index c656e3041..7f06bfcd6 100644 --- a/temporalio/worker/workflow_sandbox/_runner.py +++ b/temporalio/worker/workflow_sandbox/_runner.py @@ -7,16 +7,18 @@ from __future__ import annotations import threading +from collections.abc import Sequence from dataclasses import dataclass, field from datetime import datetime, timedelta, timezone -from typing import Any, Optional, Sequence, Type +from typing import Any import temporalio.bridge.proto.workflow_activation import temporalio.bridge.proto.workflow_completion import temporalio.common import temporalio.converter -import temporalio.worker._workflow_instance import temporalio.workflow +from temporalio.converter._extstore import StorageDriverStoreContext +from temporalio.worker import _command_aware_visitor from ...api.common.v1.message_pb2 import Payloads from ...api.failure.v1.message_pb2 import Failure @@ -64,7 +66,7 @@ class SandboxedWorkflowRunner(WorkflowRunner): restrictions: SandboxRestrictions = SandboxRestrictions.default """Set of restrictions to apply to this sandbox""" - runner_class: Type[WorkflowRunner] = UnsandboxedWorkflowRunner + runner_class: type[WorkflowRunner] = UnsandboxedWorkflowRunner """The class for underlying runner the sandbox will instantiate and use to run workflows. Note, this class is re-imported and instantiated for *each* workflow run.""" @@ -77,7 +79,7 @@ def prepare_workflow(self, defn: temporalio.workflow._Definition) -> None: # Just create with fake info which validates self.create_instance( WorkflowInstanceDetails( - payload_converter_class=temporalio.converter.DataConverter.default.payload_converter_class, + payload_converter_factory=temporalio.converter.DataConverter.default._new_payload_converter, failure_converter_class=temporalio.converter.DataConverter.default.failure_converter_class, interceptor_classes=[], defn=defn, @@ -87,6 +89,7 @@ def prepare_workflow(self, defn: temporalio.workflow._Definition) -> None: extern_functions={}, disable_eager_activity_execution=False, worker_level_failure_exception_types=self._worker_level_failure_exception_types, + patch_activation_callback=None, last_completion_result=Payloads(), last_failure=Failure(), ), @@ -109,14 +112,14 @@ class _Instance(WorkflowInstance): def __init__( self, instance_details: WorkflowInstanceDetails, - runner_class: Type[WorkflowRunner], + runner_class: type[WorkflowRunner], restrictions: SandboxRestrictions, ) -> None: self.instance_details = instance_details self.runner_class = runner_class self.importer = Importer(restrictions, RestrictionContext()) - self._current_thread_id: Optional[int] = None + self._current_thread_id: int | None = None # Create the instance self.globals_and_locals = { @@ -159,6 +162,7 @@ def activate( self, act: temporalio.bridge.proto.workflow_activation.WorkflowActivation ) -> temporalio.bridge.proto.workflow_completion.WorkflowActivationCompletion: self.importer.restriction_context.is_runtime = True + self.importer.restriction_context.in_activation = True try: self._run_code( "with __temporal_importer.applied():\n" @@ -169,6 +173,7 @@ def activate( return self.globals_and_locals.pop("__temporal_completion") # type: ignore finally: self.importer.restriction_context.is_runtime = False + self.importer.restriction_context.in_activation = False def _run_code(self, code: str, **extra_globals: Any) -> None: for k, v in extra_globals.items(): @@ -183,5 +188,44 @@ def _run_code(self, code: str, **extra_globals: Any) -> None: for k, v in extra_globals.items(): self.globals_and_locals.pop(k, None) - def get_thread_id(self) -> Optional[int]: + def get_info(self) -> temporalio.workflow.Info: + return self.instance_details.info + + def get_thread_id(self) -> int | None: return self._current_thread_id + + def get_serialization_context( + self, + command_info: _command_aware_visitor.CommandInfo | None, + ) -> temporalio.converter.SerializationContext | None: + # Forward call to the sandboxed instance + self.importer.restriction_context.is_runtime = True + try: + self._run_code( + "with __temporal_importer.applied():\n" + " __temporal_context = __temporal_in_sandbox.get_serialization_context(__temporal_command_info)\n", + __temporal_importer=self.importer, + __temporal_command_info=command_info, + ) + return self.globals_and_locals.pop("__temporal_context", None) # type: ignore + finally: + self.importer.restriction_context.is_runtime = False + + def get_external_store_context( + self, + command_info: _command_aware_visitor.CommandInfo | None, + ) -> StorageDriverStoreContext: + # Forward call to the sandboxed instance + self.importer.restriction_context.is_runtime = True + try: + self._run_code( + "with __temporal_importer.applied():\n" + " __temporal_context = __temporal_in_sandbox.get_external_store_context(__temporal_command_info)\n", + __temporal_importer=self.importer, + __temporal_command_info=command_info, + ) + return self.globals_and_locals.pop( + "__temporal_context", StorageDriverStoreContext(target=None) + ) # type: ignore + finally: + self.importer.restriction_context.is_runtime = False diff --git a/temporalio/workflow.py b/temporalio/workflow.py deleted file mode 100644 index 98b45e367..000000000 --- a/temporalio/workflow.py +++ /dev/null @@ -1,5614 +0,0 @@ -"""Utilities that can decorate or be called inside workflows.""" - -from __future__ import annotations - -import asyncio -import contextvars -import inspect -import logging -import threading -import uuid -import warnings -from abc import ABC, abstractmethod -from contextlib import contextmanager -from dataclasses import dataclass -from datetime import datetime, timedelta, timezone -from enum import Enum, IntEnum -from functools import partial -from random import Random -from typing import ( - TYPE_CHECKING, - Any, - Awaitable, - Callable, - Dict, - Generator, - Generic, - Iterable, - Iterator, - List, - Mapping, - MutableMapping, - NoReturn, - Optional, - Sequence, - Tuple, - Type, - TypeVar, - Union, - cast, - overload, -) - -import nexusrpc -import nexusrpc.handler -from nexusrpc import InputT, OutputT -from typing_extensions import ( - Concatenate, - Literal, - Protocol, - TypedDict, - runtime_checkable, -) - -import temporalio.api.common.v1 -import temporalio.bridge.proto.child_workflow -import temporalio.bridge.proto.workflow_commands -import temporalio.common -import temporalio.converter -import temporalio.exceptions -import temporalio.nexus -import temporalio.workflow -from temporalio.nexus._util import ServiceHandlerT - -from .api.failure.v1.message_pb2 import Failure -from .types import ( - AnyType, - CallableAsyncNoParam, - CallableAsyncSingleParam, - CallableAsyncType, - CallableSyncNoParam, - CallableSyncOrAsyncReturnNoneType, - CallableSyncOrAsyncType, - CallableSyncSingleParam, - CallableType, - ClassType, - MethodAsyncNoParam, - MethodAsyncSingleParam, - MethodSyncNoParam, - MethodSyncOrAsyncNoParam, - MethodSyncOrAsyncSingleParam, - MethodSyncSingleParam, - MultiParamSpec, - ParamType, - ProtocolReturnType, - ReturnType, - SelfType, -) - - -@overload -def defn(cls: ClassType) -> ClassType: ... - - -@overload -def defn( - *, - name: Optional[str] = None, - sandboxed: bool = True, - failure_exception_types: Sequence[Type[BaseException]] = [], - versioning_behavior: temporalio.common.VersioningBehavior = temporalio.common.VersioningBehavior.UNSPECIFIED, -) -> Callable[[ClassType], ClassType]: ... - - -@overload -def defn( - *, - sandboxed: bool = True, - dynamic: bool = False, - versioning_behavior: temporalio.common.VersioningBehavior = temporalio.common.VersioningBehavior.UNSPECIFIED, -) -> Callable[[ClassType], ClassType]: ... - - -def defn( - cls: Optional[ClassType] = None, - *, - name: Optional[str] = None, - sandboxed: bool = True, - dynamic: bool = False, - failure_exception_types: Sequence[Type[BaseException]] = [], - versioning_behavior: temporalio.common.VersioningBehavior = temporalio.common.VersioningBehavior.UNSPECIFIED, -): - """Decorator for workflow classes. - - This must be set on any registered workflow class (it is ignored if on a - base class). - - Args: - cls: The class to decorate. - name: Name to use for the workflow. Defaults to class ``__name__``. This - cannot be set if dynamic is set. - sandboxed: Whether the workflow should run in a sandbox. Default is - true. - dynamic: If true, this activity will be dynamic. Dynamic workflows have - to accept a single 'Sequence[RawValue]' parameter. This cannot be - set to true if name is present. - failure_exception_types: The types of exceptions that, if a - workflow-thrown exception extends, will cause the workflow/update to - fail instead of suspending the workflow via task failure. These are - applied in addition to ones set on the worker constructor. If - ``Exception`` is set, it effectively will fail a workflow/update in - all user exception cases. WARNING: This setting is experimental. - versioning_behavior: Specifies the versioning behavior to use for this workflow. - WARNING: This setting is experimental. - """ - - def decorator(cls: ClassType) -> ClassType: - # This performs validation - _Definition._apply_to_class( - cls, - workflow_name=name or cls.__name__ if not dynamic else None, - sandboxed=sandboxed, - failure_exception_types=failure_exception_types, - versioning_behavior=versioning_behavior, - ) - return cls - - if cls is not None: - return decorator(cls) - return decorator - - -def init( - init_fn: CallableType, -) -> CallableType: - """Decorator for the workflow init method. - - This may be used on the __init__ method of the workflow class to specify - that it accepts the same workflow input arguments as the ``@workflow.run`` - method. If used, the parameters of your __init__ and ``@workflow.run`` - methods must be identical. - - Args: - init_fn: The __init__ method to decorate. - """ - if init_fn.__name__ != "__init__": - raise ValueError("@workflow.init may only be used on the __init__ method") - - setattr(init_fn, "__temporal_workflow_init", True) - return init_fn - - -def run(fn: CallableAsyncType) -> CallableAsyncType: - """Decorator for the workflow run method. - - This must be used on one and only one async method defined on the same class - as ``@workflow.defn``. This can be defined on a base class method but must - then be explicitly overridden and defined on the workflow class. - - Run methods can only have positional parameters. Best practice is to only - take a single object/dataclass argument that can accept more fields later if - needed. - - Args: - fn: The function to decorate. - """ - if not inspect.iscoroutinefunction(fn): - raise ValueError("Workflow run method must be an async function") - # Disallow local classes because we need to have the class globally - # referenceable by name - if "" in fn.__qualname__: - raise ValueError( - "Local classes unsupported, @workflow.run cannot be on a local class" - ) - setattr(fn, "__temporal_workflow_run", True) - # TODO(cretz): Why is MyPy unhappy with this return? - return fn # type: ignore[return-value] - - -class HandlerUnfinishedPolicy(Enum): - """Actions taken if a workflow terminates with running handlers. - - Policy defining actions taken when a workflow exits while update or signal handlers are running. - The workflow exit may be due to successful return, failure, cancellation, or continue-as-new. - """ - - WARN_AND_ABANDON = 1 - """Issue a warning in addition to abandoning.""" - ABANDON = 2 - """Abandon the handler. - - In the case of an update handler this means that the client will receive an error rather than - the update result.""" - - -class UnfinishedUpdateHandlersWarning(RuntimeWarning): - """The workflow exited before all update handlers had finished executing.""" - - -class UnfinishedSignalHandlersWarning(RuntimeWarning): - """The workflow exited before all signal handlers had finished executing.""" - - -@overload -def signal( - fn: CallableSyncOrAsyncReturnNoneType, -) -> CallableSyncOrAsyncReturnNoneType: ... - - -@overload -def signal( - *, - unfinished_policy: HandlerUnfinishedPolicy = HandlerUnfinishedPolicy.WARN_AND_ABANDON, - description: Optional[str] = None, -) -> Callable[ - [CallableSyncOrAsyncReturnNoneType], CallableSyncOrAsyncReturnNoneType -]: ... - - -@overload -def signal( - *, - name: str, - unfinished_policy: HandlerUnfinishedPolicy = HandlerUnfinishedPolicy.WARN_AND_ABANDON, - description: Optional[str] = None, -) -> Callable[ - [CallableSyncOrAsyncReturnNoneType], CallableSyncOrAsyncReturnNoneType -]: ... - - -@overload -def signal( - *, - dynamic: Literal[True], - unfinished_policy: HandlerUnfinishedPolicy = HandlerUnfinishedPolicy.WARN_AND_ABANDON, - description: Optional[str] = None, -) -> Callable[ - [CallableSyncOrAsyncReturnNoneType], CallableSyncOrAsyncReturnNoneType -]: ... - - -def signal( - fn: Optional[CallableSyncOrAsyncReturnNoneType] = None, - *, - name: Optional[str] = None, - dynamic: Optional[bool] = False, - unfinished_policy: HandlerUnfinishedPolicy = HandlerUnfinishedPolicy.WARN_AND_ABANDON, - description: Optional[str] = None, -): - """Decorator for a workflow signal method. - - This is used on any async or non-async method that you wish to be called upon - receiving a signal. If a function overrides one with this decorator, it too - must be decorated. - - Signal methods can only have positional parameters. Best practice for - non-dynamic signal methods is to only take a single object/dataclass - argument that can accept more fields later if needed. Return values from - signal methods are ignored. - - Args: - fn: The function to decorate. - name: Signal name. Defaults to method ``__name__``. Cannot be present - when ``dynamic`` is present. - dynamic: If true, this handles all signals not otherwise handled. The - parameters of the method must be self, a string name, and a - ``*args`` positional varargs. Cannot be present when ``name`` is - present. - unfinished_policy: Actions taken if a workflow terminates with - a running instance of this handler. - description: A short description of the signal that may appear in the UI/CLI. - """ - - def decorator( - name: Optional[str], - unfinished_policy: HandlerUnfinishedPolicy, - fn: CallableSyncOrAsyncReturnNoneType, - ) -> CallableSyncOrAsyncReturnNoneType: - if not name and not dynamic: - name = fn.__name__ - defn = _SignalDefinition( - name=name, - fn=fn, - is_method=True, - unfinished_policy=unfinished_policy, - description=description, - ) - setattr(fn, "__temporal_signal_definition", defn) - if defn.dynamic_vararg: - warnings.warn( - "Dynamic signals with vararg third param is deprecated, use Sequence[RawValue]", - DeprecationWarning, - stacklevel=2, - ) - return fn - - if not fn: - if name is not None and dynamic: - raise RuntimeError("Cannot provide name and dynamic boolean") - return partial(decorator, name, unfinished_policy) - else: - return decorator(fn.__name__, unfinished_policy, fn) - - -@overload -def query(fn: CallableType) -> CallableType: ... - - -@overload -def query( - *, name: str, description: Optional[str] = None -) -> Callable[[CallableType], CallableType]: ... - - -@overload -def query( - *, dynamic: Literal[True], description: Optional[str] = None -) -> Callable[[CallableType], CallableType]: ... - - -@overload -def query(*, description: str) -> Callable[[CallableType], CallableType]: ... - - -def query( - fn: Optional[CallableType] = None, - *, - name: Optional[str] = None, - dynamic: Optional[bool] = False, - description: Optional[str] = None, -): - """Decorator for a workflow query method. - - This is used on any non-async method that expects to handle a query. If a - function overrides one with this decorator, it too must be decorated. - - Query methods can only have positional parameters. Best practice for - non-dynamic query methods is to only take a single object/dataclass - argument that can accept more fields later if needed. The return value is - the resulting query value. Query methods must not mutate any workflow state. - - Args: - fn: The function to decorate. - name: Query name. Defaults to method ``__name__``. Cannot be present - when ``dynamic`` is present. - dynamic: If true, this handles all queries not otherwise handled. The - parameters of the method should be self, a string name, and a - ``Sequence[RawValue]``. An older form of this accepted vararg - parameters which will now warn. Cannot be present when ``name`` is - present. - description: A short description of the query that may appear in the UI/CLI. - """ - - def decorator( - name: Optional[str], - description: Optional[str], - fn: CallableType, - *, - bypass_async_check: bool = False, - ) -> CallableType: - if not name and not dynamic: - name = fn.__name__ - if not bypass_async_check and inspect.iscoroutinefunction(fn): - warnings.warn( - "Queries as async def functions are deprecated", - DeprecationWarning, - stacklevel=2, - ) - defn = _QueryDefinition( - name=name, fn=fn, is_method=True, description=description - ) - setattr(fn, "__temporal_query_definition", defn) - if defn.dynamic_vararg: - warnings.warn( - "Dynamic queries with vararg third param is deprecated, use Sequence[RawValue]", - DeprecationWarning, - stacklevel=2, - ) - return fn - - if name is not None or dynamic or description: - if name is not None and dynamic: - raise RuntimeError("Cannot provide name and dynamic boolean") - return partial(decorator, name, description) - if fn is None: - raise RuntimeError("Cannot create query without function or name or dynamic") - if inspect.iscoroutinefunction(fn): - warnings.warn( - "Queries as async def functions are deprecated", - DeprecationWarning, - stacklevel=2, - ) - return decorator(fn.__name__, description, fn, bypass_async_check=True) - - -@dataclass(frozen=True) -class DynamicWorkflowConfig: - """Returned by functions using the :py:func:`dynamic_config` decorator, see it for more.""" - - failure_exception_types: Optional[Sequence[Type[BaseException]]] = None - """The types of exceptions that, if a workflow-thrown exception extends, will cause the - workflow/update to fail instead of suspending the workflow via task failure. These are applied - in addition to ones set on the worker constructor. If ``Exception`` is set, it effectively will - fail a workflow/update in all user exception cases. - - Always overrides the equivalent parameter on :py:func:`defn` if set not-None. - - WARNING: This setting is experimental. - """ - versioning_behavior: temporalio.common.VersioningBehavior = ( - temporalio.common.VersioningBehavior.UNSPECIFIED - ) - """Specifies the versioning behavior to use for this workflow. - - Always overrides the equivalent parameter on :py:func:`defn`. - - WARNING: This setting is experimental. - """ - - -def dynamic_config( - fn: MethodSyncNoParam[SelfType, DynamicWorkflowConfig], -) -> MethodSyncNoParam[SelfType, DynamicWorkflowConfig]: - """Decorator to allow configuring a dynamic workflow's behavior. - - Because dynamic workflows may conceptually represent more than one workflow type, it may be - desirable to have different settings for fields that would normally be passed to - :py:func:`defn`, but vary based on the workflow type name or other information available in - the workflow's context. This function will be called after the workflow's :py:func:`init`, - if it has one, but before the workflow's :py:func:`run` method. - - The method must only take self as a parameter, and any values set in the class it returns will - override those provided to :py:func:`defn`. - - Cannot be specified on non-dynamic workflows. - - Args: - fn: The function to decorate. - """ - if inspect.iscoroutinefunction(fn): - raise ValueError("Workflow dynamic_config method must be synchronous") - params = list(inspect.signature(fn).parameters.values()) - if len(params) != 1: - raise ValueError("Workflow dynamic_config method must only take self parameter") - - # Add marker attribute - setattr(fn, "__temporal_workflow_dynamic_config", True) - return fn - - -@dataclass(frozen=True) -class Info: - """Information about the running workflow. - - Retrieved inside a workflow via :py:func:`info`. This object is immutable - with the exception of the :py:attr:`search_attributes` and - :py:attr:`typed_search_attributes` which is updated on - :py:func:`upsert_search_attributes`. - - Note, required fields may be added here in future versions. This class - should never be constructed by users. - """ - - attempt: int - continued_run_id: Optional[str] - cron_schedule: Optional[str] - execution_timeout: Optional[timedelta] - first_execution_run_id: str - headers: Mapping[str, temporalio.api.common.v1.Payload] - namespace: str - parent: Optional[ParentInfo] - root: Optional[RootInfo] - priority: temporalio.common.Priority - """The priority of this workflow execution. If not set, or this server predates priorities, - then returns a default instance.""" - raw_memo: Mapping[str, temporalio.api.common.v1.Payload] - retry_policy: Optional[temporalio.common.RetryPolicy] - run_id: str - run_timeout: Optional[timedelta] - - search_attributes: temporalio.common.SearchAttributes - """Search attributes for the workflow. - - .. deprecated:: - Use :py:attr:`typed_search_attributes` instead. - """ - - start_time: datetime - """The start time of the first task executed by the workflow.""" - - task_queue: str - task_timeout: timedelta - - typed_search_attributes: temporalio.common.TypedSearchAttributes - """Search attributes for the workflow. - - Note, this may have invalid values or be missing values if passing the - deprecated form of dictionary attributes to - :py:meth:`upsert_search_attributes`. - """ - - workflow_id: str - - workflow_start_time: datetime - """The start time of the workflow based on the workflow initialization.""" - - workflow_type: str - - def _logger_details(self) -> Mapping[str, Any]: - return { - # TODO(cretz): worker ID? - "attempt": self.attempt, - "namespace": self.namespace, - "run_id": self.run_id, - "task_queue": self.task_queue, - "workflow_id": self.workflow_id, - "workflow_type": self.workflow_type, - } - - def get_current_build_id(self) -> str: - """Get the Build ID of the worker which executed the current Workflow Task. - - May be undefined if the task was completed by a worker without a Build ID. If this worker is - the one executing this task for the first time and has a Build ID set, then its ID will be - used. This value may change over the lifetime of the workflow run, but is deterministic and - safe to use for branching. - - .. deprecated:: - Use get_current_deployment_version instead. - """ - return _Runtime.current().workflow_get_current_build_id() - - def get_current_deployment_version( - self, - ) -> Optional[temporalio.common.WorkerDeploymentVersion]: - """Get the deployment version of the worker which executed the current Workflow Task. - - May be None if the task was completed by a worker without a deployment version or build - id. If this worker is the one executing this task for the first time and has a deployment - version set, then its ID will be used. This value may change over the lifetime of the - workflow run, but is deterministic and safe to use for branching. - """ - return _Runtime.current().workflow_get_current_deployment_version() - - def get_current_history_length(self) -> int: - """Get the current number of events in history. - - Note, this value may not be up to date if accessed inside a query. - - Returns: - Current number of events in history (up until the current task). - """ - return _Runtime.current().workflow_get_current_history_length() - - def get_current_history_size(self) -> int: - """Get the current byte size of history. - - Note, this value may not be up to date if accessed inside a query. - - Returns: - Current byte-size of history (up until the current task). - """ - return _Runtime.current().workflow_get_current_history_size() - - def is_continue_as_new_suggested(self) -> bool: - """Get whether or not continue as new is suggested. - - Note, this value may not be up to date if accessed inside a query. - - Returns: - True if the server is configured to suggest continue as new and it - is suggested. - """ - return _Runtime.current().workflow_is_continue_as_new_suggested() - - -@dataclass(frozen=True) -class ParentInfo: - """Information about the parent workflow.""" - - namespace: str - run_id: str - workflow_id: str - - -@dataclass(frozen=True) -class RootInfo: - """Information about the root workflow.""" - - run_id: str - workflow_id: str - - -@dataclass(frozen=True) -class UpdateInfo: - """Information about a workflow update.""" - - id: str - """Update ID.""" - - name: str - """Update type name.""" - - @property - def _logger_details(self) -> Mapping[str, Any]: - """Data to be included in string appended to default logging output.""" - return { - "update_id": self.id, - "update_name": self.name, - } - - -class _Runtime(ABC): - @staticmethod - def current() -> _Runtime: - loop = _Runtime.maybe_current() - if not loop: - raise _NotInWorkflowEventLoopError("Not in workflow event loop") - return loop - - @staticmethod - def maybe_current() -> Optional[_Runtime]: - try: - return getattr( - asyncio.get_running_loop(), "__temporal_workflow_runtime", None - ) - except RuntimeError: - return None - - @staticmethod - def set_on_loop( - loop: asyncio.AbstractEventLoop, runtime: Optional[_Runtime] - ) -> None: - if runtime: - setattr(loop, "__temporal_workflow_runtime", runtime) - elif hasattr(loop, "__temporal_workflow_runtime"): - delattr(loop, "__temporal_workflow_runtime") - - def __init__(self) -> None: - super().__init__() - self._logger_details: Optional[Mapping[str, Any]] = None - - @property - def logger_details(self) -> Mapping[str, Any]: - if self._logger_details is None: - self._logger_details = self.workflow_info()._logger_details() - return self._logger_details - - @abstractmethod - def workflow_all_handlers_finished(self) -> bool: ... - - @abstractmethod - def workflow_continue_as_new( - self, - *args: Any, - workflow: Union[None, Callable, str], - task_queue: Optional[str], - run_timeout: Optional[timedelta], - task_timeout: Optional[timedelta], - retry_policy: Optional[temporalio.common.RetryPolicy], - memo: Optional[Mapping[str, Any]], - search_attributes: Optional[ - Union[ - temporalio.common.SearchAttributes, - temporalio.common.TypedSearchAttributes, - ] - ], - versioning_intent: Optional[VersioningIntent], - ) -> NoReturn: ... - - @abstractmethod - def workflow_extern_functions(self) -> Mapping[str, Callable]: ... - - @abstractmethod - def workflow_get_current_build_id(self) -> str: ... - - @abstractmethod - def workflow_get_current_deployment_version( - self, - ) -> Optional[temporalio.common.WorkerDeploymentVersion]: ... - - @abstractmethod - def workflow_get_current_history_length(self) -> int: ... - - @abstractmethod - def workflow_get_current_history_size(self) -> int: ... - - @abstractmethod - def workflow_get_external_workflow_handle( - self, id: str, *, run_id: Optional[str] - ) -> ExternalWorkflowHandle[Any]: ... - - @abstractmethod - def workflow_get_query_handler(self, name: Optional[str]) -> Optional[Callable]: ... - - @abstractmethod - def workflow_get_signal_handler( - self, name: Optional[str] - ) -> Optional[Callable]: ... - - @abstractmethod - def workflow_get_update_handler( - self, name: Optional[str] - ) -> Optional[Callable]: ... - - @abstractmethod - def workflow_get_update_validator( - self, name: Optional[str] - ) -> Optional[Callable]: ... - - @abstractmethod - def workflow_info(self) -> Info: ... - - @abstractmethod - def workflow_instance(self) -> Any: ... - - @abstractmethod - def workflow_is_continue_as_new_suggested(self) -> bool: ... - - @abstractmethod - def workflow_is_replaying(self) -> bool: ... - - @abstractmethod - def workflow_memo(self) -> Mapping[str, Any]: ... - - @abstractmethod - def workflow_memo_value( - self, key: str, default: Any, *, type_hint: Optional[Type] - ) -> Any: ... - - @abstractmethod - def workflow_upsert_memo(self, updates: Mapping[str, Any]) -> None: ... - - @abstractmethod - def workflow_metric_meter(self) -> temporalio.common.MetricMeter: ... - - @abstractmethod - def workflow_patch(self, id: str, *, deprecated: bool) -> bool: ... - - @abstractmethod - def workflow_payload_converter(self) -> temporalio.converter.PayloadConverter: ... - - @abstractmethod - def workflow_random(self) -> Random: ... - - @abstractmethod - def workflow_set_query_handler( - self, name: Optional[str], handler: Optional[Callable] - ) -> None: ... - - @abstractmethod - def workflow_set_signal_handler( - self, name: Optional[str], handler: Optional[Callable] - ) -> None: ... - - @abstractmethod - def workflow_set_update_handler( - self, - name: Optional[str], - handler: Optional[Callable], - validator: Optional[Callable], - ) -> None: ... - - @abstractmethod - def workflow_start_activity( - self, - activity: Any, - *args: Any, - task_queue: Optional[str], - result_type: Optional[Type], - schedule_to_close_timeout: Optional[timedelta], - schedule_to_start_timeout: Optional[timedelta], - start_to_close_timeout: Optional[timedelta], - heartbeat_timeout: Optional[timedelta], - retry_policy: Optional[temporalio.common.RetryPolicy], - cancellation_type: ActivityCancellationType, - activity_id: Optional[str], - versioning_intent: Optional[VersioningIntent], - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - ) -> ActivityHandle[Any]: ... - - @abstractmethod - async def workflow_start_child_workflow( - self, - workflow: Any, - *args: Any, - id: str, - task_queue: Optional[str], - result_type: Optional[Type], - cancellation_type: ChildWorkflowCancellationType, - parent_close_policy: ParentClosePolicy, - execution_timeout: Optional[timedelta], - run_timeout: Optional[timedelta], - task_timeout: Optional[timedelta], - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy, - retry_policy: Optional[temporalio.common.RetryPolicy], - cron_schedule: str, - memo: Optional[Mapping[str, Any]], - search_attributes: Optional[ - Union[ - temporalio.common.SearchAttributes, - temporalio.common.TypedSearchAttributes, - ] - ], - versioning_intent: Optional[VersioningIntent], - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - ) -> ChildWorkflowHandle[Any, Any]: ... - - @abstractmethod - def workflow_start_local_activity( - self, - activity: Any, - *args: Any, - result_type: Optional[Type], - schedule_to_close_timeout: Optional[timedelta], - schedule_to_start_timeout: Optional[timedelta], - start_to_close_timeout: Optional[timedelta], - retry_policy: Optional[temporalio.common.RetryPolicy], - local_retry_threshold: Optional[timedelta], - cancellation_type: ActivityCancellationType, - activity_id: Optional[str], - summary: Optional[str], - ) -> ActivityHandle[Any]: ... - - @abstractmethod - async def workflow_start_nexus_operation( - self, - endpoint: str, - service: str, - operation: Union[nexusrpc.Operation[InputT, OutputT], str, Callable[..., Any]], - input: Any, - output_type: Optional[Type[OutputT]], - schedule_to_close_timeout: Optional[timedelta], - cancellation_type: temporalio.workflow.NexusOperationCancellationType, - headers: Optional[Mapping[str, str]], - ) -> NexusOperationHandle[OutputT]: ... - - @abstractmethod - def workflow_time_ns(self) -> int: ... - - @abstractmethod - def workflow_upsert_search_attributes( - self, - attributes: Union[ - temporalio.common.SearchAttributes, - Sequence[temporalio.common.SearchAttributeUpdate], - ], - ) -> None: ... - - @abstractmethod - async def workflow_sleep( - self, duration: float, *, summary: Optional[str] = None - ) -> None: ... - - @abstractmethod - async def workflow_wait_condition( - self, - fn: Callable[[], bool], - *, - timeout: Optional[float] = None, - timeout_summary: Optional[str] = None, - ) -> None: ... - - @abstractmethod - def workflow_get_current_details(self) -> str: ... - - @abstractmethod - def workflow_set_current_details(self, details: str): ... - - @abstractmethod - def workflow_is_failure_exception(self, err: BaseException) -> bool: ... - - @abstractmethod - def workflow_has_last_completion_result(self) -> bool: ... - - @abstractmethod - def workflow_last_completion_result( - self, type_hint: Optional[Type] - ) -> Optional[Any]: ... - - @abstractmethod - def workflow_last_failure(self) -> Optional[BaseException]: ... - - -_current_update_info: contextvars.ContextVar[UpdateInfo] = contextvars.ContextVar( - "__temporal_current_update_info" -) - - -def _set_current_update_info(info: UpdateInfo) -> None: - _current_update_info.set(info) - - -def current_update_info() -> Optional[UpdateInfo]: - """Info for the current update if any. - - This is powered by :py:mod:`contextvars` so it is only valid within the - update handler and coroutines/tasks it has started. - - Returns: - Info for the current update handler the code calling this is executing - within if any. - """ - return _current_update_info.get(None) - - -def deprecate_patch(id: str) -> None: - """Mark a patch as deprecated. - - This marks a workflow that had :py:func:`patched` in a previous version of - the code as no longer applicable because all workflows that use the old code - path are done and will never be queried again. Therefore the old code path - is removed as well. - - Args: - id: The identifier originally used with :py:func:`patched`. - """ - _Runtime.current().workflow_patch(id, deprecated=True) - - -def extern_functions() -> Mapping[str, Callable]: - """External functions available in the workflow sandbox. - - Returns: - Mapping of external functions that can be called from inside a workflow - sandbox. - """ - return _Runtime.current().workflow_extern_functions() - - -def info() -> Info: - """Current workflow's info. - - Returns: - Info for the currently running workflow. - """ - return _Runtime.current().workflow_info() - - -def instance() -> Any: - """Current workflow's instance. - - Returns: - The currently running workflow instance. - """ - return _Runtime.current().workflow_instance() - - -def in_workflow() -> bool: - """Whether the code is currently running in a workflow.""" - return _Runtime.maybe_current() is not None - - -def memo() -> Mapping[str, Any]: - """Current workflow's memo values, converted without type hints. - - Since type hints are not used, the default converted values will come back. - For example, if the memo was originally created with a dataclass, the value - will be a dict. To convert using proper type hints, use - :py:func:`memo_value`. - - Returns: - Mapping of all memo keys and they values without type hints. - """ - return _Runtime.current().workflow_memo() - - -def is_failure_exception(err: BaseException) -> bool: - """Checks if the given exception is a workflow failure in the current workflow. - - Returns: - True if the given exception is a workflow failure in the current workflow. - """ - return _Runtime.current().workflow_is_failure_exception(err) - - -@overload -def memo_value(key: str, default: Any = temporalio.common._arg_unset) -> Any: ... - - -@overload -def memo_value(key: str, *, type_hint: Type[ParamType]) -> ParamType: ... - - -@overload -def memo_value( - key: str, default: AnyType, *, type_hint: Type[ParamType] -) -> Union[AnyType, ParamType]: ... - - -def memo_value( - key: str, - default: Any = temporalio.common._arg_unset, - *, - type_hint: Optional[Type] = None, -) -> Any: - """Memo value for the given key, optional default, and optional type - hint. - - Args: - key: Key to get memo value for. - default: Default to use if key is not present. If unset, a - :py:class:`KeyError` is raised when the key does not exist. - type_hint: Type hint to use when converting. - - Returns: - Memo value, converted with the type hint if present. - - Raises: - KeyError: Key not present and default not set. - """ - return _Runtime.current().workflow_memo_value(key, default, type_hint=type_hint) - - -def upsert_memo(updates: Mapping[str, Any]) -> None: - """Adds, modifies, and/or removes memos, with upsert semantics. - - Every memo that has a matching key has its value replaced with the one specified in ``updates``. - If the value is set to ``None``, the memo is removed instead. - For every key with no existing memo, a new memo is added with specified value (unless the value is ``None``). - Memos with keys not included in ``updates`` remain unchanged. - """ - return _Runtime.current().workflow_upsert_memo(updates) - - -def get_current_details() -> str: - """Get the current details of the workflow which may appear in the UI/CLI. - Unlike static details set at start, this value can be updated throughout - the life of the workflow and is independent of the static details. - This can be in Temporal markdown format and can span multiple lines. - """ - return _Runtime.current().workflow_get_current_details() - - -def has_last_completion_result() -> bool: - """Gets whether there is a last completion result of the workflow.""" - return _Runtime.current().workflow_has_last_completion_result() - - -@overload -def get_last_completion_result() -> Optional[Any]: ... - - -@overload -def get_last_completion_result(type_hint: Type[ParamType]) -> Optional[ParamType]: ... - - -def get_last_completion_result(type_hint: Optional[Type] = None) -> Optional[Any]: - """Get the result of the last run of the workflow. This will be None if there was - no previous completion or the result was None. has_last_completion_result() - can be used to differentiate. - """ - return _Runtime.current().workflow_last_completion_result(type_hint) - - -def get_last_failure() -> Optional[BaseException]: - """Get the last failure of the workflow if it has run previously.""" - return _Runtime.current().workflow_last_failure() - - -def set_current_details(description: str) -> None: - """Set the current details of the workflow which may appear in the UI/CLI. - Unlike static details set at start, this value can be updated throughout - the life of the workflow and is independent of the static details. - This can be in Temporal markdown format and can span multiple lines. - """ - _Runtime.current().workflow_set_current_details(description) - - -def metric_meter() -> temporalio.common.MetricMeter: - """Get the metric meter for the current workflow. - - This meter is replay safe which means that metrics will not be recorded - during replay. - - Returns: - Current metric meter for this workflow for recording metrics. - """ - return _Runtime.current().workflow_metric_meter() - - -def now() -> datetime: - """Current time from the workflow perspective. - - This is the workflow equivalent of :py:func:`datetime.now` with the - :py:attr:`timezone.utc` parameter. - - Returns: - UTC datetime for the current workflow time. The datetime does have UTC - set as the time zone. - """ - return datetime.fromtimestamp(time(), timezone.utc) - - -def patched(id: str) -> bool: - """Patch a workflow. - - When called, this will only return true if code should take the newer path - which means this is either not replaying or is replaying and has seen this - patch before. - - Use :py:func:`deprecate_patch` when all workflows are done and will never be - queried again. The old code path can be used at that time too. - - Args: - id: The identifier for this patch. This identifier may be used - repeatedly in the same workflow to represent the same patch - - Returns: - True if this should take the newer path, false if it should take the - older path. - """ - return _Runtime.current().workflow_patch(id, deprecated=False) - - -def payload_converter() -> temporalio.converter.PayloadConverter: - """Get the payload converter for the current workflow. - - This is often used for dynamic workflows/signals/queries to convert - payloads. - """ - return _Runtime.current().workflow_payload_converter() - - -def random() -> Random: - """Get a deterministic pseudo-random number generator. - - Note, this random number generator is not cryptographically safe and should - not be used for security purposes. - - Returns: - The deterministically-seeded pseudo-random number generator. - """ - return _Runtime.current().workflow_random() - - -def time() -> float: - """Current seconds since the epoch from the workflow perspective. - - This is the workflow equivalent of :py:func:`time.time`. - - Returns: - Seconds since the epoch as a float. - """ - return time_ns() / 1e9 - - -def time_ns() -> int: - """Current nanoseconds since the epoch from the workflow perspective. - - This is the workflow equivalent of :py:func:`time.time_ns`. - - Returns: - Nanoseconds since the epoch - """ - return _Runtime.current().workflow_time_ns() - - -def upsert_search_attributes( - attributes: Union[ - temporalio.common.SearchAttributes, - Sequence[temporalio.common.SearchAttributeUpdate], - ], -) -> None: - """Upsert search attributes for this workflow. - - Args: - attributes: The attributes to set. This should be a sequence of - updates (i.e. values created via value_set and value_unset calls on - search attribute keys). The dictionary form of attributes is - DEPRECATED and if used, result in invalid key types on the - typed_search_attributes property in the info. - """ - if not attributes: - return - temporalio.common._warn_on_deprecated_search_attributes(attributes) - _Runtime.current().workflow_upsert_search_attributes(attributes) - - -# Needs to be defined here to avoid a circular import -@runtime_checkable -class UpdateMethodMultiParam(Protocol[MultiParamSpec, ProtocolReturnType]): - """Decorated workflow update functions implement this.""" - - _defn: temporalio.workflow._UpdateDefinition - - def __call__( - self, *args: MultiParamSpec.args, **kwargs: MultiParamSpec.kwargs - ) -> Union[ProtocolReturnType, Awaitable[ProtocolReturnType]]: - """Generic callable type callback.""" - ... - - def validator( - self, vfunc: Callable[MultiParamSpec, None] - ) -> Callable[MultiParamSpec, None]: - """Use to decorate a function to validate the arguments passed to the update handler.""" - ... - - -@overload -def update( - fn: Callable[MultiParamSpec, Awaitable[ReturnType]], -) -> UpdateMethodMultiParam[MultiParamSpec, ReturnType]: ... - - -@overload -def update( - fn: Callable[MultiParamSpec, ReturnType], -) -> UpdateMethodMultiParam[MultiParamSpec, ReturnType]: ... - - -@overload -def update( - *, - unfinished_policy: HandlerUnfinishedPolicy = HandlerUnfinishedPolicy.WARN_AND_ABANDON, - description: Optional[str] = None, -) -> Callable[ - [Callable[MultiParamSpec, ReturnType]], - UpdateMethodMultiParam[MultiParamSpec, ReturnType], -]: ... - - -@overload -def update( - *, - name: str, - unfinished_policy: HandlerUnfinishedPolicy = HandlerUnfinishedPolicy.WARN_AND_ABANDON, - description: Optional[str] = None, -) -> Callable[ - [Callable[MultiParamSpec, ReturnType]], - UpdateMethodMultiParam[MultiParamSpec, ReturnType], -]: ... - - -@overload -def update( - *, - dynamic: Literal[True], - unfinished_policy: HandlerUnfinishedPolicy = HandlerUnfinishedPolicy.WARN_AND_ABANDON, - description: Optional[str] = None, -) -> Callable[ - [Callable[MultiParamSpec, ReturnType]], - UpdateMethodMultiParam[MultiParamSpec, ReturnType], -]: ... - - -def update( - fn: Optional[CallableSyncOrAsyncType] = None, - *, - name: Optional[str] = None, - dynamic: Optional[bool] = False, - unfinished_policy: HandlerUnfinishedPolicy = HandlerUnfinishedPolicy.WARN_AND_ABANDON, - description: Optional[str] = None, -): - """Decorator for a workflow update handler method. - - This is used on any async or non-async method that you wish to be called upon - receiving an update. If a function overrides one with this decorator, it too - must be decorated. - - You may also optionally define a validator method that will be called before - this handler you have applied this decorator to. You can specify the validator - with ``@update_handler_function_name.validator``. - - Update methods can only have positional parameters. Best practice for - non-dynamic update methods is to only take a single object/dataclass - argument that can accept more fields later if needed. The handler may return - a serializable value which will be sent back to the caller of the update. - - Args: - fn: The function to decorate. - name: Update name. Defaults to method ``__name__``. Cannot be present - when ``dynamic`` is present. - dynamic: If true, this handles all updates not otherwise handled. The - parameters of the method must be self, a string name, and a - ``*args`` positional varargs. Cannot be present when ``name`` is - present. - unfinished_policy: Actions taken if a workflow terminates with - a running instance of this handler. - description: A short description of the update that may appear in the UI/CLI. - """ - - def decorator( - name: Optional[str], - unfinished_policy: HandlerUnfinishedPolicy, - fn: CallableSyncOrAsyncType, - ) -> CallableSyncOrAsyncType: - if not name and not dynamic: - name = fn.__name__ - defn = _UpdateDefinition( - name=name, - fn=fn, - is_method=True, - unfinished_policy=unfinished_policy, - description=description, - ) - if defn.dynamic_vararg: - raise RuntimeError( - "Dynamic updates do not support a vararg third param, use Sequence[RawValue]", - ) - setattr(fn, "_defn", defn) - setattr(fn, "validator", partial(_update_validator, defn)) - return fn - - if not fn: - if name is not None and dynamic: - raise RuntimeError("Cannot provide name and dynamic boolean") - return partial(decorator, name, unfinished_policy) - else: - return decorator(fn.__name__, unfinished_policy, fn) - - -def _update_validator( - update_def: _UpdateDefinition, fn: Optional[Callable[..., None]] = None -) -> Optional[Callable[..., None]]: - """Decorator for a workflow update validator method.""" - if fn is not None: - update_def.set_validator(fn) - return fn - - -def uuid4() -> uuid.UUID: - """Get a new, determinism-safe v4 UUID based on :py:func:`random`. - - Note, this UUID is not cryptographically safe and should not be used for - security purposes. - - Returns: - A deterministically-seeded v4 UUID. - """ - return uuid.UUID(bytes=random().getrandbits(16 * 8).to_bytes(16, "big"), version=4) - - -async def sleep( - duration: Union[float, timedelta], *, summary: Optional[str] = None -) -> None: - """Sleep for the given duration. - - Args: - duration: Duration to sleep in seconds or as a timedelta. - summary: A single-line fixed summary for this timer that may appear in UI/CLI. - This can be in single-line Temporal markdown format. - """ - await _Runtime.current().workflow_sleep( - duration=( - duration.total_seconds() if isinstance(duration, timedelta) else duration - ), - summary=summary, - ) - - -async def wait_condition( - fn: Callable[[], bool], - *, - timeout: Optional[Union[timedelta, float]] = None, - timeout_summary: Optional[str] = None, -) -> None: - """Wait on a callback to become true. - - This function returns when the callback returns true (invoked each loop - iteration) or the timeout has been reached. - - Args: - fn: Non-async callback that accepts no parameters and returns a boolean. - timeout: Optional number of seconds to wait until throwing - :py:class:`asyncio.TimeoutError`. - timeout_summary: Optional simple string identifying the timer (created if ``timeout`` is - present) that may be visible in UI/CLI. While it can be normal text, it is best to treat - as a timer ID. - """ - await _Runtime.current().workflow_wait_condition( - fn, - timeout=timeout.total_seconds() if isinstance(timeout, timedelta) else timeout, - timeout_summary=timeout_summary, - ) - - -_sandbox_unrestricted = threading.local() -_in_sandbox = threading.local() -_imports_passed_through = threading.local() - - -class unsafe: - """Contains static methods that should not normally be called during - workflow execution except in advanced cases. - """ - - def __init__(self) -> None: # noqa: D107 - raise NotImplementedError - - @staticmethod - def in_sandbox() -> bool: - """Whether the code is executing on a sandboxed thread. - - Returns: - True if the code is executing in the sandbox thread. - """ - return getattr(_in_sandbox, "value", False) - - @staticmethod - def _set_in_sandbox(v: bool) -> None: - _in_sandbox.value = v - - @staticmethod - def is_replaying() -> bool: - """Whether the workflow is currently replaying. - - Returns: - True if the workflow is currently replaying - """ - return _Runtime.current().workflow_is_replaying() - - @staticmethod - def is_sandbox_unrestricted() -> bool: - """Whether the current block of code is not restricted via sandbox. - - Returns: - True if the current code is not restricted in the sandbox. - """ - # Activations happen in different threads than init and possibly the - # local hasn't been initialized in _that_ thread, so we allow unset here - # instead of just setting value = False globally. - return getattr(_sandbox_unrestricted, "value", False) - - @staticmethod - @contextmanager - def sandbox_unrestricted() -> Iterator[None]: - """A context manager to run code without sandbox restrictions.""" - # Only apply if not already applied. Nested calls just continue - # unrestricted. - if unsafe.is_sandbox_unrestricted(): - yield None - return - _sandbox_unrestricted.value = True - try: - yield None - finally: - _sandbox_unrestricted.value = False - - @staticmethod - def is_imports_passed_through() -> bool: - """Whether the current block of code is in - :py:meth:imports_passed_through. - - Returns: - True if the current code's imports will be passed through - """ - # See comment in is_sandbox_unrestricted for why we allow unset instead - # of just global false. - return getattr(_imports_passed_through, "value", False) - - @staticmethod - @contextmanager - def imports_passed_through() -> Iterator[None]: - """Context manager to mark all imports that occur within it as passed - through (meaning not reloaded by the sandbox). - """ - # Only apply if not already applied. Nested calls just continue - # passed through. - if unsafe.is_imports_passed_through(): - yield None - return - _imports_passed_through.value = True - try: - yield None - finally: - _imports_passed_through.value = False - - -class LoggerAdapter(logging.LoggerAdapter): - """Adapter that adds details to the log about the running workflow. - - Attributes: - workflow_info_on_message: Boolean for whether a string representation of - a dict of some workflow info will be appended to each message. - Default is True. - workflow_info_on_extra: Boolean for whether a ``temporal_workflow`` - dictionary value will be added to the ``extra`` dictionary with some - workflow info, making it present on the ``LogRecord.__dict__`` for - use by others. Default is True. - full_workflow_info_on_extra: Boolean for whether a ``workflow_info`` - value will be added to the ``extra`` dictionary with the entire - workflow info, making it present on the ``LogRecord.__dict__`` for - use by others. Default is False. - log_during_replay: Boolean for whether logs should occur during replay. - Default is False. - - Values added to ``extra`` are merged with the ``extra`` dictionary from a - logging call, with values from the logging call taking precedence. I.e. the - behavior is that of ``merge_extra=True`` in Python >= 3.13. - """ - - def __init__( - self, logger: logging.Logger, extra: Optional[Mapping[str, Any]] - ) -> None: - """Create the logger adapter.""" - super().__init__(logger, extra or {}) - self.workflow_info_on_message = True - self.workflow_info_on_extra = True - self.full_workflow_info_on_extra = False - self.log_during_replay = False - - def process( - self, msg: Any, kwargs: MutableMapping[str, Any] - ) -> Tuple[Any, MutableMapping[str, Any]]: - """Override to add workflow details.""" - extra: Dict[str, Any] = {} - msg_extra: Dict[str, Any] = {} - - if ( - self.workflow_info_on_message - or self.workflow_info_on_extra - or self.full_workflow_info_on_extra - ): - runtime = _Runtime.maybe_current() - if runtime: - workflow_details = runtime.logger_details - if self.workflow_info_on_message: - msg_extra.update(workflow_details) - if self.workflow_info_on_extra: - extra["temporal_workflow"] = workflow_details - if self.full_workflow_info_on_extra: - extra["workflow_info"] = runtime.workflow_info() - update_info = current_update_info() - if update_info: - update_details = update_info._logger_details - if self.workflow_info_on_message: - msg_extra.update(update_details) - if self.workflow_info_on_extra: - extra.setdefault("temporal_workflow", {}).update(update_details) - - kwargs["extra"] = {**extra, **(kwargs.get("extra") or {})} - if msg_extra: - msg = f"{msg} ({msg_extra})" - return (msg, kwargs) - - def isEnabledFor(self, level: int) -> bool: - """Override to ignore replay logs.""" - if not self.log_during_replay and unsafe.is_replaying(): - return False - return super().isEnabledFor(level) - - @property - def base_logger(self) -> logging.Logger: - """Underlying logger usable for actions such as adding - handlers/formatters. - """ - return self.logger - - -logger = LoggerAdapter(logging.getLogger(__name__), None) -"""Logger that will have contextual workflow details embedded. - -Logs are skipped during replay by default. -""" - - -@dataclass(frozen=True) -class _Definition: - name: Optional[str] - cls: Type - run_fn: Callable[..., Awaitable] - signals: Mapping[Optional[str], _SignalDefinition] - queries: Mapping[Optional[str], _QueryDefinition] - updates: Mapping[Optional[str], _UpdateDefinition] - sandboxed: bool - failure_exception_types: Sequence[Type[BaseException]] - # Types loaded on post init if both are None - arg_types: Optional[List[Type]] = None - ret_type: Optional[Type] = None - versioning_behavior: Optional[temporalio.common.VersioningBehavior] = None - dynamic_config_fn: Optional[Callable[..., DynamicWorkflowConfig]] = None - - @staticmethod - def from_class(cls: Type) -> Optional[_Definition]: - # We make sure to only return it if it's on _this_ class - defn = getattr(cls, "__temporal_workflow_definition", None) - if defn and defn.cls == cls: - return defn - return None - - @staticmethod - def must_from_class(cls: Type) -> _Definition: - ret = _Definition.from_class(cls) - if ret: - return ret - cls_name = getattr(cls, "__name__", "") - raise ValueError( - f"Workflow {cls_name} missing attributes, was it decorated with @workflow.defn?" - ) - - @staticmethod - def from_run_fn(fn: Callable[..., Awaitable[Any]]) -> Optional[_Definition]: - return getattr(fn, "__temporal_workflow_definition", None) - - @staticmethod - def must_from_run_fn(fn: Callable[..., Awaitable[Any]]) -> _Definition: - ret = _Definition.from_run_fn(fn) - if ret: - return ret - fn_name = getattr(fn, "__qualname__", "") - raise ValueError( - f"Function {fn_name} missing attributes, was it decorated with @workflow.run and was its class decorated with @workflow.defn?" - ) - - @classmethod - def get_name_and_result_type( - cls, name_or_run_fn: Union[str, Callable[..., Awaitable[Any]]] - ) -> Tuple[str, Optional[Type]]: - if isinstance(name_or_run_fn, str): - return name_or_run_fn, None - elif callable(name_or_run_fn): - defn = cls.must_from_run_fn(name_or_run_fn) - if not defn.name: - raise ValueError("Cannot invoke dynamic workflow explicitly") - return defn.name, defn.ret_type - else: - raise TypeError("Workflow must be a string or callable") - - @staticmethod - def _apply_to_class( - cls: Type, - *, - workflow_name: Optional[str], - sandboxed: bool, - failure_exception_types: Sequence[Type[BaseException]], - versioning_behavior: temporalio.common.VersioningBehavior, - ) -> None: - # Check it's not being doubly applied - if _Definition.from_class(cls): - raise ValueError("Class already contains workflow definition") - issues: List[str] = [] - - # Collect run fn and all signal/query/update fns - init_fn: Optional[Callable[..., None]] = None - run_fn: Optional[Callable[..., Awaitable[Any]]] = None - dynamic_config_fn: Optional[Callable[..., DynamicWorkflowConfig]] = None - seen_run_attr = False - signals: Dict[Optional[str], _SignalDefinition] = {} - queries: Dict[Optional[str], _QueryDefinition] = {} - updates: Dict[Optional[str], _UpdateDefinition] = {} - for name, member in inspect.getmembers(cls): - if hasattr(member, "__temporal_workflow_run"): - seen_run_attr = True - if not _is_unbound_method_on_cls(member, cls): - issues.append( - f"@workflow.run method {name} must be defined on {cls.__qualname__}" - ) - elif run_fn is not None: - issues.append( - f"Multiple @workflow.run methods found (at least on {name} and {run_fn.__name__})" - ) - else: - # We can guarantee the @workflow.run decorator did - # validation of the function itself - run_fn = member - elif hasattr(member, "__temporal_signal_definition"): - signal_defn = cast( - _SignalDefinition, getattr(member, "__temporal_signal_definition") - ) - if signal_defn.name in signals: - defn_name = signal_defn.name or "" - # TODO(cretz): Remove cast when https://github.com/python/mypy/issues/5485 fixed - other_fn = cast(Callable, signals[signal_defn.name].fn) - issues.append( - f"Multiple signal methods found for {defn_name} " - f"(at least on {name} and {other_fn.__name__})" - ) - else: - signals[signal_defn.name] = signal_defn - elif hasattr(member, "__temporal_query_definition"): - query_defn = cast( - _QueryDefinition, getattr(member, "__temporal_query_definition") - ) - if query_defn.name in queries: - defn_name = query_defn.name or "" - issues.append( - f"Multiple query methods found for {defn_name} " - f"(at least on {name} and {queries[query_defn.name].fn.__name__})" - ) - else: - queries[query_defn.name] = query_defn - elif name == "__init__" and hasattr(member, "__temporal_workflow_init"): - init_fn = member - elif hasattr(member, "__temporal_workflow_dynamic_config"): - if workflow_name: - issues.append( - "@workflow.dynamic_config can only be used in dynamic workflows, but " - f"workflow class {workflow_name} ({cls.__name__}) is not dynamic" - ) - if dynamic_config_fn: - issues.append( - "@workflow.dynamic_config can only be defined once per workflow" - ) - dynamic_config_fn = member - elif isinstance(member, UpdateMethodMultiParam): - update_defn = member._defn - if update_defn.name in updates: - defn_name = update_defn.name or "" - issues.append( - f"Multiple update methods found for {defn_name} " - f"(at least on {name} and {updates[update_defn.name].fn.__name__})" - ) - elif update_defn.validator and not _parameters_identical_up_to_naming( - update_defn.fn, update_defn.validator - ): - issues.append( - f"Update validator method {update_defn.validator.__name__} parameters " - f"do not match update method {update_defn.fn.__name__} parameters" - ) - else: - updates[update_defn.name] = update_defn - - # Check base classes haven't defined things with different decorators - for base_cls in inspect.getmro(cls)[1:]: - for _, base_member in inspect.getmembers(base_cls): - # We only care about methods defined on this class - if not inspect.isfunction(base_member) or not _is_unbound_method_on_cls( - base_member, base_cls - ): - continue - if hasattr(base_member, "__temporal_workflow_run"): - seen_run_attr = True - if not run_fn or base_member.__name__ != run_fn.__name__: - issues.append( - f"@workflow.run defined on {base_member.__qualname__} but not on the override" - ) - elif hasattr(base_member, "__temporal_signal_definition"): - signal_defn = cast( - _SignalDefinition, - getattr(base_member, "__temporal_signal_definition"), - ) - if signal_defn.name not in signals: - issues.append( - f"@workflow.signal defined on {base_member.__qualname__} but not on the override" - ) - elif hasattr(base_member, "__temporal_query_definition"): - query_defn = cast( - _QueryDefinition, - getattr(base_member, "__temporal_query_definition"), - ) - if query_defn.name not in queries: - issues.append( - f"@workflow.query defined on {base_member.__qualname__} but not on the override" - ) - elif isinstance(base_member, UpdateMethodMultiParam): - update_defn = base_member._defn - if update_defn.name not in updates: - issues.append( - f"@workflow.update defined on {base_member.__qualname__} but not on the override" - ) - - if not seen_run_attr: - issues.append("Missing @workflow.run method") - if init_fn and run_fn: - if not _parameters_identical_up_to_naming(init_fn, run_fn): - issues.append( - "@workflow.init and @workflow.run method parameters do not match" - ) - if issues: - if len(issues) == 1: - raise ValueError(f"Invalid workflow class: {issues[0]}") - raise ValueError( - f"Invalid workflow class for {len(issues)} reasons: {', '.join(issues)}" - ) - - assert run_fn - assert seen_run_attr - defn = _Definition( - name=workflow_name, - cls=cls, - run_fn=run_fn, - signals=signals, - queries=queries, - updates=updates, - sandboxed=sandboxed, - failure_exception_types=failure_exception_types, - versioning_behavior=versioning_behavior, - dynamic_config_fn=dynamic_config_fn, - ) - setattr(cls, "__temporal_workflow_definition", defn) - setattr(run_fn, "__temporal_workflow_definition", defn) - - def __post_init__(self) -> None: - if self.arg_types is None and self.ret_type is None: - dynamic = self.name is None - arg_types, ret_type = temporalio.common._type_hints_from_func(self.run_fn) - # If dynamic, must be a sequence of raw values - if dynamic and ( - not arg_types - or len(arg_types) != 1 - or arg_types[0] != Sequence[temporalio.common.RawValue] - ): - raise TypeError( - "Dynamic workflow must accept a single Sequence[temporalio.common.RawValue]" - ) - object.__setattr__(self, "arg_types", arg_types) - object.__setattr__(self, "ret_type", ret_type) - - -def _parameters_identical_up_to_naming(fn1: Callable, fn2: Callable) -> bool: - """Return True if the functions have identical parameter lists, ignoring parameter names.""" - - def params(fn: Callable) -> List[inspect.Parameter]: - # Ignore name when comparing parameters (remaining fields are kind, - # default, and annotation). - return [p.replace(name="x") for p in inspect.signature(fn).parameters.values()] - - # We require that any type annotations present match exactly; i.e. we do - # not support any notion of subtype compatibility. - return params(fn1) == params(fn2) - - -# Async safe version of partial -def _bind_method(obj: Any, fn: Callable[..., Any]) -> Callable[..., Any]: - # Curry instance on the definition function since that represents an - # unbound method - if inspect.iscoroutinefunction(fn): - # We cannot use functools.partial here because in <= 3.7 that isn't - # considered an inspect.iscoroutinefunction - fn = cast(Callable[..., Awaitable[Any]], fn) - - async def with_object(*args, **kwargs) -> Any: - return await fn(obj, *args, **kwargs) - - return with_object - return partial(fn, obj) - - -# Returns true if normal form, false if vararg form -def _assert_dynamic_handler_args( - fn: Callable, arg_types: Optional[List[Type]], is_method: bool -) -> bool: - # Dynamic query/signal/update must have three args: self, name, and - # Sequence[RawValue]. An older form accepted varargs for the third param for signals/queries so - # we will too (but will warn in the signal/query code). - params = list(inspect.signature(fn).parameters.values()) - total_expected_params = 3 if is_method else 2 - if ( - len(params) == total_expected_params - and params[-2].kind is inspect.Parameter.POSITIONAL_OR_KEYWORD - and params[-1].kind is inspect.Parameter.VAR_POSITIONAL - ): - # Old var-arg form - return False - if ( - not arg_types - or len(arg_types) != 2 - or arg_types[0] != str - or arg_types[1] != Sequence[temporalio.common.RawValue] - ): - raise RuntimeError( - "Dynamic handler must have 3 arguments: self, str, and Sequence[temporalio.common.RawValue]" - ) - return True - - -@dataclass(frozen=True) -class _SignalDefinition: - # None if dynamic - name: Optional[str] - fn: Callable[..., Union[None, Awaitable[None]]] - is_method: bool - unfinished_policy: HandlerUnfinishedPolicy = ( - HandlerUnfinishedPolicy.WARN_AND_ABANDON - ) - description: Optional[str] = None - # Types loaded on post init if None - arg_types: Optional[List[Type]] = None - dynamic_vararg: bool = False - - @staticmethod - def from_fn(fn: Callable) -> Optional[_SignalDefinition]: - return getattr(fn, "__temporal_signal_definition", None) - - @staticmethod - def must_name_from_fn_or_str(signal: Union[str, Callable]) -> str: - if callable(signal): - defn = _SignalDefinition.from_fn(signal) - if not defn: - raise RuntimeError( - f"Signal definition not found on {signal.__qualname__}, " - "is it decorated with @workflow.signal?" - ) - elif not defn.name: - raise RuntimeError("Cannot invoke dynamic signal definition") - # TODO(cretz): Check count/type of args at runtime? - return defn.name - return str(signal) - - def __post_init__(self) -> None: - if self.arg_types is None: - arg_types, _ = temporalio.common._type_hints_from_func(self.fn) - # If dynamic, assert it - if not self.name: - object.__setattr__( - self, - "dynamic_vararg", - not _assert_dynamic_handler_args( - self.fn, arg_types, self.is_method - ), - ) - object.__setattr__(self, "arg_types", arg_types) - - def bind_fn(self, obj: Any) -> Callable[..., Any]: - return _bind_method(obj, self.fn) - - -@dataclass(frozen=True) -class _QueryDefinition: - # None if dynamic - name: Optional[str] - fn: Callable[..., Any] - is_method: bool - description: Optional[str] = None - # Types loaded on post init if both are None - arg_types: Optional[List[Type]] = None - ret_type: Optional[Type] = None - dynamic_vararg: bool = False - - @staticmethod - def from_fn(fn: Callable) -> Optional[_QueryDefinition]: - return getattr(fn, "__temporal_query_definition", None) - - def __post_init__(self) -> None: - if self.arg_types is None and self.ret_type is None: - arg_types, ret_type = temporalio.common._type_hints_from_func(self.fn) - # If dynamic, assert it - if not self.name: - object.__setattr__( - self, - "dynamic_vararg", - not _assert_dynamic_handler_args( - self.fn, arg_types, self.is_method - ), - ) - object.__setattr__(self, "arg_types", arg_types) - object.__setattr__(self, "ret_type", ret_type) - - def bind_fn(self, obj: Any) -> Callable[..., Any]: - return _bind_method(obj, self.fn) - - -@dataclass(frozen=True) -class _UpdateDefinition: - # None if dynamic - name: Optional[str] - fn: Callable[..., Union[Any, Awaitable[Any]]] - is_method: bool - unfinished_policy: HandlerUnfinishedPolicy = ( - HandlerUnfinishedPolicy.WARN_AND_ABANDON - ) - description: Optional[str] = None - # Types loaded on post init if None - arg_types: Optional[List[Type]] = None - ret_type: Optional[Type] = None - validator: Optional[Callable[..., None]] = None - dynamic_vararg: bool = False - - def __post_init__(self) -> None: - if self.arg_types is None: - arg_types, ret_type = temporalio.common._type_hints_from_func(self.fn) - # Disallow dynamic varargs - if not self.name and not _assert_dynamic_handler_args( - self.fn, arg_types, self.is_method - ): - raise RuntimeError( - "Dynamic updates do not support a vararg third param, use Sequence[RawValue]", - ) - object.__setattr__(self, "arg_types", arg_types) - object.__setattr__(self, "ret_type", ret_type) - - def bind_fn(self, obj: Any) -> Callable[..., Any]: - return _bind_method(obj, self.fn) - - def bind_validator(self, obj: Any) -> Callable[..., Any]: - if self.validator is not None: - return _bind_method(obj, self.validator) - return lambda *args, **kwargs: None - - def set_validator(self, validator: Callable[..., None]) -> None: - if self.validator: - raise RuntimeError(f"Validator already set for update {self.name}") - object.__setattr__(self, "validator", validator) - - @classmethod - def get_name_and_result_type( - cls, - name_or_update_fn: Union[str, Callable[..., Any]], - ) -> Tuple[str, Optional[Type]]: - if isinstance(name_or_update_fn, temporalio.workflow.UpdateMethodMultiParam): - defn = name_or_update_fn._defn - if not defn.name: - raise RuntimeError("Cannot invoke dynamic update definition") - # TODO(cretz): Check count/type of args at runtime? - return defn.name, defn.ret_type - else: - return str(name_or_update_fn), None - - -# See https://mypy.readthedocs.io/en/latest/runtime_troubles.html#using-classes-that-are-generic-in-stubs-but-not-at-runtime -if TYPE_CHECKING: - - class _AsyncioTask(asyncio.Task[AnyType]): - pass - -else: - # TODO: inherited classes should be other way around? - class _AsyncioTask(Generic[AnyType], asyncio.Task): - pass - - -class ActivityHandle(_AsyncioTask[ReturnType]): # type: ignore[type-var] - """Handle returned from :py:func:`start_activity` and - :py:func:`start_local_activity`. - - This extends :py:class:`asyncio.Task` and supports all task features. - """ - - pass - - -class ActivityCancellationType(IntEnum): - """How an activity cancellation should be handled.""" - - TRY_CANCEL = int( - temporalio.bridge.proto.workflow_commands.ActivityCancellationType.TRY_CANCEL - ) - WAIT_CANCELLATION_COMPLETED = int( - temporalio.bridge.proto.workflow_commands.ActivityCancellationType.WAIT_CANCELLATION_COMPLETED - ) - ABANDON = int( - temporalio.bridge.proto.workflow_commands.ActivityCancellationType.ABANDON - ) - - -class ActivityConfig(TypedDict, total=False): - """TypedDict of config that can be used for :py:func:`start_activity` and - :py:func:`execute_activity`. - """ - - task_queue: Optional[str] - schedule_to_close_timeout: Optional[timedelta] - schedule_to_start_timeout: Optional[timedelta] - start_to_close_timeout: Optional[timedelta] - heartbeat_timeout: Optional[timedelta] - retry_policy: Optional[temporalio.common.RetryPolicy] - cancellation_type: ActivityCancellationType - activity_id: Optional[str] - versioning_intent: Optional[VersioningIntent] - summary: Optional[str] - priority: temporalio.common.Priority - - -# Overload for async no-param activity -@overload -def start_activity( - activity: CallableAsyncNoParam[ReturnType], - *, - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for sync no-param activity -@overload -def start_activity( - activity: CallableSyncNoParam[ReturnType], - *, - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for async single-param activity -@overload -def start_activity( - activity: CallableAsyncSingleParam[ParamType, ReturnType], - arg: ParamType, - *, - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for sync single-param activity -@overload -def start_activity( - activity: CallableSyncSingleParam[ParamType, ReturnType], - arg: ParamType, - *, - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for async multi-param activity -@overload -def start_activity( - activity: Callable[..., Awaitable[ReturnType]], - *, - args: Sequence[Any], - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for sync multi-param activity -@overload -def start_activity( - activity: Callable[..., ReturnType], - *, - args: Sequence[Any], - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for string-name activity -@overload -def start_activity( - activity: str, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - task_queue: Optional[str] = None, - result_type: Optional[Type] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ActivityHandle[Any]: ... - - -def start_activity( - activity: Any, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - task_queue: Optional[str] = None, - result_type: Optional[Type] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ActivityHandle[Any]: - """Start an activity and return its handle. - - At least one of ``schedule_to_close_timeout`` or ``start_to_close_timeout`` - must be present. - - Args: - activity: Activity name or function reference. - arg: Single argument to the activity. - args: Multiple arguments to the activity. Cannot be set if arg is. - task_queue: Task queue to run the activity on. Defaults to the current - workflow's task queue. - result_type: For string activities, this can set the specific result - type hint to deserialize into. - schedule_to_close_timeout: Max amount of time the activity can take from - first being scheduled to being completed before it times out. This - is inclusive of all retries. - schedule_to_start_timeout: Max amount of time the activity can take to - be started from first being scheduled. - start_to_close_timeout: Max amount of time a single activity run can - take from when it starts to when it completes. This is per retry. - heartbeat_timeout: How frequently an activity must invoke heartbeat - while running before it is considered timed out. - retry_policy: How an activity is retried on failure. If unset, a - server-defined default is used. Set maximum attempts to 1 to disable - retries. - cancellation_type: How the activity is treated when it is cancelled from - the workflow. - activity_id: Optional unique identifier for the activity. This is an - advanced setting that should not be set unless users are sure they - need to. Contact Temporal before setting this value. - versioning_intent: When using the Worker Versioning feature, specifies whether this Activity - should run on a worker with a compatible Build Id or not. - Deprecated: Use Worker Deployment versioning instead. - summary: A single-line fixed summary for this activity that may appear in UI/CLI. - This can be in single-line Temporal markdown format. - priority: Priority of the activity. - - Returns: - An activity handle to the activity which is an async task. - """ - return _Runtime.current().workflow_start_activity( - activity, - *temporalio.common._arg_or_args(arg, args), - task_queue=task_queue, - result_type=result_type, - schedule_to_close_timeout=schedule_to_close_timeout, - schedule_to_start_timeout=schedule_to_start_timeout, - start_to_close_timeout=start_to_close_timeout, - heartbeat_timeout=heartbeat_timeout, - retry_policy=retry_policy, - cancellation_type=cancellation_type, - activity_id=activity_id, - versioning_intent=versioning_intent, - summary=summary, - priority=priority, - ) - - -# Overload for async no-param activity -@overload -async def execute_activity( - activity: CallableAsyncNoParam[ReturnType], - *, - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ReturnType: ... - - -# Overload for sync no-param activity -@overload -async def execute_activity( - activity: CallableSyncNoParam[ReturnType], - *, - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ReturnType: ... - - -# Overload for async single-param activity -@overload -async def execute_activity( - activity: CallableAsyncSingleParam[ParamType, ReturnType], - arg: ParamType, - *, - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ReturnType: ... - - -# Overload for sync single-param activity -@overload -async def execute_activity( - activity: CallableSyncSingleParam[ParamType, ReturnType], - arg: ParamType, - *, - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ReturnType: ... - - -# Overload for async multi-param activity -@overload -async def execute_activity( - activity: Callable[..., Awaitable[ReturnType]], - *, - args: Sequence[Any], - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ReturnType: ... - - -# Overload for sync multi-param activity -@overload -async def execute_activity( - activity: Callable[..., ReturnType], - *, - args: Sequence[Any], - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ReturnType: ... - - -# Overload for string-name activity -@overload -async def execute_activity( - activity: str, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - task_queue: Optional[str] = None, - result_type: Optional[Type] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> Any: ... - - -async def execute_activity( - activity: Any, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - task_queue: Optional[str] = None, - result_type: Optional[Type] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> Any: - """Start an activity and wait for completion. - - This is a shortcut for ``await`` :py:meth:`start_activity`. - """ - # We call the runtime directly instead of top-level start_activity to ensure - # we don't miss new parameters - return await _Runtime.current().workflow_start_activity( - activity, - *temporalio.common._arg_or_args(arg, args), - task_queue=task_queue, - result_type=result_type, - schedule_to_close_timeout=schedule_to_close_timeout, - schedule_to_start_timeout=schedule_to_start_timeout, - start_to_close_timeout=start_to_close_timeout, - heartbeat_timeout=heartbeat_timeout, - retry_policy=retry_policy, - cancellation_type=cancellation_type, - activity_id=activity_id, - versioning_intent=versioning_intent, - summary=summary, - priority=priority, - ) - - -# Overload for async no-param activity -@overload -def start_activity_class( - activity: Type[CallableAsyncNoParam[ReturnType]], - *, - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for sync no-param activity -@overload -def start_activity_class( - activity: Type[CallableSyncNoParam[ReturnType]], - *, - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for async single-param activity -@overload -def start_activity_class( - activity: Type[CallableAsyncSingleParam[ParamType, ReturnType]], - arg: ParamType, - *, - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for sync single-param activity -@overload -def start_activity_class( - activity: Type[CallableSyncSingleParam[ParamType, ReturnType]], - arg: ParamType, - *, - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for async multi-param activity -@overload -def start_activity_class( - activity: Type[Callable[..., Awaitable[ReturnType]]], - *, - args: Sequence[Any], - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for sync multi-param activity -@overload -def start_activity_class( - activity: Type[Callable[..., ReturnType]], - *, - args: Sequence[Any], - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ActivityHandle[ReturnType]: ... - - -def start_activity_class( - activity: Type[Callable], - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ActivityHandle[Any]: - """Start an activity from a callable class. - - See :py:meth:`start_activity` for parameter and return details. - """ - return _Runtime.current().workflow_start_activity( - activity, - *temporalio.common._arg_or_args(arg, args), - task_queue=task_queue, - result_type=None, - schedule_to_close_timeout=schedule_to_close_timeout, - schedule_to_start_timeout=schedule_to_start_timeout, - start_to_close_timeout=start_to_close_timeout, - heartbeat_timeout=heartbeat_timeout, - retry_policy=retry_policy, - cancellation_type=cancellation_type, - activity_id=activity_id, - versioning_intent=versioning_intent, - summary=summary, - priority=priority, - ) - - -# Overload for async no-param activity -@overload -async def execute_activity_class( - activity: Type[CallableAsyncNoParam[ReturnType]], - *, - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ReturnType: ... - - -# Overload for sync no-param activity -@overload -async def execute_activity_class( - activity: Type[CallableSyncNoParam[ReturnType]], - *, - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ReturnType: ... - - -# Overload for async single-param activity -@overload -async def execute_activity_class( - activity: Type[CallableAsyncSingleParam[ParamType, ReturnType]], - arg: ParamType, - *, - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ReturnType: ... - - -# Overload for sync single-param activity -@overload -async def execute_activity_class( - activity: Type[CallableSyncSingleParam[ParamType, ReturnType]], - arg: ParamType, - *, - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ReturnType: ... - - -# Overload for async multi-param activity -@overload -async def execute_activity_class( - activity: Type[Callable[..., Awaitable[ReturnType]]], - *, - args: Sequence[Any], - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ReturnType: ... - - -# Overload for sync multi-param activity -@overload -async def execute_activity_class( - activity: Type[Callable[..., ReturnType]], - *, - args: Sequence[Any], - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ReturnType: ... - - -async def execute_activity_class( - activity: Type[Callable], - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> Any: - """Start an activity from a callable class and wait for completion. - - This is a shortcut for ``await`` :py:meth:`start_activity_class`. - """ - return await _Runtime.current().workflow_start_activity( - activity, - *temporalio.common._arg_or_args(arg, args), - task_queue=task_queue, - result_type=None, - schedule_to_close_timeout=schedule_to_close_timeout, - schedule_to_start_timeout=schedule_to_start_timeout, - start_to_close_timeout=start_to_close_timeout, - heartbeat_timeout=heartbeat_timeout, - retry_policy=retry_policy, - cancellation_type=cancellation_type, - activity_id=activity_id, - versioning_intent=versioning_intent, - summary=summary, - priority=priority, - ) - - -# Overload for async no-param activity -@overload -def start_activity_method( - activity: MethodAsyncNoParam[SelfType, ReturnType], - *, - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for sync no-param activity -@overload -def start_activity_method( - activity: MethodSyncNoParam[SelfType, ReturnType], - *, - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for async single-param activity -@overload -def start_activity_method( - activity: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], - arg: ParamType, - *, - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for sync single-param activity -@overload -def start_activity_method( - activity: MethodSyncSingleParam[SelfType, ParamType, ReturnType], - arg: ParamType, - *, - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for async multi-param activity -@overload -def start_activity_method( - activity: Callable[Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType]], - *, - args: Sequence[Any], - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for sync multi-param activity -@overload -def start_activity_method( - activity: Callable[Concatenate[SelfType, MultiParamSpec], ReturnType], - *, - args: Sequence[Any], - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ActivityHandle[ReturnType]: ... - - -def start_activity_method( - activity: Callable, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ActivityHandle[Any]: - """Start an activity from a method. - - See :py:meth:`start_activity` for parameter and return details. - """ - return _Runtime.current().workflow_start_activity( - activity, - *temporalio.common._arg_or_args(arg, args), - task_queue=task_queue, - result_type=None, - schedule_to_close_timeout=schedule_to_close_timeout, - schedule_to_start_timeout=schedule_to_start_timeout, - start_to_close_timeout=start_to_close_timeout, - heartbeat_timeout=heartbeat_timeout, - retry_policy=retry_policy, - cancellation_type=cancellation_type, - activity_id=activity_id, - versioning_intent=versioning_intent, - summary=summary, - priority=priority, - ) - - -# Overload for async no-param activity -@overload -async def execute_activity_method( - activity: MethodAsyncNoParam[SelfType, ReturnType], - *, - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ReturnType: ... - - -# Overload for sync no-param activity -@overload -async def execute_activity_method( - activity: MethodSyncNoParam[SelfType, ReturnType], - *, - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ReturnType: ... - - -# Overload for async single-param activity -@overload -async def execute_activity_method( - activity: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], - arg: ParamType, - *, - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ReturnType: ... - - -# Overload for sync single-param activity -@overload -async def execute_activity_method( - activity: MethodSyncSingleParam[SelfType, ParamType, ReturnType], - arg: ParamType, - *, - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ReturnType: ... - - -# Overload for async multi-param activity -@overload -async def execute_activity_method( - activity: Callable[Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType]], - *, - args: Sequence[Any], - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ReturnType: ... - - -# Overload for sync multi-param activity -@overload -async def execute_activity_method( - activity: Callable[Concatenate[SelfType, MultiParamSpec], ReturnType], - *, - args: Sequence[Any], - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ReturnType: ... - - -async def execute_activity_method( - activity: Callable, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - task_queue: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - heartbeat_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - versioning_intent: Optional[VersioningIntent] = None, - summary: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> Any: - """Start an activity from a method and wait for completion. - - This is a shortcut for ``await`` :py:meth:`start_activity_method`. - """ - # We call the runtime directly instead of top-level start_activity to ensure - # we don't miss new parameters - return await _Runtime.current().workflow_start_activity( - activity, - *temporalio.common._arg_or_args(arg, args), - task_queue=task_queue, - result_type=None, - schedule_to_close_timeout=schedule_to_close_timeout, - schedule_to_start_timeout=schedule_to_start_timeout, - start_to_close_timeout=start_to_close_timeout, - heartbeat_timeout=heartbeat_timeout, - retry_policy=retry_policy, - cancellation_type=cancellation_type, - activity_id=activity_id, - versioning_intent=versioning_intent, - summary=summary, - priority=priority, - ) - - -class LocalActivityConfig(TypedDict, total=False): - """TypedDict of config that can be used for :py:func:`start_local_activity` - and :py:func:`execute_local_activity`. - """ - - schedule_to_close_timeout: Optional[timedelta] - schedule_to_start_timeout: Optional[timedelta] - start_to_close_timeout: Optional[timedelta] - retry_policy: Optional[temporalio.common.RetryPolicy] - local_retry_threshold: Optional[timedelta] - cancellation_type: ActivityCancellationType - activity_id: Optional[str] - summary: Optional[str] - - -# Overload for async no-param activity -@overload -def start_local_activity( - activity: CallableAsyncNoParam[ReturnType], - *, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for sync no-param activity -@overload -def start_local_activity( - activity: CallableSyncNoParam[ReturnType], - *, - activity_id: Optional[str] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - summary: Optional[str] = None, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for async single-param activity -@overload -def start_local_activity( - activity: CallableAsyncSingleParam[ParamType, ReturnType], - arg: ParamType, - *, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for sync single-param activity -@overload -def start_local_activity( - activity: CallableSyncSingleParam[ParamType, ReturnType], - arg: ParamType, - *, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for async multi-param activity -@overload -def start_local_activity( - activity: Callable[..., Awaitable[ReturnType]], - *, - args: Sequence[Any], - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for sync multi-param activity -@overload -def start_local_activity( - activity: Callable[..., ReturnType], - *, - args: Sequence[Any], - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for string-name activity -@overload -def start_local_activity( - activity: str, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - result_type: Optional[Type] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ActivityHandle[Any]: ... - - -def start_local_activity( - activity: Any, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - result_type: Optional[Type] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ActivityHandle[Any]: - """Start a local activity and return its handle. - - At least one of ``schedule_to_close_timeout`` or ``start_to_close_timeout`` - must be present. - - Args: - activity: Activity name or function reference. - arg: Single argument to the activity. - args: Multiple arguments to the activity. Cannot be set if arg is. - result_type: For string activities, this can set the specific result - type hint to deserialize into. - schedule_to_close_timeout: Max amount of time the activity can take from - first being scheduled to being completed before it times out. This - is inclusive of all retries. - schedule_to_start_timeout: Max amount of time the activity can take to - be started from first being scheduled. - start_to_close_timeout: Max amount of time a single activity run can - take from when it starts to when it completes. This is per retry. - retry_policy: How an activity is retried on failure. If unset, an - SDK-defined default is used. Set maximum attempts to 1 to disable - retries. - cancellation_type: How the activity is treated when it is cancelled from - the workflow. - activity_id: Optional unique identifier for the activity. This is an - advanced setting that should not be set unless users are sure they - need to. Contact Temporal before setting this value. - summary: Optional summary for the activity. - - Returns: - An activity handle to the activity which is an async task. - """ - return _Runtime.current().workflow_start_local_activity( - activity, - *temporalio.common._arg_or_args(arg, args), - result_type=result_type, - schedule_to_close_timeout=schedule_to_close_timeout, - schedule_to_start_timeout=schedule_to_start_timeout, - start_to_close_timeout=start_to_close_timeout, - retry_policy=retry_policy, - local_retry_threshold=local_retry_threshold, - cancellation_type=cancellation_type, - activity_id=activity_id, - summary=summary, - ) - - -# Overload for async no-param activity -@overload -async def execute_local_activity( - activity: CallableAsyncNoParam[ReturnType], - *, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ReturnType: ... - - -# Overload for sync no-param activity -@overload -async def execute_local_activity( - activity: CallableSyncNoParam[ReturnType], - *, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ReturnType: ... - - -# Overload for async single-param activity -@overload -async def execute_local_activity( - activity: CallableAsyncSingleParam[ParamType, ReturnType], - arg: ParamType, - *, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ReturnType: ... - - -# Overload for sync single-param activity -@overload -async def execute_local_activity( - activity: CallableSyncSingleParam[ParamType, ReturnType], - arg: ParamType, - *, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ReturnType: ... - - -# Overload for async multi-param activity -@overload -async def execute_local_activity( - activity: Callable[..., Awaitable[ReturnType]], - *, - args: Sequence[Any], - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ReturnType: ... - - -# Overload for sync multi-param activity -@overload -async def execute_local_activity( - activity: Callable[..., ReturnType], - *, - args: Sequence[Any], - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ReturnType: ... - - -# Overload for string-name activity -@overload -async def execute_local_activity( - activity: str, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - result_type: Optional[Type] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> Any: ... - - -async def execute_local_activity( - activity: Any, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - result_type: Optional[Type] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> Any: - """Start a local activity and wait for completion. - - This is a shortcut for ``await`` :py:meth:`start_local_activity`. - """ - # We call the runtime directly instead of top-level start_local_activity to - # ensure we don't miss new parameters - return await _Runtime.current().workflow_start_local_activity( - activity, - *temporalio.common._arg_or_args(arg, args), - result_type=result_type, - schedule_to_close_timeout=schedule_to_close_timeout, - schedule_to_start_timeout=schedule_to_start_timeout, - start_to_close_timeout=start_to_close_timeout, - retry_policy=retry_policy, - local_retry_threshold=local_retry_threshold, - cancellation_type=cancellation_type, - activity_id=activity_id, - summary=summary, - ) - - -# Overload for async no-param activity -@overload -def start_local_activity_class( - activity: Type[CallableAsyncNoParam[ReturnType]], - *, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for sync no-param activity -@overload -def start_local_activity_class( - activity: Type[CallableSyncNoParam[ReturnType]], - *, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for async single-param activity -@overload -def start_local_activity_class( - activity: Type[CallableAsyncSingleParam[ParamType, ReturnType]], - arg: ParamType, - *, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for sync single-param activity -@overload -def start_local_activity_class( - activity: Type[CallableSyncSingleParam[ParamType, ReturnType]], - arg: ParamType, - *, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for async multi-param activity -@overload -def start_local_activity_class( - activity: Type[Callable[..., Awaitable[ReturnType]]], - *, - args: Sequence[Any], - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for sync multi-param activity -@overload -def start_local_activity_class( - activity: Type[Callable[..., ReturnType]], - *, - args: Sequence[Any], - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, -) -> ActivityHandle[ReturnType]: ... - - -def start_local_activity_class( - activity: Type[Callable], - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ActivityHandle[Any]: - """Start a local activity from a callable class. - - See :py:meth:`start_local_activity` for parameter and return details. - """ - return _Runtime.current().workflow_start_local_activity( - activity, - *temporalio.common._arg_or_args(arg, args), - result_type=None, - schedule_to_close_timeout=schedule_to_close_timeout, - schedule_to_start_timeout=schedule_to_start_timeout, - start_to_close_timeout=start_to_close_timeout, - retry_policy=retry_policy, - local_retry_threshold=local_retry_threshold, - cancellation_type=cancellation_type, - activity_id=activity_id, - summary=summary, - ) - - -# Overload for async no-param activity -@overload -async def execute_local_activity_class( - activity: Type[CallableAsyncNoParam[ReturnType]], - *, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ReturnType: ... - - -# Overload for sync no-param activity -@overload -async def execute_local_activity_class( - activity: Type[CallableSyncNoParam[ReturnType]], - *, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ReturnType: ... - - -# Overload for async single-param activity -@overload -async def execute_local_activity_class( - activity: Type[CallableAsyncSingleParam[ParamType, ReturnType]], - arg: ParamType, - *, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ReturnType: ... - - -# Overload for sync single-param activity -@overload -async def execute_local_activity_class( - activity: Type[CallableSyncSingleParam[ParamType, ReturnType]], - arg: ParamType, - *, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ReturnType: ... - - -# Overload for async multi-param activity -@overload -async def execute_local_activity_class( - activity: Type[Callable[..., Awaitable[ReturnType]]], - *, - args: Sequence[Any], - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ReturnType: ... - - -# Overload for sync multi-param activity -@overload -async def execute_local_activity_class( - activity: Type[Callable[..., ReturnType]], - *, - args: Sequence[Any], - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ReturnType: ... - - -async def execute_local_activity_class( - activity: Type[Callable], - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> Any: - """Start a local activity from a callable class and wait for completion. - - This is a shortcut for ``await`` :py:meth:`start_local_activity_class`. - """ - # We call the runtime directly instead of top-level start_local_activity to - # ensure we don't miss new parameters - return await _Runtime.current().workflow_start_local_activity( - activity, - *temporalio.common._arg_or_args(arg, args), - result_type=None, - schedule_to_close_timeout=schedule_to_close_timeout, - schedule_to_start_timeout=schedule_to_start_timeout, - start_to_close_timeout=start_to_close_timeout, - retry_policy=retry_policy, - local_retry_threshold=local_retry_threshold, - cancellation_type=cancellation_type, - activity_id=activity_id, - summary=summary, - ) - - -# Overload for async no-param activity -@overload -def start_local_activity_method( - activity: MethodAsyncNoParam[SelfType, ReturnType], - *, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for sync no-param activity -@overload -def start_local_activity_method( - activity: MethodSyncNoParam[SelfType, ReturnType], - *, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for async single-param activity -@overload -def start_local_activity_method( - activity: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], - arg: ParamType, - *, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for sync single-param activity -@overload -def start_local_activity_method( - activity: MethodSyncSingleParam[SelfType, ParamType, ReturnType], - arg: ParamType, - *, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for async multi-param activity -@overload -def start_local_activity_method( - activity: Callable[Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType]], - *, - args: Sequence[Any], - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ActivityHandle[ReturnType]: ... - - -# Overload for sync multi-param activity -@overload -def start_local_activity_method( - activity: Callable[Concatenate[SelfType, MultiParamSpec], ReturnType], - *, - args: Sequence[Any], - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ActivityHandle[ReturnType]: ... - - -def start_local_activity_method( - activity: Callable, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ActivityHandle[Any]: - """Start a local activity from a method. - - See :py:meth:`start_local_activity` for parameter and return details. - """ - return _Runtime.current().workflow_start_local_activity( - activity, - *temporalio.common._arg_or_args(arg, args), - result_type=None, - schedule_to_close_timeout=schedule_to_close_timeout, - schedule_to_start_timeout=schedule_to_start_timeout, - start_to_close_timeout=start_to_close_timeout, - retry_policy=retry_policy, - local_retry_threshold=local_retry_threshold, - cancellation_type=cancellation_type, - activity_id=activity_id, - summary=summary, - ) - - -# Overload for async no-param activity -@overload -async def execute_local_activity_method( - activity: MethodAsyncNoParam[SelfType, ReturnType], - *, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ReturnType: ... - - -# Overload for sync no-param activity -@overload -async def execute_local_activity_method( - activity: MethodSyncNoParam[SelfType, ReturnType], - *, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ReturnType: ... - - -# Overload for async single-param activity -@overload -async def execute_local_activity_method( - activity: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], - arg: ParamType, - *, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ReturnType: ... - - -# Overload for sync single-param activity -@overload -async def execute_local_activity_method( - activity: MethodSyncSingleParam[SelfType, ParamType, ReturnType], - arg: ParamType, - *, - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ReturnType: ... - - -# Overload for async multi-param activity -@overload -async def execute_local_activity_method( - activity: Callable[Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType]], - *, - args: Sequence[Any], - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ReturnType: ... - - -# Overload for sync multi-param activity -@overload -async def execute_local_activity_method( - activity: Callable[Concatenate[SelfType, MultiParamSpec], ReturnType], - *, - args: Sequence[Any], - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> ReturnType: ... - - -async def execute_local_activity_method( - activity: Callable, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - schedule_to_close_timeout: Optional[timedelta] = None, - schedule_to_start_timeout: Optional[timedelta] = None, - start_to_close_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - local_retry_threshold: Optional[timedelta] = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: Optional[str] = None, - summary: Optional[str] = None, -) -> Any: - """Start a local activity from a method and wait for completion. - - This is a shortcut for ``await`` :py:meth:`start_local_activity_method`. - """ - # We call the runtime directly instead of top-level start_local_activity to - # ensure we don't miss new parameters - return await _Runtime.current().workflow_start_local_activity( - activity, - *temporalio.common._arg_or_args(arg, args), - result_type=None, - schedule_to_close_timeout=schedule_to_close_timeout, - schedule_to_start_timeout=schedule_to_start_timeout, - start_to_close_timeout=start_to_close_timeout, - retry_policy=retry_policy, - local_retry_threshold=local_retry_threshold, - cancellation_type=cancellation_type, - activity_id=activity_id, - summary=summary, - ) - - -class ChildWorkflowHandle(_AsyncioTask[ReturnType], Generic[SelfType, ReturnType]): # type: ignore[type-var] - """Handle for interacting with a child workflow. - - This is created via :py:func:`start_child_workflow`. - - This extends :py:class:`asyncio.Task` and supports all task features. - """ - - @property - def id(self) -> str: - """ID for the workflow.""" - raise NotImplementedError - - @property - def first_execution_run_id(self) -> Optional[str]: - """Run ID for the workflow.""" - raise NotImplementedError - - @overload - async def signal( - self, - signal: MethodSyncOrAsyncNoParam[SelfType, None], - ) -> None: ... - - @overload - async def signal( - self, - signal: MethodSyncOrAsyncSingleParam[SelfType, ParamType, None], - arg: ParamType, - ) -> None: ... - - @overload - async def signal( - self, - signal: Callable[ - Concatenate[SelfType, MultiParamSpec], Union[Awaitable[None], None] - ], - *, - args: Sequence[Any], - ) -> None: ... - - @overload - async def signal( - self, - signal: str, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - ) -> None: ... - - async def signal( - self, - signal: Union[str, Callable], - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - ) -> None: - """Signal this child workflow. - - Args: - signal: Name or method reference for the signal. - arg: Single argument to the signal. - args: Multiple arguments to the signal. Cannot be set if arg is. - - """ - raise NotImplementedError - - -class ChildWorkflowCancellationType(IntEnum): - """How a child workflow cancellation should be handled.""" - - ABANDON = int( - temporalio.bridge.proto.child_workflow.ChildWorkflowCancellationType.ABANDON - ) - TRY_CANCEL = int( - temporalio.bridge.proto.child_workflow.ChildWorkflowCancellationType.TRY_CANCEL - ) - WAIT_CANCELLATION_COMPLETED = int( - temporalio.bridge.proto.child_workflow.ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED - ) - WAIT_CANCELLATION_REQUESTED = int( - temporalio.bridge.proto.child_workflow.ChildWorkflowCancellationType.WAIT_CANCELLATION_REQUESTED - ) - - -class ParentClosePolicy(IntEnum): - """How a child workflow should be handled when the parent closes.""" - - UNSPECIFIED = int( - temporalio.bridge.proto.child_workflow.ParentClosePolicy.PARENT_CLOSE_POLICY_UNSPECIFIED - ) - TERMINATE = int( - temporalio.bridge.proto.child_workflow.ParentClosePolicy.PARENT_CLOSE_POLICY_TERMINATE - ) - ABANDON = int( - temporalio.bridge.proto.child_workflow.ParentClosePolicy.PARENT_CLOSE_POLICY_ABANDON - ) - REQUEST_CANCEL = int( - temporalio.bridge.proto.child_workflow.ParentClosePolicy.PARENT_CLOSE_POLICY_REQUEST_CANCEL - ) - - -class ChildWorkflowConfig(TypedDict, total=False): - """TypedDict of config that can be used for :py:func:`start_child_workflow` - and :py:func:`execute_child_workflow`. - """ - - id: Optional[str] - task_queue: Optional[str] - cancellation_type: ChildWorkflowCancellationType - parent_close_policy: ParentClosePolicy - execution_timeout: Optional[timedelta] - run_timeout: Optional[timedelta] - task_timeout: Optional[timedelta] - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy - retry_policy: Optional[temporalio.common.RetryPolicy] - cron_schedule: str - memo: Optional[Mapping[str, Any]] - search_attributes: Optional[ - Union[ - temporalio.common.SearchAttributes, temporalio.common.TypedSearchAttributes - ] - ] - versioning_intent: Optional[VersioningIntent] - static_summary: Optional[str] - static_details: Optional[str] - priority: temporalio.common.Priority - - -# Overload for no-param workflow -@overload -async def start_child_workflow( - workflow: MethodAsyncNoParam[SelfType, ReturnType], - *, - id: Optional[str] = None, - task_queue: Optional[str] = None, - cancellation_type: ChildWorkflowCancellationType = ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED, - parent_close_policy: ParentClosePolicy = ParentClosePolicy.TERMINATE, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.SearchAttributes, temporalio.common.TypedSearchAttributes - ] - ] = None, - versioning_intent: Optional[VersioningIntent] = None, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ChildWorkflowHandle[SelfType, ReturnType]: ... - - -# Overload for single-param workflow -@overload -async def start_child_workflow( - workflow: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], - arg: ParamType, - *, - id: Optional[str] = None, - task_queue: Optional[str] = None, - cancellation_type: ChildWorkflowCancellationType = ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED, - parent_close_policy: ParentClosePolicy = ParentClosePolicy.TERMINATE, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.SearchAttributes, temporalio.common.TypedSearchAttributes - ] - ] = None, - versioning_intent: Optional[VersioningIntent] = None, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ChildWorkflowHandle[SelfType, ReturnType]: ... - - -# Overload for multi-param workflow -@overload -async def start_child_workflow( - workflow: Callable[Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType]], - *, - args: Sequence[Any], - id: Optional[str] = None, - task_queue: Optional[str] = None, - cancellation_type: ChildWorkflowCancellationType = ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED, - parent_close_policy: ParentClosePolicy = ParentClosePolicy.TERMINATE, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.SearchAttributes, temporalio.common.TypedSearchAttributes - ] - ] = None, - versioning_intent: Optional[VersioningIntent] = None, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ChildWorkflowHandle[SelfType, ReturnType]: ... - - -# Overload for string-name workflow -@overload -async def start_child_workflow( - workflow: str, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: Optional[str] = None, - task_queue: Optional[str] = None, - result_type: Optional[Type] = None, - cancellation_type: ChildWorkflowCancellationType = ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED, - parent_close_policy: ParentClosePolicy = ParentClosePolicy.TERMINATE, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.SearchAttributes, temporalio.common.TypedSearchAttributes - ] - ] = None, - versioning_intent: Optional[VersioningIntent] = None, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ChildWorkflowHandle[Any, Any]: ... - - -async def start_child_workflow( - workflow: Any, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: Optional[str] = None, - task_queue: Optional[str] = None, - result_type: Optional[Type] = None, - cancellation_type: ChildWorkflowCancellationType = ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED, - parent_close_policy: ParentClosePolicy = ParentClosePolicy.TERMINATE, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.SearchAttributes, temporalio.common.TypedSearchAttributes - ] - ] = None, - versioning_intent: Optional[VersioningIntent] = None, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ChildWorkflowHandle[Any, Any]: - """Start a child workflow and return its handle. - - Args: - workflow: String name or class method decorated with ``@workflow.run`` - for the workflow to start. - arg: Single argument to the child workflow. - args: Multiple arguments to the child workflow. Cannot be set if arg is. - id: Optional unique identifier for the workflow execution. If not set, - defaults to :py:func:`uuid4`. - task_queue: Task queue to run the workflow on. Defaults to the current - workflow's task queue. - result_type: For string workflows, this can set the specific result type - hint to deserialize into. - cancellation_type: How the child workflow will react to cancellation. - parent_close_policy: How to handle the child workflow when the parent - workflow closes. - execution_timeout: Total workflow execution timeout including - retries and continue as new. - run_timeout: Timeout of a single workflow run. - task_timeout: Timeout of a single workflow task. - id_reuse_policy: How already-existing IDs are treated. - retry_policy: Retry policy for the workflow. - cron_schedule: See https://docs.temporal.io/docs/content/what-is-a-temporal-cron-job/ - memo: Memo for the workflow. - search_attributes: Search attributes for the workflow. The dictionary - form of this is DEPRECATED. - versioning_intent: When using the Worker Versioning feature, specifies whether this Child - Workflow should run on a worker with a compatible Build Id or not. - Deprecated: Use Worker Deployment versioning instead. - static_summary: A single-line fixed summary for this child workflow execution that may appear - in the UI/CLI. This can be in single-line Temporal markdown format. - static_details: General fixed details for this child workflow execution that may appear in - UI/CLI. This can be in Temporal markdown format and can span multiple lines. This is - a fixed value on the workflow that cannot be updated. For details that can be - updated, use :py:meth:`Workflow.get_current_details` within the workflow. - priority: Priority to use for this workflow. - - Returns: - A workflow handle to the started/existing workflow. - """ - temporalio.common._warn_on_deprecated_search_attributes(search_attributes) - return await _Runtime.current().workflow_start_child_workflow( - workflow, - *temporalio.common._arg_or_args(arg, args), - id=id or str(uuid4()), - task_queue=task_queue, - result_type=result_type, - cancellation_type=cancellation_type, - parent_close_policy=parent_close_policy, - execution_timeout=execution_timeout, - run_timeout=run_timeout, - task_timeout=task_timeout, - id_reuse_policy=id_reuse_policy, - retry_policy=retry_policy, - cron_schedule=cron_schedule, - memo=memo, - search_attributes=search_attributes, - versioning_intent=versioning_intent, - static_summary=static_summary, - static_details=static_details, - priority=priority, - ) - - -# Overload for no-param workflow -@overload -async def execute_child_workflow( - workflow: MethodAsyncNoParam[SelfType, ReturnType], - *, - id: Optional[str] = None, - task_queue: Optional[str] = None, - cancellation_type: ChildWorkflowCancellationType = ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED, - parent_close_policy: ParentClosePolicy = ParentClosePolicy.TERMINATE, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.SearchAttributes, temporalio.common.TypedSearchAttributes - ] - ] = None, - versioning_intent: Optional[VersioningIntent] = None, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ReturnType: ... - - -# Overload for single-param workflow -@overload -async def execute_child_workflow( - workflow: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], - arg: ParamType, - *, - id: Optional[str] = None, - task_queue: Optional[str] = None, - cancellation_type: ChildWorkflowCancellationType = ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED, - parent_close_policy: ParentClosePolicy = ParentClosePolicy.TERMINATE, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.SearchAttributes, temporalio.common.TypedSearchAttributes - ] - ] = None, - versioning_intent: Optional[VersioningIntent] = None, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ReturnType: ... - - -# Overload for multi-param workflow -@overload -async def execute_child_workflow( - workflow: Callable[Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType]], - *, - args: Sequence[Any], - id: Optional[str] = None, - task_queue: Optional[str] = None, - cancellation_type: ChildWorkflowCancellationType = ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED, - parent_close_policy: ParentClosePolicy = ParentClosePolicy.TERMINATE, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.SearchAttributes, temporalio.common.TypedSearchAttributes - ] - ] = None, - versioning_intent: Optional[VersioningIntent] = None, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> ReturnType: ... - - -# Overload for string-name workflow -@overload -async def execute_child_workflow( - workflow: str, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: Optional[str] = None, - task_queue: Optional[str] = None, - result_type: Optional[Type] = None, - cancellation_type: ChildWorkflowCancellationType = ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED, - parent_close_policy: ParentClosePolicy = ParentClosePolicy.TERMINATE, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.SearchAttributes, temporalio.common.TypedSearchAttributes - ] - ] = None, - versioning_intent: Optional[VersioningIntent] = None, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> Any: ... - - -async def execute_child_workflow( - workflow: Any, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: Optional[str] = None, - task_queue: Optional[str] = None, - result_type: Optional[Type] = None, - cancellation_type: ChildWorkflowCancellationType = ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED, - parent_close_policy: ParentClosePolicy = ParentClosePolicy.TERMINATE, - execution_timeout: Optional[timedelta] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - cron_schedule: str = "", - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.SearchAttributes, temporalio.common.TypedSearchAttributes - ] - ] = None, - versioning_intent: Optional[VersioningIntent] = None, - static_summary: Optional[str] = None, - static_details: Optional[str] = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, -) -> Any: - """Start a child workflow and wait for completion. - - This is a shortcut for ``await (await`` :py:meth:`start_child_workflow` ``)``. - """ - temporalio.common._warn_on_deprecated_search_attributes(search_attributes) - # We call the runtime directly instead of top-level start_child_workflow to - # ensure we don't miss new parameters - handle = await _Runtime.current().workflow_start_child_workflow( - workflow, - *temporalio.common._arg_or_args(arg, args), - id=id or str(uuid4()), - task_queue=task_queue, - result_type=result_type, - cancellation_type=cancellation_type, - parent_close_policy=parent_close_policy, - execution_timeout=execution_timeout, - run_timeout=run_timeout, - task_timeout=task_timeout, - id_reuse_policy=id_reuse_policy, - retry_policy=retry_policy, - cron_schedule=cron_schedule, - memo=memo, - search_attributes=search_attributes, - versioning_intent=versioning_intent, - static_summary=static_summary, - static_details=static_details, - priority=priority, - ) - return await handle - - -class NexusOperationHandle(Generic[OutputT]): - """Handle for interacting with a Nexus operation. - - .. warning:: - This API is experimental and unstable. - """ - - # TODO(nexus-preview): should attempts to instantiate directly throw? - - def cancel(self) -> bool: - """Request cancellation of the operation.""" - raise NotImplementedError - - def __await__(self) -> Generator[Any, Any, OutputT]: - """Support await.""" - raise NotImplementedError - - @property - def operation_token(self) -> Optional[str]: - """The operation token for this handle.""" - raise NotImplementedError - - -class ExternalWorkflowHandle(Generic[SelfType]): - """Handle for interacting with an external workflow. - - This is created via :py:func:`get_external_workflow_handle` or - :py:func:`get_external_workflow_handle_for`. - """ - - @property - def id(self) -> str: - """ID for the workflow.""" - raise NotImplementedError - - @property - def run_id(self) -> Optional[str]: - """Run ID for the workflow if any.""" - raise NotImplementedError - - @overload - async def signal( - self, - signal: MethodSyncOrAsyncNoParam[SelfType, None], - ) -> None: ... - - @overload - async def signal( - self, - signal: MethodSyncOrAsyncSingleParam[SelfType, ParamType, None], - arg: ParamType, - ) -> None: ... - - @overload - async def signal( - self, - signal: str, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - ) -> None: ... - - async def signal( - self, - signal: Union[str, Callable], - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - ) -> None: - """Signal this external workflow. - - Args: - signal: Name or method reference for the signal. - arg: Single argument to the signal. - args: Multiple arguments to the signal. Cannot be set if arg is. - - """ - raise NotImplementedError - - async def cancel(self) -> None: - """Send a cancellation request to this external workflow. - - This will fail if the workflow cannot accept the request (e.g. if the - workflow is not found). - """ - raise NotImplementedError - - -def get_external_workflow_handle( - workflow_id: str, - *, - run_id: Optional[str] = None, -) -> ExternalWorkflowHandle[Any]: - """Get a workflow handle to an existing workflow by its ID. - - Args: - workflow_id: Workflow ID to get a handle to. - run_id: Optional run ID for the workflow. - - Returns: - The external workflow handle. - """ - return _Runtime.current().workflow_get_external_workflow_handle( - workflow_id, run_id=run_id - ) - - -def get_external_workflow_handle_for( - workflow: Union[ - MethodAsyncNoParam[SelfType, Any], MethodAsyncSingleParam[SelfType, Any, Any] - ], - workflow_id: str, - *, - run_id: Optional[str] = None, -) -> ExternalWorkflowHandle[SelfType]: - """Get a typed workflow handle to an existing workflow by its ID. - - This is the same as :py:func:`get_external_workflow_handle` but typed. Note, - the workflow type given is not validated, it is only for typing. - - Args: - workflow: The workflow run method to use for typing the handle. - workflow_id: Workflow ID to get a handle to. - run_id: Optional run ID for the workflow. - - Returns: - The external workflow handle. - """ - return get_external_workflow_handle(workflow_id, run_id=run_id) - - -class ContinueAsNewError(BaseException): - """Error thrown by :py:func:`continue_as_new`. - - This should not be caught, but instead be allowed to throw out of the - workflow which then triggers the continue as new. This should never be - instantiated directly. - """ - - def __init__(self, *args: object) -> None: - """Direct instantiation is disabled. Use :py:func:`continue_as_new`.""" - if type(self) == ContinueAsNewError: - raise RuntimeError("Cannot instantiate ContinueAsNewError directly") - super().__init__(*args) - - -# Overload for self (unfortunately, cannot type args) -@overload -def continue_as_new( - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - task_queue: Optional[str] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.SearchAttributes, temporalio.common.TypedSearchAttributes - ] - ] = None, - versioning_intent: Optional[VersioningIntent] = None, -) -> NoReturn: ... - - -# Overload for no-param workflow -@overload -def continue_as_new( - *, - workflow: MethodAsyncNoParam[SelfType, Any], - task_queue: Optional[str] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.SearchAttributes, temporalio.common.TypedSearchAttributes - ] - ] = None, - versioning_intent: Optional[VersioningIntent] = None, -) -> NoReturn: ... - - -# Overload for single-param workflow -@overload -def continue_as_new( - arg: ParamType, - *, - workflow: MethodAsyncSingleParam[SelfType, ParamType, Any], - task_queue: Optional[str] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.SearchAttributes, temporalio.common.TypedSearchAttributes - ] - ] = None, - versioning_intent: Optional[VersioningIntent] = None, -) -> NoReturn: ... - - -# Overload for multi-param workflow -@overload -def continue_as_new( - *, - workflow: Callable[Concatenate[SelfType, MultiParamSpec], Awaitable[Any]], - args: Sequence[Any], - task_queue: Optional[str] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.SearchAttributes, temporalio.common.TypedSearchAttributes - ] - ] = None, - versioning_intent: Optional[VersioningIntent] = None, -) -> NoReturn: ... - - -# Overload for string-name workflow -@overload -def continue_as_new( - *, - workflow: str, - args: Sequence[Any] = [], - task_queue: Optional[str] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.SearchAttributes, temporalio.common.TypedSearchAttributes - ] - ] = None, - versioning_intent: Optional[VersioningIntent] = None, -) -> NoReturn: ... - - -def continue_as_new( - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - workflow: Union[None, Callable, str] = None, - task_queue: Optional[str] = None, - run_timeout: Optional[timedelta] = None, - task_timeout: Optional[timedelta] = None, - retry_policy: Optional[temporalio.common.RetryPolicy] = None, - memo: Optional[Mapping[str, Any]] = None, - search_attributes: Optional[ - Union[ - temporalio.common.SearchAttributes, temporalio.common.TypedSearchAttributes - ] - ] = None, - versioning_intent: Optional[VersioningIntent] = None, -) -> NoReturn: - """Stop the workflow immediately and continue as new. - - Args: - arg: Single argument to the continued workflow. - args: Multiple arguments to the continued workflow. Cannot be set if arg - is. - workflow: Specific workflow to continue to. Defaults to the current - workflow. - task_queue: Task queue to run the workflow on. Defaults to the current - workflow's task queue. - run_timeout: Timeout of a single workflow run. Defaults to the current - workflow's run timeout. - task_timeout: Timeout of a single workflow task. Defaults to the current - workflow's task timeout. - memo: Memo for the workflow. Defaults to the current workflow's memo. - search_attributes: Search attributes for the workflow. Defaults to the - current workflow's search attributes. The dictionary form of this is - DEPRECATED. - versioning_intent: When using the Worker Versioning feature, specifies whether this Workflow - should Continue-as-New onto a worker with a compatible Build Id or not. - Deprecated: Use Worker Deployment versioning instead. - - Returns: - Never returns, always raises a :py:class:`ContinueAsNewError`. - - Raises: - ContinueAsNewError: Always raised by this function. Should not be caught - but instead be allowed to - """ - temporalio.common._warn_on_deprecated_search_attributes(search_attributes) - _Runtime.current().workflow_continue_as_new( - *temporalio.common._arg_or_args(arg, args), - workflow=workflow, - task_queue=task_queue, - run_timeout=run_timeout, - task_timeout=task_timeout, - retry_policy=retry_policy, - memo=memo, - search_attributes=search_attributes, - versioning_intent=versioning_intent, - ) - - -def get_signal_handler(name: str) -> Optional[Callable]: - """Get the signal handler for the given name if any. - - This includes handlers created via the ``@workflow.signal`` decorator. - - Args: - name: Name of the signal. - - Returns: - Callable for the signal if any. If a handler is not found for the name, - this will not return the dynamic handler even if there is one. - """ - return _Runtime.current().workflow_get_signal_handler(name) - - -def set_signal_handler(name: str, handler: Optional[Callable]) -> None: - """Set or unset the signal handler for the given name. - - This overrides any existing handlers for the given name, including handlers - created via the ``@workflow.signal`` decorator. - - When set, all unhandled past signals for the given name are immediately sent - to the handler. - - Args: - name: Name of the signal. - handler: Callable to set or None to unset. - """ - _Runtime.current().workflow_set_signal_handler(name, handler) - - -def get_dynamic_signal_handler() -> Optional[Callable]: - """Get the dynamic signal handler if any. - - This includes dynamic handlers created via the ``@workflow.signal`` - decorator. - - Returns: - Callable for the dynamic signal handler if any. - """ - return _Runtime.current().workflow_get_signal_handler(None) - - -def set_dynamic_signal_handler(handler: Optional[Callable]) -> None: - """Set or unset the dynamic signal handler. - - This overrides the existing dynamic handler even if it was created via the - ``@workflow.signal`` decorator. - - When set, all unhandled past signals are immediately sent to the handler. - - Args: - handler: Callable to set or None to unset. - """ - _Runtime.current().workflow_set_signal_handler(None, handler) - - -def get_query_handler(name: str) -> Optional[Callable]: - """Get the query handler for the given name if any. - - This includes handlers created via the ``@workflow.query`` decorator. - - Args: - name: Name of the query. - - Returns: - Callable for the query if any. If a handler is not found for the name, - this will not return the dynamic handler even if there is one. - """ - return _Runtime.current().workflow_get_query_handler(name) - - -def set_query_handler(name: str, handler: Optional[Callable]) -> None: - """Set or unset the query handler for the given name. - - This overrides any existing handlers for the given name, including handlers - created via the ``@workflow.query`` decorator. - - Args: - name: Name of the query. - handler: Callable to set or None to unset. - """ - _Runtime.current().workflow_set_query_handler(name, handler) - - -def get_dynamic_query_handler() -> Optional[Callable]: - """Get the dynamic query handler if any. - - This includes dynamic handlers created via the ``@workflow.query`` - decorator. - - Returns: - Callable for the dynamic query handler if any. - """ - return _Runtime.current().workflow_get_query_handler(None) - - -def set_dynamic_query_handler(handler: Optional[Callable]) -> None: - """Set or unset the dynamic query handler. - - This overrides the existing dynamic handler even if it was created via the - ``@workflow.query`` decorator. - - Args: - handler: Callable to set or None to unset. - """ - _Runtime.current().workflow_set_query_handler(None, handler) - - -def get_update_handler(name: str) -> Optional[Callable]: - """Get the update handler for the given name if any. - - This includes handlers created via the ``@workflow.update`` decorator. - - Args: - name: Name of the update. - - Returns: - Callable for the update if any. If a handler is not found for the name, - this will not return the dynamic handler even if there is one. - """ - return _Runtime.current().workflow_get_update_handler(name) - - -def set_update_handler( - name: str, handler: Optional[Callable], *, validator: Optional[Callable] = None -) -> None: - """Set or unset the update handler for the given name. - - This overrides any existing handlers for the given name, including handlers - created via the ``@workflow.update`` decorator. - - Args: - name: Name of the update. - handler: Callable to set or None to unset. - validator: Callable to set or None to unset as the update validator. - """ - _Runtime.current().workflow_set_update_handler(name, handler, validator) - - -def get_dynamic_update_handler() -> Optional[Callable]: - """Get the dynamic update handler if any. - - This includes dynamic handlers created via the ``@workflow.update`` - decorator. - - Returns: - Callable for the dynamic update handler if any. - """ - return _Runtime.current().workflow_get_update_handler(None) - - -def set_dynamic_update_handler( - handler: Optional[Callable], *, validator: Optional[Callable] = None -) -> None: - """Set or unset the dynamic update handler. - - This overrides the existing dynamic handler even if it was created via the - ``@workflow.update`` decorator. - - Args: - handler: Callable to set or None to unset. - validator: Callable to set or None to unset as the update validator. - """ - _Runtime.current().workflow_set_update_handler(None, handler, validator) - - -def all_handlers_finished() -> bool: - """Whether update and signal handlers have finished executing. - - Consider waiting on this condition before workflow return or continue-as-new, to prevent - interruption of in-progress handlers by workflow exit: - ``await workflow.wait_condition(lambda: workflow.all_handlers_finished())`` - - Returns: - True if there are no in-progress update or signal handler executions. - """ - return _Runtime.current().workflow_all_handlers_finished() - - -def as_completed( - fs: Iterable[Awaitable[AnyType]], *, timeout: Optional[float] = None -) -> Iterator[Awaitable[AnyType]]: - """Return an iterator whose values are coroutines. - - This is a deterministic version of :py:func:`asyncio.as_completed`. This - function should be used instead of that one in workflows. - """ - # Taken almost verbatim from - # https://github.com/python/cpython/blob/v3.12.3/Lib/asyncio/tasks.py#L584 - # but the "set" is changed out for a "list" and fixed up some typing/format - - if asyncio.isfuture(fs) or asyncio.iscoroutine(fs): - raise TypeError(f"expect an iterable of futures, not {type(fs).__name__}") - - done: asyncio.Queue[Optional[asyncio.Future]] = asyncio.Queue() - - loop = asyncio.get_event_loop() - todo: List[asyncio.Future] = [asyncio.ensure_future(f, loop=loop) for f in list(fs)] - timeout_handle = None - - def _on_timeout(): - for f in todo: - f.remove_done_callback(_on_completion) - done.put_nowait(None) # Queue a dummy value for _wait_for_one(). - todo.clear() # Can't do todo.remove(f) in the loop. - - def _on_completion(f): - if not todo: - return # _on_timeout() was here first. - todo.remove(f) - done.put_nowait(f) - if not todo and timeout_handle is not None: - timeout_handle.cancel() - - async def _wait_for_one(): - f = await done.get() - if f is None: - # Dummy value from _on_timeout(). - raise asyncio.TimeoutError - return f.result() # May raise f.exception(). - - for f in todo: - f.add_done_callback(_on_completion) - if todo and timeout is not None: - timeout_handle = loop.call_later(timeout, _on_timeout) - for _ in range(len(todo)): - yield _wait_for_one() - - -if TYPE_CHECKING: - _FT = TypeVar("_FT", bound=asyncio.Future[Any]) -else: - _FT = TypeVar("_FT", bound=asyncio.Future) - - -@overload -async def wait( # type: ignore[misc] - fs: Iterable[_FT], - *, - timeout: Optional[float] = None, - return_when: str = asyncio.ALL_COMPLETED, -) -> Tuple[List[_FT], List[_FT]]: ... - - -@overload -async def wait( - fs: Iterable[asyncio.Task[AnyType]], - *, - timeout: Optional[float] = None, - return_when: str = asyncio.ALL_COMPLETED, -) -> Tuple[List[asyncio.Task[AnyType]], List[asyncio.Task[AnyType]]]: ... - - -async def wait( - fs: Iterable, - *, - timeout: Optional[float] = None, - return_when: str = asyncio.ALL_COMPLETED, -) -> Tuple: - """Wait for the Futures or Tasks given by fs to complete. - - This is a deterministic version of :py:func:`asyncio.wait`. This function - should be used instead of that one in workflows. - """ - # Taken almost verbatim from - # https://github.com/python/cpython/blob/v3.12.3/Lib/asyncio/tasks.py#L435 - # but the "set" is changed out for a "list" and fixed up some typing/format - - if asyncio.isfuture(fs) or asyncio.iscoroutine(fs): - raise TypeError(f"Expect an iterable of Tasks/Futures, not {type(fs).__name__}") - if not fs: - raise ValueError("Sequence of Tasks/Futures must not be empty.") - if return_when not in ( - asyncio.FIRST_COMPLETED, - asyncio.FIRST_EXCEPTION, - asyncio.ALL_COMPLETED, - ): - raise ValueError(f"Invalid return_when value: {return_when}") - - fs = list(fs) - - if any(asyncio.iscoroutine(f) for f in fs): - raise TypeError("Passing coroutines is forbidden, use tasks explicitly.") - - loop = asyncio.get_running_loop() - return await _wait(fs, timeout, return_when, loop) - - -async def _wait( - fs: Iterable[Union[asyncio.Future, asyncio.Task]], - timeout: Optional[float], - return_when: str, - loop: asyncio.AbstractEventLoop, -) -> Tuple[List, List]: - # Taken almost verbatim from - # https://github.com/python/cpython/blob/v3.12.3/Lib/asyncio/tasks.py#L522 - # but the "set" is changed out for a "list" and fixed up some typing/format - - assert fs, "Sequence of Tasks/Futures must not be empty." - waiter = loop.create_future() - timeout_handle = None - if timeout is not None: - timeout_handle = loop.call_later(timeout, _release_waiter, waiter) - counter = len(fs) # type: ignore[arg-type] - - def _on_completion(f): - nonlocal counter - counter -= 1 - if ( - counter <= 0 - or return_when == asyncio.FIRST_COMPLETED - or return_when == asyncio.FIRST_EXCEPTION - and (not f.cancelled() and f.exception() is not None) - ): - if timeout_handle is not None: - timeout_handle.cancel() - if not waiter.done(): - waiter.set_result(None) - - for f in fs: - f.add_done_callback(_on_completion) - - try: - await waiter - finally: - if timeout_handle is not None: - timeout_handle.cancel() - for f in fs: - f.remove_done_callback(_on_completion) - - done, pending = [], [] - for f in fs: - if f.done(): - done.append(f) - else: - pending.append(f) - return done, pending - - -def _release_waiter(waiter: asyncio.Future[Any], *args) -> None: - # Taken almost verbatim from - # https://github.com/python/cpython/blob/v3.12.3/Lib/asyncio/tasks.py#L467 - - if not waiter.done(): - waiter.set_result(None) - - -def _is_unbound_method_on_cls(fn: Callable[..., Any], cls: Type) -> bool: - # Python 3 does not make this easy, ref https://stackoverflow.com/questions/3589311 - return ( - inspect.isfunction(fn) - and inspect.getmodule(fn) is inspect.getmodule(cls) - and fn.__qualname__.rsplit(".", 1)[0] == cls.__name__ - ) - - -class _UnexpectedEvictionError(temporalio.exceptions.TemporalError): - def __init__( - self, - reason: temporalio.bridge.proto.workflow_activation.RemoveFromCache.EvictionReason.ValueType, - message: str, - ) -> None: - self.reason = temporalio.bridge.proto.workflow_activation.RemoveFromCache.EvictionReason.Name( - reason - ) - self.message = message - super().__init__(f"{self.reason}: {message}") - - -class NondeterminismError(temporalio.exceptions.TemporalError): - """Error that can be thrown during replay for non-deterministic workflow.""" - - def __init__(self, message: str) -> None: - """Initialize a nondeterminism error.""" - super().__init__(message) - self.message = message - - -class ReadOnlyContextError(temporalio.exceptions.TemporalError): - """Error thrown when trying to do mutable workflow calls in a read-only - context like a query or update validator. - """ - - def __init__(self, message: str) -> None: - """Initialize a read-only context error.""" - super().__init__(message) - self.message = message - - -class _NotInWorkflowEventLoopError(temporalio.exceptions.TemporalError): - def __init__(self, *args: object) -> None: - super().__init__("Not in workflow event loop") - self.message = "Not in workflow event loop" - - -class VersioningIntent(Enum): - """Indicates whether the user intends certain commands to be run on a compatible worker Build - Id version or not. - - `COMPATIBLE` indicates that the command should run on a worker with compatible version if - possible. It may not be possible if the target task queue does not also have knowledge of the - current worker's Build Id. - - `DEFAULT` indicates that the command should run on the target task queue's current - overall-default Build Id. - - Where this type is accepted optionally, an unset value indicates that the SDK should choose the - most sensible default behavior for the type of command, accounting for whether the command will - be run on the same task queue as the current worker. - - .. deprecated:: - Use Worker Deployment versioning instead. - """ - - COMPATIBLE = 1 - DEFAULT = 2 - - def _to_proto(self) -> temporalio.bridge.proto.common.VersioningIntent.ValueType: - if self == VersioningIntent.COMPATIBLE: - return temporalio.bridge.proto.common.VersioningIntent.COMPATIBLE - elif self == VersioningIntent.DEFAULT: - return temporalio.bridge.proto.common.VersioningIntent.DEFAULT - return temporalio.bridge.proto.common.VersioningIntent.UNSPECIFIED - - -ServiceT = TypeVar("ServiceT") - - -class NexusOperationCancellationType(IntEnum): - """Defines behavior of a Nexus operation when the caller workflow initiates cancellation. - - Pass one of these values to :py:meth:`NexusClient.start_operation` to define cancellation - behavior. - - To initiate cancellation, use :py:meth:`NexusOperationHandle.cancel` and then `await` the - operation handle. This will result in a :py:class:`exceptions.NexusOperationError`. The values - of this enum define what is guaranteed to have happened by that point. - """ - - ABANDON = int(temporalio.bridge.proto.nexus.NexusOperationCancellationType.ABANDON) - """Do not send any cancellation request to the operation handler; just report cancellation to the caller""" - - TRY_CANCEL = int( - temporalio.bridge.proto.nexus.NexusOperationCancellationType.TRY_CANCEL - ) - """Send a cancellation request but immediately report cancellation to the caller. Note that this - does not guarantee that cancellation is delivered to the operation handler if the caller exits - before the delivery is done. - """ - - WAIT_REQUESTED = int( - temporalio.bridge.proto.nexus.NexusOperationCancellationType.WAIT_CANCELLATION_REQUESTED - ) - """Send a cancellation request and wait for confirmation that the request was received. - Does not wait for the operation to complete. - """ - - WAIT_COMPLETED = int( - temporalio.bridge.proto.nexus.NexusOperationCancellationType.WAIT_CANCELLATION_COMPLETED - ) - """Send a cancellation request and wait for the operation to complete. - Note that the operation may not complete as cancelled (for example, if it catches the - :py:exc:`asyncio.CancelledError` resulting from the cancellation request).""" - - -class NexusClient(ABC, Generic[ServiceT]): - """A client for invoking Nexus operations. - - .. warning:: - This API is experimental and unstable. - - Example:: - - nexus_client = workflow.create_nexus_client( - endpoint=my_nexus_endpoint, - service=MyService, - ) - handle = await nexus_client.start_operation( - operation=MyService.my_operation, - input=MyOperationInput(value="hello"), - schedule_to_close_timeout=timedelta(seconds=10), - ) - result = await handle.result() - """ - - # Overload for nexusrpc.Operation - @overload - @abstractmethod - async def start_operation( - self, - operation: nexusrpc.Operation[InputT, OutputT], - input: InputT, - *, - output_type: Optional[Type[OutputT]] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, - headers: Optional[Mapping[str, str]] = None, - ) -> NexusOperationHandle[OutputT]: ... - - # Overload for string operation name - @overload - @abstractmethod - async def start_operation( - self, - operation: str, - input: Any, - *, - output_type: Optional[Type[OutputT]] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, - headers: Optional[Mapping[str, str]] = None, - ) -> NexusOperationHandle[OutputT]: ... - - # Overload for workflow_run_operation methods - @overload - @abstractmethod - async def start_operation( - self, - operation: Callable[ - [ServiceHandlerT, temporalio.nexus.WorkflowRunOperationContext, InputT], - Awaitable[temporalio.nexus.WorkflowHandle[OutputT]], - ], - input: InputT, - *, - output_type: Optional[Type[OutputT]] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, - headers: Optional[Mapping[str, str]] = None, - ) -> NexusOperationHandle[OutputT]: ... - - # Overload for sync_operation methods (async def) - @overload - @abstractmethod - async def start_operation( - self, - operation: Callable[ - [ServiceHandlerT, nexusrpc.handler.StartOperationContext, InputT], - Awaitable[OutputT], - ], - input: InputT, - *, - output_type: Optional[Type[OutputT]] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, - headers: Optional[Mapping[str, str]] = None, - ) -> NexusOperationHandle[OutputT]: ... - - # Overload for sync_operation methods (def) - @overload - @abstractmethod - async def start_operation( - self, - operation: Callable[ - [ServiceHandlerT, nexusrpc.handler.StartOperationContext, InputT], - OutputT, - ], - input: InputT, - *, - output_type: Optional[Type[OutputT]] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, - headers: Optional[Mapping[str, str]] = None, - ) -> NexusOperationHandle[OutputT]: ... - - @abstractmethod - async def start_operation( - self, - operation: Any, - input: Any, - *, - output_type: Optional[Type[OutputT]] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, - headers: Optional[Mapping[str, str]] = None, - ) -> Any: - """Start a Nexus operation and return its handle. - - Args: - operation: The Nexus operation. - input: The Nexus operation input. - output_type: The Nexus operation output type. - schedule_to_close_timeout: Timeout for the entire operation attempt. - headers: Headers to send with the Nexus HTTP request. - - Returns: - A handle to the Nexus operation. The result can be obtained as - ```python - await handle.result() - ``` - """ - ... - - # Overload for nexusrpc.Operation - @overload - @abstractmethod - async def execute_operation( - self, - operation: nexusrpc.Operation[InputT, OutputT], - input: InputT, - *, - output_type: Optional[Type[OutputT]] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, - headers: Optional[Mapping[str, str]] = None, - ) -> OutputT: ... - - # Overload for string operation name - @overload - @abstractmethod - async def execute_operation( - self, - operation: str, - input: Any, - *, - output_type: Optional[Type[OutputT]] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, - headers: Optional[Mapping[str, str]] = None, - ) -> OutputT: ... - - # Overload for workflow_run_operation methods - @overload - @abstractmethod - async def execute_operation( - self, - operation: Callable[ - [ServiceHandlerT, temporalio.nexus.WorkflowRunOperationContext, InputT], - Awaitable[temporalio.nexus.WorkflowHandle[OutputT]], - ], - input: InputT, - *, - output_type: Optional[Type[OutputT]] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, - headers: Optional[Mapping[str, str]] = None, - ) -> OutputT: ... - - # TODO(nexus-preview): in practice, both these overloads match an async def sync - # operation (i.e. either can be deleted without causing a type error). - - # Overload for sync_operation methods (async def) - @overload - @abstractmethod - async def execute_operation( - self, - operation: Callable[ - [ServiceT, nexusrpc.handler.StartOperationContext, InputT], - Awaitable[OutputT], - ], - input: InputT, - *, - output_type: Optional[Type[OutputT]] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, - headers: Optional[Mapping[str, str]] = None, - ) -> OutputT: ... - - # Overload for sync_operation methods (def) - @overload - @abstractmethod - async def execute_operation( - self, - operation: Callable[ - [ServiceT, nexusrpc.handler.StartOperationContext, InputT], - OutputT, - ], - input: InputT, - *, - output_type: Optional[Type[OutputT]] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, - headers: Optional[Mapping[str, str]] = None, - ) -> OutputT: ... - - @abstractmethod - async def execute_operation( - self, - operation: Any, - input: Any, - *, - output_type: Optional[Type[OutputT]] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, - headers: Optional[Mapping[str, str]] = None, - ) -> Any: - """Execute a Nexus operation and return its result. - - Args: - operation: The Nexus operation. - input: The Nexus operation input. - output_type: The Nexus operation output type. - schedule_to_close_timeout: Timeout for the entire operation attempt. - headers: Headers to send with the Nexus HTTP request. - - Returns: - The operation result. - """ - ... - - -class _NexusClient(NexusClient[ServiceT]): - def __init__( - self, - *, - endpoint: str, - service: Union[Type[ServiceT], str], - ) -> None: - """Create a Nexus client. - - Args: - service: The Nexus service. - endpoint: The Nexus endpoint. - """ - # If service is not a str, then it must be a service interface or implementation - # class. - if isinstance(service, str): - self.service_name = service - elif service_defn := nexusrpc.get_service_definition(service): - self.service_name = service_defn.name - else: - raise ValueError( - f"`service` may be a name (str), or a class decorated with either " - f"@nexusrpc.handler.service_handler or @nexusrpc.service. " - f"Invalid service type: {type(service)}" - ) - self.endpoint = endpoint - - async def start_operation( - self, - operation: Any, - input: Any, - *, - output_type: Optional[Type] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, - headers: Optional[Mapping[str, str]] = None, - ) -> Any: - return ( - await temporalio.workflow._Runtime.current().workflow_start_nexus_operation( - endpoint=self.endpoint, - service=self.service_name, - operation=operation, - input=input, - output_type=output_type, - schedule_to_close_timeout=schedule_to_close_timeout, - cancellation_type=cancellation_type, - headers=headers, - ) - ) - - async def execute_operation( - self, - operation: Any, - input: Any, - *, - output_type: Optional[Type] = None, - schedule_to_close_timeout: Optional[timedelta] = None, - cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, - headers: Optional[Mapping[str, str]] = None, - ) -> Any: - handle = await self.start_operation( - operation, - input, - output_type=output_type, - schedule_to_close_timeout=schedule_to_close_timeout, - cancellation_type=cancellation_type, - headers=headers, - ) - return await handle - - -@overload -def create_nexus_client( - *, - service: Type[ServiceT], - endpoint: str, -) -> NexusClient[ServiceT]: ... - - -@overload -def create_nexus_client( - *, - service: str, - endpoint: str, -) -> NexusClient[Any]: ... - - -def create_nexus_client( - *, - service: Union[Type[ServiceT], str], - endpoint: str, -) -> NexusClient[ServiceT]: - """Create a Nexus client. - - .. warning:: - This API is experimental and unstable. - - Args: - service: The Nexus service. - endpoint: The Nexus endpoint. - """ - return _NexusClient(endpoint=endpoint, service=service) diff --git a/temporalio/workflow/__init__.py b/temporalio/workflow/__init__.py new file mode 100644 index 000000000..fa2681139 --- /dev/null +++ b/temporalio/workflow/__init__.py @@ -0,0 +1,328 @@ +"""Utilities that can decorate or be called inside workflows.""" + +from __future__ import annotations + +# BEGIN GENERATED NEXUS SYSTEM EXPORTS +from temporalio.nexus.system.workflow_service import ( + signal_with_start_workflow, +) + +# END GENERATED NEXUS SYSTEM EXPORTS +from ..types import ( + AnyType, + CallableAsyncNoParam, + CallableAsyncSingleParam, + CallableAsyncType, + CallableSyncNoParam, + CallableSyncOrAsyncReturnNoneType, + CallableSyncOrAsyncType, + CallableSyncSingleParam, + CallableType, + ClassType, + MethodAsyncNoParam, + MethodAsyncSingleParam, + MethodSyncNoParam, + MethodSyncOrAsyncNoParam, + MethodSyncOrAsyncSingleParam, + MethodSyncSingleParam, + MultiParamSpec, + ParamType, + ProtocolReturnType, + ReturnType, + SelfType, +) +from ._activities import ( + ActivityCancellationType, + ActivityConfig, + ActivityHandle, + LocalActivityConfig, + _AsyncioTask, + execute_activity, + execute_activity_class, + execute_activity_method, + execute_local_activity, + execute_local_activity_class, + execute_local_activity_method, + start_activity, + start_activity_class, + start_activity_method, + start_local_activity, + start_local_activity_class, + start_local_activity_method, +) +from ._asyncio import ( + _FT, + _release_waiter, + _wait, + as_completed, + wait, +) +from ._context import ( + Info, + ParentInfo, + RootInfo, + UpdateInfo, + _current_update_info, + _Runtime, + _set_current_update_info, + cancellation_reason, + current_update_info, + deprecate_patch, + extern_functions, + get_current_details, + get_last_completion_result, + get_last_failure, + has_last_completion_result, + in_workflow, + info, + instance, + is_failure_exception, + memo, + memo_value, + metric_meter, + new_random, + now, + patched, + payload_converter, + random, + random_seed, + register_random_seed_callback, + set_current_details, + sleep, + time, + time_ns, + upsert_memo, + upsert_search_attributes, + uuid4, + uuid7, + wait_condition, +) +from ._definition import ( + DynamicWorkflowConfig, + _Definition, + _is_unbound_method_on_cls, + _parameters_identical_up_to_naming, + defn, + dynamic_config, + init, + run, +) +from ._exceptions import ( + ContinueAsNewVersioningBehavior, + NondeterminismError, + ReadOnlyContextError, + VersioningIntent, + _NotInWorkflowEventLoopError, +) +from ._handlers import ( + HandlerUnfinishedPolicy, + UnfinishedSignalHandlersWarning, + UnfinishedUpdateHandlersWarning, + UpdateMethodMultiParam, + _assert_dynamic_handler_args, + _bind_method, + _QueryDefinition, + _SignalDefinition, + _update_validator, + _UpdateDefinition, + query, + signal, + update, +) +from ._nexus import ( + NexusClient, + NexusOperationCancellationType, + NexusOperationHandle, + _NexusClient, + create_nexus_client, +) +from ._sandbox import ( + LoggerAdapter, + SandboxImportNotificationPolicy, + _build_log_context, + _imports_passed_through, + _in_sandbox, + _sandbox_import_notification_policy_override, + _sandbox_unrestricted, + logger, + unsafe, +) +from ._workflow_ops import ( + ChildWorkflowCancellationType, + ChildWorkflowConfig, + ChildWorkflowHandle, + ContinueAsNewError, + ExternalWorkflowHandle, + ParentClosePolicy, + all_handlers_finished, + continue_as_new, + execute_child_workflow, + get_dynamic_query_handler, + get_dynamic_signal_handler, + get_dynamic_update_handler, + get_external_workflow_handle, + get_external_workflow_handle_for, + get_query_handler, + get_signal_handler, + get_update_handler, + set_dynamic_query_handler, + set_dynamic_signal_handler, + set_dynamic_update_handler, + set_query_handler, + set_signal_handler, + set_update_handler, + start_child_workflow, +) + +__all__ = [ + "ActivityCancellationType", + "ActivityConfig", + "ActivityHandle", + "LocalActivityConfig", + "execute_activity", + "execute_activity_class", + "execute_activity_method", + "execute_local_activity", + "execute_local_activity_class", + "execute_local_activity_method", + "start_activity", + "start_activity_class", + "start_activity_method", + "start_local_activity", + "start_local_activity_class", + "start_local_activity_method", + "as_completed", + "wait", + "Info", + "ParentInfo", + "RootInfo", + "UpdateInfo", + "current_update_info", + "deprecate_patch", + "extern_functions", + "get_current_details", + "get_last_completion_result", + "get_last_failure", + "has_last_completion_result", + "cancellation_reason", + "in_workflow", + "info", + "instance", + "is_failure_exception", + "memo", + "memo_value", + "metric_meter", + "new_random", + "now", + "patched", + "payload_converter", + "random", + "random_seed", + "register_random_seed_callback", + "set_current_details", + "sleep", + "time", + "time_ns", + "upsert_memo", + "upsert_search_attributes", + "uuid4", + "uuid7", + "wait_condition", + "DynamicWorkflowConfig", + "defn", + "dynamic_config", + "init", + "run", + "NondeterminismError", + "ReadOnlyContextError", + "VersioningIntent", + "ContinueAsNewVersioningBehavior", + "HandlerUnfinishedPolicy", + "UnfinishedSignalHandlersWarning", + "UnfinishedUpdateHandlersWarning", + "UpdateMethodMultiParam", + "query", + "signal", + "update", + "NexusClient", + "NexusOperationCancellationType", + "NexusOperationHandle", + "create_nexus_client", + "LoggerAdapter", + "SandboxImportNotificationPolicy", + "logger", + "unsafe", + "ChildWorkflowCancellationType", + "ChildWorkflowConfig", + "ChildWorkflowHandle", + "ContinueAsNewError", + "ExternalWorkflowHandle", + "ParentClosePolicy", + "all_handlers_finished", + "continue_as_new", + "execute_child_workflow", + "get_dynamic_query_handler", + "get_dynamic_signal_handler", + "get_dynamic_update_handler", + "get_external_workflow_handle", + "get_external_workflow_handle_for", + "get_query_handler", + "get_signal_handler", + "get_update_handler", + "set_dynamic_query_handler", + "set_dynamic_signal_handler", + "set_dynamic_update_handler", + "set_query_handler", + "set_signal_handler", + "set_update_handler", + "start_child_workflow", + "_AsyncioTask", + "_FT", + "_release_waiter", + "_wait", + "_current_update_info", + "_Runtime", + "_set_current_update_info", + "_Definition", + "_is_unbound_method_on_cls", + "_parameters_identical_up_to_naming", + "_NotInWorkflowEventLoopError", + "_assert_dynamic_handler_args", + "_bind_method", + "_QueryDefinition", + "_SignalDefinition", + "_update_validator", + "_UpdateDefinition", + "_NexusClient", + "_build_log_context", + "_imports_passed_through", + "_in_sandbox", + "_sandbox_import_notification_policy_override", + "_sandbox_unrestricted", + # Re-export Temporal-owned names that old temporalio/workflow.py imported + # at module scope so explicit imports from temporalio.workflow keep working. + "AnyType", + "CallableAsyncNoParam", + "CallableAsyncSingleParam", + "CallableAsyncType", + "CallableSyncNoParam", + "CallableSyncOrAsyncReturnNoneType", + "CallableSyncOrAsyncType", + "CallableSyncSingleParam", + "CallableType", + "ClassType", + "MethodAsyncNoParam", + "MethodAsyncSingleParam", + "MethodSyncNoParam", + "MethodSyncOrAsyncNoParam", + "MethodSyncOrAsyncSingleParam", + "MethodSyncSingleParam", + "MultiParamSpec", + "ParamType", + "ProtocolReturnType", + "ReturnType", + "SelfType", + # BEGIN GENERATED NEXUS SYSTEM __ALL__ + "signal_with_start_workflow", + # END GENERATED NEXUS SYSTEM __ALL__ +] diff --git a/temporalio/workflow/_activities.py b/temporalio/workflow/_activities.py new file mode 100644 index 000000000..ef883c016 --- /dev/null +++ b/temporalio/workflow/_activities.py @@ -0,0 +1,2008 @@ +from __future__ import annotations + +import asyncio +from collections.abc import Awaitable, Callable, Sequence +from datetime import timedelta +from enum import IntEnum +from typing import TYPE_CHECKING, Any, Concatenate, Generic, TypedDict, overload + +import temporalio.bridge.proto.workflow_commands +import temporalio.common + +from ..types import ( + AnyType, + CallableAsyncNoParam, + CallableAsyncSingleParam, + CallableSyncNoParam, + CallableSyncSingleParam, + MethodAsyncNoParam, + MethodAsyncSingleParam, + MethodSyncNoParam, + MethodSyncSingleParam, + MultiParamSpec, + ParamType, + ReturnType, + SelfType, +) +from ._context import _Runtime +from ._exceptions import VersioningIntent + +__all__ = [ + "ActivityCancellationType", + "ActivityConfig", + "ActivityHandle", + "LocalActivityConfig", + "execute_activity", + "execute_activity_class", + "execute_activity_method", + "execute_local_activity", + "execute_local_activity_class", + "execute_local_activity_method", + "start_activity", + "start_activity_class", + "start_activity_method", + "start_local_activity", + "start_local_activity_class", + "start_local_activity_method", +] + +# See https://mypy.readthedocs.io/en/latest/runtime_troubles.html#using-classes-that-are-generic-in-stubs-but-not-at-runtime +if TYPE_CHECKING: + + class _AsyncioTask(asyncio.Task[AnyType]): + pass + +else: + # TODO: inherited classes should be other way around? + class _AsyncioTask(Generic[AnyType], asyncio.Task): + pass + + +class ActivityHandle(_AsyncioTask[ReturnType]): # type: ignore[type-var] + """Handle returned from :py:func:`start_activity` and + :py:func:`start_local_activity`. + + This extends :py:class:`asyncio.Task` and supports all task features. + """ + + pass + + +class ActivityCancellationType(IntEnum): + """How an activity cancellation should be handled.""" + + TRY_CANCEL = int( + temporalio.bridge.proto.workflow_commands.ActivityCancellationType.TRY_CANCEL + ) + WAIT_CANCELLATION_COMPLETED = int( + temporalio.bridge.proto.workflow_commands.ActivityCancellationType.WAIT_CANCELLATION_COMPLETED + ) + ABANDON = int( + temporalio.bridge.proto.workflow_commands.ActivityCancellationType.ABANDON + ) + + +class ActivityConfig(TypedDict, total=False): + """TypedDict of config that can be used for :py:func:`start_activity` and + :py:func:`execute_activity`. + """ + + task_queue: str | None + schedule_to_close_timeout: timedelta | None + schedule_to_start_timeout: timedelta | None + start_to_close_timeout: timedelta | None + heartbeat_timeout: timedelta | None + retry_policy: temporalio.common.RetryPolicy | None + cancellation_type: ActivityCancellationType + activity_id: str | None + versioning_intent: VersioningIntent | None + summary: str | None + priority: temporalio.common.Priority + + +# Overload for async no-param activity +@overload +def start_activity( + activity: CallableAsyncNoParam[ReturnType], + *, + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for sync no-param activity +@overload +def start_activity( + activity: CallableSyncNoParam[ReturnType], + *, + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for async single-param activity +@overload +def start_activity( + activity: CallableAsyncSingleParam[ParamType, ReturnType], + arg: ParamType, + *, + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for sync single-param activity +@overload +def start_activity( + activity: CallableSyncSingleParam[ParamType, ReturnType], + arg: ParamType, + *, + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for async multi-param activity +@overload +def start_activity( + activity: Callable[..., Awaitable[ReturnType]], + *, + args: Sequence[Any], + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for sync multi-param activity +@overload +def start_activity( + activity: Callable[..., ReturnType], + *, + args: Sequence[Any], + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for string-name activity +@overload +def start_activity( + activity: str, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + task_queue: str | None = None, + result_type: type | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ActivityHandle[Any]: ... + + +def start_activity( + activity: Any, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + task_queue: str | None = None, + result_type: type | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ActivityHandle[Any]: + """Start an activity and return its handle. + + At least one of ``schedule_to_close_timeout`` or ``start_to_close_timeout`` + must be present. + + Args: + activity: Activity name or function reference. + arg: Single argument to the activity. + args: Multiple arguments to the activity. Cannot be set if arg is. + task_queue: Task queue to run the activity on. Defaults to the current + workflow's task queue. + result_type: For string activities, this can set the specific result + type hint to deserialize into. + schedule_to_close_timeout: Max amount of time the activity can take from + first being scheduled to being completed before it times out. This + is inclusive of all retries. + schedule_to_start_timeout: Max amount of time the activity can take to + be started from first being scheduled. + start_to_close_timeout: Max amount of time a single activity run can + take from when it starts to when it completes. This is per retry. + heartbeat_timeout: How frequently an activity must invoke heartbeat + while running before it is considered timed out. + retry_policy: How an activity is retried on failure. If unset, a + server-defined default is used. Set maximum attempts to 1 to disable + retries. + cancellation_type: How the activity is treated when it is cancelled from + the workflow. + activity_id: Optional unique identifier for the activity. This is an + advanced setting that should not be set unless users are sure they + need to. Contact Temporal before setting this value. + versioning_intent: When using the Worker Versioning feature, specifies whether this Activity + should run on a worker with a compatible Build Id or not. + Deprecated: Use Worker Deployment versioning instead. + summary: A single-line fixed summary for this activity that may appear in UI/CLI. + This can be in single-line Temporal markdown format. + priority: Priority of the activity. + + Returns: + An activity handle to the activity which is an async task. + """ + return _Runtime.current().workflow_start_activity( + activity, + *temporalio.common._arg_or_args(arg, args), + task_queue=task_queue, + result_type=result_type, + schedule_to_close_timeout=schedule_to_close_timeout, + schedule_to_start_timeout=schedule_to_start_timeout, + start_to_close_timeout=start_to_close_timeout, + heartbeat_timeout=heartbeat_timeout, + retry_policy=retry_policy, + cancellation_type=cancellation_type, + activity_id=activity_id, + versioning_intent=versioning_intent, + summary=summary, + priority=priority, + ) + + +# Overload for async no-param activity +@overload +async def execute_activity( + activity: CallableAsyncNoParam[ReturnType], + *, + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ReturnType: ... + + +# Overload for sync no-param activity +@overload +async def execute_activity( + activity: CallableSyncNoParam[ReturnType], + *, + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ReturnType: ... + + +# Overload for async single-param activity +@overload +async def execute_activity( + activity: CallableAsyncSingleParam[ParamType, ReturnType], + arg: ParamType, + *, + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ReturnType: ... + + +# Overload for sync single-param activity +@overload +async def execute_activity( + activity: CallableSyncSingleParam[ParamType, ReturnType], + arg: ParamType, + *, + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ReturnType: ... + + +# Overload for async multi-param activity +@overload +async def execute_activity( + activity: Callable[..., Awaitable[ReturnType]], + *, + args: Sequence[Any], + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ReturnType: ... + + +# Overload for sync multi-param activity +@overload +async def execute_activity( + activity: Callable[..., ReturnType], + *, + args: Sequence[Any], + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ReturnType: ... + + +# Overload for string-name activity +@overload +async def execute_activity( + activity: str, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + task_queue: str | None = None, + result_type: type | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> Any: ... + + +async def execute_activity( + activity: Any, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + task_queue: str | None = None, + result_type: type | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> Any: + """Start an activity and wait for completion. + + This is a shortcut for ``await`` :py:meth:`start_activity`. + """ + # We call the runtime directly instead of top-level start_activity to ensure + # we don't miss new parameters + return await _Runtime.current().workflow_start_activity( + activity, + *temporalio.common._arg_or_args(arg, args), + task_queue=task_queue, + result_type=result_type, + schedule_to_close_timeout=schedule_to_close_timeout, + schedule_to_start_timeout=schedule_to_start_timeout, + start_to_close_timeout=start_to_close_timeout, + heartbeat_timeout=heartbeat_timeout, + retry_policy=retry_policy, + cancellation_type=cancellation_type, + activity_id=activity_id, + versioning_intent=versioning_intent, + summary=summary, + priority=priority, + ) + + +# Overload for async no-param activity +@overload +def start_activity_class( + activity: type[CallableAsyncNoParam[ReturnType]], + *, + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for sync no-param activity +@overload +def start_activity_class( + activity: type[CallableSyncNoParam[ReturnType]], + *, + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for async single-param activity +@overload +def start_activity_class( + activity: type[CallableAsyncSingleParam[ParamType, ReturnType]], + arg: ParamType, + *, + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for sync single-param activity +@overload +def start_activity_class( + activity: type[CallableSyncSingleParam[ParamType, ReturnType]], + arg: ParamType, + *, + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for async multi-param activity +@overload +def start_activity_class( + activity: type[Callable[..., Awaitable[ReturnType]]], # type: ignore[reportOverlappingOverload] + *, + args: Sequence[Any], + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for sync multi-param activity +@overload +def start_activity_class( # type: ignore[reportOverlappingOverload] + activity: type[Callable[..., ReturnType]], # type: ignore[reportOverlappingOverload] + *, + args: Sequence[Any], + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ActivityHandle[ReturnType]: ... + + +def start_activity_class( + activity: type[Callable], # type: ignore[reportOverlappingOverload] + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ActivityHandle[Any]: + """Start an activity from a callable class. + + See :py:meth:`start_activity` for parameter and return details. + """ + return _Runtime.current().workflow_start_activity( + activity, + *temporalio.common._arg_or_args(arg, args), + task_queue=task_queue, + result_type=None, + schedule_to_close_timeout=schedule_to_close_timeout, + schedule_to_start_timeout=schedule_to_start_timeout, + start_to_close_timeout=start_to_close_timeout, + heartbeat_timeout=heartbeat_timeout, + retry_policy=retry_policy, + cancellation_type=cancellation_type, + activity_id=activity_id, + versioning_intent=versioning_intent, + summary=summary, + priority=priority, + ) + + +# Overload for async no-param activity +@overload +async def execute_activity_class( + activity: type[CallableAsyncNoParam[ReturnType]], + *, + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ReturnType: ... + + +# Overload for sync no-param activity +@overload +async def execute_activity_class( + activity: type[CallableSyncNoParam[ReturnType]], + *, + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ReturnType: ... + + +# Overload for async single-param activity +@overload +async def execute_activity_class( + activity: type[CallableAsyncSingleParam[ParamType, ReturnType]], + arg: ParamType, + *, + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ReturnType: ... + + +# Overload for sync single-param activity +@overload +async def execute_activity_class( + activity: type[CallableSyncSingleParam[ParamType, ReturnType]], + arg: ParamType, + *, + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ReturnType: ... + + +# Overload for async multi-param activity +@overload +async def execute_activity_class( + activity: type[Callable[..., Awaitable[ReturnType]]], # type: ignore[reportOverlappingOverload] + *, + args: Sequence[Any], + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ReturnType: ... + + +# Overload for sync multi-param activity +@overload +async def execute_activity_class( + activity: type[Callable[..., ReturnType]], # type: ignore[reportOverlappingOverload] + *, + args: Sequence[Any], + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ReturnType: ... + + +async def execute_activity_class( + activity: type[Callable], # type: ignore[reportOverlappingOverload] + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> Any: + """Start an activity from a callable class and wait for completion. + + This is a shortcut for ``await`` :py:meth:`start_activity_class`. + """ + return await _Runtime.current().workflow_start_activity( + activity, + *temporalio.common._arg_or_args(arg, args), + task_queue=task_queue, + result_type=None, + schedule_to_close_timeout=schedule_to_close_timeout, + schedule_to_start_timeout=schedule_to_start_timeout, + start_to_close_timeout=start_to_close_timeout, + heartbeat_timeout=heartbeat_timeout, + retry_policy=retry_policy, + cancellation_type=cancellation_type, + activity_id=activity_id, + versioning_intent=versioning_intent, + summary=summary, + priority=priority, + ) + + +# Overload for async no-param activity +@overload +def start_activity_method( + activity: MethodAsyncNoParam[SelfType, ReturnType], + *, + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for sync no-param activity +@overload +def start_activity_method( + activity: MethodSyncNoParam[SelfType, ReturnType], + *, + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for async single-param activity +@overload +def start_activity_method( + activity: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], + arg: ParamType, + *, + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for sync single-param activity +@overload +def start_activity_method( + activity: MethodSyncSingleParam[SelfType, ParamType, ReturnType], + arg: ParamType, + *, + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for async multi-param activity +@overload +def start_activity_method( + activity: Callable[Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType]], + *, + args: Sequence[Any], + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for sync multi-param activity +@overload +def start_activity_method( + activity: Callable[Concatenate[SelfType, MultiParamSpec], ReturnType], + *, + args: Sequence[Any], + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ActivityHandle[ReturnType]: ... + + +def start_activity_method( + activity: Callable, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ActivityHandle[Any]: + """Start an activity from a method. + + See :py:meth:`start_activity` for parameter and return details. + """ + return _Runtime.current().workflow_start_activity( + activity, + *temporalio.common._arg_or_args(arg, args), + task_queue=task_queue, + result_type=None, + schedule_to_close_timeout=schedule_to_close_timeout, + schedule_to_start_timeout=schedule_to_start_timeout, + start_to_close_timeout=start_to_close_timeout, + heartbeat_timeout=heartbeat_timeout, + retry_policy=retry_policy, + cancellation_type=cancellation_type, + activity_id=activity_id, + versioning_intent=versioning_intent, + summary=summary, + priority=priority, + ) + + +# Overload for async no-param activity +@overload +async def execute_activity_method( + activity: MethodAsyncNoParam[SelfType, ReturnType], + *, + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ReturnType: ... + + +# Overload for sync no-param activity +@overload +async def execute_activity_method( + activity: MethodSyncNoParam[SelfType, ReturnType], + *, + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ReturnType: ... + + +# Overload for async single-param activity +@overload +async def execute_activity_method( + activity: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], + arg: ParamType, + *, + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ReturnType: ... + + +# Overload for sync single-param activity +@overload +async def execute_activity_method( + activity: MethodSyncSingleParam[SelfType, ParamType, ReturnType], + arg: ParamType, + *, + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ReturnType: ... + + +# Overload for async multi-param activity +@overload +async def execute_activity_method( + activity: Callable[Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType]], + *, + args: Sequence[Any], + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ReturnType: ... + + +# Overload for sync multi-param activity +@overload +async def execute_activity_method( + activity: Callable[Concatenate[SelfType, MultiParamSpec], ReturnType], + *, + args: Sequence[Any], + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ReturnType: ... + + +async def execute_activity_method( + activity: Callable, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + task_queue: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + versioning_intent: VersioningIntent | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> Any: + """Start an activity from a method and wait for completion. + + This is a shortcut for ``await`` :py:meth:`start_activity_method`. + """ + # We call the runtime directly instead of top-level start_activity to ensure + # we don't miss new parameters + return await _Runtime.current().workflow_start_activity( + activity, + *temporalio.common._arg_or_args(arg, args), + task_queue=task_queue, + result_type=None, + schedule_to_close_timeout=schedule_to_close_timeout, + schedule_to_start_timeout=schedule_to_start_timeout, + start_to_close_timeout=start_to_close_timeout, + heartbeat_timeout=heartbeat_timeout, + retry_policy=retry_policy, + cancellation_type=cancellation_type, + activity_id=activity_id, + versioning_intent=versioning_intent, + summary=summary, + priority=priority, + ) + + +class LocalActivityConfig(TypedDict, total=False): + """TypedDict of config that can be used for :py:func:`start_local_activity` + and :py:func:`execute_local_activity`. + """ + + schedule_to_close_timeout: timedelta | None + schedule_to_start_timeout: timedelta | None + start_to_close_timeout: timedelta | None + retry_policy: temporalio.common.RetryPolicy | None + local_retry_threshold: timedelta | None + cancellation_type: ActivityCancellationType + activity_id: str | None + summary: str | None + + +# Overload for async no-param activity +@overload +def start_local_activity( + activity: CallableAsyncNoParam[ReturnType], + *, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for sync no-param activity +@overload +def start_local_activity( + activity: CallableSyncNoParam[ReturnType], + *, + activity_id: str | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + summary: str | None = None, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for async single-param activity +@overload +def start_local_activity( + activity: CallableAsyncSingleParam[ParamType, ReturnType], + arg: ParamType, + *, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for sync single-param activity +@overload +def start_local_activity( + activity: CallableSyncSingleParam[ParamType, ReturnType], + arg: ParamType, + *, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for async multi-param activity +@overload +def start_local_activity( + activity: Callable[..., Awaitable[ReturnType]], + *, + args: Sequence[Any], + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for sync multi-param activity +@overload +def start_local_activity( + activity: Callable[..., ReturnType], + *, + args: Sequence[Any], + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for string-name activity +@overload +def start_local_activity( + activity: str, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + result_type: type | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ActivityHandle[Any]: ... + + +def start_local_activity( + activity: Any, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + result_type: type | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ActivityHandle[Any]: + """Start a local activity and return its handle. + + At least one of ``schedule_to_close_timeout`` or ``start_to_close_timeout`` + must be present. + + Args: + activity: Activity name or function reference. + arg: Single argument to the activity. + args: Multiple arguments to the activity. Cannot be set if arg is. + result_type: For string activities, this can set the specific result + type hint to deserialize into. + schedule_to_close_timeout: Max amount of time the activity can take from + first being scheduled to being completed before it times out. This + is inclusive of all retries. + schedule_to_start_timeout: Max amount of time the activity can take to + be started from first being scheduled. + start_to_close_timeout: Max amount of time a single activity run can + take from when it starts to when it completes. This is per retry. + retry_policy: How an activity is retried on failure. If unset, an + SDK-defined default is used. Set maximum attempts to 1 to disable + retries. + cancellation_type: How the activity is treated when it is cancelled from + the workflow. + activity_id: Optional unique identifier for the activity. This is an + advanced setting that should not be set unless users are sure they + need to. Contact Temporal before setting this value. + summary: Optional summary for the activity. + + Returns: + An activity handle to the activity which is an async task. + """ + return _Runtime.current().workflow_start_local_activity( + activity, + *temporalio.common._arg_or_args(arg, args), + result_type=result_type, + schedule_to_close_timeout=schedule_to_close_timeout, + schedule_to_start_timeout=schedule_to_start_timeout, + start_to_close_timeout=start_to_close_timeout, + retry_policy=retry_policy, + local_retry_threshold=local_retry_threshold, + cancellation_type=cancellation_type, + activity_id=activity_id, + summary=summary, + ) + + +# Overload for async no-param activity +@overload +async def execute_local_activity( + activity: CallableAsyncNoParam[ReturnType], + *, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ReturnType: ... + + +# Overload for sync no-param activity +@overload +async def execute_local_activity( + activity: CallableSyncNoParam[ReturnType], + *, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ReturnType: ... + + +# Overload for async single-param activity +@overload +async def execute_local_activity( + activity: CallableAsyncSingleParam[ParamType, ReturnType], + arg: ParamType, + *, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ReturnType: ... + + +# Overload for sync single-param activity +@overload +async def execute_local_activity( + activity: CallableSyncSingleParam[ParamType, ReturnType], + arg: ParamType, + *, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ReturnType: ... + + +# Overload for async multi-param activity +@overload +async def execute_local_activity( + activity: Callable[..., Awaitable[ReturnType]], + *, + args: Sequence[Any], + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ReturnType: ... + + +# Overload for sync multi-param activity +@overload +async def execute_local_activity( + activity: Callable[..., ReturnType], + *, + args: Sequence[Any], + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ReturnType: ... + + +# Overload for string-name activity +@overload +async def execute_local_activity( + activity: str, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + result_type: type | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> Any: ... + + +async def execute_local_activity( + activity: Any, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + result_type: type | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> Any: + """Start a local activity and wait for completion. + + This is a shortcut for ``await`` :py:meth:`start_local_activity`. + """ + # We call the runtime directly instead of top-level start_local_activity to + # ensure we don't miss new parameters + return await _Runtime.current().workflow_start_local_activity( + activity, + *temporalio.common._arg_or_args(arg, args), + result_type=result_type, + schedule_to_close_timeout=schedule_to_close_timeout, + schedule_to_start_timeout=schedule_to_start_timeout, + start_to_close_timeout=start_to_close_timeout, + retry_policy=retry_policy, + local_retry_threshold=local_retry_threshold, + cancellation_type=cancellation_type, + activity_id=activity_id, + summary=summary, + ) + + +# Overload for async no-param activity +@overload +def start_local_activity_class( + activity: type[CallableAsyncNoParam[ReturnType]], + *, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for sync no-param activity +@overload +def start_local_activity_class( + activity: type[CallableSyncNoParam[ReturnType]], + *, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for async single-param activity +@overload +def start_local_activity_class( + activity: type[CallableAsyncSingleParam[ParamType, ReturnType]], + arg: ParamType, + *, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for sync single-param activity +@overload +def start_local_activity_class( + activity: type[CallableSyncSingleParam[ParamType, ReturnType]], + arg: ParamType, + *, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for async multi-param activity +@overload +def start_local_activity_class( + activity: type[Callable[..., Awaitable[ReturnType]]], # type: ignore[reportInvalidTypeForm] + *, + args: Sequence[Any], + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for sync multi-param activity +@overload +def start_local_activity_class( # type: ignore[reportOverlappingOverload] + activity: type[Callable[..., ReturnType]], # type: ignore[reportInvalidTypeForm] + *, + args: Sequence[Any], + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, +) -> ActivityHandle[ReturnType]: ... + + +def start_local_activity_class( + activity: type[Callable], # type: ignore[reportInvalidTypeForm] + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ActivityHandle[Any]: + """Start a local activity from a callable class. + + See :py:meth:`start_local_activity` for parameter and return details. + """ + return _Runtime.current().workflow_start_local_activity( + activity, + *temporalio.common._arg_or_args(arg, args), + result_type=None, + schedule_to_close_timeout=schedule_to_close_timeout, + schedule_to_start_timeout=schedule_to_start_timeout, + start_to_close_timeout=start_to_close_timeout, + retry_policy=retry_policy, + local_retry_threshold=local_retry_threshold, + cancellation_type=cancellation_type, + activity_id=activity_id, + summary=summary, + ) + + +# Overload for async no-param activity +@overload +async def execute_local_activity_class( + activity: type[CallableAsyncNoParam[ReturnType]], + *, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ReturnType: ... + + +# Overload for sync no-param activity +@overload +async def execute_local_activity_class( + activity: type[CallableSyncNoParam[ReturnType]], + *, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ReturnType: ... + + +# Overload for async single-param activity +@overload +async def execute_local_activity_class( + activity: type[CallableAsyncSingleParam[ParamType, ReturnType]], + arg: ParamType, + *, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ReturnType: ... + + +# Overload for sync single-param activity +@overload +async def execute_local_activity_class( + activity: type[CallableSyncSingleParam[ParamType, ReturnType]], + arg: ParamType, + *, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ReturnType: ... + + +# Overload for async multi-param activity +@overload +async def execute_local_activity_class( # type: ignore[reportOverlappingOverload] + activity: type[Callable[..., Awaitable[ReturnType]]], # type: ignore[reportInvalidTypeForm] + *, + args: Sequence[Any], + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ReturnType: ... + + +# Overload for sync multi-param activity +@overload +async def execute_local_activity_class( + activity: type[Callable[..., ReturnType]], # type: ignore[reportInvalidTypeForm] + *, + args: Sequence[Any], + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ReturnType: ... + + +async def execute_local_activity_class( + activity: type[Callable], # type: ignore[reportInvalidTypeForm] + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> Any: + """Start a local activity from a callable class and wait for completion. + + This is a shortcut for ``await`` :py:meth:`start_local_activity_class`. + """ + # We call the runtime directly instead of top-level start_local_activity to + # ensure we don't miss new parameters + return await _Runtime.current().workflow_start_local_activity( + activity, + *temporalio.common._arg_or_args(arg, args), + result_type=None, + schedule_to_close_timeout=schedule_to_close_timeout, + schedule_to_start_timeout=schedule_to_start_timeout, + start_to_close_timeout=start_to_close_timeout, + retry_policy=retry_policy, + local_retry_threshold=local_retry_threshold, + cancellation_type=cancellation_type, + activity_id=activity_id, + summary=summary, + ) + + +# Overload for async no-param activity +@overload +def start_local_activity_method( + activity: MethodAsyncNoParam[SelfType, ReturnType], + *, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for sync no-param activity +@overload +def start_local_activity_method( + activity: MethodSyncNoParam[SelfType, ReturnType], + *, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for async single-param activity +@overload +def start_local_activity_method( + activity: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], + arg: ParamType, + *, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for sync single-param activity +@overload +def start_local_activity_method( + activity: MethodSyncSingleParam[SelfType, ParamType, ReturnType], + arg: ParamType, + *, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for async multi-param activity +@overload +def start_local_activity_method( + activity: Callable[Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType]], + *, + args: Sequence[Any], + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ActivityHandle[ReturnType]: ... + + +# Overload for sync multi-param activity +@overload +def start_local_activity_method( + activity: Callable[Concatenate[SelfType, MultiParamSpec], ReturnType], + *, + args: Sequence[Any], + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ActivityHandle[ReturnType]: ... + + +def start_local_activity_method( + activity: Callable, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ActivityHandle[Any]: + """Start a local activity from a method. + + See :py:meth:`start_local_activity` for parameter and return details. + """ + return _Runtime.current().workflow_start_local_activity( + activity, + *temporalio.common._arg_or_args(arg, args), + result_type=None, + schedule_to_close_timeout=schedule_to_close_timeout, + schedule_to_start_timeout=schedule_to_start_timeout, + start_to_close_timeout=start_to_close_timeout, + retry_policy=retry_policy, + local_retry_threshold=local_retry_threshold, + cancellation_type=cancellation_type, + activity_id=activity_id, + summary=summary, + ) + + +# Overload for async no-param activity +@overload +async def execute_local_activity_method( + activity: MethodAsyncNoParam[SelfType, ReturnType], + *, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ReturnType: ... + + +# Overload for sync no-param activity +@overload +async def execute_local_activity_method( + activity: MethodSyncNoParam[SelfType, ReturnType], + *, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ReturnType: ... + + +# Overload for async single-param activity +@overload +async def execute_local_activity_method( + activity: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], + arg: ParamType, + *, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ReturnType: ... + + +# Overload for sync single-param activity +@overload +async def execute_local_activity_method( + activity: MethodSyncSingleParam[SelfType, ParamType, ReturnType], + arg: ParamType, + *, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ReturnType: ... + + +# Overload for async multi-param activity +@overload +async def execute_local_activity_method( + activity: Callable[Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType]], + *, + args: Sequence[Any], + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ReturnType: ... + + +# Overload for sync multi-param activity +@overload +async def execute_local_activity_method( + activity: Callable[Concatenate[SelfType, MultiParamSpec], ReturnType], + *, + args: Sequence[Any], + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> ReturnType: ... + + +async def execute_local_activity_method( + activity: Callable, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + local_retry_threshold: timedelta | None = None, + cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, + activity_id: str | None = None, + summary: str | None = None, +) -> Any: + """Start a local activity from a method and wait for completion. + + This is a shortcut for ``await`` :py:meth:`start_local_activity_method`. + """ + # We call the runtime directly instead of top-level start_local_activity to + # ensure we don't miss new parameters + return await _Runtime.current().workflow_start_local_activity( + activity, + *temporalio.common._arg_or_args(arg, args), + result_type=None, + schedule_to_close_timeout=schedule_to_close_timeout, + schedule_to_start_timeout=schedule_to_start_timeout, + start_to_close_timeout=start_to_close_timeout, + retry_policy=retry_policy, + local_retry_threshold=local_retry_threshold, + cancellation_type=cancellation_type, + activity_id=activity_id, + summary=summary, + ) diff --git a/temporalio/workflow/_asyncio.py b/temporalio/workflow/_asyncio.py new file mode 100644 index 000000000..5ca7e66d8 --- /dev/null +++ b/temporalio/workflow/_asyncio.py @@ -0,0 +1,180 @@ +from __future__ import annotations + +import asyncio +from collections.abc import Awaitable, Iterable, Iterator +from typing import TYPE_CHECKING, Any, TypeVar, overload + +from ..types import AnyType + +__all__ = [ + "as_completed", + "wait", +] + + +def as_completed( + fs: Iterable[Awaitable[AnyType]], *, timeout: float | None = None +) -> Iterator[Awaitable[AnyType]]: + """Return an iterator whose values are coroutines. + + This is a deterministic version of :py:func:`asyncio.as_completed`. This + function should be used instead of that one in workflows. + """ + # Taken almost verbatim from + # https://github.com/python/cpython/blob/v3.12.3/Lib/asyncio/tasks.py#L584 + # but the "set" is changed out for a "list" and fixed up some typing/format + + if asyncio.isfuture(fs) or asyncio.iscoroutine(fs): + raise TypeError(f"expect an iterable of futures, not {type(fs).__name__}") + + done: asyncio.Queue[asyncio.Future | None] = asyncio.Queue() + + loop = asyncio.get_event_loop() + todo: list[asyncio.Future] = [asyncio.ensure_future(f, loop=loop) for f in list(fs)] + timeout_handle = None + + def _on_timeout(): + for f in todo: + f.remove_done_callback(_on_completion) + done.put_nowait(None) # Queue a dummy value for _wait_for_one(). + todo.clear() # Can't do todo.remove(f) in the loop. + + def _on_completion(f): # type:ignore[reportMissingParameterType] + if not todo: + return # _on_timeout() was here first. + todo.remove(f) + done.put_nowait(f) + if not todo and timeout_handle is not None: + timeout_handle.cancel() + + async def _wait_for_one(): + f = await done.get() + if f is None: + # Dummy value from _on_timeout(). + raise asyncio.TimeoutError + return f.result() # May raise f.exception(). + + for f in todo: + f.add_done_callback(_on_completion) + if todo and timeout is not None: + timeout_handle = loop.call_later(timeout, _on_timeout) + for _ in range(len(todo)): + yield _wait_for_one() + + +if TYPE_CHECKING: + _FT = TypeVar("_FT", bound=asyncio.Future[Any]) +else: + _FT = TypeVar("_FT", bound=asyncio.Future) + + +@overload +async def wait( # type: ignore[misc] + fs: Iterable[_FT], + *, + timeout: float | None = None, + return_when: str = asyncio.ALL_COMPLETED, +) -> tuple[list[_FT], list[_FT]]: ... + + +@overload +async def wait( + fs: Iterable[asyncio.Task[AnyType]], + *, + timeout: float | None = None, + return_when: str = asyncio.ALL_COMPLETED, +) -> tuple[list[asyncio.Task[AnyType]], list[asyncio.Task[AnyType]]]: ... + + +async def wait( + fs: Iterable, + *, + timeout: float | None = None, + return_when: str = asyncio.ALL_COMPLETED, +) -> tuple: + """Wait for the Futures or Tasks given by fs to complete. + + This is a deterministic version of :py:func:`asyncio.wait`. This function + should be used instead of that one in workflows. + """ + # Taken almost verbatim from + # https://github.com/python/cpython/blob/v3.12.3/Lib/asyncio/tasks.py#L435 + # but the "set" is changed out for a "list" and fixed up some typing/format + + if asyncio.isfuture(fs) or asyncio.iscoroutine(fs): + raise TypeError(f"Expect an iterable of Tasks/Futures, not {type(fs).__name__}") + if not fs: + raise ValueError("Sequence of Tasks/Futures must not be empty.") + if return_when not in ( + asyncio.FIRST_COMPLETED, + asyncio.FIRST_EXCEPTION, + asyncio.ALL_COMPLETED, + ): + raise ValueError(f"Invalid return_when value: {return_when}") + + fs = list(fs) + + if any(asyncio.iscoroutine(f) for f in fs): + raise TypeError("Passing coroutines is forbidden, use tasks explicitly.") + + loop = asyncio.get_running_loop() + return await _wait(fs, timeout, return_when, loop) + + +async def _wait( + fs: Iterable[asyncio.Future | asyncio.Task], + timeout: float | None, + return_when: str, + loop: asyncio.AbstractEventLoop, +) -> tuple[list, list]: + # Taken almost verbatim from + # https://github.com/python/cpython/blob/v3.12.3/Lib/asyncio/tasks.py#L522 + # but the "set" is changed out for a "list" and fixed up some typing/format + + assert fs, "Sequence of Tasks/Futures must not be empty." + waiter = loop.create_future() + timeout_handle = None + if timeout is not None: + timeout_handle = loop.call_later(timeout, _release_waiter, waiter) + counter = len(fs) # type: ignore[arg-type] + + def _on_completion(f): # type:ignore[reportMissingParameterType] + nonlocal counter + counter -= 1 + if ( + counter <= 0 + or return_when == asyncio.FIRST_COMPLETED + or return_when == asyncio.FIRST_EXCEPTION + and (not f.cancelled() and f.exception() is not None) + ): + if timeout_handle is not None: + timeout_handle.cancel() + if not waiter.done(): + waiter.set_result(None) + + for f in fs: + f.add_done_callback(_on_completion) + + try: + await waiter + finally: + if timeout_handle is not None: + timeout_handle.cancel() + for f in fs: + f.remove_done_callback(_on_completion) + + done, pending = [], [] + for f in fs: + if f.done(): + done.append(f) + else: + pending.append(f) + return done, pending + + +def _release_waiter(waiter: asyncio.Future[Any], *_args: Any) -> None: + # Taken almost verbatim from + # https://github.com/python/cpython/blob/v3.12.3/Lib/asyncio/tasks.py#L467 + + if not waiter.done(): + waiter.set_result(None) diff --git a/temporalio/workflow/_context.py b/temporalio/workflow/_context.py new file mode 100644 index 000000000..b33f83150 --- /dev/null +++ b/temporalio/workflow/_context.py @@ -0,0 +1,976 @@ +from __future__ import annotations + +import asyncio +import contextvars +import uuid +from abc import ABC, abstractmethod +from collections.abc import Callable, Mapping, Sequence +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +from random import Random +from typing import TYPE_CHECKING, Any, NoReturn, overload + +import nexusrpc +from nexusrpc import InputT, OutputT + +import temporalio.api.common.v1 +import temporalio.common +import temporalio.converter + +from ..types import AnyType, ParamType +from ._exceptions import _NotInWorkflowEventLoopError + +if TYPE_CHECKING: + from ._activities import ActivityCancellationType, ActivityHandle + from ._exceptions import ContinueAsNewVersioningBehavior, VersioningIntent + from ._nexus import NexusOperationCancellationType, NexusOperationHandle + from ._workflow_ops import ( + ChildWorkflowCancellationType, + ChildWorkflowHandle, + ExternalWorkflowHandle, + ParentClosePolicy, + ) + +__all__ = [ + "Info", + "ParentInfo", + "RootInfo", + "UpdateInfo", + "cancellation_reason", + "current_update_info", + "deprecate_patch", + "extern_functions", + "get_current_details", + "get_last_completion_result", + "get_last_failure", + "has_last_completion_result", + "in_workflow", + "info", + "instance", + "is_failure_exception", + "memo", + "memo_value", + "metric_meter", + "new_random", + "now", + "patched", + "payload_converter", + "random", + "random_seed", + "register_random_seed_callback", + "set_current_details", + "sleep", + "time", + "time_ns", + "upsert_memo", + "upsert_search_attributes", + "uuid4", + "uuid7", + "wait_condition", +] + + +@dataclass(frozen=True) +class Info: + """Information about the running workflow. + + Retrieved inside a workflow via :py:func:`info`. This object is immutable + with the exception of the :py:attr:`search_attributes` and + :py:attr:`typed_search_attributes` which is updated on + :py:func:`upsert_search_attributes`. + + Note, required fields may be added here in future versions. This class + should never be constructed by users. + """ + + attempt: int + continued_run_id: str | None + cron_schedule: str | None + execution_timeout: timedelta | None + first_execution_run_id: str + headers: Mapping[str, temporalio.api.common.v1.Payload] + namespace: str + parent: ParentInfo | None + root: RootInfo | None + priority: temporalio.common.Priority + """The priority of this workflow execution. If not set, or this server predates priorities, + then returns a default instance.""" + raw_memo: Mapping[str, temporalio.api.common.v1.Payload] + retry_policy: temporalio.common.RetryPolicy | None + run_id: str + run_timeout: timedelta | None + + search_attributes: temporalio.common.SearchAttributes + """Search attributes for the workflow. + + .. deprecated:: + Use :py:attr:`typed_search_attributes` instead. + """ + + start_time: datetime + """The start time of the first task executed by the workflow.""" + + task_queue: str + task_timeout: timedelta + + typed_search_attributes: temporalio.common.TypedSearchAttributes + """Search attributes for the workflow. + + Note, this may have invalid values or be missing values if passing the + deprecated form of dictionary attributes to + :py:meth:`upsert_search_attributes`. + """ + + workflow_id: str + + workflow_start_time: datetime + """The start time of the workflow based on the workflow initialization.""" + + workflow_type: str + + def _logger_details(self) -> Mapping[str, Any]: + return { + # TODO(cretz): worker ID? + "attempt": self.attempt, + "namespace": self.namespace, + "run_id": self.run_id, + "task_queue": self.task_queue, + "workflow_id": self.workflow_id, + "workflow_type": self.workflow_type, + } + + def get_current_build_id(self) -> str: + """Get the Build ID of the worker which executed the current Workflow Task. + + May be undefined if the task was completed by a worker without a Build ID. If this worker is + the one executing this task for the first time and has a Build ID set, then its ID will be + used. This value may change over the lifetime of the workflow run, but is deterministic and + safe to use for branching. + + .. deprecated:: + Use get_current_deployment_version instead. + """ + return _Runtime.current().workflow_get_current_build_id() + + def get_current_deployment_version( + self, + ) -> temporalio.common.WorkerDeploymentVersion | None: + """Get the deployment version of the worker which executed the current Workflow Task. + + May be None if the task was completed by a worker without a deployment version or build + id. If this worker is the one executing this task for the first time and has a deployment + version set, then its ID will be used. This value may change over the lifetime of the + workflow run, but is deterministic and safe to use for branching. + """ + return _Runtime.current().workflow_get_current_deployment_version() + + def get_current_history_length(self) -> int: + """Get the current number of events in history. + + Note, this value may not be up to date if accessed inside a query. + + Returns: + Current number of events in history (up until the current task). + """ + return _Runtime.current().workflow_get_current_history_length() + + def get_current_history_size(self) -> int: + """Get the current byte size of history. + + Note, this value may not be up to date if accessed inside a query. + + Returns: + Current byte-size of history (up until the current task). + """ + return _Runtime.current().workflow_get_current_history_size() + + def is_continue_as_new_suggested(self) -> bool: + """Get whether or not continue as new is suggested. + + Note, this value may not be up to date if accessed inside a query. + + Returns: + True if the server is configured to suggest continue as new and it + is suggested. + """ + return _Runtime.current().workflow_is_continue_as_new_suggested() + + def is_target_worker_deployment_version_changed(self) -> bool: + """Check whether the target worker deployment version has changed. + + Note: Upgrade-on-Continue-as-New is currently experimental. + + Returns: + True if the target worker deployment version has changed. + """ + return _Runtime.current().workflow_is_target_worker_deployment_version_changed() + + +@dataclass(frozen=True) +class ParentInfo: + """Information about the parent workflow.""" + + namespace: str + run_id: str + workflow_id: str + + +@dataclass(frozen=True) +class RootInfo: + """Information about the root workflow.""" + + run_id: str + workflow_id: str + + +@dataclass(frozen=True) +class UpdateInfo: + """Information about a workflow update.""" + + id: str + """Update ID.""" + + name: str + """Update type name.""" + + @property + def _logger_details(self) -> Mapping[str, Any]: + """Data to be included in string appended to default logging output.""" + return { + "update_id": self.id, + "update_name": self.name, + } + + +class _Runtime(ABC): + @staticmethod + def current() -> _Runtime: + loop = _Runtime.maybe_current() + if not loop: + raise _NotInWorkflowEventLoopError("Not in workflow event loop") + return loop + + @staticmethod + def maybe_current() -> _Runtime | None: + try: + return getattr( + asyncio.get_running_loop(), "__temporal_workflow_runtime", None + ) + except RuntimeError: + return None + + @staticmethod + def set_on_loop(loop: asyncio.AbstractEventLoop, runtime: _Runtime | None) -> None: + if runtime: + setattr(loop, "__temporal_workflow_runtime", runtime) + elif hasattr(loop, "__temporal_workflow_runtime"): + delattr(loop, "__temporal_workflow_runtime") + + def __init__(self) -> None: + super().__init__() + self._logger_details: Mapping[str, Any] | None = None + + @property + def logger_details(self) -> Mapping[str, Any]: + if self._logger_details is None: + self._logger_details = self.workflow_info()._logger_details() + return self._logger_details + + @abstractmethod + def workflow_all_handlers_finished(self) -> bool: ... + + @abstractmethod + def workflow_continue_as_new( + self, + *args: Any, + workflow: None | Callable | str, + task_queue: str | None, + run_timeout: timedelta | None, + task_timeout: timedelta | None, + backoff_start_interval: timedelta | None, + retry_policy: temporalio.common.RetryPolicy | None, + memo: Mapping[str, Any] | None, + search_attributes: None + | ( + temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes + ), + versioning_intent: VersioningIntent | None, + initial_versioning_behavior: ContinueAsNewVersioningBehavior | None, + ) -> NoReturn: ... + + @abstractmethod + def workflow_cancellation_reason(self) -> str | None: ... + + @abstractmethod + def workflow_extern_functions(self) -> Mapping[str, Callable]: ... + + @abstractmethod + def workflow_get_current_build_id(self) -> str: ... + + @abstractmethod + def workflow_get_current_deployment_version( + self, + ) -> temporalio.common.WorkerDeploymentVersion | None: ... + + @abstractmethod + def workflow_get_current_history_length(self) -> int: ... + + @abstractmethod + def workflow_get_current_history_size(self) -> int: ... + + @abstractmethod + def workflow_get_external_workflow_handle( + self, id: str, *, run_id: str | None + ) -> ExternalWorkflowHandle[Any]: ... + + @abstractmethod + def workflow_get_query_handler(self, name: str | None) -> Callable | None: ... + + @abstractmethod + def workflow_get_signal_handler(self, name: str | None) -> Callable | None: ... + + @abstractmethod + def workflow_get_update_handler(self, name: str | None) -> Callable | None: ... + + @abstractmethod + def workflow_get_update_validator(self, name: str | None) -> Callable | None: ... + + @abstractmethod + def workflow_info(self) -> Info: ... + + @abstractmethod + def workflow_instance(self) -> Any: ... + + @abstractmethod + def workflow_is_continue_as_new_suggested(self) -> bool: ... + + @abstractmethod + def workflow_is_target_worker_deployment_version_changed(self) -> bool: ... + + @abstractmethod + def workflow_is_replaying(self) -> bool: ... + + @abstractmethod + def workflow_is_replaying_history_events(self) -> bool: ... + + @abstractmethod + def workflow_is_read_only(self) -> bool: ... + + @abstractmethod + def workflow_memo(self) -> Mapping[str, Any]: ... + + @abstractmethod + def workflow_memo_value( + self, key: str, default: Any, *, type_hint: type | None + ) -> Any: ... + + @abstractmethod + def workflow_upsert_memo(self, updates: Mapping[str, Any]) -> None: ... + + @abstractmethod + def workflow_metric_meter(self) -> temporalio.common.MetricMeter: ... + + @abstractmethod + def workflow_patch(self, id: str, *, deprecated: bool) -> bool: ... + + @abstractmethod + def workflow_payload_converter(self) -> temporalio.converter.PayloadConverter: ... + + @abstractmethod + def workflow_random(self) -> Random: ... + + @abstractmethod + def workflow_set_query_handler( + self, name: str | None, handler: Callable | None + ) -> None: ... + + @abstractmethod + def workflow_set_signal_handler( + self, name: str | None, handler: Callable | None + ) -> None: ... + + @abstractmethod + def workflow_set_update_handler( + self, + name: str | None, + handler: Callable | None, + validator: Callable | None, + ) -> None: ... + + @abstractmethod + def workflow_start_activity( + self, + activity: Any, + *args: Any, + task_queue: str | None, + result_type: type | None, + schedule_to_close_timeout: timedelta | None, + schedule_to_start_timeout: timedelta | None, + start_to_close_timeout: timedelta | None, + heartbeat_timeout: timedelta | None, + retry_policy: temporalio.common.RetryPolicy | None, + cancellation_type: ActivityCancellationType, + activity_id: str | None, + versioning_intent: VersioningIntent | None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + ) -> ActivityHandle[Any]: ... + + @abstractmethod + async def workflow_start_child_workflow( + self, + workflow: Any, + *args: Any, + id: str, + task_queue: str | None, + result_type: type | None, + cancellation_type: ChildWorkflowCancellationType, + parent_close_policy: ParentClosePolicy, + execution_timeout: timedelta | None, + run_timeout: timedelta | None, + task_timeout: timedelta | None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy, + retry_policy: temporalio.common.RetryPolicy | None, + cron_schedule: str, + memo: Mapping[str, Any] | None, + search_attributes: None + | ( + temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes + ), + versioning_intent: VersioningIntent | None, + static_summary: str | None = None, + static_details: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + ) -> ChildWorkflowHandle[Any, Any]: ... + + @abstractmethod + def workflow_start_local_activity( + self, + activity: Any, + *args: Any, + result_type: type | None, + schedule_to_close_timeout: timedelta | None, + schedule_to_start_timeout: timedelta | None, + start_to_close_timeout: timedelta | None, + retry_policy: temporalio.common.RetryPolicy | None, + local_retry_threshold: timedelta | None, + cancellation_type: ActivityCancellationType, + activity_id: str | None, + summary: str | None, + ) -> ActivityHandle[Any]: ... + + @abstractmethod + async def workflow_start_nexus_operation( + self, + endpoint: str, + service: str, + operation: nexusrpc.Operation[InputT, OutputT] | str | Callable[..., Any], + input: Any, + output_type: type[OutputT] | None, + schedule_to_close_timeout: timedelta | None, + schedule_to_start_timeout: timedelta | None, + start_to_close_timeout: timedelta | None, + cancellation_type: NexusOperationCancellationType, + headers: Mapping[str, str] | None, + summary: str | None, + ) -> NexusOperationHandle[OutputT]: ... + + @abstractmethod + def workflow_time_ns(self) -> int: ... + + @abstractmethod + def workflow_upsert_search_attributes( + self, + attributes: ( + temporalio.common.SearchAttributes + | Sequence[temporalio.common.SearchAttributeUpdate] + ), + ) -> None: ... + + @abstractmethod + async def workflow_sleep( + self, duration: float, *, summary: str | None = None + ) -> None: ... + + @abstractmethod + async def workflow_wait_condition( + self, + fn: Callable[[], bool], + *, + timeout: float | None = None, + timeout_summary: str | None = None, + ) -> None: ... + + @abstractmethod + def workflow_get_current_details(self) -> str: ... + + @abstractmethod + def workflow_set_current_details(self, details: str): ... + + @abstractmethod + def workflow_is_failure_exception(self, err: BaseException) -> bool: ... + + @abstractmethod + def workflow_has_last_completion_result(self) -> bool: ... + + @abstractmethod + def workflow_last_completion_result(self, type_hint: type | None) -> Any | None: ... + + @abstractmethod + def workflow_last_failure(self) -> BaseException | None: ... + + @abstractmethod + def workflow_random_seed(self) -> int: ... + + @abstractmethod + def workflow_register_random_seed_callback( + self, callback: Callable[[int], None] + ) -> None: ... + + +_current_update_info: contextvars.ContextVar[UpdateInfo] = contextvars.ContextVar( + "__temporal_current_update_info" +) + + +def _set_current_update_info(info: UpdateInfo) -> None: # type: ignore[reportUnusedFunction] + _current_update_info.set(info) + + +def current_update_info() -> UpdateInfo | None: + """Info for the current update if any. + + This is powered by :py:mod:`contextvars` so it is only valid within the + update handler and coroutines/tasks it has started. + + Returns: + Info for the current update handler the code calling this is executing + within if any. + """ + return _current_update_info.get(None) + + +def deprecate_patch(id: str) -> None: + """Mark a patch as deprecated. + + This marks a workflow that had :py:func:`patched` in a previous version of + the code as no longer applicable because all workflows that use the old code + path are done and will never be queried again. Therefore the old code path + is removed as well. + + Args: + id: The identifier originally used with :py:func:`patched`. + """ + _Runtime.current().workflow_patch(id, deprecated=True) + + +def extern_functions() -> Mapping[str, Callable]: + """External functions available in the workflow sandbox. + + Returns: + Mapping of external functions that can be called from inside a workflow + sandbox. + """ + return _Runtime.current().workflow_extern_functions() + + +def info() -> Info: + """Current workflow's info. + + Returns: + Info for the currently running workflow. + """ + return _Runtime.current().workflow_info() + + +def instance() -> Any: + """Current workflow's instance. + + Returns: + The currently running workflow instance. + """ + return _Runtime.current().workflow_instance() + + +def in_workflow() -> bool: + """Whether the code is currently running in a workflow.""" + return _Runtime.maybe_current() is not None + + +def cancellation_reason() -> str | None: + """Reason the workflow was cancelled, or None if no external cancellation + request has been received. + + A non-None value (including an empty string) indicates that the workflow + received an explicit cancellation request from the server. This can be used + when catching an :py:class:`asyncio.CancelledError` to distinguish a + workflow-level cancel from a cancel that originated from inner asyncio task + cancellation. + + Note, this only reflects cancellation requested via the server; it is not + set for cache eviction or for cancels of inner tasks/scopes. + + Returns: + The reason string sent with the workflow cancellation request (which + may be empty), or ``None`` if the workflow has not been cancelled via + an external request. + """ + return _Runtime.current().workflow_cancellation_reason() + + +def memo() -> Mapping[str, Any]: + """Current workflow's memo values, converted without type hints. + + Since type hints are not used, the default converted values will come back. + For example, if the memo was originally created with a dataclass, the value + will be a dict. To convert using proper type hints, use + :py:func:`memo_value`. + + Returns: + Mapping of all memo keys and they values without type hints. + """ + return _Runtime.current().workflow_memo() + + +def is_failure_exception(err: BaseException) -> bool: + """Checks if the given exception is a workflow failure in the current workflow. + + Returns: + True if the given exception is a workflow failure in the current workflow. + """ + return _Runtime.current().workflow_is_failure_exception(err) + + +@overload +def memo_value(key: str, default: Any = temporalio.common._arg_unset) -> Any: ... + + +@overload +def memo_value(key: str, *, type_hint: type[ParamType]) -> ParamType: ... + + +@overload +def memo_value( + key: str, default: AnyType, *, type_hint: type[ParamType] +) -> AnyType | ParamType: ... + + +def memo_value( + key: str, + default: Any = temporalio.common._arg_unset, + *, + type_hint: type | None = None, +) -> Any: + """Memo value for the given key, optional default, and optional type + hint. + + Args: + key: Key to get memo value for. + default: Default to use if key is not present. If unset, a + :py:class:`KeyError` is raised when the key does not exist. + type_hint: Type hint to use when converting. + + Returns: + Memo value, converted with the type hint if present. + + Raises: + KeyError: Key not present and default not set. + """ + return _Runtime.current().workflow_memo_value(key, default, type_hint=type_hint) + + +def upsert_memo(updates: Mapping[str, Any]) -> None: + """Adds, modifies, and/or removes memos, with upsert semantics. + + Every memo that has a matching key has its value replaced with the one specified in ``updates``. + If the value is set to ``None``, the memo is removed instead. + For every key with no existing memo, a new memo is added with specified value (unless the value is ``None``). + Memos with keys not included in ``updates`` remain unchanged. + """ + return _Runtime.current().workflow_upsert_memo(updates) + + +def get_current_details() -> str: + """Get the current details of the workflow which may appear in the UI/CLI. + Unlike static details set at start, this value can be updated throughout + the life of the workflow and is independent of the static details. + This can be in Temporal markdown format and can span multiple lines. + """ + return _Runtime.current().workflow_get_current_details() + + +def has_last_completion_result() -> bool: + """Gets whether there is a last completion result of the workflow.""" + return _Runtime.current().workflow_has_last_completion_result() + + +@overload +def get_last_completion_result() -> Any | None: ... + + +@overload +def get_last_completion_result(type_hint: type[ParamType]) -> ParamType | None: ... + + +def get_last_completion_result(type_hint: type | None = None) -> Any | None: + """Get the result of the last run of the workflow. This will be None if there was + no previous completion or the result was None. has_last_completion_result() + can be used to differentiate. + """ + return _Runtime.current().workflow_last_completion_result(type_hint) + + +def get_last_failure() -> BaseException | None: + """Get the last failure of the workflow if it has run previously.""" + return _Runtime.current().workflow_last_failure() + + +def set_current_details(description: str) -> None: + """Set the current details of the workflow which may appear in the UI/CLI. + Unlike static details set at start, this value can be updated throughout + the life of the workflow and is independent of the static details. + This can be in Temporal markdown format and can span multiple lines. + """ + _Runtime.current().workflow_set_current_details(description) + + +def metric_meter() -> temporalio.common.MetricMeter: + """Get the metric meter for the current workflow. + + This meter is replay safe which means that metrics will not be recorded + during replay. + + Returns: + Current metric meter for this workflow for recording metrics. + """ + return _Runtime.current().workflow_metric_meter() + + +def now() -> datetime: + """Current time from the workflow perspective. + + This is the workflow equivalent of :py:func:`datetime.now` with the + :py:attr:`timezone.utc` parameter. + + Returns: + UTC datetime for the current workflow time. The datetime does have UTC + set as the time zone. + """ + return datetime.fromtimestamp(time(), timezone.utc) + + +def patched(id: str) -> bool: + """Patch a workflow. + + When called, this will only return true if code should take the newer path + which means this is either not replaying or is replaying and has seen this + patch before. + + Use :py:func:`deprecate_patch` when all workflows are done and will never be + queried again. The old code path can be used at that time too. + + Args: + id: The identifier for this patch. This identifier may be used + repeatedly in the same workflow to represent the same patch + + Returns: + True if this should take the newer path, false if it should take the + older path. + """ + return _Runtime.current().workflow_patch(id, deprecated=False) + + +def payload_converter() -> temporalio.converter.PayloadConverter: + """Get the payload converter for the current workflow. + + The returned converter has :py:class:`temporalio.converter.WorkflowSerializationContext` set. + This is often used for dynamic workflows/signals/queries to convert + payloads. + """ + return _Runtime.current().workflow_payload_converter() + + +def random() -> Random: + """Get a deterministic pseudo-random number generator. + + Note, this random number generator is not cryptographically safe and should + not be used for security purposes. + + Returns: + The deterministically-seeded pseudo-random number generator. + """ + return _Runtime.current().workflow_random() + + +def random_seed() -> int: + """Get the current random seed value from core. + + This returns the seed value currently being used by the workflow's + deterministic random number generator. + + Returns: + The current random seed as an integer. + """ + return _Runtime.current().workflow_random_seed() + + +def register_random_seed_callback(callback: Callable[[int], None]) -> None: + """Register a callback to be notified when the random seed changes. + + The callback will be invoked whenever the workflow receives a new random + seed from the core. This is useful for maintaining external random number + generators that need to stay in sync with the workflow's randomness. + + Args: + callback: Function to be called with the new seed value when it changes. + """ + return _Runtime.current().workflow_register_random_seed_callback(callback) + + +def new_random() -> Random: + """Create a Random instance that automatically reseeds when the workflow seed changes. + + This creates a new Random instance that is initially seeded with the current + workflow seed, and automatically registers a callback to reseed itself + whenever the workflow receives a new seed from core. + + Returns: + A Random instance that stays synchronized with the workflow's randomness. + """ + current_seed = random_seed() + auto_random = Random(current_seed) + + def reseed_callback(new_seed: int) -> None: + auto_random.seed(new_seed) + + register_random_seed_callback(reseed_callback) + return auto_random + + +def time() -> float: + """Current seconds since the epoch from the workflow perspective. + + This is the workflow equivalent of :py:func:`time.time`. + + Returns: + Seconds since the epoch as a float. + """ + return time_ns() / 1e9 + + +def time_ns() -> int: + """Current nanoseconds since the epoch from the workflow perspective. + + This is the workflow equivalent of :py:func:`time.time_ns`. + + Returns: + Nanoseconds since the epoch + """ + return _Runtime.current().workflow_time_ns() + + +def upsert_search_attributes( + attributes: ( + temporalio.common.SearchAttributes + | Sequence[temporalio.common.SearchAttributeUpdate] + ), +) -> None: + """Upsert search attributes for this workflow. + + Args: + attributes: The attributes to set. This should be a sequence of + updates (i.e. values created via value_set and value_unset calls on + search attribute keys). The dictionary form of attributes is + DEPRECATED and if used, result in invalid key types on the + typed_search_attributes property in the info. + """ + if not attributes: + return + temporalio.common._warn_on_deprecated_search_attributes(attributes) + _Runtime.current().workflow_upsert_search_attributes(attributes) + + +def uuid4() -> uuid.UUID: + """Get a new, determinism-safe v4 UUID based on :py:func:`random`. + + Note, this UUID is not cryptographically safe and should not be used for + security purposes. + + Returns: + A deterministically-seeded v4 UUID. + """ + return uuid.UUID(bytes=random().getrandbits(16 * 8).to_bytes(16, "big"), version=4) + + +def uuid7() -> uuid.UUID: + """Get a new, determinism-safe v7 UUID based on :py:func:`time_ns` and + :py:func:`random`. + + Per RFC 9562, the UUID's leading 48 bits are the current workflow time as + milliseconds since the epoch, so UUIDs from successive workflow tasks sort + by creation time. The remaining 74 bits are random. UUIDs generated within + the same workflow task share the same workflow time and are not guaranteed + to be monotonically ordered with respect to one another. + + Note, this UUID is not cryptographically safe and should not be used for + security purposes. + + Returns: + A deterministically-seeded v7 UUID. + """ + # uuid.UUID's version parameter only accepts 1-5 before Python 3.14, so + # the version and variant bits are set manually. + unix_ts_ms = (time_ns() // 1_000_000) & 0xFFFF_FFFF_FFFF + rand = random() + rand_a = rand.getrandbits(12) + rand_b = rand.getrandbits(62) + return uuid.UUID( + int=(unix_ts_ms << 80) | (0x7 << 76) | (rand_a << 64) | (0b10 << 62) | rand_b + ) + + +async def sleep(duration: float | timedelta, *, summary: str | None = None) -> None: + """Sleep for the given duration. + + Args: + duration: Duration to sleep in seconds or as a timedelta. + summary: A single-line fixed summary for this timer that may appear in UI/CLI. + This can be in single-line Temporal markdown format. + """ + await _Runtime.current().workflow_sleep( + duration=( + duration.total_seconds() if isinstance(duration, timedelta) else duration + ), + summary=summary, + ) + + +async def wait_condition( + fn: Callable[[], bool], + *, + timeout: timedelta | float | None = None, + timeout_summary: str | None = None, +) -> None: + """Wait on a callback to become true. + + This function returns when the callback returns true (invoked each loop + iteration) or the timeout has been reached. + + Importantly, using `None` value for ``timeout`` means that no Temporal timer is created. This + means changing from `None` to a positive value (or the inverse) constitutes a nondeterministic + change to workflow code. Using ``0`` will cause this function to immediately throw a timeout and + should not be used. + + Args: + fn: Non-async callback that accepts no parameters and returns a boolean. + timeout: Optional number of seconds to wait until throwing + :py:class:`asyncio.TimeoutError`. + timeout_summary: Optional simple string identifying the timer (created if ``timeout`` is + present) that may be visible in UI/CLI. While it can be normal text, it is best to treat + as a timer ID. + """ + await _Runtime.current().workflow_wait_condition( + fn, + timeout=timeout.total_seconds() if isinstance(timeout, timedelta) else timeout, + timeout_summary=timeout_summary, + ) diff --git a/temporalio/workflow/_definition.py b/temporalio/workflow/_definition.py new file mode 100644 index 000000000..a14e7640a --- /dev/null +++ b/temporalio/workflow/_definition.py @@ -0,0 +1,466 @@ +from __future__ import annotations + +import inspect +from collections.abc import Awaitable, Callable, Mapping, Sequence +from dataclasses import dataclass +from typing import Any, cast, overload + +import temporalio.common + +from ..types import ( + CallableAsyncType, + CallableType, + ClassType, + MethodSyncNoParam, + SelfType, +) +from ._handlers import ( + UpdateMethodMultiParam, + _QueryDefinition, + _SignalDefinition, + _UpdateDefinition, +) + +__all__ = [ + "DynamicWorkflowConfig", + "defn", + "dynamic_config", + "init", + "run", +] + + +@overload +def defn(cls: ClassType) -> ClassType: ... + + +@overload +def defn( + *, + name: str | None = None, + sandboxed: bool = True, + failure_exception_types: Sequence[type[BaseException]] = [], + versioning_behavior: temporalio.common.VersioningBehavior = temporalio.common.VersioningBehavior.UNSPECIFIED, +) -> Callable[[ClassType], ClassType]: ... + + +@overload +def defn( + *, + sandboxed: bool = True, + dynamic: bool = False, + versioning_behavior: temporalio.common.VersioningBehavior = temporalio.common.VersioningBehavior.UNSPECIFIED, +) -> Callable[[ClassType], ClassType]: ... + + +def defn( + cls: ClassType | None = None, + *, + name: str | None = None, + sandboxed: bool = True, + dynamic: bool = False, + failure_exception_types: Sequence[type[BaseException]] = [], + versioning_behavior: temporalio.common.VersioningBehavior = temporalio.common.VersioningBehavior.UNSPECIFIED, +) -> Callable[[ClassType], ClassType]: + """Decorator for workflow classes. + + This must be set on any registered workflow class (it is ignored if on a + base class). + + Args: + cls: The class to decorate. + name: Name to use for the workflow. Defaults to class ``__name__``. This + cannot be set if dynamic is set. + sandboxed: Whether the workflow should run in a sandbox. Default is + true. + dynamic: If true, this workflow will be dynamic. Dynamic workflows have + to accept a single 'Sequence[RawValue]' parameter. This cannot be + set to true if name is present. + failure_exception_types: The types of exceptions that, if a + workflow-thrown exception extends, will cause the workflow/update to + fail instead of suspending the workflow via task failure. These are + applied in addition to ones set on the worker constructor. If + ``Exception`` is set, it effectively will fail a workflow/update in + all user exception cases. WARNING: This setting is experimental. + versioning_behavior: Specifies the versioning behavior to use for this workflow. + """ + + def decorator(cls: ClassType) -> ClassType: + # This performs validation + _Definition._apply_to_class( + cls, + workflow_name=name or cls.__name__ if not dynamic else None, + sandboxed=sandboxed, + failure_exception_types=failure_exception_types, + versioning_behavior=versioning_behavior, + ) + return cls + + if cls is not None: + return decorator(cls) + return decorator + + +def init( + init_fn: CallableType, +) -> CallableType: + """Decorator for the workflow init method. + + This may be used on the __init__ method of the workflow class to specify + that it accepts the same workflow input arguments as the ``@workflow.run`` + method. If used, the parameters of your __init__ and ``@workflow.run`` + methods must be identical. + + Args: + init_fn: The __init__ method to decorate. + """ + if init_fn.__name__ != "__init__": + raise ValueError("@workflow.init may only be used on the __init__ method") + + setattr(init_fn, "__temporal_workflow_init", True) + return init_fn + + +def run(fn: CallableAsyncType) -> CallableAsyncType: + """Decorator for the workflow run method. + + This must be used on one and only one async method defined on the same class + as ``@workflow.defn``. This can be defined on a base class method but must + then be explicitly overridden and defined on the workflow class. + + Run methods can only have positional parameters. Best practice is to only + take a single object/dataclass argument that can accept more fields later if + needed. + + Args: + fn: The function to decorate. + """ + if not inspect.iscoroutinefunction(fn): + raise ValueError("Workflow run method must be an async function") + # Disallow local classes because we need to have the class globally + # referenceable by name + if "" in fn.__qualname__: + raise ValueError( + "Local classes unsupported, @workflow.run cannot be on a local class" + ) + setattr(fn, "__temporal_workflow_run", True) + # TODO(cretz): Why is MyPy unhappy with this return? + return fn # type: ignore[return-value] + + +@dataclass(frozen=True) +class DynamicWorkflowConfig: + """Returned by functions using the :py:func:`dynamic_config` decorator, see it for more.""" + + failure_exception_types: Sequence[type[BaseException]] | None = None + """The types of exceptions that, if a workflow-thrown exception extends, will cause the + workflow/update to fail instead of suspending the workflow via task failure. These are applied + in addition to ones set on the worker constructor. If ``Exception`` is set, it effectively will + fail a workflow/update in all user exception cases. + + Always overrides the equivalent parameter on :py:func:`defn` if set not-None. + + WARNING: This setting is experimental. + """ + versioning_behavior: temporalio.common.VersioningBehavior = ( + temporalio.common.VersioningBehavior.UNSPECIFIED + ) + """Specifies the versioning behavior to use for this workflow. + + Always overrides the equivalent parameter on :py:func:`defn`. + """ + + +def dynamic_config( + fn: MethodSyncNoParam[SelfType, DynamicWorkflowConfig], +) -> MethodSyncNoParam[SelfType, DynamicWorkflowConfig]: + """Decorator to allow configuring a dynamic workflow's behavior. + + Because dynamic workflows may conceptually represent more than one workflow type, it may be + desirable to have different settings for fields that would normally be passed to + :py:func:`defn`, but vary based on the workflow type name or other information available in + the workflow's context. This function will be called after the workflow's :py:func:`init`, + if it has one, but before the workflow's :py:func:`run` method. + + The method must only take self as a parameter, and any values set in the class it returns will + override those provided to :py:func:`defn`. + + Cannot be specified on non-dynamic workflows. + + Args: + fn: The function to decorate. + """ + if inspect.iscoroutinefunction(fn): + raise ValueError("Workflow dynamic_config method must be synchronous") + params = list(inspect.signature(fn).parameters.values()) + if len(params) != 1: + raise ValueError("Workflow dynamic_config method must only take self parameter") + + # Add marker attribute + setattr(fn, "__temporal_workflow_dynamic_config", True) + return fn + + +@dataclass(frozen=True) +class _Definition: + name: str | None + cls: type + run_fn: Callable[..., Awaitable] + signals: Mapping[str | None, _SignalDefinition] + queries: Mapping[str | None, _QueryDefinition] + updates: Mapping[str | None, _UpdateDefinition] + sandboxed: bool + failure_exception_types: Sequence[type[BaseException]] + # Types loaded on post init if both are None + arg_types: list[type] | None = None + ret_type: type | None = None + versioning_behavior: temporalio.common.VersioningBehavior | None = None + dynamic_config_fn: Callable[..., DynamicWorkflowConfig] | None = None + + @staticmethod + def from_class(cls: type) -> _Definition | None: # type: ignore[reportSelfClsParameterName] + # We make sure to only return it if it's on _this_ class + defn = getattr(cls, "__temporal_workflow_definition", None) + if defn and defn.cls == cls: + return defn + return None + + @staticmethod + def must_from_class(cls: type) -> _Definition: # type: ignore[reportSelfClsParameterName] + ret = _Definition.from_class(cls) + if ret: + return ret + cls_name = getattr(cls, "__name__", "") + raise ValueError( + f"Workflow {cls_name} missing attributes, was it decorated with @workflow.defn?" + ) + + @staticmethod + def from_run_fn(fn: Callable[..., Awaitable[Any]]) -> _Definition | None: + return getattr(fn, "__temporal_workflow_definition", None) + + @staticmethod + def must_from_run_fn(fn: Callable[..., Awaitable[Any]]) -> _Definition: + ret = _Definition.from_run_fn(fn) + if ret: + return ret + fn_name = getattr(fn, "__qualname__", "") + raise ValueError( + f"Function {fn_name} missing attributes, was it decorated with @workflow.run and was its class decorated with @workflow.defn?" + ) + + @classmethod + def get_name_and_result_type( + cls, name_or_run_fn: str | Callable[..., Awaitable[Any]] + ) -> tuple[str, type | None]: + if isinstance(name_or_run_fn, str): + return name_or_run_fn, None + elif callable(name_or_run_fn): + defn = cls.must_from_run_fn(name_or_run_fn) + if not defn.name: + raise ValueError("Cannot invoke dynamic workflow explicitly") + return defn.name, defn.ret_type + else: + raise TypeError("Workflow must be a string or callable") # type: ignore[reportUnreachable] + + @staticmethod + def _apply_to_class( + cls: type, # type: ignore[reportSelfClsParameterName] + *, + workflow_name: str | None, + sandboxed: bool, + failure_exception_types: Sequence[type[BaseException]], + versioning_behavior: temporalio.common.VersioningBehavior, + ) -> None: + # Check it's not being doubly applied + if _Definition.from_class(cls): + raise ValueError("Class already contains workflow definition") + issues: list[str] = [] + + # Collect run fn and all signal/query/update fns + init_fn: Callable[..., None] | None = None + run_fn: Callable[..., Awaitable[Any]] | None = None + dynamic_config_fn: Callable[..., DynamicWorkflowConfig] | None = None + seen_run_attr = False + signals: dict[str | None, _SignalDefinition] = {} + queries: dict[str | None, _QueryDefinition] = {} + updates: dict[str | None, _UpdateDefinition] = {} + for name, member in inspect.getmembers(cls): + if hasattr(member, "__temporal_workflow_run"): + seen_run_attr = True + if not _is_unbound_method_on_cls(member, cls): + issues.append( + f"@workflow.run method {name} must be defined on {cls.__qualname__}" + ) + elif run_fn is not None: + issues.append( + f"Multiple @workflow.run methods found (at least on {name} and {run_fn.__name__})" + ) + else: + # We can guarantee the @workflow.run decorator did + # validation of the function itself + run_fn = member + elif hasattr(member, "__temporal_signal_definition"): + signal_defn = cast( + _SignalDefinition, getattr(member, "__temporal_signal_definition") + ) + if signal_defn.name in signals: + defn_name = signal_defn.name or "" + # TODO(cretz): Remove cast when https://github.com/python/mypy/issues/5485 fixed + other_fn = cast(Callable, signals[signal_defn.name].fn) + issues.append( + f"Multiple signal methods found for {defn_name} " + f"(at least on {name} and {other_fn.__name__})" + ) + else: + signals[signal_defn.name] = signal_defn + elif hasattr(member, "__temporal_query_definition"): + query_defn = cast( + _QueryDefinition, getattr(member, "__temporal_query_definition") + ) + if query_defn.name in queries: + defn_name = query_defn.name or "" + issues.append( + f"Multiple query methods found for {defn_name} " + f"(at least on {name} and {queries[query_defn.name].fn.__name__})" + ) + else: + queries[query_defn.name] = query_defn + elif name == "__init__" and hasattr(member, "__temporal_workflow_init"): + init_fn = member + elif hasattr(member, "__temporal_workflow_dynamic_config"): + if workflow_name: + issues.append( + "@workflow.dynamic_config can only be used in dynamic workflows, but " + f"workflow class {workflow_name} ({cls.__name__}) is not dynamic" + ) + if dynamic_config_fn: + issues.append( + "@workflow.dynamic_config can only be defined once per workflow" + ) + dynamic_config_fn = member + elif isinstance(member, UpdateMethodMultiParam): + update_defn = member._defn + if update_defn.name in updates: + defn_name = update_defn.name or "" + issues.append( + f"Multiple update methods found for {defn_name} " + f"(at least on {name} and {updates[update_defn.name].fn.__name__})" + ) + elif update_defn.validator and not _parameters_identical_up_to_naming( + update_defn.fn, update_defn.validator + ): + issues.append( + f"Update validator method {update_defn.validator.__name__} parameters " + f"do not match update method {update_defn.fn.__name__} parameters" + ) + else: + updates[update_defn.name] = update_defn + + # Check base classes haven't defined things with different decorators + for base_cls in inspect.getmro(cls)[1:]: + for _, base_member in inspect.getmembers(base_cls): + # We only care about methods defined on this class + if not inspect.isfunction(base_member) or not _is_unbound_method_on_cls( + base_member, base_cls + ): + continue + if hasattr(base_member, "__temporal_workflow_run"): + seen_run_attr = True + if not run_fn or base_member.__name__ != run_fn.__name__: + issues.append( + f"@workflow.run defined on {base_member.__qualname__} but not on the override" + ) + elif hasattr(base_member, "__temporal_signal_definition"): + signal_defn = cast( + _SignalDefinition, + getattr(base_member, "__temporal_signal_definition"), + ) + if signal_defn.name not in signals: + issues.append( + f"@workflow.signal defined on {base_member.__qualname__} but not on the override" + ) + elif hasattr(base_member, "__temporal_query_definition"): + query_defn = cast( + _QueryDefinition, + getattr(base_member, "__temporal_query_definition"), + ) + if query_defn.name not in queries: + issues.append( + f"@workflow.query defined on {base_member.__qualname__} but not on the override" + ) + elif isinstance(base_member, UpdateMethodMultiParam): + update_defn = base_member._defn + if update_defn.name not in updates: + issues.append( + f"@workflow.update defined on {base_member.__qualname__} but not on the override" + ) + + if not seen_run_attr: + issues.append("Missing @workflow.run method") + if init_fn and run_fn: + if not _parameters_identical_up_to_naming(init_fn, run_fn): + issues.append( + "@workflow.init and @workflow.run method parameters do not match" + ) + if issues: + if len(issues) == 1: + raise ValueError(f"Invalid workflow class: {issues[0]}") + raise ValueError( + f"Invalid workflow class for {len(issues)} reasons: {', '.join(issues)}" + ) + + assert run_fn + assert seen_run_attr + defn = _Definition( + name=workflow_name, + cls=cls, + run_fn=run_fn, + signals=signals, + queries=queries, + updates=updates, + sandboxed=sandboxed, + failure_exception_types=failure_exception_types, + versioning_behavior=versioning_behavior, + dynamic_config_fn=dynamic_config_fn, + ) + setattr(cls, "__temporal_workflow_definition", defn) + setattr(run_fn, "__temporal_workflow_definition", defn) + + def __post_init__(self) -> None: + if self.arg_types is None and self.ret_type is None: + dynamic = self.name is None + arg_types, ret_type = temporalio.common._type_hints_from_func(self.run_fn) + # If dynamic, must be a sequence of raw values + if dynamic and ( + not arg_types + or len(arg_types) != 1 + or arg_types[0] != Sequence[temporalio.common.RawValue] + ): + raise TypeError( + "Dynamic workflow must accept a single Sequence[temporalio.common.RawValue]" + ) + object.__setattr__(self, "arg_types", arg_types) + object.__setattr__(self, "ret_type", ret_type) + + +def _parameters_identical_up_to_naming(fn1: Callable, fn2: Callable) -> bool: + """Return True if the functions have identical parameter lists, ignoring parameter names.""" + + def params(fn: Callable) -> list[inspect.Parameter]: + # Ignore name when comparing parameters (remaining fields are kind, + # default, and annotation). + return [p.replace(name="x") for p in inspect.signature(fn).parameters.values()] + + # We require that any type annotations present match exactly; i.e. we do + # not support any notion of subtype compatibility. + return params(fn1) == params(fn2) + + +def _is_unbound_method_on_cls(fn: Callable[..., Any], cls: type) -> bool: + # Python 3 does not make this easy, ref https://stackoverflow.com/questions/3589311 + return ( + inspect.isfunction(fn) + and inspect.getmodule(fn) is inspect.getmodule(cls) + and fn.__qualname__.rsplit(".", 1)[0] == cls.__name__ + ) diff --git a/temporalio/workflow/_exceptions.py b/temporalio/workflow/_exceptions.py new file mode 100644 index 000000000..2d34c2fed --- /dev/null +++ b/temporalio/workflow/_exceptions.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +from enum import Enum, IntEnum + +import temporalio.api.enums.v1 +import temporalio.bridge.proto.common +import temporalio.exceptions + +__all__ = [ + "NondeterminismError", + "ReadOnlyContextError", + "VersioningIntent", + "ContinueAsNewVersioningBehavior", +] + + +class NondeterminismError(temporalio.exceptions.TemporalError): + """Error that can be thrown during replay for non-deterministic workflow.""" + + def __init__(self, message: str) -> None: + """Initialize a nondeterminism error.""" + super().__init__(message) + self.message = message + + +class ReadOnlyContextError(temporalio.exceptions.TemporalError): + """Error thrown when trying to do mutable workflow calls in a read-only + context like a query or update validator. + """ + + def __init__(self, message: str) -> None: + """Initialize a read-only context error.""" + super().__init__(message) + self.message = message + + +class _NotInWorkflowEventLoopError( # pyright: ignore[reportUnusedClass] + temporalio.exceptions.TemporalError +): + def __init__(self, *args: object) -> None: + super().__init__("Not in workflow event loop") + self.message = "Not in workflow event loop" + + +class VersioningIntent(Enum): + """Indicates whether the user intends certain commands to be run on a compatible worker Build + Id version or not. + + `COMPATIBLE` indicates that the command should run on a worker with compatible version if + possible. It may not be possible if the target task queue does not also have knowledge of the + current worker's Build Id. + + `DEFAULT` indicates that the command should run on the target task queue's current + overall-default Build Id. + + Where this type is accepted optionally, an unset value indicates that the SDK should choose the + most sensible default behavior for the type of command, accounting for whether the command will + be run on the same task queue as the current worker. + + .. deprecated:: + Use Worker Deployment versioning instead. + """ + + COMPATIBLE = 1 + DEFAULT = 2 + + def _to_proto(self) -> temporalio.bridge.proto.common.VersioningIntent.ValueType: + if self == VersioningIntent.COMPATIBLE: + return temporalio.bridge.proto.common.VersioningIntent.COMPATIBLE + elif self == VersioningIntent.DEFAULT: + return temporalio.bridge.proto.common.VersioningIntent.DEFAULT + return temporalio.bridge.proto.common.VersioningIntent.UNSPECIFIED + + +class ContinueAsNewVersioningBehavior(IntEnum): + """Experimental. Optionally decide the versioning behavior that the first task of the new run should use. + For example, choose to AutoUpgrade on continue-as-new instead of inheriting the pinned version + of the previous run. + """ + + UNSPECIFIED = int( + temporalio.api.enums.v1.ContinueAsNewVersioningBehavior.CONTINUE_AS_NEW_VERSIONING_BEHAVIOR_UNSPECIFIED + ) + """An initial versioning behavior is not set, follow the existing continue-as-new inheritance semantics. + See https://docs.temporal.io/worker-versioning#inheritance-semantics for more detail. + """ + + AUTO_UPGRADE = int( + temporalio.api.enums.v1.ContinueAsNewVersioningBehavior.CONTINUE_AS_NEW_VERSIONING_BEHAVIOR_AUTO_UPGRADE + ) + """Start the new run with AutoUpgrade behavior. Use the Target Version of the workflow's task queue at + start-time, as AutoUpgrade workflows do. After the first workflow task completes, use whatever + Versioning Behavior the workflow is annotated with in the workflow code. + + Note that if the previous workflow had a Pinned override, that override will be inherited by the + new workflow run regardless of the ContinueAsNewVersioningBehavior specified in the continue-as-new + command. If a Pinned override is inherited by the new run, and the new run starts with AutoUpgrade + behavior, the base version of the new run will be the Target Version as described above, but the + effective version will be whatever is specified by the Versioning Override until the override is removed. + """ + + USE_RAMPING_VERSION = int( + temporalio.api.enums.v1.ContinueAsNewVersioningBehavior.CONTINUE_AS_NEW_VERSIONING_BEHAVIOR_USE_RAMPING_VERSION + ) + """Use the Ramping Version of the workflow's task queue at start time, regardless of the workflow's + Target Version. After the first workflow task completes, the workflow will use whatever Versioning + Behavior it is annotated with. If there is no Ramping Version by the time that the first workflow task + is dispatched, it will be sent to the Current Version. + + It is highly discouraged to use this if the workflow is annotated with AutoUpgrade behavior, because + this setting ONLY applies to the first task of the workflow. If, after the first task, the workflow + is AutoUpgrade, it will behave like a normal AutoUpgrade workflow and go to the Target Version, which + may be the Current Version instead of the Ramping Version. + + Note that if the workflow being continued has a Pinned override, that override will be inherited by the + new workflow run regardless of the ContinueAsNewVersioningBehavior specified in the continue-as-new + command. Versioning Override always takes precedence until it's removed manually via + UpdateWorkflowExecutionOptions. + """ diff --git a/temporalio/workflow/_handlers.py b/temporalio/workflow/_handlers.py new file mode 100644 index 000000000..afa0bb6e4 --- /dev/null +++ b/temporalio/workflow/_handlers.py @@ -0,0 +1,587 @@ +from __future__ import annotations + +import inspect +import typing +import warnings +from collections.abc import Awaitable, Callable, Sequence +from dataclasses import dataclass +from enum import Enum +from functools import partial +from typing import Any, Literal, cast, overload + +from typing_extensions import Protocol, runtime_checkable + +import temporalio.common + +from ..types import ( + CallableSyncOrAsyncReturnNoneType, + CallableSyncOrAsyncType, + CallableType, + MultiParamSpec, + ProtocolReturnType, + ReturnType, +) + +__all__ = [ + "HandlerUnfinishedPolicy", + "UnfinishedSignalHandlersWarning", + "UnfinishedUpdateHandlersWarning", + "UpdateMethodMultiParam", + "query", + "signal", + "update", +] + + +class HandlerUnfinishedPolicy(Enum): + """Actions taken if a workflow terminates with running handlers. + + Policy defining actions taken when a workflow exits while update or signal handlers are running. + The workflow exit may be due to successful return, failure, cancellation, or continue-as-new. + """ + + WARN_AND_ABANDON = 1 + """Issue a warning in addition to abandoning.""" + ABANDON = 2 + """Abandon the handler. + + In the case of an update handler this means that the client will receive an error rather than + the update result.""" + + +class UnfinishedUpdateHandlersWarning(RuntimeWarning): + """The workflow exited before all update handlers had finished executing.""" + + +class UnfinishedSignalHandlersWarning(RuntimeWarning): + """The workflow exited before all signal handlers had finished executing.""" + + +@overload +def signal( + fn: CallableSyncOrAsyncReturnNoneType, +) -> CallableSyncOrAsyncReturnNoneType: ... + + +@overload +def signal( + *, + unfinished_policy: HandlerUnfinishedPolicy = HandlerUnfinishedPolicy.WARN_AND_ABANDON, + description: str | None = None, +) -> Callable[ + [CallableSyncOrAsyncReturnNoneType], CallableSyncOrAsyncReturnNoneType +]: ... + + +@overload +def signal( + *, + name: str, + unfinished_policy: HandlerUnfinishedPolicy = HandlerUnfinishedPolicy.WARN_AND_ABANDON, + description: str | None = None, +) -> Callable[ + [CallableSyncOrAsyncReturnNoneType], CallableSyncOrAsyncReturnNoneType +]: ... + + +@overload +def signal( + *, + dynamic: Literal[True], + unfinished_policy: HandlerUnfinishedPolicy = HandlerUnfinishedPolicy.WARN_AND_ABANDON, + description: str | None = None, +) -> Callable[ + [CallableSyncOrAsyncReturnNoneType], CallableSyncOrAsyncReturnNoneType +]: ... + + +def signal( + fn: CallableSyncOrAsyncReturnNoneType | None = None, + *, + name: str | None = None, + dynamic: bool | None = False, + unfinished_policy: HandlerUnfinishedPolicy = HandlerUnfinishedPolicy.WARN_AND_ABANDON, + description: str | None = None, +) -> ( + Callable[[CallableSyncOrAsyncReturnNoneType], CallableSyncOrAsyncReturnNoneType] + | CallableSyncOrAsyncReturnNoneType +): + """Decorator for a workflow signal method. + + This is used on any async or non-async method that you wish to be called upon + receiving a signal. If a function overrides one with this decorator, it too + must be decorated. + + Signal methods can only have positional parameters. Best practice for + non-dynamic signal methods is to only take a single object/dataclass + argument that can accept more fields later if needed. Return values from + signal methods are ignored. + + Args: + fn: The function to decorate. + name: Signal name. Defaults to method ``__name__``. Cannot be present + when ``dynamic`` is present. + dynamic: If true, this handles all signals not otherwise handled. The + parameters of the method must be self, a string name, and a + ``*args`` positional varargs. Cannot be present when ``name`` is + present. + unfinished_policy: Actions taken if a workflow terminates with + a running instance of this handler. + description: A short description of the signal that may appear in the UI/CLI. + """ + + def decorator( + name: str | None, + unfinished_policy: HandlerUnfinishedPolicy, + fn: CallableSyncOrAsyncReturnNoneType, + ) -> CallableSyncOrAsyncReturnNoneType: + if not name and not dynamic: + name = fn.__name__ + defn = _SignalDefinition( + name=name, + fn=fn, + is_method=True, + unfinished_policy=unfinished_policy, + description=description, + ) + setattr(fn, "__temporal_signal_definition", defn) + if defn.dynamic_vararg: + warnings.warn( + "Dynamic signals with vararg third param is deprecated, use Sequence[RawValue]", + DeprecationWarning, + stacklevel=2, + ) + return fn + + if not fn: + if name is not None and dynamic: + raise RuntimeError("Cannot provide name and dynamic boolean") + return partial(decorator, name, unfinished_policy) + else: + return decorator(fn.__name__, unfinished_policy, fn) + + +@overload +def query(fn: CallableType) -> CallableType: ... + + +@overload +def query( + *, name: str, description: str | None = None +) -> Callable[[CallableType], CallableType]: ... + + +@overload +def query( + *, dynamic: Literal[True], description: str | None = None +) -> Callable[[CallableType], CallableType]: ... + + +@overload +def query(*, description: str) -> Callable[[CallableType], CallableType]: ... + + +def query( + fn: CallableType | None = None, # type: ignore[reportInvalidTypeVarUse] + *, + name: str | None = None, + dynamic: bool | None = False, + description: str | None = None, +): + """Decorator for a workflow query method. + + This is used on any non-async method that expects to handle a query. If a + function overrides one with this decorator, it too must be decorated. + + Query methods can only have positional parameters. Best practice for + non-dynamic query methods is to only take a single object/dataclass + argument that can accept more fields later if needed. The return value is + the resulting query value. Query methods must not mutate any workflow state. + + Args: + fn: The function to decorate. + name: Query name. Defaults to method ``__name__``. Cannot be present + when ``dynamic`` is present. + dynamic: If true, this handles all queries not otherwise handled. The + parameters of the method should be self, a string name, and a + ``Sequence[RawValue]``. An older form of this accepted vararg + parameters which will now warn. Cannot be present when ``name`` is + present. + description: A short description of the query that may appear in the UI/CLI. + """ + + def decorator( + name: str | None, + description: str | None, + fn: CallableType, + *, + bypass_async_check: bool = False, + ) -> CallableType: + if not name and not dynamic: + name = fn.__name__ + if not bypass_async_check and inspect.iscoroutinefunction(fn): + warnings.warn( + "Queries as async def functions are deprecated", + DeprecationWarning, + stacklevel=2, + ) + defn = _QueryDefinition( + name=name, fn=fn, is_method=True, description=description + ) + setattr(fn, "__temporal_query_definition", defn) + if defn.dynamic_vararg: + warnings.warn( + "Dynamic queries with vararg third param is deprecated, use Sequence[RawValue]", + DeprecationWarning, + stacklevel=2, + ) + return fn + + if name is not None or dynamic or description: + if name is not None and dynamic: + raise RuntimeError("Cannot provide name and dynamic boolean") + return partial(decorator, name, description) + if fn is None: + raise RuntimeError("Cannot create query without function or name or dynamic") + if inspect.iscoroutinefunction(fn): + warnings.warn( + "Queries as async def functions are deprecated", + DeprecationWarning, + stacklevel=2, + ) + return decorator(fn.__name__, description, fn, bypass_async_check=True) + + +@runtime_checkable +class UpdateMethodMultiParam(Protocol[MultiParamSpec, ProtocolReturnType]): + """Decorated workflow update functions implement this.""" + + _defn: _UpdateDefinition + + def __call__( + self, *args: MultiParamSpec.args, **kwargs: MultiParamSpec.kwargs + ) -> ProtocolReturnType | Awaitable[ProtocolReturnType]: + """Generic callable type callback.""" + ... + + def validator( + self, vfunc: Callable[MultiParamSpec, None] + ) -> Callable[MultiParamSpec, None]: + """Use to decorate a function to validate the arguments passed to the update handler.""" + ... + + +@overload +def update( + fn: Callable[MultiParamSpec, Awaitable[ReturnType]], +) -> UpdateMethodMultiParam[MultiParamSpec, ReturnType]: ... + + +@overload +def update( + fn: Callable[MultiParamSpec, ReturnType], +) -> UpdateMethodMultiParam[MultiParamSpec, ReturnType]: ... + + +@overload +def update( + *, + unfinished_policy: HandlerUnfinishedPolicy = HandlerUnfinishedPolicy.WARN_AND_ABANDON, + description: str | None = None, +) -> Callable[ + [Callable[MultiParamSpec, ReturnType]], + UpdateMethodMultiParam[MultiParamSpec, ReturnType], +]: ... + + +@overload +def update( + *, + name: str, + unfinished_policy: HandlerUnfinishedPolicy = HandlerUnfinishedPolicy.WARN_AND_ABANDON, + description: str | None = None, +) -> Callable[ + [Callable[MultiParamSpec, ReturnType]], + UpdateMethodMultiParam[MultiParamSpec, ReturnType], +]: ... + + +@overload +def update( + *, + dynamic: Literal[True], + unfinished_policy: HandlerUnfinishedPolicy = HandlerUnfinishedPolicy.WARN_AND_ABANDON, + description: str | None = None, +) -> Callable[ + [Callable[MultiParamSpec, ReturnType]], + UpdateMethodMultiParam[MultiParamSpec, ReturnType], +]: ... + + +def update( + fn: CallableSyncOrAsyncType | None = None, # type: ignore[reportInvalidTypeVarUse] + *, + name: str | None = None, + dynamic: bool | None = False, + unfinished_policy: HandlerUnfinishedPolicy = HandlerUnfinishedPolicy.WARN_AND_ABANDON, + description: str | None = None, +) -> ( + UpdateMethodMultiParam[MultiParamSpec, ReturnType] + | Callable[ + [Callable[MultiParamSpec, ReturnType]], + UpdateMethodMultiParam[MultiParamSpec, ReturnType], + ] +): + """Decorator for a workflow update handler method. + + This is used on any async or non-async method that you wish to be called upon + receiving an update. If a function overrides one with this decorator, it too + must be decorated. + + You may also optionally define a validator method that will be called before + this handler you have applied this decorator to. You can specify the validator + with ``@update_handler_function_name.validator``. + + Update methods can only have positional parameters. Best practice for + non-dynamic update methods is to only take a single object/dataclass + argument that can accept more fields later if needed. The handler may return + a serializable value which will be sent back to the caller of the update. + + Args: + fn: The function to decorate. + name: Update name. Defaults to method ``__name__``. Cannot be present + when ``dynamic`` is present. + dynamic: If true, this handles all updates not otherwise handled. The + parameters of the method must be self, a string name, and a + ``*args`` positional varargs. Cannot be present when ``name`` is + present. + unfinished_policy: Actions taken if a workflow terminates with + a running instance of this handler. + description: A short description of the update that may appear in the UI/CLI. + """ + + def decorator( + name: str | None, + unfinished_policy: HandlerUnfinishedPolicy, + fn: CallableSyncOrAsyncType, + ) -> CallableSyncOrAsyncType: + if not name and not dynamic: + name = fn.__name__ + defn = _UpdateDefinition( + name=name, + fn=fn, + is_method=True, + unfinished_policy=unfinished_policy, + description=description, + ) + if defn.dynamic_vararg: + raise RuntimeError( + "Dynamic updates do not support a vararg third param, use Sequence[RawValue]", + ) + setattr(fn, "_defn", defn) + setattr(fn, "validator", partial(_update_validator, defn)) + return fn + + if not fn: + if name is not None and dynamic: + raise RuntimeError("Cannot provide name and dynamic boolean") + return partial(decorator, name, unfinished_policy) # type: ignore[reportReturnType, return-value] + else: + return decorator(fn.__name__, unfinished_policy, fn) # type: ignore[reportReturnType, return-value] + + +def _update_validator( + update_def: _UpdateDefinition, fn: Callable[..., None] | None = None +) -> Callable[..., None] | None: + """Decorator for a workflow update validator method.""" + if fn is not None: + update_def.set_validator(fn) + return fn + + +def _bind_method(obj: Any, fn: Callable[..., Any]) -> Callable[..., Any]: + # Curry instance on the definition function since that represents an + # unbound method + if inspect.iscoroutinefunction(fn): + # We cannot use functools.partial here because in <= 3.7 that isn't + # considered an inspect.iscoroutinefunction + fn = cast(Callable[..., Awaitable[Any]], fn) + + async def with_object(*args: Any, **kwargs: Any) -> Any: + return await fn(obj, *args, **kwargs) + + return with_object + return partial(fn, obj) + + +def _assert_dynamic_handler_args( + fn: Callable, arg_types: list[type] | None, is_method: bool +) -> bool: + # Dynamic query/signal/update must have three args: self, name, and + # Sequence[RawValue]. An older form accepted varargs for the third param for signals/queries so + # we will too (but will warn in the signal/query code). + params = list(inspect.signature(fn).parameters.values()) + total_expected_params = 3 if is_method else 2 + if ( + len(params) == total_expected_params + and params[-2].kind is inspect.Parameter.POSITIONAL_OR_KEYWORD + and params[-1].kind is inspect.Parameter.VAR_POSITIONAL + ): + # Old var-arg form + return False + if ( + not arg_types + or len(arg_types) != 2 + or arg_types[0] is not str + or ( + arg_types[1] != Sequence[temporalio.common.RawValue] + and arg_types[1] != typing.Sequence[temporalio.common.RawValue] # type: ignore[reportDeprecated] + ) + ): + raise RuntimeError( + "Dynamic handler must have 3 arguments: self, str, and Sequence[RawValue]" + ) + return True + + +@dataclass(frozen=True) +class _SignalDefinition: + # None if dynamic + name: str | None + fn: Callable[..., None | Awaitable[None]] + is_method: bool + unfinished_policy: HandlerUnfinishedPolicy = ( + HandlerUnfinishedPolicy.WARN_AND_ABANDON + ) + description: str | None = None + # Types loaded on post init if None + arg_types: list[type] | None = None + dynamic_vararg: bool = False + + @staticmethod + def from_fn(fn: Callable) -> _SignalDefinition | None: + return getattr(fn, "__temporal_signal_definition", None) + + @staticmethod + def must_name_from_fn_or_str(signal: str | Callable) -> str: + if callable(signal): + defn = _SignalDefinition.from_fn(signal) + if not defn: + raise RuntimeError( + f"Signal definition not found on {signal.__qualname__}, " + "is it decorated with @workflow.signal?" + ) + elif not defn.name: + raise RuntimeError("Cannot invoke dynamic signal definition") + # TODO(cretz): Check count/type of args at runtime? + return defn.name + return str(signal) + + def __post_init__(self) -> None: + if self.arg_types is None: + arg_types, _ = temporalio.common._type_hints_from_func(self.fn) + # If dynamic, assert it + if not self.name: + object.__setattr__( + self, + "dynamic_vararg", + not _assert_dynamic_handler_args( + self.fn, arg_types, self.is_method + ), + ) + object.__setattr__(self, "arg_types", arg_types) + + def bind_fn(self, obj: Any) -> Callable[..., Any]: + return _bind_method(obj, self.fn) + + +@dataclass(frozen=True) +class _QueryDefinition: + # None if dynamic + name: str | None + fn: Callable[..., Any] + is_method: bool + description: str | None = None + # Types loaded on post init if both are None + arg_types: list[type] | None = None + ret_type: type | None = None + dynamic_vararg: bool = False + + @staticmethod + def from_fn(fn: Callable) -> _QueryDefinition | None: + return getattr(fn, "__temporal_query_definition", None) + + def __post_init__(self) -> None: + if self.arg_types is None and self.ret_type is None: + arg_types, ret_type = temporalio.common._type_hints_from_func(self.fn) + # If dynamic, assert it + if not self.name: + object.__setattr__( + self, + "dynamic_vararg", + not _assert_dynamic_handler_args( + self.fn, arg_types, self.is_method + ), + ) + object.__setattr__(self, "arg_types", arg_types) + object.__setattr__(self, "ret_type", ret_type) + + def bind_fn(self, obj: Any) -> Callable[..., Any]: + return _bind_method(obj, self.fn) + + +@dataclass(frozen=True) +class _UpdateDefinition: + # None if dynamic + name: str | None + fn: Callable[..., Any | Awaitable[Any]] + is_method: bool + unfinished_policy: HandlerUnfinishedPolicy = ( + HandlerUnfinishedPolicy.WARN_AND_ABANDON + ) + description: str | None = None + # Types loaded on post init if None + arg_types: list[type] | None = None + ret_type: type | None = None + validator: Callable[..., None] | None = None + dynamic_vararg: bool = False + + def __post_init__(self) -> None: + if self.arg_types is None: + arg_types, ret_type = temporalio.common._type_hints_from_func(self.fn) + # Disallow dynamic varargs + if not self.name and not _assert_dynamic_handler_args( + self.fn, arg_types, self.is_method + ): + raise RuntimeError( + "Dynamic updates do not support a vararg third param, use Sequence[RawValue]", + ) + object.__setattr__(self, "arg_types", arg_types) + object.__setattr__(self, "ret_type", ret_type) + + def bind_fn(self, obj: Any) -> Callable[..., Any]: + return _bind_method(obj, self.fn) + + def bind_validator(self, obj: Any) -> Callable[..., Any]: + if self.validator is not None: + return _bind_method(obj, self.validator) + return lambda *args, **kwargs: None + + def set_validator(self, validator: Callable[..., None]) -> None: + if self.validator: + raise RuntimeError(f"Validator already set for update {self.name}") + object.__setattr__(self, "validator", validator) + + @classmethod + def get_name_and_result_type( + cls, + name_or_update_fn: str | Callable[..., Any], + ) -> tuple[str, type | None]: + if isinstance(name_or_update_fn, UpdateMethodMultiParam): + defn = name_or_update_fn._defn + if not defn.name: + raise RuntimeError("Cannot invoke dynamic update definition") + # TODO(cretz): Check count/type of args at runtime? + return defn.name, defn.ret_type + else: + return str(name_or_update_fn), None diff --git a/temporalio/workflow/_nexus.py b/temporalio/workflow/_nexus.py new file mode 100644 index 000000000..29bd10715 --- /dev/null +++ b/temporalio/workflow/_nexus.py @@ -0,0 +1,549 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Awaitable, Callable, Generator, Mapping +from datetime import timedelta +from enum import IntEnum +from typing import Any, Generic, overload + +import nexusrpc +import nexusrpc.handler +from nexusrpc import InputT, OutputT + +import temporalio.bridge.proto.nexus +import temporalio.nexus +from temporalio.types import NexusServiceType + +from ._context import _Runtime + +__all__ = [ + "NexusClient", + "NexusOperationCancellationType", + "NexusOperationHandle", + "create_nexus_client", +] + + +class NexusOperationHandle(Generic[OutputT]): + """Handle for interacting with a Nexus operation.""" + + # TODO(nexus-preview): should attempts to instantiate directly throw? + + def cancel(self) -> bool: + """Request cancellation of the operation.""" + raise NotImplementedError + + def __await__(self) -> Generator[Any, Any, OutputT]: + """Support await.""" + raise NotImplementedError + + @property + def operation_token(self) -> str | None: + """The operation token for this handle.""" + raise NotImplementedError + + +class NexusOperationCancellationType(IntEnum): + """Defines behavior of a Nexus operation when the caller workflow initiates cancellation. + + Pass one of these values to :py:meth:`NexusClient.start_operation` to define cancellation + behavior. + + To initiate cancellation, use :py:meth:`NexusOperationHandle.cancel` and then ``await`` the + operation handle. This will result in a :py:class:`exceptions.NexusOperationError`. The values + of this enum define what is guaranteed to have happened by that point. + """ + + ABANDON = int(temporalio.bridge.proto.nexus.NexusOperationCancellationType.ABANDON) + """Do not send any cancellation request to the operation handler; just report cancellation to the caller""" + + TRY_CANCEL = int( + temporalio.bridge.proto.nexus.NexusOperationCancellationType.TRY_CANCEL + ) + """Send a cancellation request but immediately report cancellation to the caller. Note that this + does not guarantee that cancellation is delivered to the operation handler if the caller exits + before the delivery is done. + """ + + WAIT_REQUESTED = int( + temporalio.bridge.proto.nexus.NexusOperationCancellationType.WAIT_CANCELLATION_REQUESTED + ) + """Send a cancellation request and wait for confirmation that the request was received. + Does not wait for the operation to complete. + """ + + WAIT_COMPLETED = int( + temporalio.bridge.proto.nexus.NexusOperationCancellationType.WAIT_CANCELLATION_COMPLETED + ) + """Send a cancellation request and wait for the operation to complete. + Note that the operation may not complete as cancelled (for example, if it catches the + :py:exc:`asyncio.CancelledError` resulting from the cancellation request).""" + + +class NexusClient(ABC, Generic[NexusServiceType]): + """A client for invoking Nexus operations. + + Example:: + + nexus_client = workflow.create_nexus_client( + endpoint=my_nexus_endpoint, + service=MyService, + ) + handle = await nexus_client.start_operation( + operation=MyService.my_operation, + input=MyOperationInput(value="hello"), + schedule_to_close_timeout=timedelta(seconds=10), + ) + result = await handle.result() + """ + + # Overload for nexusrpc.Operation + @overload + @abstractmethod + async def start_operation( + self, + operation: nexusrpc.Operation[InputT, OutputT], + input: InputT, + *, + output_type: type[OutputT] | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, + headers: Mapping[str, str] | None = None, + summary: str | None = None, + ) -> NexusOperationHandle[OutputT]: ... + + # Overload for string operation name + @overload + @abstractmethod + async def start_operation( + self, + operation: str, + input: Any, + *, + output_type: type[OutputT] | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, + headers: Mapping[str, str] | None = None, + summary: str | None = None, + ) -> NexusOperationHandle[OutputT]: ... + + # Overload for workflow_run_operation methods + @overload + @abstractmethod + async def start_operation( + self, + operation: Callable[ + [NexusServiceType, temporalio.nexus.WorkflowRunOperationContext, InputT], + Awaitable[temporalio.nexus.WorkflowHandle[OutputT]], + ], + input: InputT, + *, + output_type: type[OutputT] | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, + headers: Mapping[str, str] | None = None, + summary: str | None = None, + ) -> NexusOperationHandle[OutputT]: ... + + # Overload for sync_operation methods (async def) + @overload + @abstractmethod + async def start_operation( + self, + operation: Callable[ + [NexusServiceType, nexusrpc.handler.StartOperationContext, InputT], + Awaitable[OutputT], + ], + input: InputT, + *, + output_type: type[OutputT] | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, + headers: Mapping[str, str] | None = None, + summary: str | None = None, + ) -> NexusOperationHandle[OutputT]: ... + + # Overload for sync_operation methods (def) + @overload + @abstractmethod + async def start_operation( + self, + operation: Callable[ + [NexusServiceType, nexusrpc.handler.StartOperationContext, InputT], + OutputT, + ], + input: InputT, + *, + output_type: type[OutputT] | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, + headers: Mapping[str, str] | None = None, + summary: str | None = None, + ) -> NexusOperationHandle[OutputT]: ... + + # Overload for operation_handler + @overload + @abstractmethod + async def start_operation( + self, + operation: Callable[ + [NexusServiceType], nexusrpc.handler.OperationHandler[InputT, OutputT] + ], + input: InputT, + *, + output_type: type[OutputT] | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, + headers: Mapping[str, str] | None = None, + summary: str | None = None, + ) -> NexusOperationHandle[OutputT]: ... + + # Overload for temporal_operation methods + @overload + @abstractmethod + async def start_operation( + self, + operation: Callable[ + [ + NexusServiceType, + temporalio.nexus.TemporalStartOperationContext, + temporalio.nexus.TemporalNexusClient, + InputT, + ], + Awaitable[temporalio.nexus.TemporalOperationResult[OutputT]], + ], + input: InputT, + *, + output_type: type[OutputT] | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, + headers: Mapping[str, str] | None = None, + summary: str | None = None, + ) -> NexusOperationHandle[OutputT]: ... + + @abstractmethod + async def start_operation( + self, + operation: Any, + input: Any, + *, + output_type: type[OutputT] | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, + headers: Mapping[str, str] | None = None, + summary: str | None = None, + ) -> Any: + """Start a Nexus operation and return its handle. + + Args: + operation: The Nexus operation. + input: The Nexus operation input. + output_type: The Nexus operation output type. + schedule_to_close_timeout: Timeout for the entire operation attempt. + schedule_to_start_timeout: Timeout for the operation to be started. + start_to_close_timeout: Timeout for async operations to complete after starting. + headers: Headers to send with the Nexus HTTP request. + + Returns: + A handle to the Nexus operation. The result can be obtained as + ```python + await handle.result() + ``` + """ + ... + + # Overload for nexusrpc.Operation + @overload + @abstractmethod + async def execute_operation( + self, + operation: nexusrpc.Operation[InputT, OutputT], + input: InputT, + *, + output_type: type[OutputT] | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, + headers: Mapping[str, str] | None = None, + summary: str | None = None, + ) -> OutputT: ... + + # Overload for string operation name + @overload + @abstractmethod + async def execute_operation( + self, + operation: str, + input: Any, + *, + output_type: type[OutputT] | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, + headers: Mapping[str, str] | None = None, + summary: str | None = None, + ) -> OutputT: ... + + # Overload for workflow_run_operation methods + @overload + @abstractmethod + async def execute_operation( + self, + operation: Callable[ + [NexusServiceType, temporalio.nexus.WorkflowRunOperationContext, InputT], + Awaitable[temporalio.nexus.WorkflowHandle[OutputT]], + ], + input: InputT, + *, + output_type: type[OutputT] | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, + headers: Mapping[str, str] | None = None, + summary: str | None = None, + ) -> OutputT: ... + + # Overload for sync_operation methods (async def) + @overload + @abstractmethod + async def execute_operation( + self, + operation: Callable[ + [NexusServiceType, nexusrpc.handler.StartOperationContext, InputT], + Awaitable[OutputT], + ], + input: InputT, + *, + output_type: type[OutputT] | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, + headers: Mapping[str, str] | None = None, + summary: str | None = None, + ) -> OutputT: ... + + # Overload for sync_operation methods (def) + @overload + @abstractmethod + async def execute_operation( + self, + operation: Callable[ + [NexusServiceType, nexusrpc.handler.StartOperationContext, InputT], + OutputT, + ], + input: InputT, + *, + output_type: type[OutputT] | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, + headers: Mapping[str, str] | None = None, + summary: str | None = None, + ) -> OutputT: ... + + # Overload for operation_handler + @overload + @abstractmethod + async def execute_operation( + self, + operation: Callable[ + [NexusServiceType], + nexusrpc.handler.OperationHandler[InputT, OutputT], + ], + input: InputT, + *, + output_type: type[OutputT] | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, + headers: Mapping[str, str] | None = None, + summary: str | None = None, + ) -> OutputT: ... + + # Overload for temporal_operation methods + @overload + @abstractmethod + async def execute_operation( + self, + operation: Callable[ + [ + NexusServiceType, + temporalio.nexus.TemporalStartOperationContext, + temporalio.nexus.TemporalNexusClient, + InputT, + ], + Awaitable[temporalio.nexus.TemporalOperationResult[OutputT]], + ], + input: InputT, + *, + output_type: type[OutputT] | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, + headers: Mapping[str, str] | None = None, + summary: str | None = None, + ) -> OutputT: ... + + @abstractmethod + async def execute_operation( + self, + operation: Any, + input: Any, + *, + output_type: type[OutputT] | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, + headers: Mapping[str, str] | None = None, + summary: str | None = None, + ) -> Any: + """Execute a Nexus operation and return its result. + + Args: + operation: The Nexus operation. + input: The Nexus operation input. + output_type: The Nexus operation output type. + schedule_to_close_timeout: Timeout for the entire operation attempt. + schedule_to_start_timeout: Timeout for the operation to be started. + start_to_close_timeout: Timeout for async operations to complete after starting. + headers: Headers to send with the Nexus HTTP request. + + Returns: + The operation result. + """ + ... + + +class _NexusClient(NexusClient[NexusServiceType]): + def __init__( + self, + *, + endpoint: str, + service: type[NexusServiceType] | str, + ) -> None: + """Create a Nexus client. + + Args: + service: The Nexus service. + endpoint: The Nexus endpoint. + """ + # If service is not a str, then it must be a service interface or implementation + # class. + if isinstance(service, str): + self.service_name = service + elif service_defn := nexusrpc.get_service_definition(service): + self.service_name = service_defn.name + else: + raise ValueError( + f"`service` may be a name (str), or a class decorated with either " + f"@nexusrpc.handler.service_handler or @nexusrpc.service. " + f"Invalid service type: {type(service)}" + ) + self.endpoint = endpoint + + async def start_operation( + self, + operation: Any, + input: Any, + *, + output_type: type[OutputT] | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, + headers: Mapping[str, str] | None = None, + summary: str | None = None, + ) -> Any: + return await _Runtime.current().workflow_start_nexus_operation( + endpoint=self.endpoint, + service=self.service_name, + operation=operation, + input=input, + output_type=output_type, + schedule_to_close_timeout=schedule_to_close_timeout, + schedule_to_start_timeout=schedule_to_start_timeout, + start_to_close_timeout=start_to_close_timeout, + cancellation_type=cancellation_type, + headers=headers, + summary=summary, + ) + + async def execute_operation( + self, + operation: Any, + input: Any, + *, + output_type: type[OutputT] | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, + headers: Mapping[str, str] | None = None, + summary: str | None = None, + ) -> Any: + handle = await self.start_operation( + operation, + input, + output_type=output_type, + schedule_to_close_timeout=schedule_to_close_timeout, + schedule_to_start_timeout=schedule_to_start_timeout, + start_to_close_timeout=start_to_close_timeout, + cancellation_type=cancellation_type, + headers=headers, + summary=summary, + ) + return await handle + + +@overload +def create_nexus_client( + *, + service: type[NexusServiceType], + endpoint: str, +) -> NexusClient[NexusServiceType]: ... + + +@overload +def create_nexus_client( + *, + service: str, + endpoint: str, +) -> NexusClient[Any]: ... + + +def create_nexus_client( + *, + service: type[NexusServiceType] | str, + endpoint: str, +) -> NexusClient[NexusServiceType]: + """Create a Nexus client. + + Args: + service: The Nexus service. + endpoint: The Nexus endpoint. + """ + return _NexusClient(endpoint=endpoint, service=service) diff --git a/temporalio/workflow/_sandbox.py b/temporalio/workflow/_sandbox.py new file mode 100644 index 000000000..32a053604 --- /dev/null +++ b/temporalio/workflow/_sandbox.py @@ -0,0 +1,321 @@ +from __future__ import annotations + +import logging +import sys +import threading +from collections.abc import Iterator, Mapping, MutableMapping +from contextlib import contextmanager +from enum import Flag, auto +from typing import Any + +from ._context import Info, _Runtime, current_update_info + +__all__ = [ + "LoggerAdapter", + "SandboxImportNotificationPolicy", + "logger", + "unsafe", +] + +_sandbox_unrestricted = threading.local() +_in_sandbox = threading.local() +_imports_passed_through = threading.local() +_sandbox_import_notification_policy_override = threading.local() + + +class SandboxImportNotificationPolicy(Flag): + """Defines the behavior taken when modules are imported into the sandbox after the workflow is initially loaded or unintentionally missing from the passthrough list.""" + + SILENT = auto() + """Allow imports that do not violate sandbox restrictions and no warnings are generated.""" + WARN_ON_DYNAMIC_IMPORT = auto() + """Allows dynamic imports that do not violate sandbox restrictions but issues a warning when an import is triggered in the sandbox after initial workflow load.""" + WARN_ON_UNINTENTIONAL_PASSTHROUGH = auto() + """Allows imports that do not violate sandbox restrictions but issues a warning when an import is triggered in the sandbox that was unintentionally passed through.""" + RAISE_ON_UNINTENTIONAL_PASSTHROUGH = auto() + """Raise an error when an import is triggered in the sandbox that was unintentionally passed through.""" + + +class unsafe: + """Contains static methods that should not normally be called during + workflow execution except in advanced cases. + """ + + def __init__(self) -> None: # noqa: D107 + raise NotImplementedError + + @staticmethod + def in_sandbox() -> bool: + """Whether the code is executing on a sandboxed thread. + + Returns: + True if the code is executing in the sandbox thread. + """ + return getattr(_in_sandbox, "value", False) + + @staticmethod + def _set_in_sandbox(v: bool) -> None: + _in_sandbox.value = v + + @staticmethod + def is_replaying() -> bool: + """Whether the workflow is currently replaying. + + This includes queries and update validators that occur during replay. + + Returns: + True if the workflow is currently replaying + """ + return _Runtime.current().workflow_is_replaying() + + @staticmethod + def is_replaying_history_events() -> bool: + """Whether the workflow is replaying history events. + + This excludes queries and update validators, which are live operations. + + Returns: + True if replaying history events, False otherwise. + """ + return _Runtime.current().workflow_is_replaying_history_events() + + @staticmethod + def is_read_only() -> bool: + """Whether the workflow is currently in read-only mode. + + Read-only mode occurs during queries and update validators where + side effects are not allowed. + + Returns: + True if the workflow is in read-only mode, False otherwise. + """ + return _Runtime.current().workflow_is_read_only() + + @staticmethod + def is_sandbox_unrestricted() -> bool: + """Whether the current block of code is not restricted via sandbox. + + Returns: + True if the current code is not restricted in the sandbox. + """ + # Activations happen in different threads than init and possibly the + # local hasn't been initialized in _that_ thread, so we allow unset here + # instead of just setting value = False globally. + return getattr(_sandbox_unrestricted, "value", False) + + @staticmethod + @contextmanager + def sandbox_unrestricted() -> Iterator[None]: + """A context manager to run code without sandbox restrictions.""" + # Only apply if not already applied. Nested calls just continue + # unrestricted. + if unsafe.is_sandbox_unrestricted(): + yield None + return + _sandbox_unrestricted.value = True + try: + yield None + finally: + _sandbox_unrestricted.value = False + + @staticmethod + def is_imports_passed_through() -> bool: + """Whether the current block of code is in + :py:meth:imports_passed_through. + + Returns: + True if the current code's imports will be passed through + """ + # See comment in is_sandbox_unrestricted for why we allow unset instead + # of just global false. + return getattr(_imports_passed_through, "value", False) + + @staticmethod + @contextmanager + def imports_passed_through() -> Iterator[None]: + """Context manager to mark all imports that occur within it as passed + through (meaning not reloaded by the sandbox). + """ + # Only apply if not already applied. Nested calls just continue + # passed through. + if unsafe.is_imports_passed_through(): + yield None + return + _imports_passed_through.value = True + try: + yield None + finally: + _imports_passed_through.value = False + + @staticmethod + def current_import_notification_policy_override() -> ( + SandboxImportNotificationPolicy | None + ): + """Gets the current import notification policy override if one is set.""" + applied_policy = getattr( + _sandbox_import_notification_policy_override, + "value", + None, + ) + return applied_policy + + @staticmethod + @contextmanager + def sandbox_import_notification_policy( + policy: SandboxImportNotificationPolicy, + ) -> Iterator[None]: + """Context manager to apply the given import notification policy.""" + original_policy = _sandbox_import_notification_policy_override.value = getattr( + _sandbox_import_notification_policy_override, + "value", + None, + ) + _sandbox_import_notification_policy_override.value = policy + try: + yield None + finally: + _sandbox_import_notification_policy_override.value = original_policy + + +def _build_log_context( + workflow_details: Mapping[str, Any] | None, + update_details: Mapping[str, Any] | None = None, + *, + workflow_info_on_message: bool = True, + workflow_info_on_extra: bool = True, + full_workflow_info: Info | None = None, +) -> tuple[dict[str, Any], dict[str, Any]]: + """Build the msg_extra suffix and extra dict entries for a temporal log record. + + Returns: + (msg_extra, extra) where msg_extra should be appended to the log message + and extra should be merged into the log record's extra dict. + """ + msg_extra: dict[str, Any] = {} + extra: dict[str, Any] = {} + + if workflow_details is not None: + if workflow_info_on_message: + msg_extra.update(workflow_details) + if workflow_info_on_extra: + extra["temporal_workflow"] = dict(workflow_details) + + if update_details is not None: + if workflow_info_on_message: + msg_extra.update(update_details) + if workflow_info_on_extra: + extra.setdefault("temporal_workflow", {}).update(update_details) + + if full_workflow_info is not None: + extra["workflow_info"] = full_workflow_info + + return msg_extra, extra + + +class LoggerAdapter(logging.LoggerAdapter): + """Adapter that adds details to the log about the running workflow. + + Attributes: + workflow_info_on_message: Boolean for whether a string representation of + a dict of some workflow info will be appended to each message. + Default is True. + workflow_info_on_extra: Boolean for whether a ``temporal_workflow`` + dictionary value will be added to the ``extra`` dictionary with some + workflow info, making it present on the ``LogRecord.__dict__`` for + use by others. Default is True. + full_workflow_info_on_extra: Boolean for whether a ``workflow_info`` + value will be added to the ``extra`` dictionary with the entire + workflow info, making it present on the ``LogRecord.__dict__`` for + use by others. Default is False. + log_during_replay: Boolean for whether logs should occur during replay. + Default is False. + + Values added to ``extra`` are merged with the ``extra`` dictionary from a + logging call, with values from the logging call taking precedence. I.e. the + behavior is that of ``merge_extra=True`` in Python >= 3.13. + """ + + def __init__(self, logger: logging.Logger, extra: Mapping[str, Any] | None) -> None: + """Create the logger adapter.""" + super().__init__(logger, extra or {}) + self.workflow_info_on_message = True + self.workflow_info_on_extra = True + self.full_workflow_info_on_extra = False + self.log_during_replay = False + self.disable_sandbox = False + + def process( + self, msg: Any, kwargs: MutableMapping[str, Any] + ) -> tuple[Any, MutableMapping[str, Any]]: + """Override to add workflow details.""" + msg_extra: dict[str, Any] = {} + extra: dict[str, Any] = {} + + if ( + self.workflow_info_on_message + or self.workflow_info_on_extra + or self.full_workflow_info_on_extra + ): + runtime = _Runtime.maybe_current() + update_info = current_update_info() + msg_extra, extra = _build_log_context( + runtime.logger_details if runtime else None, + update_info._logger_details if update_info else None, + workflow_info_on_message=self.workflow_info_on_message, + workflow_info_on_extra=self.workflow_info_on_extra, + full_workflow_info=runtime.workflow_info() + if runtime and self.full_workflow_info_on_extra + else None, + ) + + kwargs["extra"] = {**extra, **(kwargs.get("extra") or {})} + if msg_extra: + msg = f"{msg} ({msg_extra})" + return msg, kwargs + + def log( + self, + level: int, + msg: object, + *args: Any, + stacklevel: int = 1, + **kwargs: Any, + ): + """Override to potentially disable the sandbox.""" + if sys.version_info < (3, 11) and stacklevel == 1: + # An additional stacklevel is needed on 3.10 because it doesn't skip internal frames until after stacklevel + # is decremented, so it needs an additional stacklevel to skip the internal frame. + stacklevel += 1 # type: ignore[reportUnreachable] + stacklevel += 1 + if self.disable_sandbox: + with unsafe.sandbox_unrestricted(): + with unsafe.imports_passed_through(): + super().log(level, msg, *args, stacklevel=stacklevel, **kwargs) + else: + super().log(level, msg, *args, stacklevel=stacklevel, **kwargs) + + def isEnabledFor(self, level: int) -> bool: + """Override to ignore replay logs.""" + if not self.log_during_replay and unsafe.is_replaying_history_events(): + return False + return super().isEnabledFor(level) + + @property + def base_logger(self) -> logging.Logger: + """Underlying logger usable for actions such as adding + handlers/formatters. + """ + return self.logger + + def unsafe_disable_sandbox(self, value: bool = True): + """Disable the sandbox during log processing. + Can be turned back on with unsafe_disable_sandbox(False). + """ + self.disable_sandbox = value + + +logger = LoggerAdapter(logging.getLogger("temporalio.workflow"), None) +"""Logger that will have contextual workflow details embedded. + +Logs are skipped during replay by default. +""" diff --git a/temporalio/workflow/_workflow_ops.py b/temporalio/workflow/_workflow_ops.py new file mode 100644 index 000000000..f80ca1bdb --- /dev/null +++ b/temporalio/workflow/_workflow_ops.py @@ -0,0 +1,1023 @@ +from __future__ import annotations + +from collections.abc import Awaitable, Callable, Mapping, Sequence +from datetime import timedelta +from enum import IntEnum +from typing import Any, Concatenate, Generic, NoReturn, TypedDict, overload + +import temporalio.bridge.proto.child_workflow +import temporalio.common + +from ..types import ( + MethodAsyncNoParam, + MethodAsyncSingleParam, + MethodSyncOrAsyncNoParam, + MethodSyncOrAsyncSingleParam, + MultiParamSpec, + ParamType, + ReturnType, + SelfType, +) +from ._activities import _AsyncioTask +from ._context import _Runtime, uuid4 +from ._exceptions import ContinueAsNewVersioningBehavior, VersioningIntent + +__all__ = [ + "ChildWorkflowCancellationType", + "ChildWorkflowConfig", + "ChildWorkflowHandle", + "ContinueAsNewError", + "ExternalWorkflowHandle", + "ParentClosePolicy", + "all_handlers_finished", + "continue_as_new", + "execute_child_workflow", + "get_dynamic_query_handler", + "get_dynamic_signal_handler", + "get_dynamic_update_handler", + "get_external_workflow_handle", + "get_external_workflow_handle_for", + "get_query_handler", + "get_signal_handler", + "get_update_handler", + "set_dynamic_query_handler", + "set_dynamic_signal_handler", + "set_dynamic_update_handler", + "set_query_handler", + "set_signal_handler", + "set_update_handler", + "start_child_workflow", +] + + +class ChildWorkflowHandle(_AsyncioTask[ReturnType], Generic[SelfType, ReturnType]): # type: ignore[type-var] + """Handle for interacting with a child workflow. + + This is created via :py:func:`start_child_workflow`. + + This extends :py:class:`asyncio.Task` and supports all task features. + """ + + @property + def id(self) -> str: + """ID for the workflow.""" + raise NotImplementedError + + @property + def first_execution_run_id(self) -> str | None: + """Run ID for the workflow.""" + raise NotImplementedError + + @overload + async def signal( + self, + signal: MethodSyncOrAsyncNoParam[SelfType, None], + ) -> None: ... + + @overload + async def signal( + self, + signal: MethodSyncOrAsyncSingleParam[SelfType, ParamType, None], + arg: ParamType, + ) -> None: ... + + @overload + async def signal( + self, + signal: Callable[Concatenate[SelfType, MultiParamSpec], Awaitable[None] | None], + *, + args: Sequence[Any], + ) -> None: ... + + @overload + async def signal( + self, + signal: str, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + ) -> None: ... + + async def signal( + self, + signal: str | Callable, # type: ignore[reportUnusedParameter] + arg: Any = temporalio.common._arg_unset, # type: ignore[reportUnusedParameter] + *, + args: Sequence[Any] = [], # type: ignore[reportUnusedParameter] + ) -> None: + """Signal this child workflow. + + Args: + signal: Name or method reference for the signal. + arg: Single argument to the signal. + args: Multiple arguments to the signal. Cannot be set if arg is. + + """ + raise NotImplementedError + + +class ChildWorkflowCancellationType(IntEnum): + """How a child workflow cancellation should be handled.""" + + ABANDON = int( + temporalio.bridge.proto.child_workflow.ChildWorkflowCancellationType.ABANDON + ) + TRY_CANCEL = int( + temporalio.bridge.proto.child_workflow.ChildWorkflowCancellationType.TRY_CANCEL + ) + WAIT_CANCELLATION_COMPLETED = int( + temporalio.bridge.proto.child_workflow.ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED + ) + WAIT_CANCELLATION_REQUESTED = int( + temporalio.bridge.proto.child_workflow.ChildWorkflowCancellationType.WAIT_CANCELLATION_REQUESTED + ) + + +class ParentClosePolicy(IntEnum): + """How a child workflow should be handled when the parent closes.""" + + UNSPECIFIED = int( + temporalio.bridge.proto.child_workflow.ParentClosePolicy.PARENT_CLOSE_POLICY_UNSPECIFIED + ) + TERMINATE = int( + temporalio.bridge.proto.child_workflow.ParentClosePolicy.PARENT_CLOSE_POLICY_TERMINATE + ) + ABANDON = int( + temporalio.bridge.proto.child_workflow.ParentClosePolicy.PARENT_CLOSE_POLICY_ABANDON + ) + REQUEST_CANCEL = int( + temporalio.bridge.proto.child_workflow.ParentClosePolicy.PARENT_CLOSE_POLICY_REQUEST_CANCEL + ) + + +class ChildWorkflowConfig(TypedDict, total=False): + """TypedDict of config that can be used for :py:func:`start_child_workflow` + and :py:func:`execute_child_workflow`. + """ + + id: str | None + task_queue: str | None + cancellation_type: ChildWorkflowCancellationType + parent_close_policy: ParentClosePolicy + execution_timeout: timedelta | None + run_timeout: timedelta | None + task_timeout: timedelta | None + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy + retry_policy: temporalio.common.RetryPolicy | None + cron_schedule: str + memo: Mapping[str, Any] | None + search_attributes: None | ( + temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes + ) + versioning_intent: VersioningIntent | None + static_summary: str | None + static_details: str | None + priority: temporalio.common.Priority + + +# Overload for no-param workflow +@overload +async def start_child_workflow( + workflow: MethodAsyncNoParam[SelfType, ReturnType], + *, + id: str | None = None, + task_queue: str | None = None, + cancellation_type: ChildWorkflowCancellationType = ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED, + parent_close_policy: ParentClosePolicy = ParentClosePolicy.TERMINATE, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes + ) = None, + versioning_intent: VersioningIntent | None = None, + static_summary: str | None = None, + static_details: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ChildWorkflowHandle[SelfType, ReturnType]: ... + + +# Overload for single-param workflow +@overload +async def start_child_workflow( + workflow: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], + arg: ParamType, + *, + id: str | None = None, + task_queue: str | None = None, + cancellation_type: ChildWorkflowCancellationType = ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED, + parent_close_policy: ParentClosePolicy = ParentClosePolicy.TERMINATE, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes + ) = None, + versioning_intent: VersioningIntent | None = None, + static_summary: str | None = None, + static_details: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ChildWorkflowHandle[SelfType, ReturnType]: ... + + +# Overload for multi-param workflow +@overload +async def start_child_workflow( + workflow: Callable[Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType]], + *, + args: Sequence[Any], + id: str | None = None, + task_queue: str | None = None, + cancellation_type: ChildWorkflowCancellationType = ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED, + parent_close_policy: ParentClosePolicy = ParentClosePolicy.TERMINATE, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes + ) = None, + versioning_intent: VersioningIntent | None = None, + static_summary: str | None = None, + static_details: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ChildWorkflowHandle[SelfType, ReturnType]: ... + + +# Overload for string-name workflow +@overload +async def start_child_workflow( + workflow: str, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str | None = None, + task_queue: str | None = None, + result_type: type | None = None, + cancellation_type: ChildWorkflowCancellationType = ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED, + parent_close_policy: ParentClosePolicy = ParentClosePolicy.TERMINATE, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes + ) = None, + versioning_intent: VersioningIntent | None = None, + static_summary: str | None = None, + static_details: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ChildWorkflowHandle[Any, Any]: ... + + +async def start_child_workflow( + workflow: Any, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str | None = None, + task_queue: str | None = None, + result_type: type | None = None, + cancellation_type: ChildWorkflowCancellationType = ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED, + parent_close_policy: ParentClosePolicy = ParentClosePolicy.TERMINATE, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes + ) = None, + versioning_intent: VersioningIntent | None = None, + static_summary: str | None = None, + static_details: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ChildWorkflowHandle[Any, Any]: + """Start a child workflow and return its handle. + + Args: + workflow: String name or class method decorated with ``@workflow.run`` + for the workflow to start. + arg: Single argument to the child workflow. + args: Multiple arguments to the child workflow. Cannot be set if arg is. + id: Optional unique identifier for the workflow execution. If not set, + defaults to :py:func:`uuid4`. + task_queue: Task queue to run the workflow on. Defaults to the current + workflow's task queue. + result_type: For string workflows, this can set the specific result type + hint to deserialize into. + cancellation_type: How the child workflow will react to cancellation. + parent_close_policy: How to handle the child workflow when the parent + workflow closes. + execution_timeout: Total workflow execution timeout including + retries and continue as new. + run_timeout: Timeout of a single workflow run. + task_timeout: Timeout of a single workflow task. + id_reuse_policy: How already-existing IDs are treated. + retry_policy: Retry policy for the workflow. + cron_schedule: See https://docs.temporal.io/docs/content/what-is-a-temporal-cron-job/ + memo: Memo for the workflow. + search_attributes: Search attributes for the workflow. The dictionary + form of this is DEPRECATED. + versioning_intent: When using the Worker Versioning feature, specifies whether this Child + Workflow should run on a worker with a compatible Build Id or not. + Deprecated: Use Worker Deployment versioning instead. + static_summary: A single-line fixed summary for this child workflow execution that may appear + in the UI/CLI. This can be in single-line Temporal markdown format. + static_details: General fixed details for this child workflow execution that may appear in + UI/CLI. This can be in Temporal markdown format and can span multiple lines. This is + a fixed value on the workflow that cannot be updated. For details that can be + updated, use :py:meth:`get_current_details` within the workflow. + priority: Priority to use for this workflow. + + Returns: + A workflow handle to the started/existing workflow. + """ + temporalio.common._warn_on_deprecated_search_attributes(search_attributes) + return await _Runtime.current().workflow_start_child_workflow( + workflow, + *temporalio.common._arg_or_args(arg, args), + id=id or str(uuid4()), + task_queue=task_queue, + result_type=result_type, + cancellation_type=cancellation_type, + parent_close_policy=parent_close_policy, + execution_timeout=execution_timeout, + run_timeout=run_timeout, + task_timeout=task_timeout, + id_reuse_policy=id_reuse_policy, + retry_policy=retry_policy, + cron_schedule=cron_schedule, + memo=memo, + search_attributes=search_attributes, + versioning_intent=versioning_intent, + static_summary=static_summary, + static_details=static_details, + priority=priority, + ) + + +# Overload for no-param workflow +@overload +async def execute_child_workflow( + workflow: MethodAsyncNoParam[SelfType, ReturnType], + *, + id: str | None = None, + task_queue: str | None = None, + cancellation_type: ChildWorkflowCancellationType = ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED, + parent_close_policy: ParentClosePolicy = ParentClosePolicy.TERMINATE, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes + ) = None, + versioning_intent: VersioningIntent | None = None, + static_summary: str | None = None, + static_details: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ReturnType: ... + + +# Overload for single-param workflow +@overload +async def execute_child_workflow( + workflow: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], + arg: ParamType, + *, + id: str | None = None, + task_queue: str | None = None, + cancellation_type: ChildWorkflowCancellationType = ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED, + parent_close_policy: ParentClosePolicy = ParentClosePolicy.TERMINATE, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes + ) = None, + versioning_intent: VersioningIntent | None = None, + static_summary: str | None = None, + static_details: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ReturnType: ... + + +# Overload for multi-param workflow +@overload +async def execute_child_workflow( + workflow: Callable[Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType]], + *, + args: Sequence[Any], + id: str | None = None, + task_queue: str | None = None, + cancellation_type: ChildWorkflowCancellationType = ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED, + parent_close_policy: ParentClosePolicy = ParentClosePolicy.TERMINATE, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes + ) = None, + versioning_intent: VersioningIntent | None = None, + static_summary: str | None = None, + static_details: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> ReturnType: ... + + +# Overload for string-name workflow +@overload +async def execute_child_workflow( + workflow: str, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str | None = None, + task_queue: str | None = None, + result_type: type | None = None, + cancellation_type: ChildWorkflowCancellationType = ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED, + parent_close_policy: ParentClosePolicy = ParentClosePolicy.TERMINATE, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes + ) = None, + versioning_intent: VersioningIntent | None = None, + static_summary: str | None = None, + static_details: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> Any: ... + + +async def execute_child_workflow( + workflow: Any, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str | None = None, + task_queue: str | None = None, + result_type: type | None = None, + cancellation_type: ChildWorkflowCancellationType = ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED, + parent_close_policy: ParentClosePolicy = ParentClosePolicy.TERMINATE, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes + ) = None, + versioning_intent: VersioningIntent | None = None, + static_summary: str | None = None, + static_details: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, +) -> Any: + """Start a child workflow and wait for completion. + + This is a shortcut for ``await (await`` :py:meth:`start_child_workflow` ``)``. + """ + temporalio.common._warn_on_deprecated_search_attributes(search_attributes) + # We call the runtime directly instead of top-level start_child_workflow to + # ensure we don't miss new parameters + handle = await _Runtime.current().workflow_start_child_workflow( + workflow, + *temporalio.common._arg_or_args(arg, args), + id=id or str(uuid4()), + task_queue=task_queue, + result_type=result_type, + cancellation_type=cancellation_type, + parent_close_policy=parent_close_policy, + execution_timeout=execution_timeout, + run_timeout=run_timeout, + task_timeout=task_timeout, + id_reuse_policy=id_reuse_policy, + retry_policy=retry_policy, + cron_schedule=cron_schedule, + memo=memo, + search_attributes=search_attributes, + versioning_intent=versioning_intent, + static_summary=static_summary, + static_details=static_details, + priority=priority, + ) + return await handle + + +class ExternalWorkflowHandle(Generic[SelfType]): + """Handle for interacting with an external workflow. + + This is created via :py:func:`get_external_workflow_handle` or + :py:func:`get_external_workflow_handle_for`. + """ + + @property + def id(self) -> str: + """ID for the workflow.""" + raise NotImplementedError + + @property + def run_id(self) -> str | None: + """Run ID for the workflow if any.""" + raise NotImplementedError + + @overload + async def signal( + self, + signal: MethodSyncOrAsyncNoParam[SelfType, None], + ) -> None: ... + + @overload + async def signal( + self, + signal: MethodSyncOrAsyncSingleParam[SelfType, ParamType, None], + arg: ParamType, + ) -> None: ... + + @overload + async def signal( + self, + signal: str, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + ) -> None: ... + + async def signal( + self, + signal: str | Callable, # type: ignore[reportUnusedParameter] + arg: Any = temporalio.common._arg_unset, # type: ignore[reportUnusedParameter] + *, + args: Sequence[Any] = [], # type: ignore[reportUnusedParameter] + ) -> None: + """Signal this external workflow. + + Args: + signal: Name or method reference for the signal. + arg: Single argument to the signal. + args: Multiple arguments to the signal. Cannot be set if arg is. + + """ + raise NotImplementedError + + async def cancel(self, *, reason: str = "") -> None: # pyright: ignore[reportUnusedParameter] + """Send a cancellation request to this external workflow. + + This will fail if the workflow cannot accept the request (e.g. if the + workflow is not found). + + Args: + reason: Reason recorded with the cancellation request. Available in + the target workflow via :py:func:`cancellation_reason`. + """ + raise NotImplementedError + + +def get_external_workflow_handle( + workflow_id: str, + *, + run_id: str | None = None, +) -> ExternalWorkflowHandle[Any]: + """Get a workflow handle to an existing workflow by its ID. + + Args: + workflow_id: Workflow ID to get a handle to. + run_id: Optional run ID for the workflow. + + Returns: + The external workflow handle. + """ + return _Runtime.current().workflow_get_external_workflow_handle( + workflow_id, run_id=run_id + ) + + +def get_external_workflow_handle_for( + workflow: MethodAsyncNoParam[SelfType, Any] # type: ignore[reportUnusedParameter] + | MethodAsyncSingleParam[SelfType, Any, Any], + workflow_id: str, + *, + run_id: str | None = None, +) -> ExternalWorkflowHandle[SelfType]: + """Get a typed workflow handle to an existing workflow by its ID. + + This is the same as :py:func:`get_external_workflow_handle` but typed. Note, + the workflow type given is not validated, it is only for typing. + + Args: + workflow: The workflow run method to use for typing the handle. + workflow_id: Workflow ID to get a handle to. + run_id: Optional run ID for the workflow. + + Returns: + The external workflow handle. + """ + return get_external_workflow_handle(workflow_id, run_id=run_id) + + +class ContinueAsNewError(BaseException): + """Error thrown by :py:func:`continue_as_new`. + + This should not be caught, but instead be allowed to throw out of the + workflow which then triggers the continue as new. This should never be + instantiated directly. + """ + + def __init__(self, *args: object) -> None: + """Direct instantiation is disabled. Use :py:func:`continue_as_new`.""" + if type(self) is ContinueAsNewError: + raise RuntimeError("Cannot instantiate ContinueAsNewError directly") + super().__init__(*args) + + +# Overload for self (unfortunately, cannot type args) +@overload +def continue_as_new( + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + task_queue: str | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + backoff_start_interval: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes + ) = None, + versioning_intent: VersioningIntent | None = None, + initial_versioning_behavior: ContinueAsNewVersioningBehavior | None = None, +) -> NoReturn: ... + + +# Overload for no-param workflow +@overload +def continue_as_new( + *, + workflow: MethodAsyncNoParam[SelfType, Any], + task_queue: str | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + backoff_start_interval: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes + ) = None, + versioning_intent: VersioningIntent | None = None, + initial_versioning_behavior: ContinueAsNewVersioningBehavior | None = None, +) -> NoReturn: ... + + +# Overload for single-param workflow +@overload +def continue_as_new( + arg: ParamType, + *, + workflow: MethodAsyncSingleParam[SelfType, ParamType, Any], + task_queue: str | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + backoff_start_interval: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes + ) = None, + versioning_intent: VersioningIntent | None = None, + initial_versioning_behavior: ContinueAsNewVersioningBehavior | None = None, +) -> NoReturn: ... + + +# Overload for multi-param workflow +@overload +def continue_as_new( + *, + workflow: Callable[Concatenate[SelfType, MultiParamSpec], Awaitable[Any]], + args: Sequence[Any], + task_queue: str | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + backoff_start_interval: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes + ) = None, + versioning_intent: VersioningIntent | None = None, + initial_versioning_behavior: ContinueAsNewVersioningBehavior | None = None, +) -> NoReturn: ... + + +# Overload for string-name workflow +@overload +def continue_as_new( + *, + workflow: str, + args: Sequence[Any] = [], + task_queue: str | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + backoff_start_interval: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes + ) = None, + versioning_intent: VersioningIntent | None = None, + initial_versioning_behavior: ContinueAsNewVersioningBehavior | None = None, +) -> NoReturn: ... + + +def continue_as_new( + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + workflow: None | Callable | str = None, + task_queue: str | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + backoff_start_interval: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes + ) = None, + versioning_intent: VersioningIntent | None = None, + initial_versioning_behavior: ContinueAsNewVersioningBehavior | None = None, +) -> NoReturn: + """Stop the workflow immediately and continue as new. + + Args: + arg: Single argument to the continued workflow. + args: Multiple arguments to the continued workflow. Cannot be set if arg + is. + workflow: Specific workflow to continue to. Defaults to the current + workflow. + task_queue: Task queue to run the workflow on. Defaults to the current + workflow's task queue. + run_timeout: Timeout of a single workflow run. Defaults to the current + workflow's run timeout. + task_timeout: Timeout of a single workflow task. Defaults to the current + workflow's task timeout. + backoff_start_interval: Delay before the first workflow task of the + continued run is scheduled. + memo: Memo for the workflow. Defaults to the current workflow's memo. + search_attributes: Search attributes for the workflow. Defaults to the + current workflow's search attributes. The dictionary form of this is + DEPRECATED. + versioning_intent: When using the Worker Versioning feature, specifies whether this Workflow + should Continue-as-New onto a worker with a compatible Build Id or not. + Deprecated: Use Worker Deployment versioning instead. + + Returns: + Never returns, always raises a :py:class:`ContinueAsNewError`. + + Raises: + ContinueAsNewError: Always raised by this function. Should not be caught + but instead be allowed to + """ + temporalio.common._warn_on_deprecated_search_attributes(search_attributes) + _Runtime.current().workflow_continue_as_new( + *temporalio.common._arg_or_args(arg, args), + workflow=workflow, + task_queue=task_queue, + run_timeout=run_timeout, + task_timeout=task_timeout, + backoff_start_interval=backoff_start_interval, + retry_policy=retry_policy, + memo=memo, + search_attributes=search_attributes, + versioning_intent=versioning_intent, + initial_versioning_behavior=initial_versioning_behavior, + ) + + +def get_signal_handler(name: str) -> Callable | None: + """Get the signal handler for the given name if any. + + This includes handlers created via the ``@workflow.signal`` decorator. + + Args: + name: Name of the signal. + + Returns: + Callable for the signal if any. If a handler is not found for the name, + this will not return the dynamic handler even if there is one. + """ + return _Runtime.current().workflow_get_signal_handler(name) + + +def set_signal_handler(name: str, handler: Callable | None) -> None: + """Set or unset the signal handler for the given name. + + This overrides any existing handlers for the given name, including handlers + created via the ``@workflow.signal`` decorator. + + When set, all unhandled past signals for the given name are immediately sent + to the handler. + + Args: + name: Name of the signal. + handler: Callable to set or None to unset. + """ + _Runtime.current().workflow_set_signal_handler(name, handler) + + +def get_dynamic_signal_handler() -> Callable | None: + """Get the dynamic signal handler if any. + + This includes dynamic handlers created via the ``@workflow.signal`` + decorator. + + Returns: + Callable for the dynamic signal handler if any. + """ + return _Runtime.current().workflow_get_signal_handler(None) + + +def set_dynamic_signal_handler(handler: Callable | None) -> None: + """Set or unset the dynamic signal handler. + + This overrides the existing dynamic handler even if it was created via the + ``@workflow.signal`` decorator. + + When set, all unhandled past signals are immediately sent to the handler. + + Args: + handler: Callable to set or None to unset. + """ + _Runtime.current().workflow_set_signal_handler(None, handler) + + +def get_query_handler(name: str) -> Callable | None: + """Get the query handler for the given name if any. + + This includes handlers created via the ``@workflow.query`` decorator. + + Args: + name: Name of the query. + + Returns: + Callable for the query if any. If a handler is not found for the name, + this will not return the dynamic handler even if there is one. + """ + return _Runtime.current().workflow_get_query_handler(name) + + +def set_query_handler(name: str, handler: Callable | None) -> None: + """Set or unset the query handler for the given name. + + This overrides any existing handlers for the given name, including handlers + created via the ``@workflow.query`` decorator. + + Args: + name: Name of the query. + handler: Callable to set or None to unset. + """ + _Runtime.current().workflow_set_query_handler(name, handler) + + +def get_dynamic_query_handler() -> Callable | None: + """Get the dynamic query handler if any. + + This includes dynamic handlers created via the ``@workflow.query`` + decorator. + + Returns: + Callable for the dynamic query handler if any. + """ + return _Runtime.current().workflow_get_query_handler(None) + + +def set_dynamic_query_handler(handler: Callable | None) -> None: + """Set or unset the dynamic query handler. + + This overrides the existing dynamic handler even if it was created via the + ``@workflow.query`` decorator. + + Args: + handler: Callable to set or None to unset. + """ + _Runtime.current().workflow_set_query_handler(None, handler) + + +def get_update_handler(name: str) -> Callable | None: + """Get the update handler for the given name if any. + + This includes handlers created via the ``@workflow.update`` decorator. + + Args: + name: Name of the update. + + Returns: + Callable for the update if any. If a handler is not found for the name, + this will not return the dynamic handler even if there is one. + """ + return _Runtime.current().workflow_get_update_handler(name) + + +def set_update_handler( + name: str, handler: Callable | None, *, validator: Callable | None = None +) -> None: + """Set or unset the update handler for the given name. + + This overrides any existing handlers for the given name, including handlers + created via the ``@workflow.update`` decorator. + + Args: + name: Name of the update. + handler: Callable to set or None to unset. + validator: Callable to set or None to unset as the update validator. + """ + _Runtime.current().workflow_set_update_handler(name, handler, validator) + + +def get_dynamic_update_handler() -> Callable | None: + """Get the dynamic update handler if any. + + This includes dynamic handlers created via the ``@workflow.update`` + decorator. + + Returns: + Callable for the dynamic update handler if any. + """ + return _Runtime.current().workflow_get_update_handler(None) + + +def set_dynamic_update_handler( + handler: Callable | None, *, validator: Callable | None = None +) -> None: + """Set or unset the dynamic update handler. + + This overrides the existing dynamic handler even if it was created via the + ``@workflow.update`` decorator. + + Args: + handler: Callable to set or None to unset. + validator: Callable to set or None to unset as the update validator. + """ + _Runtime.current().workflow_set_update_handler(None, handler, validator) + + +def all_handlers_finished() -> bool: + """Whether update and signal handlers have finished executing. + + Consider waiting on this condition before workflow return or continue-as-new, to prevent + interruption of in-progress handlers by workflow exit: + ``await workflow.wait_condition(lambda: workflow.all_handlers_finished())`` + + Returns: + True if there are no in-progress update or signal handler executions. + """ + return _Runtime.current().workflow_all_handlers_finished() diff --git a/tests/__init__.py b/tests/__init__.py index 6a624f6ca..af97849fe 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1 @@ -DEV_SERVER_DOWNLOAD_VERSION = "default" +DEV_SERVER_DOWNLOAD_VERSION = "v1.7.4-standalone-nexus-operations" diff --git a/tests/api/test_grpc_stub.py b/tests/api/test_grpc_stub.py index 25c3f5bee..64ac90256 100644 --- a/tests/api/test_grpc_stub.py +++ b/tests/api/test_grpc_stub.py @@ -1,6 +1,9 @@ +import re +from collections.abc import Mapping from datetime import timedelta -from typing import Mapping +from typing import Any, cast +import pytest from google.protobuf.empty_pb2 import Empty from google.protobuf.timestamp_pb2 import Timestamp from grpc.aio import ServicerContext @@ -36,9 +39,9 @@ def assert_time_remaining(context: ServicerContext, expected: int) -> None: class SimpleWorkflowServer(WorkflowServiceServicer): def __init__(self) -> None: super().__init__() - self.last_metadata: Mapping[str, str] = {} + self.last_metadata: Mapping[str, str | bytes] = {} - def assert_last_metadata(self, expected: Mapping[str, str]) -> None: + def assert_last_metadata(self, expected: Mapping[str, str | bytes]) -> None: for k, v in expected.items(): assert self.last_metadata.get(k) == v @@ -47,7 +50,7 @@ async def GetSystemInfo( # type: ignore # https://github.com/nipunn1313/mypy-pr request: GetSystemInfoRequest, context: ServicerContext, ) -> GetSystemInfoResponse: - self.last_metadata = dict(context.invocation_metadata()) + self.last_metadata = dict(context.invocation_metadata()) # type: ignore[reportCallIssue,reportAttributeAccessIssue,reportArgumentType] return GetSystemInfoResponse() async def CountWorkflowExecutions( # type: ignore # https://github.com/nipunn1313/mypy-protobuf/issues/216 @@ -55,7 +58,7 @@ async def CountWorkflowExecutions( # type: ignore # https://github.com/nipunn13 request: CountWorkflowExecutionsRequest, context: ServicerContext, ) -> CountWorkflowExecutionsResponse: - self.last_metadata = dict(context.invocation_metadata()) + self.last_metadata = dict(context.invocation_metadata()) # type: ignore[reportCallIssue,reportAttributeAccessIssue,reportArgumentType] assert_time_remaining(context, 123) assert request.namespace == "my namespace" assert request.query == "my query" @@ -89,7 +92,7 @@ async def test_python_grpc_stub(): # Start server server = grpc_server() workflow_server = SimpleWorkflowServer() # type: ignore[abstract] - add_WorkflowServiceServicer_to_server(workflow_server, server) + add_WorkflowServiceServicer_to_server(workflow_server, server) # type: ignore[reportArgumentType] add_OperatorServiceServicer_to_server(SimpleOperatorServer(), server) # type: ignore[abstract] add_TestServiceServicer_to_server(SimpleTestServer(), server) # type: ignore[abstract] port = server.add_insecure_port("[::]:0") @@ -118,7 +121,7 @@ async def test_grpc_metadata(): # Start server server = grpc_server() workflow_server = SimpleWorkflowServer() # type: ignore[abstract] - add_WorkflowServiceServicer_to_server(workflow_server, server) + add_WorkflowServiceServicer_to_server(workflow_server, server) # type: ignore[reportArgumentType] port = server.add_insecure_port("[::]:0") await server.start() @@ -127,6 +130,7 @@ async def test_grpc_metadata(): f"localhost:{port}", api_key="my-api-key", rpc_metadata={"my-meta-key": "my-meta-val"}, + tls=False, ) workflow_server.assert_last_metadata( { @@ -135,6 +139,133 @@ async def test_grpc_metadata(): } ) + # Binary metadata values should work: + await client.workflow_service.get_system_info( + GetSystemInfoRequest(), + metadata={ + "my-binary-key-bin": b"\x00\x01", + }, + ) + workflow_server.assert_last_metadata( + { + "authorization": "Bearer my-api-key", + "my-meta-key": "my-meta-val", + "my-binary-key-bin": b"\x00\x01", + } + ) + + # Binary metadata should be configurable on the client: + client.rpc_metadata = { + "my-binary-key-bin": b"\x00\x01", + "my-binary-key2-bin": b"\x02\x03", + } + await client.workflow_service.get_system_info( + GetSystemInfoRequest(), + metadata={ + "my-binary-key-bin": b"abc", + }, + ) + workflow_server.assert_last_metadata( + { + "authorization": "Bearer my-api-key", + "my-binary-key-bin": b"abc", + "my-binary-key2-bin": b"\x02\x03", + } + ) + + # Setting invalid RPC metadata should raise: + with pytest.raises( + ValueError, + match="Invalid binary header key 'my-ascii-key': invalid gRPC metadata key name", + ): + client.rpc_metadata = { + "my-ascii-key": b"binary-value", + } + with pytest.raises( + ValueError, + match="Invalid ASCII header key 'my-binary-key-bin': invalid gRPC metadata key name", + ): + client.rpc_metadata = { + "my-binary-key-bin": "ascii-value", + } + + # Making a request with invalid RPC metadata should raise: + with pytest.raises( + ValueError, + match="Invalid metadata value for ASCII key my-ascii-key: expected str", + ): + await client.workflow_service.get_system_info( + GetSystemInfoRequest(), + metadata={ + "my-ascii-key": b"binary-value", + }, + ) + with pytest.raises( + ValueError, + match="Invalid metadata value for binary key my-binary-key-bin: expected bytes", + ): + await client.workflow_service.get_system_info( + GetSystemInfoRequest(), + metadata={ + "my-binary-key-bin": "ascii-value", + }, + ) + + # Passing in non-`str | bytes` should raise: + with pytest.raises(TypeError) as err: + await client.workflow_service.get_system_info( + GetSystemInfoRequest(), + metadata={ + # Not a valid header: + "my-int-key": cast(Any, 256), + }, + ) + cause = err.value.__cause__ + assert isinstance(cause, TypeError) + assert re.match( + re.escape(r"failed to extract enum RpcMetadataValue ('str | bytes')"), + str(cause), + ) + with pytest.raises( + TypeError, + match=re.escape(r"failed to extract enum RpcMetadataValue ('str | bytes')"), + ) as err: + client.rpc_metadata = { + "my-binary-key-bin": cast(Any, 256), + } + + # Setting invalid RPC metadata in a mixed client will partially fail: + client.rpc_metadata = { + "x-my-binary-bin": b"\x00", + "x-my-ascii": "foo", + } + assert client.rpc_metadata == { + "x-my-binary-bin": b"\x00", + "x-my-ascii": "foo", + } + with pytest.raises( + ValueError, + match="Invalid binary header key 'x-invalid-ascii-with-bin-value': invalid gRPC metadata key name", + ): + client.rpc_metadata = { + "x-invalid-ascii-with-bin-value": b"not-ascii", + "x-my-ascii": "bar", + } + assert client.rpc_metadata == { + "x-my-binary-bin": b"\x00", + "x-my-ascii": "foo", + } + await client.workflow_service.get_system_info(GetSystemInfoRequest()) + workflow_server.assert_last_metadata( + { + "authorization": "Bearer my-api-key", + # This is inconsistent with what `client.rpc_metadata` returns + # (`x-my-ascii` was updated): + "x-my-binary-bin": b"\x00", + "x-my-ascii": "bar", + } + ) + # Overwrite API key via client RPC metadata, confirm there client.rpc_metadata = { "authorization": "my-auth-val1", diff --git a/tests/bridge/test_runtime.py b/tests/bridge/test_runtime.py index af9c7006d..2a1c48834 100644 --- a/tests/bridge/test_runtime.py +++ b/tests/bridge/test_runtime.py @@ -1,6 +1,5 @@ from threading import Event, Thread from time import sleep -from typing import Optional from temporalio.bridge.runtime import Runtime @@ -11,7 +10,7 @@ class SomeException(Exception): def test_bridge_runtime_raise_in_thread(): waiting = Event() - exc_in_thread: Optional[BaseException] = None + exc_in_thread: BaseException | None = None def wait_forever(): try: diff --git a/tests/conftest.py b/tests/conftest.py index fa868530a..a9c6abb89 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,105 +1,142 @@ import asyncio +import multiprocessing.context import os import sys -from typing import AsyncGenerator +from collections.abc import AsyncGenerator, Iterator +import opentelemetry.trace import pytest import pytest_asyncio +from opentelemetry.util._once import Once + +from temporalio.client import Client +from temporalio.envconfig import ClientConfigProfile +from temporalio.testing import WorkflowEnvironment +from temporalio.worker import SharedStateManager +from tests.helpers.worker import ExternalPythonWorker, ExternalWorker from . import DEV_SERVER_DOWNLOAD_VERSION # If there is an integration test environment variable set, we must remove the # first path from the sys.path so we can import the wheel instead if os.getenv("TEMPORAL_INTEGRATION_TEST"): - assert ( - sys.path[0] == os.getcwd() - ), "Expected first sys.path to be the current working dir" + assert sys.path[0] == os.getcwd(), ( + "Expected first sys.path to be the current working dir" + ) sys.path.pop(0) # Import temporalio and confirm it is prefixed with virtual env import temporalio - assert temporalio.__file__.startswith( - sys.prefix - ), f"Expected {temporalio.__file__} to be in {sys.prefix}" + assert temporalio.__file__.startswith(sys.prefix), ( + f"Expected {temporalio.__file__} to be in {sys.prefix}" + ) -# Unless specifically overridden, we expect tests to run under protobuf 4.x/5.x lib +# Unless specifically overridden, we expect tests to run under protobuf 4.x/5.x/6.x/7.x lib import google.protobuf protobuf_version = google.protobuf.__version__ if os.getenv("TEMPORAL_TEST_PROTO3"): - assert protobuf_version.startswith( - "3." - ), f"Expected protobuf 3.x, got {protobuf_version}" + assert protobuf_version.startswith("3."), ( + f"Expected protobuf 3.x, got {protobuf_version}" + ) else: - assert protobuf_version.startswith("4.") or protobuf_version.startswith( - "5." - ), f"Expected protobuf 4.x/5.x, got {protobuf_version}" + assert ( + protobuf_version.startswith("4.") + or protobuf_version.startswith("5.") + or protobuf_version.startswith("6.") + or protobuf_version.startswith("7.") + ), f"Expected protobuf 4.x/5.x/6.x/7.x, got {protobuf_version}" -from temporalio.client import Client -from temporalio.testing import WorkflowEnvironment -from tests.helpers.worker import ExternalPythonWorker, ExternalWorker + +def pytest_runtest_setup(item): # type: ignore[reportMissingParameterType] + """Print a newline so that custom printed output starts on new line.""" + if item.config.getoption("-s"): + print() -def pytest_addoption(parser): +def pytest_addoption(parser): # type: ignore[reportMissingParameterType] parser.addoption( "-E", "--workflow-environment", default="local", - help="Which workflow environment to use ('local', 'time-skipping', or ip:port for existing server)", + help="Which workflow environment to use ('local', 'time-skipping', 'envconfig', or ip:port for existing server)", ) +def _uses_envconfig_server(env_type: str) -> bool: + return env_type == "envconfig" + + +def pytest_configure(config: pytest.Config) -> None: + config.addinivalue_line( + "markers", + "requires_local_server: test requires local-server-only behavior and cannot run against an envconfig server", + ) + + +def pytest_collection_modifyitems( + config: pytest.Config, items: list[pytest.Item] +) -> None: + if not _uses_envconfig_server(config.getoption("--workflow-environment")): + return + skip_local_only = pytest.mark.skip( + reason="requires a local Temporal server, not the configured envconfig server" + ) + for item in items: + if item.get_closest_marker("requires_local_server"): + item.add_marker(skip_local_only) + + +async def _create_env_from_envconfig() -> WorkflowEnvironment: + config = ClientConfigProfile.load().to_client_connect_config() + if not config.get("target_host"): + raise ValueError( + "An envconfig workflow environment requires TEMPORAL_ADDRESS or an envconfig profile with an address" + ) + return WorkflowEnvironment.from_client(await Client.connect(**config)) + + @pytest.fixture(scope="session") def event_loop(): - loop = asyncio.get_event_loop_policy().new_event_loop() + loop = asyncio.get_event_loop_policy().new_event_loop() # type: ignore[reportDeprecated] yield loop try: loop.close() except TypeError: - # In 3.9 tests, loop closing fails for an unclear reason, but not in - # 3.13 tests - if sys.version_info >= (3, 10): - raise - finally: - # In 3.9 tests, the pytest-asyncio library finalizer that creates a new - # event loop fails, but not in 3.13 tests. So for now we will make a new - # policy that does not create the loop. - if sys.version_info < (3, 10): - asyncio.set_event_loop_policy( - NoEventLoopPolicy(asyncio.get_event_loop_policy()) - ) + raise -class NoEventLoopPolicy(asyncio.AbstractEventLoopPolicy): - def __init__(self, underlying: asyncio.AbstractEventLoopPolicy): +class NoEventLoopPolicy(asyncio.AbstractEventLoopPolicy): # type: ignore[name-defined] + def __init__(self, underlying: asyncio.AbstractEventLoopPolicy): # type: ignore[name-defined] super().__init__() self._underlying = underlying def get_event_loop(self): return self._underlying.get_event_loop() - def set_event_loop(self, loop): + def set_event_loop(self, loop): # type: ignore[reportMissingParameterType] return self._underlying.set_event_loop(loop) - def new_event_loop(self): + def new_event_loop(self): # type: ignore[reportIncompatibleMethodOverride] return None def get_child_watcher(self): - return self._underlying.get_child_watcher() + return self._underlying.get_child_watcher() # type: ignore[reportDeprecated] - def set_child_watcher(self, watcher): - return self._underlying.set_child_watcher(watcher) + def set_child_watcher(self, watcher): # type: ignore[reportMissingParameterType] + return self._underlying.set_child_watcher(watcher) # type: ignore[reportDeprecated] @pytest.fixture(scope="session") def env_type(request: pytest.FixtureRequest) -> str: - return request.config.getoption("--workflow-environment") + return request.config.getoption("--workflow-environment") # type: ignore[reportReturnType] -@pytest_asyncio.fixture(scope="session") +@pytest_asyncio.fixture(scope="session") # type: ignore[reportUntypedFunctionDecorator] async def env(env_type: str) -> AsyncGenerator[WorkflowEnvironment, None]: - if env_type == "local": - http_port = 7243 + if _uses_envconfig_server(env_type): + env = await _create_env_from_envconfig() + elif env_type == "local": env = await WorkflowEnvironment.start_local( dev_server_extra_args=[ "--dynamic-config-value", @@ -119,14 +156,34 @@ async def env(env_type: str) -> AsyncGenerator[WorkflowEnvironment, None]: "--dynamic-config-value", "frontend.activityAPIsEnabled=true", "--dynamic-config-value", + "frontend.enableCancelWorkerPollsOnShutdown=true", + "--dynamic-config-value", "component.nexusoperations.recordCancelRequestCompletionEvents=true", - "--http-port", - str(http_port), + "--dynamic-config-value", + "activity.enableStandalone=true", + "--dynamic-config-value", + "activity.startDelayEnabled=true", + "--dynamic-config-value", + "history.enableChasm=true", + "--dynamic-config-value", + "history.enableTransitionHistory=true", + "--dynamic-config-value", + "history.enableCHASMCallbacks=true", + "--dynamic-config-value", + "history.enableCHASMSignalBacklinks=true", + "--dynamic-config-value", + "nexusoperation.enableStandalone=true", + "--dynamic-config-value", + 'system.system.refreshNexusEndpointsMinWait="0s"', + "--dynamic-config-value", + "history.enableSignalWithStartFromWorkflow=true", + "--dynamic-config-value", + "history.enableUpdateCallbacks=true", + "--dynamic-config-value", + "activity.enableCallbacks=true", ], dev_server_download_version=DEV_SERVER_DOWNLOAD_VERSION, ) - # TODO(nexus-preview): expose this in a more principled way - env._http_port = http_port # type: ignore elif env_type == "time-skipping": env = await WorkflowEnvironment.start_time_skipping() else: @@ -136,12 +193,40 @@ async def env(env_type: str) -> AsyncGenerator[WorkflowEnvironment, None]: await env.shutdown() -@pytest_asyncio.fixture +@pytest.fixture(scope="session") +def shared_state_manager() -> Iterator[SharedStateManager]: + mp_mgr = multiprocessing.Manager() + mgr = SharedStateManager.create_from_multiprocessing(mp_mgr) + + try: + yield mgr + finally: + mp_mgr.shutdown() + + +@pytest.fixture(scope="session") +def mp_fork_ctx() -> Iterator[multiprocessing.context.BaseContext | None]: + mp_ctx = None + try: + mp_ctx = multiprocessing.get_context("fork") + except ValueError: + pass + + try: + yield mp_ctx + finally: + if mp_ctx: + for p in mp_ctx.active_children(): + p.terminate() + p.join() + + +@pytest_asyncio.fixture # type: ignore[reportUntypedFunctionDecorator] async def client(env: WorkflowEnvironment) -> Client: return env.client -@pytest_asyncio.fixture(scope="session") +@pytest_asyncio.fixture(scope="session") # type: ignore[reportUntypedFunctionDecorator] async def worker( env: WorkflowEnvironment, ) -> AsyncGenerator[ExternalWorker, None]: @@ -155,11 +240,18 @@ async def worker( # hook forcefully kills the process as success when the exit code from pytest # is a success. @pytest.hookimpl(hookwrapper=True, trylast=True) -def pytest_cmdline_main(config): +def pytest_cmdline_main(config): # type: ignore[reportMissingParameterType, reportUnusedParameter] result = yield - if result.get_result() == 0: + exit_code = result.get_result() + numprocesses = getattr(config.option, "numprocesses", None) + running_with_xdist = hasattr(config, "workerinput") or numprocesses not in ( + None, + 0, + "0", + ) + if exit_code == 0 and not running_with_xdist: os._exit(0) - return result.get_result() + return exit_code CONTINUE_AS_NEW_SUGGEST_HISTORY_COUNT = 50 @@ -168,3 +260,13 @@ def pytest_cmdline_main(config): @pytest.fixture def continue_as_new_suggest_history_count() -> int: return CONTINUE_AS_NEW_SUGGEST_HISTORY_COUNT + + +@pytest.fixture +def reset_otel_tracer_provider(): + """Reset global OpenTelemetry tracer provider state around tests.""" + opentelemetry.trace._TRACER_PROVIDER_SET_ONCE = Once() + opentelemetry.trace._TRACER_PROVIDER = None + yield + opentelemetry.trace._TRACER_PROVIDER_SET_ONCE = Once() + opentelemetry.trace._TRACER_PROVIDER = None diff --git a/tests/contrib/aws/__init__.py b/tests/contrib/aws/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/contrib/aws/lambda_worker/__init__.py b/tests/contrib/aws/lambda_worker/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/contrib/aws/lambda_worker/test_lambda_worker.py b/tests/contrib/aws/lambda_worker/test_lambda_worker.py new file mode 100644 index 000000000..9d2ec78a7 --- /dev/null +++ b/tests/contrib/aws/lambda_worker/test_lambda_worker.py @@ -0,0 +1,668 @@ +"""Tests for temporalio.contrib.aws.lambda_worker.""" + +from __future__ import annotations + +import dataclasses +import itertools +from contextlib import asynccontextmanager +from datetime import timedelta +from pathlib import Path +from typing import Any +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from temporalio.common import WorkerDeploymentVersion +from temporalio.contrib.aws.lambda_worker._configure import ( + LambdaWorkerConfig, + _run_shutdown_hooks, +) +from temporalio.contrib.aws.lambda_worker._defaults import ( + DEFAULT_MAX_CACHED_WORKFLOWS, + DEFAULT_MAX_CONCURRENT_ACTIVITIES, + DEFAULT_MAX_CONCURRENT_LOCAL_ACTIVITIES, + DEFAULT_MAX_CONCURRENT_NEXUS_TASKS, + DEFAULT_MAX_CONCURRENT_WORKFLOW_TASKS, + apply_lambda_worker_defaults, + build_lambda_identity, + lambda_default_config_file_path, +) +from temporalio.contrib.aws.lambda_worker._run_worker import ( + _run_worker_internal, + _WorkerDeps, +) +from temporalio.envconfig import ClientConfigProfile +from temporalio.worker import WorkerConfig + +TEST_VERSION = WorkerDeploymentVersion( + deployment_name="test-deployment", + build_id="test-build", +) + + +# ---- LambdaWorkerConfig tests ---- + + +class TestLambdaWorkerConfig: + def test_worker_config_task_queue(self) -> None: + config = LambdaWorkerConfig() + assert config.worker_config.get("task_queue") is None + config.worker_config["task_queue"] = "my-queue" + assert config.worker_config["task_queue"] == "my-queue" + + def test_worker_config_workflows(self) -> None: + config = LambdaWorkerConfig() + + class FakeWorkflow: + pass + + config.worker_config["workflows"] = [FakeWorkflow] + assert FakeWorkflow in config.worker_config["workflows"] + + def test_worker_config_activities(self) -> None: + config = LambdaWorkerConfig() + + def fake_activity() -> None: + pass + + config.worker_config["activities"] = [fake_activity] + assert fake_activity in config.worker_config["activities"] + + def test_client_connect_config_directly_modifiable(self) -> None: + config = LambdaWorkerConfig() + config.client_connect_config["namespace"] = "custom-ns" + assert config.client_connect_config["namespace"] == "custom-ns" + + def test_worker_config_directly_modifiable(self) -> None: + config = LambdaWorkerConfig() + config.worker_config["max_concurrent_activities"] = 42 + assert config.worker_config["max_concurrent_activities"] == 42 + + def test_shutdown_deadline_buffer(self) -> None: + config = LambdaWorkerConfig() + config.shutdown_deadline_buffer = timedelta(seconds=5) + assert config.shutdown_deadline_buffer == timedelta(seconds=5) + + def test_shutdown_hooks_list(self) -> None: + config = LambdaWorkerConfig() + fn = MagicMock() + config.shutdown_hooks.append(fn) + assert fn in config.shutdown_hooks + + @pytest.mark.asyncio + async def test_run_shutdown_hooks_in_order(self) -> None: + config = LambdaWorkerConfig() + order: list[str] = [] + config.shutdown_hooks.append(lambda: order.append("first")) + config.shutdown_hooks.append(lambda: order.append("second")) + await _run_shutdown_hooks(config) + assert order == ["first", "second"] + + @pytest.mark.asyncio + async def test_run_shutdown_hooks_async(self) -> None: + config = LambdaWorkerConfig() + called = False + + async def async_hook() -> None: + nonlocal called + called = True + + config.shutdown_hooks.append(async_hook) + await _run_shutdown_hooks(config) + assert called + + @pytest.mark.asyncio + async def test_run_shutdown_hooks_error_continues(self) -> None: + config = LambdaWorkerConfig() + second_called = False + + def failing_hook() -> None: + raise RuntimeError("flush failed") + + def second_hook() -> None: + nonlocal second_called + second_called = True + + config.shutdown_hooks.append(failing_hook) + config.shutdown_hooks.append(second_hook) + await _run_shutdown_hooks(config) + assert second_called + + def test_is_dataclass(self) -> None: + assert dataclasses.is_dataclass(LambdaWorkerConfig) + + def test_default_field_independence(self) -> None: + """Each instance gets its own mutable containers.""" + a = LambdaWorkerConfig() + b = LambdaWorkerConfig() + a.worker_config["max_concurrent_activities"] = 99 + assert "max_concurrent_activities" not in b.worker_config + + +# ---- Defaults tests ---- + + +class TestDefaults: + def test_apply_lambda_worker_defaults(self) -> None: + config: WorkerConfig = {} + apply_lambda_worker_defaults(config) + assert ( + config.get("max_concurrent_activities") == DEFAULT_MAX_CONCURRENT_ACTIVITIES + ) + assert ( + config.get("max_concurrent_workflow_tasks") + == DEFAULT_MAX_CONCURRENT_WORKFLOW_TASKS + ) + assert ( + config.get("max_concurrent_local_activities") + == DEFAULT_MAX_CONCURRENT_LOCAL_ACTIVITIES + ) + assert ( + config.get("max_concurrent_nexus_tasks") + == DEFAULT_MAX_CONCURRENT_NEXUS_TASKS + ) + assert config.get("max_cached_workflows") == DEFAULT_MAX_CACHED_WORKFLOWS + assert config.get("disable_eager_activity_execution") is True + + def test_apply_lambda_worker_defaults_preserves_existing(self) -> None: + config: WorkerConfig = { + "max_concurrent_activities": 50, + "graceful_shutdown_timeout": timedelta(seconds=10), + } + apply_lambda_worker_defaults(config) + assert config.get("max_concurrent_activities") == 50 + assert config.get("graceful_shutdown_timeout") == timedelta(seconds=10) + assert config.get("disable_eager_activity_execution") is True + + def test_build_lambda_identity(self) -> None: + assert ( + build_lambda_identity("req-123", "arn:aws:lambda:us-east-1:123:function:f") + == "req-123@arn:aws:lambda:us-east-1:123:function:f" + ) + + def test_build_lambda_identity_empty(self) -> None: + assert build_lambda_identity("", "") == "unknown@unknown" + + def test_lambda_default_config_file_path_env_var(self) -> None: + env = {"TEMPORAL_CONFIG_FILE": "/custom/path.toml"} + assert ( + lambda_default_config_file_path(env.get) # type: ignore[arg-type] + == Path("/custom/path.toml") + ) + + def test_lambda_default_config_file_path_lambda_root(self) -> None: + env = {"LAMBDA_TASK_ROOT": "/var/task"} + assert ( + lambda_default_config_file_path(env.get) # type: ignore[arg-type] + == Path("/var/task/temporal.toml") + ) + + def test_lambda_default_config_file_path_cwd(self) -> None: + env: dict[str, str] = {} + assert ( + lambda_default_config_file_path(env.get) # type: ignore[arg-type] + == Path("temporal.toml") + ) + + +# ---- RunWorker tests ---- + + +def _make_lambda_context( + *, + remaining_ms: int = 3_600_000, + request_id: str = "req-123", + function_arn: str = "arn:aws:lambda:us-east-1:123:function:my-func", +) -> Any: + """Create a mock Lambda context object.""" + ctx = MagicMock() + ctx.get_remaining_time_in_millis.return_value = remaining_ms + ctx.aws_request_id = request_id + ctx.invoked_function_arn = function_arn + return ctx + + +def _make_test_deps( + *, + connect_kwargs_capture: list[dict[str, Any]] | None = None, + worker_kwargs_capture: list[dict[str, Any]] | None = None, +) -> _WorkerDeps: + """Create test deps with mocked connect and worker.""" + mock_client = MagicMock() + mock_worker = MagicMock() + mock_worker.run = AsyncMock() + + async def fake_connect(**kwargs: Any) -> Any: + if connect_kwargs_capture is not None: + connect_kwargs_capture.append(kwargs) + return mock_client + + def fake_create_worker(_client: Any, **kwargs: Any) -> Any: + if worker_kwargs_capture is not None: + worker_kwargs_capture.append(kwargs) + return mock_worker + + return _WorkerDeps( + connect=fake_connect, + create_worker=fake_create_worker, + load_config=lambda: ClientConfigProfile(), + getenv={"TEMPORAL_TASK_QUEUE": "test-queue"}.get, # type: ignore[arg-type] + extract_lambda_ctx=lambda ctx: ( + ( + ctx.aws_request_id, + ctx.invoked_function_arn, + ) + if hasattr(ctx, "aws_request_id") + else None + ), + ) + + +class TestRunWorkerInternal: + def test_returns_handler(self) -> None: + deps = _make_test_deps() + handler = _run_worker_internal(TEST_VERSION, lambda config: None, deps) + assert callable(handler) + + def test_success(self) -> None: + deps = _make_test_deps() + + def configure(config: LambdaWorkerConfig) -> None: + config.worker_config["workflows"] = [type("FakeWf", (), {})] + + handler = _run_worker_internal(TEST_VERSION, configure, deps) + handler({}, _make_lambda_context()) + + def test_configure_callback_error(self) -> None: + deps = _make_test_deps() + + def bad_configure(_config: LambdaWorkerConfig) -> None: + raise RuntimeError("bad config") + + # configure runs per invocation, so the error surfaces when the handler is invoked. + handler = _run_worker_internal(TEST_VERSION, bad_configure, deps) + with pytest.raises(RuntimeError, match="bad config"): + handler({}, _make_lambda_context()) + + def test_missing_task_queue(self) -> None: + deps = _make_test_deps() + deps.getenv = lambda _: None # type: ignore[assignment] + handler = _run_worker_internal(TEST_VERSION, lambda config: None, deps) + with pytest.raises(ValueError, match="task queue not configured"): + handler({}, _make_lambda_context()) + + def test_missing_version(self) -> None: + deps = _make_test_deps() + with pytest.raises(ValueError, match="version is required"): + _run_worker_internal( + WorkerDeploymentVersion(deployment_name="", build_id=""), + lambda config: None, + deps, + ) + + def test_user_overrides_applied(self) -> None: + connect_capture: list[dict[str, Any]] = [] + worker_capture: list[dict[str, Any]] = [] + deps = _make_test_deps( + connect_kwargs_capture=connect_capture, + worker_kwargs_capture=worker_capture, + ) + + def configure(config: LambdaWorkerConfig) -> None: + config.worker_config["task_queue"] = "user-queue" + config.client_connect_config["namespace"] = "custom-ns" + config.worker_config["max_concurrent_activities"] = 99 + + handler = _run_worker_internal(TEST_VERSION, configure, deps) + handler({}, _make_lambda_context()) + + assert connect_capture[0]["namespace"] == "custom-ns" + assert worker_capture[0]["max_concurrent_activities"] == 99 + + def test_lambda_defaults_applied(self) -> None: + worker_capture: list[dict[str, Any]] = [] + deps = _make_test_deps(worker_kwargs_capture=worker_capture) + handler = _run_worker_internal(TEST_VERSION, lambda config: None, deps) + handler({}, _make_lambda_context()) + + kwargs = worker_capture[0] + assert kwargs["max_concurrent_activities"] == DEFAULT_MAX_CONCURRENT_ACTIVITIES + assert ( + kwargs["max_concurrent_workflow_tasks"] + == DEFAULT_MAX_CONCURRENT_WORKFLOW_TASKS + ) + assert kwargs["disable_eager_activity_execution"] is True + dc = kwargs["deployment_config"] + assert dc.use_worker_versioning is True + assert dc.version == TEST_VERSION + + def test_identity_from_lambda_context(self) -> None: + connect_capture: list[dict[str, Any]] = [] + deps = _make_test_deps(connect_kwargs_capture=connect_capture) + handler = _run_worker_internal(TEST_VERSION, lambda config: None, deps) + handler( + {}, + _make_lambda_context( + request_id="req-abc-123", + function_arn="arn:aws:lambda:us-east-1:123456:function:my-func", + ), + ) + + assert ( + connect_capture[0]["identity"] + == "req-abc-123@arn:aws:lambda:us-east-1:123456:function:my-func" + ) + + def test_identity_user_override_wins(self) -> None: + connect_capture: list[dict[str, Any]] = [] + deps = _make_test_deps(connect_kwargs_capture=connect_capture) + + def configure(config: LambdaWorkerConfig) -> None: + config.client_connect_config["identity"] = "my-custom-identity" + + handler = _run_worker_internal(TEST_VERSION, configure, deps) + handler({}, _make_lambda_context()) + assert connect_capture[0]["identity"] == "my-custom-identity" + + def test_identity_no_lambda_context(self) -> None: + connect_capture: list[dict[str, Any]] = [] + deps = _make_test_deps(connect_kwargs_capture=connect_capture) + deps.extract_lambda_ctx = lambda ctx: None + handler = _run_worker_internal(TEST_VERSION, lambda config: None, deps) + handler({}, MagicMock(spec=[])) + assert "identity" not in connect_capture[0] + + def test_shutdown_hooks_called(self) -> None: + deps = _make_test_deps() + shutdown_called = False + + def configure(config: LambdaWorkerConfig) -> None: + def hook() -> None: + nonlocal shutdown_called + shutdown_called = True + + config.shutdown_hooks.append(hook) + + handler = _run_worker_internal(TEST_VERSION, configure, deps) + handler({}, _make_lambda_context()) + assert shutdown_called + + def test_shutdown_hooks_called_per_invocation(self) -> None: + deps = _make_test_deps() + shutdown_count = 0 + + def configure(config: LambdaWorkerConfig) -> None: + def hook() -> None: + nonlocal shutdown_count + shutdown_count += 1 + + config.shutdown_hooks.append(hook) + + handler = _run_worker_internal(TEST_VERSION, configure, deps) + handler({}, _make_lambda_context()) + handler({}, _make_lambda_context()) + handler({}, _make_lambda_context()) + assert shutdown_count == 3 + + def test_shutdown_hooks_multiple_funcs_order(self) -> None: + deps = _make_test_deps() + order: list[str] = [] + + def configure(config: LambdaWorkerConfig) -> None: + config.shutdown_hooks.append(lambda: order.append("first")) + config.shutdown_hooks.append(lambda: order.append("second")) + + handler = _run_worker_internal(TEST_VERSION, configure, deps) + handler({}, _make_lambda_context()) + assert order == ["first", "second"] + + def test_shutdown_hooks_error_continues(self) -> None: + deps = _make_test_deps() + second_called = False + + def configure(config: LambdaWorkerConfig) -> None: + def failing() -> None: + raise RuntimeError("flush failed") + + def second() -> None: + nonlocal second_called + second_called = True + + config.shutdown_hooks.append(failing) + config.shutdown_hooks.append(second) + + handler = _run_worker_internal(TEST_VERSION, configure, deps) + handler({}, _make_lambda_context()) + assert second_called + + def test_tight_deadline_raises_error(self) -> None: + deps = _make_test_deps() + + def configure(config: LambdaWorkerConfig) -> None: + config.shutdown_deadline_buffer = timedelta(milliseconds=1500) + + handler = _run_worker_internal(TEST_VERSION, configure, deps) + with pytest.raises(RuntimeError, match="Lambda timeout is too short"): + handler({}, _make_lambda_context(remaining_ms=2000)) + + def test_tight_deadline_logs_warning(self) -> None: + deps = _make_test_deps() + + def configure(config: LambdaWorkerConfig) -> None: + config.shutdown_deadline_buffer = timedelta(milliseconds=500) + + handler = _run_worker_internal(TEST_VERSION, configure, deps) + with patch( + "temporalio.contrib.aws.lambda_worker._run_worker.logger" + ) as mock_logger: + handler({}, _make_lambda_context(remaining_ms=2000)) + mock_logger.warning.assert_called_once() + assert "less than 5s" in mock_logger.warning.call_args[0][0] + + def test_per_invocation_lifecycle(self) -> None: + """Each invocation creates its own client and worker.""" + connect_count = 0 + deps = _make_test_deps() + original_connect = deps.connect + + async def counting_connect(**kwargs: Any) -> Any: + nonlocal connect_count + connect_count += 1 + return await original_connect(**kwargs) + + deps.connect = counting_connect + + handler = _run_worker_internal(TEST_VERSION, lambda config: None, deps) + handler({}, _make_lambda_context()) + handler({}, _make_lambda_context()) + handler({}, _make_lambda_context()) + assert connect_count == 3 + + def test_task_queue_from_config(self) -> None: + worker_capture: list[dict[str, Any]] = [] + deps = _make_test_deps(worker_kwargs_capture=worker_capture) + deps.getenv = lambda _: None # type: ignore[assignment] + + def configure(config: LambdaWorkerConfig) -> None: + config.worker_config["task_queue"] = "explicit-queue" + + handler = _run_worker_internal(TEST_VERSION, configure, deps) + handler({}, _make_lambda_context()) + assert worker_capture[0]["task_queue"] == "explicit-queue" + + def test_task_queue_pre_populated_from_env(self) -> None: + """Task queue is pre-populated from TEMPORAL_TASK_QUEUE env var.""" + deps = _make_test_deps() + task_queues: list[str | None] = [] + + def configure(config: LambdaWorkerConfig) -> None: + task_queues.append(config.worker_config.get("task_queue")) + + handler = _run_worker_internal(TEST_VERSION, configure, deps) + handler({}, _make_lambda_context()) + assert task_queues[0] == "test-queue" + + def test_config_pre_populated_with_defaults(self) -> None: + """Configure callback receives pre-populated LambdaWorkerConfig.""" + deps = _make_test_deps() + captured: list[LambdaWorkerConfig] = [] + + def configure(config: LambdaWorkerConfig) -> None: + captured.append(config) + + handler = _run_worker_internal(TEST_VERSION, configure, deps) + handler({}, _make_lambda_context()) + wc = captured[0].worker_config + assert wc.get("max_concurrent_activities") == DEFAULT_MAX_CONCURRENT_ACTIVITIES + assert wc.get("disable_eager_activity_execution") is True + dc = wc.get("deployment_config") + assert dc is not None + assert dc.use_worker_versioning is True + assert dc.version == TEST_VERSION + + def test_no_deadline_runs_until_complete(self) -> None: + """When no deadline is available, worker runs until it completes.""" + deps = _make_test_deps() + handler = _run_worker_internal(TEST_VERSION, lambda config: None, deps) + ctx = MagicMock(spec=["aws_request_id", "invoked_function_arn"]) + ctx.aws_request_id = "req-123" + ctx.invoked_function_arn = "arn:aws:lambda:us-east-1:123:function:f" + handler({}, ctx) + + +class TestAsyncConfigure: + """configure may be sync, an async coroutine, or an async generator; all run once per + invocation inside that invocation's event loop.""" + + def test_async_configure_called_per_invocation(self) -> None: + deps = _make_test_deps() + calls = 0 + + async def configure(config: LambdaWorkerConfig) -> None: + nonlocal calls + calls += 1 + config.worker_config["task_queue"] = "async-queue" + + handler = _run_worker_internal(TEST_VERSION, configure, deps) + handler({}, _make_lambda_context()) + handler({}, _make_lambda_context()) + assert calls == 2 + + def test_async_configure_can_set_data_converter(self) -> None: + connect_capture: list[dict[str, Any]] = [] + deps = _make_test_deps(connect_kwargs_capture=connect_capture) + sentinel = object() + + async def configure(config: LambdaWorkerConfig) -> None: + config.client_connect_config["data_converter"] = sentinel # type: ignore[typeddict-item] + + handler = _run_worker_internal(TEST_VERSION, configure, deps) + handler({}, _make_lambda_context()) + assert connect_capture[0]["data_converter"] is sentinel + + def test_async_generator_setup_runs_before_connect_teardown_after(self) -> None: + order: list[str] = [] + deps = _make_test_deps() + + original_connect = deps.connect + + async def tracking_connect(**kwargs: Any) -> Any: + order.append("connect") + return await original_connect(**kwargs) + + deps.connect = tracking_connect + + async def configure(config: LambdaWorkerConfig): + order.append("setup") + config.worker_config["task_queue"] = "gen-queue" + yield + order.append("teardown") + + handler = _run_worker_internal(TEST_VERSION, configure, deps) + handler({}, _make_lambda_context()) + assert order == ["setup", "connect", "teardown"] + + def test_async_generator_teardown_runs_on_error(self) -> None: + """Teardown after the yield runs even when the worker run raises.""" + torn_down = False + deps = _make_test_deps() + + async def failing_run() -> None: + raise RuntimeError("worker boom") + + def fake_create_worker(_client: Any, **_kwargs: Any) -> Any: + w = MagicMock() + w.run = failing_run + return w + + deps.create_worker = fake_create_worker + + async def configure(config: LambdaWorkerConfig): + nonlocal torn_down + config.worker_config["task_queue"] = "gen-queue" + try: + yield + finally: + torn_down = True + + handler = _run_worker_internal(TEST_VERSION, configure, deps) + with pytest.raises(RuntimeError, match="worker boom"): + handler({}, _make_lambda_context()) + assert torn_down + + def test_async_generator_resource_per_invocation(self) -> None: + """Each invocation builds and tears down its own resource instance. Tagging each + resource with a construction sequence number proves the instances are distinct + (open-1/close-1, then open-2/close-2) rather than one resource reopened.""" + events: list[str] = [] + counter = itertools.count(1) + deps = _make_test_deps() + + class FakeResource: + def __init__(self) -> None: + self.n = next(counter) + + async def __aenter__(self) -> FakeResource: + events.append(f"open-{self.n}") + return self + + async def __aexit__(self, *exc: Any) -> None: + events.append(f"close-{self.n}") + + async def configure(config: LambdaWorkerConfig): + config.worker_config["task_queue"] = "gen-queue" + async with FakeResource(): + yield + + handler = _run_worker_internal(TEST_VERSION, configure, deps) + handler({}, _make_lambda_context()) + handler({}, _make_lambda_context()) + assert events == ["open-1", "close-1", "open-2", "close-2"] + + def test_async_configure_validates_task_queue_per_invocation(self) -> None: + deps = _make_test_deps() + deps.getenv = lambda _: None # type: ignore[assignment] + + async def configure(_config: LambdaWorkerConfig) -> None: + pass + + handler = _run_worker_internal(TEST_VERSION, configure, deps) + with pytest.raises(ValueError, match="task queue not configured"): + handler({}, _make_lambda_context()) + + def test_asynccontextmanager_decorated_configure_supported(self) -> None: + """A @asynccontextmanager-decorated configure is entered and exited per invocation, + the same as a bare async generator.""" + events: list[str] = [] + deps = _make_test_deps() + + @asynccontextmanager + async def configure(config: LambdaWorkerConfig): + events.append("setup") + config.worker_config["task_queue"] = "gen-queue" + yield + events.append("teardown") + + handler = _run_worker_internal(TEST_VERSION, configure, deps) + handler({}, _make_lambda_context()) + assert events == ["setup", "teardown"] diff --git a/tests/contrib/aws/lambda_worker/test_otel.py b/tests/contrib/aws/lambda_worker/test_otel.py new file mode 100644 index 000000000..97f3d9647 --- /dev/null +++ b/tests/contrib/aws/lambda_worker/test_otel.py @@ -0,0 +1,167 @@ +"""Tests for temporalio.contrib.aws.lambda_worker.otel.""" + +from __future__ import annotations + +from datetime import timedelta +from unittest.mock import patch + +import pytest + +from temporalio.contrib.aws.lambda_worker._configure import ( + LambdaWorkerConfig, + _run_shutdown_hooks, +) +from temporalio.contrib.aws.lambda_worker.otel import ( + OtelOptions, + apply_defaults, + apply_tracing, + build_metrics_telemetry_config, +) +from temporalio.contrib.opentelemetry import OpenTelemetryPlugin +from temporalio.runtime import OpenTelemetryConfig, TelemetryConfig + + +class TestApplyTracing: + def test_adds_plugin(self) -> None: + config = LambdaWorkerConfig() + apply_tracing(config) + plugins = config.worker_config.get("plugins", []) + assert len(plugins) == 1 + assert isinstance(plugins[0], OpenTelemetryPlugin) + + def test_appends_to_existing_plugins(self) -> None: + config = LambdaWorkerConfig() + existing = OpenTelemetryPlugin() + config.worker_config["plugins"] = [existing] + apply_tracing(config) + plugins = config.worker_config["plugins"] + assert len(plugins) == 2 + assert plugins[0] is existing + + def test_registers_flush_shutdown_hook(self) -> None: + config = LambdaWorkerConfig() + apply_tracing(config) + assert len(config.shutdown_hooks) == 1 + + @pytest.mark.asyncio + async def test_shutdown_hook_flushes(self) -> None: + config = LambdaWorkerConfig() + apply_tracing(config) + # Should not raise even with the default noop global provider. + await _run_shutdown_hooks(config) + + +class TestBuildMetricsTelemetryConfig: + def test_returns_telemetry_config(self) -> None: + tc = build_metrics_telemetry_config(endpoint="http://localhost:4317") + assert isinstance(tc, TelemetryConfig) + assert isinstance(tc.metrics, OpenTelemetryConfig) + assert tc.metrics.url == "http://localhost:4317" + + def test_default_endpoint(self) -> None: + tc = build_metrics_telemetry_config() + assert isinstance(tc.metrics, OpenTelemetryConfig) + assert tc.metrics.url == "http://localhost:4317" + + def test_service_name_as_global_tag(self) -> None: + tc = build_metrics_telemetry_config(service_name="my-svc") + assert tc.global_tags.get("service_name") == "my-svc" + + def test_no_service_name_no_tag(self) -> None: + tc = build_metrics_telemetry_config() + assert "service_name" not in tc.global_tags + + def test_metric_periodicity(self) -> None: + tc = build_metrics_telemetry_config(metric_periodicity=timedelta(seconds=30)) + assert isinstance(tc.metrics, OpenTelemetryConfig) + assert tc.metrics.metric_periodicity == timedelta(seconds=30) + + def test_composable_with_custom_runtime(self) -> None: + """User can compose the returned config into a custom Runtime.""" + import dataclasses + + tc = build_metrics_telemetry_config(endpoint="http://localhost:4317") + custom_tc = dataclasses.replace(tc, logging=None) + assert custom_tc.logging is None + assert isinstance(custom_tc.metrics, OpenTelemetryConfig) + + +class TestApplyDefaults: + def test_configures_metrics_and_tracing(self) -> None: + config = LambdaWorkerConfig() + apply_defaults(config, OtelOptions(collector_endpoint="http://localhost:4317")) + + # Metrics: runtime should be set. + assert "runtime" in config.client_connect_config + # Tracing: plugin should be added. + plugins = config.worker_config.get("plugins", []) + assert len(plugins) == 1 + assert isinstance(plugins[0], OpenTelemetryPlugin) + # Shutdown hook for tracer flush. + assert len(config.shutdown_hooks) == 1 + + def test_sets_global_tracer_provider(self) -> None: + from opentelemetry.trace import get_tracer_provider + + from temporalio.contrib.opentelemetry._tracer_provider import ( + ReplaySafeTracerProvider, + ) + + config = LambdaWorkerConfig() + apply_defaults(config) + provider = get_tracer_provider() + assert isinstance(provider, ReplaySafeTracerProvider) + + def test_service_name_from_options(self) -> None: + config = LambdaWorkerConfig() + apply_defaults(config, OtelOptions(service_name="my-service")) + assert "runtime" in config.client_connect_config + + def test_service_name_from_env(self) -> None: + config = LambdaWorkerConfig() + with patch.dict("os.environ", {"OTEL_SERVICE_NAME": "env-service"}): + apply_defaults(config) + assert "runtime" in config.client_connect_config + + def test_service_name_from_lambda_function_name(self) -> None: + config = LambdaWorkerConfig() + with patch.dict( + "os.environ", + {"AWS_LAMBDA_FUNCTION_NAME": "my-lambda"}, + clear=True, + ): + apply_defaults(config) + assert "runtime" in config.client_connect_config + + def test_endpoint_from_env(self) -> None: + config = LambdaWorkerConfig() + with patch.dict( + "os.environ", + {"OTEL_EXPORTER_OTLP_ENDPOINT": "http://custom:4317"}, + ): + apply_defaults(config) + assert "runtime" in config.client_connect_config + + def test_default_options_used_when_none(self) -> None: + config = LambdaWorkerConfig() + apply_defaults(config) + assert "runtime" in config.client_connect_config + assert len(config.shutdown_hooks) == 1 + + +class TestOtelOptions: + def test_defaults(self) -> None: + opts = OtelOptions() + assert opts.service_name == "" + assert opts.collector_endpoint == "" + assert opts.metric_periodicity == timedelta(seconds=10) + + def test_custom_values(self) -> None: + opts = OtelOptions( + service_name="svc", + collector_endpoint="http://host:4317", + metric_periodicity=timedelta(seconds=30), + ) + assert opts.service_name == "svc" + assert opts.collector_endpoint == "http://host:4317" + assert opts.metric_periodicity == timedelta(seconds=30) diff --git a/tests/contrib/aws/s3driver/__init__.py b/tests/contrib/aws/s3driver/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/contrib/aws/s3driver/conftest.py b/tests/contrib/aws/s3driver/conftest.py new file mode 100644 index 000000000..6213014af --- /dev/null +++ b/tests/contrib/aws/s3driver/conftest.py @@ -0,0 +1,65 @@ +"""Shared fixtures for S3 storage driver tests.""" + +from __future__ import annotations + +import socket +import urllib.request +from collections.abc import AsyncIterator, Iterator + +import aioboto3 +import pytest +from types_aiobotocore_s3.client import S3Client + +from temporalio.contrib.aws.s3driver import S3StorageDriverClient +from temporalio.contrib.aws.s3driver.aioboto3 import new_aioboto3_client + +BUCKET = "test-bucket" +CLIENT_REGION = "us-east-1" + + +def _find_free_port() -> int: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("127.0.0.1", 0)) + return s.getsockname()[1] + + +@pytest.fixture(scope="session") +def moto_server_url() -> Iterator[str]: + """Start a moto S3 server for the test session and yield its base URL.""" + port = _find_free_port() + from moto.server import ThreadedMotoServer + + server = ThreadedMotoServer(port=port) + server.start() + yield f"http://127.0.0.1:{port}" + server.stop() + + +@pytest.fixture +async def aioboto3_client(moto_server_url: str) -> AsyncIterator[S3Client]: + """Yield an aioboto3 S3 client pointed at the moto server. + + Resets all moto state before each test to guarantee isolation, then + pre-creates the standard test bucket. + """ + urllib.request.urlopen( + urllib.request.Request( + f"{moto_server_url}/moto-api/reset", method="POST", data=b"" + ) + ) + session = aioboto3.Session() + async with session.client( + "s3", + region_name=CLIENT_REGION, + endpoint_url=moto_server_url, + aws_access_key_id="testing", + aws_secret_access_key="testing", + ) as client: + await client.create_bucket(Bucket=BUCKET) + yield client + + +@pytest.fixture +def driver_client(aioboto3_client: S3Client) -> S3StorageDriverClient: + """Wrap the aioboto3 S3 client in an S3StorageDriverClient adapter.""" + return new_aioboto3_client(aioboto3_client) diff --git a/tests/contrib/aws/s3driver/test_s3driver.py b/tests/contrib/aws/s3driver/test_s3driver.py new file mode 100644 index 000000000..05628a7f2 --- /dev/null +++ b/tests/contrib/aws/s3driver/test_s3driver.py @@ -0,0 +1,971 @@ +"""Unit tests for S3StorageDriver using moto's ThreadedMotoServer to mock AWS S3. + +moto's standard mock_aws() context manager intercepts boto3/botocore via the +requests library and does not intercept aiobotocore (which aioboto3 wraps), +because aiobotocore uses aiohttp and returns coroutines where moto's mock +returns plain bytes. ThreadedMotoServer starts a real local HTTP server; the +aioboto3 client is pointed at it via endpoint_url so all API calls are +intercepted correctly. +""" + +from __future__ import annotations + +import asyncio +import hashlib +from collections.abc import Callable, Coroutine +from functools import wraps +from typing import Any +from unittest.mock import MagicMock + +import pytest +from botocore.exceptions import ClientError +from types_aiobotocore_s3.client import S3Client + +from temporalio.api.common.v1 import Payload +from temporalio.contrib.aws.s3driver import ( + S3StorageDriver, + S3StorageDriverClient, +) +from temporalio.contrib.aws.s3driver._driver import _format_client_context +from temporalio.contrib.aws.s3driver.aioboto3 import _Aioboto3StorageDriverClient +from temporalio.converter import ( + JSONPlainPayloadConverter, + StorageDriverActivityInfo, + StorageDriverClaim, + StorageDriverRetrieveContext, + StorageDriverStoreContext, + StorageDriverWorkflowInfo, +) +from tests.contrib.aws.s3driver.conftest import BUCKET, CLIENT_REGION + +_CONVERTER = JSONPlainPayloadConverter() + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def make_payload(value: str = "hello") -> Payload: + p = _CONVERTER.to_payload(value) + assert p is not None + return p + + +def make_store_context( + target: StorageDriverActivityInfo | StorageDriverWorkflowInfo | None = None, +) -> StorageDriverStoreContext: + return StorageDriverStoreContext( + target=target, + ) + + +def make_workflow_context( + namespace: str = "my-namespace", + workflow_id: str = "my-workflow", + workflow_type: str | None = None, + run_id: str | None = None, +) -> StorageDriverStoreContext: + return make_store_context( + target=StorageDriverWorkflowInfo( + id=workflow_id, type=workflow_type, run_id=run_id, namespace=namespace + ), + ) + + +def make_activity_context( + namespace: str = "my-namespace", + activity_id: str | None = "my-activity", + activity_type: str | None = None, + run_id: str | None = None, +) -> StorageDriverStoreContext: + return make_store_context( + target=StorageDriverActivityInfo( + id=activity_id, type=activity_type, run_id=run_id, namespace=namespace + ), + ) + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +class CountingDriverClient(S3StorageDriverClient): + """S3StorageDriverClient wrapper that counts calls to each method.""" + + def __init__(self, delegate: S3StorageDriverClient) -> None: + self._delegate = delegate + self.put_object_count = 0 + self.get_object_count = 0 + self.object_exists_count = 0 + + async def put_object(self, *, bucket: str, key: str, data: bytes) -> None: + """Delegate to wrapped client and increment put_object counter.""" + self.put_object_count += 1 + await self._delegate.put_object(bucket=bucket, key=key, data=data) + + async def object_exists(self, *, bucket: str, key: str) -> bool: + """Delegate to wrapped client and increment object_exists counter.""" + self.object_exists_count += 1 + return await self._delegate.object_exists(bucket=bucket, key=key) + + async def get_object(self, *, bucket: str, key: str) -> bytes: + """Delegate to wrapped client and increment get_object counter.""" + self.get_object_count += 1 + return await self._delegate.get_object(bucket=bucket, key=key) + + +class FailOnceDriverClient(S3StorageDriverClient): + """S3StorageDriverClient wrapper that fails the first call to a specified + method and blocks subsequent calls until cancelled. + + Used to verify that the driver cancels in-flight tasks when one fails. + """ + + def __init__( + self, + delegate: S3StorageDriverClient, + fail_on: str, + ) -> None: + self._delegate = delegate + self._fail_on = fail_on + self._call_count = 0 + self.cancelled: list[bool] = [] + + async def _maybe_fail(self) -> None: + self._call_count += 1 + if self._call_count == 1: + raise ConnectionError("S3 connection lost") + try: + await asyncio.sleep(60) + except asyncio.CancelledError: + self.cancelled.append(True) + raise + + async def put_object(self, *, bucket: str, key: str, data: bytes) -> None: + """Delegate or fail depending on configuration.""" + if self._fail_on == "put_object": + await self._maybe_fail() + await self._delegate.put_object(bucket=bucket, key=key, data=data) + + async def object_exists(self, *, bucket: str, key: str) -> bool: + """Delegate or fail depending on configuration.""" + if self._fail_on == "object_exists": + await self._maybe_fail() + return await self._delegate.object_exists(bucket=bucket, key=key) + + async def get_object(self, *, bucket: str, key: str) -> bytes: + """Delegate or fail depending on configuration.""" + if self._fail_on == "get_object": + await self._maybe_fail() + return await self._delegate.get_object(bucket=bucket, key=key) + + +@pytest.fixture +def counting_driver_client( + driver_client: S3StorageDriverClient, +) -> CountingDriverClient: + """Wrap the driver client in a counting decorator.""" + return CountingDriverClient(driver_client) + + +# --------------------------------------------------------------------------- +# TestS3StorageDriverInit — no S3 calls; MagicMock client is sufficient +# --------------------------------------------------------------------------- + + +class TestS3StorageDriverInit: + def test_default_name(self) -> None: + driver = S3StorageDriver( + client=MagicMock(spec=S3StorageDriverClient), bucket=BUCKET + ) + assert driver.name() == "aws.s3driver" + + def test_custom_name(self) -> None: + driver = S3StorageDriver( + client=MagicMock(spec=S3StorageDriverClient), + bucket=BUCKET, + driver_name="my-s3", + ) + assert driver.name() == "my-s3" + + def test_type(self) -> None: + driver = S3StorageDriver( + client=MagicMock(spec=S3StorageDriverClient), bucket=BUCKET + ) + assert driver.type() == "aws.s3driver" + + +# --------------------------------------------------------------------------- +# TestS3StorageDriverKeyConstruction +# --------------------------------------------------------------------------- + + +class TestS3StorageDriverKeyConstruction: + async def test_key_context_none(self, driver_client: S3StorageDriverClient) -> None: + driver = S3StorageDriver(client=driver_client, bucket=BUCKET) + payload = make_payload() + [claim] = await driver.store(make_store_context(), [payload]) + expected_hash = hashlib.sha256(payload.SerializeToString()).hexdigest() + assert claim.claim_data["key"] == f"v0/d/sha256/{expected_hash}" + + async def test_key_context_workflow( + self, driver_client: S3StorageDriverClient + ) -> None: + driver = S3StorageDriver(client=driver_client, bucket=BUCKET) + payload = make_payload() + ctx = make_workflow_context(namespace="ns1", workflow_id="wf1") + [claim] = await driver.store(ctx, [payload]) + expected_hash = hashlib.sha256(payload.SerializeToString()).hexdigest() + assert ( + claim.claim_data["key"] + == f"v0/ns/ns1/wt/null/wi/wf1/ri/null/d/sha256/{expected_hash}" + ) + + async def test_key_context_workflow_with_type_and_run_id( + self, driver_client: S3StorageDriverClient + ) -> None: + driver = S3StorageDriver(client=driver_client, bucket=BUCKET) + payload = make_payload() + ctx = make_workflow_context( + namespace="ns1", + workflow_id="wf1", + workflow_type="MyWorkflow", + run_id="run-abc", + ) + [claim] = await driver.store(ctx, [payload]) + expected_hash = hashlib.sha256(payload.SerializeToString()).hexdigest() + assert ( + claim.claim_data["key"] + == f"v0/ns/ns1/wt/MyWorkflow/wi/wf1/ri/run-abc/d/sha256/{expected_hash}" + ) + + async def test_key_context_activity( + self, driver_client: S3StorageDriverClient + ) -> None: + """activity target uses activity key segment.""" + driver = S3StorageDriver(client=driver_client, bucket=BUCKET) + payload = make_payload() + ctx = make_activity_context(namespace="ns1", activity_id="act1") + [claim] = await driver.store(ctx, [payload]) + expected_hash = hashlib.sha256(payload.SerializeToString()).hexdigest() + assert ( + claim.claim_data["key"] + == f"v0/ns/ns1/at/null/ai/act1/ri/null/d/sha256/{expected_hash}" + ) + + async def test_key_context_activity_with_type_and_run_id( + self, driver_client: S3StorageDriverClient + ) -> None: + driver = S3StorageDriver(client=driver_client, bucket=BUCKET) + payload = make_payload() + ctx = make_activity_context( + namespace="ns1", + activity_id="act1", + activity_type="MyActivity", + run_id="run-abc", + ) + [claim] = await driver.store(ctx, [payload]) + expected_hash = hashlib.sha256(payload.SerializeToString()).hexdigest() + assert ( + claim.claim_data["key"] + == f"v0/ns/ns1/at/MyActivity/ai/act1/ri/run-abc/d/sha256/{expected_hash}" + ) + + async def test_key_preserves_case( + self, driver_client: S3StorageDriverClient + ) -> None: + driver = S3StorageDriver(client=driver_client, bucket=BUCKET) + payload = make_payload() + ctx = make_workflow_context(namespace="MyNamespace", workflow_id="MyWorkflow") + [claim] = await driver.store(ctx, [payload]) + key = claim.claim_data["key"] + assert "MyNamespace" in key + assert "MyWorkflow" in key + + async def test_key_urlencodes_workflow_id_with_slashes( + self, driver_client: S3StorageDriverClient + ) -> None: + driver = S3StorageDriver(client=driver_client, bucket=BUCKET) + payload = make_payload() + ctx = make_workflow_context(namespace="ns1", workflow_id="order/123/v2") + [claim] = await driver.store(ctx, [payload]) + expected_hash = hashlib.sha256(payload.SerializeToString()).hexdigest() + assert ( + claim.claim_data["key"] + == f"v0/ns/ns1/wt/null/wi/order%2F123%2Fv2/ri/null/d/sha256/{expected_hash}" + ) + + async def test_key_urlencodes_workflow_id_with_special_chars( + self, driver_client: S3StorageDriverClient + ) -> None: + driver = S3StorageDriver(client=driver_client, bucket=BUCKET) + payload = make_payload() + ctx = make_workflow_context(namespace="ns1", workflow_id="wf#1 &foo=bar") + [claim] = await driver.store(ctx, [payload]) + expected_hash = hashlib.sha256(payload.SerializeToString()).hexdigest() + assert ( + claim.claim_data["key"] + == f"v0/ns/ns1/wt/null/wi/wf%231%20%26foo%3Dbar/ri/null/d/sha256/{expected_hash}" + ) + + async def test_key_urlencodes_activity_id( + self, driver_client: S3StorageDriverClient + ) -> None: + driver = S3StorageDriver(client=driver_client, bucket=BUCKET) + payload = make_payload() + ctx = make_activity_context(namespace="ns1", activity_id="act/1#2") + [claim] = await driver.store(ctx, [payload]) + expected_hash = hashlib.sha256(payload.SerializeToString()).hexdigest() + assert ( + claim.claim_data["key"] + == f"v0/ns/ns1/at/null/ai/act%2F1%232/ri/null/d/sha256/{expected_hash}" + ) + + async def test_key_urlencodes_namespace( + self, driver_client: S3StorageDriverClient + ) -> None: + driver = S3StorageDriver(client=driver_client, bucket=BUCKET) + payload = make_payload() + ctx = make_workflow_context(namespace="my/ns#1", workflow_id="wf1") + [claim] = await driver.store(ctx, [payload]) + expected_hash = hashlib.sha256(payload.SerializeToString()).hexdigest() + assert ( + claim.claim_data["key"] + == f"v0/ns/my%2Fns%231/wt/null/wi/wf1/ri/null/d/sha256/{expected_hash}" + ) + + async def test_key_preserves_s3_safe_special_chars( + self, driver_client: S3StorageDriverClient + ) -> None: + """S3's safe special characters are left unescaped per the key spec.""" + driver = S3StorageDriver(client=driver_client, bucket=BUCKET) + payload = make_payload() + # Every special character S3 lists as safe: ! - _ . * ' ( ) + ctx = make_workflow_context(namespace="ns1", workflow_id="!-_.*'()") + [claim] = await driver.store(ctx, [payload]) + expected_hash = hashlib.sha256(payload.SerializeToString()).hexdigest() + assert ( + claim.claim_data["key"] + == f"v0/ns/ns1/wt/null/wi/!-_.*'()/ri/null/d/sha256/{expected_hash}" + ) + + async def test_key_escapes_tilde( + self, driver_client: S3StorageDriverClient + ) -> None: + """Tilde is not in S3's safe set and must be percent-encoded (%7E).""" + driver = S3StorageDriver(client=driver_client, bucket=BUCKET) + payload = make_payload() + ctx = make_workflow_context(namespace="ns1", workflow_id="wf~1") + [claim] = await driver.store(ctx, [payload]) + expected_hash = hashlib.sha256(payload.SerializeToString()).hexdigest() + assert ( + claim.claim_data["key"] + == f"v0/ns/ns1/wt/null/wi/wf%7E1/ri/null/d/sha256/{expected_hash}" + ) + + async def test_key_escapes_non_ascii_as_utf8_bytes( + self, driver_client: S3StorageDriverClient + ) -> None: + """Non-ASCII characters are percent-encoded byte by byte from UTF-8.""" + driver = S3StorageDriver(client=driver_client, bucket=BUCKET) + payload = make_payload() + # "é" is U+00E9, which is 0xC3 0xA9 in UTF-8. + ctx = make_workflow_context(namespace="ns1", workflow_id="café") + [claim] = await driver.store(ctx, [payload]) + expected_hash = hashlib.sha256(payload.SerializeToString()).hexdigest() + assert ( + claim.claim_data["key"] + == f"v0/ns/ns1/wt/null/wi/caf%C3%A9/ri/null/d/sha256/{expected_hash}" + ) + + async def test_key_matches_spec_workflow_example( + self, driver_client: S3StorageDriverClient + ) -> None: + """Reproduces the workflow key example from the S3 key spec.""" + driver = S3StorageDriver(client=driver_client, bucket=BUCKET) + payload = make_payload() + ctx = make_workflow_context( + namespace="payments prod", + workflow_type="ChargeWorkflow", + workflow_id="order+123=abc", + run_id="3f1d6c7a-8b2e-4f7a-9d0a-87a6f95e4d31", + ) + [claim] = await driver.store(ctx, [payload]) + expected_hash = hashlib.sha256(payload.SerializeToString()).hexdigest() + assert claim.claim_data["key"] == ( + "v0/ns/payments%20prod/wt/ChargeWorkflow/wi/order%2B123%3Dabc" + f"/ri/3f1d6c7a-8b2e-4f7a-9d0a-87a6f95e4d31/d/sha256/{expected_hash}" + ) + + async def test_key_matches_spec_activity_example( + self, driver_client: S3StorageDriverClient + ) -> None: + """Reproduces the activity key example from the S3 key spec.""" + driver = S3StorageDriver(client=driver_client, bucket=BUCKET) + payload = make_payload() + ctx = make_activity_context( + namespace="payments prod", + activity_type="Capture/Charge", + activity_id="activity id+42", + run_id="9e1d1fd9-2f8a-4c40-93e2-731f31b9268b", + ) + [claim] = await driver.store(ctx, [payload]) + expected_hash = hashlib.sha256(payload.SerializeToString()).hexdigest() + assert claim.claim_data["key"] == ( + "v0/ns/payments%20prod/at/Capture%2FCharge/ai/activity%20id%2B42" + f"/ri/9e1d1fd9-2f8a-4c40-93e2-731f31b9268b/d/sha256/{expected_hash}" + ) + + async def test_key_urlencoded_roundtrip( + self, driver_client: S3StorageDriverClient + ) -> None: + """Payloads stored with special-char IDs can be retrieved correctly.""" + driver = S3StorageDriver(client=driver_client, bucket=BUCKET) + payload = make_payload("special-char-roundtrip") + ctx = make_workflow_context(namespace="ns/1", workflow_id="wf/2#3") + [claim] = await driver.store(ctx, [payload]) + [retrieved] = await driver.retrieve(StorageDriverRetrieveContext(), [claim]) + assert retrieved == payload + + +# --------------------------------------------------------------------------- +# TestS3StorageDriverStoreRetrieve +# --------------------------------------------------------------------------- + + +class TestS3StorageDriverStoreRetrieve: + async def test_store_returns_claim_with_bucket_key_and_hash( + self, driver_client: S3StorageDriverClient + ) -> None: + driver = S3StorageDriver(client=driver_client, bucket=BUCKET) + payload = make_payload() + [claim] = await driver.store(make_store_context(), [payload]) + assert claim.claim_data["bucket"] == BUCKET + assert "key" in claim.claim_data + assert claim.claim_data["hash_algorithm"] == "sha256" + expected_hash = hashlib.sha256(payload.SerializeToString()).hexdigest() + assert claim.claim_data["hash_value"] == expected_hash + + async def test_roundtrip_single_payload( + self, driver_client: S3StorageDriverClient + ) -> None: + driver = S3StorageDriver(client=driver_client, bucket=BUCKET) + payload = make_payload("round-trip value") + [claim] = await driver.store(make_store_context(), [payload]) + [retrieved] = await driver.retrieve(StorageDriverRetrieveContext(), [claim]) + assert retrieved == payload + + async def test_roundtrip_multiple_payloads( + self, driver_client: S3StorageDriverClient + ) -> None: + driver = S3StorageDriver(client=driver_client, bucket=BUCKET) + payloads = [make_payload(f"value-{i}") for i in range(3)] + claims = await driver.store(make_store_context(), payloads) + retrieved = await driver.retrieve(StorageDriverRetrieveContext(), claims) + assert retrieved == payloads + + async def test_empty_payloads_returns_empty_list( + self, driver_client: S3StorageDriverClient + ) -> None: + driver = S3StorageDriver(client=driver_client, bucket=BUCKET) + assert await driver.store(make_store_context(), []) == [] + assert await driver.retrieve(StorageDriverRetrieveContext(), []) == [] + + async def test_roundtrip_multipart_payload( + self, aioboto3_client: S3Client, driver_client: S3StorageDriverClient + ) -> None: + """Payloads above the 8 MiB multipart threshold are uploaded via multipart + and retrieved correctly. The S3 ETag for multipart objects contains a '-' + suffix (e.g. 'hash-2'), which we assert to confirm multipart was used.""" + driver = S3StorageDriver(client=driver_client, bucket=BUCKET) + # Slightly above the default 8 MiB multipart_threshold + large_payload = make_payload("x" * (9 * 1024 * 1024)) + [claim] = await driver.store(make_store_context(), [large_payload]) + [retrieved] = await driver.retrieve(StorageDriverRetrieveContext(), [claim]) + assert retrieved == large_payload + head = await aioboto3_client.head_object( + Bucket=BUCKET, Key=claim.claim_data["key"] + ) + assert "-" in head["ETag"], "Expected a multipart ETag (hash-N format)" + + async def test_content_addressable_deduplication( + self, aioboto3_client: S3Client, driver_client: S3StorageDriverClient + ) -> None: + """Two identical payloads produce the same S3 key; only one object is stored.""" + driver = S3StorageDriver(client=driver_client, bucket=BUCKET) + payload = make_payload("same-value") + claims = await driver.store(make_store_context(), [payload, payload]) + assert claims[0].claim_data["key"] == claims[1].claim_data["key"] + response = await aioboto3_client.list_objects_v2(Bucket=BUCKET) + assert response["KeyCount"] == 1 + + async def test_skips_upload_when_key_exists( + self, counting_driver_client: CountingDriverClient + ) -> None: + """When a key already exists in S3, put_object is not called again.""" + driver = S3StorageDriver(client=counting_driver_client, bucket=BUCKET) + payload = make_payload("upload-once") + + await driver.store(make_store_context(), [payload]) + assert counting_driver_client.put_object_count == 1 + + await driver.store(make_store_context(), [payload]) + assert counting_driver_client.put_object_count == 1, ( + "put_object should not be called for an existing key" + ) + + async def test_skips_upload_preserves_data( + self, driver_client: S3StorageDriverClient + ) -> None: + """Storing the same payload twice returns correct data on retrieve.""" + driver = S3StorageDriver(client=driver_client, bucket=BUCKET) + payload = make_payload("preserve-me") + + [claim1] = await driver.store(make_store_context(), [payload]) + [claim2] = await driver.store(make_store_context(), [payload]) + assert claim1 == claim2 + + [retrieved] = await driver.retrieve(StorageDriverRetrieveContext(), [claim2]) + assert retrieved == payload + + async def test_retrieve_validates_hash( + self, driver_client: S3StorageDriverClient + ) -> None: + """Retrieve raises RuntimeError when the hash in the claim doesn't match.""" + driver = S3StorageDriver(client=driver_client, bucket=BUCKET) + payload = make_payload("check-integrity") + [claim] = await driver.store(make_store_context(), [payload]) + + tampered_claim = StorageDriverClaim( + claim_data={ + **claim.claim_data, + "hash_value": "0" * 64, + }, + ) + with pytest.raises( + ValueError, + match=r"S3StorageDriver integrity check failed \[bucket=.+, key=.+\]: expected sha256:.+, got sha256:.+", + ): + await driver.retrieve(StorageDriverRetrieveContext(), [tampered_claim]) + + async def test_retrieve_rejects_unsupported_hash_algorithm( + self, driver_client: S3StorageDriverClient + ) -> None: + """Retrieve raises ValueError when the claim specifies a non-sha256 algorithm.""" + driver = S3StorageDriver(client=driver_client, bucket=BUCKET) + payload = make_payload("unsupported-algo") + [claim] = await driver.store(make_store_context(), [payload]) + + bad_claim = StorageDriverClaim( + claim_data={ + **claim.claim_data, + "hash_algorithm": "md5", + }, + ) + with pytest.raises( + ValueError, + match=r"S3StorageDriver unsupported hash algorithm \[bucket=.+, key=.+\]: expected sha256, got md5", + ): + await driver.retrieve(StorageDriverRetrieveContext(), [bad_claim]) + + async def test_retrieve_without_hash_in_claim( + self, driver_client: S3StorageDriverClient + ) -> None: + """Claims missing content hash fields raise ValueError on retrieve.""" + driver = S3StorageDriver(client=driver_client, bucket=BUCKET) + payload = make_payload("no-hash-claim") + [claim] = await driver.store(make_store_context(), [payload]) + + legacy_claim = StorageDriverClaim( + claim_data={ + "bucket": claim.claim_data["bucket"], + "key": claim.claim_data["key"], + }, + ) + with pytest.raises( + ValueError, + match=r"S3StorageDriver claim is missing required content hash information", + ): + await driver.retrieve(StorageDriverRetrieveContext(), [legacy_claim]) + + +# --------------------------------------------------------------------------- +# TestS3StorageDriverBucketCallable +# --------------------------------------------------------------------------- + + +class TestS3StorageDriverBucketCallable: + async def test_callable_selector_routes_bucket( + self, aioboto3_client: S3Client, driver_client: S3StorageDriverClient + ) -> None: + other_bucket = "other-bucket" + await aioboto3_client.create_bucket(Bucket=other_bucket) + driver = S3StorageDriver( + client=driver_client, + bucket=lambda ctx, p: other_bucket, + ) + [claim] = await driver.store(make_store_context(), [make_payload()]) + assert claim.claim_data["bucket"] == other_bucket + + async def test_selector_called_per_payload( + self, driver_client: S3StorageDriverClient + ) -> None: + call_count = 0 + + def counting_selector(_ctx: StorageDriverStoreContext, _p: Payload) -> str: + nonlocal call_count + call_count += 1 + return BUCKET + + driver = S3StorageDriver(client=driver_client, bucket=counting_selector) + await driver.store( + make_store_context(), [make_payload(f"v{i}") for i in range(3)] + ) + assert call_count == 3 + + async def test_selector_routes_by_activity_type( + self, aioboto3_client: S3Client, driver_client: S3StorageDriverClient + ) -> None: + """bucket callable can route payloads to different buckets by activity type.""" + bucket_a = "bucket-type-a" + bucket_b = "bucket-type-b" + await aioboto3_client.create_bucket(Bucket=bucket_a) + await aioboto3_client.create_bucket(Bucket=bucket_b) + + type_buckets = {"type-a": bucket_a, "type-b": bucket_b} + + def type_selector(ctx: StorageDriverStoreContext, p: Payload) -> str: + del p + act = ( + ctx.target + if isinstance(ctx.target, StorageDriverActivityInfo) + else None + ) + if act and act.type and act.type in type_buckets: + return type_buckets[act.type] + return BUCKET + + driver = S3StorageDriver(client=driver_client, bucket=type_selector) + + ctx_a = make_activity_context( + namespace="ns1", + activity_id="act1", + activity_type="type-a", + ) + [claim_a] = await driver.store(ctx_a, [make_payload("payload-a")]) + assert claim_a.claim_data["bucket"] == bucket_a + + ctx_b = make_activity_context( + namespace="ns1", + activity_id="act2", + activity_type="type-b", + ) + [claim_b] = await driver.store(ctx_b, [make_payload("payload-b")]) + assert claim_b.claim_data["bucket"] == bucket_b + + async def test_selector_receives_context_and_payload( + self, driver_client: S3StorageDriverClient + ) -> None: + received: list[tuple[StorageDriverStoreContext, Payload]] = [] + + def capturing_selector(ctx: StorageDriverStoreContext, p: Payload) -> str: + received.append((ctx, p)) + return BUCKET + + payload = make_payload() + store_ctx = make_workflow_context() + driver = S3StorageDriver(client=driver_client, bucket=capturing_selector) + await driver.store(store_ctx, [payload]) + + assert len(received) == 1 + assert received[0][0] is store_ctx + assert received[0][1] == payload + + +# --------------------------------------------------------------------------- +# TestS3StorageDriverErrors +# --------------------------------------------------------------------------- + + +class TestS3StorageDriverErrors: + async def test_store_nonexistent_bucket_raises( + self, driver_client: S3StorageDriverClient + ) -> None: + bucket = "does-not-exist" + payload = make_payload() + driver = S3StorageDriver(client=driver_client, bucket=bucket) + expected_hash = hashlib.sha256(payload.SerializeToString()).hexdigest() + expected_key = f"v0/d/sha256/{expected_hash}" + with pytest.raises(RuntimeError) as exc_info: + await driver.store(make_store_context(), [payload]) + assert ( + str(exc_info.value) + == f"S3StorageDriver store failed [bucket={bucket}, key={expected_key}, client_region={CLIENT_REGION}]" + ) + assert isinstance(exc_info.value.__cause__, ClientError) + assert ( + exc_info.value.__cause__.response.get("Error", {}).get("Code") + == "NoSuchBucket" + ) + + async def test_retrieve_nonexistent_key_raises( + self, driver_client: S3StorageDriverClient + ) -> None: + key = "/d/sha256/nonexistent" + driver = S3StorageDriver(client=driver_client, bucket=BUCKET) + claim = StorageDriverClaim(claim_data={"bucket": BUCKET, "key": key}) + with pytest.raises(RuntimeError) as exc_info: + await driver.retrieve(StorageDriverRetrieveContext(), [claim]) + assert ( + str(exc_info.value) + == f"S3StorageDriver retrieve failed [bucket={BUCKET}, key={key}, client_region={CLIENT_REGION}]" + ) + assert isinstance(exc_info.value.__cause__, ClientError) + assert ( + exc_info.value.__cause__.response.get("Error", {}).get("Code") + == "NoSuchKey" + ) + + async def test_retrieve_nonexistent_bucket_raises( + self, driver_client: S3StorageDriverClient + ) -> None: + bucket = "does-not-exist" + key = "/d/sha256/anything" + driver = S3StorageDriver(client=driver_client, bucket=BUCKET) + claim = StorageDriverClaim(claim_data={"bucket": bucket, "key": key}) + with pytest.raises(RuntimeError) as exc_info: + await driver.retrieve(StorageDriverRetrieveContext(), [claim]) + assert ( + str(exc_info.value) + == f"S3StorageDriver retrieve failed [bucket={bucket}, key={key}, client_region={CLIENT_REGION}]" + ) + assert isinstance(exc_info.value.__cause__, ClientError) + assert ( + exc_info.value.__cause__.response.get("Error", {}).get("Code") + == "NoSuchBucket" + ) + + async def test_bucket_callable_exception_propagates( + self, driver_client: S3StorageDriverClient + ) -> None: + selector = MagicMock(side_effect=RuntimeError("selector failed")) + driver = S3StorageDriver(client=driver_client, bucket=selector) + with pytest.raises(RuntimeError, match="selector failed"): + await driver.store(make_store_context(), [make_payload()]) + + def test_max_payload_size_zero_raises(self) -> None: + with pytest.raises( + ValueError, match="max_payload_size must be greater than zero" + ): + S3StorageDriver( + client=MagicMock(spec=S3StorageDriverClient), + bucket=BUCKET, + max_payload_size=0, + ) + + def test_max_payload_size_negative_raises(self) -> None: + with pytest.raises( + ValueError, match="max_payload_size must be greater than zero" + ): + S3StorageDriver( + client=MagicMock(spec=S3StorageDriverClient), + bucket=BUCKET, + max_payload_size=-1, + ) + + async def test_payload_exceeds_max_size_raises( + self, driver_client: S3StorageDriverClient + ) -> None: + driver = S3StorageDriver( + client=driver_client, bucket=BUCKET, max_payload_size=10 + ) + with pytest.raises( + ValueError, + match=r"Payload size \d+ bytes exceeds the configured max_payload_size of 10 bytes", + ): + await driver.store(make_store_context(), [make_payload("exceeds-limit")]) + + async def test_payload_at_max_size_succeeds( + self, driver_client: S3StorageDriverClient + ) -> None: + payload = make_payload("x") + driver = S3StorageDriver( + client=driver_client, + bucket=BUCKET, + max_payload_size=len(payload.SerializeToString()), + ) + await driver.store(make_store_context(), [payload]) + + +# --------------------------------------------------------------------------- +# TestS3StorageDriverConcurrency +# --------------------------------------------------------------------------- + + +class _AsyncBarrier: + """Minimal asyncio.Barrier equivalent for Python <3.11.""" + + def __init__(self, parties: int) -> None: + self._parties = parties + self._count = 0 + self._event = asyncio.Event() + + async def wait(self) -> None: + self._count += 1 + if self._count >= self._parties: + self._event.set() + else: + await self._event.wait() + + +def _barrier_wrapper( + fn: Callable[..., Coroutine[Any, Any, Any]], barrier: _AsyncBarrier +): + """Wrap an async method to wait at a barrier before proceeding. + + All concurrent callers must reach the barrier before any of them continue. + If the calls are sequential, the barrier will never be satisfied and the + test times out. + """ + + @wraps(fn) + async def wrapper(*args: Any, **kwargs: Any) -> Any: + await asyncio.wait_for(barrier.wait(), timeout=5) + return await fn(*args, **kwargs) + + return wrapper + + +class TestS3StorageDriverConcurrency: + async def test_store_payloads_concurrently( + self, driver_client: S3StorageDriverClient + ) -> None: + """All uploads must be in-flight concurrently. + + A barrier sized to ``num_payloads`` blocks each upload until every + upload has started. If the driver dispatches sequentially the barrier + is never satisfied and the test times out. + """ + num_payloads = 5 + barrier = _AsyncBarrier(num_payloads) + driver_client.put_object = _barrier_wrapper(driver_client.put_object, barrier) # type: ignore[method-assign] + + driver = S3StorageDriver(client=driver_client, bucket=BUCKET) + payloads = [make_payload(f"concurrent-store-{i}") for i in range(num_payloads)] + + claims = await driver.store(make_store_context(), payloads) + assert len(claims) == num_payloads + + async def test_retrieve_payloads_concurrently( + self, driver_client: S3StorageDriverClient + ) -> None: + """All downloads must be in-flight concurrently.""" + num_payloads = 5 + driver = S3StorageDriver(client=driver_client, bucket=BUCKET) + payloads = [ + make_payload(f"concurrent-retrieve-{i}") for i in range(num_payloads) + ] + claims = await driver.store(make_store_context(), payloads) + + barrier = _AsyncBarrier(num_payloads) + driver_client.get_object = _barrier_wrapper(driver_client.get_object, barrier) # type: ignore[method-assign] + + retrieved = await driver.retrieve(StorageDriverRetrieveContext(), claims) + assert retrieved == payloads + + async def test_store_cancels_remaining_on_failure( + self, driver_client: S3StorageDriverClient + ) -> None: + """When one upload fails, all other in-flight uploads are cancelled.""" + faulty_client = FailOnceDriverClient( + delegate=driver_client, + fail_on="object_exists", + ) + driver = S3StorageDriver(client=faulty_client, bucket=BUCKET) + payloads = [make_payload(f"cancel-store-{i}") for i in range(3)] + + with pytest.raises( + RuntimeError, + match=r"S3StorageDriver store failed \[bucket=.+, key=.+\]", + ) as exc_info: + await driver.store(make_store_context(), payloads) + + assert isinstance(exc_info.value.__cause__, ConnectionError) + assert str(exc_info.value.__cause__) == "S3 connection lost" + assert len(faulty_client.cancelled) == 2, ( + "Expected 2 remaining tasks to be cancelled" + ) + + async def test_retrieve_cancels_remaining_on_failure( + self, driver_client: S3StorageDriverClient + ) -> None: + """When one download fails, all other in-flight downloads are cancelled.""" + driver = S3StorageDriver(client=driver_client, bucket=BUCKET) + payloads = [make_payload(f"cancel-retrieve-{i}") for i in range(3)] + claims = await driver.store(make_store_context(), payloads) + + faulty_client = FailOnceDriverClient( + delegate=driver_client, + fail_on="get_object", + ) + driver = S3StorageDriver(client=faulty_client, bucket=BUCKET) + + with pytest.raises( + RuntimeError, + match=r"S3StorageDriver retrieve failed \[bucket=.+, key=.+\]", + ) as exc_info: + await driver.retrieve(StorageDriverRetrieveContext(), claims) + + assert isinstance(exc_info.value.__cause__, ConnectionError) + assert str(exc_info.value.__cause__) == "S3 connection lost" + assert len(faulty_client.cancelled) == 2, ( + "Expected 2 remaining tasks to be cancelled" + ) + + +# --------------------------------------------------------------------------- +# TestAioboto3StorageDriverClientDescribe +# --------------------------------------------------------------------------- + + +class TestAioboto3StorageDriverClientDescribe: + def _make_client(self, region: str | None) -> _Aioboto3StorageDriverClient: + mock_s3 = MagicMock() + mock_s3.meta.region_name = region + return _Aioboto3StorageDriverClient(mock_s3) + + def test_returns_region(self) -> None: + client = self._make_client(region="ap-southeast-1") + assert client.describe() == {"client_region": "ap-southeast-1"} + + def test_omits_region_when_none(self) -> None: + client = self._make_client(region=None) + assert client.describe() == {} + + def test_omits_region_when_empty_string(self) -> None: + client = self._make_client(region="") + assert client.describe() == {} + + +# --------------------------------------------------------------------------- +# TestFormatClientContext +# --------------------------------------------------------------------------- + + +class TestFormatClientContext: + def test_formats_entry(self) -> None: + client = MagicMock(spec=S3StorageDriverClient) + client.describe.return_value = {"region": "us-east-1"} + assert _format_client_context(client) == ", region=us-east-1" + + def test_returns_empty_string_for_empty_describe(self) -> None: + client = MagicMock(spec=S3StorageDriverClient) + client.describe.return_value = {} + assert _format_client_context(client) == "" + + def test_returns_empty_string_when_describe_raises(self) -> None: + client = MagicMock(spec=S3StorageDriverClient) + client.describe.side_effect = RuntimeError("oops") + assert _format_client_context(client) == "" diff --git a/tests/contrib/aws/s3driver/test_s3driver_worker.py b/tests/contrib/aws/s3driver/test_s3driver_worker.py new file mode 100644 index 000000000..fcf17fc16 --- /dev/null +++ b/tests/contrib/aws/s3driver/test_s3driver_worker.py @@ -0,0 +1,522 @@ +"""Worker integration tests for S3StorageDriver key structure. + +Runs real Temporal workflows against a real worker (backed by a moto S3 +server) and asserts the S3 object key structure produced for each Temporal +primitive: workflow input/output, activity input/output, signals, queries, +updates, and child workflows. +""" + +from __future__ import annotations + +import dataclasses +import hashlib +import uuid +from collections.abc import AsyncIterator +from datetime import timedelta + +import aioboto3 +import pytest +from types_aiobotocore_s3.client import S3Client + +import temporalio.converter +from temporalio.client import Client, WorkflowFailureError +from temporalio.contrib.aws.s3driver import S3StorageDriver +from temporalio.contrib.aws.s3driver.aioboto3 import new_aioboto3_client +from temporalio.converter import ExternalStorage, JSONPlainPayloadConverter +from temporalio.exceptions import ActivityError, ApplicationError +from temporalio.testing import WorkflowEnvironment +from tests.contrib.aws.s3driver.conftest import BUCKET, CLIENT_REGION +from tests.contrib.aws.s3driver.workflows import ( + LARGE, + ChildWorkflow, + DocumentIngestionWorkflow, + LargeIOWorkflow, + LargeOutputNoRetryWorkflow, + ModelTrainingWorkflow, + OrderFulfillmentWorkflow, + ParentWithChildWorkflow, + PaymentProcessingWorkflow, + SignalQueryUpdateWorkflow, + download_document, + extract_text, + index_document, + large_io_activity, + large_output_activity, +) +from tests.helpers import new_worker + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- + +_THRESHOLD = 256 # bytes — low so all test payloads are offloaded + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture +async def tmprl_client( + env: WorkflowEnvironment, aioboto3_client: S3Client +) -> AsyncIterator[Client]: + """Temporal client wired with ExternalStorage backed by the moto S3 server.""" + driver = S3StorageDriver(client=new_aioboto3_client(aioboto3_client), bucket=BUCKET) + yield await env.connect_client( + data_converter=dataclasses.replace( + temporalio.converter.default(), + external_storage=ExternalStorage( + drivers=[driver], + payload_size_threshold=_THRESHOLD, + ), + ), + ) + + +# --------------------------------------------------------------------------- +# Helper +# --------------------------------------------------------------------------- + + +async def _list_keys(aioboto3_client: S3Client) -> list[str]: + resp = await aioboto3_client.list_objects_v2(Bucket=BUCKET) + return sorted( + key for obj in resp.get("Contents", []) if (key := obj.get("Key")) is not None + ) + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + + +async def test_s3_driver_workflow_input_key( + tmprl_client: Client, aioboto3_client: S3Client +) -> None: + workflow_id = str(uuid.uuid4()) + async with new_worker( + tmprl_client, LargeIOWorkflow, activities=[large_io_activity] + ) as worker: + await tmprl_client.execute_workflow( + LargeIOWorkflow.run, + LARGE, + id=workflow_id, + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=5), + ) + keys = await _list_keys(aioboto3_client) + + # Client stores workflow input with ri=null (run ID not yet assigned); + # worker stores activity input with ri=run_id — same bytes, two S3 objects. + assert len(keys) == 2 + assert all( + f"/ns/{tmprl_client.namespace}/wt/LargeIOWorkflow/wi/{workflow_id}/ri/" in k + for k in keys + ) + # Client-side store: ri=null because run ID is not yet known. + assert sum(1 for k in keys if "/ri/null/" in k) == 1 + # Worker-side store: ri=run_id, assigned by the server. + assert sum(1 for k in keys if "/ri/null/" not in k) == 1 + + +async def test_s3_driver_workflow_output_key( + tmprl_client: Client, aioboto3_client: S3Client +) -> None: + workflow_id = str(uuid.uuid4()) + async with new_worker( + tmprl_client, LargeIOWorkflow, activities=[large_io_activity] + ) as worker: + result = await tmprl_client.execute_workflow( + LargeIOWorkflow.run, + "small", # small input stays inline; workflow returns LARGE + id=workflow_id, + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=5), + ) + assert result == LARGE + keys = await _list_keys(aioboto3_client) + # Activity result and workflow result dedup to same key + assert len(keys) == 1 + assert ( + f"/ns/{tmprl_client.namespace}/wt/LargeIOWorkflow/wi/{workflow_id}/ri/" + in keys[0] + ) + # Run ID is known for both activity completion and workflow completion + assert "/ri/null/" not in keys[0] + + +async def test_s3_driver_workflow_activity_input_key( + tmprl_client: Client, aioboto3_client: S3Client +) -> None: + workflow_id = str(uuid.uuid4()) + async with new_worker( + tmprl_client, LargeIOWorkflow, activities=[large_io_activity] + ) as worker: + await tmprl_client.execute_workflow( + LargeIOWorkflow.run, + LARGE, # passed through as the activity's input + id=workflow_id, + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=5), + ) + keys = await _list_keys(aioboto3_client) + # Client start (ri=null) + worker schedules activity (ri=run_id) — same bytes, two objects. + assert len(keys) == 2 + # Both keys are under the workflow wi/ri prefix, not the activity. + assert all( + f"/ns/{tmprl_client.namespace}/wt/LargeIOWorkflow/wi/{workflow_id}/ri/" in k + for k in keys + ) + # Activity input is keyed under the scheduling workflow, not the activity. + assert all("/ai/" not in k for k in keys) + + +async def test_s3_driver_workflow_activity_output_key( + tmprl_client: Client, aioboto3_client: S3Client +) -> None: + workflow_id = str(uuid.uuid4()) + async with new_worker( + tmprl_client, LargeIOWorkflow, activities=[large_io_activity] + ) as worker: + await tmprl_client.execute_workflow( + LargeIOWorkflow.run, + "small", # small input; activity returns LARGE + id=workflow_id, + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=5), + ) + keys = await _list_keys(aioboto3_client) + # Activity result and workflow result are both LARGE so they deduplicate to one object. + assert len(keys) == 1 + assert ( + f"/ns/{tmprl_client.namespace}/wt/LargeIOWorkflow/wi/{workflow_id}/ri/" + in keys[0] + ) + # ri=run_id for both stores (run ID is known by the time the activity completes). + assert "/ri/null/" not in keys[0] + + +async def test_s3_driver_standalone_activity_input_key( + env: WorkflowEnvironment, tmprl_client: Client, aioboto3_client: S3Client +) -> None: + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + activity_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + async with new_worker( + tmprl_client, activities=[large_io_activity], task_queue=task_queue + ): + await tmprl_client.execute_activity( + large_io_activity, + LARGE, + id=activity_id, + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=5), + ) + keys = await _list_keys(aioboto3_client) + # Input and output are the same LARGE bytes but stored under different keys. + assert len(keys) == 2 + # Both keyed under the activity, not a workflow. + assert all( + f"/ns/{tmprl_client.namespace}/at/large_io_activity/ai/{activity_id}/ri/" in k + for k in keys + ) + assert all("/wt/" not in k for k in keys) + # Client-side store does not have run ID information + assert sum(1 for k in keys if "/ri/null/" in k) == 1 + # Worker-side store does have run ID information + assert sum(1 for k in keys if "/ri/null/" not in k) == 1 + + +async def test_s3_driver_standalone_activity_output_key( + env: WorkflowEnvironment, tmprl_client: Client, aioboto3_client: S3Client +) -> None: + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + activity_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + async with new_worker( + tmprl_client, activities=[large_output_activity], task_queue=task_queue + ): + await tmprl_client.execute_activity( + large_output_activity, + id=activity_id, + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=5), + ) + keys = await _list_keys(aioboto3_client) + # Only the output is large; keyed under the activity. + assert len(keys) == 1 + assert ( + f"/ns/{tmprl_client.namespace}/at/large_output_activity/ai/{activity_id}/ri/" + in keys[0] + ) + assert "/ri/null/" not in keys[0] + assert "/wt/" not in keys[0] + + +async def test_s3_driver_signal_arg_key( + tmprl_client: Client, aioboto3_client: S3Client +) -> None: + workflow_id = str(uuid.uuid4()) + async with new_worker(tmprl_client, SignalQueryUpdateWorkflow) as worker: + handle = await tmprl_client.start_workflow( + SignalQueryUpdateWorkflow.run, + id=workflow_id, + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=5), + ) + await handle.signal(SignalQueryUpdateWorkflow.finish, LARGE) + await handle.result() + keys = await _list_keys(aioboto3_client) + # Signal arg + workflow result — two distinct keys (different wt and ri). + assert len(keys) == 2 + # Signal arg: client stores with wt=null (type not known) and ri=null. + assert any(f"/wt/null/wi/{workflow_id}/ri/null/" in k for k in keys) + # Workflow result: worker stores with real type and ri=run_id. + assert any(f"/wt/SignalQueryUpdateWorkflow/wi/{workflow_id}/" in k for k in keys) + + +async def test_s3_driver_query_result_key( + tmprl_client: Client, aioboto3_client: S3Client +) -> None: + workflow_id = str(uuid.uuid4()) + async with new_worker(tmprl_client, SignalQueryUpdateWorkflow) as worker: + handle = await tmprl_client.start_workflow( + SignalQueryUpdateWorkflow.run, + id=workflow_id, + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=5), + ) + result = await handle.query(SignalQueryUpdateWorkflow.get_value, LARGE) + assert result == LARGE + await handle.signal(SignalQueryUpdateWorkflow.finish, "done") + await handle.result() + keys = await _list_keys(aioboto3_client) + # Query arg + (query result deduplicated with workflow result) — two distinct keys. + assert len(keys) == 2 + # Query arg: client stores with wt=null (type not known) and ri=null. + assert any(f"/wt/null/wi/{workflow_id}/ri/null/" in k for k in keys) + # Query result and workflow result are both LARGE and deduplicate to one key with ri=run_id. + assert any(f"/wt/SignalQueryUpdateWorkflow/wi/{workflow_id}/" in k for k in keys) + + +async def test_s3_driver_update_result_key( + tmprl_client: Client, aioboto3_client: S3Client +) -> None: + workflow_id = str(uuid.uuid4()) + async with new_worker(tmprl_client, SignalQueryUpdateWorkflow) as worker: + handle = await tmprl_client.start_workflow( + SignalQueryUpdateWorkflow.run, + id=workflow_id, + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=5), + ) + result = await handle.execute_update(SignalQueryUpdateWorkflow.do_update, LARGE) + assert result == LARGE + await handle.signal(SignalQueryUpdateWorkflow.finish, "done") + await handle.result() + keys = await _list_keys(aioboto3_client) + # Update arg + (update result deduplicated with workflow result) — two distinct keys. + assert len(keys) == 2 + # Update arg: client stores with wt=null (type not known) and ri=null. + assert any(f"/wt/null/wi/{workflow_id}/ri/null/" in k for k in keys) + # Update result and workflow result are both LARGE and deduplicate to one key with ri=run_id. + assert any(f"/wt/SignalQueryUpdateWorkflow/wi/{workflow_id}/" in k for k in keys) + + +async def test_s3_driver_child_workflow_input_key( + tmprl_client: Client, aioboto3_client: S3Client +) -> None: + workflow_id = str(uuid.uuid4()) + async with new_worker( + tmprl_client, ParentWithChildWorkflow, ChildWorkflow + ) as worker: + await tmprl_client.execute_workflow( + ParentWithChildWorkflow.run, + id=workflow_id, + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=5), + ) + keys = await _list_keys(aioboto3_client) + child_workflow_id = f"{workflow_id}-child" + # Child input is the only large payload — stored under the child's wi/ri. + assert len(keys) == 1 + # Keyed under the child: child input is stored in the child's context. + assert ( + f"/ns/{tmprl_client.namespace}/wt/ChildWorkflow/wi/{child_workflow_id}/ri/" + in keys[0] + ) + + +async def test_s3_driver_identified_casing( + tmprl_client: Client, aioboto3_client: S3Client +) -> None: + workflow_id = f"MyWorkflow-{uuid.uuid4()}" + async with new_worker( + tmprl_client, LargeIOWorkflow, activities=[large_io_activity] + ) as worker: + await tmprl_client.execute_workflow( + LargeIOWorkflow.run, + LARGE, + id=workflow_id, + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=5), + ) + keys = await _list_keys(aioboto3_client) + # Client start (ri=null) + worker stores (ri=run_id) — two objects. + assert len(keys) == 2 + # Workflow ID is percent-encoded but casing is preserved verbatim. + assert all( + f"/ns/{tmprl_client.namespace}/wt/LargeIOWorkflow/wi/{workflow_id}/ri/" in k + for k in keys + ), "Workflow ID should preserve original case in the key" + + +async def test_s3_driver_content_dedup( + tmprl_client: Client, aioboto3_client: S3Client +) -> None: + """Document ingestion produces exactly two distinct S3 keys, even though + the payloads are repeatedly passed to different activities.""" + workflow_id = str(uuid.uuid4()) + async with new_worker( + tmprl_client, + DocumentIngestionWorkflow, + activities=[download_document, extract_text, index_document], + ) as worker: + await tmprl_client.execute_workflow( + DocumentIngestionWorkflow.run, + "doc-001", + id=workflow_id, + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=5), + ) + keys = await _list_keys(aioboto3_client) + # Two distinct content hashes (LARGE from download, LARGE_2 from extract) → two keys. + assert len(keys) == 2 + # Both are under the same workflow wi/ri prefix despite crossing activity boundaries. + assert all( + f"/ns/{tmprl_client.namespace}/wt/DocumentIngestionWorkflow/wi/{workflow_id}/ri/" + in k + for k in keys + ) + # The two keys differ by content hash only. + assert keys[0] != keys[1] + + +async def test_s3_driver_single_workflow_same_key_namespace( + tmprl_client: Client, aioboto3_client: S3Client +) -> None: + """A training job started with a large config, injected with large override + parameters mid-run, and polled for large metrics — all produce S3 keys + containing the same workflow ID.""" + workflow_id = str(uuid.uuid4()) + async with new_worker(tmprl_client, ModelTrainingWorkflow) as worker: + handle = await tmprl_client.start_workflow( + ModelTrainingWorkflow.run, + LARGE, # large training config as workflow input + id=workflow_id, + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=5), + ) + metrics = await handle.execute_update( + ModelTrainingWorkflow.get_metrics, "checkpoint-1" + ) + assert metrics is not None + await handle.signal(ModelTrainingWorkflow.apply_overrides, LARGE) + await handle.signal(ModelTrainingWorkflow.complete) + await handle.result() + keys = await _list_keys(aioboto3_client) + # Four distinct keys: client start, signal arg, update result, workflow result. + assert len(keys) == 4 + # All keys are anchored under the same workflow ID regardless of which primitive carried the payload. + assert all(f"/wi/{workflow_id}/" in k for k in keys) + + +async def test_s3_driver_parent_child_independent_key_namespaces( + tmprl_client: Client, aioboto3_client: S3Client +) -> None: + """An order fulfillment workflow spawns a child payment processor and passes + it a large order payload. Child input is keyed under the parent (it lives in + the parent's history); child output is keyed under the parent (for lifecycle + resilience — the child result lives in the parent's completion history).""" + workflow_id = str(uuid.uuid4()) + payment_id = f"{workflow_id}-payment" + async with new_worker( + tmprl_client, OrderFulfillmentWorkflow, PaymentProcessingWorkflow + ) as worker: + await tmprl_client.execute_workflow( + OrderFulfillmentWorkflow.run, + LARGE, # large order details passed to parent and forwarded to child + id=workflow_id, + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=5), + ) + keys = await _list_keys(aioboto3_client) + parent_keys = [k for k in keys if f"/wi/{workflow_id}/" in k] + child_keys = [k for k in keys if f"/wi/{payment_id}/" in k] + # Parent accumulates 3 keys: + # 1. Client start stored in parent's key space (ri=null) + # 2. Child result stored in parent's key space + # 3. Parent's own workflow result + assert len(parent_keys) == 3 + # Child accumulates 1 key: its input from the parent + assert len(child_keys) == 1 + + +async def test_s3_store_failure_surfaces_in_workflow_history( + env: WorkflowEnvironment, moto_server_url: str +) -> None: + """Verifies that an S3 store failure (nonexistent bucket) produces a + RuntimeError with bucket and key context that is visible in Temporal + workflow history via the WorkflowFailureError cause chain.""" + bad_bucket = "nonexistent-bucket" + session = aioboto3.Session() + async with session.client( + "s3", + region_name=CLIENT_REGION, + endpoint_url=moto_server_url, + aws_access_key_id="testing", + aws_secret_access_key="testing", + ) as client: + driver = S3StorageDriver(client=new_aioboto3_client(client), bucket=bad_bucket) + bad_client = await env.connect_client( + data_converter=dataclasses.replace( + temporalio.converter.default(), + external_storage=ExternalStorage( + drivers=[driver], + payload_size_threshold=_THRESHOLD, + ), + ), + ) + workflow_id = str(uuid.uuid4()) + async with new_worker( + bad_client, LargeOutputNoRetryWorkflow, activities=[large_output_activity] + ) as worker: + with pytest.raises(WorkflowFailureError) as exc_info: + await bad_client.execute_workflow( + LargeOutputNoRetryWorkflow.run, + id=workflow_id, + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=5), + ) + + large_payload = JSONPlainPayloadConverter().to_payload(LARGE) + assert large_payload is not None + expected_hash = hashlib.sha256(large_payload.SerializeToString()).hexdigest() + + assert isinstance(exc_info.value, WorkflowFailureError) + activity_error = exc_info.value.__cause__ + assert isinstance(activity_error, ActivityError) + app_error = activity_error.__cause__ + assert isinstance(app_error, ApplicationError) + assert app_error.type == "RuntimeError" + # Key includes run_id which is only known at runtime; use substring checks. + msg = app_error.message + assert f"S3StorageDriver store failed [bucket={bad_bucket}, key=" in msg + assert f"/wt/LargeOutputNoRetryWorkflow/wi/{workflow_id}/ri/" in msg + assert f"/d/sha256/{expected_hash}, client_region={CLIENT_REGION}]" in msg diff --git a/tests/contrib/aws/s3driver/workflows.py b/tests/contrib/aws/s3driver/workflows.py new file mode 100644 index 000000000..4f4b43099 --- /dev/null +++ b/tests/contrib/aws/s3driver/workflows.py @@ -0,0 +1,223 @@ +"""Workflow and activity definitions for test_s3driver.py integration tests. + +Kept in a separate module so the workflow sandbox does not encounter +aioboto3/aiobotocore/botocore/urllib3 imports when preparing workflow classes. +""" + +from __future__ import annotations + +from datetime import timedelta + +from temporalio import activity, workflow +from temporalio.common import RetryPolicy + +LARGE = "x" * 356 # ~358 bytes as a JSON string, above the 256-byte test threshold +LARGE_2 = "y" * 356 # distinct large payload with a different SHA-256 hash + + +@activity.defn +async def large_io_activity(_data: str) -> str: + return LARGE + + +@activity.defn +async def large_output_activity() -> str: + """Returns a large payload with no retries; used to test S3 store failures.""" + return LARGE + + +@workflow.defn +class LargeOutputNoRetryWorkflow: + """Executes a single activity that returns a large payload with no retries. + + Used to verify that S3 store failures surface in workflow history without + retries masking the error. + """ + + @workflow.run + async def run(self) -> str: + return await workflow.execute_activity( + large_output_activity, + schedule_to_close_timeout=timedelta(seconds=10), + retry_policy=RetryPolicy(maximum_attempts=1), + ) + + +@workflow.defn +class LargeIOWorkflow: + """Passes its input to an activity and returns a large output.""" + + @workflow.run + async def run(self, data: str) -> str: + await workflow.execute_activity( + large_io_activity, + data, + schedule_to_close_timeout=timedelta(seconds=10), + ) + return LARGE + + +@activity.defn +async def download_document(document_id: str) -> str: + """Downloads the raw document content from remote storage.""" + del document_id + return LARGE # simulates a large raw document + + +@activity.defn +async def extract_text(raw_content: str) -> str: + """Extracts and normalizes text from the raw document content.""" + del raw_content + return LARGE_2 # simulates extracted text — different content, different hash + + +@activity.defn +async def index_document(text: str) -> str: + """Indexes the extracted text into the search index. Returns the index record ID.""" + del text + return "idx-00001" # small confirmation — not offloaded to external storage + + +@workflow.defn +class DocumentIngestionWorkflow: + """Downloads a document, extracts its text, and indexes it for search. + + Illustrates how large intermediate payloads (raw document content, extracted + text) are transparently offloaded to S3 between activity boundaries without + any special handling in the workflow code. + """ + + @workflow.run + async def run(self, document_id: str) -> str: + raw_content = await workflow.execute_activity( + download_document, + document_id, + schedule_to_close_timeout=timedelta(seconds=10), + ) + extracted_text = await workflow.execute_activity( + extract_text, + raw_content, + schedule_to_close_timeout=timedelta(seconds=10), + ) + return await workflow.execute_activity( + index_document, + extracted_text, + schedule_to_close_timeout=timedelta(seconds=10), + ) + + +@workflow.defn +class ChildWorkflow: + @workflow.run + async def run(self, data: str) -> str: + return f"{len(data)}" + + +@workflow.defn +class ParentWithChildWorkflow: + """Delegates work to a child workflow whose ID is {parent_id}-child.""" + + @workflow.run + async def run(self) -> str: + child_id = f"{workflow.info().workflow_id}-child" + return await workflow.execute_child_workflow( + ChildWorkflow.run, + LARGE, + id=child_id, + execution_timeout=timedelta(seconds=10), + ) + + +@workflow.defn +class PaymentProcessingWorkflow: + """Processes payment for an order and returns a large payment confirmation. + + Intended to be spawned as a child of OrderFulfillmentWorkflow. + """ + + @workflow.run + async def run(self, order_details: str) -> str: + del order_details + return LARGE_2 # payment confirmation + + +@workflow.defn +class OrderFulfillmentWorkflow: + """Coordinates order fulfillment by delegating payment to a child workflow. + + Passes the large order details to a PaymentProcessingWorkflow child whose ID + is {parent_id}-payment, then returns the child's payment confirmation. + """ + + @workflow.run + async def run(self, order_details: str) -> str: + payment_id = f"{workflow.info().workflow_id}-payment" + return await workflow.execute_child_workflow( + PaymentProcessingWorkflow.run, + order_details, + id=payment_id, + execution_timeout=timedelta(seconds=10), + ) + + +@workflow.defn +class ModelTrainingWorkflow: + """Simulates a long-running ML training job. + + Accepts a large training config as input, allows the caller to inject + override parameters mid-run via signal, and exposes intermediate metrics + via an update. Demonstrates that large payloads crossing all three + primitive boundaries (input, signal arg, update result) are stored under + the same workflow ID prefix in S3. + """ + + def __init__(self) -> None: + self._overrides_received = False + self._done = False + + @workflow.run + async def run(self, training_config: str) -> str: + del training_config + await workflow.wait_condition(lambda: self._done) + return LARGE # final training summary + + @workflow.signal + async def apply_overrides(self, override_params: str) -> None: + """Injects updated hyperparameters into the running training job.""" + del override_params + self._overrides_received = True + + @workflow.signal + async def complete(self) -> None: + self._done = True + + @workflow.update + async def get_metrics(self, checkpoint_id: str) -> str: + """Returns the current training metrics snapshot.""" + del checkpoint_id + return LARGE_2 # large metrics payload + + +@workflow.defn +class SignalQueryUpdateWorkflow: + """Long-running workflow that accepts a signal, query, and update.""" + + def __init__(self) -> None: + self._done = False + + @workflow.run + async def run(self) -> str: + await workflow.wait_condition(lambda: self._done) + return LARGE + + @workflow.signal + async def finish(self, _data: str) -> None: + self._done = True + + @workflow.query + def get_value(self, _data: str) -> str: + return LARGE + + @workflow.update + async def do_update(self, _data: str) -> str: + return LARGE diff --git a/tests/contrib/deepagents/__init__.py b/tests/contrib/deepagents/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/contrib/deepagents/helpers.py b/tests/contrib/deepagents/helpers.py new file mode 100644 index 000000000..eb5ba295b --- /dev/null +++ b/tests/contrib/deepagents/helpers.py @@ -0,0 +1,16 @@ +"""Shared helpers for the Deep Agents plugin test suite.""" + +from collections import Counter + +from temporalio.api.enums.v1 import EventType +from temporalio.client import WorkflowHandle + + +async def count_scheduled_activities(handle: WorkflowHandle) -> Counter: + """Count ``ActivityTaskScheduled`` events by activity-type name.""" + counts: Counter = Counter() + async for event in handle.fetch_history_events(): + if event.event_type == EventType.EVENT_TYPE_ACTIVITY_TASK_SCHEDULED: + name = event.activity_task_scheduled_event_attributes.activity_type.name + counts[name] += 1 + return counts diff --git a/tests/contrib/deepagents/test_backends.py b/tests/contrib/deepagents/test_backends.py new file mode 100644 index 000000000..b32a6f12b --- /dev/null +++ b/tests/contrib/deepagents/test_backends.py @@ -0,0 +1,315 @@ +"""``TemporalBackend`` routes real-I/O backend ops through activities. + +A backend that touches disk or a shell must not run its operations from workflow +code. ``TemporalBackend`` wraps such a backend so each op becomes a +``deepagents.backend_op`` activity. The wrapped backend here is a plain object +(no LangChain / deepagents needed), so this boots a real server and proves the +op crosses the activity boundary. + +A state-only backend needs no wrapping — that path is covered against the real +``deepagents.StateBackend`` when it is importable. +""" + +from __future__ import annotations + +import gc +import sys +import uuid +from datetime import timedelta +from pathlib import Path + +import pytest + +from temporalio.testing import WorkflowEnvironment + +pytestmark = pytest.mark.skipif( + sys.version_info < (3, 11), reason="deepagents requires Python >= 3.11" +) +pytest.importorskip("deepagents") +pytest.importorskip("langchain_core") + +from temporalio import workflow +from temporalio.contrib.deepagents import DeepAgentsPlugin, TemporalBackend +from temporalio.contrib.deepagents._tools import ( + register_backend, + registered_backends, +) +from temporalio.worker import Worker +from tests.contrib.deepagents.helpers import count_scheduled_activities + +BACKEND_OP = "deepagents.backend_op" + + +class RecordingBackend: + """A minimal backend doing 'real' work off-workflow, exposing both halves + of the deepagents backend protocol: a sync op (``read``) and its async + twin (``aread``). The async twin is the regression-critical case — + deepagents' filesystem middleware calls ``aread``/``awrite``/…, and an + earlier op list intercepted only sync names, so agent-driven file tools + ran their I/O in-workflow.""" + + def read(self, file_path: str) -> str: + return f"contents of {file_path}" + + async def aread(self, file_path: str) -> str: + return f"acontents of {file_path}" + + +@workflow.defn +class BackendWorkflow: + @workflow.run + async def run(self, path: str) -> str: + backend = TemporalBackend( + RecordingBackend(), + activity_options={"start_to_close_timeout": timedelta(seconds=10)}, + ) + sync_out = await backend.read(path) + async_out = await backend.aread(path) + return f"{sync_out}|{async_out}" + + +# Bind deepagents symbols off the module importorskip returns: a static +# `from deepagents import ...` cannot resolve on Python 3.10 (deepagents +# needs >= 3.11), and with the package absent the type checkers mis-resolve +# the name against this same-named test directory. +_deepagents_mod = pytest.importorskip("deepagents") +_backends_mod = pytest.importorskip("deepagents.backends") +create_deep_agent = _deepagents_mod.create_deep_agent +FilesystemBackend = _backends_mod.FilesystemBackend +StateBackend = _backends_mod.StateBackend + + +# A state-only backend is pure workflow state and must NOT schedule an activity. +@workflow.defn +class StateBackendWorkflow: + @workflow.run + async def run(self) -> str: + backend = StateBackend() + # Merely holding a StateBackend schedules no activity; it is not + # wrapped. Return the class provenance so the assertion is on a real + # runtime property rather than a statically-decidable comparison. + return type(backend).__module__ + + +# The full agent-level seam: a REAL Deep Agent whose BUILT-IN file tools drive a +# REAL FilesystemBackend through TemporalBackend. This is the path a fake-backend +# test cannot cover: deepagents' filesystem middleware calls the ASYNC protocol +# (`awrite` / `aread`), and the ops return protocol dataclasses (WriteResult / +# ReadResult) that must survive the activity boundary as real objects — the +# middleware reads their attributes in-workflow. +@workflow.defn +class FilesystemAgentWorkflow: + @workflow.run + async def run(self, root_dir: str) -> str: + backend = TemporalBackend( + FilesystemBackend(root_dir=root_dir, virtual_mode=True), + activity_options={"start_to_close_timeout": timedelta(seconds=10)}, + ) + agent = create_deep_agent( + model="anthropic:claude-sonnet-4-5", + backend=backend, + system_prompt="Write the note, read it back, then report it.", + ) + result = await agent.ainvoke( + {"messages": [{"role": "user", "content": "Note 'hello' down."}]} + ) + return str(result["messages"][-1].content) + + +@pytest.mark.asyncio +async def test_temporal_backend_op_activity(env: WorkflowEnvironment) -> None: + plugin = DeepAgentsPlugin() + async with Worker( + env.client, + task_queue="da-backend", + workflows=[BackendWorkflow], + plugins=[plugin], + ): + handle = await env.client.start_workflow( + BackendWorkflow.run, + "notes.txt", + id=f"da-backend-{uuid.uuid4()}", + task_queue="da-backend", + ) + out = await handle.result() + + assert out == "contents of notes.txt|acontents of notes.txt" + counts = await count_scheduled_activities(handle) + # One activity per op — the sync read AND the async aread both cross. + assert counts[BACKEND_OP] == 2, counts + + +def test_temporal_backend_unregisters_on_gc() -> None: + # A wrapper is typically constructed per workflow run; its registry entry + # must not outlive it, or a long-lived worker leaks one entry per run. + inner = RecordingBackend() + before = set(registered_backends()) + wrapper = TemporalBackend(inner) + (ref,) = set(registered_backends()) - before + assert registered_backends()[ref] is inner + del wrapper + gc.collect() + assert ref not in registered_backends() + + +def test_temporal_backend_gc_keeps_reregistered_ref() -> None: + # Refs are deterministic per run: after a cache eviction, a replay + # re-registers the SAME ref with a fresh inner backend. The evicted + # wrapper's GC cleanup must leave that live registration alone. + before = set(registered_backends()) + wrapper = TemporalBackend(RecordingBackend()) + (ref,) = set(registered_backends()) - before + replacement = RecordingBackend() + register_backend(ref, replacement) + del wrapper + gc.collect() + assert registered_backends().get(ref) is replacement + registered_backends().pop(ref, None) + + +@pytest.mark.asyncio +async def test_state_backend_in_workflow(env: WorkflowEnvironment) -> None: + # A state-only backend is pure workflow state and must NOT schedule an + # activity. Exercised against the real StateBackend when deepagents is present. + plugin = DeepAgentsPlugin() + async with Worker( + env.client, + task_queue="da-state-backend", + workflows=[StateBackendWorkflow], + plugins=[plugin], + ): + handle = await env.client.start_workflow( + StateBackendWorkflow.run, + id=f"da-state-backend-{uuid.uuid4()}", + task_queue="da-state-backend", + ) + assert (await handle.result()).startswith("deepagents") + counts = await count_scheduled_activities(handle) + assert counts[BACKEND_OP] == 0, counts + + +@pytest.mark.asyncio +async def test_agent_builtin_file_tools_route_backend_ops( + env: WorkflowEnvironment, tmp_path: Path +) -> None: + """An unmodified agent's built-in write_file/read_file tools cross the + activity boundary when the backend is TemporalBackend-wrapped — under + ``max_cached_workflows=0``, so every workflow task replays from history. + + Regression: an earlier op list intercepted only sync method names, so the + middleware's async calls (`awrite`/`aread`) forwarded to the inner backend + and ran real disk I/O in-workflow. This test fails if that recurs, if the + protocol result dataclasses stop surviving the activity boundary, or if + replay diverges. + """ + from langchain_core.messages import AIMessage # real lib; guarded above + + write_turn = AIMessage( + content="", + tool_calls=[ + { + "name": "write_file", + "args": {"file_path": "/notes.txt", "content": "hello"}, + "id": "call-write", + } + ], + ) + read_turn = AIMessage( + content="", + tool_calls=[ + { + "name": "read_file", + "args": {"file_path": "/notes.txt"}, + "id": "call-read", + } + ], + ) + final = AIMessage(content="The note says: hello") + from temporalio.contrib.deepagents.testing import mock_model_provider + + plugin = DeepAgentsPlugin( + model_provider=mock_model_provider([write_turn, read_turn, final]), + ) + async with Worker( + env.client, + task_queue="da-fs-agent", + workflows=[FilesystemAgentWorkflow], + plugins=[plugin], + max_cached_workflows=0, + ): + handle = await env.client.start_workflow( + FilesystemAgentWorkflow.run, + str(tmp_path), + id=f"da-fs-agent-{uuid.uuid4()}", + task_queue="da-fs-agent", + ) + out = await handle.result() + + assert "hello" in out + # The write really happened on disk — in the activity, not the workflow. + assert (tmp_path / "notes.txt").read_text() == "hello" + counts = await count_scheduled_activities(handle) + # Exactly one backend_op per file tool call (awrite + aread), three model turns. + assert counts[BACKEND_OP] == 2, counts + assert counts["deepagents.invoke_model"] == 3, counts + + +@workflow.defn +class DefaultBackendGrepWorkflow: + @workflow.run + async def run(self, prompt: str) -> str: + # No backend argument: deepagents uses its default state-only backend. + agent = create_deep_agent(model="anthropic:claude-sonnet-4-5") + result = await agent.ainvoke( + {"messages": [{"role": "user", "content": prompt}]} + ) + return str(result["messages"][-1].content) + + +@pytest.mark.asyncio +async def test_builtin_tool_on_default_backend_runs_in_workflow( + env: WorkflowEnvironment, +) -> None: + """A built-in tool call (grep) on the DEFAULT state backend runs inline + in the workflow — no activity, no thread hop — under + ``max_cached_workflows=0`` so every task replays from history. + + Regression: ``BackendProtocol``'s async defaults wrap their sync twins in + ``asyncio.to_thread``, which the deterministic workflow event loop + rejects with ``NotImplementedError``. A real model's first spontaneous + ``grep``/``read_file`` call crashed the workflow task; scripted tests + that never invoked built-ins on the default backend sailed past it. The + plugin now runs the sync twin inline when ``workflow.in_workflow()``. + """ + from langchain_core.messages import AIMessage + + from temporalio.contrib.deepagents.testing import mock_model_provider + + grep_turn = AIMessage( + content="", + tool_calls=[{"name": "grep", "args": {"pattern": "hello"}, "id": "call-grep"}], + ) + final = AIMessage(content="No matches found; done.") + plugin = DeepAgentsPlugin( + model_provider=mock_model_provider([grep_turn, final]), + ) + async with Worker( + env.client, + task_queue="da-default-backend", + workflows=[DefaultBackendGrepWorkflow], + plugins=[plugin], + max_cached_workflows=0, + ): + handle = await env.client.start_workflow( + DefaultBackendGrepWorkflow.run, + "Grep the workspace for 'hello'.", + id=f"da-default-backend-{uuid.uuid4()}", + task_queue="da-default-backend", + ) + out = await handle.result() + + assert "done" in out + counts = await count_scheduled_activities(handle) + # The state-backend op stays in-workflow: model turns are the ONLY activities. + assert counts[BACKEND_OP] == 0, counts + assert counts["deepagents.invoke_model"] == 2, counts diff --git a/tests/contrib/deepagents/test_checkpointer.py b/tests/contrib/deepagents/test_checkpointer.py new file mode 100644 index 000000000..851bbb018 --- /dev/null +++ b/tests/contrib/deepagents/test_checkpointer.py @@ -0,0 +1,98 @@ +"""Checkpointing follows the ``contrib.langgraph`` precedent. + +The zero-config default is an in-workflow ``InMemorySaver`` rehydrated by +deterministic replay; no bespoke checkpointer adapter ships. A user-supplied +durable checkpointer would run its own I/O from workflow code, which is not +replay-safe, so the plugin warns (respecting the choice rather than hard-failing) +and points at the snapshot + continue-as-new path. +""" + +from __future__ import annotations + +import sys +import uuid +import warnings + +import pytest + +from temporalio.testing import WorkflowEnvironment + +pytestmark = pytest.mark.skipif( + sys.version_info < (3, 11), reason="deepagents requires Python >= 3.11" +) +pytest.importorskip("deepagents") +pytest.importorskip("langchain_core") + +from temporalio import workflow +from temporalio.contrib.deepagents.workflow import warn_durable_checkpointer + +# Bind deepagents symbols off the module importorskip returns: a static +# `from deepagents import ...` cannot resolve on Python 3.10 (deepagents +# needs >= 3.11), and with the package absent the type checkers mis-resolve +# the name against this same-named test directory. +create_deep_agent = pytest.importorskip("deepagents").create_deep_agent + + +class _DurableSaver: + """Stand-in for a checkpointer that does its own database I/O.""" + + +class InMemorySaver: + """Same class name LangGraph's in-workflow saver uses.""" + + +@workflow.defn +class CheckpointWorkflow: + @workflow.run + async def run(self, prompt: str) -> str: + agent = create_deep_agent(model="fake:model") + result = await agent.ainvoke( + {"messages": [{"role": "user", "content": prompt}]} + ) + return str(result["messages"][-1].content) + + +def test_durable_checkpointer_warns() -> None: + # None and in-workflow savers are silent... + with warnings.catch_warnings(record=True) as records: + warnings.simplefilter("always") + warn_durable_checkpointer(None) + warn_durable_checkpointer(InMemorySaver()) + assert not records, [str(r.message) for r in records] + + # ...a durable saver warns (respect the choice, do not hard-fail). + with pytest.warns(UserWarning, match="durable checkpointer"): + warn_durable_checkpointer(_DurableSaver()) + + +@pytest.mark.asyncio +async def test_default_saver_rehydrates(env: WorkflowEnvironment) -> None: + # Against the real deepagents default (in-workflow InMemorySaver): the agent + # runs and its recorded history replays cleanly, proving replay rehydrates + # the in-workflow checkpoint state with no external checkpointer. + pytest.importorskip("deepagents") + pytest.importorskip("langchain_core") + + from temporalio.contrib.deepagents import DeepAgentsPlugin + from temporalio.contrib.deepagents.testing import mock_model_provider + from temporalio.worker import Replayer, Worker + + plugin = DeepAgentsPlugin(model_provider=mock_model_provider(["Checkpointed."])) + async with Worker( + env.client, + task_queue="da-ckpt", + workflows=[CheckpointWorkflow], + plugins=[plugin], + ): + handle = await env.client.start_workflow( + CheckpointWorkflow.run, + "hello", + id=f"da-ckpt-{uuid.uuid4()}", + task_queue="da-ckpt", + ) + await handle.result() + history = await handle.fetch_history() + + await Replayer( + workflows=[CheckpointWorkflow], plugins=[DeepAgentsPlugin()] + ).replay_workflow(history) diff --git a/tests/contrib/deepagents/test_continue_as_new.py b/tests/contrib/deepagents/test_continue_as_new.py new file mode 100644 index 000000000..c5add7cf7 --- /dev/null +++ b/tests/contrib/deepagents/test_continue_as_new.py @@ -0,0 +1,167 @@ +"""Continue-as-new state carry for long-running Deep Agents. + +``run_deep_agent(continue_as_new_after=...)`` keeps a long conversation from +bloating workflow history: once the current turn finishes past the threshold and +there is still pending work, it snapshots the accumulated messages plus the +model/tool result cache and continues into a fresh run. These tests use a plain +fake agent (no LangChain needed) so they boot a real Temporal server and exercise +the continue-as-new machinery end to end. +""" + +from __future__ import annotations + +import sys +import uuid +from typing import Any + +import pytest + +from temporalio.testing import WorkflowEnvironment + +pytestmark = pytest.mark.skipif( + sys.version_info < (3, 11), reason="deepagents requires Python >= 3.11" +) +from temporalio import workflow +from temporalio.contrib.deepagents import DeepAgentsPlugin, _serde, run_deep_agent +from temporalio.worker import Worker + + +class FakeAgent: + """A stand-in compiled agent that appends a step and reports a todo. + + It is *not* a LangChain object — it just satisfies the ``ainvoke`` shape + ``run_deep_agent`` drives, so the continue-as-new path can be tested without + a model provider or the LangChain import tree. + """ + + async def ainvoke(self, input: Any) -> dict: + messages = list(input.get("messages", [])) if isinstance(input, dict) else [] + messages = [*messages, "step"] + done = len(messages) >= 3 + return { + "messages": messages, + "todos": [ + {"content": "work", "status": "completed" if done else "pending"} + ], + } + + +@workflow.defn +class ContinueAsNewWorkflow: + @workflow.run + async def run(self, input: dict, state_snapshot: dict | None = None) -> dict: + # Threshold of 1 means: continue-as-new as soon as there is pending work, + # which the fake agent reports until the conversation reaches 3 messages. + return await run_deep_agent( + FakeAgent(), + input, + continue_as_new_after=1, + state_snapshot=state_snapshot, + ) + + +@pytest.mark.asyncio +async def test_can_threshold_and_cache(env: WorkflowEnvironment) -> None: + plugin = DeepAgentsPlugin() + async with Worker( + env.client, + task_queue="da-can", + workflows=[ContinueAsNewWorkflow], + plugins=[plugin], + ): + handle = await env.client.start_workflow( + ContinueAsNewWorkflow.run, + {"messages": ["start"]}, + id=f"da-can-{uuid.uuid4()}", + task_queue="da-can", + ) + result = await handle.result() + + # The only way the conversation reaches >= 3 messages is if the snapshot from + # the pre-continue-as-new run was carried into the continued run and merged. + assert len(result["messages"]) >= 3, result + assert result["todos"][0]["status"] == "completed" + + +def test_state_snapshot_roundtrip() -> None: + # The result cache carried in a snapshot rehydrates to the same hits, so work + # done before a continue-as-new is reused, not recomputed, afterwards. + _serde.set_result_cache({}) + key = _serde.cache_key("model", "fake:model", [["m"], []]) + _serde.cache_put(key, {"dumped": "message"}) + snapshot = _serde.result_cache_snapshot() + assert snapshot and key in snapshot + + # Simulate the continued run: a fresh cache seeded from the snapshot. + _serde.set_result_cache(dict(snapshot)) + hit, value = _serde.cache_lookup(key) + assert hit and value == {"dumped": "message"} + + +class SlowFakeAgent: + """Like ``FakeAgent`` but each turn burns timers so a single run's history + grows past the dev server's continue-as-new suggestion threshold (the test + env pins ``limit.historyCount.suggestContinueAsNew`` low).""" + + async def ainvoke(self, input: Any) -> dict: + for _ in range(20): + await workflow.sleep(0.001) + messages = list(input.get("messages", [])) if isinstance(input, dict) else [] + messages = [*messages, "step"] + done = len(messages) >= 3 + return { + "messages": messages, + "todos": [ + {"content": "work", "status": "completed" if done else "pending"} + ], + } + + +@workflow.defn +class SuggestedCanWorkflow: + @workflow.run + async def run(self, input: dict, state_snapshot: dict | None = None) -> dict: + # No continue_as_new_after: the default follows the server's own + # is_continue_as_new_suggested() signal. + return await run_deep_agent( + SlowFakeAgent(), + input, + state_snapshot=state_snapshot, + ) + + +@pytest.mark.asyncio +async def test_can_defaults_to_server_suggestion( + env: WorkflowEnvironment, env_type: str +) -> None: + """With ``continue_as_new_after`` unset, the driver continues-as-new when + the SERVER suggests it (history count/size), not on a fixed threshold.""" + if env_type != "local": + pytest.skip("needs the local dev server's low suggestContinueAsNew threshold") + plugin = DeepAgentsPlugin() + async with Worker( + env.client, + task_queue="da-can-suggested", + workflows=[SuggestedCanWorkflow], + plugins=[plugin], + ): + handle = await env.client.start_workflow( + SuggestedCanWorkflow.run, + {"messages": ["start"]}, + id=f"da-can-suggested-{uuid.uuid4()}", + task_queue="da-can-suggested", + ) + result = await handle.result() + + # Carry across the suggested continue-as-new: the conversation only reaches + # 3 messages if snapshots crossed run boundaries. + assert len(result["messages"]) >= 3, result + assert result["todos"][0]["status"] == "completed" + # The first run really did continue-as-new (not complete). + first = env.client.get_workflow_handle( + handle.id, run_id=handle.first_execution_run_id + ) + desc = await first.describe() + assert desc.status is not None and desc.status.name == "CONTINUED_AS_NEW", ( + desc.status + ) diff --git a/tests/contrib/deepagents/test_failures.py b/tests/contrib/deepagents/test_failures.py new file mode 100644 index 000000000..6cfe6e5ff --- /dev/null +++ b/tests/contrib/deepagents/test_failures.py @@ -0,0 +1,119 @@ +"""Error handling: retry classification and the workflow-failure type. + +Two dep-free paths run against a real server: the HTTP-error → Temporal retry +translation, and that a raised :class:`DeepAgentsWorkflowError` surfaces to the +client as a non-retryable failure with a stable ``ApplicationError.type`` (never +a stringified peer exception). The model-instance validation error needs +LangChain and guards on its import. +""" + +from __future__ import annotations + +import sys +import uuid +from datetime import timedelta + +import pytest + +from temporalio.testing import WorkflowEnvironment + +pytestmark = pytest.mark.skipif( + sys.version_info < (3, 11), reason="deepagents requires Python >= 3.11" +) +from temporalio import workflow +from temporalio.client import WorkflowFailureError +from temporalio.contrib.deepagents import DeepAgentsPlugin, DeepAgentsWorkflowError +from temporalio.contrib.deepagents._activity import _translate_api_error +from temporalio.exceptions import ApplicationError +from temporalio.worker import Worker + + +class _FakeResponse: + def __init__(self, headers: dict) -> None: + self.headers = headers + + +class _FakeHTTPError(Exception): + def __init__(self, status_code: int, headers: dict | None = None) -> None: + super().__init__(f"HTTP {status_code}") + self.status_code = status_code + self.response = _FakeResponse(headers or {}) + + +def test_error_classification() -> None: + # 429 is retryable and honors the upstream Retry-After. + err = _translate_api_error(_FakeHTTPError(429, {"retry-after": "7"})) + assert isinstance(err, ApplicationError) + assert err.non_retryable is False + assert err.next_retry_delay == timedelta(seconds=7) + + # 400 is a client error: non-retryable. + e400 = _translate_api_error(_FakeHTTPError(400)) + assert isinstance(e400, ApplicationError) + assert e400.non_retryable is True + + # 503 is retryable by default... + e503 = _translate_api_error(_FakeHTTPError(503)) + assert isinstance(e503, ApplicationError) + assert e503.non_retryable is False + # ...unless the server explicitly says not to. + forced = _translate_api_error(_FakeHTTPError(503, {"x-should-retry": "false"})) + assert isinstance(forced, ApplicationError) + assert forced.non_retryable is True + + # retry-after-ms wins over retry-after when both are present. + ms = _translate_api_error( + _FakeHTTPError(429, {"retry-after-ms": "250", "retry-after": "7"}) + ) + assert isinstance(ms, ApplicationError) + assert ms.next_retry_delay == timedelta(milliseconds=250) + + # A non-HTTP exception is not recognized, so the caller falls through. + assert _translate_api_error(ValueError("nope")) is None + + +@workflow.defn +class FailingWorkflow: + @workflow.run + async def run(self) -> None: + raise DeepAgentsWorkflowError("deliberate non-retryable failure") + + +@pytest.mark.asyncio +async def test_workflow_failure_type(env: WorkflowEnvironment) -> None: + plugin = DeepAgentsPlugin() + async with Worker( + env.client, + task_queue="da-fail", + workflows=[FailingWorkflow], + plugins=[plugin], + ): + handle = await env.client.start_workflow( + FailingWorkflow.run, + id=f"da-fail-{uuid.uuid4()}", + task_queue="da-fail", + ) + with pytest.raises(WorkflowFailureError) as excinfo: + await handle.result() + + cause = excinfo.value.cause + assert isinstance(cause, ApplicationError) + assert cause.type == "deepagents.DeepAgentsWorkflowError" + assert cause.non_retryable is True + + +@pytest.mark.asyncio +async def test_unwrappable_model_instance() -> None: + pytest.importorskip("langchain_core") + from langchain_core.language_models.fake_chat_models import FakeListChatModel + + # A string is auto-wrapped; a TemporalModel passes through; a live model + # instance is rejected at the workflow boundary with the typed failure. + from temporalio.contrib.deepagents import TemporalModel + from temporalio.contrib.deepagents._model import _wrap_model_arg + + assert isinstance(_wrap_model_arg("anthropic:claude"), TemporalModel) + tm = TemporalModel(model="anthropic:claude") + assert _wrap_model_arg(tm) is tm + with pytest.raises(DeepAgentsWorkflowError): + _wrap_model_arg(FakeListChatModel(responses=["hi"])) diff --git a/tests/contrib/deepagents/test_hitl.py b/tests/contrib/deepagents/test_hitl.py new file mode 100644 index 000000000..6124d41f1 --- /dev/null +++ b/tests/contrib/deepagents/test_hitl.py @@ -0,0 +1,139 @@ +"""Human-in-the-loop: SDK-native interrupt mapped to Query + Update. + +``interrupt_on={...}`` makes Deep Agents pause before a guarded tool runs. With a +checkpointer configured, LangGraph does *not* raise out of ``ainvoke`` — it +returns the current state with an ``__interrupt__`` entry describing the pending +approval (verified against deepagents 0.6.12 / langchain 1.x). Because the loop +runs in the workflow, that pause surfaces directly in workflow code. The plugin's +recommended mapping — used here — is: detect the returned ``__interrupt__``, +expose its payload via a Query, and resume with a Workflow Update carrying the +human's decision through ``Command(resume=...)``. No shim exception is invented; +the native LangGraph resume protocol (``{"decisions": [{"type": ...}]}``) is used +as-is. + +State lives on the workflow instance (per-execution), which is the idiomatic +Temporal pattern for state shared between the run method and its handlers. +""" + +from __future__ import annotations + +import asyncio +import sys +import uuid +from datetime import timedelta +from typing import Any + +import pytest + +from temporalio.testing import WorkflowEnvironment + +pytestmark = pytest.mark.skipif( + sys.version_info < (3, 11), reason="deepagents requires Python >= 3.11" +) + +pytest.importorskip("deepagents") +pytest.importorskip("langchain_core") +pytest.importorskip("langgraph") + +from temporalio import workflow # noqa: E402 +from temporalio.worker import Worker # noqa: E402 + +# Bind deepagents symbols off the module importorskip returns: a static +# `from deepagents import ...` cannot resolve on Python 3.10 (deepagents +# needs >= 3.11), and with the package absent the type checkers mis-resolve +# the name against this same-named test directory. +create_deep_agent = pytest.importorskip("deepagents").create_deep_agent + +with workflow.unsafe.imports_passed_through(): + from langchain_core.messages import AIMessage + from langchain_core.runnables import RunnableConfig + from langgraph.checkpoint.memory import InMemorySaver + from langgraph.types import Command + + from temporalio.contrib.deepagents import DeepAgentsPlugin, tool_as_activity + from temporalio.contrib.deepagents.testing import mock_model_provider + + +@workflow.defn +class HitlWorkflow: + def __init__(self) -> None: + self._interrupt: str | None = None + self._resume_value: str | None = None + self._resumed = False + + @workflow.run + async def run(self, city: str) -> str: + def book_trip(city: str) -> str: + """Book a trip to a city (requires human approval).""" + return f"Booked a trip to {city}." + + trip_tool = tool_as_activity( + book_trip, start_to_close_timeout=timedelta(seconds=30) + ) + agent = create_deep_agent( + model="anthropic:claude-sonnet-4-5", + tools=[trip_tool], + interrupt_on={"book_trip": True}, + checkpointer=InMemorySaver(), + ) + config: RunnableConfig = { + "configurable": {"thread_id": workflow.info().workflow_id} + } + payload: Any = {"messages": [{"role": "user", "content": f"Book {city}."}]} + result = await agent.ainvoke(payload, config=config) + # LangGraph returns (not raises) the pending approval under __interrupt__. + pending = result.get("__interrupt__") + if pending: + self._interrupt = str(getattr(pending[0], "value", pending[0])) + await workflow.wait_condition(lambda: self._resumed) + result = await agent.ainvoke( + Command(resume={"decisions": [{"type": self._resume_value}]}), + config=config, + ) + return result["messages"][-1].content + + @workflow.query + def pending_interrupt(self) -> str | None: + return self._interrupt + + @workflow.update + async def resume(self, decision: str) -> None: + self._resume_value = decision + self._resumed = True + + +@pytest.mark.asyncio +async def test_interrupt_query_then_resume(env: WorkflowEnvironment) -> None: + approve = AIMessage( + content="", + tool_calls=[{"name": "book_trip", "args": {"city": "Rome"}, "id": "c1"}], + ) + done = AIMessage(content="Booked a trip to Rome.") + plugin = DeepAgentsPlugin(model_provider=mock_model_provider([approve, done])) + async with Worker( + env.client, + task_queue="da-hitl", + workflows=[HitlWorkflow], + plugins=[plugin], + ): + handle = await env.client.start_workflow( + HitlWorkflow.run, + "Rome", + id=f"da-hitl-{uuid.uuid4()}", + task_queue="da-hitl", + ) + + # Wait for the agent to hit the interrupt (surfaced via the Query), then + # approve via an Update. A bounded poll with a sleep, so a regression that + # never raises the interrupt fails fast instead of busy-spinning. + for _ in range(100): + if await handle.query(HitlWorkflow.pending_interrupt) is not None: + break + await asyncio.sleep(0.1) + else: + pytest.fail("workflow never surfaced the HITL interrupt via the query") + + await handle.execute_update(HitlWorkflow.resume, "approve") + out = await handle.result() + + assert "Rome" in out diff --git a/tests/contrib/deepagents/test_model_activity.py b/tests/contrib/deepagents/test_model_activity.py new file mode 100644 index 000000000..37a98ef00 --- /dev/null +++ b/tests/contrib/deepagents/test_model_activity.py @@ -0,0 +1,103 @@ +"""The model seam: every ``TemporalModel`` generation is one activity. + +These exercise the seam directly through ``TemporalModel`` (no full agent +needed), so they depend only on LangChain, not on deepagents. +""" + +from __future__ import annotations + +import sys +import uuid +from datetime import timedelta + +import pytest + +from temporalio.testing import WorkflowEnvironment + +pytestmark = pytest.mark.skipif( + sys.version_info < (3, 11), reason="deepagents requires Python >= 3.11" +) + +pytest.importorskip("langchain_core") + +from temporalio import workflow # noqa: E402 +from temporalio.worker import Worker # noqa: E402 +from tests.contrib.deepagents.helpers import count_scheduled_activities # noqa: E402 + +with workflow.unsafe.imports_passed_through(): + from langchain_core.messages import HumanMessage + + from temporalio.contrib.deepagents import DeepAgentsPlugin, TemporalModel + from temporalio.contrib.deepagents.testing import mock_model_provider + +INVOKE_MODEL = "deepagents.invoke_model" + + +@workflow.defn +class ModelWorkflow: + @workflow.run + async def run(self, prompt: str) -> str: + model = TemporalModel(model="fake:model") + message = await model.ainvoke([HumanMessage(content=prompt)]) + return str(message.content) + + +@workflow.defn +class ExplicitTimeoutWorkflow: + @workflow.run + async def run(self, prompt: str) -> str: + model = TemporalModel( + model="fake:model", + activity_options={"start_to_close_timeout": timedelta(seconds=20)}, + ) + message = await model.ainvoke([HumanMessage(content=prompt)]) + return str(message.content) + + +@pytest.mark.asyncio +async def test_model_call_is_activity(env: WorkflowEnvironment) -> None: + plugin = DeepAgentsPlugin( + model_provider=mock_model_provider(["The capital of France is Paris."]), + model_activity_options={"start_to_close_timeout": timedelta(seconds=30)}, + ) + async with Worker( + env.client, + task_queue="da-model", + workflows=[ModelWorkflow], + plugins=[plugin], + ): + handle = await env.client.start_workflow( + ModelWorkflow.run, + "What is the capital of France?", + id=f"da-model-{uuid.uuid4()}", + task_queue="da-model", + ) + out = await handle.result() + assert "Paris" in out + counts = await count_scheduled_activities(handle) + assert counts[INVOKE_MODEL] == 1, counts + + +@pytest.mark.asyncio +async def test_temporal_model_explicit(env: WorkflowEnvironment) -> None: + # The explicit escape hatch routes through the same activity, with a + # per-model timeout override rather than the plugin default. + plugin = DeepAgentsPlugin( + model_provider=mock_model_provider(["Bonjour."]), + ) + async with Worker( + env.client, + task_queue="da-model-explicit", + workflows=[ExplicitTimeoutWorkflow], + plugins=[plugin], + ): + handle = await env.client.start_workflow( + ExplicitTimeoutWorkflow.run, + "hi", + id=f"da-model-x-{uuid.uuid4()}", + task_queue="da-model-explicit", + ) + out = await handle.result() + assert out == "Bonjour." + counts = await count_scheduled_activities(handle) + assert counts[INVOKE_MODEL] == 1, counts diff --git a/tests/contrib/deepagents/test_native_e2e.py b/tests/contrib/deepagents/test_native_e2e.py new file mode 100644 index 000000000..29b373da6 --- /dev/null +++ b/tests/contrib/deepagents/test_native_e2e.py @@ -0,0 +1,141 @@ +"""Cardinal end-to-end test: unmodified ``deepagents`` code, made durable. + +Builds a real Deep Agent with ``create_deep_agent(...)`` and drives it with +``agent.ainvoke(...)`` inside a ``@workflow.defn`` — the only addition is +``plugins=[DeepAgentsPlugin(...)]``. No user call to +``workflow.execute_activity``; the plugin routes model and tool calls to +activities under the hood. + +Guards on the ``deepagents`` / ``langchain_core`` imports (capability detection), +because they are the plugin's own runtime dependency and may be absent on a +docs-only checkout — there is no env-var gate. +""" + +from __future__ import annotations + +import sys +import uuid +from datetime import timedelta + +import pytest + +from temporalio.testing import WorkflowEnvironment + +pytestmark = pytest.mark.skipif( + sys.version_info < (3, 11), reason="deepagents requires Python >= 3.11" +) + +pytest.importorskip("deepagents") +pytest.importorskip("langchain_core") + +from temporalio import workflow # noqa: E402 +from temporalio.worker import Worker # noqa: E402 +from tests.contrib.deepagents.helpers import count_scheduled_activities # noqa: E402 + +# Bind deepagents symbols off the module importorskip returns: a static +# `from deepagents import ...` cannot resolve on Python 3.10 (deepagents +# needs >= 3.11), and with the package absent the type checkers mis-resolve +# the name against this same-named test directory. +create_deep_agent = pytest.importorskip("deepagents").create_deep_agent + +with workflow.unsafe.imports_passed_through(): + from langchain_core.messages import AIMessage + + from temporalio.contrib.deepagents import DeepAgentsPlugin, tool_as_activity + from temporalio.contrib.deepagents.testing import mock_model_provider + +INVOKE_MODEL = "deepagents.invoke_model" +INVOKE_TOOL = "deepagents.invoke_tool" + + +@workflow.defn +class DeepAgentWorkflow: + @workflow.run + async def run(self, question: str) -> str: + agent = create_deep_agent( + model="anthropic:claude-sonnet-4-5", + system_prompt="You are a helpful assistant.", + ) + result = await agent.ainvoke( + {"messages": [{"role": "user", "content": question}]} + ) + return result["messages"][-1].content + + +@workflow.defn +class ToolLoopWorkflow: + @workflow.run + async def run(self, city: str) -> str: + def get_weather(city: str) -> str: + """Return the weather for a city.""" + return f"It is sunny in {city}." + + weather_tool = tool_as_activity( + get_weather, start_to_close_timeout=timedelta(seconds=30) + ) + agent = create_deep_agent( + model="anthropic:claude-sonnet-4-5", + tools=[weather_tool], + system_prompt="Use the weather tool to answer.", + ) + result = await agent.ainvoke( + {"messages": [{"role": "user", "content": f"Weather in {city}?"}]} + ) + return result["messages"][-1].content + + +@pytest.mark.asyncio +async def test_deep_agent_runs_via_plugin(env: WorkflowEnvironment) -> None: + plugin = DeepAgentsPlugin( + model_provider=mock_model_provider(["The answer is 42."]), + ) + async with Worker( + env.client, + task_queue="da-native", + workflows=[DeepAgentWorkflow], + plugins=[plugin], + ): + handle = await env.client.start_workflow( + DeepAgentWorkflow.run, + "What is the meaning of life?", + id=f"da-native-{uuid.uuid4()}", + task_queue="da-native", + ) + out = await handle.result() + + assert "42" in out + counts = await count_scheduled_activities(handle) + # The model call was made durable as an activity, with no user wiring. + assert counts[INVOKE_MODEL] >= 1, counts + + +@pytest.mark.asyncio +async def test_agent_tool_loop_routes_to_activities(env: WorkflowEnvironment) -> None: + # Script the model: first turn asks for the tool, second turn answers. This + # forces model -> tool -> model, proving each call routes through an activity. + tool_call = AIMessage( + content="", + tool_calls=[{"name": "get_weather", "args": {"city": "Paris"}, "id": "call-1"}], + ) + final = AIMessage(content="It is sunny in Paris.") + plugin = DeepAgentsPlugin( + model_provider=mock_model_provider([tool_call, final]), + ) + async with Worker( + env.client, + task_queue="da-tool-loop", + workflows=[ToolLoopWorkflow], + plugins=[plugin], + ): + handle = await env.client.start_workflow( + ToolLoopWorkflow.run, + "Paris", + id=f"da-tool-loop-{uuid.uuid4()}", + task_queue="da-tool-loop", + ) + out = await handle.result() + + assert "Paris" in out + counts = await count_scheduled_activities(handle) + assert counts[INVOKE_MODEL] == 2, counts + assert counts[INVOKE_TOOL] == 1, counts diff --git a/tests/contrib/deepagents/test_readme.py b/tests/contrib/deepagents/test_readme.py new file mode 100644 index 000000000..7aa8ddd7a --- /dev/null +++ b/tests/contrib/deepagents/test_readme.py @@ -0,0 +1,36 @@ +"""The README's code blocks must stay valid Python. + +A copy-paste example that does not even parse is worse than no example. This +compiles every ```python fenced block in the README so a stale snippet fails the +suite. Compilation (not execution) keeps the check dependency-free. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +README = ( + Path(__file__).resolve().parents[3] + / "temporalio" + / "contrib" + / "deepagents" + / "README.md" +) + +_BLOCK = re.compile(r"```python\n(.*?)```", re.DOTALL) + + +def _python_blocks() -> list[str]: + return _BLOCK.findall(README.read_text(encoding="utf-8")) + + +def test_readme_has_python_blocks() -> None: + blocks = _python_blocks() + assert len(blocks) >= 3, "expected the hello-world and composition examples" + + +def test_readme_hello_world_constructs() -> None: + # Every documented snippet must compile as written. + for i, block in enumerate(_python_blocks()): + compile(block, f"", "exec") diff --git a/tests/contrib/deepagents/test_replay.py b/tests/contrib/deepagents/test_replay.py new file mode 100644 index 000000000..ac43a4034 --- /dev/null +++ b/tests/contrib/deepagents/test_replay.py @@ -0,0 +1,65 @@ +"""Recorded histories replay cleanly through the plugin. + +The Deep Agents control loop runs in the workflow, so replay determinism is the +core safety property. This records the history of a real run (a plain fake agent +driven by ``run_deep_agent``, no LangChain needed) and feeds it back through a +``Replayer`` configured with the plugin. A nondeterministic seam would raise on +replay; a clean pass proves the in-workflow dispatch is deterministic. +""" + +from __future__ import annotations + +import sys +import uuid +from typing import Any + +import pytest + +from temporalio.testing import WorkflowEnvironment + +pytestmark = pytest.mark.skipif( + sys.version_info < (3, 11), reason="deepagents requires Python >= 3.11" +) +from temporalio import workflow +from temporalio.contrib.deepagents import DeepAgentsPlugin, run_deep_agent +from temporalio.worker import Replayer, Worker + + +class FakeAgent: + async def ainvoke(self, input: Any) -> dict: + messages = list(input.get("messages", [])) if isinstance(input, dict) else [] + return {"messages": [*messages, "answered"], "todos": []} + + +@workflow.defn +class ReplayWorkflow: + @workflow.run + async def run(self, input: dict) -> dict: + return await run_deep_agent(FakeAgent(), input) + + +@pytest.mark.asyncio +async def test_replay_with_plugin(env: WorkflowEnvironment) -> None: + plugin = DeepAgentsPlugin() + async with Worker( + env.client, + task_queue="da-replay", + workflows=[ReplayWorkflow], + plugins=[plugin], + ): + handle = await env.client.start_workflow( + ReplayWorkflow.run, + {"messages": ["question"]}, + id=f"da-replay-{uuid.uuid4()}", + task_queue="da-replay", + ) + await handle.result() + history = await handle.fetch_history() + + # A fresh replayer (new worker identity) must replay the recorded history + # without a nondeterminism error. + replayer = Replayer( + workflows=[ReplayWorkflow], + plugins=[DeepAgentsPlugin()], + ) + await replayer.replay_workflow(history) diff --git a/tests/contrib/deepagents/test_sandbox_passthrough.py b/tests/contrib/deepagents/test_sandbox_passthrough.py new file mode 100644 index 000000000..26a5ce2e6 --- /dev/null +++ b/tests/contrib/deepagents/test_sandbox_passthrough.py @@ -0,0 +1,89 @@ +"""The plugin's sandbox passthrough makes explicit import guards unnecessary. + +This module imports ``deepagents`` at the top WITHOUT +``workflow.unsafe.imports_passed_through()``. The workflow sandbox re-imports +the defining module of every workflow, so if the plugin's passthrough +configuration did not cover deepagents' import tree, worker registration +below would fail. This is the executable proof behind the README's +guard-free examples. + +Also the e2e for ``create_temporal_deep_agent``: the explicit construction +path that scopes ``activity_options`` to one agent. +""" + +from __future__ import annotations + +import sys +import uuid +from datetime import timedelta + +import pytest + +from temporalio.testing import WorkflowEnvironment + +pytestmark = pytest.mark.skipif( + sys.version_info < (3, 11), reason="deepagents requires Python >= 3.11" +) +pytest.importorskip("deepagents") +pytest.importorskip("langchain_core") + +import deepagents # noqa: F401 # pyright: ignore[reportUnusedImport, reportImplicitRelativeImport] + +from temporalio import workflow +from temporalio.contrib.deepagents import ( + DeepAgentsPlugin, + create_temporal_deep_agent, +) +from temporalio.worker import Worker +from tests.contrib.deepagents.helpers import count_scheduled_activities + + +@workflow.defn +class GuardFreeAgentWorkflow: + @workflow.run + async def run(self, question: str) -> str: + agent = create_temporal_deep_agent( + model="anthropic:claude-sonnet-4-5", + activity_options={"start_to_close_timeout": timedelta(minutes=2)}, + ) + result = await agent.ainvoke( + {"messages": [{"role": "user", "content": question}]} + ) + return str(result["messages"][-1].content) + + +@pytest.mark.asyncio +async def test_guard_free_import_and_explicit_agent( + env: WorkflowEnvironment, +) -> None: + from temporalio.contrib.deepagents.testing import mock_model_provider + + plugin = DeepAgentsPlugin( + model_provider=mock_model_provider(["The answer is 42."]), + ) + async with Worker( + env.client, + task_queue="da-guard-free", + workflows=[GuardFreeAgentWorkflow], + plugins=[plugin], + max_cached_workflows=0, + ): + handle = await env.client.start_workflow( + GuardFreeAgentWorkflow.run, + "What is the meaning of life?", + id=f"da-guard-free-{uuid.uuid4()}", + task_queue="da-guard-free", + ) + out = await handle.result() + + assert "42" in out + counts = await count_scheduled_activities(handle) + assert counts["deepagents.invoke_model"] == 1, counts + + +def test_wrapper_rejects_options_without_wrappable_model() -> None: + with pytest.raises(ValueError, match="activity_options requires"): + create_temporal_deep_agent( + model=object(), + activity_options={"start_to_close_timeout": timedelta(minutes=1)}, + ) diff --git a/tests/contrib/deepagents/test_side_effects.py b/tests/contrib/deepagents/test_side_effects.py new file mode 100644 index 000000000..2b32b9dee --- /dev/null +++ b/tests/contrib/deepagents/test_side_effects.py @@ -0,0 +1,74 @@ +"""Determinism: no unexpected side effects and a bounded activity count. + +Running the worker with ``max_cached_workflows=0`` forces the workflow to be +replayed from history on every activation. If the in-workflow dispatch did +anything nondeterministic, replay would diverge and the workflow would fail. A +clean completion plus an exact ``backend_op`` schedule count proves the seam +schedules one activity per op and nothing more. +""" + +from __future__ import annotations + +import sys +import uuid +from datetime import timedelta + +import pytest + +from temporalio.testing import WorkflowEnvironment + +pytestmark = pytest.mark.skipif( + sys.version_info < (3, 11), reason="deepagents requires Python >= 3.11" +) +from temporalio import workflow +from temporalio.contrib.deepagents import DeepAgentsPlugin, TemporalBackend +from temporalio.worker import Worker +from tests.contrib.deepagents.helpers import count_scheduled_activities + +BACKEND_OP = "deepagents.backend_op" + + +class TwoOpBackend: + """Protocol-named ops: one async (``awrite``, as the filesystem + middleware calls it) and one sync (``read``).""" + + async def awrite(self, file_path: str, _content: str) -> str: + return f"wrote:{file_path}" + + def read(self, file_path: str) -> str: + return f"read:{file_path}" + + +@workflow.defn +class TwoOpWorkflow: + @workflow.run + async def run(self) -> str: + backend = TemporalBackend( + TwoOpBackend(), + activity_options={"start_to_close_timeout": timedelta(seconds=10)}, + ) + await backend.awrite("a.txt", "hello") + return await backend.read("a.txt") + + +@pytest.mark.asyncio +async def test_activity_schedule_counts(env: WorkflowEnvironment) -> None: + plugin = DeepAgentsPlugin() + async with Worker( + env.client, + task_queue="da-side-effects", + workflows=[TwoOpWorkflow], + plugins=[plugin], + max_cached_workflows=0, + ): + handle = await env.client.start_workflow( + TwoOpWorkflow.run, + id=f"da-side-effects-{uuid.uuid4()}", + task_queue="da-side-effects", + ) + out = await handle.result() + + assert out == "read:a.txt" + counts = await count_scheduled_activities(handle) + # Exactly the two backend ops, each scheduled once — no hidden replays of work. + assert counts[BACKEND_OP] == 2, counts diff --git a/tests/contrib/deepagents/test_streaming.py b/tests/contrib/deepagents/test_streaming.py new file mode 100644 index 000000000..6b65aa0af --- /dev/null +++ b/tests/contrib/deepagents/test_streaming.py @@ -0,0 +1,107 @@ +"""Streaming: model calls route through the streaming activity when a topic is set. + +Setting ``streaming_topic`` flips model dispatch from ``invoke_model`` to +``invoke_model_streaming``, which streams chunks out of the activity and returns +the aggregated final message to the workflow (so the durable result matches the +non-streaming path). These assert the observable effects: which activity is +scheduled, the aggregated content, and that a custom batch interval is threaded +into the streaming activity. +""" + +from __future__ import annotations + +import sys +import uuid +from datetime import timedelta + +import pytest + +from temporalio.testing import WorkflowEnvironment + +pytestmark = pytest.mark.skipif( + sys.version_info < (3, 11), reason="deepagents requires Python >= 3.11" +) + +pytest.importorskip("langchain_core") + +from temporalio import workflow # noqa: E402 +from temporalio.worker import Worker # noqa: E402 +from tests.contrib.deepagents.helpers import count_scheduled_activities # noqa: E402 + +with workflow.unsafe.imports_passed_through(): + from langchain_core.messages import HumanMessage + + from temporalio.contrib.deepagents import DeepAgentsPlugin, TemporalModel + from temporalio.contrib.deepagents.testing import mock_model_provider + +INVOKE_MODEL = "deepagents.invoke_model" +INVOKE_MODEL_STREAMING = "deepagents.invoke_model_streaming" + + +@workflow.defn +class StreamWorkflow: + @workflow.run + async def run(self, prompt: str) -> str: + model = TemporalModel(model="fake:model") + parts: list[str] = [] + async for chunk in model.astream([HumanMessage(content=prompt)]): + parts.append(str(chunk.content)) + return "".join(parts) + + +@pytest.mark.asyncio +async def test_stream_chunks_published(env: WorkflowEnvironment) -> None: + plugin = DeepAgentsPlugin( + model_provider=mock_model_provider(["Streamed answer."]), + streaming_topic="da-stream-topic", + model_activity_options={"start_to_close_timeout": timedelta(seconds=30)}, + ) + async with Worker( + env.client, + task_queue="da-stream", + workflows=[StreamWorkflow], + plugins=[plugin], + ): + handle = await env.client.start_workflow( + StreamWorkflow.run, + "stream please", + id=f"da-stream-{uuid.uuid4()}", + task_queue="da-stream", + ) + out = await handle.result() + + assert "Streamed answer." in out + counts = await count_scheduled_activities(handle) + # The topic is set, so dispatch used the streaming activity, not invoke_model. + assert counts[INVOKE_MODEL_STREAMING] == 1, counts + assert counts[INVOKE_MODEL] == 0, counts + + +@pytest.mark.asyncio +async def test_batch_interval_coalesces(env: WorkflowEnvironment) -> None: + # A custom batch interval is threaded into the streaming activity that + # coalesces chunks; streaming still returns the aggregated message. + plugin = DeepAgentsPlugin( + model_provider=mock_model_provider(["Batched."]), + streaming_topic="da-batch-topic", + streaming_batch_interval=timedelta(milliseconds=500), + ) + assert plugin._activities._streaming_batch_interval == timedelta(milliseconds=500) + + async with Worker( + env.client, + task_queue="da-batch", + workflows=[StreamWorkflow], + plugins=[plugin], + ): + handle = await env.client.start_workflow( + StreamWorkflow.run, + "batch please", + id=f"da-batch-{uuid.uuid4()}", + task_queue="da-batch", + ) + out = await handle.result() + + assert "Batched." in out + counts = await count_scheduled_activities(handle) + assert counts[INVOKE_MODEL_STREAMING] == 1, counts diff --git a/tests/contrib/deepagents/test_subagents.py b/tests/contrib/deepagents/test_subagents.py new file mode 100644 index 000000000..49a407e9d --- /dev/null +++ b/tests/contrib/deepagents/test_subagents.py @@ -0,0 +1,90 @@ +"""Sub-agents inherit the activity seams. + +Deep Agents builds sub-agents as separate graphs, but they inherit the parent's +``model`` object and tools by default. Because the plugin substitutes the model +*object*, every sub-agent's model call routes through an activity without any +per-sub-agent wiring. This runs a real agent configured with a sub-agent and +asserts model calls still land on the activity seam. + +The exact number of hops depends on ``deepagents`` internals we do not pin, so +the assertion is the robust invariant: the configured agent runs and at least one +model call went through an activity. +""" + +from __future__ import annotations + +import sys +import uuid + +import pytest + +from temporalio.testing import WorkflowEnvironment + +pytestmark = pytest.mark.skipif( + sys.version_info < (3, 11), reason="deepagents requires Python >= 3.11" +) + +pytest.importorskip("deepagents") +pytest.importorskip("langchain_core") + +from temporalio import workflow # noqa: E402 +from temporalio.worker import Worker # noqa: E402 +from tests.contrib.deepagents.helpers import count_scheduled_activities # noqa: E402 + +# Bind deepagents symbols off the module importorskip returns: a static +# `from deepagents import ...` cannot resolve on Python 3.10 (deepagents +# needs >= 3.11), and with the package absent the type checkers mis-resolve +# the name against this same-named test directory. +create_deep_agent = pytest.importorskip("deepagents").create_deep_agent + +with workflow.unsafe.imports_passed_through(): + from temporalio.contrib.deepagents import DeepAgentsPlugin + from temporalio.contrib.deepagents.testing import mock_model_provider + +INVOKE_MODEL = "deepagents.invoke_model" + + +@workflow.defn +class SubAgentWorkflow: + @workflow.run + async def run(self, question: str) -> str: + agent = create_deep_agent( + model="anthropic:claude-sonnet-4-5", + system_prompt="You coordinate research.", + subagents=[ + { + "name": "researcher", + "description": "Researches a topic in depth.", + "system_prompt": "You research topics.", + } + ], + ) + result = await agent.ainvoke( + {"messages": [{"role": "user", "content": question}]} + ) + return result["messages"][-1].content + + +@pytest.mark.asyncio +async def test_subagent_calls_route_to_activities(env: WorkflowEnvironment) -> None: + plugin = DeepAgentsPlugin( + model_provider=mock_model_provider(["Coordinated answer."]), + ) + async with Worker( + env.client, + task_queue="da-subagent", + workflows=[SubAgentWorkflow], + plugins=[plugin], + ): + handle = await env.client.start_workflow( + SubAgentWorkflow.run, + "Investigate the topic.", + id=f"da-subagent-{uuid.uuid4()}", + task_queue="da-subagent", + ) + out = await handle.result() + + assert out + counts = await count_scheduled_activities(handle) + # A model instance shared with the sub-agent means model calls are activities. + assert counts[INVOKE_MODEL] >= 1, counts diff --git a/tests/contrib/deepagents/test_tools.py b/tests/contrib/deepagents/test_tools.py new file mode 100644 index 000000000..1fc1ff212 --- /dev/null +++ b/tests/contrib/deepagents/test_tools.py @@ -0,0 +1,238 @@ +"""The tool seam: existing activities and wrapped tools route to activities. + +These need LangChain (a tool is a ``BaseTool``) and guard on its import. Each +runs a real workflow that invokes the wrapped tool and asserts it scheduled the +expected activity. +""" + +from __future__ import annotations + +import sys +import uuid +from collections.abc import Sequence +from datetime import timedelta +from types import SimpleNamespace +from typing import Any + +import pytest + +from temporalio.testing import WorkflowEnvironment + +pytestmark = pytest.mark.skipif( + sys.version_info < (3, 11), reason="deepagents requires Python >= 3.11" +) + +pytest.importorskip("langchain_core") + +from temporalio import activity, workflow # noqa: E402 +from temporalio.worker import Worker # noqa: E402 +from tests.contrib.deepagents.helpers import count_scheduled_activities # noqa: E402 + +with workflow.unsafe.imports_passed_through(): + from temporalio.contrib.deepagents import ( # noqa: E402 + DeepAgentsPlugin, + activity_as_tool, + tool_as_activity, + ) + from temporalio.contrib.deepagents._tools import warn_unwrapped_tools # noqa: E402 + +INVOKE_TOOL = "deepagents.invoke_tool" + +# Bind deepagents symbols off the module importorskip returns: a static +# `from deepagents import ...` cannot resolve on Python 3.10 (deepagents +# needs >= 3.11), and with the package absent the type checkers mis-resolve +# the name against this same-named test directory. +create_deep_agent = pytest.importorskip("deepagents").create_deep_agent + + +def pairing_weather(city: str) -> str: + """Return the weather for a city.""" + return f"weather:{city}" + + +@activity.defn +async def echo_activity(text: str) -> str: + return f"echo:{text}" + + +@workflow.defn +class ActivityAsToolWorkflow: + @workflow.run + async def run(self, text: str) -> str: + tool = activity_as_tool( + echo_activity, start_to_close_timeout=timedelta(seconds=10) + ) + return await tool.ainvoke({"text": text}) + + +@workflow.defn +class ToolAsActivityWorkflow: + @workflow.run + async def run(self, city: str) -> str: + def get_weather(city: str) -> str: + """Look up the weather for a city.""" + return f"sunny in {city}" + + tool = tool_as_activity( + get_weather, start_to_close_timeout=timedelta(seconds=10) + ) + # The wrapper returns plain CONTENT (the tool node stamps the model's + # tool_call_id onto it) — not a pre-built ToolMessage. + return str(await tool.ainvoke({"city": city})) + + +@pytest.mark.asyncio +async def test_activity_as_tool(env: WorkflowEnvironment) -> None: + plugin = DeepAgentsPlugin() + async with Worker( + env.client, + task_queue="da-act-tool", + workflows=[ActivityAsToolWorkflow], + activities=[echo_activity], + plugins=[plugin], + ): + handle = await env.client.start_workflow( + ActivityAsToolWorkflow.run, + "hi", + id=f"da-act-tool-{uuid.uuid4()}", + task_queue="da-act-tool", + ) + out = await handle.result() + + assert out == "echo:hi" + counts = await count_scheduled_activities(handle) + assert counts["echo_activity"] == 1, counts + + +@pytest.mark.asyncio +async def test_tool_as_activity(env: WorkflowEnvironment) -> None: + plugin = DeepAgentsPlugin() + async with Worker( + env.client, + task_queue="da-tool-act", + workflows=[ToolAsActivityWorkflow], + plugins=[plugin], + ): + handle = await env.client.start_workflow( + ToolAsActivityWorkflow.run, + "Paris", + id=f"da-tool-act-{uuid.uuid4()}", + task_queue="da-tool-act", + ) + out = await handle.result() + + assert "sunny in Paris" in out + counts = await count_scheduled_activities(handle) + assert counts[INVOKE_TOOL] == 1, counts + + +def test_builtin_tool_in_workflow(recwarn: pytest.WarningsRecorder) -> None: + # Built-in tool names never warn (they are pure, in-workflow); an unwrapped + # user tool does warn so the Workflow-vs-Activity choice is conscious. + warn_unwrapped_tools([SimpleNamespace(name="write_todos")]) + assert len(recwarn) == 0 + + warn_unwrapped_tools([SimpleNamespace(name="scrape_website")]) + assert any("scrape_website" in str(w.message) for w in recwarn) + + +# Turn-2 requests observed by the fake model, captured activity-side. Module +# state is shared with the in-process worker, same as the tool registries. +_captured_requests: list[list] = [] + + +@workflow.defn +class ToolCallIdPairingWorkflow: + @workflow.run + async def run(self, city: str) -> str: + weather_tool = tool_as_activity( + pairing_weather, start_to_close_timeout=timedelta(seconds=10) + ) + agent = create_deep_agent( + model="anthropic:claude-sonnet-4-5", + tools=[weather_tool], + system_prompt="Use the weather tool.", + ) + result = await agent.ainvoke( + {"messages": [{"role": "user", "content": f"Weather in {city}?"}]} + ) + return str(result["messages"][-1].content) + + +@pytest.mark.asyncio +async def test_wrapped_tool_result_pairs_with_model_tool_call_id( + env: WorkflowEnvironment, +) -> None: + """The tool_result the model sees on turn 2 must carry the model's OWN + tool_call_id. Regression: ``tool_as_activity`` returned the activity-built + ``ToolMessage`` whose workflow-generated id a real provider (Anthropic) + rejects as an unpaired ``tool_result`` — offline fakes never validate the + pairing, so only a live-model run surfaced it. The wrapper now returns + plain content and the tool node stamps the correct id. + """ + from langchain_core.messages import AIMessage, ToolMessage + + from temporalio.contrib.deepagents.testing import FakeModel + + _captured_requests.clear() + + class RecordingModel(FakeModel): + def __init__(self, responses: Sequence[Any]) -> None: + super().__init__(responses) + + async def _agenerate( + self, + messages: list[Any], + stop: list[str] | None = None, + run_manager: Any = None, + **kwargs: Any, + ) -> Any: + _captured_requests.append(list(messages)) + return await super()._agenerate( + messages, stop=stop, run_manager=run_manager, **kwargs + ) + + tool_turn = AIMessage( + content="", + tool_calls=[ + { + "name": "pairing_weather", + "args": {"city": "Paris"}, + "id": "toolu_scripted_pairing_id", + } + ], + ) + final = AIMessage(content="It is sunny in Paris.") + responses = [tool_turn, final] + cursor = {"i": 0} + + def provider(_model_name: str) -> RecordingModel: + reply = responses[cursor["i"] % len(responses)] + cursor["i"] += 1 + return RecordingModel([reply]) + + plugin = DeepAgentsPlugin(model_provider=provider) + async with Worker( + env.client, + task_queue="da-toolcall-pairing", + workflows=[ToolCallIdPairingWorkflow], + plugins=[plugin], + max_cached_workflows=0, + ): + handle = await env.client.start_workflow( + ToolCallIdPairingWorkflow.run, + "Paris", + id=f"da-toolcall-pairing-{uuid.uuid4()}", + task_queue="da-toolcall-pairing", + ) + out = await handle.result() + + assert "sunny" in out.lower() + # Turn 2's request must contain the tool result under the MODEL's id. + assert len(_captured_requests) >= 2, len(_captured_requests) + tool_messages = [m for m in _captured_requests[1] if isinstance(m, ToolMessage)] + assert tool_messages, _captured_requests[1] + assert tool_messages[0].tool_call_id == "toolu_scripted_pairing_id", tool_messages[ + 0 + ].tool_call_id + assert "weather:Paris" in str(tool_messages[0].content) diff --git a/tests/contrib/google_adk_agents/__init__.py b/tests/contrib/google_adk_agents/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/contrib/google_adk_agents/histories/multi_agent.json b/tests/contrib/google_adk_agents/histories/multi_agent.json new file mode 100644 index 000000000..7323575d9 --- /dev/null +++ b/tests/contrib/google_adk_agents/histories/multi_agent.json @@ -0,0 +1,499 @@ +{ + "events": [ + { + "eventId": "1", + "eventTime": "2026-01-26T21:08:54.450497Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED", + "taskId": "1103693", + "workflowExecutionStartedEventAttributes": { + "workflowType": { + "name": "MultiAgentWorkflow" + }, + "taskQueue": { + "name": "adk-task-queue", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IlJ1biBtdWx0LWFnZW50IGZsb3ci" + }, + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "InJlc2VhcmNoX21vZGVsIg==" + } + ] + }, + "workflowTaskTimeout": "10s", + "originalExecutionRunId": "019bfc23-5c32-7791-ae30-aa4b590df541", + "identity": "69823@Tims-MacBook-Pro.local", + "firstExecutionRunId": "019bfc23-5c32-7791-ae30-aa4b590df541", + "attempt": 1, + "firstWorkflowTaskBackoff": "0s", + "workflowId": "multi-agent-workflow-a0d23123-4773-479e-849e-e66e529cd9aa", + "priority": {} + } + }, + { + "eventId": "2", + "eventTime": "2026-01-26T21:08:54.450553Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1103694", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "adk-task-queue", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "3", + "eventTime": "2026-01-26T21:08:54.451648Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1103699", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "2", + "identity": "69823@Tims-MacBook-Pro.local", + "requestId": "35fb116a-4647-4936-9f56-688f623a4a1f", + "historySizeBytes": "397", + "workerVersion": { + "buildId": "2f45d1deb022376ac8b03fdcb17f8e5c" + } + } + }, + { + "eventId": "4", + "eventTime": "2026-01-26T21:08:54.511359Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1103703", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "2", + "startedEventId": "3", + "identity": "69823@Tims-MacBook-Pro.local", + "workerVersion": { + "buildId": "2f45d1deb022376ac8b03fdcb17f8e5c" + }, + "sdkMetadata": { + "coreUsedFlags": [ + 2, + 3, + 1 + ], + "sdkName": "temporal-python", + "sdkVersion": "1.21.1" + }, + "meteringMetadata": {} + } + }, + { + "eventId": "5", + "eventTime": "2026-01-26T21:08:54.511391Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", + "taskId": "1103704", + "activityTaskScheduledEventAttributes": { + "activityId": "1", + "activityType": { + "name": "invoke_model" + }, + "taskQueue": { + "name": "adk-task-queue", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "header": {}, + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJtb2RlbCI6InJlc2VhcmNoX21vZGVsIiwiY29udGVudHMiOlt7InBhcnRzIjpbeyJtZWRpYVJlc29sdXRpb24iOm51bGwsImNvZGVFeGVjdXRpb25SZXN1bHQiOm51bGwsImV4ZWN1dGFibGVDb2RlIjpudWxsLCJmaWxlRGF0YSI6bnVsbCwiZnVuY3Rpb25DYWxsIjpudWxsLCJmdW5jdGlvblJlc3BvbnNlIjpudWxsLCJpbmxpbmVEYXRhIjpudWxsLCJ0ZXh0IjoiV3JpdGUgYSBoYWlrdSBhYm91dCBSdW4gbXVsdC1hZ2VudCBmbG93LiBGaXJzdCByZXNlYXJjaCBpdCwgdGhlbiB3cml0ZSBpdC4iLCJ0aG91Z2h0IjpudWxsLCJ0aG91Z2h0U2lnbmF0dXJlIjpudWxsLCJ2aWRlb01ldGFkYXRhIjpudWxsfV0sInJvbGUiOiJ1c2VyIn1dLCJjb25maWciOnsiaHR0cE9wdGlvbnMiOm51bGwsInNob3VsZFJldHVybkh0dHBSZXNwb25zZSI6bnVsbCwic3lzdGVtSW5zdHJ1Y3Rpb24iOiJZb3UgYXJlIGEgY29vcmRpbmF0b3IuIERlbGVnYXRlIHRvIHJlc2VhcmNoZXIgdGhlbiB3cml0ZXIuXG5cbllvdSBhcmUgYW4gYWdlbnQuIFlvdXIgaW50ZXJuYWwgbmFtZSBpcyBcImNvb3JkaW5hdG9yXCIuXG5cblxuWW91IGhhdmUgYSBsaXN0IG9mIG90aGVyIGFnZW50cyB0byB0cmFuc2ZlciB0bzpcblxuXG5BZ2VudCBuYW1lOiByZXNlYXJjaGVyXG5BZ2VudCBkZXNjcmlwdGlvbjogXG5cblxuQWdlbnQgbmFtZTogd3JpdGVyXG5BZ2VudCBkZXNjcmlwdGlvbjogXG5cblxuSWYgeW91IGFyZSB0aGUgYmVzdCB0byBhbnN3ZXIgdGhlIHF1ZXN0aW9uIGFjY29yZGluZyB0byB5b3VyIGRlc2NyaXB0aW9uLFxueW91IGNhbiBhbnN3ZXIgaXQuXG5cbklmIGFub3RoZXIgYWdlbnQgaXMgYmV0dGVyIGZvciBhbnN3ZXJpbmcgdGhlIHF1ZXN0aW9uIGFjY29yZGluZyB0byBpdHNcbmRlc2NyaXB0aW9uLCBjYWxsIGB0cmFuc2Zlcl90b19hZ2VudGAgZnVuY3Rpb24gdG8gdHJhbnNmZXIgdGhlIHF1ZXN0aW9uIHRvIHRoYXRcbmFnZW50LiBXaGVuIHRyYW5zZmVycmluZywgZG8gbm90IGdlbmVyYXRlIGFueSB0ZXh0IG90aGVyIHRoYW4gdGhlIGZ1bmN0aW9uXG5jYWxsLlxuXG4qKk5PVEUqKjogdGhlIG9ubHkgYXZhaWxhYmxlIGFnZW50cyBmb3IgYHRyYW5zZmVyX3RvX2FnZW50YCBmdW5jdGlvbiBhcmVcbmByZXNlYXJjaGVyYCwgYHdyaXRlcmAuXG4iLCJ0ZW1wZXJhdHVyZSI6bnVsbCwidG9wUCI6bnVsbCwidG9wSyI6bnVsbCwiY2FuZGlkYXRlQ291bnQiOm51bGwsIm1heE91dHB1dFRva2VucyI6bnVsbCwic3RvcFNlcXVlbmNlcyI6bnVsbCwicmVzcG9uc2VMb2dwcm9icyI6bnVsbCwibG9ncHJvYnMiOm51bGwsInByZXNlbmNlUGVuYWx0eSI6bnVsbCwiZnJlcXVlbmN5UGVuYWx0eSI6bnVsbCwic2VlZCI6bnVsbCwicmVzcG9uc2VNaW1lVHlwZSI6bnVsbCwicmVzcG9uc2VTY2hlbWEiOm51bGwsInJlc3BvbnNlSnNvblNjaGVtYSI6bnVsbCwicm91dGluZ0NvbmZpZyI6bnVsbCwibW9kZWxTZWxlY3Rpb25Db25maWciOm51bGwsInNhZmV0eVNldHRpbmdzIjpudWxsLCJ0b29scyI6W3sicmV0cmlldmFsIjpudWxsLCJjb21wdXRlclVzZSI6bnVsbCwiZmlsZVNlYXJjaCI6bnVsbCwiY29kZUV4ZWN1dGlvbiI6bnVsbCwiZW50ZXJwcmlzZVdlYlNlYXJjaCI6bnVsbCwiZnVuY3Rpb25EZWNsYXJhdGlvbnMiOlt7ImRlc2NyaXB0aW9uIjoiVHJhbnNmZXIgdGhlIHF1ZXN0aW9uIHRvIGFub3RoZXIgYWdlbnQuXG5cblRoaXMgdG9vbCBoYW5kcyBvZmYgY29udHJvbCB0byBhbm90aGVyIGFnZW50IHdoZW4gaXQncyBtb3JlIHN1aXRhYmxlIHRvXG5hbnN3ZXIgdGhlIHVzZXIncyBxdWVzdGlvbiBhY2NvcmRpbmcgdG8gdGhlIGFnZW50J3MgZGVzY3JpcHRpb24uXG5cbk5vdGU6XG4gIEZvciBtb3N0IHVzZSBjYXNlcywgeW91IHNob3VsZCB1c2UgVHJhbnNmZXJUb0FnZW50VG9vbCBpbnN0ZWFkIG9mIHRoaXNcbiAgZnVuY3Rpb24gZGlyZWN0bHkuIFRyYW5zZmVyVG9BZ2VudFRvb2wgcHJvdmlkZXMgYWRkaXRpb25hbCBlbnVtIGNvbnN0cmFpbnRzXG4gIHRoYXQgcHJldmVudCBMTE1zIGZyb20gaGFsbHVjaW5hdGluZyBpbnZhbGlkIGFnZW50IG5hbWVzLlxuXG5BcmdzOlxuICBhZ2VudF9uYW1lOiB0aGUgYWdlbnQgbmFtZSB0byB0cmFuc2ZlciB0by5cbiIsIm5hbWUiOiJ0cmFuc2Zlcl90b19hZ2VudCIsInBhcmFtZXRlcnMiOnsiYWRkaXRpb25hbFByb3BlcnRpZXMiOm51bGwsImRlZnMiOm51bGwsInJlZiI6bnVsbCwiYW55T2YiOm51bGwsImRlZmF1bHQiOm51bGwsImRlc2NyaXB0aW9uIjpudWxsLCJlbnVtIjpudWxsLCJleGFtcGxlIjpudWxsLCJmb3JtYXQiOm51bGwsIml0ZW1zIjpudWxsLCJtYXhJdGVtcyI6bnVsbCwibWF4TGVuZ3RoIjpudWxsLCJtYXhQcm9wZXJ0aWVzIjpudWxsLCJtYXhpbXVtIjpudWxsLCJtaW5JdGVtcyI6bnVsbCwibWluTGVuZ3RoIjpudWxsLCJtaW5Qcm9wZXJ0aWVzIjpudWxsLCJtaW5pbXVtIjpudWxsLCJudWxsYWJsZSI6bnVsbCwicGF0dGVybiI6bnVsbCwicHJvcGVydGllcyI6eyJhZ2VudF9uYW1lIjp7ImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjpudWxsLCJkZWZzIjpudWxsLCJyZWYiOm51bGwsImFueU9mIjpudWxsLCJkZWZhdWx0IjpudWxsLCJkZXNjcmlwdGlvbiI6bnVsbCwiZW51bSI6WyJyZXNlYXJjaGVyIiwid3JpdGVyIl0sImV4YW1wbGUiOm51bGwsImZvcm1hdCI6bnVsbCwiaXRlbXMiOm51bGwsIm1heEl0ZW1zIjpudWxsLCJtYXhMZW5ndGgiOm51bGwsIm1heFByb3BlcnRpZXMiOm51bGwsIm1heGltdW0iOm51bGwsIm1pbkl0ZW1zIjpudWxsLCJtaW5MZW5ndGgiOm51bGwsIm1pblByb3BlcnRpZXMiOm51bGwsIm1pbmltdW0iOm51bGwsIm51bGxhYmxlIjpudWxsLCJwYXR0ZXJuIjpudWxsLCJwcm9wZXJ0aWVzIjpudWxsLCJwcm9wZXJ0eU9yZGVyaW5nIjpudWxsLCJyZXF1aXJlZCI6bnVsbCwidGl0bGUiOm51bGwsInR5cGUiOiJTVFJJTkcifX0sInByb3BlcnR5T3JkZXJpbmciOm51bGwsInJlcXVpcmVkIjpbImFnZW50X25hbWUiXSwidGl0bGUiOm51bGwsInR5cGUiOiJPQkpFQ1QifSwicGFyYW1ldGVyc0pzb25TY2hlbWEiOm51bGwsInJlc3BvbnNlIjpudWxsLCJyZXNwb25zZUpzb25TY2hlbWEiOm51bGwsImJlaGF2aW9yIjpudWxsfV0sImdvb2dsZU1hcHMiOm51bGwsImdvb2dsZVNlYXJjaCI6bnVsbCwiZ29vZ2xlU2VhcmNoUmV0cmlldmFsIjpudWxsLCJ1cmxDb250ZXh0IjpudWxsfV0sInRvb2xDb25maWciOm51bGwsImxhYmVscyI6bnVsbCwiY2FjaGVkQ29udGVudCI6bnVsbCwicmVzcG9uc2VNb2RhbGl0aWVzIjpudWxsLCJtZWRpYVJlc29sdXRpb24iOm51bGwsInNwZWVjaENvbmZpZyI6bnVsbCwiYXVkaW9UaW1lc3RhbXAiOm51bGwsImF1dG9tYXRpY0Z1bmN0aW9uQ2FsbGluZyI6bnVsbCwidGhpbmtpbmdDb25maWciOm51bGwsImltYWdlQ29uZmlnIjpudWxsLCJlbmFibGVFbmhhbmNlZENpdmljQW5zd2VycyI6bnVsbCwibW9kZWxBcm1vckNvbmZpZyI6bnVsbH0sImxpdmVfY29ubmVjdF9jb25maWciOnsiaHR0cE9wdGlvbnMiOm51bGwsImdlbmVyYXRpb25Db25maWciOm51bGwsInJlc3BvbnNlTW9kYWxpdGllcyI6bnVsbCwidGVtcGVyYXR1cmUiOm51bGwsInRvcFAiOm51bGwsInRvcEsiOm51bGwsIm1heE91dHB1dFRva2VucyI6bnVsbCwibWVkaWFSZXNvbHV0aW9uIjpudWxsLCJzZWVkIjpudWxsLCJzcGVlY2hDb25maWciOm51bGwsInRoaW5raW5nQ29uZmlnIjpudWxsLCJlbmFibGVBZmZlY3RpdmVEaWFsb2ciOm51bGwsInN5c3RlbUluc3RydWN0aW9uIjpudWxsLCJ0b29scyI6bnVsbCwic2Vzc2lvblJlc3VtcHRpb24iOm51bGwsImlucHV0QXVkaW9UcmFuc2NyaXB0aW9uIjp7fSwib3V0cHV0QXVkaW9UcmFuc2NyaXB0aW9uIjp7fSwicmVhbHRpbWVJbnB1dENvbmZpZyI6bnVsbCwiY29udGV4dFdpbmRvd0NvbXByZXNzaW9uIjpudWxsLCJwcm9hY3Rpdml0eSI6bnVsbCwiZXhwbGljaXRWYWRTaWduYWwiOm51bGx9LCJjYWNoZV9jb25maWciOm51bGwsImNhY2hlX21ldGFkYXRhIjpudWxsLCJjYWNoZWFibGVfY29udGVudHNfdG9rZW5fY291bnQiOm51bGwsInByZXZpb3VzX2ludGVyYWN0aW9uX2lkIjpudWxsfQ==" + } + ] + }, + "scheduleToCloseTimeout": "0s", + "scheduleToStartTimeout": "0s", + "startToCloseTimeout": "120s", + "heartbeatTimeout": "0s", + "workflowTaskCompletedEventId": "4", + "retryPolicy": { + "initialInterval": "1s", + "backoffCoefficient": 2.0, + "maximumInterval": "100s" + }, + "useWorkflowBuildId": true, + "priority": {} + }, + "userMetadata": { + "summary": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "ImNvb3JkaW5hdG9yIg==" + } + } + }, + { + "eventId": "6", + "eventTime": "2026-01-26T21:08:54.512495Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", + "taskId": "1103710", + "activityTaskStartedEventAttributes": { + "scheduledEventId": "5", + "identity": "69823@Tims-MacBook-Pro.local", + "requestId": "c9cbbe3d-3e1b-4631-b323-7e43ad2b3b11", + "attempt": 1, + "workerVersion": { + "buildId": "2f45d1deb022376ac8b03fdcb17f8e5c" + } + } + }, + { + "eventId": "7", + "eventTime": "2026-01-26T21:08:54.514972Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", + "taskId": "1103711", + "activityTaskCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "W3sibW9kZWxWZXJzaW9uIjpudWxsLCJjb250ZW50Ijp7InBhcnRzIjpbeyJtZWRpYVJlc29sdXRpb24iOm51bGwsImNvZGVFeGVjdXRpb25SZXN1bHQiOm51bGwsImV4ZWN1dGFibGVDb2RlIjpudWxsLCJmaWxlRGF0YSI6bnVsbCwiZnVuY3Rpb25DYWxsIjp7ImlkIjpudWxsLCJhcmdzIjp7ImFnZW50X25hbWUiOiJyZXNlYXJjaGVyIn0sIm5hbWUiOiJ0cmFuc2Zlcl90b19hZ2VudCIsInBhcnRpYWxBcmdzIjpudWxsLCJ3aWxsQ29udGludWUiOm51bGx9LCJmdW5jdGlvblJlc3BvbnNlIjpudWxsLCJpbmxpbmVEYXRhIjpudWxsLCJ0ZXh0IjpudWxsLCJ0aG91Z2h0IjpudWxsLCJ0aG91Z2h0U2lnbmF0dXJlIjpudWxsLCJ2aWRlb01ldGFkYXRhIjpudWxsfV0sInJvbGUiOiJtb2RlbCJ9LCJncm91bmRpbmdNZXRhZGF0YSI6bnVsbCwicGFydGlhbCI6bnVsbCwidHVybkNvbXBsZXRlIjpudWxsLCJmaW5pc2hSZWFzb24iOm51bGwsImVycm9yQ29kZSI6bnVsbCwiZXJyb3JNZXNzYWdlIjpudWxsLCJpbnRlcnJ1cHRlZCI6bnVsbCwiY3VzdG9tTWV0YWRhdGEiOm51bGwsInVzYWdlTWV0YWRhdGEiOm51bGwsImxpdmVTZXNzaW9uUmVzdW1wdGlvblVwZGF0ZSI6bnVsbCwiaW5wdXRUcmFuc2NyaXB0aW9uIjpudWxsLCJvdXRwdXRUcmFuc2NyaXB0aW9uIjpudWxsLCJhdmdMb2dwcm9icyI6bnVsbCwibG9ncHJvYnNSZXN1bHQiOm51bGwsImNhY2hlTWV0YWRhdGEiOm51bGwsImNpdGF0aW9uTWV0YWRhdGEiOm51bGwsImludGVyYWN0aW9uSWQiOm51bGx9XQ==" + } + ] + }, + "scheduledEventId": "5", + "startedEventId": "6", + "identity": "69823@Tims-MacBook-Pro.local" + } + }, + { + "eventId": "8", + "eventTime": "2026-01-26T21:08:54.514974Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1103712", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "adk-task-queue", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "9", + "eventTime": "2026-01-26T21:08:54.515493Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1103715", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "8", + "identity": "69823@Tims-MacBook-Pro.local", + "requestId": "11d5c461-029c-4bd8-aae4-2646fb9c9f2c", + "historySizeBytes": "5961", + "workerVersion": { + "buildId": "2f45d1deb022376ac8b03fdcb17f8e5c" + } + } + }, + { + "eventId": "10", + "eventTime": "2026-01-26T21:08:54.547494Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1103719", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "8", + "startedEventId": "9", + "identity": "69823@Tims-MacBook-Pro.local", + "workerVersion": { + "buildId": "2f45d1deb022376ac8b03fdcb17f8e5c" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "11", + "eventTime": "2026-01-26T21:08:54.547517Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", + "taskId": "1103720", + "activityTaskScheduledEventAttributes": { + "activityId": "2", + "activityType": { + "name": "invoke_model" + }, + "taskQueue": { + "name": "adk-task-queue", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "header": {}, + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJtb2RlbCI6InJlc2VhcmNoX21vZGVsIiwiY29udGVudHMiOlt7InBhcnRzIjpbeyJtZWRpYVJlc29sdXRpb24iOm51bGwsImNvZGVFeGVjdXRpb25SZXN1bHQiOm51bGwsImV4ZWN1dGFibGVDb2RlIjpudWxsLCJmaWxlRGF0YSI6bnVsbCwiZnVuY3Rpb25DYWxsIjpudWxsLCJmdW5jdGlvblJlc3BvbnNlIjpudWxsLCJpbmxpbmVEYXRhIjpudWxsLCJ0ZXh0IjoiV3JpdGUgYSBoYWlrdSBhYm91dCBSdW4gbXVsdC1hZ2VudCBmbG93LiBGaXJzdCByZXNlYXJjaCBpdCwgdGhlbiB3cml0ZSBpdC4iLCJ0aG91Z2h0IjpudWxsLCJ0aG91Z2h0U2lnbmF0dXJlIjpudWxsLCJ2aWRlb01ldGFkYXRhIjpudWxsfV0sInJvbGUiOiJ1c2VyIn0seyJwYXJ0cyI6W3sibWVkaWFSZXNvbHV0aW9uIjpudWxsLCJjb2RlRXhlY3V0aW9uUmVzdWx0IjpudWxsLCJleGVjdXRhYmxlQ29kZSI6bnVsbCwiZmlsZURhdGEiOm51bGwsImZ1bmN0aW9uQ2FsbCI6bnVsbCwiZnVuY3Rpb25SZXNwb25zZSI6bnVsbCwiaW5saW5lRGF0YSI6bnVsbCwidGV4dCI6IkZvciBjb250ZXh0OiIsInRob3VnaHQiOm51bGwsInRob3VnaHRTaWduYXR1cmUiOm51bGwsInZpZGVvTWV0YWRhdGEiOm51bGx9LHsibWVkaWFSZXNvbHV0aW9uIjpudWxsLCJjb2RlRXhlY3V0aW9uUmVzdWx0IjpudWxsLCJleGVjdXRhYmxlQ29kZSI6bnVsbCwiZmlsZURhdGEiOm51bGwsImZ1bmN0aW9uQ2FsbCI6bnVsbCwiZnVuY3Rpb25SZXNwb25zZSI6bnVsbCwiaW5saW5lRGF0YSI6bnVsbCwidGV4dCI6Iltjb29yZGluYXRvcl0gY2FsbGVkIHRvb2wgYHRyYW5zZmVyX3RvX2FnZW50YCB3aXRoIHBhcmFtZXRlcnM6IHsnYWdlbnRfbmFtZSc6ICdyZXNlYXJjaGVyJ30iLCJ0aG91Z2h0IjpudWxsLCJ0aG91Z2h0U2lnbmF0dXJlIjpudWxsLCJ2aWRlb01ldGFkYXRhIjpudWxsfV0sInJvbGUiOiJ1c2VyIn0seyJwYXJ0cyI6W3sibWVkaWFSZXNvbHV0aW9uIjpudWxsLCJjb2RlRXhlY3V0aW9uUmVzdWx0IjpudWxsLCJleGVjdXRhYmxlQ29kZSI6bnVsbCwiZmlsZURhdGEiOm51bGwsImZ1bmN0aW9uQ2FsbCI6bnVsbCwiZnVuY3Rpb25SZXNwb25zZSI6bnVsbCwiaW5saW5lRGF0YSI6bnVsbCwidGV4dCI6IkZvciBjb250ZXh0OiIsInRob3VnaHQiOm51bGwsInRob3VnaHRTaWduYXR1cmUiOm51bGwsInZpZGVvTWV0YWRhdGEiOm51bGx9LHsibWVkaWFSZXNvbHV0aW9uIjpudWxsLCJjb2RlRXhlY3V0aW9uUmVzdWx0IjpudWxsLCJleGVjdXRhYmxlQ29kZSI6bnVsbCwiZmlsZURhdGEiOm51bGwsImZ1bmN0aW9uQ2FsbCI6bnVsbCwiZnVuY3Rpb25SZXNwb25zZSI6bnVsbCwiaW5saW5lRGF0YSI6bnVsbCwidGV4dCI6Iltjb29yZGluYXRvcl0gYHRyYW5zZmVyX3RvX2FnZW50YCB0b29sIHJldHVybmVkIHJlc3VsdDogeydyZXN1bHQnOiBOb25lfSIsInRob3VnaHQiOm51bGwsInRob3VnaHRTaWduYXR1cmUiOm51bGwsInZpZGVvTWV0YWRhdGEiOm51bGx9XSwicm9sZSI6InVzZXIifV0sImNvbmZpZyI6eyJodHRwT3B0aW9ucyI6bnVsbCwic2hvdWxkUmV0dXJuSHR0cFJlc3BvbnNlIjpudWxsLCJzeXN0ZW1JbnN0cnVjdGlvbiI6IllvdSBhcmUgYSByZXNlYXJjaGVyLiBGaW5kIGluZm9ybWF0aW9uIGFib3V0IHRoZSB0b3BpYy5cblxuWW91IGFyZSBhbiBhZ2VudC4gWW91ciBpbnRlcm5hbCBuYW1lIGlzIFwicmVzZWFyY2hlclwiLlxuXG5cbllvdSBoYXZlIGEgbGlzdCBvZiBvdGhlciBhZ2VudHMgdG8gdHJhbnNmZXIgdG86XG5cblxuQWdlbnQgbmFtZTogY29vcmRpbmF0b3JcbkFnZW50IGRlc2NyaXB0aW9uOiBcblxuXG5BZ2VudCBuYW1lOiB3cml0ZXJcbkFnZW50IGRlc2NyaXB0aW9uOiBcblxuXG5JZiB5b3UgYXJlIHRoZSBiZXN0IHRvIGFuc3dlciB0aGUgcXVlc3Rpb24gYWNjb3JkaW5nIHRvIHlvdXIgZGVzY3JpcHRpb24sXG55b3UgY2FuIGFuc3dlciBpdC5cblxuSWYgYW5vdGhlciBhZ2VudCBpcyBiZXR0ZXIgZm9yIGFuc3dlcmluZyB0aGUgcXVlc3Rpb24gYWNjb3JkaW5nIHRvIGl0c1xuZGVzY3JpcHRpb24sIGNhbGwgYHRyYW5zZmVyX3RvX2FnZW50YCBmdW5jdGlvbiB0byB0cmFuc2ZlciB0aGUgcXVlc3Rpb24gdG8gdGhhdFxuYWdlbnQuIFdoZW4gdHJhbnNmZXJyaW5nLCBkbyBub3QgZ2VuZXJhdGUgYW55IHRleHQgb3RoZXIgdGhhbiB0aGUgZnVuY3Rpb25cbmNhbGwuXG5cbioqTk9URSoqOiB0aGUgb25seSBhdmFpbGFibGUgYWdlbnRzIGZvciBgdHJhbnNmZXJfdG9fYWdlbnRgIGZ1bmN0aW9uIGFyZVxuYGNvb3JkaW5hdG9yYCwgYHdyaXRlcmAuXG5cbklmIG5laXRoZXIgeW91IG5vciB0aGUgb3RoZXIgYWdlbnRzIGFyZSBiZXN0IGZvciB0aGUgcXVlc3Rpb24sIHRyYW5zZmVyIHRvIHlvdXIgcGFyZW50IGFnZW50IGNvb3JkaW5hdG9yLlxuIiwidGVtcGVyYXR1cmUiOm51bGwsInRvcFAiOm51bGwsInRvcEsiOm51bGwsImNhbmRpZGF0ZUNvdW50IjpudWxsLCJtYXhPdXRwdXRUb2tlbnMiOm51bGwsInN0b3BTZXF1ZW5jZXMiOm51bGwsInJlc3BvbnNlTG9ncHJvYnMiOm51bGwsImxvZ3Byb2JzIjpudWxsLCJwcmVzZW5jZVBlbmFsdHkiOm51bGwsImZyZXF1ZW5jeVBlbmFsdHkiOm51bGwsInNlZWQiOm51bGwsInJlc3BvbnNlTWltZVR5cGUiOm51bGwsInJlc3BvbnNlU2NoZW1hIjpudWxsLCJyZXNwb25zZUpzb25TY2hlbWEiOm51bGwsInJvdXRpbmdDb25maWciOm51bGwsIm1vZGVsU2VsZWN0aW9uQ29uZmlnIjpudWxsLCJzYWZldHlTZXR0aW5ncyI6bnVsbCwidG9vbHMiOlt7InJldHJpZXZhbCI6bnVsbCwiY29tcHV0ZXJVc2UiOm51bGwsImZpbGVTZWFyY2giOm51bGwsImNvZGVFeGVjdXRpb24iOm51bGwsImVudGVycHJpc2VXZWJTZWFyY2giOm51bGwsImZ1bmN0aW9uRGVjbGFyYXRpb25zIjpbeyJkZXNjcmlwdGlvbiI6IlRyYW5zZmVyIHRoZSBxdWVzdGlvbiB0byBhbm90aGVyIGFnZW50LlxuXG5UaGlzIHRvb2wgaGFuZHMgb2ZmIGNvbnRyb2wgdG8gYW5vdGhlciBhZ2VudCB3aGVuIGl0J3MgbW9yZSBzdWl0YWJsZSB0b1xuYW5zd2VyIHRoZSB1c2VyJ3MgcXVlc3Rpb24gYWNjb3JkaW5nIHRvIHRoZSBhZ2VudCdzIGRlc2NyaXB0aW9uLlxuXG5Ob3RlOlxuICBGb3IgbW9zdCB1c2UgY2FzZXMsIHlvdSBzaG91bGQgdXNlIFRyYW5zZmVyVG9BZ2VudFRvb2wgaW5zdGVhZCBvZiB0aGlzXG4gIGZ1bmN0aW9uIGRpcmVjdGx5LiBUcmFuc2ZlclRvQWdlbnRUb29sIHByb3ZpZGVzIGFkZGl0aW9uYWwgZW51bSBjb25zdHJhaW50c1xuICB0aGF0IHByZXZlbnQgTExNcyBmcm9tIGhhbGx1Y2luYXRpbmcgaW52YWxpZCBhZ2VudCBuYW1lcy5cblxuQXJnczpcbiAgYWdlbnRfbmFtZTogdGhlIGFnZW50IG5hbWUgdG8gdHJhbnNmZXIgdG8uXG4iLCJuYW1lIjoidHJhbnNmZXJfdG9fYWdlbnQiLCJwYXJhbWV0ZXJzIjp7ImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjpudWxsLCJkZWZzIjpudWxsLCJyZWYiOm51bGwsImFueU9mIjpudWxsLCJkZWZhdWx0IjpudWxsLCJkZXNjcmlwdGlvbiI6bnVsbCwiZW51bSI6bnVsbCwiZXhhbXBsZSI6bnVsbCwiZm9ybWF0IjpudWxsLCJpdGVtcyI6bnVsbCwibWF4SXRlbXMiOm51bGwsIm1heExlbmd0aCI6bnVsbCwibWF4UHJvcGVydGllcyI6bnVsbCwibWF4aW11bSI6bnVsbCwibWluSXRlbXMiOm51bGwsIm1pbkxlbmd0aCI6bnVsbCwibWluUHJvcGVydGllcyI6bnVsbCwibWluaW11bSI6bnVsbCwibnVsbGFibGUiOm51bGwsInBhdHRlcm4iOm51bGwsInByb3BlcnRpZXMiOnsiYWdlbnRfbmFtZSI6eyJhZGRpdGlvbmFsUHJvcGVydGllcyI6bnVsbCwiZGVmcyI6bnVsbCwicmVmIjpudWxsLCJhbnlPZiI6bnVsbCwiZGVmYXVsdCI6bnVsbCwiZGVzY3JpcHRpb24iOm51bGwsImVudW0iOlsiY29vcmRpbmF0b3IiLCJ3cml0ZXIiXSwiZXhhbXBsZSI6bnVsbCwiZm9ybWF0IjpudWxsLCJpdGVtcyI6bnVsbCwibWF4SXRlbXMiOm51bGwsIm1heExlbmd0aCI6bnVsbCwibWF4UHJvcGVydGllcyI6bnVsbCwibWF4aW11bSI6bnVsbCwibWluSXRlbXMiOm51bGwsIm1pbkxlbmd0aCI6bnVsbCwibWluUHJvcGVydGllcyI6bnVsbCwibWluaW11bSI6bnVsbCwibnVsbGFibGUiOm51bGwsInBhdHRlcm4iOm51bGwsInByb3BlcnRpZXMiOm51bGwsInByb3BlcnR5T3JkZXJpbmciOm51bGwsInJlcXVpcmVkIjpudWxsLCJ0aXRsZSI6bnVsbCwidHlwZSI6IlNUUklORyJ9fSwicHJvcGVydHlPcmRlcmluZyI6bnVsbCwicmVxdWlyZWQiOlsiYWdlbnRfbmFtZSJdLCJ0aXRsZSI6bnVsbCwidHlwZSI6Ik9CSkVDVCJ9LCJwYXJhbWV0ZXJzSnNvblNjaGVtYSI6bnVsbCwicmVzcG9uc2UiOm51bGwsInJlc3BvbnNlSnNvblNjaGVtYSI6bnVsbCwiYmVoYXZpb3IiOm51bGx9XSwiZ29vZ2xlTWFwcyI6bnVsbCwiZ29vZ2xlU2VhcmNoIjpudWxsLCJnb29nbGVTZWFyY2hSZXRyaWV2YWwiOm51bGwsInVybENvbnRleHQiOm51bGx9XSwidG9vbENvbmZpZyI6bnVsbCwibGFiZWxzIjpudWxsLCJjYWNoZWRDb250ZW50IjpudWxsLCJyZXNwb25zZU1vZGFsaXRpZXMiOm51bGwsIm1lZGlhUmVzb2x1dGlvbiI6bnVsbCwic3BlZWNoQ29uZmlnIjpudWxsLCJhdWRpb1RpbWVzdGFtcCI6bnVsbCwiYXV0b21hdGljRnVuY3Rpb25DYWxsaW5nIjpudWxsLCJ0aGlua2luZ0NvbmZpZyI6bnVsbCwiaW1hZ2VDb25maWciOm51bGwsImVuYWJsZUVuaGFuY2VkQ2l2aWNBbnN3ZXJzIjpudWxsLCJtb2RlbEFybW9yQ29uZmlnIjpudWxsfSwibGl2ZV9jb25uZWN0X2NvbmZpZyI6eyJodHRwT3B0aW9ucyI6bnVsbCwiZ2VuZXJhdGlvbkNvbmZpZyI6bnVsbCwicmVzcG9uc2VNb2RhbGl0aWVzIjpudWxsLCJ0ZW1wZXJhdHVyZSI6bnVsbCwidG9wUCI6bnVsbCwidG9wSyI6bnVsbCwibWF4T3V0cHV0VG9rZW5zIjpudWxsLCJtZWRpYVJlc29sdXRpb24iOm51bGwsInNlZWQiOm51bGwsInNwZWVjaENvbmZpZyI6bnVsbCwidGhpbmtpbmdDb25maWciOm51bGwsImVuYWJsZUFmZmVjdGl2ZURpYWxvZyI6bnVsbCwic3lzdGVtSW5zdHJ1Y3Rpb24iOm51bGwsInRvb2xzIjpudWxsLCJzZXNzaW9uUmVzdW1wdGlvbiI6bnVsbCwiaW5wdXRBdWRpb1RyYW5zY3JpcHRpb24iOnt9LCJvdXRwdXRBdWRpb1RyYW5zY3JpcHRpb24iOnt9LCJyZWFsdGltZUlucHV0Q29uZmlnIjpudWxsLCJjb250ZXh0V2luZG93Q29tcHJlc3Npb24iOm51bGwsInByb2FjdGl2aXR5IjpudWxsLCJleHBsaWNpdFZhZFNpZ25hbCI6bnVsbH0sImNhY2hlX2NvbmZpZyI6bnVsbCwiY2FjaGVfbWV0YWRhdGEiOm51bGwsImNhY2hlYWJsZV9jb250ZW50c190b2tlbl9jb3VudCI6bnVsbCwicHJldmlvdXNfaW50ZXJhY3Rpb25faWQiOm51bGx9" + } + ] + }, + "scheduleToCloseTimeout": "0s", + "scheduleToStartTimeout": "0s", + "startToCloseTimeout": "120s", + "heartbeatTimeout": "0s", + "workflowTaskCompletedEventId": "10", + "retryPolicy": { + "initialInterval": "1s", + "backoffCoefficient": 2.0, + "maximumInterval": "100s" + }, + "useWorkflowBuildId": true, + "priority": {} + }, + "userMetadata": { + "summary": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "InJlc2VhcmNoZXIi" + } + } + }, + { + "eventId": "12", + "eventTime": "2026-01-26T21:08:54.548338Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", + "taskId": "1103725", + "activityTaskStartedEventAttributes": { + "scheduledEventId": "11", + "identity": "69823@Tims-MacBook-Pro.local", + "requestId": "99a0aa4a-e3b2-4e6a-bd2a-85ab2814b094", + "attempt": 1, + "workerVersion": { + "buildId": "2f45d1deb022376ac8b03fdcb17f8e5c" + } + } + }, + { + "eventId": "13", + "eventTime": "2026-01-26T21:08:54.550067Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", + "taskId": "1103726", + "activityTaskCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "W3sibW9kZWxWZXJzaW9uIjpudWxsLCJjb250ZW50Ijp7InBhcnRzIjpbeyJtZWRpYVJlc29sdXRpb24iOm51bGwsImNvZGVFeGVjdXRpb25SZXN1bHQiOm51bGwsImV4ZWN1dGFibGVDb2RlIjpudWxsLCJmaWxlRGF0YSI6bnVsbCwiZnVuY3Rpb25DYWxsIjp7ImlkIjpudWxsLCJhcmdzIjp7ImFnZW50X25hbWUiOiJ3cml0ZXIifSwibmFtZSI6InRyYW5zZmVyX3RvX2FnZW50IiwicGFydGlhbEFyZ3MiOm51bGwsIndpbGxDb250aW51ZSI6bnVsbH0sImZ1bmN0aW9uUmVzcG9uc2UiOm51bGwsImlubGluZURhdGEiOm51bGwsInRleHQiOm51bGwsInRob3VnaHQiOm51bGwsInRob3VnaHRTaWduYXR1cmUiOm51bGwsInZpZGVvTWV0YWRhdGEiOm51bGx9XSwicm9sZSI6Im1vZGVsIn0sImdyb3VuZGluZ01ldGFkYXRhIjpudWxsLCJwYXJ0aWFsIjpudWxsLCJ0dXJuQ29tcGxldGUiOm51bGwsImZpbmlzaFJlYXNvbiI6bnVsbCwiZXJyb3JDb2RlIjpudWxsLCJlcnJvck1lc3NhZ2UiOm51bGwsImludGVycnVwdGVkIjpudWxsLCJjdXN0b21NZXRhZGF0YSI6bnVsbCwidXNhZ2VNZXRhZGF0YSI6bnVsbCwibGl2ZVNlc3Npb25SZXN1bXB0aW9uVXBkYXRlIjpudWxsLCJpbnB1dFRyYW5zY3JpcHRpb24iOm51bGwsIm91dHB1dFRyYW5zY3JpcHRpb24iOm51bGwsImF2Z0xvZ3Byb2JzIjpudWxsLCJsb2dwcm9ic1Jlc3VsdCI6bnVsbCwiY2FjaGVNZXRhZGF0YSI6bnVsbCwiY2l0YXRpb25NZXRhZGF0YSI6bnVsbCwiaW50ZXJhY3Rpb25JZCI6bnVsbH1d" + } + ] + }, + "scheduledEventId": "11", + "startedEventId": "12", + "identity": "69823@Tims-MacBook-Pro.local" + } + }, + { + "eventId": "14", + "eventTime": "2026-01-26T21:08:54.550070Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1103727", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "adk-task-queue", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "15", + "eventTime": "2026-01-26T21:08:54.550644Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1103730", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "14", + "identity": "69823@Tims-MacBook-Pro.local", + "requestId": "3bfc29c0-857f-460b-aafa-19e6e1667dad", + "historySizeBytes": "12724", + "workerVersion": { + "buildId": "2f45d1deb022376ac8b03fdcb17f8e5c" + } + } + }, + { + "eventId": "16", + "eventTime": "2026-01-26T21:08:54.582213Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1103734", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "14", + "startedEventId": "15", + "identity": "69823@Tims-MacBook-Pro.local", + "workerVersion": { + "buildId": "2f45d1deb022376ac8b03fdcb17f8e5c" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "17", + "eventTime": "2026-01-26T21:08:54.582242Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", + "taskId": "1103735", + "activityTaskScheduledEventAttributes": { + "activityId": "3", + "activityType": { + "name": "invoke_model" + }, + "taskQueue": { + "name": "adk-task-queue", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "header": {}, + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJtb2RlbCI6InJlc2VhcmNoX21vZGVsIiwiY29udGVudHMiOlt7InBhcnRzIjpbeyJtZWRpYVJlc29sdXRpb24iOm51bGwsImNvZGVFeGVjdXRpb25SZXN1bHQiOm51bGwsImV4ZWN1dGFibGVDb2RlIjpudWxsLCJmaWxlRGF0YSI6bnVsbCwiZnVuY3Rpb25DYWxsIjpudWxsLCJmdW5jdGlvblJlc3BvbnNlIjpudWxsLCJpbmxpbmVEYXRhIjpudWxsLCJ0ZXh0IjoiV3JpdGUgYSBoYWlrdSBhYm91dCBSdW4gbXVsdC1hZ2VudCBmbG93LiBGaXJzdCByZXNlYXJjaCBpdCwgdGhlbiB3cml0ZSBpdC4iLCJ0aG91Z2h0IjpudWxsLCJ0aG91Z2h0U2lnbmF0dXJlIjpudWxsLCJ2aWRlb01ldGFkYXRhIjpudWxsfV0sInJvbGUiOiJ1c2VyIn0seyJwYXJ0cyI6W3sibWVkaWFSZXNvbHV0aW9uIjpudWxsLCJjb2RlRXhlY3V0aW9uUmVzdWx0IjpudWxsLCJleGVjdXRhYmxlQ29kZSI6bnVsbCwiZmlsZURhdGEiOm51bGwsImZ1bmN0aW9uQ2FsbCI6bnVsbCwiZnVuY3Rpb25SZXNwb25zZSI6bnVsbCwiaW5saW5lRGF0YSI6bnVsbCwidGV4dCI6IkZvciBjb250ZXh0OiIsInRob3VnaHQiOm51bGwsInRob3VnaHRTaWduYXR1cmUiOm51bGwsInZpZGVvTWV0YWRhdGEiOm51bGx9LHsibWVkaWFSZXNvbHV0aW9uIjpudWxsLCJjb2RlRXhlY3V0aW9uUmVzdWx0IjpudWxsLCJleGVjdXRhYmxlQ29kZSI6bnVsbCwiZmlsZURhdGEiOm51bGwsImZ1bmN0aW9uQ2FsbCI6bnVsbCwiZnVuY3Rpb25SZXNwb25zZSI6bnVsbCwiaW5saW5lRGF0YSI6bnVsbCwidGV4dCI6Iltjb29yZGluYXRvcl0gY2FsbGVkIHRvb2wgYHRyYW5zZmVyX3RvX2FnZW50YCB3aXRoIHBhcmFtZXRlcnM6IHsnYWdlbnRfbmFtZSc6ICdyZXNlYXJjaGVyJ30iLCJ0aG91Z2h0IjpudWxsLCJ0aG91Z2h0U2lnbmF0dXJlIjpudWxsLCJ2aWRlb01ldGFkYXRhIjpudWxsfV0sInJvbGUiOiJ1c2VyIn0seyJwYXJ0cyI6W3sibWVkaWFSZXNvbHV0aW9uIjpudWxsLCJjb2RlRXhlY3V0aW9uUmVzdWx0IjpudWxsLCJleGVjdXRhYmxlQ29kZSI6bnVsbCwiZmlsZURhdGEiOm51bGwsImZ1bmN0aW9uQ2FsbCI6bnVsbCwiZnVuY3Rpb25SZXNwb25zZSI6bnVsbCwiaW5saW5lRGF0YSI6bnVsbCwidGV4dCI6IkZvciBjb250ZXh0OiIsInRob3VnaHQiOm51bGwsInRob3VnaHRTaWduYXR1cmUiOm51bGwsInZpZGVvTWV0YWRhdGEiOm51bGx9LHsibWVkaWFSZXNvbHV0aW9uIjpudWxsLCJjb2RlRXhlY3V0aW9uUmVzdWx0IjpudWxsLCJleGVjdXRhYmxlQ29kZSI6bnVsbCwiZmlsZURhdGEiOm51bGwsImZ1bmN0aW9uQ2FsbCI6bnVsbCwiZnVuY3Rpb25SZXNwb25zZSI6bnVsbCwiaW5saW5lRGF0YSI6bnVsbCwidGV4dCI6Iltjb29yZGluYXRvcl0gYHRyYW5zZmVyX3RvX2FnZW50YCB0b29sIHJldHVybmVkIHJlc3VsdDogeydyZXN1bHQnOiBOb25lfSIsInRob3VnaHQiOm51bGwsInRob3VnaHRTaWduYXR1cmUiOm51bGwsInZpZGVvTWV0YWRhdGEiOm51bGx9XSwicm9sZSI6InVzZXIifSx7InBhcnRzIjpbeyJtZWRpYVJlc29sdXRpb24iOm51bGwsImNvZGVFeGVjdXRpb25SZXN1bHQiOm51bGwsImV4ZWN1dGFibGVDb2RlIjpudWxsLCJmaWxlRGF0YSI6bnVsbCwiZnVuY3Rpb25DYWxsIjpudWxsLCJmdW5jdGlvblJlc3BvbnNlIjpudWxsLCJpbmxpbmVEYXRhIjpudWxsLCJ0ZXh0IjoiRm9yIGNvbnRleHQ6IiwidGhvdWdodCI6bnVsbCwidGhvdWdodFNpZ25hdHVyZSI6bnVsbCwidmlkZW9NZXRhZGF0YSI6bnVsbH0seyJtZWRpYVJlc29sdXRpb24iOm51bGwsImNvZGVFeGVjdXRpb25SZXN1bHQiOm51bGwsImV4ZWN1dGFibGVDb2RlIjpudWxsLCJmaWxlRGF0YSI6bnVsbCwiZnVuY3Rpb25DYWxsIjpudWxsLCJmdW5jdGlvblJlc3BvbnNlIjpudWxsLCJpbmxpbmVEYXRhIjpudWxsLCJ0ZXh0IjoiW3Jlc2VhcmNoZXJdIGNhbGxlZCB0b29sIGB0cmFuc2Zlcl90b19hZ2VudGAgd2l0aCBwYXJhbWV0ZXJzOiB7J2FnZW50X25hbWUnOiAnd3JpdGVyJ30iLCJ0aG91Z2h0IjpudWxsLCJ0aG91Z2h0U2lnbmF0dXJlIjpudWxsLCJ2aWRlb01ldGFkYXRhIjpudWxsfV0sInJvbGUiOiJ1c2VyIn0seyJwYXJ0cyI6W3sibWVkaWFSZXNvbHV0aW9uIjpudWxsLCJjb2RlRXhlY3V0aW9uUmVzdWx0IjpudWxsLCJleGVjdXRhYmxlQ29kZSI6bnVsbCwiZmlsZURhdGEiOm51bGwsImZ1bmN0aW9uQ2FsbCI6bnVsbCwiZnVuY3Rpb25SZXNwb25zZSI6bnVsbCwiaW5saW5lRGF0YSI6bnVsbCwidGV4dCI6IkZvciBjb250ZXh0OiIsInRob3VnaHQiOm51bGwsInRob3VnaHRTaWduYXR1cmUiOm51bGwsInZpZGVvTWV0YWRhdGEiOm51bGx9LHsibWVkaWFSZXNvbHV0aW9uIjpudWxsLCJjb2RlRXhlY3V0aW9uUmVzdWx0IjpudWxsLCJleGVjdXRhYmxlQ29kZSI6bnVsbCwiZmlsZURhdGEiOm51bGwsImZ1bmN0aW9uQ2FsbCI6bnVsbCwiZnVuY3Rpb25SZXNwb25zZSI6bnVsbCwiaW5saW5lRGF0YSI6bnVsbCwidGV4dCI6IltyZXNlYXJjaGVyXSBgdHJhbnNmZXJfdG9fYWdlbnRgIHRvb2wgcmV0dXJuZWQgcmVzdWx0OiB7J3Jlc3VsdCc6IE5vbmV9IiwidGhvdWdodCI6bnVsbCwidGhvdWdodFNpZ25hdHVyZSI6bnVsbCwidmlkZW9NZXRhZGF0YSI6bnVsbH1dLCJyb2xlIjoidXNlciJ9XSwiY29uZmlnIjp7Imh0dHBPcHRpb25zIjpudWxsLCJzaG91bGRSZXR1cm5IdHRwUmVzcG9uc2UiOm51bGwsInN5c3RlbUluc3RydWN0aW9uIjoiWW91IGFyZSBhIHBvZXQuIFdyaXRlIGEgaGFpa3UgYmFzZWQgb24gdGhlIHJlc2VhcmNoLlxuXG5Zb3UgYXJlIGFuIGFnZW50LiBZb3VyIGludGVybmFsIG5hbWUgaXMgXCJ3cml0ZXJcIi5cblxuXG5Zb3UgaGF2ZSBhIGxpc3Qgb2Ygb3RoZXIgYWdlbnRzIHRvIHRyYW5zZmVyIHRvOlxuXG5cbkFnZW50IG5hbWU6IGNvb3JkaW5hdG9yXG5BZ2VudCBkZXNjcmlwdGlvbjogXG5cblxuQWdlbnQgbmFtZTogcmVzZWFyY2hlclxuQWdlbnQgZGVzY3JpcHRpb246IFxuXG5cbklmIHlvdSBhcmUgdGhlIGJlc3QgdG8gYW5zd2VyIHRoZSBxdWVzdGlvbiBhY2NvcmRpbmcgdG8geW91ciBkZXNjcmlwdGlvbixcbnlvdSBjYW4gYW5zd2VyIGl0LlxuXG5JZiBhbm90aGVyIGFnZW50IGlzIGJldHRlciBmb3IgYW5zd2VyaW5nIHRoZSBxdWVzdGlvbiBhY2NvcmRpbmcgdG8gaXRzXG5kZXNjcmlwdGlvbiwgY2FsbCBgdHJhbnNmZXJfdG9fYWdlbnRgIGZ1bmN0aW9uIHRvIHRyYW5zZmVyIHRoZSBxdWVzdGlvbiB0byB0aGF0XG5hZ2VudC4gV2hlbiB0cmFuc2ZlcnJpbmcsIGRvIG5vdCBnZW5lcmF0ZSBhbnkgdGV4dCBvdGhlciB0aGFuIHRoZSBmdW5jdGlvblxuY2FsbC5cblxuKipOT1RFKio6IHRoZSBvbmx5IGF2YWlsYWJsZSBhZ2VudHMgZm9yIGB0cmFuc2Zlcl90b19hZ2VudGAgZnVuY3Rpb24gYXJlXG5gY29vcmRpbmF0b3JgLCBgcmVzZWFyY2hlcmAuXG5cbklmIG5laXRoZXIgeW91IG5vciB0aGUgb3RoZXIgYWdlbnRzIGFyZSBiZXN0IGZvciB0aGUgcXVlc3Rpb24sIHRyYW5zZmVyIHRvIHlvdXIgcGFyZW50IGFnZW50IGNvb3JkaW5hdG9yLlxuIiwidGVtcGVyYXR1cmUiOm51bGwsInRvcFAiOm51bGwsInRvcEsiOm51bGwsImNhbmRpZGF0ZUNvdW50IjpudWxsLCJtYXhPdXRwdXRUb2tlbnMiOm51bGwsInN0b3BTZXF1ZW5jZXMiOm51bGwsInJlc3BvbnNlTG9ncHJvYnMiOm51bGwsImxvZ3Byb2JzIjpudWxsLCJwcmVzZW5jZVBlbmFsdHkiOm51bGwsImZyZXF1ZW5jeVBlbmFsdHkiOm51bGwsInNlZWQiOm51bGwsInJlc3BvbnNlTWltZVR5cGUiOm51bGwsInJlc3BvbnNlU2NoZW1hIjpudWxsLCJyZXNwb25zZUpzb25TY2hlbWEiOm51bGwsInJvdXRpbmdDb25maWciOm51bGwsIm1vZGVsU2VsZWN0aW9uQ29uZmlnIjpudWxsLCJzYWZldHlTZXR0aW5ncyI6bnVsbCwidG9vbHMiOlt7InJldHJpZXZhbCI6bnVsbCwiY29tcHV0ZXJVc2UiOm51bGwsImZpbGVTZWFyY2giOm51bGwsImNvZGVFeGVjdXRpb24iOm51bGwsImVudGVycHJpc2VXZWJTZWFyY2giOm51bGwsImZ1bmN0aW9uRGVjbGFyYXRpb25zIjpbeyJkZXNjcmlwdGlvbiI6IlRyYW5zZmVyIHRoZSBxdWVzdGlvbiB0byBhbm90aGVyIGFnZW50LlxuXG5UaGlzIHRvb2wgaGFuZHMgb2ZmIGNvbnRyb2wgdG8gYW5vdGhlciBhZ2VudCB3aGVuIGl0J3MgbW9yZSBzdWl0YWJsZSB0b1xuYW5zd2VyIHRoZSB1c2VyJ3MgcXVlc3Rpb24gYWNjb3JkaW5nIHRvIHRoZSBhZ2VudCdzIGRlc2NyaXB0aW9uLlxuXG5Ob3RlOlxuICBGb3IgbW9zdCB1c2UgY2FzZXMsIHlvdSBzaG91bGQgdXNlIFRyYW5zZmVyVG9BZ2VudFRvb2wgaW5zdGVhZCBvZiB0aGlzXG4gIGZ1bmN0aW9uIGRpcmVjdGx5LiBUcmFuc2ZlclRvQWdlbnRUb29sIHByb3ZpZGVzIGFkZGl0aW9uYWwgZW51bSBjb25zdHJhaW50c1xuICB0aGF0IHByZXZlbnQgTExNcyBmcm9tIGhhbGx1Y2luYXRpbmcgaW52YWxpZCBhZ2VudCBuYW1lcy5cblxuQXJnczpcbiAgYWdlbnRfbmFtZTogdGhlIGFnZW50IG5hbWUgdG8gdHJhbnNmZXIgdG8uXG4iLCJuYW1lIjoidHJhbnNmZXJfdG9fYWdlbnQiLCJwYXJhbWV0ZXJzIjp7ImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjpudWxsLCJkZWZzIjpudWxsLCJyZWYiOm51bGwsImFueU9mIjpudWxsLCJkZWZhdWx0IjpudWxsLCJkZXNjcmlwdGlvbiI6bnVsbCwiZW51bSI6bnVsbCwiZXhhbXBsZSI6bnVsbCwiZm9ybWF0IjpudWxsLCJpdGVtcyI6bnVsbCwibWF4SXRlbXMiOm51bGwsIm1heExlbmd0aCI6bnVsbCwibWF4UHJvcGVydGllcyI6bnVsbCwibWF4aW11bSI6bnVsbCwibWluSXRlbXMiOm51bGwsIm1pbkxlbmd0aCI6bnVsbCwibWluUHJvcGVydGllcyI6bnVsbCwibWluaW11bSI6bnVsbCwibnVsbGFibGUiOm51bGwsInBhdHRlcm4iOm51bGwsInByb3BlcnRpZXMiOnsiYWdlbnRfbmFtZSI6eyJhZGRpdGlvbmFsUHJvcGVydGllcyI6bnVsbCwiZGVmcyI6bnVsbCwicmVmIjpudWxsLCJhbnlPZiI6bnVsbCwiZGVmYXVsdCI6bnVsbCwiZGVzY3JpcHRpb24iOm51bGwsImVudW0iOlsiY29vcmRpbmF0b3IiLCJyZXNlYXJjaGVyIl0sImV4YW1wbGUiOm51bGwsImZvcm1hdCI6bnVsbCwiaXRlbXMiOm51bGwsIm1heEl0ZW1zIjpudWxsLCJtYXhMZW5ndGgiOm51bGwsIm1heFByb3BlcnRpZXMiOm51bGwsIm1heGltdW0iOm51bGwsIm1pbkl0ZW1zIjpudWxsLCJtaW5MZW5ndGgiOm51bGwsIm1pblByb3BlcnRpZXMiOm51bGwsIm1pbmltdW0iOm51bGwsIm51bGxhYmxlIjpudWxsLCJwYXR0ZXJuIjpudWxsLCJwcm9wZXJ0aWVzIjpudWxsLCJwcm9wZXJ0eU9yZGVyaW5nIjpudWxsLCJyZXF1aXJlZCI6bnVsbCwidGl0bGUiOm51bGwsInR5cGUiOiJTVFJJTkcifX0sInByb3BlcnR5T3JkZXJpbmciOm51bGwsInJlcXVpcmVkIjpbImFnZW50X25hbWUiXSwidGl0bGUiOm51bGwsInR5cGUiOiJPQkpFQ1QifSwicGFyYW1ldGVyc0pzb25TY2hlbWEiOm51bGwsInJlc3BvbnNlIjpudWxsLCJyZXNwb25zZUpzb25TY2hlbWEiOm51bGwsImJlaGF2aW9yIjpudWxsfV0sImdvb2dsZU1hcHMiOm51bGwsImdvb2dsZVNlYXJjaCI6bnVsbCwiZ29vZ2xlU2VhcmNoUmV0cmlldmFsIjpudWxsLCJ1cmxDb250ZXh0IjpudWxsfV0sInRvb2xDb25maWciOm51bGwsImxhYmVscyI6bnVsbCwiY2FjaGVkQ29udGVudCI6bnVsbCwicmVzcG9uc2VNb2RhbGl0aWVzIjpudWxsLCJtZWRpYVJlc29sdXRpb24iOm51bGwsInNwZWVjaENvbmZpZyI6bnVsbCwiYXVkaW9UaW1lc3RhbXAiOm51bGwsImF1dG9tYXRpY0Z1bmN0aW9uQ2FsbGluZyI6bnVsbCwidGhpbmtpbmdDb25maWciOm51bGwsImltYWdlQ29uZmlnIjpudWxsLCJlbmFibGVFbmhhbmNlZENpdmljQW5zd2VycyI6bnVsbCwibW9kZWxBcm1vckNvbmZpZyI6bnVsbH0sImxpdmVfY29ubmVjdF9jb25maWciOnsiaHR0cE9wdGlvbnMiOm51bGwsImdlbmVyYXRpb25Db25maWciOm51bGwsInJlc3BvbnNlTW9kYWxpdGllcyI6bnVsbCwidGVtcGVyYXR1cmUiOm51bGwsInRvcFAiOm51bGwsInRvcEsiOm51bGwsIm1heE91dHB1dFRva2VucyI6bnVsbCwibWVkaWFSZXNvbHV0aW9uIjpudWxsLCJzZWVkIjpudWxsLCJzcGVlY2hDb25maWciOm51bGwsInRoaW5raW5nQ29uZmlnIjpudWxsLCJlbmFibGVBZmZlY3RpdmVEaWFsb2ciOm51bGwsInN5c3RlbUluc3RydWN0aW9uIjpudWxsLCJ0b29scyI6bnVsbCwic2Vzc2lvblJlc3VtcHRpb24iOm51bGwsImlucHV0QXVkaW9UcmFuc2NyaXB0aW9uIjp7fSwib3V0cHV0QXVkaW9UcmFuc2NyaXB0aW9uIjp7fSwicmVhbHRpbWVJbnB1dENvbmZpZyI6bnVsbCwiY29udGV4dFdpbmRvd0NvbXByZXNzaW9uIjpudWxsLCJwcm9hY3Rpdml0eSI6bnVsbCwiZXhwbGljaXRWYWRTaWduYWwiOm51bGx9LCJjYWNoZV9jb25maWciOm51bGwsImNhY2hlX21ldGFkYXRhIjpudWxsLCJjYWNoZWFibGVfY29udGVudHNfdG9rZW5fY291bnQiOm51bGwsInByZXZpb3VzX2ludGVyYWN0aW9uX2lkIjpudWxsfQ==" + } + ] + }, + "scheduleToCloseTimeout": "0s", + "scheduleToStartTimeout": "0s", + "startToCloseTimeout": "120s", + "heartbeatTimeout": "0s", + "workflowTaskCompletedEventId": "16", + "retryPolicy": { + "initialInterval": "1s", + "backoffCoefficient": 2.0, + "maximumInterval": "100s" + }, + "useWorkflowBuildId": true, + "priority": {} + }, + "userMetadata": { + "summary": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IndyaXRlciI=" + } + } + }, + { + "eventId": "18", + "eventTime": "2026-01-26T21:08:54.583085Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", + "taskId": "1103740", + "activityTaskStartedEventAttributes": { + "scheduledEventId": "17", + "identity": "69823@Tims-MacBook-Pro.local", + "requestId": "a880f966-0e32-4c24-a531-ae7a427847b5", + "attempt": 1, + "workerVersion": { + "buildId": "2f45d1deb022376ac8b03fdcb17f8e5c" + } + } + }, + { + "eventId": "19", + "eventTime": "2026-01-26T21:08:54.584874Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", + "taskId": "1103741", + "activityTaskCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "W3sibW9kZWxWZXJzaW9uIjpudWxsLCJjb250ZW50Ijp7InBhcnRzIjpbeyJtZWRpYVJlc29sdXRpb24iOm51bGwsImNvZGVFeGVjdXRpb25SZXN1bHQiOm51bGwsImV4ZWN1dGFibGVDb2RlIjpudWxsLCJmaWxlRGF0YSI6bnVsbCwiZnVuY3Rpb25DYWxsIjpudWxsLCJmdW5jdGlvblJlc3BvbnNlIjpudWxsLCJpbmxpbmVEYXRhIjpudWxsLCJ0ZXh0IjoiaGFpa3UiLCJ0aG91Z2h0IjpudWxsLCJ0aG91Z2h0U2lnbmF0dXJlIjpudWxsLCJ2aWRlb01ldGFkYXRhIjpudWxsfV0sInJvbGUiOiJtb2RlbCJ9LCJncm91bmRpbmdNZXRhZGF0YSI6bnVsbCwicGFydGlhbCI6bnVsbCwidHVybkNvbXBsZXRlIjpudWxsLCJmaW5pc2hSZWFzb24iOm51bGwsImVycm9yQ29kZSI6bnVsbCwiZXJyb3JNZXNzYWdlIjpudWxsLCJpbnRlcnJ1cHRlZCI6bnVsbCwiY3VzdG9tTWV0YWRhdGEiOm51bGwsInVzYWdlTWV0YWRhdGEiOm51bGwsImxpdmVTZXNzaW9uUmVzdW1wdGlvblVwZGF0ZSI6bnVsbCwiaW5wdXRUcmFuc2NyaXB0aW9uIjpudWxsLCJvdXRwdXRUcmFuc2NyaXB0aW9uIjpudWxsLCJhdmdMb2dwcm9icyI6bnVsbCwibG9ncHJvYnNSZXN1bHQiOm51bGwsImNhY2hlTWV0YWRhdGEiOm51bGwsImNpdGF0aW9uTWV0YWRhdGEiOm51bGwsImludGVyYWN0aW9uSWQiOm51bGx9XQ==" + } + ] + }, + "scheduledEventId": "17", + "startedEventId": "18", + "identity": "69823@Tims-MacBook-Pro.local" + } + }, + { + "eventId": "20", + "eventTime": "2026-01-26T21:08:54.584876Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1103742", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "adk-task-queue", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "21", + "eventTime": "2026-01-26T21:08:54.585780Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1103745", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "20", + "identity": "69823@Tims-MacBook-Pro.local", + "requestId": "5a954014-8c9b-4759-a859-6785b3d33967", + "historySizeBytes": "20508", + "workerVersion": { + "buildId": "2f45d1deb022376ac8b03fdcb17f8e5c" + } + } + }, + { + "eventId": "22", + "eventTime": "2026-01-26T21:08:54.617825Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1103749", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "20", + "startedEventId": "21", + "identity": "69823@Tims-MacBook-Pro.local", + "workerVersion": { + "buildId": "2f45d1deb022376ac8b03fdcb17f8e5c" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "23", + "eventTime": "2026-01-26T21:08:54.617846Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED", + "taskId": "1103750", + "workflowExecutionCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "ImhhaWt1Ig==" + } + ] + }, + "workflowTaskCompletedEventId": "22" + } + } + ] +} \ No newline at end of file diff --git a/tests/contrib/google_adk_agents/histories/single_agent.json b/tests/contrib/google_adk_agents/histories/single_agent.json new file mode 100644 index 000000000..c6eb1f45a --- /dev/null +++ b/tests/contrib/google_adk_agents/histories/single_agent.json @@ -0,0 +1,491 @@ +{ + "events": [ + { + "eventId": "1", + "eventTime": "2026-01-26T21:11:30.080188Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED", + "taskId": "1104115", + "workflowExecutionStartedEventAttributes": { + "workflowType": { + "name": "WeatherAgent" + }, + "taskQueue": { + "name": "adk-task-queue", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IldoYXQgaXMgdGhlIHdlYXRoZXIgaW4gTmV3IFlvcms/Ig==" + }, + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IndlYXRoZXJfbW9kZWwi" + } + ] + }, + "workflowTaskTimeout": "10s", + "originalExecutionRunId": "019bfc25-bc20-72da-8597-e94f48dc9057", + "identity": "70127@Tims-MacBook-Pro.local", + "firstExecutionRunId": "019bfc25-bc20-72da-8597-e94f48dc9057", + "attempt": 1, + "firstWorkflowTaskBackoff": "0s", + "workflowId": "weather-agent-workflow-b9d42dfd-2318-45d8-b952-650a79362a09", + "priority": {} + } + }, + { + "eventId": "2", + "eventTime": "2026-01-26T21:11:30.083361Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1104116", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "adk-task-queue", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "3", + "eventTime": "2026-01-26T21:11:30.094782Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1104121", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "2", + "identity": "70127@Tims-MacBook-Pro.local", + "requestId": "e605155e-5b3a-4a72-b539-e53710562fae", + "historySizeBytes": "403", + "workerVersion": { + "buildId": "33d06b1c69b2db724aa60c7d3ac4fea9" + } + } + }, + { + "eventId": "4", + "eventTime": "2026-01-26T21:11:30.184056Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1104125", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "2", + "startedEventId": "3", + "identity": "70127@Tims-MacBook-Pro.local", + "workerVersion": { + "buildId": "33d06b1c69b2db724aa60c7d3ac4fea9" + }, + "sdkMetadata": { + "coreUsedFlags": [ + 3, + 2, + 1 + ], + "sdkName": "temporal-python", + "sdkVersion": "1.21.1" + }, + "meteringMetadata": {} + } + }, + { + "eventId": "5", + "eventTime": "2026-01-26T21:11:30.184120Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", + "taskId": "1104126", + "activityTaskScheduledEventAttributes": { + "activityId": "1", + "activityType": { + "name": "invoke_model" + }, + "taskQueue": { + "name": "adk-task-queue", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "header": {}, + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJtb2RlbCI6IndlYXRoZXJfbW9kZWwiLCJjb250ZW50cyI6W3sicGFydHMiOlt7Im1lZGlhUmVzb2x1dGlvbiI6bnVsbCwiY29kZUV4ZWN1dGlvblJlc3VsdCI6bnVsbCwiZXhlY3V0YWJsZUNvZGUiOm51bGwsImZpbGVEYXRhIjpudWxsLCJmdW5jdGlvbkNhbGwiOm51bGwsImZ1bmN0aW9uUmVzcG9uc2UiOm51bGwsImlubGluZURhdGEiOm51bGwsInRleHQiOiJXaGF0IGlzIHRoZSB3ZWF0aGVyIGluIE5ldyBZb3JrPyIsInRob3VnaHQiOm51bGwsInRob3VnaHRTaWduYXR1cmUiOm51bGwsInZpZGVvTWV0YWRhdGEiOm51bGx9XSwicm9sZSI6InVzZXIifV0sImNvbmZpZyI6eyJodHRwT3B0aW9ucyI6bnVsbCwic2hvdWxkUmV0dXJuSHR0cFJlc3BvbnNlIjpudWxsLCJzeXN0ZW1JbnN0cnVjdGlvbiI6IllvdSBhcmUgYW4gYWdlbnQuIFlvdXIgaW50ZXJuYWwgbmFtZSBpcyBcInRlc3RfYWdlbnRcIi4iLCJ0ZW1wZXJhdHVyZSI6bnVsbCwidG9wUCI6bnVsbCwidG9wSyI6bnVsbCwiY2FuZGlkYXRlQ291bnQiOm51bGwsIm1heE91dHB1dFRva2VucyI6bnVsbCwic3RvcFNlcXVlbmNlcyI6bnVsbCwicmVzcG9uc2VMb2dwcm9icyI6bnVsbCwibG9ncHJvYnMiOm51bGwsInByZXNlbmNlUGVuYWx0eSI6bnVsbCwiZnJlcXVlbmN5UGVuYWx0eSI6bnVsbCwic2VlZCI6bnVsbCwicmVzcG9uc2VNaW1lVHlwZSI6bnVsbCwicmVzcG9uc2VTY2hlbWEiOm51bGwsInJlc3BvbnNlSnNvblNjaGVtYSI6bnVsbCwicm91dGluZ0NvbmZpZyI6bnVsbCwibW9kZWxTZWxlY3Rpb25Db25maWciOm51bGwsInNhZmV0eVNldHRpbmdzIjpudWxsLCJ0b29scyI6W3sicmV0cmlldmFsIjpudWxsLCJjb21wdXRlclVzZSI6bnVsbCwiZmlsZVNlYXJjaCI6bnVsbCwiY29kZUV4ZWN1dGlvbiI6bnVsbCwiZW50ZXJwcmlzZVdlYlNlYXJjaCI6bnVsbCwiZnVuY3Rpb25EZWNsYXJhdGlvbnMiOlt7ImRlc2NyaXB0aW9uIjoiQWN0aXZpdHkgdGhhdCBnZXRzIHdlYXRoZXIgZm9yIGEgZ2l2ZW4gY2l0eS4iLCJuYW1lIjoiZ2V0X3dlYXRoZXIiLCJwYXJhbWV0ZXJzIjp7ImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjpudWxsLCJkZWZzIjpudWxsLCJyZWYiOm51bGwsImFueU9mIjpudWxsLCJkZWZhdWx0IjpudWxsLCJkZXNjcmlwdGlvbiI6bnVsbCwiZW51bSI6bnVsbCwiZXhhbXBsZSI6bnVsbCwiZm9ybWF0IjpudWxsLCJpdGVtcyI6bnVsbCwibWF4SXRlbXMiOm51bGwsIm1heExlbmd0aCI6bnVsbCwibWF4UHJvcGVydGllcyI6bnVsbCwibWF4aW11bSI6bnVsbCwibWluSXRlbXMiOm51bGwsIm1pbkxlbmd0aCI6bnVsbCwibWluUHJvcGVydGllcyI6bnVsbCwibWluaW11bSI6bnVsbCwibnVsbGFibGUiOm51bGwsInBhdHRlcm4iOm51bGwsInByb3BlcnRpZXMiOnsiY2l0eSI6eyJhZGRpdGlvbmFsUHJvcGVydGllcyI6bnVsbCwiZGVmcyI6bnVsbCwicmVmIjpudWxsLCJhbnlPZiI6bnVsbCwiZGVmYXVsdCI6bnVsbCwiZGVzY3JpcHRpb24iOm51bGwsImVudW0iOm51bGwsImV4YW1wbGUiOm51bGwsImZvcm1hdCI6bnVsbCwiaXRlbXMiOm51bGwsIm1heEl0ZW1zIjpudWxsLCJtYXhMZW5ndGgiOm51bGwsIm1heFByb3BlcnRpZXMiOm51bGwsIm1heGltdW0iOm51bGwsIm1pbkl0ZW1zIjpudWxsLCJtaW5MZW5ndGgiOm51bGwsIm1pblByb3BlcnRpZXMiOm51bGwsIm1pbmltdW0iOm51bGwsIm51bGxhYmxlIjpudWxsLCJwYXR0ZXJuIjpudWxsLCJwcm9wZXJ0aWVzIjpudWxsLCJwcm9wZXJ0eU9yZGVyaW5nIjpudWxsLCJyZXF1aXJlZCI6bnVsbCwidGl0bGUiOm51bGwsInR5cGUiOiJTVFJJTkcifX0sInByb3BlcnR5T3JkZXJpbmciOm51bGwsInJlcXVpcmVkIjpbImNpdHkiXSwidGl0bGUiOm51bGwsInR5cGUiOiJPQkpFQ1QifSwicGFyYW1ldGVyc0pzb25TY2hlbWEiOm51bGwsInJlc3BvbnNlIjpudWxsLCJyZXNwb25zZUpzb25TY2hlbWEiOm51bGwsImJlaGF2aW9yIjpudWxsfV0sImdvb2dsZU1hcHMiOm51bGwsImdvb2dsZVNlYXJjaCI6bnVsbCwiZ29vZ2xlU2VhcmNoUmV0cmlldmFsIjpudWxsLCJ1cmxDb250ZXh0IjpudWxsfV0sInRvb2xDb25maWciOm51bGwsImxhYmVscyI6bnVsbCwiY2FjaGVkQ29udGVudCI6bnVsbCwicmVzcG9uc2VNb2RhbGl0aWVzIjpudWxsLCJtZWRpYVJlc29sdXRpb24iOm51bGwsInNwZWVjaENvbmZpZyI6bnVsbCwiYXVkaW9UaW1lc3RhbXAiOm51bGwsImF1dG9tYXRpY0Z1bmN0aW9uQ2FsbGluZyI6bnVsbCwidGhpbmtpbmdDb25maWciOm51bGwsImltYWdlQ29uZmlnIjpudWxsLCJlbmFibGVFbmhhbmNlZENpdmljQW5zd2VycyI6bnVsbCwibW9kZWxBcm1vckNvbmZpZyI6bnVsbH0sImxpdmVfY29ubmVjdF9jb25maWciOnsiaHR0cE9wdGlvbnMiOm51bGwsImdlbmVyYXRpb25Db25maWciOm51bGwsInJlc3BvbnNlTW9kYWxpdGllcyI6bnVsbCwidGVtcGVyYXR1cmUiOm51bGwsInRvcFAiOm51bGwsInRvcEsiOm51bGwsIm1heE91dHB1dFRva2VucyI6bnVsbCwibWVkaWFSZXNvbHV0aW9uIjpudWxsLCJzZWVkIjpudWxsLCJzcGVlY2hDb25maWciOm51bGwsInRoaW5raW5nQ29uZmlnIjpudWxsLCJlbmFibGVBZmZlY3RpdmVEaWFsb2ciOm51bGwsInN5c3RlbUluc3RydWN0aW9uIjpudWxsLCJ0b29scyI6bnVsbCwic2Vzc2lvblJlc3VtcHRpb24iOm51bGwsImlucHV0QXVkaW9UcmFuc2NyaXB0aW9uIjp7fSwib3V0cHV0QXVkaW9UcmFuc2NyaXB0aW9uIjp7fSwicmVhbHRpbWVJbnB1dENvbmZpZyI6bnVsbCwiY29udGV4dFdpbmRvd0NvbXByZXNzaW9uIjpudWxsLCJwcm9hY3Rpdml0eSI6bnVsbCwiZXhwbGljaXRWYWRTaWduYWwiOm51bGx9LCJjYWNoZV9jb25maWciOm51bGwsImNhY2hlX21ldGFkYXRhIjpudWxsLCJjYWNoZWFibGVfY29udGVudHNfdG9rZW5fY291bnQiOm51bGwsInByZXZpb3VzX2ludGVyYWN0aW9uX2lkIjpudWxsfQ==" + } + ] + }, + "scheduleToCloseTimeout": "0s", + "scheduleToStartTimeout": "0s", + "startToCloseTimeout": "120s", + "heartbeatTimeout": "0s", + "workflowTaskCompletedEventId": "4", + "retryPolicy": { + "initialInterval": "1s", + "backoffCoefficient": 2.0, + "maximumInterval": "100s" + }, + "useWorkflowBuildId": true, + "priority": {} + }, + "userMetadata": { + "summary": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "InRlc3RfYWdlbnQi" + } + } + }, + { + "eventId": "6", + "eventTime": "2026-01-26T21:11:30.185095Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", + "taskId": "1104132", + "activityTaskStartedEventAttributes": { + "scheduledEventId": "5", + "identity": "70127@Tims-MacBook-Pro.local", + "requestId": "affae0ec-873d-4c27-85c3-cbbd6cec61f6", + "attempt": 1, + "workerVersion": { + "buildId": "33d06b1c69b2db724aa60c7d3ac4fea9" + } + } + }, + { + "eventId": "7", + "eventTime": "2026-01-26T21:11:30.188610Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", + "taskId": "1104133", + "activityTaskCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "W3sibW9kZWxWZXJzaW9uIjpudWxsLCJjb250ZW50Ijp7InBhcnRzIjpbeyJtZWRpYVJlc29sdXRpb24iOm51bGwsImNvZGVFeGVjdXRpb25SZXN1bHQiOm51bGwsImV4ZWN1dGFibGVDb2RlIjpudWxsLCJmaWxlRGF0YSI6bnVsbCwiZnVuY3Rpb25DYWxsIjp7ImlkIjpudWxsLCJhcmdzIjp7ImNpdHkiOiJOZXcgWW9yayJ9LCJuYW1lIjoiZ2V0X3dlYXRoZXIiLCJwYXJ0aWFsQXJncyI6bnVsbCwid2lsbENvbnRpbnVlIjpudWxsfSwiZnVuY3Rpb25SZXNwb25zZSI6bnVsbCwiaW5saW5lRGF0YSI6bnVsbCwidGV4dCI6bnVsbCwidGhvdWdodCI6bnVsbCwidGhvdWdodFNpZ25hdHVyZSI6bnVsbCwidmlkZW9NZXRhZGF0YSI6bnVsbH1dLCJyb2xlIjoibW9kZWwifSwiZ3JvdW5kaW5nTWV0YWRhdGEiOm51bGwsInBhcnRpYWwiOm51bGwsInR1cm5Db21wbGV0ZSI6bnVsbCwiZmluaXNoUmVhc29uIjpudWxsLCJlcnJvckNvZGUiOm51bGwsImVycm9yTWVzc2FnZSI6bnVsbCwiaW50ZXJydXB0ZWQiOm51bGwsImN1c3RvbU1ldGFkYXRhIjpudWxsLCJ1c2FnZU1ldGFkYXRhIjpudWxsLCJsaXZlU2Vzc2lvblJlc3VtcHRpb25VcGRhdGUiOm51bGwsImlucHV0VHJhbnNjcmlwdGlvbiI6bnVsbCwib3V0cHV0VHJhbnNjcmlwdGlvbiI6bnVsbCwiYXZnTG9ncHJvYnMiOm51bGwsImxvZ3Byb2JzUmVzdWx0IjpudWxsLCJjYWNoZU1ldGFkYXRhIjpudWxsLCJjaXRhdGlvbk1ldGFkYXRhIjpudWxsLCJpbnRlcmFjdGlvbklkIjpudWxsfV0=" + } + ] + }, + "scheduledEventId": "5", + "startedEventId": "6", + "identity": "70127@Tims-MacBook-Pro.local" + } + }, + { + "eventId": "8", + "eventTime": "2026-01-26T21:11:30.188613Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1104134", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "adk-task-queue", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "9", + "eventTime": "2026-01-26T21:11:30.189631Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1104137", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "8", + "identity": "70127@Tims-MacBook-Pro.local", + "requestId": "755e188a-9cdd-4818-b328-4d40f4627910", + "historySizeBytes": "4799", + "workerVersion": { + "buildId": "33d06b1c69b2db724aa60c7d3ac4fea9" + } + } + }, + { + "eventId": "10", + "eventTime": "2026-01-26T21:11:30.221995Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1104141", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "8", + "startedEventId": "9", + "identity": "70127@Tims-MacBook-Pro.local", + "workerVersion": { + "buildId": "33d06b1c69b2db724aa60c7d3ac4fea9" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "11", + "eventTime": "2026-01-26T21:11:30.222027Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", + "taskId": "1104142", + "activityTaskScheduledEventAttributes": { + "activityId": "2", + "activityType": { + "name": "get_weather" + }, + "taskQueue": { + "name": "adk-task-queue", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "header": {}, + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "Ik5ldyBZb3JrIg==" + } + ] + }, + "scheduleToCloseTimeout": "0s", + "scheduleToStartTimeout": "0s", + "startToCloseTimeout": "60s", + "heartbeatTimeout": "0s", + "workflowTaskCompletedEventId": "10", + "retryPolicy": { + "initialInterval": "1s", + "backoffCoefficient": 2.0, + "maximumInterval": "100s" + }, + "useWorkflowBuildId": true, + "priority": {} + } + }, + { + "eventId": "12", + "eventTime": "2026-01-26T21:11:30.223081Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", + "taskId": "1104147", + "activityTaskStartedEventAttributes": { + "scheduledEventId": "11", + "identity": "70127@Tims-MacBook-Pro.local", + "requestId": "be038a7a-79fc-4e78-bae2-798c1d77b9e4", + "attempt": 1, + "workerVersion": { + "buildId": "33d06b1c69b2db724aa60c7d3ac4fea9" + } + } + }, + { + "eventId": "13", + "eventTime": "2026-01-26T21:11:30.225066Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", + "taskId": "1104148", + "activityTaskCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "Ildhcm0gYW5kIHN1bm55LiAxNyBkZWdyZWVzLiI=" + } + ] + }, + "scheduledEventId": "11", + "startedEventId": "12", + "identity": "70127@Tims-MacBook-Pro.local" + } + }, + { + "eventId": "14", + "eventTime": "2026-01-26T21:11:30.225069Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1104149", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "adk-task-queue", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "15", + "eventTime": "2026-01-26T21:11:30.225814Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1104152", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "14", + "identity": "70127@Tims-MacBook-Pro.local", + "requestId": "ab62b9fc-373f-4157-a891-87c6ecc47405", + "historySizeBytes": "5479", + "workerVersion": { + "buildId": "33d06b1c69b2db724aa60c7d3ac4fea9" + } + } + }, + { + "eventId": "16", + "eventTime": "2026-01-26T21:11:30.258759Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1104156", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "14", + "startedEventId": "15", + "identity": "70127@Tims-MacBook-Pro.local", + "workerVersion": { + "buildId": "33d06b1c69b2db724aa60c7d3ac4fea9" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "17", + "eventTime": "2026-01-26T21:11:30.258787Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", + "taskId": "1104157", + "activityTaskScheduledEventAttributes": { + "activityId": "3", + "activityType": { + "name": "invoke_model" + }, + "taskQueue": { + "name": "adk-task-queue", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "header": {}, + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJtb2RlbCI6IndlYXRoZXJfbW9kZWwiLCJjb250ZW50cyI6W3sicGFydHMiOlt7Im1lZGlhUmVzb2x1dGlvbiI6bnVsbCwiY29kZUV4ZWN1dGlvblJlc3VsdCI6bnVsbCwiZXhlY3V0YWJsZUNvZGUiOm51bGwsImZpbGVEYXRhIjpudWxsLCJmdW5jdGlvbkNhbGwiOm51bGwsImZ1bmN0aW9uUmVzcG9uc2UiOm51bGwsImlubGluZURhdGEiOm51bGwsInRleHQiOiJXaGF0IGlzIHRoZSB3ZWF0aGVyIGluIE5ldyBZb3JrPyIsInRob3VnaHQiOm51bGwsInRob3VnaHRTaWduYXR1cmUiOm51bGwsInZpZGVvTWV0YWRhdGEiOm51bGx9XSwicm9sZSI6InVzZXIifSx7InBhcnRzIjpbeyJtZWRpYVJlc29sdXRpb24iOm51bGwsImNvZGVFeGVjdXRpb25SZXN1bHQiOm51bGwsImV4ZWN1dGFibGVDb2RlIjpudWxsLCJmaWxlRGF0YSI6bnVsbCwiZnVuY3Rpb25DYWxsIjp7ImlkIjpudWxsLCJhcmdzIjp7ImNpdHkiOiJOZXcgWW9yayJ9LCJuYW1lIjoiZ2V0X3dlYXRoZXIiLCJwYXJ0aWFsQXJncyI6bnVsbCwid2lsbENvbnRpbnVlIjpudWxsfSwiZnVuY3Rpb25SZXNwb25zZSI6bnVsbCwiaW5saW5lRGF0YSI6bnVsbCwidGV4dCI6bnVsbCwidGhvdWdodCI6bnVsbCwidGhvdWdodFNpZ25hdHVyZSI6bnVsbCwidmlkZW9NZXRhZGF0YSI6bnVsbH1dLCJyb2xlIjoibW9kZWwifSx7InBhcnRzIjpbeyJtZWRpYVJlc29sdXRpb24iOm51bGwsImNvZGVFeGVjdXRpb25SZXN1bHQiOm51bGwsImV4ZWN1dGFibGVDb2RlIjpudWxsLCJmaWxlRGF0YSI6bnVsbCwiZnVuY3Rpb25DYWxsIjpudWxsLCJmdW5jdGlvblJlc3BvbnNlIjp7IndpbGxDb250aW51ZSI6bnVsbCwic2NoZWR1bGluZyI6bnVsbCwicGFydHMiOm51bGwsImlkIjpudWxsLCJuYW1lIjoiZ2V0X3dlYXRoZXIiLCJyZXNwb25zZSI6eyJyZXN1bHQiOiJXYXJtIGFuZCBzdW5ueS4gMTcgZGVncmVlcy4ifX0sImlubGluZURhdGEiOm51bGwsInRleHQiOm51bGwsInRob3VnaHQiOm51bGwsInRob3VnaHRTaWduYXR1cmUiOm51bGwsInZpZGVvTWV0YWRhdGEiOm51bGx9XSwicm9sZSI6InVzZXIifV0sImNvbmZpZyI6eyJodHRwT3B0aW9ucyI6bnVsbCwic2hvdWxkUmV0dXJuSHR0cFJlc3BvbnNlIjpudWxsLCJzeXN0ZW1JbnN0cnVjdGlvbiI6IllvdSBhcmUgYW4gYWdlbnQuIFlvdXIgaW50ZXJuYWwgbmFtZSBpcyBcInRlc3RfYWdlbnRcIi4iLCJ0ZW1wZXJhdHVyZSI6bnVsbCwidG9wUCI6bnVsbCwidG9wSyI6bnVsbCwiY2FuZGlkYXRlQ291bnQiOm51bGwsIm1heE91dHB1dFRva2VucyI6bnVsbCwic3RvcFNlcXVlbmNlcyI6bnVsbCwicmVzcG9uc2VMb2dwcm9icyI6bnVsbCwibG9ncHJvYnMiOm51bGwsInByZXNlbmNlUGVuYWx0eSI6bnVsbCwiZnJlcXVlbmN5UGVuYWx0eSI6bnVsbCwic2VlZCI6bnVsbCwicmVzcG9uc2VNaW1lVHlwZSI6bnVsbCwicmVzcG9uc2VTY2hlbWEiOm51bGwsInJlc3BvbnNlSnNvblNjaGVtYSI6bnVsbCwicm91dGluZ0NvbmZpZyI6bnVsbCwibW9kZWxTZWxlY3Rpb25Db25maWciOm51bGwsInNhZmV0eVNldHRpbmdzIjpudWxsLCJ0b29scyI6W3sicmV0cmlldmFsIjpudWxsLCJjb21wdXRlclVzZSI6bnVsbCwiZmlsZVNlYXJjaCI6bnVsbCwiY29kZUV4ZWN1dGlvbiI6bnVsbCwiZW50ZXJwcmlzZVdlYlNlYXJjaCI6bnVsbCwiZnVuY3Rpb25EZWNsYXJhdGlvbnMiOlt7ImRlc2NyaXB0aW9uIjoiQWN0aXZpdHkgdGhhdCBnZXRzIHdlYXRoZXIgZm9yIGEgZ2l2ZW4gY2l0eS4iLCJuYW1lIjoiZ2V0X3dlYXRoZXIiLCJwYXJhbWV0ZXJzIjp7ImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjpudWxsLCJkZWZzIjpudWxsLCJyZWYiOm51bGwsImFueU9mIjpudWxsLCJkZWZhdWx0IjpudWxsLCJkZXNjcmlwdGlvbiI6bnVsbCwiZW51bSI6bnVsbCwiZXhhbXBsZSI6bnVsbCwiZm9ybWF0IjpudWxsLCJpdGVtcyI6bnVsbCwibWF4SXRlbXMiOm51bGwsIm1heExlbmd0aCI6bnVsbCwibWF4UHJvcGVydGllcyI6bnVsbCwibWF4aW11bSI6bnVsbCwibWluSXRlbXMiOm51bGwsIm1pbkxlbmd0aCI6bnVsbCwibWluUHJvcGVydGllcyI6bnVsbCwibWluaW11bSI6bnVsbCwibnVsbGFibGUiOm51bGwsInBhdHRlcm4iOm51bGwsInByb3BlcnRpZXMiOnsiY2l0eSI6eyJhZGRpdGlvbmFsUHJvcGVydGllcyI6bnVsbCwiZGVmcyI6bnVsbCwicmVmIjpudWxsLCJhbnlPZiI6bnVsbCwiZGVmYXVsdCI6bnVsbCwiZGVzY3JpcHRpb24iOm51bGwsImVudW0iOm51bGwsImV4YW1wbGUiOm51bGwsImZvcm1hdCI6bnVsbCwiaXRlbXMiOm51bGwsIm1heEl0ZW1zIjpudWxsLCJtYXhMZW5ndGgiOm51bGwsIm1heFByb3BlcnRpZXMiOm51bGwsIm1heGltdW0iOm51bGwsIm1pbkl0ZW1zIjpudWxsLCJtaW5MZW5ndGgiOm51bGwsIm1pblByb3BlcnRpZXMiOm51bGwsIm1pbmltdW0iOm51bGwsIm51bGxhYmxlIjpudWxsLCJwYXR0ZXJuIjpudWxsLCJwcm9wZXJ0aWVzIjpudWxsLCJwcm9wZXJ0eU9yZGVyaW5nIjpudWxsLCJyZXF1aXJlZCI6bnVsbCwidGl0bGUiOm51bGwsInR5cGUiOiJTVFJJTkcifX0sInByb3BlcnR5T3JkZXJpbmciOm51bGwsInJlcXVpcmVkIjpbImNpdHkiXSwidGl0bGUiOm51bGwsInR5cGUiOiJPQkpFQ1QifSwicGFyYW1ldGVyc0pzb25TY2hlbWEiOm51bGwsInJlc3BvbnNlIjpudWxsLCJyZXNwb25zZUpzb25TY2hlbWEiOm51bGwsImJlaGF2aW9yIjpudWxsfV0sImdvb2dsZU1hcHMiOm51bGwsImdvb2dsZVNlYXJjaCI6bnVsbCwiZ29vZ2xlU2VhcmNoUmV0cmlldmFsIjpudWxsLCJ1cmxDb250ZXh0IjpudWxsfV0sInRvb2xDb25maWciOm51bGwsImxhYmVscyI6bnVsbCwiY2FjaGVkQ29udGVudCI6bnVsbCwicmVzcG9uc2VNb2RhbGl0aWVzIjpudWxsLCJtZWRpYVJlc29sdXRpb24iOm51bGwsInNwZWVjaENvbmZpZyI6bnVsbCwiYXVkaW9UaW1lc3RhbXAiOm51bGwsImF1dG9tYXRpY0Z1bmN0aW9uQ2FsbGluZyI6bnVsbCwidGhpbmtpbmdDb25maWciOm51bGwsImltYWdlQ29uZmlnIjpudWxsLCJlbmFibGVFbmhhbmNlZENpdmljQW5zd2VycyI6bnVsbCwibW9kZWxBcm1vckNvbmZpZyI6bnVsbH0sImxpdmVfY29ubmVjdF9jb25maWciOnsiaHR0cE9wdGlvbnMiOm51bGwsImdlbmVyYXRpb25Db25maWciOm51bGwsInJlc3BvbnNlTW9kYWxpdGllcyI6bnVsbCwidGVtcGVyYXR1cmUiOm51bGwsInRvcFAiOm51bGwsInRvcEsiOm51bGwsIm1heE91dHB1dFRva2VucyI6bnVsbCwibWVkaWFSZXNvbHV0aW9uIjpudWxsLCJzZWVkIjpudWxsLCJzcGVlY2hDb25maWciOm51bGwsInRoaW5raW5nQ29uZmlnIjpudWxsLCJlbmFibGVBZmZlY3RpdmVEaWFsb2ciOm51bGwsInN5c3RlbUluc3RydWN0aW9uIjpudWxsLCJ0b29scyI6bnVsbCwic2Vzc2lvblJlc3VtcHRpb24iOm51bGwsImlucHV0QXVkaW9UcmFuc2NyaXB0aW9uIjp7fSwib3V0cHV0QXVkaW9UcmFuc2NyaXB0aW9uIjp7fSwicmVhbHRpbWVJbnB1dENvbmZpZyI6bnVsbCwiY29udGV4dFdpbmRvd0NvbXByZXNzaW9uIjpudWxsLCJwcm9hY3Rpdml0eSI6bnVsbCwiZXhwbGljaXRWYWRTaWduYWwiOm51bGx9LCJjYWNoZV9jb25maWciOm51bGwsImNhY2hlX21ldGFkYXRhIjpudWxsLCJjYWNoZWFibGVfY29udGVudHNfdG9rZW5fY291bnQiOm51bGwsInByZXZpb3VzX2ludGVyYWN0aW9uX2lkIjpudWxsfQ==" + } + ] + }, + "scheduleToCloseTimeout": "0s", + "scheduleToStartTimeout": "0s", + "startToCloseTimeout": "120s", + "heartbeatTimeout": "0s", + "workflowTaskCompletedEventId": "16", + "retryPolicy": { + "initialInterval": "1s", + "backoffCoefficient": 2.0, + "maximumInterval": "100s" + }, + "useWorkflowBuildId": true, + "priority": {} + }, + "userMetadata": { + "summary": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "InRlc3RfYWdlbnQi" + } + } + }, + { + "eventId": "18", + "eventTime": "2026-01-26T21:11:30.259830Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", + "taskId": "1104162", + "activityTaskStartedEventAttributes": { + "scheduledEventId": "17", + "identity": "70127@Tims-MacBook-Pro.local", + "requestId": "0e5bef66-5e35-4c0c-86eb-f2110a843377", + "attempt": 1, + "workerVersion": { + "buildId": "33d06b1c69b2db724aa60c7d3ac4fea9" + } + } + }, + { + "eventId": "19", + "eventTime": "2026-01-26T21:11:30.261889Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", + "taskId": "1104163", + "activityTaskCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "W3sibW9kZWxWZXJzaW9uIjpudWxsLCJjb250ZW50Ijp7InBhcnRzIjpbeyJtZWRpYVJlc29sdXRpb24iOm51bGwsImNvZGVFeGVjdXRpb25SZXN1bHQiOm51bGwsImV4ZWN1dGFibGVDb2RlIjpudWxsLCJmaWxlRGF0YSI6bnVsbCwiZnVuY3Rpb25DYWxsIjpudWxsLCJmdW5jdGlvblJlc3BvbnNlIjpudWxsLCJpbmxpbmVEYXRhIjpudWxsLCJ0ZXh0Ijoid2FybSBhbmQgc3VubnkiLCJ0aG91Z2h0IjpudWxsLCJ0aG91Z2h0U2lnbmF0dXJlIjpudWxsLCJ2aWRlb01ldGFkYXRhIjpudWxsfV0sInJvbGUiOiJtb2RlbCJ9LCJncm91bmRpbmdNZXRhZGF0YSI6bnVsbCwicGFydGlhbCI6bnVsbCwidHVybkNvbXBsZXRlIjpudWxsLCJmaW5pc2hSZWFzb24iOm51bGwsImVycm9yQ29kZSI6bnVsbCwiZXJyb3JNZXNzYWdlIjpudWxsLCJpbnRlcnJ1cHRlZCI6bnVsbCwiY3VzdG9tTWV0YWRhdGEiOm51bGwsInVzYWdlTWV0YWRhdGEiOm51bGwsImxpdmVTZXNzaW9uUmVzdW1wdGlvblVwZGF0ZSI6bnVsbCwiaW5wdXRUcmFuc2NyaXB0aW9uIjpudWxsLCJvdXRwdXRUcmFuc2NyaXB0aW9uIjpudWxsLCJhdmdMb2dwcm9icyI6bnVsbCwibG9ncHJvYnNSZXN1bHQiOm51bGwsImNhY2hlTWV0YWRhdGEiOm51bGwsImNpdGF0aW9uTWV0YWRhdGEiOm51bGwsImludGVyYWN0aW9uSWQiOm51bGx9XQ==" + } + ] + }, + "scheduledEventId": "17", + "startedEventId": "18", + "identity": "70127@Tims-MacBook-Pro.local" + } + }, + { + "eventId": "20", + "eventTime": "2026-01-26T21:11:30.261891Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1104164", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "adk-task-queue", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "21", + "eventTime": "2026-01-26T21:11:30.262662Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1104167", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "20", + "identity": "70127@Tims-MacBook-Pro.local", + "requestId": "b23c5065-a19d-4fe0-b13b-37a67e21d355", + "historySizeBytes": "10489", + "workerVersion": { + "buildId": "33d06b1c69b2db724aa60c7d3ac4fea9" + } + } + }, + { + "eventId": "22", + "eventTime": "2026-01-26T21:11:30.298088Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1104171", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "20", + "startedEventId": "21", + "identity": "70127@Tims-MacBook-Pro.local", + "workerVersion": { + "buildId": "33d06b1c69b2db724aa60c7d3ac4fea9" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "23", + "eventTime": "2026-01-26T21:11:30.298110Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED", + "taskId": "1104172", + "workflowExecutionCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJtb2RlbFZlcnNpb24iOm51bGwsImNvbnRlbnQiOnsicGFydHMiOlt7Im1lZGlhUmVzb2x1dGlvbiI6bnVsbCwiY29kZUV4ZWN1dGlvblJlc3VsdCI6bnVsbCwiZXhlY3V0YWJsZUNvZGUiOm51bGwsImZpbGVEYXRhIjpudWxsLCJmdW5jdGlvbkNhbGwiOm51bGwsImZ1bmN0aW9uUmVzcG9uc2UiOm51bGwsImlubGluZURhdGEiOm51bGwsInRleHQiOiJ3YXJtIGFuZCBzdW5ueSIsInRob3VnaHQiOm51bGwsInRob3VnaHRTaWduYXR1cmUiOm51bGwsInZpZGVvTWV0YWRhdGEiOm51bGx9XSwicm9sZSI6Im1vZGVsIn0sImdyb3VuZGluZ01ldGFkYXRhIjpudWxsLCJwYXJ0aWFsIjpudWxsLCJ0dXJuQ29tcGxldGUiOm51bGwsImZpbmlzaFJlYXNvbiI6bnVsbCwiZXJyb3JDb2RlIjpudWxsLCJlcnJvck1lc3NhZ2UiOm51bGwsImludGVycnVwdGVkIjpudWxsLCJjdXN0b21NZXRhZGF0YSI6bnVsbCwidXNhZ2VNZXRhZGF0YSI6bnVsbCwibGl2ZVNlc3Npb25SZXN1bXB0aW9uVXBkYXRlIjpudWxsLCJpbnB1dFRyYW5zY3JpcHRpb24iOm51bGwsIm91dHB1dFRyYW5zY3JpcHRpb24iOm51bGwsImF2Z0xvZ3Byb2JzIjpudWxsLCJsb2dwcm9ic1Jlc3VsdCI6bnVsbCwiY2FjaGVNZXRhZGF0YSI6bnVsbCwiY2l0YXRpb25NZXRhZGF0YSI6bnVsbCwiaW50ZXJhY3Rpb25JZCI6bnVsbCwiaW52b2NhdGlvbklkIjoiZS1mMDdiNjJmOS03NzUwLTQ5YjctYmYwYi1jZDkwM2M2YTM0MGYiLCJhdXRob3IiOiJ0ZXN0X2FnZW50IiwiYWN0aW9ucyI6eyJza2lwU3VtbWFyaXphdGlvbiI6bnVsbCwic3RhdGVEZWx0YSI6e30sImFydGlmYWN0RGVsdGEiOnt9LCJ0cmFuc2ZlclRvQWdlbnQiOm51bGwsImVzY2FsYXRlIjpudWxsLCJyZXF1ZXN0ZWRBdXRoQ29uZmlncyI6e30sInJlcXVlc3RlZFRvb2xDb25maXJtYXRpb25zIjp7fSwiY29tcGFjdGlvbiI6bnVsbCwiZW5kT2ZBZ2VudCI6bnVsbCwiYWdlbnRTdGF0ZSI6bnVsbCwicmV3aW5kQmVmb3JlSW52b2NhdGlvbklkIjpudWxsfSwibG9uZ1J1bm5pbmdUb29sSWRzIjpudWxsLCJicmFuY2giOm51bGwsImlkIjoiZmViZmMyMGEtOTM1OS00YWY1LTliMGEtNGI2ODAyNjAyZTdhIiwidGltZXN0YW1wIjoxNzY5NDYxODkwLjIyNTgxNH0=" + } + ] + }, + "workflowTaskCompletedEventId": "22" + } + } + ] +} \ No newline at end of file diff --git a/tests/contrib/google_adk_agents/test_adk_streaming.py b/tests/contrib/google_adk_agents/test_adk_streaming.py new file mode 100644 index 000000000..30aecd9f4 --- /dev/null +++ b/tests/contrib/google_adk_agents/test_adk_streaming.py @@ -0,0 +1,195 @@ +"""Integration tests for ADK streaming support. + +Verifies that the streaming model activity publishes raw ``LlmResponse`` +chunks via the WorkflowStream broker. Non-streaming behavior is covered +by ``test_google_adk_agents.py``. +""" + +import asyncio +import uuid +from collections.abc import AsyncGenerator +from datetime import timedelta + +import pytest +from google.adk import Agent +from google.adk.agents.run_config import RunConfig, StreamingMode +from google.adk.models import BaseLlm, LLMRegistry +from google.adk.models.llm_request import LlmRequest +from google.adk.models.llm_response import LlmResponse +from google.adk.runners import InMemoryRunner +from google.genai.types import Content, Part + +from temporalio import workflow +from temporalio.client import Client, WorkflowFailureError +from temporalio.contrib.google_adk_agents import GoogleAdkPlugin, TemporalModel +from temporalio.contrib.workflow_streams import WorkflowStream, WorkflowStreamClient +from temporalio.worker import Worker + + +class StreamingTestModel(BaseLlm): + """Test model that yields multiple partial responses to simulate streaming.""" + + @classmethod + def supported_models(cls) -> list[str]: + return ["streaming_test_model"] + + async def generate_content_async( + self, llm_request: LlmRequest, stream: bool = False + ) -> AsyncGenerator[LlmResponse, None]: + # The streaming activity must call us with stream=True; if a + # regression drops the flag this test should fail. + if not stream: + raise AssertionError( + "StreamingTestModel.generate_content_async requires stream=True" + ) + yield LlmResponse(content=Content(role="model", parts=[Part(text="Hello ")])) + yield LlmResponse(content=Content(role="model", parts=[Part(text="world!")])) + + +@workflow.defn +class StreamingAdkWorkflow: + """Test workflow that opts into streaming via RunConfig.streaming_mode.""" + + @workflow.init + def __init__(self, prompt: str) -> None: + self.stream = WorkflowStream() + + @workflow.run + async def run(self, prompt: str) -> str: + model = TemporalModel("streaming_test_model", streaming_topic="events") + agent = Agent( + name="test_agent", + model=model, + instruction="You are a test agent.", + ) + + runner = InMemoryRunner(agent=agent, app_name="test-app") + session = await runner.session_service.create_session( + app_name="test-app", user_id="test" + ) + + final_text = "" + async for event in runner.run_async( + user_id="test", + session_id=session.id, + new_message=Content(role="user", parts=[Part(text=prompt)]), + run_config=RunConfig(streaming_mode=StreamingMode.SSE), + ): + if event.content and event.content.parts: + for part in event.content.parts: + if part.text: + final_text = part.text + + return final_text + + +@pytest.mark.asyncio +async def test_streaming_publishes_events(client: Client): + """Streaming activity publishes raw LlmResponse chunks to the topic.""" + LLMRegistry.register(StreamingTestModel) + + new_config = client.config() + new_config["plugins"] = [GoogleAdkPlugin()] + client = Client(**new_config) + + workflow_id = f"adk-streaming-test-{uuid.uuid4()}" + + async with Worker( + client, + task_queue="adk-streaming-test", + workflows=[StreamingAdkWorkflow], + max_cached_workflows=0, + ): + handle = await client.start_workflow( + StreamingAdkWorkflow.run, + "Hello", + id=workflow_id, + task_queue="adk-streaming-test", + execution_timeout=timedelta(seconds=30), + ) + + stream = WorkflowStreamClient.create(client, workflow_id) + responses: list[LlmResponse] = [] + + async def collect_events() -> None: + async for item in stream.subscribe( + ["events"], + from_offset=0, + result_type=LlmResponse, + poll_cooldown=timedelta(milliseconds=50), + ): + responses.append(item.data) + if len(responses) >= 2: + break + + collect_task = asyncio.create_task(collect_events()) + result = await handle.result() + await asyncio.wait_for(collect_task, timeout=10.0) + + # Workflow assembles streamed parts; the last part it observes is "world!". + assert result == "world!" + + texts: list[str] = [] + for r in responses: + if r.content and r.content.parts: + for part in r.content.parts: + if part.text: + texts.append(part.text) + assert texts == ["Hello ", "world!"], f"Unexpected text deltas: {texts}" + + +@workflow.defn +class StreamingAdkRequiresTopicWorkflow: + """Calls ``generate_content_async(stream=True)`` without configuring + ``streaming_topic``; the call must raise before any activity + is scheduled.""" + + @workflow.run + async def run(self, prompt: str) -> str: + model = TemporalModel("streaming_test_model") + agent = Agent( + name="test_agent", + model=model, + instruction="You are a test agent.", + ) + runner = InMemoryRunner(agent=agent, app_name="test-app") + session = await runner.session_service.create_session( + app_name="test-app", user_id="test" + ) + async for _ in runner.run_async( + user_id="test", + session_id=session.id, + new_message=Content(role="user", parts=[Part(text=prompt)]), + run_config=RunConfig(streaming_mode=StreamingMode.SSE), + ): + pass + return "should not reach" + + +@pytest.mark.asyncio +async def test_streaming_requires_topic(client: Client): + """``stream=True`` fails fast when no streaming topic was configured + on ``TemporalModel``. The error is raised in the workflow before any + streaming activity is scheduled.""" + LLMRegistry.register(StreamingTestModel) + + new_config = client.config() + new_config["plugins"] = [GoogleAdkPlugin()] + client = Client(**new_config) + + async with Worker( + client, + task_queue="adk-streaming-requires-topic", + workflows=[StreamingAdkRequiresTopicWorkflow], + max_cached_workflows=0, + ): + with pytest.raises(WorkflowFailureError) as exc_info: + await client.execute_workflow( + StreamingAdkRequiresTopicWorkflow.run, + "Hi", + id=f"adk-streaming-requires-topic-{uuid.uuid4()}", + task_queue="adk-streaming-requires-topic", + execution_timeout=timedelta(seconds=30), + ) + + assert "streaming_topic" in str(exc_info.value.cause) diff --git a/tests/contrib/google_adk_agents/test_adk_tool_context.py b/tests/contrib/google_adk_agents/test_adk_tool_context.py new file mode 100644 index 000000000..2f5d340d8 --- /dev/null +++ b/tests/contrib/google_adk_agents/test_adk_tool_context.py @@ -0,0 +1,352 @@ +"""Tests for ToolContextSnapshot injection into activity-backed tools. + +Covers https://github.com/temporalio/sdk-python/issues/1470: activities +wrapped with activity_as_tool can read the serializable subset of the ADK +ToolContext (session state and function-call id) without the live, +non-serializable ToolContext ever crossing the activity boundary, and +without the parameter leaking into the LLM-facing tool schema. +""" + +import uuid +from collections.abc import AsyncGenerator +from datetime import timedelta +from typing import Any, Optional # pyright: ignore[reportDeprecated] + +import pytest +from google.adk import Agent +from google.adk.features import FeatureName, override_feature_enabled +from google.adk.models import BaseLlm, LLMRegistry +from google.adk.models.llm_request import LlmRequest +from google.adk.models.llm_response import LlmResponse +from google.adk.runners import InMemoryRunner +from google.adk.tools.function_tool import FunctionTool +from google.adk.tools.tool_context import ToolContext +from google.adk.utils.context_utils import Aclosing +from google.genai import types +from google.genai.types import Content, FunctionCall, Part + +from temporalio import activity, workflow +from temporalio.client import Client +from temporalio.contrib.google_adk_agents import GoogleAdkPlugin, TemporalModel +from temporalio.contrib.google_adk_agents.workflow import ( + ToolContextSnapshot, + activity_as_tool, +) +from temporalio.worker import Worker + +TASK_QUEUE = "adk-tool-context-task-queue" + +SESSION_STATE: dict[str, Any] = { + "db_url": "postgres://config", + "retries": 3, + "regions": {"primary": "us-east1", "replicas": ["eu-west1"]}, +} + + +@activity.defn +async def lookup_weather( + city: str, tool_context: ToolContextSnapshot, units: str = "celsius" +) -> str: + """Activity that reads tool configuration from session state. + + Mirrors the shape reported in issue #1470, with tool_context deliberately + in the middle of the parameter list to prove positional slotting. The + returned string also records whether the code ran inside a real activity, + so tests can tell the activity boundary was actually crossed (or not). + """ + db_url = tool_context.state.get("db_url", "") + retries = tool_context.state.get("retries", -1) + region = tool_context.state.get("regions", {}).get("primary", "") + has_function_call_id = "yes" if tool_context.function_call_id else "no" + in_act = "yes" if activity.in_activity() else "no" + return f"{city}|{units}|{db_url}|{retries}|{region}|fc={has_function_call_id}|act={in_act}" + + +def weather_agent(model_name: str) -> Agent: + return Agent( + name="state_agent", + model=TemporalModel(model_name), + tools=[ + activity_as_tool( + lookup_weather, start_to_close_timeout=timedelta(seconds=30) + ) + ], + ) + + +class StateToolModel(BaseLlm): + """Scripted model: call lookup_weather once, then echo its response.""" + + async def generate_content_async( + self, llm_request: LlmRequest, stream: bool = False + ) -> AsyncGenerator[LlmResponse, None]: + tool_response = None + for content in llm_request.contents: + for part in content.parts or []: + if ( + part.function_response is not None + and part.function_response.name == "lookup_weather" + ): + tool_response = part.function_response + if tool_response is None: + yield LlmResponse( + content=Content( + role="model", + parts=[ + Part( + function_call=FunctionCall( + name="lookup_weather", args={"city": "NYC"} + ) + ) + ], + ) + ) + else: + yield LlmResponse( + content=Content( + role="model", + parts=[Part(text=f"done:{tool_response.response}")], + ) + ) + + @classmethod + def supported_models(cls) -> list[str]: + return ["state_tool_model"] + + +async def run_state_agent(model_name: str) -> str: + """Runs the agent against a session seeded with state; returns final text.""" + runner = InMemoryRunner(agent=weather_agent(model_name), app_name="test_app") + session = await runner.session_service.create_session( + app_name="test_app", + user_id="test", + state=SESSION_STATE, + ) + final_text = "" + async with Aclosing( + runner.run_async( + user_id="test", + session_id=session.id, + new_message=types.Content( + role="user", parts=[types.Part(text="weather in NYC?")] + ), + ) + ) as agen: + async for event in agen: + if event.content and event.content.parts and event.content.parts[0].text: + final_text = event.content.parts[0].text + return final_text + + +@workflow.defn +class StateToolWorkflow: + @workflow.run + async def run(self, model_name: str) -> str: + return await run_state_agent(model_name) + + +@pytest.mark.asyncio +async def test_activity_as_tool_receives_tool_context_snapshot(client: Client): + new_config = client.config() + new_config["plugins"] = [GoogleAdkPlugin()] + client = Client(**new_config) + + async with Worker( + client, + task_queue=TASK_QUEUE, + activities=[lookup_weather], + workflows=[StateToolWorkflow], + max_cached_workflows=0, + ): + LLMRegistry.register(StateToolModel) + result = await client.execute_workflow( + StateToolWorkflow.run, + "state_tool_model", + id=f"tool-context-{uuid.uuid4()}", + task_queue=TASK_QUEUE, + execution_timeout=timedelta(seconds=30), + ) + # The activity saw mixed-type session state (string, int, nested dict), + # the default parameter value, and a populated function-call id — none of + # which came from the LLM — and act=yes proves the snapshot crossed a + # real activity boundary rather than running inline in the workflow. + assert "NYC|celsius|postgres://config|3|us-east1|fc=yes|act=yes" in result + + +@pytest.mark.asyncio +async def test_activity_as_tool_snapshot_outside_workflow(): + """Local ADK runs (no Temporal) receive the same snapshot.""" + LLMRegistry.register(StateToolModel) + result = await run_state_agent("state_tool_model") + assert "NYC|celsius|postgres://config|3|us-east1|fc=yes|act=no" in result + + +def _declared_properties(tool: FunctionTool) -> dict[str, Any]: + """Property names in the LLM-facing declaration, across schema styles.""" + declaration = tool._get_declaration() + assert declaration is not None + if declaration.parameters_json_schema is not None: + return declaration.parameters_json_schema.get("properties", {}) + assert declaration.parameters is not None + return declaration.parameters.properties or {} + + +def test_tool_schema_excludes_tool_context(): + """The tool_context parameter never appears in the LLM-facing schema.""" + tool = FunctionTool( + func=activity_as_tool( + lookup_weather, start_to_close_timeout=timedelta(seconds=30) + ) + ) + properties = _declared_properties(tool) + assert "city" in properties + assert "units" in properties + assert "tool_context" not in properties + + +def test_tool_schema_excludes_tool_context_legacy_declaration(): + """Exclusion also holds on the legacy (non-JSON-schema) declaration path.""" + override_feature_enabled(FeatureName.JSON_SCHEMA_FOR_FUNC_DECL, False) + try: + tool = FunctionTool( + func=activity_as_tool( + lookup_weather, start_to_close_timeout=timedelta(seconds=30) + ) + ) + declaration = tool._get_declaration() + assert declaration is not None + assert declaration.parameters is not None + properties = declaration.parameters.properties or {} + assert "city" in properties + assert "units" in properties + assert "tool_context" not in properties + finally: + # The flag is default-on across the supported google-adk range. + override_feature_enabled(FeatureName.JSON_SCHEMA_FOR_FUNC_DECL, True) + + +def test_tool_schema_context_only_parameter(): + """A tool whose only parameter is tool_context exposes no LLM arguments.""" + + @activity.defn + async def ctx_only_tool(tool_context: ToolContextSnapshot) -> str: + return str(tool_context.state) + + tool = FunctionTool( + func=activity_as_tool( + ctx_only_tool, start_to_close_timeout=timedelta(seconds=30) + ) + ) + declaration = tool._get_declaration() + if declaration is not None: + json_properties = (declaration.parameters_json_schema or {}).get( + "properties", {} + ) + legacy_properties = ( + (declaration.parameters.properties or {}) if declaration.parameters else {} + ) + assert not json_properties + assert not legacy_properties + + +def test_activity_as_tool_accepts_optional_snapshot_annotation(): + """ToolContextSnapshot | None is accepted and still excluded from the schema.""" + + @activity.defn + async def optional_tool( + query: str, + tool_context: ToolContextSnapshot | None = None, # pyright: ignore[reportUnusedParameter] + ) -> str: + return query + + tool = FunctionTool( + func=activity_as_tool( + optional_tool, start_to_close_timeout=timedelta(seconds=30) + ) + ) + properties = _declared_properties(tool) + assert set(properties) == {"query"} + + +def test_activity_as_tool_rejects_adk_tool_context_annotation(): + """Annotating with the live ADK ToolContext gives an actionable error.""" + + @activity.defn + async def bad_tool(query: str, tool_context: ToolContext) -> str: # pyright: ignore[reportUnusedParameter] + return query + + with pytest.raises(ValueError, match="ToolContextSnapshot"): + activity_as_tool(bad_tool, start_to_close_timeout=timedelta(seconds=30)) + + +def test_activity_as_tool_rejects_optional_adk_tool_context_annotation(): + """Optional[ToolContext] is rejected with the ADK-specific message.""" + + @activity.defn + async def optional_bad_tool( + query: str, + tool_context: Optional[ToolContext] = None, # pyright: ignore[reportUnusedParameter, reportDeprecated] + ) -> str: + return query + + with pytest.raises(ValueError, match="not serializable"): + activity_as_tool( + optional_bad_tool, start_to_close_timeout=timedelta(seconds=30) + ) + + +def test_activity_as_tool_rejects_adk_context_under_any_name(): + """ADK injects into any param annotated with a context type, so all are rejected.""" + + @activity.defn + async def sneaky_tool(query: str, ctx: ToolContext) -> str: # pyright: ignore[reportUnusedParameter] + return query + + with pytest.raises(ValueError, match="not serializable"): + activity_as_tool(sneaky_tool, start_to_close_timeout=timedelta(seconds=30)) + + +def test_activity_as_tool_rejects_snapshot_under_other_name(): + """ToolContextSnapshot on a differently-named param would leak into the schema.""" + + @activity.defn + async def misnamed_tool(query: str, snap: ToolContextSnapshot) -> str: # pyright: ignore[reportUnusedParameter] + return query + + with pytest.raises(ValueError, match="named 'tool_context'"): + activity_as_tool(misnamed_tool, start_to_close_timeout=timedelta(seconds=30)) + + +def test_activity_as_tool_rejects_unannotated_tool_context(): + """The reserved name without an annotation gives an actionable error.""" + + @activity.defn + async def untyped_tool(query: str, tool_context) -> str: # type: ignore[no-untyped-def] # pyright: ignore[reportUnusedParameter, reportMissingParameterType] + return query + + with pytest.raises(ValueError, match="unannotated 'tool_context'"): + activity_as_tool(untyped_tool, start_to_close_timeout=timedelta(seconds=30)) + + +def test_activity_as_tool_rejects_other_tool_context_annotation(): + """The reserved name with an unrelated annotation gives an actionable error.""" + + @activity.defn + async def confused_tool(query: str, tool_context: dict[str, Any]) -> str: # pyright: ignore[reportUnusedParameter] + return query + + with pytest.raises(ValueError, match="reserved by ADK"): + activity_as_tool(confused_tool, start_to_close_timeout=timedelta(seconds=30)) + + +def test_activity_as_tool_without_tool_context_unchanged(): + """Activities without a tool_context parameter keep their exact schema.""" + + @activity.defn + async def plain_tool(query: str) -> str: + return query + + tool = FunctionTool( + func=activity_as_tool(plain_tool, start_to_close_timeout=timedelta(seconds=30)) + ) + assert set(_declared_properties(tool)) == {"query"} diff --git a/tests/contrib/google_adk_agents/test_google_adk_agents.py b/tests/contrib/google_adk_agents/test_google_adk_agents.py new file mode 100644 index 000000000..2a1cf6aa1 --- /dev/null +++ b/tests/contrib/google_adk_agents/test_google_adk_agents.py @@ -0,0 +1,1160 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Integration tests for ADK Temporal support.""" + +import inspect +import json +import logging +import os +import uuid +from abc import ABC, abstractmethod +from collections.abc import AsyncGenerator +from datetime import timedelta +from typing import Any + +import pytest +from google.adk import Agent, Runner +from google.adk.agents import LlmAgent +from google.adk.events import Event +from google.adk.models import BaseLlm, LLMRegistry +from google.adk.models.llm_request import LlmRequest +from google.adk.models.llm_response import LlmResponse +from google.adk.runners import InMemoryRunner +from google.adk.sessions import InMemorySessionService +from google.adk.tools.mcp_tool import McpToolset +from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams +from google.adk.utils.context_utils import Aclosing +from google.genai import types +from google.genai.types import Content, FunctionCall, Part +from mcp import StdioServerParameters +from openinference.instrumentation.google_adk import GoogleADKInstrumentor +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter +from opentelemetry.trace import set_tracer_provider + +import temporalio.contrib.google_adk_agents.workflow +from temporalio import activity, workflow +from temporalio.client import Client +from temporalio.contrib.google_adk_agents import ( + GoogleAdkPlugin, + TemporalMcpToolSet, + TemporalMcpToolSetProvider, + TemporalModel, +) +from temporalio.contrib.opentelemetry import OpenTelemetryPlugin, create_tracer_provider +from temporalio.worker import Worker +from temporalio.workflow import ActivityConfig +from tests.contrib.opentelemetry.test_opentelemetry import dump_spans + +logger = logging.getLogger(__name__) + + +@activity.defn +async def get_weather(city: str) -> str: # type: ignore[reportUnusedParameter] + """Activity that gets weather for a given city.""" + return "Warm and sunny. 17 degrees." + + +def weather_agent(model_name: str) -> Agent: + # Wraps 'get_weather' activity as a Tool + weather_tool = temporalio.contrib.google_adk_agents.workflow.activity_as_tool( + get_weather, start_to_close_timeout=timedelta(seconds=60) + ) + + return Agent( + name="test_agent", + model=TemporalModel(model_name), + tools=[weather_tool], + ) + + +@workflow.defn +class WeatherAgent: + @workflow.run + async def run(self, prompt: str, model_name: str) -> Event | None: + logger.info("Workflow started.") + + # 1. Define Agent using Temporal Helpers + # Note: AgentPlugin in the Runner automatically handles Runtime setup + # and Model Activity interception. We use standard ADK models now. + agent = weather_agent(model_name) + + # 2. Create runner + runner = InMemoryRunner( + agent=agent, + app_name="test_app", + ) + + # 3. Create Session (uses runtime.new_uuid() -> workflow.uuid4()) + logger.info("Create session.") + session = await runner.session_service.create_session( + app_name="test_app", user_id="test" + ) + logger.info(f"Session created with ID: {session.id}") + + # 4. Run + logger.info("Starting runner.") + last_event = None + async with Aclosing( + runner.run_async( + user_id="test", + session_id=session.id, + new_message=types.Content(role="user", parts=[types.Part(text=prompt)]), + ) + ) as agen: + async for event in agen: + logger.info(f"Event: {event}") + last_event = event + + return last_event + + +@workflow.defn +class MultiAgentWorkflow: + @workflow.run + async def run(self, topic: str, model_name: str) -> str | None: + # 1. Setup Session Service + session_service = InMemorySessionService() + session = await session_service.create_session( + app_name="multi_agent_app", user_id="test_user" + ) + + # 2. Define Agents + # Sub-agent: Researcher + researcher = LlmAgent( + name="researcher", + model=TemporalModel( + model_name, activity_config=ActivityConfig(summary="Researcher Agent") + ), + instruction="You are a researcher. Find information about the topic.", + ) + + # Sub-agent: Writer + writer = LlmAgent( + name="writer", + model=TemporalModel( + model_name, activity_config=ActivityConfig(summary="Writer Agent") + ), + instruction="You are a poet. Write a haiku based on the research.", + ) + + # Root Agent: Coordinator + coordinator = LlmAgent( + name="coordinator", + model=TemporalModel( + model_name, + activity_config=ActivityConfig( + start_to_close_timeout=timedelta(seconds=30), + summary="Coordinator Agent", + ), + ), + instruction="You are a coordinator. Delegate to researcher then writer.", + sub_agents=[researcher, writer], + ) + + # 3. Initialize Runner with required args + runner = Runner( + agent=coordinator, + app_name="multi_agent_app", + session_service=session_service, + ) + + # 4. Run + final_content = "" + user_msg = types.Content( + role="user", + parts=[ + types.Part( + text=f"Write a haiku about {topic}. First research it, then write it." + ) + ], + ) + async for event in runner.run_async( + user_id="test_user", session_id=session.id, new_message=user_msg + ): + if ( + event.content + and event.content.parts + and event.content.parts[0].text is not None + ): + final_content = event.content.parts[0].text + + return final_content + + +class TestModel(BaseLlm, ABC): + @abstractmethod + def responses(self) -> list[LlmResponse]: + raise NotImplementedError + + @classmethod + @abstractmethod + def supported_models(cls) -> list[str]: + raise NotImplementedError + + async def generate_content_async( + self, llm_request: LlmRequest, stream: bool = False + ) -> AsyncGenerator[LlmResponse, None]: + for response in self.responses(): + if any(content == response.content for content in llm_request.contents): + continue + yield response + return + + +class WeatherModel(TestModel): + def responses(self) -> list[LlmResponse]: + return [ + LlmResponse( + content=Content( + role="model", + parts=[ + Part( + function_call=FunctionCall( + args={"city": "New York"}, name="get_weather" + ) + ) + ], + ) + ), + LlmResponse( + content=Content( + role="model", + parts=[Part(text="warm and sunny")], + ) + ), + ] + + @classmethod + def supported_models(cls) -> list[str]: + return ["weather_model"] + + +@pytest.mark.parametrize("use_local_model", [True, False]) +@pytest.mark.asyncio +async def test_single_agent(client: Client, use_local_model: bool): + if not use_local_model and not os.environ.get("GOOGLE_API_KEY"): + pytest.skip("No google API key") + + new_config = client.config() + new_config["plugins"] = [GoogleAdkPlugin()] + client = Client(**new_config) + + # Run Worker with the ADK plugin + async with Worker( + client, + task_queue="adk-task-queue", + activities=[ + get_weather, + ], + workflows=[WeatherAgent], + max_cached_workflows=0, + ): + if use_local_model: + LLMRegistry.register(WeatherModel) + + # Test Weather Agent + handle = await client.start_workflow( + WeatherAgent.run, + args=[ + "What is the weather in New York?", + "weather_model" if use_local_model else "gemini-2.5-pro", + ], + id=f"weather-agent-workflow-{uuid.uuid4()}", + task_queue="adk-task-queue", + execution_timeout=timedelta(seconds=60), + ) + result = await handle.result() + print(f"Workflow result: {result}") + if use_local_model: + assert result is not None + assert result.content is not None + assert result.content.parts is not None + assert result.content.parts[0].text == "warm and sunny" + + +class ResearchModel(TestModel): + """Scripted coordinator -> researcher -> writer flow. + + Responses are keyed off which agent is calling (via its instruction text in + the request's system instruction) rather than deduped against conversation + history, since the ADK rewrites cross-agent history between versions (e.g. + 2.4 converts prior transfer calls into "For context:" text parts). + """ + + _responses_by_instruction = { + "You are a coordinator": LlmResponse( + content=Content( + role="model", + parts=[ + Part( + function_call=FunctionCall( + args={"agent_name": "researcher"}, + name="transfer_to_agent", + ) + ) + ], + ) + ), + "You are a researcher": LlmResponse( + content=Content( + role="model", + parts=[ + Part( + function_call=FunctionCall( + args={"agent_name": "writer"}, name="transfer_to_agent" + ) + ) + ], + ) + ), + "You are a poet": LlmResponse( + content=Content( + role="model", + parts=[Part(text="haiku")], + ) + ), + } + + def responses(self) -> list[LlmResponse]: + return list(self._responses_by_instruction.values()) + + async def generate_content_async( + self, llm_request: LlmRequest, stream: bool = False + ) -> AsyncGenerator[LlmResponse, None]: + instruction = str(llm_request.config.system_instruction or "") + for phrase, response in self._responses_by_instruction.items(): + if phrase in instruction: + yield response + return + raise ValueError(f"No scripted response for instruction: {instruction!r}") + + @classmethod + def supported_models(cls) -> list[str]: + return ["research_model"] + + +@pytest.mark.parametrize("use_local_model", [True, False]) +@pytest.mark.asyncio +async def test_multi_agent(client: Client, use_local_model: bool): + if not use_local_model and not os.environ.get("GOOGLE_API_KEY"): + pytest.skip("No google API key") + + new_config = client.config() + new_config["plugins"] = [GoogleAdkPlugin()] + client = Client(**new_config) + + # Run Worker with the ADK plugin + async with Worker( + client, + task_queue="adk-task-queue-multi-agent", + workflows=[MultiAgentWorkflow], + max_cached_workflows=0, + ): + if use_local_model: + LLMRegistry.register(ResearchModel) + + # Test Multi Agent + handle = await client.start_workflow( + MultiAgentWorkflow.run, + args=[ + "Run mult-agent flow", + "research_model" if use_local_model else "gemini-2.5-pro", + ], + id=f"multi-agent-workflow-{uuid.uuid4()}", + task_queue="adk-task-queue-multi-agent", + execution_timeout=timedelta(seconds=60), + ) + result = await handle.result() + print(f"Multi-Agent Workflow result: {result}") + if use_local_model: + assert result == "haiku" + + +def example_toolset(_: Any | None) -> McpToolset: + return McpToolset( + connection_params=StdioConnectionParams( + server_params=StdioServerParameters( + command="npx", + args=[ + "-y", + "@modelcontextprotocol/server-filesystem", + os.path.dirname(os.path.abspath(__file__)), + ], + ), + ), + ) + + +def mcp_agent(model_name: str) -> Agent: + return Agent( + name="test_agent", + # instruction="Always use your tools to answer questions.", + model=TemporalModel(model_name), + tools=[TemporalMcpToolSet("test_set", not_in_workflow_toolset=example_toolset)], + ) + + +@workflow.defn +class McpAgent: + @workflow.run + async def run(self, prompt: str, model_name: str) -> str: + logger.info("Workflow started.") + + # 1. Define Agent using Temporal Helpers + agent = mcp_agent(model_name) + + # 2. Create Session (uses runtime.new_uuid() -> workflow.uuid4()) + session_service = InMemorySessionService() + logger.info("Create session.") + session = await session_service.create_session( + app_name="test_app", user_id="test" + ) + + logger.info(f"Session created with ID: {session.id}") + + # 3. Run Agent with AgentPlugin + runner = Runner( + agent=agent, + app_name="test_app", + session_service=session_service, + ) + + last_event = None + async with Aclosing( + runner.run_async( + user_id="test", + session_id=session.id, + new_message=types.Content(role="user", parts=[types.Part(text=prompt)]), + ) + ) as agen: + async for event in agen: + logger.info(f"Event: {event}") + last_event = event + + assert last_event + assert last_event.content + assert last_event.content.parts + assert last_event.content.parts[0].text + return last_event.content.parts[0].text + + +class McpModel(TestModel): + def responses(self) -> list[LlmResponse]: + return [ + LlmResponse( + content=Content( + role="model", + parts=[ + Part( + function_call=FunctionCall( + args={ + "path": os.path.dirname(os.path.abspath(__file__)) + }, + name="list_directory", + ) + ) + ], + ) + ), + LlmResponse( + content=Content( + role="model", + parts=[Part(text="Some files.")], + ) + ), + ] + + @classmethod + def supported_models(cls) -> list[str]: + return ["mcp_model"] + + +@pytest.mark.parametrize("use_local_model", [True, False]) +@pytest.mark.asyncio +@pytest.mark.skip # Doesn't work well in CI currently +async def test_mcp_agent(client: Client, use_local_model: bool): + if not use_local_model and not os.environ.get("GOOGLE_API_KEY"): + pytest.skip("No google API key") + + new_config = client.config() + new_config["plugins"] = [ + GoogleAdkPlugin( + toolset_providers=[ + TemporalMcpToolSetProvider( + "test_set", + example_toolset, + ) + ], + ) + ] + client = Client(**new_config) + + # Run Worker with the ADK plugin + async with Worker( + client, + task_queue="adk-task-queue-mcp", + workflows=[McpAgent], + max_cached_workflows=0, + ): + if use_local_model: + LLMRegistry.register(McpModel) + + # Test Multi Agent + handle = await client.start_workflow( + McpAgent.run, + args=[ + "What files are in the current directory?", + "mcp_model" if use_local_model else "gemini-2.5-pro", + ], + id=f"mcp-agent-workflow-{uuid.uuid4()}", + task_queue="adk-task-queue-mcp", + execution_timeout=timedelta(seconds=60), + ) + result = await handle.result() + print(f"MCP-Agent Workflow result: {result}") + if use_local_model: + assert result == "Some files." + + +@pytest.mark.asyncio +async def test_single_agent_telemetry( + client: Client, + reset_otel_tracer_provider, # type: ignore[reportUnusedParameter] +): + exporter = InMemorySpanExporter() + provider = create_tracer_provider() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + set_tracer_provider(provider) + GoogleADKInstrumentor().instrument() + + new_config = client.config() + new_config["plugins"] = [ + GoogleAdkPlugin(), + OpenTelemetryPlugin(add_temporal_spans=True), + ] + client = Client(**new_config) + + # Run Worker with the ADK plugin + async with Worker( + client, + task_queue="adk-task-queue-telemetry", + activities=[ + get_weather, + ], + workflows=[WeatherAgent], + max_cached_workflows=0, + ): + LLMRegistry.register(WeatherModel) + + # Test Weather Agent + handle = await client.start_workflow( + WeatherAgent.run, + args=[ + "What is the weather in New York?", + "weather_model", + ], + id=f"weather-agent-telemetry-workflow-{uuid.uuid4()}", + task_queue="adk-task-queue-telemetry", + execution_timeout=timedelta(seconds=60), + ) + result = await handle.result() + print(f"Workflow result: {result}") + + assert result is not None + assert result.content is not None + assert result.content.parts is not None + assert result.content.parts[0].text == "warm and sunny" + + print("\n".join(dump_spans(exporter.get_finished_spans(), with_attributes=False))) + assert dump_spans(exporter.get_finished_spans(), with_attributes=False) == [ + "StartWorkflow:WeatherAgent", + " RunWorkflow:WeatherAgent", + " invocation [test_app]", + " agent_run [test_agent]", + " call_llm", + " StartActivity:invoke_model", + " RunActivity:invoke_model", + " execute_tool get_weather", + " StartActivity:get_weather", + " RunActivity:get_weather", + " call_llm", + " StartActivity:invoke_model", + " RunActivity:invoke_model", + ] + + +async def test_unsetting_timeout(): + model = TemporalModel("", ActivityConfig(start_to_close_timeout=None)) + assert model._activity_config.get("start_to_close_timeout", None) is None + + +class SummaryFnModel(TestModel): + """Returns a single text response for summary_fn testing.""" + + def responses(self) -> list[LlmResponse]: + return [ + LlmResponse(content=Content(role="model", parts=[Part(text="response")])), + ] + + @classmethod + def supported_models(cls) -> list[str]: + return ["summary_fn_model"] + + +@workflow.defn +class SummaryTestWorkflow: + @workflow.run + async def run(self, model_name: str) -> None: + modes = [ + ("dynamic", lambda req: f"Invoking {req.model}"), + ("none", lambda req: None), + ("empty", lambda req: ""), + ("label_fallback", None), + ] + for mode_name, summary_fn in modes: + agent = Agent( + name=f"summary_test_{mode_name}", + model=TemporalModel(model_name, summary_fn=summary_fn), + ) + runner = InMemoryRunner(agent=agent, app_name=f"summary_{mode_name}") + session = await runner.session_service.create_session( + app_name=f"summary_{mode_name}", user_id="test" + ) + async with Aclosing( + runner.run_async( + user_id="test", + session_id=session.id, + new_message=types.Content( + role="user", parts=[types.Part(text="hi")] + ), + ) + ) as agen: + async for _ in agen: + pass + + +@pytest.mark.asyncio +async def test_summary_fn_variants(client: Client): + """Test summary_fn with dynamic, None, empty string, and label fallback.""" + new_config = client.config() + new_config["plugins"] = [GoogleAdkPlugin()] + client = Client(**new_config) + LLMRegistry.register(SummaryFnModel) + + async with Worker( + client, + task_queue="adk-summary-test", + workflows=[SummaryTestWorkflow], + max_cached_workflows=0, + ): + handle = await client.start_workflow( + SummaryTestWorkflow.run, + "summary_fn_model", + id=f"summary-test-{uuid.uuid4()}", + task_queue="adk-summary-test", + execution_timeout=timedelta(seconds=60), + ) + await handle.result() + + summaries = [] + async for e in handle.fetch_history_events(): + if e.HasField("activity_task_scheduled_event_attributes"): + attrs = e.activity_task_scheduled_event_attributes + if attrs.activity_type.name == "invoke_model": + summaries.append(e.user_metadata.summary.data) + + assert len(summaries) == 4 + assert summaries[0] == b'"Invoking summary_fn_model"' # dynamic + assert summaries[1] == b"" # none + assert summaries[2] == b"" # empty + assert ( + summaries[3] == b'"summary_test_label_fallback"' + ) # label fallback agent name + + +def test_summary_and_summary_fn_raises(): + """Cannot specify both summary and summary_fn.""" + with pytest.raises( + ValueError, + match="Cannot specify both ActivityConfig 'summary' and 'summary_fn'", + ): + TemporalModel( + "m", + activity_config=ActivityConfig(summary="static"), + summary_fn=lambda req: "dynamic", + ) + + +@pytest.mark.asyncio +async def test_agent_outside_workflow(): + """Test that an agent using TemporalModel and activity_as_tool works outside a Temporal workflow.""" + LLMRegistry.register(WeatherModel) + + agent = weather_agent("weather_model") + + runner = InMemoryRunner( + agent=agent, + app_name="test_app_local", + ) + + session = await runner.session_service.create_session( + app_name="test_app_local", user_id="test" + ) + + last_event = None + async with Aclosing( + runner.run_async( + user_id="test", + session_id=session.id, + new_message=types.Content( + role="user", parts=[types.Part(text="What is the weather in New York?")] + ), + ) + ) as agen: + async for event in agen: + last_event = event + + assert last_event is not None + assert last_event.content is not None + assert last_event.content.parts is not None + assert last_event.content.parts[0].text == "warm and sunny" + + +@pytest.mark.asyncio +@pytest.mark.skip # Doesn't work well in CI currently +async def test_mcp_agent_outside_workflow(): + """Test that an agent using TemporalMcpToolSet works outside a Temporal workflow.""" + LLMRegistry.register(McpModel) + + agent = mcp_agent("mcp_model") + + session_service = InMemorySessionService() + session = await session_service.create_session( + app_name="test_app_local", user_id="test" + ) + + runner = Runner( + agent=agent, + app_name="test_app_local", + session_service=session_service, + ) + + last_event = None + async with Aclosing( + runner.run_async( + user_id="test", + session_id=session.id, + new_message=types.Content( + role="user", + parts=[types.Part(text="What files are in the current directory?")], + ), + ) + ) as agen: + async for event in agen: + last_event = event + + assert last_event is not None + assert last_event.content is not None + assert last_event.content.parts is not None + assert last_event.content.parts[0].text == "Some files." + + +@pytest.mark.asyncio +async def test_mcp_toolset_outside_workflow_no_not_in_workflow_toolset(): + """Test that TemporalMcpToolSet raises ValueError outside a workflow with no not_in_workflow_toolset.""" + toolset = TemporalMcpToolSet("test_set_no_local") + with pytest.raises( + ValueError, + match="not_in_workflow_toolset", + ): + await toolset.get_tools() + + +complex_activity_inputs_seen: dict[str, object] = {} + + +@activity.defn +async def book_trip(origin: str, destination: str, passengers: int) -> str: + """Activity that formats multiple discrete arguments.""" + complex_activity_inputs_seen["book_trip"] = (origin, destination, passengers) + return f"{origin}->{destination}:{passengers}" + + +@activity.defn +async def summarize_payload( + name: str, metadata: dict[str, str | int | list[str]] +) -> str: + """Activity that formats compound map input.""" + complex_activity_inputs_seen["summarize_payload"] = (name, metadata) + tags = metadata.get("tags", []) + assert isinstance(tags, list) + return f"{name}:{metadata['count']}:{metadata['owner']}:" + ",".join( + str(tag) for tag in tags + ) + + +class ComplexActivityMethodHolder: + def __init__(self, prefix: str) -> None: + self.prefix = prefix + + @activity.defn + async def annotate_trip(self, trip: str) -> str: + complex_activity_inputs_seen["annotate_trip"] = trip + return f"{self.prefix}:{trip}" + + +@workflow.defn +class ComplexActivityInputAgent: + @workflow.run + async def run(self, prompt: str, model_name: str) -> str: + logger.info("Workflow started.") + method_holder = ComplexActivityMethodHolder("method") + + agent = Agent( + name="complex_input_agent", + model=TemporalModel(model_name), + tools=[ + temporalio.contrib.google_adk_agents.workflow.activity_as_tool( + book_trip, start_to_close_timeout=timedelta(seconds=60) + ), + temporalio.contrib.google_adk_agents.workflow.activity_as_tool( + summarize_payload, start_to_close_timeout=timedelta(seconds=60) + ), + temporalio.contrib.google_adk_agents.workflow.activity_as_tool( + method_holder.annotate_trip, + start_to_close_timeout=timedelta(seconds=60), + ), + ], + ) + + runner = InMemoryRunner( + agent=agent, + app_name="complex_input_app", + ) + + session = await runner.session_service.create_session( + app_name="complex_input_app", user_id="test" + ) + + final_text = "" + async with Aclosing( + runner.run_async( + user_id="test", + session_id=session.id, + new_message=types.Content(role="user", parts=[types.Part(text=prompt)]), + ) + ) as agen: + async for event in agen: + logger.info(f"Event: {event}") + if event.content and event.content.parts: + for part in event.content.parts: + if part.text is not None: + final_text = part.text + + return final_text + + +class ComplexActivityInputModel(TestModel): + def responses(self) -> list[LlmResponse]: + return [ + LlmResponse( + content=Content( + role="model", + parts=[ + Part( + function_call=FunctionCall( + name="book_trip", + args={ + "origin": "SFO", + "destination": "LAX", + "passengers": 3, + }, + ) + ) + ], + ) + ), + LlmResponse( + content=Content( + role="model", + parts=[ + Part( + function_call=FunctionCall( + name="summarize_payload", + args={ + "name": "fixture", + "metadata": { + "count": 2, + "owner": "team-a", + "tags": ["alpha", "beta"], + }, + }, + ) + ) + ], + ) + ), + LlmResponse( + content=Content( + role="model", + parts=[ + Part( + function_call=FunctionCall( + name="annotate_trip", + args={"trip": "SFO->LAX:3"}, + ) + ) + ], + ) + ), + LlmResponse( + content=Content( + role="model", + parts=[Part(text="completed complex input tool calls")], + ) + ), + ] + + @classmethod + def supported_models(cls) -> list[str]: + return ["complex_activity_input_model"] + + +@pytest.mark.asyncio +async def test_activity_as_tool_supports_complex_inputs_via_adk(client: Client): + new_config = client.config() + new_config["plugins"] = [GoogleAdkPlugin()] + client = Client(**new_config) + complex_activity_inputs_seen.clear() + method_holder = ComplexActivityMethodHolder("method") + + async with Worker( + client, + task_queue="adk-task-queue-complex-inputs", + activities=[ + book_trip, + summarize_payload, + method_holder.annotate_trip, + ], + workflows=[ComplexActivityInputAgent], + max_cached_workflows=0, + ): + LLMRegistry.register(ComplexActivityInputModel) + + handle = await client.start_workflow( + ComplexActivityInputAgent.run, + args=[ + "Run every registered tool using structured inputs.", + "complex_activity_input_model", + ], + id=f"complex-activity-input-workflow-{uuid.uuid4()}", + task_queue="adk-task-queue-complex-inputs", + execution_timeout=timedelta(seconds=60), + ) + result = await handle.result() + assert result == "completed complex input tool calls" + assert complex_activity_inputs_seen == { + "book_trip": ("SFO", "LAX", 3), + "summarize_payload": ( + "fixture", + {"count": 2, "owner": "team-a", "tags": ["alpha", "beta"]}, + ), + "annotate_trip": "SFO->LAX:3", + } + + +def litellm_agent(model_name: str) -> Agent: + return Agent( + name="litellm_test_agent", + model=TemporalModel(model_name), + ) + + +@workflow.defn +class LiteLlmWorkflow: + @workflow.run + async def run(self, prompt: str, model_name: str) -> Event | None: + agent = litellm_agent(model_name) + + runner = InMemoryRunner( + agent=agent, + app_name="litellm_test_app", + ) + + session = await runner.session_service.create_session( + app_name="litellm_test_app", user_id="test" + ) + + last_event = None + async with Aclosing( + runner.run_async( + user_id="test", + session_id=session.id, + new_message=types.Content(role="user", parts=[types.Part(text=prompt)]), + ) + ) as agen: + async for event in agen: + last_event = event + + return last_event + + +@pytest.mark.asyncio +async def test_litellm_model(client: Client): + """Test that a litellm-backed model works with TemporalModel through a full Temporal workflow.""" + import litellm as litellm_module + from google.adk.models.lite_llm import LiteLlm + from litellm import ModelResponse + from litellm.llms.custom_llm import CustomLLM + + class FakeLiteLlmProvider(CustomLLM): + """A fake litellm provider that returns canned responses locally.""" + + def _make_response(self, model: str) -> ModelResponse: + return ModelResponse( + choices=[ + { + "message": { + "content": "hello from litellm", + "role": "assistant", + }, + "index": 0, + "finish_reason": "stop", + } + ], + model=model, + ) + + def completion(self, *args: Any, **kwargs: Any) -> ModelResponse: + model = args[0] if args else kwargs.get("model", "unknown") + return self._make_response(model) + + async def acompletion(self, *args: Any, **kwargs: Any) -> ModelResponse: + return self.completion(*args, **kwargs) + + class FakeLiteLlm(LiteLlm): + """LiteLlm subclass that supports the fake/test-model name for testing.""" + + @classmethod + def supported_models(cls) -> list[str]: + return ["fake/test-model"] + + # Register our fake provider with litellm + litellm_module.custom_provider_map = [ + {"provider": "fake", "custom_handler": FakeLiteLlmProvider()} + ] + + LLMRegistry.register(FakeLiteLlm) + + new_config = client.config() + new_config["plugins"] = [GoogleAdkPlugin()] + client = Client(**new_config) + + async with Worker( + client, + task_queue="adk-task-queue-litellm", + workflows=[LiteLlmWorkflow], + max_cached_workflows=0, + ): + handle = await client.start_workflow( + LiteLlmWorkflow.run, + args=["Say hello", "fake/test-model"], + id=f"litellm-agent-workflow-{uuid.uuid4()}", + task_queue="adk-task-queue-litellm", + execution_timeout=timedelta(seconds=60), + ) + result = await handle.result() + + assert result is not None + assert result.content is not None + assert result.content.parts is not None + assert result.content.parts[0].text == "hello from litellm" + + +def test_unset_none_fields_stripped() -> None: + """ADK plugin converter strips unset None fields from Pydantic payloads.""" + plugin = GoogleAdkPlugin() + converter = plugin._configure_data_converter(None) + request = LlmRequest( + model="gemini-2.0-flash", + contents=[Content(parts=[Part(text="hello")])], + ) + payloads = converter.payload_converter.to_payloads([request]) + serialized = json.loads(payloads[0].data) + + assert serialized["model"] == "gemini-2.0-flash" + assert "contents" in serialized + for field in ( + "cache_config", + "cache_metadata", + "cacheable_contents_token_count", + "previous_interaction_id", + ): + assert field not in serialized, f"Unset field {field!r} should be stripped" + + +def test_explicitly_set_none_preserved() -> None: + """Explicitly-set None is preserved (exclude_unset, not exclude_none).""" + plugin = GoogleAdkPlugin() + converter = plugin._configure_data_converter(None) + request = LlmRequest( + model="gemini-2.0-flash", + contents=[Content(parts=[Part(text="hello")])], + cache_config=None, + ) + payloads = converter.payload_converter.to_payloads([request]) + serialized = json.loads(payloads[0].data) + + assert "cache_config" in serialized, "Explicitly-set None should be preserved" + assert serialized["cache_config"] is None + + +def test_activity_as_tool_preserves_metadata() -> None: + """activity_as_tool wrapper preserves the original function's metadata. + + This ensures ADK's tool schema generation can inspect __annotations__ + and __module__ on the wrapper, which are needed by + ``_handle_params_as_deferred_annotations`` to resolve type hints. + """ + + @activity.defn + async def my_activity(city: str, count: int = 1) -> str: + """Get info for a city.""" + return f"{city}: {count}" + + tool = temporalio.contrib.google_adk_agents.workflow.activity_as_tool( + my_activity, start_to_close_timeout=timedelta(seconds=30) + ) + + # __name__ and __doc__ + assert tool.__name__ == "my_activity" + assert tool.__doc__ == "Get info for a city." + + # __annotations__ — critical for ADK type introspection + assert "city" in tool.__annotations__ + assert tool.__annotations__["city"] is str + assert tool.__annotations__["count"] is int + assert tool.__annotations__["return"] is str + + # __module__ — needed by _handle_params_as_deferred_annotations + assert tool.__module__ == my_activity.__module__ + + # __signature__ — must match the original, not *args/**kw + sig = inspect.signature(tool) + params = list(sig.parameters.keys()) + assert params == ["city", "count"] + assert sig.parameters["city"].annotation is str + assert sig.parameters["count"].default == 1 diff --git a/tests/contrib/google_adk_agents/test_google_adk_agents_replay.py b/tests/contrib/google_adk_agents/test_google_adk_agents_replay.py new file mode 100644 index 000000000..810ca95f0 --- /dev/null +++ b/tests/contrib/google_adk_agents/test_google_adk_agents_replay.py @@ -0,0 +1,33 @@ +from pathlib import Path + +import pytest +from google.adk.models import LLMRegistry + +from temporalio.client import WorkflowHistory +from temporalio.contrib.google_adk_agents import GoogleAdkPlugin +from temporalio.worker import Replayer +from tests.contrib.google_adk_agents.test_google_adk_agents import ( + MultiAgentWorkflow, + ResearchModel, + WeatherAgent, + WeatherModel, +) + + +@pytest.mark.parametrize( + "file_name", + [ + "multi_agent.json", + "single_agent.json", + ], +) +async def test_replay(file_name: str) -> None: + with (Path(__file__).with_name("histories") / file_name).open("r") as f: + history_json = f.read() + + LLMRegistry.register(ResearchModel) + LLMRegistry.register(WeatherModel) + await Replayer( + workflows=[MultiAgentWorkflow, WeatherAgent], + plugins=[GoogleAdkPlugin()], + ).replay_workflow(WorkflowHistory.from_json("fake", history_json)) diff --git a/tests/contrib/google_adk_agents/test_mcp.py b/tests/contrib/google_adk_agents/test_mcp.py new file mode 100644 index 000000000..38303ffee --- /dev/null +++ b/tests/contrib/google_adk_agents/test_mcp.py @@ -0,0 +1,228 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for the stateless MCP toolset support in +``temporalio.contrib.google_adk_agents._mcp``. + +These exercise ``TemporalMcpToolSetProvider``'s generated activities directly +against a fake ``McpToolset``, so they don't require a real MCP server, a +Temporal test server, or a full ``Worker``. They are targeted regression tests +for the connection-leak fix (issue #1663): every call builds its own toolset, +honors that call's ``factory_argument``, and always closes the toolset. +""" + +from typing import Any, cast + +import pytest +from google.adk.events import EventActions +from google.adk.tools.mcp_tool import McpToolset + +from temporalio.contrib.google_adk_agents._mcp import ( + TemporalMcpToolSetProvider, + TemporalToolContext, + _CallToolArguments, + _GetToolsArguments, +) + + +class _FakeTool: + def __init__(self, name: str, *, fail_run: bool = False) -> None: + self.name = name + self.description = "a fake tool" + self.is_long_running = False + self.custom_metadata: dict[str, Any] | None = None + self._fail_run = fail_run + + def _get_declaration(self) -> None: + return None + + async def run_async( + self, + *, + args: dict[str, Any], + tool_context: Any, # pyright: ignore[reportUnusedParameter] + ) -> Any: + if self._fail_run: + raise RuntimeError("tool call failed") + return {"echo": args} + + +class _FakeToolset: + """Stands in for ``google.adk.tools.mcp_tool.McpToolset``. + + Tracks whether ``close()`` was called so tests can assert the stateless + path always tears the toolset down, and records the ``factory_argument`` + it was created with so tests can prove each call routes with its own + argument. + """ + + def __init__( + self, + factory_argument: Any = None, + *, + fail_get_tools: bool = False, + fail_run: bool = False, + ) -> None: + self.factory_argument = factory_argument + self.closed = False + self._fail_get_tools = fail_get_tools + self._fail_run = fail_run + + async def get_tools(self) -> list[_FakeTool]: + if self._fail_get_tools: + raise RuntimeError("get_tools failed") + return [_FakeTool("echo", fail_run=self._fail_run)] + + async def close(self) -> None: + self.closed = True + + +def _tool_context() -> TemporalToolContext: + return TemporalToolContext( + tool_confirmation=None, + function_call_id=None, + event_actions=EventActions(), + ) + + +def _call_tool_args(factory_argument: Any = None) -> _CallToolArguments: + return _CallToolArguments( + factory_argument=factory_argument, + name="echo", + arguments={"x": 1}, + tool_context=_tool_context(), + ) + + +async def test_call_tool_creates_and_closes_fresh_toolset_each_call(): + """Each call_tool builds its own toolset and closes it, every time.""" + created: list[_FakeToolset] = [] + + def factory(arg: Any) -> McpToolset: + toolset = _FakeToolset(arg) + created.append(toolset) + return cast(McpToolset, cast(object, toolset)) + + provider = TemporalMcpToolSetProvider("stateless_reuse", factory) + _, call_tool = provider._get_activities() + + for _ in range(5): + result = await call_tool(_call_tool_args()) + assert result.result == {"echo": {"x": 1}} + + # One fresh toolset per call, and every one was closed. + assert len(created) == 5 + assert all(t.closed for t in created) + + +async def test_get_tools_creates_and_closes_fresh_toolset(): + created: list[_FakeToolset] = [] + + def factory(arg: Any) -> McpToolset: + toolset = _FakeToolset(arg) + created.append(toolset) + return cast(McpToolset, cast(object, toolset)) + + provider = TemporalMcpToolSetProvider("stateless_list", factory) + get_tools, _ = provider._get_activities() + + tools = await get_tools(_GetToolsArguments(factory_argument=None)) + assert [t.name for t in tools] == ["echo"] + + assert len(created) == 1 + assert created[0].closed + + +async def test_factory_argument_honored_on_every_call(): + """The bug the reviewer flagged: a later call with a different + ``factory_argument`` must route with *its own* argument, never silently + reuse a connection opened for an earlier argument. + """ + created: list[_FakeToolset] = [] + + def factory(arg: Any) -> McpToolset: + toolset = _FakeToolset(arg) + created.append(toolset) + return cast(McpToolset, cast(object, toolset)) + + provider = TemporalMcpToolSetProvider("stateless_routing", factory) + _, call_tool = provider._get_activities() + + await call_tool(_call_tool_args(factory_argument="tenant-a")) + await call_tool(_call_tool_args(factory_argument="tenant-b")) + + assert [t.factory_argument for t in created] == ["tenant-a", "tenant-b"] + assert all(t.closed for t in created) + + +async def test_call_tool_closes_toolset_on_error(): + """A failure mid-call still closes the toolset (no leak on the error path).""" + created: list[_FakeToolset] = [] + + def factory(arg: Any) -> McpToolset: + toolset = _FakeToolset(arg, fail_run=True) + created.append(toolset) + return cast(McpToolset, cast(object, toolset)) + + provider = TemporalMcpToolSetProvider("stateless_run_error", factory) + _, call_tool = provider._get_activities() + + with pytest.raises(RuntimeError, match="tool call failed"): + await call_tool(_call_tool_args()) + + assert len(created) == 1 + assert created[0].closed + + +async def test_get_tools_closes_toolset_on_error(): + created: list[_FakeToolset] = [] + + def factory(arg: Any) -> McpToolset: + toolset = _FakeToolset(arg, fail_get_tools=True) + created.append(toolset) + return cast(McpToolset, cast(object, toolset)) + + provider = TemporalMcpToolSetProvider("stateless_list_error", factory) + get_tools, _ = provider._get_activities() + + with pytest.raises(RuntimeError, match="get_tools failed"): + await get_tools(_GetToolsArguments(factory_argument=None)) + + assert len(created) == 1 + assert created[0].closed + + +async def test_call_tool_no_matching_tool_still_closes(): + """A business-logic ApplicationError still closes the fresh toolset.""" + created: list[_FakeToolset] = [] + + def factory(arg: Any) -> McpToolset: + toolset = _FakeToolset(arg) + created.append(toolset) + return cast(McpToolset, cast(object, toolset)) + + provider = TemporalMcpToolSetProvider("stateless_no_match", factory) + _, call_tool = provider._get_activities() + + args = _CallToolArguments( + factory_argument=None, + name="does_not_exist", + arguments={}, + tool_context=_tool_context(), + ) + with pytest.raises(Exception, match="Unable to find matching mcp tool"): + await call_tool(args) + + assert len(created) == 1 + assert created[0].closed diff --git a/tests/contrib/google_adk_agents/test_stateful_mcp.py b/tests/contrib/google_adk_agents/test_stateful_mcp.py new file mode 100644 index 000000000..994598df9 --- /dev/null +++ b/tests/contrib/google_adk_agents/test_stateful_mcp.py @@ -0,0 +1,194 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Integration tests for the stateful (pooled) MCP toolset support in +``temporalio.contrib.google_adk_agents._mcp``. + +These drive the real ``connect`` -> dedicated-worker -> ``get_tools`` path +through a Temporal workflow, but against an in-memory fake ``McpToolset`` (no +subprocess, no real MCP server) so they run in CI. They prove the properties +the reviewer asked for: one toolset is built per workflow run, ``factory_argument`` +is consumed exactly once per run, distinct runs never share a toolset, and the +toolset is torn down when the workflow completes. +""" + +import uuid +from datetime import timedelta +from typing import Any, cast + +from google.adk.tools.mcp_tool import McpToolset + +from temporalio import workflow +from temporalio.client import Client +from temporalio.worker import Worker + +with workflow.unsafe.imports_passed_through(): + from temporalio.contrib.google_adk_agents import ( + GoogleAdkPlugin, + TemporalStatefulMcpToolSet, + TemporalStatefulMcpToolSetProvider, + ) + +# Populated inside the activity worker process (same process as the test) each +# time the toolset factory runs, so tests can inspect what was built. +CREATED: list["_FakeToolset"] = [] + + +class _FakeTool: + def __init__(self, name: str) -> None: + self.name = name + self.description = "a fake tool" + self.is_long_running = False + self.custom_metadata: dict[str, Any] | None = None + + def _get_declaration(self) -> None: + return None + + async def run_async( + self, + *, + args: dict[str, Any], + tool_context: Any, # pyright: ignore[reportUnusedParameter] + ) -> Any: + return {"echo": args} + + +class _FakeToolset: + """In-memory stand-in for ``google.adk.tools.mcp_tool.McpToolset``.""" + + def __init__(self, factory_argument: Any = None) -> None: + self.factory_argument = factory_argument + self.get_tools_calls = 0 + self.closed = False + + async def get_tools(self) -> list[_FakeTool]: + self.get_tools_calls += 1 + return [_FakeTool("echo")] + + async def close(self) -> None: + self.closed = True + + +def _factory(arg: Any) -> McpToolset: + toolset = _FakeToolset(arg) + CREATED.append(toolset) + return cast(McpToolset, cast(object, toolset)) + + +@workflow.defn +class StatefulMcpWorkflow: + @workflow.run + async def run(self, factory_argument: Any | None) -> list[str]: + async with TemporalStatefulMcpToolSet( + "stateful_set", + factory_argument=factory_argument, + ) as toolset: + # Two calls against the same persistent toolset -- exercises reuse. + tools_1 = await toolset.get_tools() + tools_2 = await toolset.get_tools() + return [t.name for t in tools_1] + [t.name for t in tools_2] + + +def _make_client( + client: Client, provider: TemporalStatefulMcpToolSetProvider +) -> Client: + new_config = client.config() + new_config["plugins"] = [GoogleAdkPlugin(toolset_providers=[provider])] + return Client(**new_config) + + +async def test_stateful_one_toolset_per_run_and_teardown(client: Client): + CREATED.clear() + provider = TemporalStatefulMcpToolSetProvider("stateful_set", _factory) + client = _make_client(client, provider) + + async with Worker( + client, + task_queue="adk-stateful-mcp", + workflows=[StatefulMcpWorkflow], + max_cached_workflows=0, + ): + result = await client.execute_workflow( + StatefulMcpWorkflow.run, + None, + id=f"stateful-mcp-{uuid.uuid4()}", + task_queue="adk-stateful-mcp", + execution_timeout=timedelta(seconds=60), + ) + + assert result == ["echo", "echo"] + # Exactly one toolset built for the whole run... + assert len(CREATED) == 1 + # ...reused across both get_tools calls (state maintained, not per-call)... + assert CREATED[0].get_tools_calls == 2 + # ...and closed when the workflow completed (no leak). + assert CREATED[0].closed + # The dedicated-worker toolset registry is empty after teardown. + assert provider._toolsets == {} + + +async def test_stateful_factory_argument_consumed_once(client: Client): + CREATED.clear() + provider = TemporalStatefulMcpToolSetProvider("stateful_set", _factory) + client = _make_client(client, provider) + + async with Worker( + client, + task_queue="adk-stateful-mcp-arg", + workflows=[StatefulMcpWorkflow], + max_cached_workflows=0, + ): + await client.execute_workflow( + StatefulMcpWorkflow.run, + {"tenant": "acme"}, + id=f"stateful-mcp-{uuid.uuid4()}", + task_queue="adk-stateful-mcp-arg", + execution_timeout=timedelta(seconds=60), + ) + + assert len(CREATED) == 1 + assert CREATED[0].factory_argument == {"tenant": "acme"} + + +async def test_stateful_no_cross_run_sharing(client: Client): + CREATED.clear() + provider = TemporalStatefulMcpToolSetProvider("stateful_set", _factory) + client = _make_client(client, provider) + + async with Worker( + client, + task_queue="adk-stateful-mcp-iso", + workflows=[StatefulMcpWorkflow], + max_cached_workflows=0, + ): + await client.execute_workflow( + StatefulMcpWorkflow.run, + "tenant-a", + id=f"stateful-mcp-a-{uuid.uuid4()}", + task_queue="adk-stateful-mcp-iso", + execution_timeout=timedelta(seconds=60), + ) + await client.execute_workflow( + StatefulMcpWorkflow.run, + "tenant-b", + id=f"stateful-mcp-b-{uuid.uuid4()}", + task_queue="adk-stateful-mcp-iso", + execution_timeout=timedelta(seconds=60), + ) + + # Two distinct runs -> two distinct toolsets, each with its own argument. + assert len(CREATED) == 2 + assert {t.factory_argument for t in CREATED} == {"tenant-a", "tenant-b"} + assert all(t.closed for t in CREATED) + assert provider._toolsets == {} diff --git a/tests/contrib/google_genai/__init__.py b/tests/contrib/google_genai/__init__.py new file mode 100644 index 000000000..26a790b56 --- /dev/null +++ b/tests/contrib/google_genai/__init__.py @@ -0,0 +1 @@ +"""Tests for the `google-genai` SDK Temporal integration.""" diff --git a/tests/contrib/google_genai/echo_mcp_server.py b/tests/contrib/google_genai/echo_mcp_server.py new file mode 100644 index 000000000..a69ac7b22 --- /dev/null +++ b/tests/contrib/google_genai/echo_mcp_server.py @@ -0,0 +1,15 @@ +"""A minimal stdio MCP server used by the google_genai MCP tests.""" + +from mcp.server.fastmcp import FastMCP + +mcp = FastMCP("echo-server") + + +@mcp.tool() +def echo(message: str) -> str: + """Return the input message unchanged.""" + return message + + +if __name__ == "__main__": + mcp.run() diff --git a/tests/contrib/google_genai/test_gemini.py b/tests/contrib/google_genai/test_gemini.py new file mode 100644 index 000000000..0881f63b3 --- /dev/null +++ b/tests/contrib/google_genai/test_gemini.py @@ -0,0 +1,2052 @@ +"""Integration tests for the Google Gemini SDK Temporal integration. + +Tests cover: +- Basic generate_content through workflow +- Tool calling via activity_as_tool (single arg, multi arg, class method) +- Workflow method as a plain tool (runs in-workflow, not as an activity) +- Tool failure propagation +- Multiple sequential tool calls with arg verification +- Batched streaming via generate_content_stream +- Per-request http_options propagation +- File upload (str path + io.BytesIO) and download via TemporalAsyncFiles +- File search store upload via TemporalAsyncFileSearchStores +- Multi-turn chat via client.chats +- TemporalAsyncClient wiring (files, file_search_stores) +- _TemporalApiClient edge cases (sync raises) +- activity_as_tool validation and metadata preservation +- TemporalAsyncClient configuration +- Interactions API (create, batched streaming, get, cancel, delete) +- Managed agents (create, get, list, delete); webhooks unsupported +""" + +import inspect +import io +import json +import uuid +from datetime import timedelta +from typing import Any, Callable, cast +from unittest.mock import AsyncMock, MagicMock + +import pytest +from google.genai import Client as GeminiClient +from google.genai import types +from google.genai.interactions import ( + Agent, # pyright: ignore[reportPrivateImportUsage] + InteractionSSEEvent, +) +from google.genai.types import HttpResponse as SdkHttpResponse + +from temporalio import activity, workflow +from temporalio.client import Client, WorkflowFailureError +from temporalio.common import RetryPolicy +from temporalio.contrib.google_genai import ( + GoogleGenAIError, + GoogleGenAIPlugin, + activity_as_tool, +) +from temporalio.contrib.google_genai._compat import Interaction +from temporalio.contrib.google_genai._models import ( + _GeminiApiRequest, + _GeminiApiResponse, + _GeminiApiStreamedResponse, + _GeminiDownloadFileRequest, + _GeminiInteractionIdRequest, + _GeminiInteractionRequest, + _GeminiInteractionStreamedResponse, + _GeminiUploadFileRequest, + _GeminiUploadToFileSearchStoreRequest, +) +from temporalio.contrib.google_genai._temporal_api_client import ( + _TemporalApiClient, +) +from temporalio.contrib.google_genai._temporal_async_client import ( + TemporalAsyncClient, + _closure_if_bound_method, + _wrap_bound_method_tools, +) +from temporalio.contrib.google_genai._temporal_file_search_stores import ( + TemporalAsyncFileSearchStores, +) +from temporalio.contrib.google_genai._temporal_files import ( + TemporalAsyncFiles, +) +from temporalio.contrib.google_genai._temporal_interactions import _deserialize +from temporalio.exceptions import ApplicationError +from temporalio.worker import Replayer +from temporalio.workflow import ActivityConfig +from tests.helpers import new_worker + +# --------------------------------------------------------------------------- +# Mock response helpers +# --------------------------------------------------------------------------- + + +def make_text_response(text: str) -> str: + """Build a JSON body string for a simple text response.""" + return json.dumps( + { + "candidates": [ + { + "content": { + "role": "model", + "parts": [{"text": text}], + }, + "finishReason": "STOP", + } + ], + "usageMetadata": { + "promptTokenCount": 5, + "candidatesTokenCount": 10, + }, + } + ) + + +def make_function_call_response(fn_name: str, args: dict) -> str: + """Build a JSON body string for a function-call response.""" + return json.dumps( + { + "candidates": [ + { + "content": { + "role": "model", + "parts": [{"functionCall": {"name": fn_name, "args": args}}], + }, + "finishReason": "STOP", + } + ], + "usageMetadata": { + "promptTokenCount": 10, + "candidatesTokenCount": 15, + }, + } + ) + + +INTERACTION_ID = "interactions/test-123" + + +def make_interaction_dict(status: str = "completed") -> dict[str, Any]: + """Build a minimal Interaction dict as the API would return it.""" + return {"id": INTERACTION_ID, "object": "interaction", "status": status} + + +def make_interaction_sse_events() -> list[dict[str, Any]]: + """Build a small SSE event sequence for a streamed interaction. + + Includes a sparse ``interaction.created`` payload (just ``id`` and + ``object``) to exercise the lenient ``_deserialize`` rehydration. + """ + return [ + { + "event_type": "interaction.created", + "interaction": {"id": INTERACTION_ID, "object": "interaction"}, + }, + { + "event_type": "step.delta", + "index": 0, + "delta": {"type": "text", "text": "Hello "}, + }, + { + "event_type": "step.delta", + "index": 0, + "delta": {"type": "text", "text": "world"}, + }, + {"event_type": "interaction.completed", "interaction": make_interaction_dict()}, + ] + + +def make_agent_dict(agent_id: str = "test-agent") -> dict[str, Any]: + """Build a minimal managed-agent dict as the API would return it.""" + return {"id": agent_id, "system_instruction": "Be helpful."} + + +# --------------------------------------------------------------------------- +# Tool call tracker — records every tool invocation for assertion +# --------------------------------------------------------------------------- + + +class ToolCallTracker: + """Tracks tool invocations across activities and workflow methods. + + Each tool appends (name, args_dict) to ``calls`` so tests can assert + exactly which tools were called, in what order, with what arguments. + """ + + def __init__(self) -> None: + self.calls: list[tuple[str, dict]] = [] + + @activity.defn + async def get_weather(self, city: str) -> str: + """Get the weather for a given city.""" + self.calls.append(("get_weather", {"city": city})) + return f"Weather in {city}: Sunny, 20C" + + @activity.defn + async def get_weather_country(self, city: str, country: str) -> str: + """Get the weather for a given city in a country.""" + self.calls.append(("get_weather_country", {"city": city, "country": country})) + return f"Weather in {city}, {country}: Rainy, 15C" + + @activity.defn + async def get_weather_failure(self, city: str) -> str: + """Activity that always fails.""" + self.calls.append(("get_weather_failure", {"city": city})) + raise ApplicationError("Weather service unavailable", non_retryable=True) + + +# --------------------------------------------------------------------------- +# Test helper: tracking gemini_api_client_async_request activity +# --------------------------------------------------------------------------- + + +class GeminiApiCallTracker: + """A test replacement for the gemini_api_client activities. + + Records every ``_GeminiApiRequest`` received and returns canned + ``_GeminiApiResponse`` bodies in order. After the workflow completes, + inspect ``requests`` to verify exactly what the integration sent. + + For streamed requests, the mock response is split into per-line chunks + to simulate multiple streamed chunks. + + The real ``GoogleGenAIPlugin`` is still used for its data converter, sandbox + passthrough, and workflow runner configuration — only its activity + registration is suppressed so this tracker can take its place. + """ + + def __init__(self, mock_responses: list[str]) -> None: + self._mock_responses = mock_responses + self.requests: list[_GeminiApiRequest] = [] + self.file_upload_requests: list[_GeminiUploadFileRequest] = [] + self.file_download_requests: list[_GeminiDownloadFileRequest] = [] + self.file_search_store_upload_requests: list[ + _GeminiUploadToFileSearchStoreRequest + ] = [] + self.interaction_requests: list[_GeminiInteractionRequest] = [] + self.interaction_id_requests: list[_GeminiInteractionIdRequest] = [] + self._call_index = 0 + + def _next_response(self, req: _GeminiApiRequest) -> str: + self.requests.append(req) + idx = self._call_index + self._call_index += 1 + if idx >= len(self._mock_responses): + raise ApplicationError( + f"No more mock responses (called {idx + 1} times, " + f"have {len(self._mock_responses)})", + non_retryable=True, + ) + return self._mock_responses[idx] + + @activity.defn + async def gemini_api_client_async_request( + self, req: _GeminiApiRequest + ) -> _GeminiApiResponse: + return _GeminiApiResponse( + headers={"content-type": "application/json"}, + body=self._next_response(req), + ) + + @activity.defn + async def gemini_api_client_async_request_streamed( + self, req: _GeminiApiRequest + ) -> _GeminiApiStreamedResponse: + body = self._next_response(req) + # Split the response text into word-level chunks so tests can + # verify that multiple chunks are yielded back to the workflow. + parsed = json.loads(body) + full_text = ( + parsed.get("candidates", [{}])[0] + .get("content", {}) + .get("parts", [{}])[0] + .get("text", "") + ) + words = full_text.split() + chunks = [] + for word in words: + chunks.append( + _GeminiApiResponse( + headers={"content-type": "application/json"}, + body=make_text_response(word), + ) + ) + return _GeminiApiStreamedResponse(chunks=chunks) + + @activity.defn + async def gemini_files_upload(self, req: _GeminiUploadFileRequest) -> types.File: + self.file_upload_requests.append(req) + return types.File( + name="files/test-uploaded-file", + uri="https://fake.uri/files/test-uploaded-file", + size_bytes=len(req.file_bytes) if req.file_bytes else 0, + ) + + @activity.defn + async def gemini_files_download(self, req: _GeminiDownloadFileRequest) -> bytes: + self.file_download_requests.append(req) + return b"fake file content" + + @activity.defn + async def gemini_file_search_stores_upload( + self, req: _GeminiUploadToFileSearchStoreRequest + ) -> types.UploadToFileSearchStoreOperation: + self.file_search_store_upload_requests.append(req) + return types.UploadToFileSearchStoreOperation.model_construct( + name="operations/test-op", + ) + + @activity.defn + async def gemini_interactions_create( + self, req: _GeminiInteractionRequest + ) -> dict[str, Any]: + self.interaction_requests.append(req) + return make_interaction_dict() + + @activity.defn + async def gemini_interactions_create_streamed( + self, req: _GeminiInteractionRequest + ) -> _GeminiInteractionStreamedResponse: + self.interaction_requests.append(req) + return _GeminiInteractionStreamedResponse(events=make_interaction_sse_events()) + + @activity.defn + async def gemini_interactions_get( + self, req: _GeminiInteractionIdRequest + ) -> dict[str, Any]: + self.interaction_id_requests.append(req) + return make_interaction_dict() + + @activity.defn + async def gemini_interactions_get_streamed( + self, req: _GeminiInteractionIdRequest + ) -> _GeminiInteractionStreamedResponse: + self.interaction_id_requests.append(req) + return _GeminiInteractionStreamedResponse(events=make_interaction_sse_events()) + + @activity.defn + async def gemini_interactions_delete(self, req: _GeminiInteractionIdRequest) -> Any: + self.interaction_id_requests.append(req) + return {"deleted": True} + + @activity.defn + async def gemini_interactions_cancel( + self, req: _GeminiInteractionIdRequest + ) -> dict[str, Any]: + self.interaction_id_requests.append(req) + return make_interaction_dict(status="cancelled") + + @activity.defn + async def gemini_agents_create( + self, req: _GeminiInteractionRequest + ) -> dict[str, Any]: + self.interaction_requests.append(req) + return make_agent_dict(req.params.get("id", "test-agent")) + + @activity.defn + async def gemini_agents_list( + self, req: _GeminiInteractionRequest + ) -> dict[str, Any]: + self.interaction_requests.append(req) + return {"agents": [make_agent_dict()], "next_page_token": "next-tok"} + + @activity.defn + async def gemini_agents_get( + self, req: _GeminiInteractionIdRequest + ) -> dict[str, Any]: + self.interaction_id_requests.append(req) + return make_agent_dict(req.id) + + @activity.defn + async def gemini_agents_delete( + self, req: _GeminiInteractionIdRequest + ) -> dict[str, Any]: + self.interaction_id_requests.append(req) + return {"id": req.id, "deleted": True} + + +def apply_plugin( + client: Client, mock_responses: list[str] +) -> tuple[Client, GeminiApiCallTracker]: + """Create a real GoogleGenAIPlugin whose activities include a tracking fake. + + Monkey-patches ``GeminiApiCaller.activities`` so that when the plugin + constructs itself, it registers our tracking activity instead of + the real ones. Everything else — data converter, sandbox passthrough, + workflow runner — is the real plugin code. + + Returns the configured Temporal client and the tracker. + """ + from temporalio.contrib.google_genai._gemini_activity import GeminiApiCaller + + tracker = GeminiApiCallTracker(mock_responses) + original_activities = GeminiApiCaller.activities + GeminiApiCaller.activities = lambda self: [ # type: ignore[method-assign] + tracker.gemini_api_client_async_request, + tracker.gemini_api_client_async_request_streamed, + tracker.gemini_files_upload, + tracker.gemini_files_download, + tracker.gemini_file_search_stores_upload, + tracker.gemini_interactions_create, + tracker.gemini_interactions_create_streamed, + tracker.gemini_interactions_get, + tracker.gemini_interactions_get_streamed, + tracker.gemini_interactions_delete, + tracker.gemini_interactions_cancel, + tracker.gemini_agents_create, + tracker.gemini_agents_list, + tracker.gemini_agents_get, + tracker.gemini_agents_delete, + ] + try: + gemini = GeminiClient(api_key="fake-test-key") + plugin = GoogleGenAIPlugin(gemini) + finally: + GeminiApiCaller.activities = original_activities # type: ignore[method-assign] + + config = client.config() + config["plugins"] = [plugin] + return Client(**config), tracker + + +# --------------------------------------------------------------------------- +# Workflows +# --------------------------------------------------------------------------- + + +@workflow.defn +class SimpleGenerateWorkflow: + """Workflow that does a simple generate_content call.""" + + @workflow.run + async def run(self, prompt: str) -> str: + client = TemporalAsyncClient() + response = await client.models.generate_content( + model="gemini-2.5-flash", + contents=prompt, + ) + return response.text or "" + + +@workflow.defn +class SingleArgToolWorkflow: + """Workflow that uses activity_as_tool for a single-arg tool.""" + + @workflow.run + async def run(self, prompt: str) -> str: + client = TemporalAsyncClient() + response = await client.models.generate_content( + model="gemini-2.5-flash", + contents=prompt, + config=types.GenerateContentConfig( + tools=[ + activity_as_tool( + ToolCallTracker.get_weather, + activity_config=ActivityConfig( + start_to_close_timeout=timedelta(seconds=10), + ), + ), + ], + ), + ) + return response.text or "" + + +@workflow.defn +class MultiArgToolWorkflow: + """Workflow with multi-arg tool.""" + + @workflow.run + async def run(self, prompt: str) -> str: + client = TemporalAsyncClient() + response = await client.models.generate_content( + model="gemini-2.5-flash", + contents=prompt, + config=types.GenerateContentConfig( + tools=[ + activity_as_tool( + ToolCallTracker.get_weather_country, + activity_config=ActivityConfig( + start_to_close_timeout=timedelta(seconds=10), + ), + ), + ], + ), + ) + return response.text or "" + + +@workflow.defn +class ToolFailureWorkflow: + """Workflow with a tool that always fails.""" + + @workflow.run + async def run(self, prompt: str) -> str: + client = TemporalAsyncClient() + response = await client.models.generate_content( + model="gemini-2.5-flash", + contents=prompt, + config=types.GenerateContentConfig( + tools=[ + activity_as_tool( + ToolCallTracker.get_weather_failure, + activity_config=ActivityConfig( + start_to_close_timeout=timedelta(seconds=10), + retry_policy=RetryPolicy(maximum_attempts=1), + ), + ), + ], + ), + ) + return response.text or "" + + +@workflow.defn +class MultipleToolsWorkflow: + """Workflow with multiple tools that are called in sequence.""" + + @workflow.run + async def run(self, prompt: str) -> str: + client = TemporalAsyncClient() + response = await client.models.generate_content( + model="gemini-2.5-flash", + contents=prompt, + config=types.GenerateContentConfig( + tools=[ + activity_as_tool( + ToolCallTracker.get_weather, + activity_config=ActivityConfig( + start_to_close_timeout=timedelta(seconds=10), + ), + ), + activity_as_tool( + ToolCallTracker.get_weather_country, + activity_config=ActivityConfig( + start_to_close_timeout=timedelta(seconds=10), + ), + ), + ], + ), + ) + return response.text or "" + + +@workflow.defn +class WorkflowMethodToolWorkflow: + """Workflow that passes a plain method as a tool (runs in-workflow, not as an activity).""" + + def __init__(self) -> None: + self.tool_calls: list[tuple[str, dict]] = [] + + @workflow.run + async def run(self, prompt: str) -> str: + client = TemporalAsyncClient() + response = await client.models.generate_content( + model="gemini-2.5-flash", + contents=prompt, + config=types.GenerateContentConfig( + tools=[self.lookup_city], + ), + ) + return response.text or "" + + async def lookup_city(self, city: str) -> str: + """Look up info about a city.""" + self.tool_calls.append(("lookup_city", {"city": city})) + return f"{city} is a great place to visit" + + @workflow.query + def get_tool_calls(self) -> list[tuple[str, dict]]: + return self.tool_calls + + +@workflow.defn +class StreamedGenerateWorkflow: + """Workflow that uses generate_content_stream.""" + + @workflow.run + async def run(self, prompt: str) -> list[str]: + client = TemporalAsyncClient() + chunks: list[str] = [] + async for chunk in await client.models.generate_content_stream( + model="gemini-2.5-flash", + contents=prompt, + ): + if chunk.text: + chunks.append(chunk.text) + return chunks + + +@workflow.defn +class HttpOptionsWorkflow: + """Workflow that passes per-request http_options through generate_content.""" + + @workflow.run + async def run(self, prompt: str, http_options: types.HttpOptionsDict) -> str: + client = TemporalAsyncClient() + response = await client.models.generate_content( + model="gemini-2.5-flash", + contents=prompt, + config=types.GenerateContentConfig( + http_options=types.HttpOptions.model_validate(http_options), + ), + ) + return response.text or "" + + +@workflow.defn +class FullIntegrationWorkflow: + """Exercises every activity path in a single workflow run. + + Uses the real GoogleGenAIPlugin activities (not the tracker), so this + tests the actual activity implementations end-to-end with a mocked + genai.Client. + """ + + @workflow.run + async def run(self, prompt: str) -> dict[str, Any]: + client = TemporalAsyncClient() + results: dict[str, Any] = {} + + # 1. generate_content (async_request activity) + response = await client.models.generate_content( + model="gemini-2.5-flash", + contents=prompt, + ) + results["generate"] = response.text or "" + + # 2. generate_content_stream (async_request_streamed activity) + chunks: list[str] = [] + async for chunk in await client.models.generate_content_stream( + model="gemini-2.5-flash", + contents=prompt, + ): + if chunk.text: + chunks.append(chunk.text) + results["stream_chunks"] = chunks + + # 3. files.upload (gemini_files_upload activity) + uploaded = await client.files.upload( + file="/tmp/fake.txt", + config=types.UploadFileConfig(display_name="Integration Test"), + ) + results["upload_name"] = uploaded.name or "" + + # 4. files.download (gemini_files_download activity) + data = await client.files.download(file="files/some-file") + results["download"] = data.decode() if isinstance(data, bytes) else str(data) + + # 5. file_search_stores.upload_to_file_search_store activity + store_name = "fileSearchStores/test" + op = await client.file_search_stores.upload_to_file_search_store( + file_search_store_name=store_name, + file="/tmp/doc.txt", + ) + results["fss_upload_op"] = op.name or "" + + # 6. generate_content grounded with file_search tool (RAG query) + rag_response = await client.models.generate_content( + model="gemini-2.5-flash", + contents="What does the document say?", + config=types.GenerateContentConfig( + tools=[ + types.Tool( + file_search=types.FileSearch( + file_search_store_names=[store_name], + ), + ), + ], + ), + ) + results["rag"] = rag_response.text or "" + + # 7. Clean up the file search store + await client.file_search_stores.delete( + name=store_name, + config=types.DeleteFileSearchStoreConfig(force=True), + ) + results["store_deleted"] = True + + # 8. interactions.create (gemini_interactions_create activity) + interaction = await client.interactions.create( + model="gemini-2.5-flash", + input=prompt, + ) + assert isinstance(interaction, Interaction) + results["interaction_id"] = interaction.id + + # 9. interactions.create streamed (gemini_interactions_create_streamed) + stream = await client.interactions.create( + model="gemini-2.5-flash", + input=prompt, + stream=True, + ) + assert not isinstance(stream, Interaction) + event_types: list[str] = [] + async with stream: + async for event in stream: + event_types.append(event.event_type) + results["interaction_events"] = event_types + + # 10. agents.create (gemini_agents_create activity) + agent = await client.agents.create( + id="test-agent", + system_instruction="Be helpful.", + ) + results["agent_id"] = agent.id + + return results + + +@workflow.defn +class FileUploadStrWorkflow: + """Workflow that uploads a file via str path.""" + + @workflow.run + async def run(self, file_path: str) -> str: + client = TemporalAsyncClient() + uploaded = await client.files.upload( + file=file_path, + config=types.UploadFileConfig( + display_name="Test File", + mime_type="text/plain", + ), + ) + return uploaded.name or "" + + +@workflow.defn +class FileUploadBytesWorkflow: + """Workflow that uploads a file via io.BytesIO.""" + + @workflow.run + async def run(self, data: bytes) -> str: + client = TemporalAsyncClient() + uploaded = await client.files.upload( + file=io.BytesIO(data), + config=types.UploadFileConfig( + display_name="Bytes File", + mime_type="text/plain", + ), + ) + return uploaded.name or "" + + +@workflow.defn +class FileDownloadWorkflow: + """Workflow that downloads a file by name.""" + + @workflow.run + async def run(self, file_name: str) -> bytes: + client = TemporalAsyncClient() + return await client.files.download(file=file_name) + + +@workflow.defn +class FileSearchStoreUploadWorkflow: + """Workflow that uploads to a file search store.""" + + @workflow.run + async def run(self, store_name: str, file_path: str) -> str: + client = TemporalAsyncClient() + op = await client.file_search_stores.upload_to_file_search_store( + file_search_store_name=store_name, + file=file_path, + config=types.UploadToFileSearchStoreConfig( + display_name="Test Doc", + mime_type="text/plain", + ), + ) + return op.name or "" + + +@workflow.defn +class RegisterFilesWorkflow: + """Workflow that calls files.register_files.""" + + @workflow.run + async def run(self, uris: list[str]) -> str: + client = TemporalAsyncClient() + # auth arg is ignored by TemporalAsyncFiles — the activity uses + # credentials from GoogleGenAIPlugin init. We pass a dummy here; + # can't import google.auth.credentials in the sandbox so we + # use a sentinel that satisfies the type at runtime. + resp = await client.files.register_files( + auth=None, # type: ignore[arg-type] + uris=uris, + ) + return str(len(resp.files or [])) + + +@workflow.defn +class ChatWorkflow: + """Workflow that uses client.chats for multi-turn conversation.""" + + @workflow.run + async def run(self, prompt: str) -> list[str]: + client = TemporalAsyncClient() + chat = client.chats.create( + model="gemini-2.5-flash", + ) + r1 = await chat.send_message(prompt) + r2 = await chat.send_message("Follow up question") + return [r1.text or "", r2.text or ""] + + +@workflow.defn +class InteractionCreateWorkflow: + """Workflow that creates an interaction (non-streaming).""" + + @workflow.run + async def run(self, prompt: str) -> dict[str, Any]: + client = TemporalAsyncClient() + interaction = await client.interactions.create( + model="gemini-2.5-flash", + input=prompt, + timeout=120, + ) + assert isinstance(interaction, Interaction) + return {"id": interaction.id, "status": str(interaction.status)} + + +@workflow.defn +class InteractionStreamWorkflow: + """Workflow that creates a streamed interaction and collects event types.""" + + @workflow.run + async def run(self, prompt: str) -> list[str]: + client = TemporalAsyncClient() + stream = await client.interactions.create( + model="gemini-2.5-flash", + input=prompt, + stream=True, + ) + assert not isinstance(stream, Interaction) + event_types: list[str] = [] + async with stream: + async for event in stream: + event_types.append(event.event_type) + return event_types + + +@workflow.defn +class InteractionLifecycleWorkflow: + """Workflow that gets, cancels, and deletes an interaction.""" + + @workflow.run + async def run(self, interaction_id: str) -> dict[str, Any]: + client = TemporalAsyncClient() + got = await client.interactions.get(interaction_id) + assert isinstance(got, Interaction) + cancelled = await client.interactions.cancel(interaction_id) + deleted = await client.interactions.delete(interaction_id) + return { + "got_id": got.id, + "cancel_status": str(cancelled.status), + "deleted": deleted, + } + + +@workflow.defn +class AgentsWorkflow: + """Workflow that exercises managed-agent CRUD.""" + + @workflow.run + async def run(self) -> dict[str, Any]: + client = TemporalAsyncClient() + agent = await client.agents.create( + id="test-agent", + system_instruction="Be helpful.", + ) + got = await client.agents.get("test-agent") + listing = await client.agents.list(page_size=10) + deleted = await client.agents.delete("test-agent") + return { + "created_id": agent.id, + "got_id": got.id, + "listed_ids": [a.id for a in (listing.agents or [])], + "next_page_token": listing.next_page_token, + # AgentDeleteResponse defines no fields; the API's JSON comes + # back as extras, so return the dict form. + "delete_response": deleted.model_dump(mode="json"), + } + + +@workflow.defn +class WebhooksUnsupportedWorkflow: + """Workflow that verifies client.webhooks raises a clear error.""" + + @workflow.run + async def run(self) -> str: + client = TemporalAsyncClient() + try: + _ = client.webhooks + except RuntimeError as e: + return str(e) + return "no error" + + +# =========================================================================== +# Integration tests — run workflows against a real Temporal test server +# =========================================================================== + + +async def test_simple_generate_content(client: Client): + """Basic generate_content returns text through a workflow.""" + new_client, _ = apply_plugin(client, [make_text_response("Hello from Gemini!")]) + + async with new_worker(new_client, SimpleGenerateWorkflow) as worker: + result = await new_client.execute_workflow( + SimpleGenerateWorkflow.run, + "Say hello", + id=f"gemini-simple-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + + assert result == "Hello from Gemini!" + + +async def test_tool_call_single_arg(client: Client): + """Tool calling with a single-argument activity via AFC.""" + tool_tracker = ToolCallTracker() + new_client, _ = apply_plugin( + client, + [ + make_function_call_response("get_weather", {"city": "Tokyo"}), + make_text_response("The weather in Tokyo is sunny and 20C."), + ], + ) + + async with new_worker( + new_client, + SingleArgToolWorkflow, + activities=[tool_tracker.get_weather], + ) as worker: + result = await new_client.execute_workflow( + SingleArgToolWorkflow.run, + "What's the weather in Tokyo?", + id=f"gemini-tool-single-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + + assert tool_tracker.calls == [("get_weather", {"city": "Tokyo"})] + assert result == "The weather in Tokyo is sunny and 20C." + + +async def test_tool_call_multi_arg(client: Client): + """Tool calling with a multi-argument activity.""" + tool_tracker = ToolCallTracker() + new_client, _ = apply_plugin( + client, + [ + make_function_call_response( + "get_weather_country", {"city": "Paris", "country": "France"} + ), + make_text_response("Paris, France: Rainy, 15C."), + ], + ) + + async with new_worker( + new_client, + MultiArgToolWorkflow, + activities=[tool_tracker.get_weather_country], + ) as worker: + result = await new_client.execute_workflow( + MultiArgToolWorkflow.run, + "What's the weather in Paris, France?", + id=f"gemini-tool-multi-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + + assert tool_tracker.calls == [ + ("get_weather_country", {"city": "Paris", "country": "France"}) + ] + assert result == "Paris, France: Rainy, 15C." + + +async def test_tool_failure_propagation(client: Client): + """Tool activity failure causes the workflow to fail.""" + tool_tracker = ToolCallTracker() + new_client, _ = apply_plugin( + client, + [ + make_function_call_response("get_weather_failure", {"city": "Nowhere"}), + ], + ) + + async with new_worker( + new_client, + ToolFailureWorkflow, + activities=[tool_tracker.get_weather_failure], + ) as worker: + with pytest.raises(WorkflowFailureError): + await new_client.execute_workflow( + ToolFailureWorkflow.run, + "Weather in Nowhere?", + id=f"gemini-tool-fail-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + + assert tool_tracker.calls == [("get_weather_failure", {"city": "Nowhere"})] + + +async def test_multiple_tools_sequential(client: Client): + """Multiple tools called in sequence within one generate_content call.""" + tool_tracker = ToolCallTracker() + new_client, _ = apply_plugin( + client, + [ + make_function_call_response("get_weather", {"city": "Tokyo"}), + make_function_call_response( + "get_weather_country", {"city": "Paris", "country": "France"} + ), + make_text_response("Tokyo is sunny; Paris is rainy."), + ], + ) + + async with new_worker( + new_client, + MultipleToolsWorkflow, + activities=[ + tool_tracker.get_weather, + tool_tracker.get_weather_country, + ], + ) as worker: + result = await new_client.execute_workflow( + MultipleToolsWorkflow.run, + "Compare Tokyo and Paris weather", + id=f"gemini-multi-tools-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=15), + ) + + assert tool_tracker.calls == [ + ("get_weather", {"city": "Tokyo"}), + ("get_weather_country", {"city": "Paris", "country": "France"}), + ] + assert result == "Tokyo is sunny; Paris is rainy." + + +async def test_workflow_method_as_tool(client: Client): + """A plain workflow method (not an activity) used as a tool runs in-workflow.""" + new_client, _ = apply_plugin( + client, + [ + make_function_call_response("lookup_city", {"city": "Berlin"}), + make_text_response("Berlin is wonderful."), + ], + ) + + async with new_worker(new_client, WorkflowMethodToolWorkflow) as worker: + handle = await new_client.start_workflow( + WorkflowMethodToolWorkflow.run, + "Tell me about Berlin", + id=f"gemini-wf-method-tool-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + result = await handle.result() + # Query must happen while worker is alive + tool_calls = await handle.query(WorkflowMethodToolWorkflow.get_tool_calls) + + assert tool_calls == [("lookup_city", {"city": "Berlin"})] + assert result == "Berlin is wonderful." + + +async def test_streamed_generate_content(client: Client): + """generate_content_stream collects batched chunks from the activity.""" + new_client, _ = apply_plugin( + client, [make_text_response("The quick brown fox jumps over the lazy dog")] + ) + + async with new_worker(new_client, StreamedGenerateWorkflow) as worker: + result = await new_client.execute_workflow( + StreamedGenerateWorkflow.run, + "Say something", + id=f"gemini-streamed-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + + # The tracker splits the text into per-word chunks + assert len(result) == 9 + assert " ".join(result) == "The quick brown fox jumps over the lazy dog" + + +# =========================================================================== +# http_options propagation tests - per request overrides +# =========================================================================== + + +async def test_http_options_headers_propagate(client: Client): + """Custom headers passed via http_options arrive at the activity.""" + new_client, api_tracker = apply_plugin(client, [make_text_response("ok")]) + + async with new_worker(new_client, HttpOptionsWorkflow) as worker: + await new_client.execute_workflow( + HttpOptionsWorkflow.run, + args=["hi", {"headers": {"X-Custom": "test-value"}}], + id=f"gemini-http-headers-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + + assert len(api_tracker.requests) == 1 + opts = api_tracker.requests[0].http_options_overrides + assert opts is not None + assert opts.headers == {"X-Custom": "test-value"} + + +async def test_http_options_api_version_propagates(client: Client): + """api_version passed via http_options arrives at the activity.""" + new_client, api_tracker = apply_plugin(client, [make_text_response("ok")]) + + async with new_worker(new_client, HttpOptionsWorkflow) as worker: + await new_client.execute_workflow( + HttpOptionsWorkflow.run, + args=["hi", {"api_version": "v1"}], + id=f"gemini-http-version-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + + assert len(api_tracker.requests) == 1 + opts = api_tracker.requests[0].http_options_overrides + assert opts is not None + assert opts.api_version == "v1" + + +async def test_http_options_base_url_propagates(client: Client): + """base_url passed via http_options arrives at the activity.""" + new_client, api_tracker = apply_plugin(client, [make_text_response("ok")]) + + async with new_worker(new_client, HttpOptionsWorkflow) as worker: + await new_client.execute_workflow( + HttpOptionsWorkflow.run, + args=["hi", {"base_url": "https://custom.example.com"}], + id=f"gemini-http-base-url-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + + assert len(api_tracker.requests) == 1 + opts = api_tracker.requests[0].http_options_overrides + assert opts is not None + assert opts.base_url == "https://custom.example.com" + + +async def test_http_options_multiple_fields_propagate(client: Client): + """Multiple http_options fields propagate together to the activity.""" + new_client, api_tracker = apply_plugin(client, [make_text_response("ok")]) + + async with new_worker(new_client, HttpOptionsWorkflow) as worker: + await new_client.execute_workflow( + HttpOptionsWorkflow.run, + args=[ + "hi", + { + "api_version": "v1beta", + "headers": {"X-Foo": "bar"}, + "base_url": "https://other.example.com", + }, + ], + id=f"gemini-http-multi-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + + assert len(api_tracker.requests) == 1 + opts = api_tracker.requests[0].http_options_overrides + assert opts is not None + assert opts.api_version == "v1beta" + assert opts.headers == {"X-Foo": "bar"} + assert opts.base_url == "https://other.example.com" + + +async def test_no_http_options_passes_none(client: Client): + """When no per-request http_options are set, None reaches the activity.""" + new_client, api_tracker = apply_plugin(client, [make_text_response("ok")]) + + async with new_worker(new_client, SimpleGenerateWorkflow) as worker: + await new_client.execute_workflow( + SimpleGenerateWorkflow.run, + "hi", + id=f"gemini-http-none-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + + assert len(api_tracker.requests) == 1 + assert api_tracker.requests[0].http_options_overrides is None + + +# =========================================================================== +# File upload/download tests +# =========================================================================== + + +async def test_file_upload_str_path(client: Client): + """Upload a file via str path dispatches through the activity.""" + new_client, api_tracker = apply_plugin(client, []) + + async with new_worker(new_client, FileUploadStrWorkflow) as worker: + result = await new_client.execute_workflow( + FileUploadStrWorkflow.run, + "/tmp/test.txt", + id=f"gemini-file-upload-str-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + + assert len(api_tracker.file_upload_requests) == 1 + req = api_tracker.file_upload_requests[0] + assert req.file_path == "/tmp/test.txt" + assert req.file_bytes is None + assert req.config is not None + assert req.config.display_name == "Test File" + assert result == "files/test-uploaded-file" + + +async def test_file_upload_bytes(client: Client): + """Upload a file via io.BytesIO sends bytes through the activity.""" + new_client, api_tracker = apply_plugin(client, []) + + async with new_worker(new_client, FileUploadBytesWorkflow) as worker: + result = await new_client.execute_workflow( + FileUploadBytesWorkflow.run, + b"hello world", + id=f"gemini-file-upload-bytes-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + + assert len(api_tracker.file_upload_requests) == 1 + req = api_tracker.file_upload_requests[0] + assert req.file_bytes == b"hello world" + assert req.file_path is None + assert req.config is not None + assert req.config.display_name == "Bytes File" + assert result == "files/test-uploaded-file" + + +async def test_file_download(client: Client): + """Download a file dispatches through the activity and returns bytes.""" + new_client, api_tracker = apply_plugin(client, []) + + async with new_worker(new_client, FileDownloadWorkflow) as worker: + result = await new_client.execute_workflow( + FileDownloadWorkflow.run, + "files/some-file", + id=f"gemini-file-download-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + + assert len(api_tracker.file_download_requests) == 1 + assert api_tracker.file_download_requests[0].file == "files/some-file" + assert result == b"fake file content" + + +# =========================================================================== +# File search store upload tests +# =========================================================================== + + +async def test_file_search_store_upload(client: Client): + """Upload to file search store dispatches through the activity.""" + new_client, api_tracker = apply_plugin(client, []) + + async with new_worker(new_client, FileSearchStoreUploadWorkflow) as worker: + result = await new_client.execute_workflow( + FileSearchStoreUploadWorkflow.run, + args=["fileSearchStores/my-store", "/tmp/doc.txt"], + id=f"gemini-fss-upload-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + + assert len(api_tracker.file_search_store_upload_requests) == 1 + req = api_tracker.file_search_store_upload_requests[0] + assert req.file_search_store_name == "fileSearchStores/my-store" + assert req.file_path == "/tmp/doc.txt" + assert req.config is not None + assert req.config.display_name == "Test Doc" + assert result == "operations/test-op" + + +# =========================================================================== +# Multi-turn chat tests +# =========================================================================== + + +async def test_chat_multi_turn(client: Client): + """Multi-turn chat sends multiple requests through the activity.""" + new_client, api_tracker = apply_plugin( + client, + [ + make_text_response("First answer"), + make_text_response("Second answer"), + ], + ) + + async with new_worker(new_client, ChatWorkflow) as worker: + result = await new_client.execute_workflow( + ChatWorkflow.run, + "Hello", + id=f"gemini-chat-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + + assert len(api_tracker.requests) == 2 + assert result == ["First answer", "Second answer"] + + +class _FakeAsyncStream: + """Minimal stand-in for the SDK's AsyncStream in mocked-client tests.""" + + def __init__(self, events: list[Any]) -> None: + self._events = events + + def __aiter__(self) -> Any: + return self._gen() + + async def _gen(self) -> Any: + for event in self._events: + yield event + + async def __aenter__(self) -> "_FakeAsyncStream": + return self + + async def __aexit__(self, *_args: Any) -> None: + pass + + +# =========================================================================== +# Full integration test — real activities, mocked client +# =========================================================================== + + +def _apply_plugin_with_mock_client(client: Client, mock_responses: list[str]) -> Client: + """Create a real GoogleGenAIPlugin with real activities but a mocked client. + + Unlike ``apply_plugin``, this does NOT replace the activities. The + real ``GeminiApiCaller.activities()`` are registered, exercising the + full activity code path. The underlying ``genai.Client`` HTTP layer + and high-level file methods are mocked so no network calls are made. + """ + gemini = GeminiClient(api_key="fake-test-key") + + call_state = {"index": 0} + + async def fake_async_request(*_args: Any, **_kwargs: Any) -> SdkHttpResponse: + idx = call_state["index"] + call_state["index"] += 1 + if idx >= len(mock_responses): + raise RuntimeError( + f"No more mock responses (called {idx + 1} times, " + f"have {len(mock_responses)})" + ) + return SdkHttpResponse( + headers={"content-type": "application/json"}, + body=mock_responses[idx], + ) + + async def fake_async_request_streamed(*_args: Any, **_kwargs: Any) -> Any: + idx = call_state["index"] + call_state["index"] += 1 + if idx >= len(mock_responses): + raise RuntimeError( + f"No more mock responses (called {idx + 1} times, " + f"have {len(mock_responses)})" + ) + + async def _gen(): + yield SdkHttpResponse( + headers={"content-type": "application/json"}, + body=mock_responses[idx], + ) + + return _gen() + + gemini._api_client.async_request = fake_async_request # type: ignore[assignment] + gemini._api_client.async_request_streamed = fake_async_request_streamed # type: ignore[assignment] + + # Mock file operations at the high-level SDK interface (these are what + # the real activities call). + gemini.aio.files.upload = AsyncMock( # type: ignore[method-assign] + return_value=types.File( + name="files/mock-uploaded", + uri="https://fake.uri/files/mock-uploaded", + size_bytes=42, + ) + ) + gemini.aio.files.download = AsyncMock(return_value=b"mock download content") # type: ignore[method-assign] + gemini.aio.file_search_stores.upload_to_file_search_store = AsyncMock( # type: ignore[method-assign] + return_value=types.UploadToFileSearchStoreOperation.model_construct( + name="operations/mock-op" + ) + ) + + # Interactions and agents go through the vendored nextgen client (not + # BaseApiClient); inject a mock instance so the real activities exercise + # their code path without network access. + interaction = _deserialize(make_interaction_dict(), Interaction) + sse_events = [ + _deserialize(e, InteractionSSEEvent) for e in make_interaction_sse_events() + ] + + async def _interactions_create(*_args: Any, **kwargs: Any) -> Any: + if kwargs.get("stream"): + return _FakeAsyncStream(sse_events) + return interaction + + mock_interactions = MagicMock() + mock_interactions.create = _interactions_create + mock_agents = MagicMock() + mock_agents.create = AsyncMock(return_value=_deserialize(make_agent_dict(), Agent)) + # The interactions/agents resources are cached lazily on the async client; + # set them so the activities' client.aio.interactions/.agents calls hit the + # mocks instead of the network. + gemini.aio._interactions = mock_interactions # type: ignore[assignment] + gemini.aio._agents = mock_agents # type: ignore[assignment] + + plugin = GoogleGenAIPlugin(gemini) + config = client.config() + config["plugins"] = [plugin] + return Client(**config) + + +async def test_full_integration_with_mock_client(client: Client): + """Run a workflow through real activities with a mocked genai.Client. + + This is the only test that exercises the actual activity implementations + in _gemini_activity.py. Every other test uses the GeminiApiCallTracker + which replaces the activities entirely. + """ + # Mock responses are consumed in order by the async_request and + # async_request_streamed mocks. Steps 3-5 (file upload, download, + # store upload) are mocked separately at the SDK level and don't + # consume from this list. + new_client = _apply_plugin_with_mock_client( + client, + [ + make_text_response("Real activity response"), # generate_content + make_text_response("Streamed via real activity"), # generate_content_stream + make_text_response("Grounded RAG answer"), # RAG query with file_search + make_text_response(""), # file_search_stores.delete + ], + ) + + async with new_worker(new_client, FullIntegrationWorkflow) as worker: + result = await new_client.execute_workflow( + FullIntegrationWorkflow.run, + "test prompt", + id=f"gemini-full-integration-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=15), + ) + + assert result["generate"] == "Real activity response" + assert len(result["stream_chunks"]) > 0 + assert "Streamed" in " ".join(result["stream_chunks"]) + assert result["upload_name"] == "files/mock-uploaded" + assert result["download"] == "mock download content" + assert result["fss_upload_op"] == "operations/mock-op" + assert result["rag"] == "Grounded RAG answer" + assert result["store_deleted"] is True + assert result["interaction_id"] == INTERACTION_ID + assert result["interaction_events"] == [ + "interaction.created", + "step.delta", + "step.delta", + "interaction.completed", + ] + assert result["agent_id"] == "test-agent" + + +async def test_register_files_without_credentials_fails(client: Client): + """register_files raises when no credentials are available.""" + # _apply_plugin_with_mock_client uses api_key auth with no + # extra_credentials, so the activity should raise ValueError. + new_client = _apply_plugin_with_mock_client(client, []) + + async with new_worker(new_client, RegisterFilesWorkflow) as worker: + with pytest.raises(WorkflowFailureError) as exc_info: + await new_client.execute_workflow( + RegisterFilesWorkflow.run, + ["gs://bucket/file.txt"], + id=f"gemini-register-no-creds-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + + # The error is nested: WorkflowFailureError → ActivityError → ApplicationError + cause = exc_info.value.cause + while cause.__cause__ is not None: + cause = cause.__cause__ + assert "No credentials available for register_files" in str(cause) + + +# =========================================================================== +# TemporalAsyncClient wiring tests +# =========================================================================== + + +def test_temporal_async_client_has_temporal_files(): + """TemporalAsyncClient() returns a client with TemporalAsyncFiles.""" + client = TemporalAsyncClient() + assert isinstance(client, TemporalAsyncClient) + assert isinstance(client.files, TemporalAsyncFiles) + + +def test_temporal_async_client_has_temporal_file_search_stores(): + """TemporalAsyncClient() returns a client with TemporalAsyncFileSearchStores.""" + client = TemporalAsyncClient() + assert isinstance(client.file_search_stores, TemporalAsyncFileSearchStores) + + +# =========================================================================== +# Unit tests for _TemporalApiClient +# =========================================================================== + + +def test_sync_request_raises(): + """Synchronous request() raises RuntimeError.""" + api_client = _TemporalApiClient() + with pytest.raises(RuntimeError, match="Synchronous requests are not supported"): + api_client.request("GET", "/test", {}) + + +def test_sync_request_streamed_raises(): + """Synchronous request_streamed() raises RuntimeError.""" + api_client = _TemporalApiClient() + with pytest.raises(RuntimeError, match="Synchronous streaming is not supported"): + api_client.request_streamed("GET", "/test", {}) + + +def test_upload_file_raises(): + """Low-level upload_file() raises NotImplementedError.""" + api_client = _TemporalApiClient() + with pytest.raises(NotImplementedError, match="client.files.upload"): + api_client.upload_file() + + +def test_download_file_raises(): + """Low-level download_file() raises NotImplementedError.""" + api_client = _TemporalApiClient() + with pytest.raises(NotImplementedError, match="client.files.download"): + api_client.download_file() + + +# =========================================================================== +# Unit tests for activity_as_tool +# =========================================================================== + + +def test_activity_as_tool_bare_function_raises(): + """activity_as_tool rejects a function without @activity.defn.""" + + async def not_an_activity(x: str) -> str: + return x + + with pytest.raises(GoogleGenAIError, match="@activity.defn"): + activity_as_tool(not_an_activity) + + +def test_activity_as_tool_preserves_name(): + """Returned wrapper keeps the original function name.""" + wrapper = activity_as_tool(ToolCallTracker.get_weather) + assert wrapper.__name__ == "get_weather" + + +def test_activity_as_tool_preserves_doc(): + """Returned wrapper keeps the original docstring.""" + wrapper = activity_as_tool(ToolCallTracker.get_weather) + assert wrapper.__doc__ == "Get the weather for a given city." + + +def test_activity_as_tool_preserves_signature(): + """Returned wrapper has the correct parameter signature (self hidden).""" + wrapper = activity_as_tool(ToolCallTracker.get_weather) + sig = inspect.signature(wrapper) + params = list(sig.parameters.keys()) + assert params == ["city"] + + +def test_activity_as_tool_multi_arg_signature(): + """Multi-arg activity preserves all parameter names (self hidden).""" + wrapper = activity_as_tool(ToolCallTracker.get_weather_country) + sig = inspect.signature(wrapper) + params = list(sig.parameters.keys()) + assert params == ["city", "country"] + + +def test_activity_as_tool_is_async_callable(): + """Returned wrapper is an async callable.""" + wrapper = activity_as_tool(ToolCallTracker.get_weather) + assert inspect.iscoroutinefunction(wrapper) + + +# =========================================================================== +# Bound-method tool wrapping - shields workflow-method tools from google-genai's +# internal config deep-copy (>= 2.8.0), which would otherwise clone the workflow +# instance and drop in-workflow state mutations. Version-independent unit tests. +# =========================================================================== + + +class _StatefulTool: + def __init__(self) -> None: + self.calls: list[str] = [] + + async def lookup_city(self, city: str) -> str: + """Look up info about a city.""" + self.calls.append(city) + return f"{city} is great" + + +async def test_closure_if_bound_method_unbinds_and_mutates_original(): + """A bound method becomes a plain function that still mutates the real instance.""" + obj = _StatefulTool() + wrapped = cast(Callable[..., Any], _closure_if_bound_method(obj.lookup_city)) + + # No longer a bound method (so deepcopy leaves it — and its captured self — + # intact), but name/doc/signature are preserved for AFC schema building. + assert not inspect.ismethod(wrapped) + assert wrapped.__name__ == "lookup_city" + assert wrapped.__doc__ == "Look up info about a city." + assert list(inspect.signature(wrapped).parameters) == ["city"] + + assert await wrapped("Berlin") == "Berlin is great" + assert obj.calls == ["Berlin"] + + +def test_closure_survives_deepcopy_bound_method_does_not(): + """The wrapper keeps its instance across a deep-copy; a raw bound method clones it.""" + import copy + + obj = _StatefulTool() + + # Raw bound method: deepcopy clones __self__ (the 2.8.0 failure mode). + copied_method = copy.deepcopy(obj.lookup_city) + assert inspect.ismethod(copied_method) + assert copied_method.__self__ is not obj + + # Closure: deepcopy is a no-op, so the captured instance is preserved. + wrapped = _closure_if_bound_method(obj.lookup_city) + assert copy.deepcopy(wrapped) is wrapped + + +def test_closure_if_bound_method_passes_through_non_methods(): + """Plain functions and activity_as_tool wrappers are left untouched.""" + + def plain(city: str) -> str: + return city + + assert _closure_if_bound_method(plain) is plain + activity_tool = activity_as_tool(ToolCallTracker.get_weather) + assert _closure_if_bound_method(activity_tool) is activity_tool + + +def test_wrap_bound_method_tools_config_forms(): + """Config tools are wrapped for both model and dict configs, without mutating the caller.""" + obj = _StatefulTool() + + # Model config: bound method wrapped, caller's config left as-is. + config = types.GenerateContentConfig(tools=[obj.lookup_city]) + wrapped = _wrap_bound_method_tools(config) + assert isinstance(wrapped, types.GenerateContentConfig) + assert wrapped is not config + assert config.tools == [obj.lookup_city] # original untouched + assert wrapped.tools is not None + assert not inspect.ismethod(wrapped.tools[0]) + + # Dict config: same, and the original dict/list are not mutated. + dict_config: types.GenerateContentConfigDict = {"tools": [obj.lookup_city]} + wrapped_dict = _wrap_bound_method_tools(dict_config) + assert isinstance(wrapped_dict, dict) + assert dict_config.get("tools") == [obj.lookup_city] + wrapped_tools = wrapped_dict.get("tools") + assert wrapped_tools is not None + assert not inspect.ismethod(wrapped_tools[0]) + + # No bound methods -> returned unchanged (no needless copy). + def plain(city: str) -> str: + return city + + plain_config = types.GenerateContentConfig(tools=[plain]) + assert _wrap_bound_method_tools(plain_config) is plain_config + assert _wrap_bound_method_tools(None) is None + + +# =========================================================================== +# Unit tests for TemporalAsyncClient +# =========================================================================== + + +def test_temporal_async_client_vertexai_config(): + """TemporalAsyncClient() forwards Vertex AI configuration to the _TemporalApiClient.""" + result = TemporalAsyncClient(vertexai=True, project="proj", location="us-central1") + assert result._api_client.vertexai is True + assert result._api_client.project == "proj" + assert result._api_client.location == "us-central1" + + +# =========================================================================== +# Unit tests for io.IOBase text-stream rejection +# =========================================================================== + + +async def test_file_upload_text_stream_raises(): + """TemporalAsyncFiles.upload rejects text streams with a clear TypeError.""" + files = TemporalAsyncFiles(_TemporalApiClient()) + with pytest.raises( + TypeError, match="file must be a binary stream when passing an io.IOBase" + ): + await files.upload(file=io.StringIO("text")) + + +async def test_file_search_store_upload_text_stream_raises(): + """TemporalAsyncFileSearchStores.upload_to_file_search_store rejects text streams.""" + stores = TemporalAsyncFileSearchStores(_TemporalApiClient()) + with pytest.raises( + TypeError, match="file must be a binary stream when passing an io.IOBase" + ): + await stores.upload_to_file_search_store( + file_search_store_name="fileSearchStores/x", + file=io.StringIO("text"), + ) + + +# =========================================================================== +# Interactions API tests +# =========================================================================== + + +async def test_interaction_create(client: Client): + """Non-streaming interactions.create returns a typed Interaction.""" + new_client, tracker = apply_plugin(client, []) + + async with new_worker(new_client, InteractionCreateWorkflow) as worker: + result = await new_client.execute_workflow( + InteractionCreateWorkflow.run, + "What's an interaction?", + id=f"gemini-interaction-create-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + + assert result == {"id": INTERACTION_ID, "status": "completed"} + assert len(tracker.interaction_requests) == 1 + params = tracker.interaction_requests[0].params + assert params["model"] == "gemini-2.5-flash" + assert params["input"] == "What's an interaction?" + # stream selects the activity; timeout maps to start_to_close_timeout. + assert "stream" not in params + assert "timeout" not in params + + +async def test_interaction_create_stream(client: Client): + """Streamed interactions.create yields typed events in order.""" + new_client, tracker = apply_plugin(client, []) + + async with new_worker(new_client, InteractionStreamWorkflow) as worker: + result = await new_client.execute_workflow( + InteractionStreamWorkflow.run, + "Stream me", + id=f"gemini-interaction-stream-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + + assert result == [ + "interaction.created", + "step.delta", + "step.delta", + "interaction.completed", + ] + assert len(tracker.interaction_requests) == 1 + assert "stream" not in tracker.interaction_requests[0].params + + +async def test_interaction_lifecycle(client: Client): + """interactions.get/cancel/delete forward the interaction id.""" + new_client, tracker = apply_plugin(client, []) + + async with new_worker(new_client, InteractionLifecycleWorkflow) as worker: + result = await new_client.execute_workflow( + InteractionLifecycleWorkflow.run, + "interactions/abc", + id=f"gemini-interaction-lifecycle-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + + assert result["got_id"] == INTERACTION_ID + assert result["cancel_status"] == "cancelled" + assert result["deleted"] == {"deleted": True} + assert [r.id for r in tracker.interaction_id_requests] == ["interactions/abc"] * 3 + + +async def test_agents_crud(client: Client): + """agents.create/get/list/delete round-trip through activities.""" + new_client, tracker = apply_plugin(client, []) + + async with new_worker(new_client, AgentsWorkflow) as worker: + result = await new_client.execute_workflow( + AgentsWorkflow.run, + id=f"gemini-agents-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + + assert result["created_id"] == "test-agent" + assert result["got_id"] == "test-agent" + assert result["listed_ids"] == ["test-agent"] + assert result["next_page_token"] == "next-tok" + assert result["delete_response"]["id"] == "test-agent" + # create + list went through the no-id request path + assert [r.params.get("page_size") for r in tracker.interaction_requests] == [ + None, + 10, + ] + + +async def test_webhooks_unsupported(client: Client): + """client.webhooks raises a clear error inside a workflow.""" + new_client, _ = apply_plugin(client, []) + + async with new_worker(new_client, WebhooksUnsupportedWorkflow) as worker: + result = await new_client.execute_workflow( + WebhooksUnsupportedWorkflow.run, + id=f"gemini-webhooks-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + + assert "client.webhooks is not supported in Temporal workflows" in result + + +# =========================================================================== +# Unit tests for interactions helpers +# =========================================================================== + + +def test_pop_timeout_maps_to_activity_config(): + """A numeric timeout kwarg becomes the activity start_to_close_timeout.""" + from temporalio.contrib.google_genai._temporal_interactions import _pop_timeout + + config = ActivityConfig() + params: dict[str, Any] = {"model": "gemini-2.5-flash", "timeout": 120} + _pop_timeout(params, config) + assert "timeout" not in params + assert config.get("start_to_close_timeout") == timedelta(seconds=120) + + +def test_pop_timeout_rejects_non_numeric(): + """Non-numeric timeouts (e.g. httpx.Timeout) are rejected.""" + import httpx + + from temporalio.contrib.google_genai._temporal_interactions import _pop_timeout + + with pytest.raises(ValueError, match="timeout must be numeric seconds"): + _pop_timeout({"timeout": httpx.Timeout(5.0)}, ActivityConfig()) + + +# =========================================================================== +# Retry handling + error classification (Temporal owns retries) +# =========================================================================== + + +def test_plugin_rejects_client_retry_options(): + """A genai.Client with retry_options is rejected at plugin construction.""" + from google.genai.types import HttpOptions, HttpRetryOptions + + from temporalio.contrib.google_genai._google_genai_plugin import ( + _reject_sdk_retries, + ) + + client = GeminiClient( + api_key="fake-test-key", + http_options=HttpOptions(retry_options=HttpRetryOptions(attempts=3)), + ) + with pytest.raises(ValueError, match="retry_options"): + _reject_sdk_retries(client) + + +def test_plugin_allows_no_retry_options(): + """A default client (no retry_options) passes the retry check.""" + from temporalio.contrib.google_genai._google_genai_plugin import ( + _reject_sdk_retries, + ) + + _reject_sdk_retries(GeminiClient(api_key="fake-test-key")) + + +def test_process_http_options_rejects_retry_options(): + """Per-request http_options.retry_options raises in the workflow.""" + from google.genai.types import HttpOptions, HttpRetryOptions + + from temporalio.contrib.google_genai._temporal_api_client import _TemporalApiClient + + with pytest.raises(GoogleGenAIError, match="retry_options"): + _TemporalApiClient._process_http_options( + HttpOptions(retry_options=HttpRetryOptions(attempts=2)), + ActivityConfig(), + ) + + +def test_classify_api_error_retryable_vs_non_retryable(): + """Transient HTTP statuses stay retryable; client errors are non-retryable.""" + from google.genai import errors + + from temporalio.contrib.google_genai._gemini_activity import _classify_api_error + + client_err = errors.ClientError.__new__(errors.ClientError) + client_err.code = 400 + classified = _classify_api_error(client_err) + assert classified.non_retryable is True + assert classified.type == "ClientError" + + server_err = errors.ServerError.__new__(errors.ServerError) + server_err.code = 503 + assert _classify_api_error(server_err).non_retryable is False + + +def test_google_genai_error_is_application_error(): + """GoogleGenAIError is an ApplicationError so existing handling still works.""" + assert issubclass(GoogleGenAIError, ApplicationError) + + +# =========================================================================== +# Replay determinism + side-effect (activity scheduling) tests +# =========================================================================== + + +def _replay_plugin() -> GoogleGenAIPlugin: + """Build a real plugin instance for the Replayer. + + Replay never executes activities, so a fake-key client is sufficient. + What matters is that the Replayer uses the plugin's data converter, + sandbox passthrough, and workflow runner — the same configuration that + runs in production — so a history recorded by the plugin replays under + it without nondeterminism. + """ + return GoogleGenAIPlugin(GeminiClient(api_key="fake-test-key")) + + +async def test_replay_simple_generate(client: Client): + """A recorded simple generate_content history replays deterministically.""" + new_client, _ = apply_plugin(client, [make_text_response("Hello from Gemini!")]) + + async with new_worker(new_client, SimpleGenerateWorkflow) as worker: + handle = await new_client.start_workflow( + SimpleGenerateWorkflow.run, + "Say hello", + id=f"gemini-replay-simple-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + await handle.result() + history = await handle.fetch_history() + + await Replayer( + workflows=[SimpleGenerateWorkflow], + plugins=[_replay_plugin()], + ).replay_workflow(history) + + +async def test_replay_tool_loop(client: Client): + """The in-workflow AFC tool loop replays deterministically. + + The Gemini SDK's automatic-function-calling loop runs inside the + workflow, interleaving multiple activity calls with SDK-side request + formatting. That makes it the replay path most likely to surface + nondeterminism, so the recorded history is replayed under the real + plugin to prove the loop is replay-safe. + """ + tool_tracker = ToolCallTracker() + new_client, _ = apply_plugin( + client, + [ + make_function_call_response("get_weather", {"city": "Tokyo"}), + make_function_call_response( + "get_weather_country", {"city": "Paris", "country": "France"} + ), + make_text_response("Tokyo is sunny; Paris is rainy."), + ], + ) + + async with new_worker( + new_client, + MultipleToolsWorkflow, + activities=[tool_tracker.get_weather, tool_tracker.get_weather_country], + ) as worker: + handle = await new_client.start_workflow( + MultipleToolsWorkflow.run, + "Compare Tokyo and Paris weather", + id=f"gemini-replay-tools-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=15), + ) + await handle.result() + history = await handle.fetch_history() + + await Replayer( + workflows=[MultipleToolsWorkflow], + plugins=[_replay_plugin()], + ).replay_workflow(history) + + +async def test_side_effects_activity_scheduling(client: Client): + """Each Gemini API call and tool call schedules exactly one activity. + + Runs with ``max_cached_workflows=0`` so the workflow is evicted and + replayed from history between tasks — any nondeterminism in the + in-workflow AFC loop would fail the run — then asserts the exact number + of ``ActivityTaskScheduled`` events per activity type. The multi-tool + workflow issues three generate_content calls (initial + one per tool + result) and one activity per tool invocation. + """ + tool_tracker = ToolCallTracker() + new_client, _ = apply_plugin( + client, + [ + make_function_call_response("get_weather", {"city": "Tokyo"}), + make_function_call_response( + "get_weather_country", {"city": "Paris", "country": "France"} + ), + make_text_response("Tokyo is sunny; Paris is rainy."), + ], + ) + + async with new_worker( + new_client, + MultipleToolsWorkflow, + activities=[tool_tracker.get_weather, tool_tracker.get_weather_country], + max_cached_workflows=0, + ) as worker: + handle = await new_client.start_workflow( + MultipleToolsWorkflow.run, + "Compare Tokyo and Paris weather", + id=f"gemini-side-effects-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=15), + ) + await handle.result() + + scheduled: dict[str, int] = {} + async for e in handle.fetch_history_events(): + if e.HasField("activity_task_scheduled_event_attributes"): + name = e.activity_task_scheduled_event_attributes.activity_type.name + scheduled[name] = scheduled.get(name, 0) + 1 + + assert scheduled == { + "gemini_api_client_async_request": 3, + "get_weather": 1, + "get_weather_country": 1, + } diff --git a/tests/contrib/google_genai/test_gemini_mcp.py b/tests/contrib/google_genai/test_gemini_mcp.py new file mode 100644 index 000000000..04e36bb9d --- /dev/null +++ b/tests/contrib/google_genai/test_gemini_mcp.py @@ -0,0 +1,440 @@ +"""MCP integration tests for the Google Gemini SDK Temporal integration. + +Covers the client-side ``McpClientSession`` path (Gemini Developer API) routed +through ``TemporalMcpClientSession``: +- tool discovery + call through a real stdio MCP server on the worker +- worker-side connection pooling and idle eviction +- ``cache_tools`` listing frequency +- full parameter-schema propagation to the model (the MCP wire-format check) +- replay determinism and exact activity-scheduling counts + +Plus the server-side pass-through paths that need no shim code: +- Vertex AI ``Tool(mcp_servers=[McpServer(...)])`` config serialization +- Interactions API ``MCPServerToolCallStep`` / ``MCPServerToolResultStep`` rehydration +""" + +from __future__ import annotations + +import json +import sys +from collections.abc import AsyncIterator +from contextlib import AbstractAsyncContextManager, asynccontextmanager +from datetime import timedelta +from pathlib import Path +from typing import Any +from uuid import uuid4 + +import pytest +from google.genai import types +from mcp import ClientSession, StdioServerParameters +from mcp.client.stdio import stdio_client + +from temporalio import workflow +from temporalio.client import Client +from temporalio.contrib.google_genai import ( + GoogleGenAIPlugin, + TemporalAsyncClient, + TemporalMcpClientSession, +) +from temporalio.contrib.google_genai._temporal_interactions import _deserialize +from temporalio.contrib.google_genai.testing import ( + GeminiTestServer, + function_call_response, + text_response, +) +from temporalio.worker import Replayer +from temporalio.workflow import ActivityConfig +from tests.contrib.google_genai.test_gemini import ( + GeminiApiCallTracker, + make_function_call_response, + make_text_response, +) +from tests.helpers import new_worker + +_ECHO_SERVER = str(Path(__file__).parent / "echo_mcp_server.py") + + +@asynccontextmanager +async def _echo_session() -> AsyncIterator[ClientSession]: + """Yield a connected, initialized session to the stdio echo MCP server.""" + params = StdioServerParameters(command=sys.executable, args=[_ECHO_SERVER]) + async with stdio_client(params) as (read, write): + async with ClientSession(read, write) as session: + await session.initialize() + yield session + + +class _CountingFactory: + """Wraps the echo factory to count how often a connection is opened.""" + + def __init__(self) -> None: + self.opens = 0 + + def __call__(self) -> AbstractAsyncContextManager[ClientSession]: + self.opens += 1 + return _echo_session() + + +def _apply_mcp_plugin( + client: Client, + mock_responses: list[str], + mcp_servers: dict, + mcp_connection_idle_timeout: timedelta | None = None, +) -> tuple[Client, GeminiApiCallTracker]: + """Build a plugin whose API activities are faked but MCP activities are real. + + Monkeypatches ``GeminiApiCaller.activities`` (so canned generate_content + responses drive the AFC loop) while leaving the plugin's MCP activities — + built from ``mcp_servers`` — to hit the real stdio echo server. + """ + from temporalio.contrib.google_genai._gemini_activity import GeminiApiCaller + + tracker = GeminiApiCallTracker(mock_responses) + original = GeminiApiCaller.activities + GeminiApiCaller.activities = lambda self: [ # type: ignore[method-assign] + tracker.gemini_api_client_async_request, + tracker.gemini_api_client_async_request_streamed, + ] + try: + from google.genai import Client as GeminiClient + + plugin = GoogleGenAIPlugin( + GeminiClient(api_key="fake-test-key"), + mcp_servers=mcp_servers, + mcp_connection_idle_timeout=mcp_connection_idle_timeout, + ) + finally: + GeminiApiCaller.activities = original # type: ignore[method-assign] + + config = client.config() + config["plugins"] = [plugin] + return Client(**config), tracker + + +def _replay_plugin(mcp_servers: dict) -> GoogleGenAIPlugin: + from google.genai import Client as GeminiClient + + return GoogleGenAIPlugin( + GeminiClient(api_key="fake-test-key"), mcp_servers=mcp_servers + ) + + +async def _activity_names(handle: Any) -> list[str]: + names: list[str] = [] + async for e in handle.fetch_history_events(): + if e.HasField("activity_task_scheduled_event_attributes"): + names.append(e.activity_task_scheduled_event_attributes.activity_type.name) + return names + + +@pytest.fixture(autouse=True) +def _clear_mcp_connections(): # pyright: ignore[reportUnusedFunction] + """Isolate the module-global MCP connection pool between tests.""" + from temporalio.contrib.google_genai import _mcp + + _mcp._CONNECTIONS.clear() + yield + _mcp._CONNECTIONS.clear() + + +# --------------------------------------------------------------------------- +# Workflow +# --------------------------------------------------------------------------- + + +@workflow.defn +class McpToolWorkflow: + """generate_content grounded by an MCP tool, via the AFC loop. + + Takes the MCP server name as an argument so each test can use a distinct + name (the worker-side connection pool is keyed by name and shared across + activity invocations in the worker process). The number of tool calls is + driven entirely by the mocked model responses, not the workflow. + """ + + @workflow.run + async def run(self, server_name: str, prompt: str) -> str: + client = TemporalAsyncClient() + session = TemporalMcpClientSession( + server_name, + cache_tools=True, + activity_config=ActivityConfig( + start_to_close_timeout=timedelta(seconds=30) + ), + ) + response = await client.models.generate_content( + model="gemini-2.5-flash", + contents=prompt, + config=types.GenerateContentConfig(tools=[session]), + ) + return response.text or "" + + +# --------------------------------------------------------------------------- +# Client-side MCP tests +# --------------------------------------------------------------------------- + + +async def test_mcp_tool_discovery_and_call(client: Client): + """The AFC loop discovers + calls an MCP tool through activities.""" + server = "echo_basic" + new_client, _ = _apply_mcp_plugin( + client, + [ + make_function_call_response("echo", {"message": "hello"}), + make_text_response("Done!"), + ], + mcp_servers={server: _echo_session}, + ) + + async with new_worker(new_client, McpToolWorkflow) as worker: + handle = await new_client.start_workflow( + McpToolWorkflow.run, + args=[server, "echo hello"], + id=f"gemini-mcp-{uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=30), + ) + result = await handle.result() + names = await _activity_names(handle) + + assert result == "Done!" + assert names == [ + f"{server}-list-tools", + "gemini_api_client_async_request", + f"{server}-call-tool", + "gemini_api_client_async_request", + ] + + +async def test_mcp_connection_pooling(client: Client): + """Two tool calls in one workflow reuse a single worker-side connection.""" + server = "echo_pool" + factory = _CountingFactory() + new_client, _ = _apply_mcp_plugin( + client, + [ + make_function_call_response("echo", {"message": "one"}), + make_function_call_response("echo", {"message": "two"}), + make_text_response("Done!"), + ], + mcp_servers={server: factory}, + ) + + async with new_worker(new_client, McpToolWorkflow) as worker: + handle = await new_client.start_workflow( + McpToolWorkflow.run, + args=[server, "echo twice"], + id=f"gemini-mcp-pool-{uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=30), + ) + assert await handle.result() == "Done!" + names = await _activity_names(handle) + + # list-tools + two call-tools all served by one lazily-opened connection. + assert names.count(f"{server}-call-tool") == 2 + assert factory.opens == 1 + + +async def test_mcp_full_schema_propagation(client: Client): + """The model receives the MCP tool's full parameter schema, not just name.""" + server = "echo_schema" + new_client, tracker = _apply_mcp_plugin( + client, + [ + make_function_call_response("echo", {"message": "hi"}), + make_text_response("Done!"), + ], + mcp_servers={server: _echo_session}, + ) + + async with new_worker(new_client, McpToolWorkflow) as worker: + await new_client.execute_workflow( + McpToolWorkflow.run, + args=[server, "echo hi"], + id=f"gemini-mcp-schema-{uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=30), + ) + + # The first generate request carries the tool declarations the SDK built + # from the MCP list_tools result. + first = tracker.requests[0].request_dict + decls = first["tools"][0]["functionDeclarations"] # type: ignore[index] + echo_decl = next(d for d in decls if d["name"] == "echo") + assert "parameters" in echo_decl + assert "message" in echo_decl["parameters"]["properties"] + + +async def test_mcp_replay(client: Client): + """A recorded MCP tool-loop history replays deterministically.""" + server = "echo_replay" + new_client, _ = _apply_mcp_plugin( + client, + [ + make_function_call_response("echo", {"message": "hello"}), + make_text_response("Done!"), + ], + mcp_servers={server: _echo_session}, + ) + + async with new_worker(new_client, McpToolWorkflow) as worker: + handle = await new_client.start_workflow( + McpToolWorkflow.run, + args=[server, "echo hello"], + id=f"gemini-mcp-replay-{uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=30), + ) + await handle.result() + history = await handle.fetch_history() + + await Replayer( + workflows=[McpToolWorkflow], + plugins=[_replay_plugin({server: _echo_session})], + ).replay_workflow(history) + + +async def test_mcp_side_effects(client: Client): + """max_cached_workflows=0: exact ActivityTaskScheduled counts per type.""" + server = "echo_side" + new_client, _ = _apply_mcp_plugin( + client, + [ + make_function_call_response("echo", {"message": "hello"}), + make_text_response("Done!"), + ], + mcp_servers={server: _echo_session}, + ) + + async with new_worker( + new_client, McpToolWorkflow, max_cached_workflows=0 + ) as worker: + handle = await new_client.start_workflow( + McpToolWorkflow.run, + args=[server, "echo hello"], + id=f"gemini-mcp-side-effects-{uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=30), + ) + await handle.result() + names = await _activity_names(handle) + + scheduled: dict[str, int] = {} + for n in names: + scheduled[n] = scheduled.get(n, 0) + 1 + assert scheduled == { + f"{server}-list-tools": 1, + "gemini_api_client_async_request": 2, + f"{server}-call-tool": 1, + } + + +async def test_mcp_via_gemini_test_server(client: Client): + """GeminiTestServer.plugin(mcp_servers=...) scripts the model, runs MCP for real. + + This is the public testing path — the tests above reach into + ``GeminiApiCaller`` to also count API calls, but a user testing an + MCP-grounded workflow should need nothing but ``GeminiTestServer``. + """ + server = "echo_public" + test_server = GeminiTestServer( + [ + function_call_response("echo", {"message": "durable execution"}), + text_response("The echo tool returned: durable execution"), + ] + ) + + config = client.config() + config["plugins"] = [test_server.plugin(mcp_servers={server: _echo_session})] + new_client = Client(**config) + + async with new_worker(new_client, McpToolWorkflow) as worker: + handle = await new_client.start_workflow( + McpToolWorkflow.run, + args=[server, "echo the phrase: durable execution"], + id=f"gemini-mcp-public-{uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=30), + ) + result = await handle.result() + names = await _activity_names(handle) + + assert result == "The echo tool returned: durable execution" + # The MCP activities really ran; only the model HTTP layer was scripted. + assert names == [ + f"{server}-list-tools", + "gemini_api_client_async_request", + f"{server}-call-tool", + "gemini_api_client_async_request", + ] + # The echoed message reached the model as a function response. + assert any( + "durable execution" in json.dumps(request) + for request in test_server.requests[1:] + ) + + +# --------------------------------------------------------------------------- +# Server-side pass-through tests (no shim code) +# --------------------------------------------------------------------------- + + +def test_vertex_mcp_server_config_serializes(): + """Vertex server-side MCP config round-trips as plain request data.""" + tool = types.Tool( + mcp_servers=[ + types.McpServer( + name="weather", + streamable_http_transport=types.StreamableHttpTransport( + url="https://example.com/mcp", + ), + ) + ] + ) + config = types.GenerateContentConfig(tools=[tool]) + dumped = config.model_dump(mode="json", exclude_none=True) + server = dumped["tools"][0]["mcp_servers"][0] + assert server["name"] == "weather" + assert server["streamable_http_transport"]["url"] == "https://example.com/mcp" + + +def test_interactions_mcp_steps_rehydrate(): + """Interactions API MCP step payloads rehydrate via _deserialize.""" + from google.genai.interactions import InteractionSSEEvent + + call_event: Any = _deserialize( + { + "event_type": "step.start", + "index": 0, + "step": { + "type": "mcp_server_tool_call", + "id": "call-1", + "name": "lookup", + "server_name": "weather", + "arguments": {"city": "Tokyo"}, + }, + }, + InteractionSSEEvent, + ) + assert call_event.step.type == "mcp_server_tool_call" + assert call_event.step.server_name == "weather" + assert call_event.step.arguments == {"city": "Tokyo"} + + result_event: Any = _deserialize( + { + "event_type": "step.start", + "index": 0, + "step": { + "type": "mcp_server_tool_result", + "call_id": "call-1", + "name": "lookup", + "server_name": "weather", + "result": "sunny", + }, + }, + InteractionSSEEvent, + ) + assert result_event.step.type == "mcp_server_tool_result" + assert result_event.step.call_id == "call-1" diff --git a/tests/contrib/google_genai/test_gemini_streaming.py b/tests/contrib/google_genai/test_gemini_streaming.py new file mode 100644 index 000000000..e7bee8747 --- /dev/null +++ b/tests/contrib/google_genai/test_gemini_streaming.py @@ -0,0 +1,122 @@ +"""Streaming tests for the Google Gemini SDK Temporal integration. + +Covers ``generate_content_stream`` publishing each chunk to a +:class:`~temporalio.contrib.workflow_streams.WorkflowStream` topic for external +consumers, and the fail-fast when ``streaming_topic`` is set without a hosted +``WorkflowStream``. +""" + +from __future__ import annotations + +import uuid +from datetime import timedelta + +import pytest +from google.genai import types + +from temporalio import workflow +from temporalio.client import Client, WorkflowFailureError +from temporalio.contrib.google_genai import TemporalAsyncClient +from temporalio.contrib.google_genai.testing import GeminiTestServer, text_response +from temporalio.contrib.workflow_streams import WorkflowStream, WorkflowStreamClient +from tests.helpers import new_worker + + +@workflow.defn +class StreamingWorkflowStreamWorkflow: + """Streams generate_content_stream chunks to a WorkflowStream topic. + + Holds the run open on a ``finish`` signal so an external subscriber can + reliably consume the published chunk before the workflow completes. + """ + + @workflow.init + def __init__(self, prompt: str) -> None: + self.stream = WorkflowStream() + self._done = False + + @workflow.run + async def run(self, prompt: str) -> str: + client = TemporalAsyncClient(streaming_topic="gemini") + out: list[str] = [] + async for chunk in await client.models.generate_content_stream( + model="gemini-2.5-flash", + contents=prompt, + ): + out.append(chunk.text or "") + await workflow.wait_condition(lambda: self._done) + return "".join(out) + + @workflow.signal + def finish(self) -> None: + self._done = True + + +@workflow.defn +class StreamingNoStreamWorkflow: + """Sets streaming_topic but hosts no WorkflowStream — must fail fast.""" + + @workflow.run + async def run(self, prompt: str) -> str: + client = TemporalAsyncClient(streaming_topic="gemini") + async for _ in await client.models.generate_content_stream( + model="gemini-2.5-flash", + contents=prompt, + ): + pass + return "done" + + +async def test_streaming_publishes_to_workflow_stream(client: Client): + """Streamed chunks are published to the WorkflowStream for external consumers.""" + server = GeminiTestServer([text_response("Hello from Gemini stream")]) + config = client.config() + config["plugins"] = [server.plugin()] + new_client = Client(**config) + + async with new_worker(new_client, StreamingWorkflowStreamWorkflow) as worker: + wf_id = f"gemini-stream-{uuid.uuid4()}" + handle = await new_client.start_workflow( + StreamingWorkflowStreamWorkflow.run, + "say hi", + id=wf_id, + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=15), + ) + + stream = WorkflowStreamClient.create(new_client, wf_id) + received: list[types.GenerateContentResponse] = [] + async for item in stream.subscribe( + ["gemini"], + result_type=types.GenerateContentResponse, + poll_cooldown=timedelta(milliseconds=20), + ): + received.append(item.data) + break # one scripted chunk + + await handle.signal(StreamingWorkflowStreamWorkflow.finish) + result = await handle.result() + + assert result == "Hello from Gemini stream" + assert len(received) == 1 + assert received[0].text == "Hello from Gemini stream" + + +async def test_streaming_without_workflow_stream_raises(client: Client): + """streaming_topic set but no WorkflowStream hosted fails the workflow.""" + server = GeminiTestServer([text_response("unused")]) + config = client.config() + config["plugins"] = [server.plugin()] + new_client = Client(**config) + + async with new_worker(new_client, StreamingNoStreamWorkflow) as worker: + with pytest.raises(WorkflowFailureError) as exc_info: + await new_client.execute_workflow( + StreamingNoStreamWorkflow.run, + "hi", + id=f"gemini-stream-nostream-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + + assert "WorkflowStream" in str(exc_info.value.cause) diff --git a/tests/contrib/langgraph/__init__.py b/tests/contrib/langgraph/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/contrib/langgraph/e2e_functional_entrypoints.py b/tests/contrib/langgraph/e2e_functional_entrypoints.py new file mode 100644 index 000000000..516fdeb52 --- /dev/null +++ b/tests/contrib/langgraph/e2e_functional_entrypoints.py @@ -0,0 +1,145 @@ +"""Functional API entrypoint definitions for E2E tests. + +These define @task and @entrypoint functions used in functional API E2E tests. +""" + +from __future__ import annotations + +import asyncio + +import langgraph.types +from langgraph.func import entrypoint, task # pyright: ignore[reportMissingTypeStubs] + + +@task +def double_value(x: int) -> int: + return x * 2 + + +@task +def add_ten(x: int) -> int: + return x + 10 + + +@entrypoint() +async def simple_functional_entrypoint(value: int) -> dict: + doubled = await double_value(value) + result = await add_ten(doubled) + return {"result": result} + + +# Track task execution count for continue-as-new testing +_task_execution_counts: dict[str, int] = {} + + +def get_task_execution_counts() -> dict[str, int]: + return _task_execution_counts.copy() + + +def reset_task_execution_counts() -> None: + _task_execution_counts.clear() + + +@task +def expensive_task_a(x: int) -> int: + _task_execution_counts["task_a"] = _task_execution_counts.get("task_a", 0) + 1 + return x * 3 + + +@task +def expensive_task_b(x: int) -> int: + _task_execution_counts["task_b"] = _task_execution_counts.get("task_b", 0) + 1 + return x + 100 + + +@task +def expensive_task_c(x: int) -> int: + _task_execution_counts["task_c"] = _task_execution_counts.get("task_c", 0) + 1 + return x * 2 + + +@entrypoint() +async def continue_as_new_entrypoint(value: int) -> dict: + """For input 10: 10 * 3 = 30 -> 30 + 100 = 130 -> 130 * 2 = 260""" + result_a = await expensive_task_a(value) + result_b = await expensive_task_b(result_a) + result_c = await expensive_task_c(result_b) + return {"result": result_c} + + +@task +def step_1(x: int) -> int: + _task_execution_counts["step_1"] = _task_execution_counts.get("step_1", 0) + 1 + return x * 2 + + +@task +def step_2(x: int) -> int: + _task_execution_counts["step_2"] = _task_execution_counts.get("step_2", 0) + 1 + return x + 5 + + +@task +def step_3(x: int) -> int: + _task_execution_counts["step_3"] = _task_execution_counts.get("step_3", 0) + 1 + return x * 3 + + +@task +def step_4(x: int) -> int: + _task_execution_counts["step_4"] = _task_execution_counts.get("step_4", 0) + 1 + return x - 10 + + +@task +def step_5(x: int) -> int: + _task_execution_counts["step_5"] = _task_execution_counts.get("step_5", 0) + 1 + return x + 100 + + +@entrypoint() +async def partial_execution_entrypoint(input_data: dict) -> dict: + """For value=10, all 5 tasks: 10*2=20 -> +5=25 -> *3=75 -> -10=65 -> +100=165""" + value = input_data["value"] + stop_after = input_data.get("stop_after", 5) + + result = value + result = await step_1(result) + if stop_after == 1: + return {"result": result, "completed_tasks": 1} + result = await step_2(result) + if stop_after == 2: + return {"result": result, "completed_tasks": 2} + result = await step_3(result) + if stop_after == 3: + return {"result": result, "completed_tasks": 3} + result = await step_4(result) + if stop_after == 4: + return {"result": result, "completed_tasks": 4} + result = await step_5(result) + return {"result": result, "completed_tasks": 5} + + +@task +def ask_human(question: str) -> str: + return langgraph.types.interrupt(question) + + +@entrypoint() +async def interrupt_entrypoint(value: str) -> dict: + """Entrypoint that interrupts for human input, then returns the answer.""" + answer = await ask_human("Do you approve?") + return {"input": value, "answer": answer} + + +@task +async def waiting_task(x: int) -> int: + # Wait (forever) until start_to_close_timeout or worker shutdown cancellation + await asyncio.Event().wait() + return x + + +@entrypoint() +async def slow_entrypoint(value: int) -> dict: + result = await waiting_task(value) + return {"result": result} diff --git a/tests/contrib/langgraph/e2e_functional_workflows.py b/tests/contrib/langgraph/e2e_functional_workflows.py new file mode 100644 index 000000000..d355bdb28 --- /dev/null +++ b/tests/contrib/langgraph/e2e_functional_workflows.py @@ -0,0 +1,97 @@ +"""Workflow definitions for Functional API E2E tests.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from temporalio import workflow +from temporalio.contrib.langgraph import cache, entrypoint + + +@workflow.defn +class SimpleFunctionalE2EWorkflow: + def __init__(self) -> None: + self.app = entrypoint("e2e_simple_functional") + + @workflow.run + async def run(self, input_value: int) -> dict: + return await self.app.ainvoke(input_value) + + +@workflow.defn +class SlowFunctionalWorkflow: + def __init__(self) -> None: + self.app = entrypoint("e2e_slow_functional") + + @workflow.run + async def run(self, input_value: int) -> dict: + return await self.app.ainvoke(input_value) + + +@dataclass +class ContinueAsNewInput: + value: int + cache: dict[str, Any] | None = None + task_a_done: bool = False + task_b_done: bool = False + + +@workflow.defn +class ContinueAsNewFunctionalWorkflow: + """Continues-as-new after each phase, passing cache for task deduplication.""" + + @workflow.run + async def run(self, input_data: ContinueAsNewInput) -> dict[str, Any]: + app = entrypoint("e2e_continue_as_new_functional", cache=input_data.cache) + + result = await app.ainvoke(input_data.value) + + if not input_data.task_a_done: + workflow.continue_as_new( + ContinueAsNewInput( + value=input_data.value, + cache=cache(), + task_a_done=True, + ) + ) + + if not input_data.task_b_done: + workflow.continue_as_new( + ContinueAsNewInput( + value=input_data.value, + cache=cache(), + task_a_done=True, + task_b_done=True, + ) + ) + + return result + + +@dataclass +class PartialExecutionInput: + value: int + cache: dict[str, Any] | None = None + phase: int = 1 + + +@workflow.defn +class PartialExecutionWorkflow: + """Phase 1: 3 tasks + cache. Phase 2: all 5 (1-3 cached).""" + + @workflow.run + async def run(self, input_data: PartialExecutionInput) -> dict[str, Any]: + app = entrypoint("e2e_partial_execution", cache=input_data.cache) + + if input_data.phase == 1: + await app.ainvoke({"value": input_data.value, "stop_after": 3}) + workflow.continue_as_new( + PartialExecutionInput( + value=input_data.value, + cache=cache(), + phase=2, + ) + ) + + return await app.ainvoke({"value": input_data.value, "stop_after": 5}) diff --git a/tests/contrib/langgraph/test_command.py b/tests/contrib/langgraph/test_command.py new file mode 100644 index 000000000..59a2fd68b --- /dev/null +++ b/tests/contrib/langgraph/test_command.py @@ -0,0 +1,68 @@ +from datetime import timedelta +from typing import Any, Literal +from uuid import uuid4 + +from langgraph.graph import ( # pyright: ignore[reportMissingTypeStubs] + START, + StateGraph, +) +from langgraph.types import Command +from typing_extensions import TypedDict + +from temporalio import workflow +from temporalio.client import Client +from temporalio.contrib.langgraph import LangGraphPlugin, graph +from temporalio.worker import Worker + + +class State(TypedDict): + value: str + + +def node_a(state: State) -> Command[Literal["node_b"]]: + return Command(update={"value": state["value"] + "a"}, goto="node_b") + + +def node_b(state: State) -> Command[Literal["__end__"]]: + return Command(update={"value": state["value"] + "b"}, goto="__end__") + + +@workflow.defn +class CommandWorkflow: + def __init__(self) -> None: + self.app = graph("my-graph").compile() + + @workflow.run + async def run(self, input: str) -> Any: + return await self.app.ainvoke({"value": input}) + + +async def test_command_goto_and_update(client: Client): + g = StateGraph(State) + g.add_node("node_a", node_a, metadata={"execute_in": "activity"}) + g.add_node("node_b", node_b, metadata={"execute_in": "activity"}) + g.add_edge(START, "node_a") + + task_queue = f"command-{uuid4()}" + + async with Worker( + client, + task_queue=task_queue, + workflows=[CommandWorkflow], + plugins=[ + LangGraphPlugin( + graphs={"my-graph": g}, + default_activity_options={ + "start_to_close_timeout": timedelta(seconds=10) + }, + ) + ], + ): + result = await client.execute_workflow( + CommandWorkflow.run, + "", + id=f"test-command-{uuid4()}", + task_queue=task_queue, + ) + + assert result == {"value": "ab"} diff --git a/tests/contrib/langgraph/test_continue_as_new.py b/tests/contrib/langgraph/test_continue_as_new.py new file mode 100644 index 000000000..304862361 --- /dev/null +++ b/tests/contrib/langgraph/test_continue_as_new.py @@ -0,0 +1,72 @@ +from datetime import timedelta +from typing import Any +from uuid import uuid4 + +from langgraph.checkpoint.memory import InMemorySaver +from langgraph.graph import START, StateGraph # pyright: ignore[reportMissingTypeStubs] +from langgraph.graph.state import ( # pyright: ignore[reportMissingTypeStubs] + RunnableConfig, +) +from typing_extensions import TypedDict + +from temporalio import workflow +from temporalio.client import Client +from temporalio.contrib.langgraph import LangGraphPlugin, graph +from temporalio.worker import Worker + + +class State(TypedDict): + value: str + + +async def node(state: State) -> dict[str, str]: + return {"value": state["value"] + "a"} + + +@workflow.defn +class ContinueAsNewWorkflow: + def __init__(self) -> None: + self.app = graph("my-graph").compile(checkpointer=InMemorySaver()) + + @workflow.run + async def run(self, values: State) -> Any: + config = RunnableConfig({"configurable": {"thread_id": "1"}}) + + await self.app.aupdate_state(config, values) + await self.app.ainvoke(values, config) + + if len(values["value"]) < 3: + state = await self.app.aget_state(config) + workflow.continue_as_new(state.values) + + return values + + +async def test_continue_as_new(client: Client): + g = StateGraph(State) + g.add_node("node", node, metadata={"execute_in": "activity"}) + g.add_edge(START, "node") + + task_queue = f"my-graph-{uuid4()}" + + async with Worker( + client, + task_queue=task_queue, + workflows=[ContinueAsNewWorkflow], + plugins=[ + LangGraphPlugin( + graphs={"my-graph": g}, + default_activity_options={ + "start_to_close_timeout": timedelta(seconds=10) + }, + ) + ], + ): + result = await client.execute_workflow( + ContinueAsNewWorkflow.run, + State(value=""), + id=f"test-workflow-{uuid4()}", + task_queue=task_queue, + ) + + assert result == {"value": "aaa"} diff --git a/tests/contrib/langgraph/test_continue_as_new_cached.py b/tests/contrib/langgraph/test_continue_as_new_cached.py new file mode 100644 index 000000000..af41f384f --- /dev/null +++ b/tests/contrib/langgraph/test_continue_as_new_cached.py @@ -0,0 +1,131 @@ +"""Test Graph API continue-as-new with task result caching. + +Verifies that node results are cached across continue-as-new boundaries, +so nodes don't re-execute when the graph is re-invoked with the same state. +""" + +from dataclasses import dataclass +from datetime import timedelta +from typing import Any +from uuid import uuid4 + +from langgraph.graph import START, StateGraph # pyright: ignore[reportMissingTypeStubs] +from typing_extensions import TypedDict + +from temporalio import workflow +from temporalio.client import Client +from temporalio.contrib.langgraph import LangGraphPlugin, cache, graph +from temporalio.worker import Worker + +# Track execution counts to verify caching +_execution_counts: dict[str, int] = {} + + +def _reset(): + _execution_counts.clear() + + +class State(TypedDict): + value: int + + +async def multiply_by_3(state: State) -> dict[str, int]: + _execution_counts["multiply"] = _execution_counts.get("multiply", 0) + 1 + return {"value": state["value"] * 3} + + +async def add_100(state: State) -> dict[str, int]: + _execution_counts["add"] = _execution_counts.get("add", 0) + 1 + return {"value": state["value"] + 100} + + +async def double(state: State) -> dict[str, int]: + _execution_counts["double"] = _execution_counts.get("double", 0) + 1 + return {"value": state["value"] * 2} + + +@dataclass +class GraphContinueAsNewInput: + value: int + cache: dict[str, Any] | None = None + phase: int = 1 # 1, 2, 3 — continues-as-new after phases 1 and 2 + + +@workflow.defn +class GraphContinueAsNewWorkflow: + """Runs a 3-node graph, continuing-as-new after each phase. + + Phase 1: runs graph (all 3 nodes execute), continues-as-new with cache. + Phase 2: runs graph again with same input (all 3 cached), continues-as-new. + Phase 3: runs graph again with same input (all 3 cached), returns result. + + Without caching: each node executes 3 times. + With caching: each node executes once (first run), cached for phases 2 & 3. + """ + + @workflow.run + async def run(self, input_data: GraphContinueAsNewInput) -> dict[str, int]: + app = graph("cached-graph", cache=input_data.cache).compile() + result = await app.ainvoke({"value": input_data.value}) + + if input_data.phase < 3: + workflow.continue_as_new( + GraphContinueAsNewInput( + value=input_data.value, + cache=cache(), + phase=input_data.phase + 1, + ) + ) + + return result + + +async def test_graph_continue_as_new_cached(client: Client): + """Each node executes once despite 3 continue-as-new cycles. + + Graph: multiply_by_3 -> add_100 -> double + Input 10: 10 * 3 = 30 -> 30 + 100 = 130 -> 130 * 2 = 260 + """ + _reset() + + metadata = { + "execute_in": "activity", + "start_to_close_timeout": timedelta(seconds=10), + } + g = StateGraph(State) + g.add_node("multiply_by_3", multiply_by_3, metadata=metadata) + g.add_node("add_100", add_100, metadata=metadata) + g.add_node("double", double, metadata=metadata) + g.add_edge(START, "multiply_by_3") + g.add_edge("multiply_by_3", "add_100") + g.add_edge("add_100", "double") + + task_queue = f"graph-cached-{uuid4()}" + + async with Worker( + client, + task_queue=task_queue, + workflows=[GraphContinueAsNewWorkflow], + plugins=[LangGraphPlugin(graphs={"cached-graph": g})], + ): + result = await client.execute_workflow( + GraphContinueAsNewWorkflow.run, + GraphContinueAsNewInput(value=10), + id=f"graph-cached-{uuid4()}", + task_queue=task_queue, + execution_timeout=timedelta(seconds=60), + ) + + # 10 * 3 = 30 -> + 100 = 130 -> * 2 = 260 + assert result == {"value": 260} + + # Each node should execute exactly once — phases 2 and 3 use cached results. + assert _execution_counts.get("multiply", 0) == 1, ( + f"multiply executed {_execution_counts.get('multiply', 0)} times, expected 1" + ) + assert _execution_counts.get("add", 0) == 1, ( + f"add executed {_execution_counts.get('add', 0)} times, expected 1" + ) + assert _execution_counts.get("double", 0) == 1, ( + f"double executed {_execution_counts.get('double', 0)} times, expected 1" + ) diff --git a/tests/contrib/langgraph/test_e2e_functional.py b/tests/contrib/langgraph/test_e2e_functional.py new file mode 100644 index 000000000..d10efb483 --- /dev/null +++ b/tests/contrib/langgraph/test_e2e_functional.py @@ -0,0 +1,338 @@ +"""End-to-end tests for LangGraph Functional API integration (v1 and v2). + +Requires a running Temporal test server (started by conftest.py). +LangGraph's Functional API requires Python >= 3.11 for async context +variable propagation (see langgraph.config.get_config). +""" + +from __future__ import annotations + +import sys +from datetime import timedelta +from typing import Any +from uuid import uuid4 + +import pytest + +pytestmark = pytest.mark.skipif( + sys.version_info < (3, 11), + reason="LangGraph Functional API requires Python >= 3.11 for async context propagation", +) +from langchain_core.runnables import RunnableConfig +from langgraph.checkpoint.memory import InMemorySaver +from langgraph.func import ( # pyright: ignore[reportMissingTypeStubs] + entrypoint as lg_entrypoint, +) +from langgraph.func import task # pyright: ignore[reportMissingTypeStubs] +from langgraph.types import Command +from pytest import raises + +from temporalio import workflow +from temporalio.client import Client, WorkflowFailureError +from temporalio.common import RetryPolicy +from temporalio.contrib.langgraph import LangGraphPlugin, entrypoint +from temporalio.worker import Worker +from tests.contrib.langgraph.e2e_functional_entrypoints import ( + add_ten, + ask_human, + continue_as_new_entrypoint, + double_value, + expensive_task_a, + expensive_task_b, + expensive_task_c, + get_task_execution_counts, + interrupt_entrypoint, + partial_execution_entrypoint, + reset_task_execution_counts, + simple_functional_entrypoint, + slow_entrypoint, + step_1, + step_2, + step_3, + step_4, + step_5, + waiting_task, +) +from tests.contrib.langgraph.e2e_functional_workflows import ( + ContinueAsNewFunctionalWorkflow, + ContinueAsNewInput, + PartialExecutionInput, + PartialExecutionWorkflow, + SimpleFunctionalE2EWorkflow, + SlowFunctionalWorkflow, +) + +_DEFAULT_ACTIVITY_OPTIONS = {"start_to_close_timeout": timedelta(seconds=30)} + + +def _execute_in_activity(*task_names: str) -> dict[str, dict[str, Any]]: + return {name: {"execute_in": "activity"} for name in task_names} + + +# V2-only tasks defined here to avoid sharing mutated _TaskFunction objects +# (Plugin wraps task.func in-place). + + +@task +def triple_value(x: int) -> int: + return x * 3 + + +@task +def add_five(x: int) -> int: + return x + 5 + + +@lg_entrypoint() +async def simple_v2_entrypoint(value: int) -> dict: + tripled = await triple_value(value) + result = await add_five(tripled) + return {"result": result} + + +@workflow.defn +class SimpleV2Workflow: + def __init__(self) -> None: + self.app = entrypoint("v2_simple") + + @workflow.run + async def run(self, input_value: int) -> dict[str, Any]: + result = await self.app.ainvoke(input_value, version="v2") + return result.value + + +@workflow.defn +class InterruptV2FunctionalWorkflow: + def __init__(self) -> None: + self.app = entrypoint("v2_interrupt") + self.app.checkpointer = InMemorySaver() + + @workflow.run + async def run(self, input_value: str) -> dict[str, Any]: + config = RunnableConfig( + {"configurable": {"thread_id": workflow.info().workflow_id}} + ) + + result = await self.app.ainvoke(input_value, config, version="v2") + + assert result.value == {} + assert len(result.interrupts) == 1 + assert result.interrupts[0].value == "Do you approve?" + + resumed = await self.app.ainvoke( + Command(resume="approved"), config, version="v2" + ) + return resumed.value + + +class TestFunctionalAPIBasicExecution: + @pytest.mark.parametrize( + "workflow_cls,entrypoint_func,entrypoint_name,tasks,expected_result", + [ + ( + SimpleFunctionalE2EWorkflow, + simple_functional_entrypoint, + "e2e_simple_functional", + [double_value, add_ten], + 30, + ), + ( + SimpleV2Workflow, + simple_v2_entrypoint, + "v2_simple", + [triple_value, add_five], + 35, + ), + ], + ids=["v1", "v2"], + ) + async def test_simple_entrypoint( + self, + client: Client, + workflow_cls: Any, + entrypoint_func: Any, + entrypoint_name: str, + tasks: list, + expected_result: int, + ) -> None: + task_queue = f"e2e-functional-{uuid4()}" + + async with Worker( + client, + task_queue=task_queue, + workflows=[workflow_cls], + plugins=[ + LangGraphPlugin( + entrypoints={entrypoint_name: entrypoint_func}, + tasks=tasks, + activity_options=_execute_in_activity( + *(t.func.__name__ for t in tasks) + ), + default_activity_options=_DEFAULT_ACTIVITY_OPTIONS, + ) + ], + ): + result = await client.execute_workflow( + workflow_cls.run, + 10, + id=f"e2e-functional-{uuid4()}", + task_queue=task_queue, + execution_timeout=timedelta(seconds=30), + ) + + assert result["result"] == expected_result + + +class TestFunctionalAPIContinueAsNew: + async def test_continue_as_new_with_checkpoint(self, client: Client) -> None: + """10 * 3 = 30 -> + 100 = 130 -> * 2 = 260. Each task executes once.""" + reset_task_execution_counts() + + tasks = [expensive_task_a, expensive_task_b, expensive_task_c] + task_queue = f"e2e-continue-as-new-{uuid4()}" + + async with Worker( + client, + task_queue=task_queue, + workflows=[ContinueAsNewFunctionalWorkflow], + plugins=[ + LangGraphPlugin( + entrypoints={ + "e2e_continue_as_new_functional": continue_as_new_entrypoint + }, + tasks=tasks, + activity_options=_execute_in_activity( + *(getattr(t.func, "__name__") for t in tasks) + ), + default_activity_options=_DEFAULT_ACTIVITY_OPTIONS, + ) + ], + ): + result = await client.execute_workflow( + ContinueAsNewFunctionalWorkflow.run, + ContinueAsNewInput(value=10), + id=f"e2e-continue-as-new-{uuid4()}", + task_queue=task_queue, + execution_timeout=timedelta(seconds=60), + ) + + assert result["result"] == 260 + + counts = get_task_execution_counts() + assert counts.get("task_a", 0) == 1, ( + f"task_a executed {counts.get('task_a', 0)} times, expected 1" + ) + assert counts.get("task_b", 0) == 1, ( + f"task_b executed {counts.get('task_b', 0)} times, expected 1" + ) + assert counts.get("task_c", 0) == 1, ( + f"task_c executed {counts.get('task_c', 0)} times, expected 1" + ) + + +class TestFunctionalAPIPartialExecution: + async def test_partial_execution_five_tasks(self, client: Client) -> None: + """10*2=20 -> +5=25 -> *3=75 -> -10=65 -> +100=165. Each task executes once.""" + reset_task_execution_counts() + + tasks = [step_1, step_2, step_3, step_4, step_5] + task_queue = f"e2e-partial-{uuid4()}" + + async with Worker( + client, + task_queue=task_queue, + workflows=[PartialExecutionWorkflow], + plugins=[ + LangGraphPlugin( + entrypoints={"e2e_partial_execution": partial_execution_entrypoint}, + tasks=tasks, + activity_options=_execute_in_activity( + *(getattr(t.func, "__name__") for t in tasks) + ), + default_activity_options=_DEFAULT_ACTIVITY_OPTIONS, + ) + ], + ): + result = await client.execute_workflow( + PartialExecutionWorkflow.run, + PartialExecutionInput(value=10), + id=f"e2e-partial-{uuid4()}", + task_queue=task_queue, + execution_timeout=timedelta(seconds=60), + ) + + assert result["result"] == 165 + assert result["completed_tasks"] == 5 + + counts = get_task_execution_counts() + for i in range(1, 6): + assert counts.get(f"step_{i}", 0) == 1, ( + f"step_{i} executed {counts.get(f'step_{i}', 0)} times, expected 1" + ) + + +class TestFunctionalAPIInterruptV2: + async def test_interrupt_v2_functional(self, client: Client) -> None: + """version='v2' separates interrupts from value in functional API.""" + tasks = [ask_human] + task_queue = f"v2-interrupt-{uuid4()}" + + async with Worker( + client, + task_queue=task_queue, + workflows=[InterruptV2FunctionalWorkflow], + plugins=[ + LangGraphPlugin( + entrypoints={"v2_interrupt": interrupt_entrypoint}, + tasks=tasks, + activity_options=_execute_in_activity( + *(getattr(t.func, "__name__") for t in tasks) + ), + default_activity_options=_DEFAULT_ACTIVITY_OPTIONS, + ) + ], + ): + result = await client.execute_workflow( + InterruptV2FunctionalWorkflow.run, + "hello", + id=f"v2-interrupt-{uuid4()}", + task_queue=task_queue, + execution_timeout=timedelta(seconds=30), + ) + + assert result["input"] == "hello" + assert result["answer"] == "approved" + + +class TestFunctionalAPIPerTaskOptions: + async def test_per_task_activity_options_override(self, client: Client) -> None: + """activity_options[task_name] overrides default_activity_options for that task.""" + task_queue = f"e2e-per-task-options-{uuid4()}" + + async with Worker( + client, + task_queue=task_queue, + workflows=[SlowFunctionalWorkflow], + plugins=[ + LangGraphPlugin( + entrypoints={"e2e_slow_functional": slow_entrypoint}, + tasks=[waiting_task], + default_activity_options=_DEFAULT_ACTIVITY_OPTIONS, + activity_options={ + "waiting_task": { + "execute_in": "activity", + "start_to_close_timeout": timedelta(milliseconds=100), + "retry_policy": RetryPolicy(maximum_attempts=1), + } + }, + ) + ], + ): + with raises(WorkflowFailureError): + await client.execute_workflow( + SlowFunctionalWorkflow.run, + 1, + id=f"e2e-per-task-options-{uuid4()}", + task_queue=task_queue, + execution_timeout=timedelta(seconds=30), + ) diff --git a/tests/contrib/langgraph/test_execute_in_workflow.py b/tests/contrib/langgraph/test_execute_in_workflow.py new file mode 100644 index 000000000..15b44f5a9 --- /dev/null +++ b/tests/contrib/langgraph/test_execute_in_workflow.py @@ -0,0 +1,51 @@ +from typing import Any +from uuid import uuid4 + +from langgraph.graph import START, StateGraph # pyright: ignore[reportMissingTypeStubs] +from typing_extensions import TypedDict + +from temporalio import workflow +from temporalio.client import Client +from temporalio.contrib.langgraph import LangGraphPlugin, graph +from temporalio.worker import Worker + + +class State(TypedDict): + value: str + + +async def node(state: State) -> dict[str, str]: # pyright: ignore[reportUnusedParameter] + return {"value": "done"} + + +@workflow.defn +class ExecuteInWorkflowWorkflow: + def __init__(self) -> None: + self.app = graph("my-graph").compile() + + @workflow.run + async def run(self, input: str) -> Any: + return await self.app.ainvoke({"value": input}) + + +async def test_execute_in_workflow(client: Client): + g = StateGraph(State) + g.add_node("node", node, metadata={"execute_in": "workflow"}) + g.add_edge(START, "node") + + task_queue = f"my-graph-{uuid4()}" + + async with Worker( + client, + task_queue=task_queue, + workflows=[ExecuteInWorkflowWorkflow], + plugins=[LangGraphPlugin(graphs={"my-graph": g})], + ): + result = await client.execute_workflow( + ExecuteInWorkflowWorkflow.run, + "", + id=f"test-workflow-{uuid4()}", + task_queue=task_queue, + ) + + assert result == {"value": "done"} diff --git a/tests/contrib/langgraph/test_interrupt.py b/tests/contrib/langgraph/test_interrupt.py new file mode 100644 index 000000000..6d4547d76 --- /dev/null +++ b/tests/contrib/langgraph/test_interrupt.py @@ -0,0 +1,97 @@ +import sys +from datetime import timedelta +from typing import Any +from uuid import uuid4 + +import langgraph.types +import pytest + +pytestmark = pytest.mark.skipif( + sys.version_info < (3, 11), + reason="langgraph.types.interrupt() requires Python >= 3.11 for async context propagation", +) +from langgraph.checkpoint.memory import InMemorySaver +from langgraph.graph import START, StateGraph # pyright: ignore[reportMissingTypeStubs] +from langgraph.graph.state import ( # pyright: ignore[reportMissingTypeStubs] + RunnableConfig, +) +from typing_extensions import TypedDict + +from temporalio import workflow +from temporalio.client import Client +from temporalio.contrib.langgraph import LangGraphPlugin, graph +from temporalio.worker import Worker + + +class State(TypedDict): + value: str + + +async def node(state: State) -> dict[str, str]: # pyright: ignore[reportUnusedParameter] + return {"value": langgraph.types.interrupt("Continue?")} + + +@workflow.defn +class InterruptWorkflow: + def __init__(self) -> None: + self.app = graph("my-graph").compile(checkpointer=InMemorySaver()) + + @workflow.run + async def run(self, input: str) -> Any: + config = RunnableConfig({"configurable": {"thread_id": "1"}}) + + result = await self.app.ainvoke({"value": input}, config) + assert result["__interrupt__"][0].value == "Continue?" + + return await self.app.ainvoke(langgraph.types.Command(resume="yes"), config) + + +@workflow.defn +class InterruptV2Workflow: + def __init__(self) -> None: + self.app = graph("my-graph").compile(checkpointer=InMemorySaver()) + + @workflow.run + async def run(self, input: str) -> Any: + config = RunnableConfig({"configurable": {"thread_id": "1"}}) + + result = await self.app.ainvoke({"value": input}, config, version="v2") + + assert result.value == {"value": ""} + assert len(result.interrupts) == 1 + assert result.interrupts[0].value == "Continue?" + + return await self.app.ainvoke(langgraph.types.Command(resume="yes"), config) + + +@pytest.mark.parametrize( + "workflow_cls", [InterruptWorkflow, InterruptV2Workflow], ids=["v1", "v2"] +) +async def test_interrupt(client: Client, workflow_cls: Any) -> None: + g = StateGraph(State) + g.add_node("node", node, metadata={"execute_in": "activity"}) + g.add_edge(START, "node") + + task_queue = f"interrupt-{uuid4()}" + + async with Worker( + client, + task_queue=task_queue, + workflows=[workflow_cls], + plugins=[ + LangGraphPlugin( + graphs={"my-graph": g}, + default_activity_options={ + "start_to_close_timeout": timedelta(seconds=10) + }, + ) + ], + ): + result = await client.execute_workflow( + workflow_cls.run, + "", + id=f"test-workflow-{uuid4()}", + task_queue=task_queue, + ) + + assert result == {"value": "yes"} diff --git a/tests/contrib/langgraph/test_node_metadata.py b/tests/contrib/langgraph/test_node_metadata.py new file mode 100644 index 000000000..ca022d3a1 --- /dev/null +++ b/tests/contrib/langgraph/test_node_metadata.py @@ -0,0 +1,65 @@ +from datetime import timedelta +from typing import Any +from uuid import uuid4 + +from langchain_core.runnables import ( + RunnableConfig, # pyright: ignore[reportMissingTypeStubs] +) +from langgraph.graph import START, StateGraph # pyright: ignore[reportMissingTypeStubs] +from typing_extensions import TypedDict + +from temporalio import workflow +from temporalio.client import Client +from temporalio.contrib.langgraph import LangGraphPlugin +from temporalio.worker import Worker + + +class State(TypedDict): + value: str + + +async def node(state: State, config: RunnableConfig) -> dict[str, str]: + metadata = config.get("metadata") or {} + return {"value": state["value"] + str(metadata.get("my_key", "NOT_FOUND"))} + + +metadata_graph: StateGraph[State, None, State, State] = StateGraph(State) +metadata_graph.add_node( + "node", + node, + metadata={ + "execute_in": "activity", + "start_to_close_timeout": timedelta(seconds=10), + "my_key": "my_value", + }, +) +metadata_graph.add_edge(START, "node") + + +@workflow.defn +class NodeMetadataWorkflow: + def __init__(self) -> None: + self.app = metadata_graph.compile() + + @workflow.run + async def run(self, input: str) -> Any: + return await self.app.ainvoke({"value": input}) + + +async def test_node_metadata_readable_in_node(client: Client): + task_queue = f"my-graph-{uuid4()}" + + async with Worker( + client, + task_queue=task_queue, + workflows=[NodeMetadataWorkflow], + plugins=[LangGraphPlugin(graphs={"my-graph": metadata_graph})], + ): + result = await client.execute_workflow( + NodeMetadataWorkflow.run, + "prefix-", + id=f"test-workflow-{uuid4()}", + task_queue=task_queue, + ) + + assert result == {"value": "prefix-my_value"} diff --git a/tests/contrib/langgraph/test_plugin_validation.py b/tests/contrib/langgraph/test_plugin_validation.py new file mode 100644 index 000000000..5b66c2241 --- /dev/null +++ b/tests/contrib/langgraph/test_plugin_validation.py @@ -0,0 +1,89 @@ +"""Tests for LangGraphPlugin validation.""" + +from __future__ import annotations + +from typing import Any +from uuid import uuid4 + +from langchain_core.runnables import RunnableLambda +from langgraph.func import task # pyright: ignore[reportMissingTypeStubs] +from langgraph.graph import START, StateGraph # pyright: ignore[reportMissingTypeStubs] +from langgraph.types import RetryPolicy # pyright: ignore[reportMissingTypeStubs] +from pytest import raises +from typing_extensions import TypedDict + +from temporalio.contrib.langgraph import LangGraphPlugin + + +class State(TypedDict): + value: str + + +async def async_node(state: State) -> dict[str, str]: # pyright: ignore[reportUnusedParameter] + return {"value": "done"} + + +def sync_node(state: State) -> dict[str, str]: # pyright: ignore[reportUnusedParameter] + return {"value": "done"} + + +def test_non_runnable_callable_node_raises() -> None: + """Nodes whose runnable isn't a RunnableCallable can't be wrapped as activities.""" + g = StateGraph(State) + g.add_node("node", RunnableLambda(sync_node)) + g.add_edge(START, "node") + + with raises(ValueError, match="must be a RunnableCallable"): + LangGraphPlugin(graphs={f"validation-{uuid4()}": g}) + + +def test_invalid_execute_in_raises() -> None: + g = StateGraph(State) + g.add_node("node", async_node, metadata={"execute_in": "bogus"}) + g.add_edge(START, "node") + + with raises(ValueError, match="Invalid execute_in value"): + LangGraphPlugin(graphs={f"validation-{uuid4()}": g}) + + +def test_graph_node_missing_execute_in_raises() -> None: + g = StateGraph(State) + g.add_node("node", async_node) + g.add_edge(START, "node") + + with raises(ValueError, match="missing required 'execute_in'"): + LangGraphPlugin(graphs={f"validation-{uuid4()}": g}) + + +def test_functional_task_missing_execute_in_raises() -> None: + @task + def my_task(x: int) -> int: + return x + 1 + + with raises(ValueError, match="missing required 'execute_in'"): + LangGraphPlugin(tasks=[my_task]) + + +def test_execute_in_in_default_activity_options_raises() -> None: + with raises(ValueError, match="cannot be set in default_activity_options"): + LangGraphPlugin(default_activity_options={"execute_in": "activity"}) + + +def test_node_retry_policy_raises() -> None: + g = StateGraph(State) + g.add_node("node", async_node, retry_policy=RetryPolicy(max_attempts=3)) + g.add_edge(START, "node") + + with raises(ValueError, match="retry_policy"): + LangGraphPlugin(graphs={f"validation-{uuid4()}": g}) + + +def test_task_retry_policy_raises() -> None: + decorator: Any = task(retry_policy=RetryPolicy(max_attempts=3)) + + @decorator + def my_task(x: int) -> int: + return x + 1 + + with raises(ValueError, match="retry_policy"): + LangGraphPlugin(tasks=[my_task]) diff --git a/tests/contrib/langgraph/test_replay.py b/tests/contrib/langgraph/test_replay.py new file mode 100644 index 000000000..f5d1a8e92 --- /dev/null +++ b/tests/contrib/langgraph/test_replay.py @@ -0,0 +1,93 @@ +import sys +from datetime import timedelta +from uuid import uuid4 + +import pytest +from langgraph.graph import START, StateGraph # pyright: ignore[reportMissingTypeStubs] + +from temporalio.client import Client +from temporalio.contrib.langgraph import LangGraphPlugin +from temporalio.worker import Replayer, Worker +from tests.contrib.langgraph.test_interrupt import ( + InterruptWorkflow, +) +from tests.contrib.langgraph.test_interrupt import ( + State as InterruptState, +) +from tests.contrib.langgraph.test_interrupt import ( + node as interrupt_node, +) +from tests.contrib.langgraph.test_two_nodes import ( + State, + TwoNodesWorkflow, + node_a, + node_b, +) + + +async def test_replay(client: Client): + g = StateGraph(State) + g.add_node("node_a", node_a, metadata={"execute_in": "activity"}) + g.add_node("node_b", node_b, metadata={"execute_in": "activity"}) + g.add_edge(START, "node_a") + g.add_edge("node_a", "node_b") + + task_queue = f"my-graph-{uuid4()}" + plugin = LangGraphPlugin( + graphs={"my-graph": g}, + default_activity_options={"start_to_close_timeout": timedelta(seconds=10)}, + ) + + async with Worker( + client, + task_queue=task_queue, + workflows=[TwoNodesWorkflow], + plugins=[plugin], + ): + handle = await client.start_workflow( + TwoNodesWorkflow.run, + "", + id=f"test-workflow-{uuid4()}", + task_queue=task_queue, + ) + await handle.result() + + await Replayer( + workflows=[TwoNodesWorkflow], + plugins=[plugin], + ).replay_workflow(await handle.fetch_history()) + + +@pytest.mark.skipif( + sys.version_info < (3, 11), + reason="langgraph.types.interrupt() requires Python >= 3.11 for async context propagation", +) +async def test_replay_interrupt(client: Client): + g = StateGraph(InterruptState) + g.add_node("node", interrupt_node, metadata={"execute_in": "activity"}) + g.add_edge(START, "node") + + task_queue = f"interrupt-replay-{uuid4()}" + plugin = LangGraphPlugin( + graphs={"my-graph": g}, + default_activity_options={"start_to_close_timeout": timedelta(seconds=10)}, + ) + + async with Worker( + client, + task_queue=task_queue, + workflows=[InterruptWorkflow], + plugins=[plugin], + ): + handle = await client.start_workflow( + InterruptWorkflow.run, + "", + id=f"test-interrupt-replay-{uuid4()}", + task_queue=task_queue, + ) + await handle.result() + + await Replayer( + workflows=[InterruptWorkflow], + plugins=[plugin], + ).replay_workflow(await handle.fetch_history()) diff --git a/tests/contrib/langgraph/test_send.py b/tests/contrib/langgraph/test_send.py new file mode 100644 index 000000000..6576c65b3 --- /dev/null +++ b/tests/contrib/langgraph/test_send.py @@ -0,0 +1,76 @@ +import operator +from datetime import timedelta +from typing import Annotated, Any +from uuid import uuid4 + +from langgraph.graph import ( # pyright: ignore[reportMissingTypeStubs] + END, + START, + StateGraph, +) +from langgraph.types import Send +from typing_extensions import TypedDict + +from temporalio import workflow +from temporalio.client import Client +from temporalio.contrib.langgraph import LangGraphPlugin, graph +from temporalio.worker import Worker + + +class State(TypedDict): + items: list[str] + results: Annotated[list[str], operator.add] + + +class WorkerState(TypedDict): + item: str + + +def worker(state: WorkerState) -> dict[str, list[str]]: + return {"results": [state["item"].upper()]} + + +async def fan_out(state: State) -> list[Send]: + return [Send("worker", {"item": item}) for item in state["items"]] + + +@workflow.defn +class SendWorkflow: + def __init__(self) -> None: + self.app = graph("my-graph").compile() + + @workflow.run + async def run(self, items: list[str]) -> Any: + return await self.app.ainvoke({"items": items, "results": []}) + + +async def test_send(client: Client): + g = StateGraph(State) + g.add_node("worker", worker, metadata={"execute_in": "activity"}) + g.add_conditional_edges(START, fan_out, ["worker"]) + g.add_edge("worker", END) + + task_queue = f"send-{uuid4()}" + + async with Worker( + client, + task_queue=task_queue, + workflows=[SendWorkflow], + plugins=[ + LangGraphPlugin( + graphs={"my-graph": g}, + default_activity_options={ + "start_to_close_timeout": timedelta(seconds=10) + }, + ) + ], + ): + result = await client.execute_workflow( + SendWorkflow.run, + ["a", "b", "c"], + id=f"test-send-{uuid4()}", + task_queue=task_queue, + ) + + assert result["items"] == ["a", "b", "c"] + assert sorted(result["results"]) == ["A", "B", "C"] diff --git a/tests/contrib/langgraph/test_streaming.py b/tests/contrib/langgraph/test_streaming.py new file mode 100644 index 000000000..5d1e1950a --- /dev/null +++ b/tests/contrib/langgraph/test_streaming.py @@ -0,0 +1,214 @@ +import sys +from datetime import timedelta +from typing import Any +from uuid import uuid4 + +import pytest +from langgraph.config import ( + get_stream_writer, # pyright: ignore[reportMissingTypeStubs] +) +from langgraph.graph import START, StateGraph # pyright: ignore[reportMissingTypeStubs] +from typing_extensions import TypedDict + +from temporalio import workflow +from temporalio.client import Client +from temporalio.contrib.langgraph import LangGraphPlugin, graph +from temporalio.contrib.workflow_streams import WorkflowStream, WorkflowStreamClient +from temporalio.worker import Worker + + +class State(TypedDict): + value: str + + +async def async_token_node(state: State) -> dict[str, str]: + tokens = ["a", "b", "c"] + writer = get_stream_writer() + for token in tokens: + writer({"token": token}) + writer({"done": True}) + return {"value": state["value"] + "".join(tokens)} + + +def sync_token_node(state: State) -> dict[str, str]: + tokens = ["a", "b", "c"] + writer = get_stream_writer() + for token in tokens: + writer({"token": token}) + writer({"done": True}) + return {"value": state["value"] + "".join(tokens)} + + +@workflow.defn +class StreamingWorkflowStreamsWorkflow: + def __init__(self) -> None: + _ = WorkflowStream() + self.app = graph("streaming-ws").compile() + self._done_acked = False + + @workflow.signal + def ack_done(self) -> None: + self._done_acked = True + + @workflow.run + async def run(self, input: str) -> str: + result = await self.app.ainvoke({"value": input}) + await workflow.wait_condition(lambda: self._done_acked) + return result["value"] + + +@pytest.mark.parametrize( + "execute_in", + [ + "activity", + pytest.param( + "workflow", + marks=pytest.mark.skipif( + sys.version_info < (3, 11), + reason=( + "execute_in='workflow' streaming relies on contextvar " + "propagation through asyncio.create_task, which only " + "works on Python >= 3.11" + ), + ), + ), + ], +) +@pytest.mark.parametrize( + "node", [async_token_node, sync_token_node], ids=["async", "sync"] +) +async def test_streaming_via_workflow_streams( + client: Client, execute_in: str, node: Any +): + g = StateGraph(State) + g.add_node("token_node", node, metadata={"execute_in": execute_in}) + g.add_edge(START, "token_node") + + task_queue = f"streaming-ws-{uuid4()}" + + async with Worker( + client, + task_queue=task_queue, + workflows=[StreamingWorkflowStreamsWorkflow], + plugins=[ + LangGraphPlugin( + graphs={"streaming-ws": g}, + default_activity_options={ + "start_to_close_timeout": timedelta(seconds=10) + }, + streaming_topic="tokens", + ) + ], + ): + handle = await client.start_workflow( + StreamingWorkflowStreamsWorkflow.run, + "", + id=f"test-streaming-ws-{uuid4()}", + task_queue=task_queue, + ) + + ws_client = WorkflowStreamClient.create(client, handle.id) + chunks: list[dict[str, Any]] = [] + async for item in ws_client.topic("tokens", type=dict).subscribe( + from_offset=0, + poll_cooldown=timedelta(milliseconds=10), + ): + chunks.append(item.data) + if chunks[-1].get("done"): + await handle.signal(StreamingWorkflowStreamsWorkflow.ack_done) + break + + result = await handle.result() + + assert result == "abc" + assert chunks == [ + {"token": "a"}, + {"token": "b"}, + {"token": "c"}, + {"done": True}, + ] + + +# --------------------------------------------------------------------------- +# Workflow-side publish: iterate astream() in the workflow and forward each +# chunk via self.stream.topic("astream").publish(...) so external subscribers +# see node-level progress alongside any activity-emitted tokens. +# --------------------------------------------------------------------------- + + +@workflow.defn +class AstreamPublishWorkflow: + def __init__(self) -> None: + self.stream = WorkflowStream() + self.app = graph("astream-publish").compile() + self._done_acked = False + + @workflow.signal + def ack_done(self) -> None: + self._done_acked = True + + @workflow.run + async def run(self, input: str) -> str: + topic = self.stream.topic("astream") + async for chunk in self.app.astream({"value": input}): + topic.publish(chunk) + topic.publish({"done": True}) + await workflow.wait_condition(lambda: self._done_acked) + return "done" + + +async def node_a(state: State) -> dict[str, str]: + return {"value": state["value"] + "a"} + + +async def node_b(state: State) -> dict[str, str]: + return {"value": state["value"] + "b"} + + +async def test_workflow_publishes_astream_chunks(client: Client): + g = StateGraph(State) + g.add_node("node_a", node_a, metadata={"execute_in": "activity"}) + g.add_node("node_b", node_b, metadata={"execute_in": "activity"}) + g.add_edge(START, "node_a") + g.add_edge("node_a", "node_b") + + task_queue = f"astream-publish-{uuid4()}" + + async with Worker( + client, + task_queue=task_queue, + workflows=[AstreamPublishWorkflow], + plugins=[ + LangGraphPlugin( + graphs={"astream-publish": g}, + default_activity_options={ + "start_to_close_timeout": timedelta(seconds=10) + }, + ) + ], + ): + handle = await client.start_workflow( + AstreamPublishWorkflow.run, + "", + id=f"test-astream-publish-{uuid4()}", + task_queue=task_queue, + ) + + ws_client = WorkflowStreamClient.create(client, handle.id) + chunks: list[dict[str, Any]] = [] + async for item in ws_client.topic("astream", type=dict).subscribe( + from_offset=0, + poll_cooldown=timedelta(milliseconds=10), + ): + chunks.append(item.data) + if chunks[-1].get("done"): + await handle.signal(AstreamPublishWorkflow.ack_done) + break + + await handle.result() + + assert chunks == [ + {"node_a": {"value": "a"}}, + {"node_b": {"value": "ab"}}, + {"done": True}, + ] diff --git a/tests/contrib/langgraph/test_subgraph_activity.py b/tests/contrib/langgraph/test_subgraph_activity.py new file mode 100644 index 000000000..76600fa57 --- /dev/null +++ b/tests/contrib/langgraph/test_subgraph_activity.py @@ -0,0 +1,67 @@ +from datetime import timedelta +from typing import Any +from uuid import uuid4 + +from langgraph.graph import START, StateGraph # pyright: ignore[reportMissingTypeStubs] +from typing_extensions import TypedDict + +from temporalio import workflow +from temporalio.client import Client +from temporalio.contrib.langgraph import LangGraphPlugin, graph +from temporalio.worker import Worker + + +class State(TypedDict): + value: str + + +async def child_node(state: State) -> dict[str, str]: # pyright: ignore[reportUnusedParameter] + return {"value": "child"} + + +async def parent_node(state: State) -> dict[str, str]: + child: StateGraph[State, None, State, State] = StateGraph(State) + child.add_node("child_node", child_node) + child.add_edge(START, "child_node") + + return await child.compile().ainvoke(state) + + +@workflow.defn +class ActivitySubgraphWorkflow: + def __init__(self) -> None: + self.app = graph("parent").compile() + + @workflow.run + async def run(self, input: str) -> Any: + return await self.app.ainvoke({"value": input}) + + +async def test_activity_subgraph(client: Client): + parent = StateGraph(State) + parent.add_node("parent_node", parent_node, metadata={"execute_in": "activity"}) + parent.add_edge(START, "parent_node") + + task_queue = f"subgraph-{uuid4()}" + + async with Worker( + client, + task_queue=task_queue, + workflows=[ActivitySubgraphWorkflow], + plugins=[ + LangGraphPlugin( + graphs={"parent": parent}, + default_activity_options={ + "start_to_close_timeout": timedelta(seconds=10) + }, + ) + ], + ): + result = await client.execute_workflow( + ActivitySubgraphWorkflow.run, + "", + id=f"test-workflow-{uuid4()}", + task_queue=task_queue, + ) + + assert result == {"value": "child"} diff --git a/tests/contrib/langgraph/test_subgraph_workflow.py b/tests/contrib/langgraph/test_subgraph_workflow.py new file mode 100644 index 000000000..a3b3741b5 --- /dev/null +++ b/tests/contrib/langgraph/test_subgraph_workflow.py @@ -0,0 +1,67 @@ +from datetime import timedelta +from typing import Any +from uuid import uuid4 + +from langgraph.graph import START, StateGraph # pyright: ignore[reportMissingTypeStubs] +from typing_extensions import TypedDict + +from temporalio import workflow +from temporalio.client import Client +from temporalio.contrib.langgraph import LangGraphPlugin, graph +from temporalio.worker import Worker + + +class State(TypedDict): + value: str + + +async def child_node(state: State) -> dict[str, str]: # pyright: ignore[reportUnusedParameter] + return {"value": "child"} + + +async def parent_node(state: State) -> dict[str, str]: + return await graph("child").compile().ainvoke(state) + + +@workflow.defn +class WorkflowSubgraphWorkflow: + def __init__(self) -> None: + self.app = graph("parent").compile() + + @workflow.run + async def run(self, input: str) -> Any: + return await self.app.ainvoke({"value": input}) + + +async def test_workflow_subgraph(client: Client): + child = StateGraph(State) + child.add_node( + "child_node", + child_node, + metadata={ + "execute_in": "activity", + "start_to_close_timeout": timedelta(seconds=10), + }, + ) + child.add_edge(START, "child_node") + + parent = StateGraph(State) + parent.add_node("parent_node", parent_node, metadata={"execute_in": "workflow"}) + parent.add_edge(START, "parent_node") + + task_queue = f"subgraph-{uuid4()}" + + async with Worker( + client, + task_queue=task_queue, + workflows=[WorkflowSubgraphWorkflow], + plugins=[LangGraphPlugin(graphs={"parent": parent, "child": child})], + ): + result = await client.execute_workflow( + WorkflowSubgraphWorkflow.run, + "", + id=f"test-workflow-{uuid4()}", + task_queue=task_queue, + ) + + assert result == {"value": "child"} diff --git a/tests/contrib/langgraph/test_summary_fn.py b/tests/contrib/langgraph/test_summary_fn.py new file mode 100644 index 000000000..687a68edb --- /dev/null +++ b/tests/contrib/langgraph/test_summary_fn.py @@ -0,0 +1,362 @@ +"""Tests for node/task summaries (static summary and summary_fn).""" + +from __future__ import annotations + +import uuid +from datetime import timedelta +from typing import Any, Callable + +import pytest +from langchain_core.runnables import ( + RunnableConfig, # pyright: ignore[reportMissingTypeStubs] +) +from langgraph.graph import START, StateGraph # pyright: ignore[reportMissingTypeStubs] +from typing_extensions import TypedDict + +import temporalio.api.sdk.v1 +from temporalio import workflow +from temporalio.client import Client +from temporalio.contrib.langgraph import LangGraphPlugin, graph +from temporalio.testing import WorkflowEnvironment +from temporalio.worker import Replayer, Worker +from tests.helpers import assert_eq_eventually + +SummaryFn = Callable[[tuple[Any, ...], dict[str, Any]], "str | None"] + + +class State(TypedDict): + value: str + + +async def passthrough(state: State) -> dict[str, str]: + return {"value": state["value"]} + + +def summarize( + args: tuple[Any, ...], + kwargs: dict[str, Any], # pyright: ignore[reportUnusedParameter] +) -> str | None: + return f"value={args[0]['value']}" + + +@workflow.defn +class SummaryWorkflow: + def __init__(self) -> None: + self.app = graph("summary-graph").compile() + + @workflow.run + async def run(self, input: str) -> Any: + return await self.app.ainvoke({"value": input}) + + +def _activity_graph( + summary_fn: SummaryFn | None, +) -> StateGraph[State, None, State, State]: + metadata: dict[str, Any] = {"execute_in": "activity"} + if summary_fn is not None: + metadata["summary_fn"] = summary_fn + g: StateGraph[State, None, State, State] = StateGraph(State) + g.add_node("node", passthrough, metadata=metadata) + g.add_edge(START, "node") + return g + + +async def _run_and_collect_summaries( + client: Client, plugin: LangGraphPlugin, input: str +) -> list[bytes]: + task_queue = f"summary-{uuid.uuid4()}" + async with Worker( + client, + task_queue=task_queue, + workflows=[SummaryWorkflow], + plugins=[plugin], + max_cached_workflows=0, + ): + handle = await client.start_workflow( + SummaryWorkflow.run, + input, + id=f"summary-{uuid.uuid4()}", + task_queue=task_queue, + ) + await handle.result() + return [ + e.user_metadata.summary.data + async for e in handle.fetch_history_events() + if e.HasField("activity_task_scheduled_event_attributes") + ] + + +def _plugin(g: StateGraph[Any, Any, Any, Any], **kwargs: Any) -> LangGraphPlugin: + return LangGraphPlugin( + graphs={"summary-graph": g}, + default_activity_options={"start_to_close_timeout": timedelta(seconds=10)}, + **kwargs, + ) + + +async def test_activity_summary_fn_in_history(client: Client) -> None: + plugin = _plugin(_activity_graph(summarize)) + summaries = await _run_and_collect_summaries(client, plugin, "hello") + assert summaries == [b'"value=hello"'] + + +@pytest.mark.parametrize( + "summary_fn,expected", + [ + (lambda args, kwargs: f"value={args[0]['value']}", b'"value=x"'), + (lambda args, kwargs: None, b""), + (lambda args, kwargs: "", b""), + ], +) +async def test_summary_fn_variants( + client: Client, summary_fn: SummaryFn, expected: bytes +) -> None: + plugin = _plugin(_activity_graph(summary_fn)) + summaries = await _run_and_collect_summaries(client, plugin, "x") + assert summaries == [expected] + + +async def test_static_summary(client: Client) -> None: + g: StateGraph[State, None, State, State] = StateGraph(State) + g.add_node( + "node", passthrough, metadata={"execute_in": "activity", "summary": "static"} + ) + g.add_edge(START, "node") + summaries = await _run_and_collect_summaries(client, _plugin(g), "x") + assert summaries == [b'"static"'] + + +async def test_node_static_summary_overrides_default_summary_fn( + client: Client, +) -> None: + g: StateGraph[State, None, State, State] = StateGraph(State) + g.add_node( + "node", + passthrough, + metadata={"execute_in": "activity", "summary": "node-static"}, + ) + g.add_edge(START, "node") + plugin = LangGraphPlugin( + graphs={"summary-graph": g}, + default_activity_options={ + "start_to_close_timeout": timedelta(seconds=10), + "summary_fn": lambda args, kwargs: "global", + }, + ) + summaries = await _run_and_collect_summaries(client, plugin, "x") + assert summaries == [b'"node-static"'] + + +async def test_node_summary_fn_overrides_default_summary(client: Client) -> None: + plugin = LangGraphPlugin( + graphs={"summary-graph": _activity_graph(lambda args, kwargs: "node-fn")}, + default_activity_options={ + "start_to_close_timeout": timedelta(seconds=10), + "summary": "global-static", + }, + ) + summaries = await _run_and_collect_summaries(client, plugin, "x") + assert summaries == [b'"node-fn"'] + + +async def test_default_summary_fn_applies_without_override(client: Client) -> None: + plugin = LangGraphPlugin( + graphs={"summary-graph": _activity_graph(None)}, + default_activity_options={ + "start_to_close_timeout": timedelta(seconds=10), + "summary_fn": lambda args, kwargs: "global", + }, + ) + summaries = await _run_and_collect_summaries(client, plugin, "x") + assert summaries == [b'"global"'] + + +def test_both_in_default_activity_options_raises() -> None: + with pytest.raises(ValueError, match="default_activity_options"): + LangGraphPlugin( + default_activity_options={ + "summary": "s", + "summary_fn": lambda args, kwargs: "f", + } + ) + + +def test_summary_and_summary_fn_raises() -> None: + g: StateGraph[State, None, State, State] = StateGraph(State) + g.add_node( + "node", + passthrough, + metadata={ + "execute_in": "activity", + "summary": "static", + "summary_fn": lambda args, kwargs: "dynamic", + }, + ) + g.add_edge(START, "node") + with pytest.raises(ValueError, match="not both"): + LangGraphPlugin(graphs={f"summary-graph-{uuid.uuid4()}": g}) + + +async def node_reads_meta(state: State, config: RunnableConfig) -> dict[str, str]: + metadata = config.get("metadata") or {} + return {"value": f"{state['value']}-has_fn={'summary_fn' in metadata}"} + + +async def test_summary_fn_not_in_node_metadata(client: Client) -> None: + g: StateGraph[State, None, State, State] = StateGraph(State) + g.add_node( + "node", + node_reads_meta, + metadata={ + "execute_in": "activity", + "summary_fn": lambda args, kwargs: "dynamic", + "my_key": "my_value", + }, + ) + g.add_edge(START, "node") + task_queue = f"summary-{uuid.uuid4()}" + async with Worker( + client, + task_queue=task_queue, + workflows=[SummaryWorkflow], + plugins=[_plugin(g)], + ): + result = await client.execute_workflow( + SummaryWorkflow.run, + "in", + id=f"summary-{uuid.uuid4()}", + task_queue=task_queue, + ) + assert result == {"value": "in-has_fn=False"} + + +@workflow.defn +class WorkflowNodeSummaryWorkflow: + def __init__(self) -> None: + self.app = graph("wf-node-graph").compile() + self._done = False + self._invoked = False + + @workflow.run + async def run(self, input: str) -> Any: + result = await self.app.ainvoke({"value": input}) + self._invoked = True + await workflow.wait_condition(lambda: self._done) + return result + + @workflow.signal + def finish(self) -> None: + self._done = True + + @workflow.query + def ran(self) -> bool: + return workflow.get_current_details() != "" + + @workflow.query + def invoked(self) -> bool: + return self._invoked + + +async def test_workflow_node_sets_current_details( + client: Client, env: WorkflowEnvironment +) -> None: + if env.supports_time_skipping: + pytest.skip("metadata query unreliable on the time-skipping test server") + g: StateGraph[State, None, State, State] = StateGraph(State) + g.add_node( + "node", + passthrough, + metadata={ + "execute_in": "workflow", + "summary_fn": lambda args, kwargs: f"wf:{args[0]['value']}", + }, + ) + g.add_edge(START, "node") + task_queue = f"wf-node-{uuid.uuid4()}" + async with Worker( + client, + task_queue=task_queue, + workflows=[WorkflowNodeSummaryWorkflow], + plugins=[LangGraphPlugin(graphs={"wf-node-graph": g})], + ): + handle = await client.start_workflow( + WorkflowNodeSummaryWorkflow.run, + "ready", + id=f"wf-node-{uuid.uuid4()}", + task_queue=task_queue, + ) + await assert_eq_eventually( + True, lambda: handle.query(WorkflowNodeSummaryWorkflow.ran) + ) + md: temporalio.api.sdk.v1.WorkflowMetadata = await handle.query( + "__temporal_workflow_metadata", + result_type=temporalio.api.sdk.v1.WorkflowMetadata, + ) + assert md.current_details == "wf:ready" + await handle.signal(WorkflowNodeSummaryWorkflow.finish) + assert await handle.result() == {"value": "ready"} + + +async def test_workflow_node_clears_current_details_on_empty( + client: Client, env: WorkflowEnvironment +) -> None: + if env.supports_time_skipping: + pytest.skip("metadata query unreliable on the time-skipping test server") + g: StateGraph[State, None, State, State] = StateGraph(State) + g.add_node( + "a", + passthrough, + metadata={"execute_in": "workflow", "summary_fn": lambda args, kwargs: "first"}, + ) + g.add_node( + "b", + passthrough, + metadata={"execute_in": "workflow", "summary_fn": lambda args, kwargs: None}, + ) + g.add_edge(START, "a") + g.add_edge("a", "b") + task_queue = f"wf-node-clear-{uuid.uuid4()}" + async with Worker( + client, + task_queue=task_queue, + workflows=[WorkflowNodeSummaryWorkflow], + plugins=[LangGraphPlugin(graphs={"wf-node-graph": g})], + ): + handle = await client.start_workflow( + WorkflowNodeSummaryWorkflow.run, + "ready", + id=f"wf-node-clear-{uuid.uuid4()}", + task_queue=task_queue, + ) + await assert_eq_eventually( + True, lambda: handle.query(WorkflowNodeSummaryWorkflow.invoked) + ) + md: temporalio.api.sdk.v1.WorkflowMetadata = await handle.query( + "__temporal_workflow_metadata", + result_type=temporalio.api.sdk.v1.WorkflowMetadata, + ) + assert md.current_details == "" + await handle.signal(WorkflowNodeSummaryWorkflow.finish) + await handle.result() + + +async def test_replay_with_summary_fn(client: Client) -> None: + plugin = _plugin(_activity_graph(summarize)) + task_queue = f"summary-replay-{uuid.uuid4()}" + async with Worker( + client, + task_queue=task_queue, + workflows=[SummaryWorkflow], + plugins=[plugin], + ): + handle = await client.start_workflow( + SummaryWorkflow.run, + "hello", + id=f"summary-replay-{uuid.uuid4()}", + task_queue=task_queue, + ) + await handle.result() + + await Replayer(workflows=[SummaryWorkflow], plugins=[plugin]).replay_workflow( + await handle.fetch_history() + ) diff --git a/tests/contrib/langgraph/test_sync_node.py b/tests/contrib/langgraph/test_sync_node.py new file mode 100644 index 000000000..92ec1beed --- /dev/null +++ b/tests/contrib/langgraph/test_sync_node.py @@ -0,0 +1,59 @@ +from datetime import timedelta +from typing import Any +from uuid import uuid4 + +from langgraph.graph import START, StateGraph # pyright: ignore[reportMissingTypeStubs] +from typing_extensions import TypedDict + +from temporalio import workflow +from temporalio.client import Client +from temporalio.contrib.langgraph import LangGraphPlugin, graph +from temporalio.worker import Worker + + +class State(TypedDict): + value: str + + +def sync_node(state: State) -> dict[str, str]: + return {"value": state["value"] + "!"} + + +@workflow.defn +class SyncNodeWorkflow: + def __init__(self) -> None: + self.app = graph("my-graph").compile() + + @workflow.run + async def run(self, input: str) -> Any: + return await self.app.ainvoke({"value": input}) + + +async def test_sync_node(client: Client): + g = StateGraph(State) + g.add_node("sync_node", sync_node, metadata={"execute_in": "activity"}) + g.add_edge(START, "sync_node") + + task_queue = f"sync-node-{uuid4()}" + + async with Worker( + client, + task_queue=task_queue, + workflows=[SyncNodeWorkflow], + plugins=[ + LangGraphPlugin( + graphs={"my-graph": g}, + default_activity_options={ + "start_to_close_timeout": timedelta(seconds=10) + }, + ) + ], + ): + result = await client.execute_workflow( + SyncNodeWorkflow.run, + "hello", + id=f"test-sync-node-{uuid4()}", + task_queue=task_queue, + ) + + assert result == {"value": "hello!"} diff --git a/tests/contrib/langgraph/test_sync_task.py b/tests/contrib/langgraph/test_sync_task.py new file mode 100644 index 000000000..fa820e522 --- /dev/null +++ b/tests/contrib/langgraph/test_sync_task.py @@ -0,0 +1,69 @@ +import sys +from datetime import timedelta +from typing import Any +from uuid import uuid4 + +import pytest + +pytestmark = pytest.mark.skipif( + sys.version_info < (3, 11), + reason="LangGraph Functional API requires Python >= 3.11 for async context propagation", +) +from langgraph.func import ( # pyright: ignore[reportMissingTypeStubs] + entrypoint as lg_entrypoint, +) +from langgraph.func import task # pyright: ignore[reportMissingTypeStubs] + +from temporalio import workflow +from temporalio.client import Client +from temporalio.contrib.langgraph import LangGraphPlugin, entrypoint +from temporalio.worker import Worker + + +@task +def sync_task(x: int) -> int: + return x + 1 + + +@lg_entrypoint() +async def sync_task_entrypoint(value: int) -> dict[str, int]: + result = await sync_task(value) + return {"result": result} + + +@workflow.defn +class SyncTaskWorkflow: + def __init__(self) -> None: + self.app = entrypoint("sync-task") + + @workflow.run + async def run(self, input: int) -> Any: + return await self.app.ainvoke(input) + + +async def test_sync_task(client: Client): + task_queue = f"sync-task-{uuid4()}" + + async with Worker( + client, + task_queue=task_queue, + workflows=[SyncTaskWorkflow], + plugins=[ + LangGraphPlugin( + entrypoints={"sync-task": sync_task_entrypoint}, + tasks=[sync_task], + activity_options={"sync_task": {"execute_in": "activity"}}, + default_activity_options={ + "start_to_close_timeout": timedelta(seconds=10) + }, + ) + ], + ): + result = await client.execute_workflow( + SyncTaskWorkflow.run, + 41, + id=f"test-sync-task-{uuid4()}", + task_queue=task_queue, + ) + + assert result == {"result": 42} diff --git a/tests/contrib/langgraph/test_timeout.py b/tests/contrib/langgraph/test_timeout.py new file mode 100644 index 000000000..7138e3a57 --- /dev/null +++ b/tests/contrib/langgraph/test_timeout.py @@ -0,0 +1,64 @@ +import asyncio +from datetime import timedelta +from typing import Any +from uuid import uuid4 + +from langgraph.graph import START, StateGraph # pyright: ignore[reportMissingTypeStubs] +from pytest import raises +from typing_extensions import TypedDict + +from temporalio import workflow +from temporalio.client import Client, WorkflowFailureError +from temporalio.common import RetryPolicy +from temporalio.contrib.langgraph import LangGraphPlugin, graph +from temporalio.worker import Worker + + +class State(TypedDict): + value: str + + +async def node(state: State) -> dict[str, str]: # pyright: ignore[reportUnusedParameter] + # Wait (forever) until start_to_close_timeout or worker shutdown cancellation + await asyncio.Event().wait() + return {"value": "done"} + + +@workflow.defn +class TimeoutWorkflow: + def __init__(self) -> None: + self.app = graph("my-graph").compile() + + @workflow.run + async def run(self, input: str) -> Any: + return await self.app.ainvoke({"value": input}) + + +async def test_timeout(client: Client): + g = StateGraph(State) + g.add_node("node", node, metadata={"execute_in": "activity"}) + g.add_edge(START, "node") + + task_queue = f"my-graph-{uuid4()}" + + async with Worker( + client, + task_queue=task_queue, + workflows=[TimeoutWorkflow], + plugins=[ + LangGraphPlugin( + graphs={"my-graph": g}, + default_activity_options={ + "start_to_close_timeout": timedelta(milliseconds=100), + "retry_policy": RetryPolicy(maximum_attempts=1), + }, + ) + ], + ): + with raises(WorkflowFailureError): + await client.execute_workflow( + TimeoutWorkflow.run, + "", + id=f"test-workflow-{uuid4()}", + task_queue=task_queue, + ) diff --git a/tests/contrib/langgraph/test_two_nodes.py b/tests/contrib/langgraph/test_two_nodes.py new file mode 100644 index 000000000..6b974d90f --- /dev/null +++ b/tests/contrib/langgraph/test_two_nodes.py @@ -0,0 +1,65 @@ +from datetime import timedelta +from typing import Any +from uuid import uuid4 + +from langgraph.graph import START, StateGraph # pyright: ignore[reportMissingTypeStubs] +from typing_extensions import TypedDict + +from temporalio import workflow +from temporalio.client import Client +from temporalio.contrib.langgraph import LangGraphPlugin, graph +from temporalio.worker import Worker + + +class State(TypedDict): + value: str + + +async def node_a(state: State) -> dict[str, str]: + return {"value": state["value"] + "a"} + + +async def node_b(state: State) -> dict[str, str]: + return {"value": state["value"] + "b"} + + +@workflow.defn +class TwoNodesWorkflow: + def __init__(self) -> None: + self.app = graph("my-graph").compile() + + @workflow.run + async def run(self, input: str) -> Any: + return await self.app.ainvoke({"value": input}) + + +async def test_two_nodes(client: Client): + g = StateGraph(State) + g.add_node("node_a", node_a, metadata={"execute_in": "activity"}) + g.add_node("node_b", node_b, metadata={"execute_in": "activity"}) + g.add_edge(START, "node_a") + g.add_edge("node_a", "node_b") + + task_queue = f"my-graph-{uuid4()}" + + async with Worker( + client, + task_queue=task_queue, + workflows=[TwoNodesWorkflow], + plugins=[ + LangGraphPlugin( + graphs={"my-graph": g}, + default_activity_options={ + "start_to_close_timeout": timedelta(seconds=10) + }, + ) + ], + ): + result = await client.execute_workflow( + TwoNodesWorkflow.run, + "", + id=f"test-workflow-{uuid4()}", + task_queue=task_queue, + ) + + assert result == {"value": "ab"} diff --git a/tests/contrib/langsmith/__init__.py b/tests/contrib/langsmith/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/contrib/langsmith/conftest.py b/tests/contrib/langsmith/conftest.py new file mode 100644 index 000000000..1711c0446 --- /dev/null +++ b/tests/contrib/langsmith/conftest.py @@ -0,0 +1,134 @@ +"""Shared test helpers for LangSmith plugin tests.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any +from unittest.mock import MagicMock + +import pytest + +from tests.helpers.trace import TraceNode + + +@pytest.fixture(autouse=True) +def _clear_langsmith_env_cache() -> Any: # pyright: ignore[reportUnusedFunction] + """Clear langsmith's lru_cache before and after each test. + + Tests manipulate LANGSMITH_TRACING / LANGCHAIN_TRACING_V2 env vars. + langsmith.utils.get_env_var caches results, so stale values would + leak across tests (or into other test modules in the same session). + """ + import langsmith.utils + + langsmith.utils.get_env_var.cache_clear() # type: ignore[attr-defined] + yield + langsmith.utils.get_env_var.cache_clear() # type: ignore[attr-defined] + + +@pytest.fixture(autouse=True) +def _enable_langsmith_tracing(monkeypatch: pytest.MonkeyPatch) -> None: # pyright: ignore[reportUnusedFunction] + """Enable LangSmith tracing by default for all tests in this directory. + + The plugin defers to ``langsmith.utils.tracing_is_enabled()``, which + requires ``LANGSMITH_TRACING=true`` (or equivalent). Without this + fixture, tests that expect runs would see zero. + + Individual tests can override with ``monkeypatch.setenv("LANGSMITH_TRACING", "false")`` + to verify disabled behavior. + """ + monkeypatch.setenv("LANGSMITH_TRACING", "true") + + +@dataclass +class _RunRecord: + """A single recorded run.""" + + id: str + parent_run_id: str | None + name: str + run_type: str + inputs: dict[str, Any] + outputs: dict[str, Any] | None = None + error: str | None = None + + +class InMemoryRunCollector: + """Collects runs from a mock LangSmith client. + + Each call to create_run / update_run appends or updates an entry. + """ + + def __init__(self) -> None: + self.runs: list[_RunRecord] = [] + self._by_id: dict[str, _RunRecord] = {} + + def record_create(self, **kwargs: Any) -> None: + run_id = str(kwargs.get("id", kwargs.get("run_id", ""))) + if run_id in self._by_id: + return + rec = _RunRecord( + id=run_id, + parent_run_id=( + str(kwargs["parent_run_id"]) if kwargs.get("parent_run_id") else None + ), + name=kwargs.get("name", ""), + run_type=kwargs.get("run_type", "chain"), + inputs=kwargs.get("inputs", {}), + ) + self.runs.append(rec) + self._by_id[rec.id] = rec + + def record_update(self, run_id: str, **kwargs: Any) -> None: + run_id_str = str(run_id) + rec = self._by_id.get(run_id_str) + if rec is None: + return + if "outputs" in kwargs: + rec.outputs = kwargs["outputs"] + if "error" in kwargs: + rec.error = kwargs["error"] + + def clear(self) -> None: + self.runs.clear() + self._by_id.clear() + + +def build_trace_trees(collector: InMemoryRunCollector) -> list[TraceNode]: + """Build trace trees from the collector's run parent relationships.""" + runs = collector.runs + children: dict[str | None, list[_RunRecord]] = {} + for r in runs: + children.setdefault(r.parent_run_id, []).append(r) + + # Strict: reject dangling parent references + known_ids = {r.id for r in runs} + for r in runs: + if r.parent_run_id is not None and r.parent_run_id not in known_ids: + raise AssertionError( + f"Run {r.name!r} (id={r.id}) has parent_run_id={r.parent_run_id} " + f"which is not in the collected runs — dangling parent reference" + ) + + def build_tree(run: _RunRecord) -> TraceNode: + return TraceNode( + run.name, + [build_tree(child) for child in children.get(run.id, [])], + ) + + return [build_tree(root) for root in children.get(None, [])] + + +def find_trace_trees(traces: list[TraceNode], root_name: str) -> list[TraceNode]: + """Filter trace trees by exact root run name.""" + return [trace for trace in traces if trace.name == root_name] + + +def make_mock_ls_client(collector: InMemoryRunCollector) -> MagicMock: + """Create a mock langsmith.Client wired to a collector.""" + client = MagicMock() + client.create_run.side_effect = collector.record_create + client.update_run.side_effect = collector.record_update + client.session = MagicMock() + client.tracing_queue = MagicMock() + return client diff --git a/tests/contrib/langsmith/test_background_io.py b/tests/contrib/langsmith/test_background_io.py new file mode 100644 index 000000000..79c48eeef --- /dev/null +++ b/tests/contrib/langsmith/test_background_io.py @@ -0,0 +1,651 @@ +"""Unit tests for _ReplaySafeRunTree and _RootReplaySafeRunTreeFactory. + +Covers create_child propagation, executor-backed post/patch, +replay suppression, and post-shutdown fallback. +""" + +from __future__ import annotations + +import logging +import threading +import uuid +from concurrent.futures import ThreadPoolExecutor +from datetime import datetime, timezone +from typing import Any +from unittest.mock import MagicMock, patch + +import pytest +from langsmith.run_trees import RunTree + +from temporalio.contrib.langsmith._interceptor import ( + _ReplaySafeRunTree, + _RootReplaySafeRunTreeFactory, + _uuid_from_random, +) + +# Common patch targets +_MOD = "temporalio.contrib.langsmith._interceptor" +_PATCH_IN_WORKFLOW = f"{_MOD}.temporalio.workflow.in_workflow" +_PATCH_IS_REPLAYING = f"{_MOD}.temporalio.workflow.unsafe.is_replaying_history_events" +_PATCH_WF_NOW = f"{_MOD}.temporalio.workflow.now" +_PATCH_GET_WF_RANDOM = f"{_MOD}._get_workflow_random" + + +def _make_executor() -> ThreadPoolExecutor: + """Create a single-worker executor for tests.""" + return ThreadPoolExecutor(max_workers=1) + + +def _make_mock_run(**kwargs: Any) -> MagicMock: + """Create a mock RunTree.""" + mock = MagicMock(spec=RunTree) + mock.to_headers.return_value = {"langsmith-trace": "test"} + mock.ls_client = kwargs.get("ls_client", MagicMock()) + mock.session_name = kwargs.get("session_name", "test-session") + mock.replicas = kwargs.get("replicas", []) + mock.id = kwargs.get("id", uuid.uuid4()) + mock.name = kwargs.get("name", "test-run") + # create_child returns another mock RunTree by default + child_mock = MagicMock(spec=RunTree) + child_mock.id = uuid.uuid4() + child_mock.ls_client = mock.ls_client + child_mock.session_name = mock.session_name + child_mock.replicas = mock.replicas + mock.create_child.return_value = child_mock + return mock + + +# =================================================================== +# TestCreateChildPropagation +# =================================================================== + + +class TestCreateChildPropagation: + """Tests for _ReplaySafeRunTree.create_child() override.""" + + def test_create_child_returns_replay_safe_run_tree(self) -> None: + """create_child() must return a _ReplaySafeRunTree wrapping the child.""" + executor = _make_executor() + mock_run = _make_mock_run() + parent = _ReplaySafeRunTree(mock_run, executor=executor) + + child = parent.create_child(name="child-op", run_type="chain") + + assert isinstance(child, _ReplaySafeRunTree) + # The wrapped child should be the result of the inner run's create_child + mock_run.create_child.assert_called_once() + + @patch(_PATCH_GET_WF_RANDOM) + @patch(_PATCH_WF_NOW) + @patch(_PATCH_IN_WORKFLOW, return_value=True) + def test_create_child_injects_deterministic_ids_in_workflow( + self, + _mock_in_wf: Any, + mock_now: Any, + mock_get_random: Any, + ) -> None: + """In workflow context, create_child injects deterministic run_id and start_time.""" + import random as stdlib_random + + rng = stdlib_random.Random(42) + mock_get_random.return_value = rng + fake_now = datetime(2025, 1, 1, tzinfo=timezone.utc) + mock_now.return_value = fake_now + + expected_id = _uuid_from_random(stdlib_random.Random(42)) # same seed + + executor = _make_executor() + mock_run = _make_mock_run() + parent = _ReplaySafeRunTree(mock_run, executor=executor) + + # Simulate what _setup_run does: passes run_id=None explicitly + child = parent.create_child(name="child-op", run_type="chain", run_id=None) + + assert isinstance(child, _ReplaySafeRunTree) + # Verify the kwargs passed to inner create_child had deterministic values + call_kwargs = mock_run.create_child.call_args.kwargs + assert call_kwargs["run_id"] == expected_id + assert call_kwargs["start_time"] == fake_now + + def test_create_child_passes_through_kwargs(self) -> None: + """create_child passes through all kwargs to the inner run's create_child.""" + executor = _make_executor() + mock_run = _make_mock_run() + parent = _ReplaySafeRunTree(mock_run, executor=executor) + + child = parent.create_child( + name="child-op", + run_type="llm", + inputs={"prompt": "hello"}, + tags=["test"], + extra_kwarg="future-proof", + ) + + assert isinstance(child, _ReplaySafeRunTree) + call_kwargs = mock_run.create_child.call_args.kwargs + assert call_kwargs["name"] == "child-op" + assert call_kwargs["run_type"] == "llm" + assert call_kwargs["inputs"] == {"prompt": "hello"} + assert call_kwargs["tags"] == ["test"] + assert call_kwargs["extra_kwarg"] == "future-proof" + + def test_create_child_propagates_executor_to_child(self) -> None: + """The child _ReplaySafeRunTree must receive the same executor reference.""" + executor = _make_executor() + mock_run = _make_mock_run() + parent = _ReplaySafeRunTree(mock_run, executor=executor) + + child = parent.create_child(name="child-op", run_type="chain") + + assert isinstance(child, _ReplaySafeRunTree) + # Child should have the same executor + assert child._executor is executor + + @patch(_PATCH_IN_WORKFLOW, return_value=False) + def test_create_child_no_deterministic_ids_outside_workflow( + self, _mock_in_wf: Any + ) -> None: + """Outside workflow context, create_child does NOT inject deterministic IDs.""" + executor = _make_executor() + mock_run = _make_mock_run() + parent = _ReplaySafeRunTree(mock_run, executor=executor) + + child = parent.create_child(name="child-op", run_type="chain", run_id=None) + + assert isinstance(child, _ReplaySafeRunTree) + # run_id should remain None (not overridden) + call_kwargs = mock_run.create_child.call_args.kwargs + assert call_kwargs.get("run_id") is None + + @patch(_PATCH_GET_WF_RANDOM) + @patch(_PATCH_WF_NOW) + @patch(_PATCH_IN_WORKFLOW, return_value=True) + def test_create_child_respects_explicit_run_id( + self, + _mock_in_wf: Any, + mock_now: Any, + mock_get_random: Any, + ) -> None: + """If run_id is explicitly provided (not None), create_child preserves it.""" + import random as stdlib_random + + mock_get_random.return_value = stdlib_random.Random(42) + mock_now.return_value = datetime(2025, 1, 1, tzinfo=timezone.utc) + + executor = _make_executor() + mock_run = _make_mock_run() + parent = _ReplaySafeRunTree(mock_run, executor=executor) + + explicit_id = uuid.uuid4() + child = parent.create_child( + name="child-op", run_type="chain", run_id=explicit_id + ) + + assert isinstance(child, _ReplaySafeRunTree) + call_kwargs = mock_run.create_child.call_args.kwargs + assert call_kwargs["run_id"] == explicit_id + + +# =================================================================== +# TestExecutorBackedPostPatch +# =================================================================== + + +class TestExecutorBackedPostPatch: + """Tests for executor-backed post()/patch() in _ReplaySafeRunTree.""" + + @patch(_PATCH_IS_REPLAYING, return_value=False) + @patch(_PATCH_IN_WORKFLOW, return_value=True) + def test_post_submits_to_executor_in_workflow( + self, _mock_in_wf: Any, _mock_replaying: Any + ) -> None: + """In workflow context, post() submits to executor, not inline.""" + executor = _make_executor() + mock_run = _make_mock_run() + calling_thread = threading.current_thread() + post_thread: list[threading.Thread] = [] + + def record_thread(*_args: Any, **_kwargs: Any) -> None: + post_thread.append(threading.current_thread()) + + mock_run.post.side_effect = record_thread + tree = _ReplaySafeRunTree(mock_run, executor=executor) + + tree.post() + + # Wait for executor to finish + executor.shutdown(wait=True) + + # post should have been called on the inner run via executor + mock_run.post.assert_called_once() + # Verify it ran on the executor thread, not the calling thread + assert len(post_thread) == 1 + assert post_thread[0] is not calling_thread + + @patch(_PATCH_IS_REPLAYING, return_value=False) + @patch(_PATCH_IN_WORKFLOW, return_value=True) + def test_patch_submits_to_executor_in_workflow( + self, _mock_in_wf: Any, _mock_replaying: Any + ) -> None: + """In workflow context, patch() submits to executor, not inline.""" + executor = _make_executor() + mock_run = _make_mock_run() + tree = _ReplaySafeRunTree(mock_run, executor=executor) + + tree.patch() + + executor.shutdown(wait=True) + mock_run.patch.assert_called_once() + + @patch(_PATCH_IN_WORKFLOW, return_value=False) + def test_post_delegates_directly_outside_workflow(self, _mock_in_wf: Any) -> None: + """Outside workflow, post() delegates directly to the inner run.""" + executor = _make_executor() + mock_run = _make_mock_run() + tree = _ReplaySafeRunTree(mock_run, executor=executor) + + tree.post() + + mock_run.post.assert_called_once() + + @patch(_PATCH_IN_WORKFLOW, return_value=False) + def test_patch_delegates_directly_outside_workflow(self, _mock_in_wf: Any) -> None: + """Outside workflow, patch() delegates directly to the inner run.""" + executor = _make_executor() + mock_run = _make_mock_run() + tree = _ReplaySafeRunTree(mock_run, executor=executor) + + tree.patch(exclude_inputs=True) + + mock_run.patch.assert_called_once_with(exclude_inputs=True) + + @patch(_PATCH_IS_REPLAYING, return_value=False) + @patch(_PATCH_IN_WORKFLOW, return_value=True) + def test_post_error_logged_via_done_callback( + self, + _mock_in_wf: Any, + _mock_replaying: Any, + caplog: pytest.LogCaptureFixture, + ) -> None: + """Errors from fire-and-forget post() are logged via Future.add_done_callback.""" + executor = _make_executor() + mock_run = _make_mock_run() + mock_run.post.side_effect = RuntimeError("LangSmith API error") + tree = _ReplaySafeRunTree(mock_run, executor=executor) + + with caplog.at_level(logging.ERROR): + tree.post() + executor.shutdown(wait=True) + + # The error should have been logged + assert any("LangSmith API error" in record.message for record in caplog.records) + + @patch(_PATCH_IS_REPLAYING, return_value=False) + @patch(_PATCH_IN_WORKFLOW, return_value=True) + def test_patch_error_logged_via_done_callback( + self, + _mock_in_wf: Any, + _mock_replaying: Any, + caplog: pytest.LogCaptureFixture, + ) -> None: + """Errors from fire-and-forget patch() are logged via Future.add_done_callback.""" + executor = _make_executor() + mock_run = _make_mock_run() + mock_run.patch.side_effect = RuntimeError("LangSmith patch error") + tree = _ReplaySafeRunTree(mock_run, executor=executor) + + with caplog.at_level(logging.ERROR): + tree.patch() + executor.shutdown(wait=True) + + # The error should have been logged + assert any( + "LangSmith patch error" in record.message for record in caplog.records + ) + + +# =================================================================== +# TestReplaySuppression +# =================================================================== + + +class TestReplaySuppression: + """Tests for _is_replaying() check before executor submission.""" + + @patch(_PATCH_IS_REPLAYING, return_value=True) + @patch(_PATCH_IN_WORKFLOW, return_value=True) + def test_post_noop_during_replay( + self, _mock_in_wf: Any, _mock_replaying: Any + ) -> None: + """post() is a no-op during replay — no executor submission.""" + executor = _make_executor() + mock_run = _make_mock_run() + tree = _ReplaySafeRunTree(mock_run, executor=executor) + + tree.post() + + executor.shutdown(wait=True) + mock_run.post.assert_not_called() + + @patch(_PATCH_IS_REPLAYING, return_value=True) + @patch(_PATCH_IN_WORKFLOW, return_value=True) + def test_patch_noop_during_replay( + self, _mock_in_wf: Any, _mock_replaying: Any + ) -> None: + """patch() is a no-op during replay.""" + executor = _make_executor() + mock_run = _make_mock_run() + tree = _ReplaySafeRunTree(mock_run, executor=executor) + + tree.patch() + + executor.shutdown(wait=True) + mock_run.patch.assert_not_called() + + @patch(_PATCH_IS_REPLAYING, return_value=True) + @patch(_PATCH_IN_WORKFLOW, return_value=True) + def test_end_noop_during_replay( + self, _mock_in_wf: Any, _mock_replaying: Any + ) -> None: + """end() is a no-op during replay.""" + executor = _make_executor() + mock_run = _make_mock_run() + tree = _ReplaySafeRunTree(mock_run, executor=executor) + + tree.end(outputs={"result": "done"}) + + mock_run.end.assert_not_called() + + @patch(_PATCH_WF_NOW, return_value=datetime.now(timezone.utc)) + @patch(_PATCH_IS_REPLAYING, return_value=False) + @patch(_PATCH_IN_WORKFLOW, return_value=True) + def test_end_delegates_during_normal_execution( + self, _mock_in_wf: Any, _mock_replaying: Any, _mock_now: Any + ) -> None: + """end() delegates to self._run.end() during normal (non-replay) execution.""" + executor = _make_executor() + mock_run = _make_mock_run() + tree = _ReplaySafeRunTree(mock_run, executor=executor) + + tree.end(outputs={"result": "done"}, error="some error") + + mock_run.end.assert_called_once() + call_kwargs = mock_run.end.call_args.kwargs + assert call_kwargs["outputs"] == {"result": "done"} + assert call_kwargs["error"] == "some error" + assert "end_time" in call_kwargs + + +# =================================================================== +# TestRootReplaySafeRunTreeFactory +# =================================================================== + + +class TestRootReplaySafeRunTreeFactory: + """Tests for _RootReplaySafeRunTreeFactory subclass.""" + + def _make_factory(self, **kwargs: Any) -> _RootReplaySafeRunTreeFactory: + """Create a _RootReplaySafeRunTreeFactory for testing.""" + from temporalio.contrib.langsmith._interceptor import ( + _RootReplaySafeRunTreeFactory, + ) + + executor = kwargs.pop("executor", _make_executor()) + mock_client = kwargs.pop("ls_client", MagicMock()) + return _RootReplaySafeRunTreeFactory( + ls_client=mock_client, executor=executor, **kwargs + ) + + def test_post_raises_runtime_error(self) -> None: + """Factory's post() raises RuntimeError — factory must never be posted.""" + factory = self._make_factory() + with pytest.raises(RuntimeError, match="must never be posted"): + factory.post() + + def test_patch_raises_runtime_error(self) -> None: + """Factory's patch() raises RuntimeError — factory must never be patched.""" + factory = self._make_factory() + with pytest.raises(RuntimeError, match="must never be patched"): + factory.patch() + + def test_end_raises_runtime_error(self) -> None: + """Factory's end() raises RuntimeError — factory must never be ended.""" + factory = self._make_factory() + with pytest.raises(RuntimeError, match="must never be ended"): + factory.end(outputs={"status": "ok"}) + + @patch(_PATCH_GET_WF_RANDOM) + @patch(_PATCH_WF_NOW) + @patch(_PATCH_IN_WORKFLOW, return_value=True) + def test_create_child_returns_root_replay_safe_run_tree( + self, + _mock_in_wf: Any, + mock_now: Any, + mock_get_random: Any, + ) -> None: + """Factory's create_child creates a root _ReplaySafeRunTree (no parent_run_id).""" + import random as stdlib_random + + mock_get_random.return_value = stdlib_random.Random(42) + mock_now.return_value = datetime(2025, 1, 1, tzinfo=timezone.utc) + + executor = _make_executor() + mock_client = MagicMock() + factory = self._make_factory(ls_client=mock_client, executor=executor) + + child = factory.create_child(name="traceable-fn", run_type="chain") + + assert isinstance(child, _ReplaySafeRunTree) + # Child should be a root run — no parent_run_id + assert child._run.parent_run_id is None + + def test_create_child_inherits_client_session_and_replicas(self) -> None: + """Factory's children inherit ls_client, session_name, replicas.""" + executor = _make_executor() + mock_client = MagicMock() + mock_replicas = [MagicMock(), MagicMock()] + factory = self._make_factory( + ls_client=mock_client, + executor=executor, + session_name="my-project", + replicas=mock_replicas, + ) + + with patch(_PATCH_IN_WORKFLOW, return_value=False): + child = factory.create_child(name="traceable-fn", run_type="chain") + + assert isinstance(child, _ReplaySafeRunTree) + # Child should have the factory's ls_client, session_name, and replicas + assert child.ls_client is mock_client + assert child.session_name == "my-project" + assert child.replicas is mock_replicas + + def test_create_child_propagates_executor(self) -> None: + """Factory propagates executor to children.""" + executor = _make_executor() + factory = self._make_factory(executor=executor) + + with patch(_PATCH_IN_WORKFLOW, return_value=False): + child = factory.create_child(name="traceable-fn", run_type="chain") + + assert isinstance(child, _ReplaySafeRunTree) + assert child._executor is executor + + def test_create_child_maps_run_id_to_id(self) -> None: + """Factory's create_child maps run_id kwarg to id on the resulting RunTree. + + The run_id kwarg is mapped to id, matching LangSmith's + RunTree.create_child convention (run_trees.py:545). + """ + executor = _make_executor() + factory = self._make_factory(executor=executor) + explicit_id = uuid.uuid4() + + with patch(_PATCH_IN_WORKFLOW, return_value=False): + child = factory.create_child( + name="traceable-fn", run_type="chain", run_id=explicit_id + ) + + assert isinstance(child, _ReplaySafeRunTree) + # The underlying RunTree should have id set to the passed run_id + assert child._run.id == explicit_id + + def test_factory_not_in_collected_runs(self) -> None: + """Factory's post/patch/end raise RuntimeError — factory is never traced.""" + factory = self._make_factory() + + with pytest.raises(RuntimeError): + factory.post() + with pytest.raises(RuntimeError): + factory.patch() + with pytest.raises(RuntimeError): + factory.end() + + +# =================================================================== +# TestPostTimingDelayedExecution +# =================================================================== + + +class TestPostTimingDelayedExecution: + """Tests for post() timing when executor is busy. + + When post() is delayed (executor busy), create_run includes finalized data + (outputs/end_time), and the subsequent update_run from patch() is idempotent. + """ + + @patch(_PATCH_IS_REPLAYING, return_value=False) + @patch(_PATCH_IN_WORKFLOW, return_value=True) + def test_post_patch_fifo_ordering( + self, _mock_in_wf: Any, _mock_replaying: Any + ) -> None: + """post() always completes before patch() starts (FIFO via single-worker executor).""" + executor = _make_executor() + mock_run = _make_mock_run() + call_order: list[str] = [] + + def record_post(*_args: Any, **_kwargs: Any) -> None: + call_order.append("post") + + def record_patch(*_args: Any, **_kwargs: Any) -> None: + call_order.append("patch") + + mock_run.post.side_effect = record_post + mock_run.patch.side_effect = record_patch + + tree = _ReplaySafeRunTree(mock_run, executor=executor) + + tree.post() + tree.patch() + + executor.shutdown(wait=True) + + assert call_order == ["post", "patch"] + + @patch(_PATCH_IS_REPLAYING, return_value=False) + @patch(_PATCH_IN_WORKFLOW, return_value=True) + def test_delayed_post_reads_finalized_fields( + self, _mock_in_wf: Any, _mock_replaying: Any + ) -> None: + """When post() is delayed, create_run sees finalized outputs/end_time. + + Simulates: block executor → submit post() (queued) → call end() on + "workflow thread" to set outputs/end_time → release blocker → verify + post() saw the finalized fields via _get_dicts_safe(). + """ + executor = _make_executor() + mock_run = _make_mock_run() + + # Barrier to block executor so post() is delayed + blocker = threading.Event() + post_saw_outputs: list[Any] = [] + post_saw_end_time: list[Any] = [] + + # Block the executor with a dummy task + def blocking_task() -> None: + blocker.wait(timeout=5.0) + + executor.submit(blocking_task) + + # Record what fields post() sees when it finally runs + def capturing_post(*_args: Any, **_kwargs: Any) -> None: + post_saw_outputs.append(getattr(mock_run, "outputs", None)) + post_saw_end_time.append(getattr(mock_run, "end_time", None)) + + mock_run.post.side_effect = capturing_post + + tree = _ReplaySafeRunTree(mock_run, executor=executor) + + # Submit post() — it's queued behind the blocker + tree.post() + + # Simulate end() on the "workflow thread" while post() is still queued + finalized_outputs = {"result": "done"} + finalized_end_time = datetime(2025, 6, 1, tzinfo=timezone.utc) + mock_run.outputs = finalized_outputs + mock_run.end_time = finalized_end_time + + # Release the blocker — post() now runs and reads finalized fields + blocker.set() + executor.shutdown(wait=True) + + # post() should have seen the finalized outputs and end_time + assert len(post_saw_outputs) == 1 + assert post_saw_outputs[0] == finalized_outputs + assert len(post_saw_end_time) == 1 + assert post_saw_end_time[0] == finalized_end_time + + +# =================================================================== +# TestPostShutdownRaises +# =================================================================== + + +class TestPostShutdownRaises: + """Tests that post/patch raise after executor shutdown.""" + + @patch(_PATCH_IS_REPLAYING, return_value=False) + @patch(_PATCH_IN_WORKFLOW, return_value=True) + def test_post_raises_after_shutdown( + self, _mock_in_wf: Any, _mock_replaying: Any + ) -> None: + """After executor.shutdown(), post() raises RuntimeError.""" + executor = _make_executor() + executor.shutdown(wait=True) + + mock_run = _make_mock_run() + tree = _ReplaySafeRunTree(mock_run, executor=executor) + + with pytest.raises(RuntimeError): + tree.post() + + @patch(_PATCH_IS_REPLAYING, return_value=False) + @patch(_PATCH_IN_WORKFLOW, return_value=True) + def test_patch_raises_after_shutdown( + self, _mock_in_wf: Any, _mock_replaying: Any + ) -> None: + """After executor.shutdown(), patch() raises RuntimeError.""" + executor = _make_executor() + executor.shutdown(wait=True) + + mock_run = _make_mock_run() + tree = _ReplaySafeRunTree(mock_run, executor=executor) + + with pytest.raises(RuntimeError): + tree.patch() + + +# =================================================================== +# Test_ReplaySafeRunTreeConstructor +# =================================================================== + + +class Test_ReplaySafeRunTreeConstructor: + """Tests for _ReplaySafeRunTree accepting executor parameter.""" + + def test_constructor_stores_executor(self) -> None: + """The executor is stored and accessible.""" + executor = _make_executor() + mock_run = _make_mock_run() + tree = _ReplaySafeRunTree(mock_run, executor=executor) + assert tree._executor is executor diff --git a/tests/contrib/langsmith/test_integration.py b/tests/contrib/langsmith/test_integration.py new file mode 100644 index 000000000..f48d9d6ac --- /dev/null +++ b/tests/contrib/langsmith/test_integration.py @@ -0,0 +1,1280 @@ +"""Integration tests for LangSmith plugin with real Temporal worker.""" + +from __future__ import annotations + +import asyncio +import uuid +from collections.abc import Callable +from datetime import timedelta +from typing import Any +from unittest.mock import MagicMock + +import nexusrpc.handler +import pytest +from langsmith import traceable, tracing_context + +from temporalio import activity, common, nexus, workflow +from temporalio.client import ( + Client, + WorkflowFailureError, + WorkflowHandle, + WorkflowQueryFailedError, +) +from temporalio.contrib.langsmith import LangSmithPlugin +from temporalio.exceptions import ApplicationError +from temporalio.service import RPCError +from temporalio.testing import WorkflowEnvironment +from tests.contrib.langsmith.conftest import ( + InMemoryRunCollector, + build_trace_trees, + find_trace_trees, + make_mock_ls_client, +) +from tests.helpers import new_worker +from tests.helpers.nexus import make_nexus_endpoint_name +from tests.helpers.trace import assert_trace_hierarchy + +# --------------------------------------------------------------------------- +# Shared @traceable functions and activities +# --------------------------------------------------------------------------- + + +@traceable(name="inner_llm_call") +async def _inner_llm_call(prompt: str) -> str: + """Simulates an LLM call decorated with @traceable.""" + return f"response to: {prompt}" + + +@traceable(name="outer_chain") +async def _outer_chain(prompt: str) -> str: + """A @traceable that calls another @traceable.""" + return await _inner_llm_call(prompt) + + +@traceable +@activity.defn +async def traceable_activity() -> str: + """Activity that calls a @traceable function.""" + result = await _inner_llm_call("hello") + return result + + +@traceable +@activity.defn +async def nested_traceable_activity() -> str: + """Activity with two levels of @traceable nesting.""" + result = await _outer_chain("hello") + return result + + +# --------------------------------------------------------------------------- +# Shared workflows +# --------------------------------------------------------------------------- + + +@workflow.defn +class TraceableActivityWorkflow: + @workflow.run + async def run(self, _input: str = "") -> str: + return await workflow.execute_activity( + traceable_activity, + start_to_close_timeout=timedelta(seconds=10), + ) + + +@nexusrpc.handler.service_handler +class NexusService: + @nexus.workflow_run_operation + async def run_operation( + self, ctx: nexus.WorkflowRunOperationContext, input: str + ) -> nexus.WorkflowHandle[str]: + return await ctx.start_workflow( + TraceableActivityWorkflow.run, + input, + id=f"nexus-wf-{ctx.request_id}", + ) + + +# --------------------------------------------------------------------------- +# Simple/basic workflows and activities +# --------------------------------------------------------------------------- + + +@traceable +@activity.defn +async def simple_activity() -> str: + return "activity-done" + + +@workflow.defn +class SimpleWorkflow: + @workflow.run + async def run(self) -> str: + result = await workflow.execute_activity( + simple_activity, + start_to_close_timeout=timedelta(seconds=10), + ) + return result + + +# --------------------------------------------------------------------------- +# Signal/query/update workflows +# --------------------------------------------------------------------------- + + +@traceable(name="step_with_activity") +async def _step_with_activity() -> str: + """A @traceable step that wraps an activity call.""" + return await workflow.execute_activity( + nested_traceable_activity, + start_to_close_timeout=timedelta(seconds=10), + ) + + +@traceable(name="step_with_child_workflow") +async def _step_with_child_workflow() -> str: + """A @traceable step that wraps a child workflow call.""" + return await workflow.execute_child_workflow( + TraceableActivityWorkflow.run, + id=f"step-child-{workflow.info().workflow_id}", + ) + + +@traceable(name="step_with_nexus") +async def _step_with_nexus() -> str: + """A @traceable step that wraps a nexus operation.""" + nexus_client = workflow.create_nexus_client( + endpoint=make_nexus_endpoint_name(workflow.info().task_queue), + service=NexusService, + ) + nexus_handle = await nexus_client.start_operation( + operation=NexusService.run_operation, + input="test-input", + ) + return await nexus_handle + + +@workflow.defn +class ComprehensiveWorkflow: + def __init__(self) -> None: + self._signal_received = False + self._waiting_for_signal = False + self._complete = False + + @workflow.run + async def run(self) -> str: + await workflow.execute_activity( + nested_traceable_activity, + start_to_close_timeout=timedelta(seconds=10), + ) + await _step_with_activity() + await workflow.execute_local_activity( + nested_traceable_activity, + start_to_close_timeout=timedelta(seconds=10), + ) + await _outer_chain("from-workflow") + await workflow.execute_child_workflow( + TraceableActivityWorkflow.run, + id=f"child-{workflow.info().workflow_id}", + ) + await _step_with_child_workflow() + nexus_client = workflow.create_nexus_client( + endpoint=make_nexus_endpoint_name(workflow.info().task_queue), + service=NexusService, + ) + nexus_handle = await nexus_client.start_operation( + operation=NexusService.run_operation, + input="test-input", + ) + await nexus_handle + await _step_with_nexus() + + self._waiting_for_signal = True + await workflow.wait_condition(lambda: self._signal_received) + await workflow.execute_activity( + nested_traceable_activity, + start_to_close_timeout=timedelta(seconds=10), + ) + await workflow.wait_condition(lambda: self._complete) + return "comprehensive-done" + + @workflow.signal + def my_signal(self, _value: str) -> None: + self._signal_received = True + + @workflow.query + def my_query(self) -> bool: + return self._signal_received + + @workflow.query + def is_waiting_for_signal(self) -> bool: + return self._waiting_for_signal + + @workflow.update + def my_update(self, value: str) -> str: + self._complete = True + return f"updated-{value}" + + @my_update.validator + def validate_my_update(self, value: str) -> None: + if not value: + raise ValueError("empty") + + @workflow.update + def my_unvalidated_update(self, value: str) -> str: + return f"unvalidated-{value}" + + +# --------------------------------------------------------------------------- +# Error workflows and activities +# --------------------------------------------------------------------------- + + +@traceable +@activity.defn +async def failing_activity() -> str: + raise ApplicationError("activity-failed", non_retryable=True) + + +@traceable +@activity.defn +async def benign_failing_activity() -> str: + from temporalio.exceptions import ApplicationErrorCategory + + raise ApplicationError( + "benign-fail", + non_retryable=True, + category=ApplicationErrorCategory.BENIGN, + ) + + +@workflow.defn +class FailingWorkflow: + @workflow.run + async def run(self) -> str: + raise ApplicationError("workflow-failed", non_retryable=True) + + +@workflow.defn +class ActivityFailureWorkflow: + @workflow.run + async def run(self) -> str: + return await workflow.execute_activity( + failing_activity, + start_to_close_timeout=timedelta(seconds=10), + retry_policy=common.RetryPolicy(maximum_attempts=1), + ) + + +@workflow.defn +class BenignErrorWorkflow: + @workflow.run + async def run(self) -> str: + return await workflow.execute_activity( + benign_failing_activity, + start_to_close_timeout=timedelta(seconds=10), + retry_policy=common.RetryPolicy(maximum_attempts=1), + ) + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_plugin_and_collector( + **kwargs: Any, +) -> tuple[LangSmithPlugin, InMemoryRunCollector, MagicMock]: + """Create a LangSmithPlugin wired to an InMemoryRunCollector via mock client.""" + collector = InMemoryRunCollector() + mock_ls_client = make_mock_ls_client(collector) + plugin = LangSmithPlugin(client=mock_ls_client, **kwargs) + return plugin, collector, mock_ls_client + + +def _make_client_and_collector( + client: Client, **kwargs: Any +) -> tuple[Client, InMemoryRunCollector, MagicMock]: + """Create a Temporal Client with LangSmith plugin and an InMemoryRunCollector.""" + plugin, collector, mock_ls_client = _make_plugin_and_collector(**kwargs) + config = client.config() + config["plugins"] = [plugin] + return Client(**config), collector, mock_ls_client + + +def _make_temporal_client( + client: Client, mock_ls_client: MagicMock, **kwargs: Any +) -> Client: + """Create a Temporal Client with a fresh LangSmith plugin.""" + plugin = LangSmithPlugin(client=mock_ls_client, **kwargs) + config = client.config() + config["plugins"] = [plugin] + return Client(**config) + + +@traceable(name="poll_query") +async def _poll_query( + handle: WorkflowHandle[Any, Any], + query: Callable[..., Any], + *, + expected: Any = True, +) -> bool: + """Poll a workflow query until it returns the expected value.""" + while True: + try: + result = await handle.query(query) + if result == expected: + return True + except (WorkflowQueryFailedError, RPCError): + pass # Query not yet available (workflow hasn't started) + await asyncio.sleep(1) + + +# --------------------------------------------------------------------------- +# TestBasicTracing +# --------------------------------------------------------------------------- + + +class TestBasicTracing: + async def test_workflow_activity_trace_hierarchy( + self, + client: Client, + env: WorkflowEnvironment, # type:ignore[reportUnusedParameter] + ) -> None: + """StartWorkflow → RunWorkflow → StartActivity → RunActivity hierarchy.""" + temporal_client, collector, _ = _make_client_and_collector( + client, add_temporal_runs=True + ) + + async with new_worker( + temporal_client, + SimpleWorkflow, + activities=[simple_activity], + max_cached_workflows=0, + ) as worker: + result = await temporal_client.start_workflow( + SimpleWorkflow.run, + id=f"basic-trace-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + assert await result.result() == "activity-done" + + expected = [ + "StartWorkflow:SimpleWorkflow", + "RunWorkflow:SimpleWorkflow", + " StartActivity:simple_activity", + " RunActivity:simple_activity", + " simple_activity", + ] + assert_trace_hierarchy(build_trace_trees(collector), expected) + + # Verify run_type: RunActivity is "tool", others are "chain" + for run in collector.runs: + if run.name == "RunActivity:simple_activity": + assert run.run_type == "tool", ( + f"Expected RunActivity run_type='tool', got '{run.run_type}'" + ) + else: + assert run.run_type == "chain", ( + f"Expected {run.name} run_type='chain', got '{run.run_type}'" + ) + + # Verify successful runs have outputs == {"status": "ok"} + for run in collector.runs: + if ":" in run.name: # Interceptor runs use "Type:Name" format + assert run.outputs == {"status": "ok"}, ( + f"Expected {run.name} outputs={{'status': 'ok'}}, got {run.outputs}" + ) + + +# --------------------------------------------------------------------------- +# TestReplay +# --------------------------------------------------------------------------- + + +class TestReplay: + async def test_no_duplicate_traces_on_replay( + self, + client: Client, + env: WorkflowEnvironment, # type:ignore[reportUnusedParameter] + ) -> None: + """With max_cached_workflows=0 (forcing replay), no duplicate runs appear.""" + temporal_client, collector, _ = _make_client_and_collector( + client, add_temporal_runs=True + ) + + async with new_worker( + temporal_client, + TraceableActivityWorkflow, + activities=[traceable_activity], + max_cached_workflows=0, + ) as worker: + handle = await temporal_client.start_workflow( + TraceableActivityWorkflow.run, + id=f"replay-test-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.result() + + # Workflow→activity→@traceable flow should produce exactly these runs + # with no duplicates from replay: + expected = [ + "StartWorkflow:TraceableActivityWorkflow", + "RunWorkflow:TraceableActivityWorkflow", + " StartActivity:traceable_activity", + " RunActivity:traceable_activity", + " traceable_activity", + " inner_llm_call", + ] + assert_trace_hierarchy(build_trace_trees(collector), expected) + + +# --------------------------------------------------------------------------- +# TestErrorTracing +# --------------------------------------------------------------------------- + + +class TestErrorTracing: + async def test_activity_failure_marked( + self, + client: Client, + env: WorkflowEnvironment, # type:ignore[reportUnusedParameter] + ) -> None: + """A failing activity run is marked with an error.""" + temporal_client, collector, _ = _make_client_and_collector( + client, add_temporal_runs=True + ) + + async with new_worker( + temporal_client, + ActivityFailureWorkflow, + activities=[failing_activity], + workflow_failure_exception_types=[ApplicationError], + max_cached_workflows=0, + ) as worker: + handle = await temporal_client.start_workflow( + ActivityFailureWorkflow.run, + id=f"act-fail-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + with pytest.raises(WorkflowFailureError): + await handle.result() + + expected = [ + "StartWorkflow:ActivityFailureWorkflow", + "RunWorkflow:ActivityFailureWorkflow", + " StartActivity:failing_activity", + " RunActivity:failing_activity", + " failing_activity", + ] + assert_trace_hierarchy(build_trace_trees(collector), expected) + # Verify the RunActivity run has an error + activity_runs = [ + r for r in collector.runs if r.name == "RunActivity:failing_activity" + ] + assert len(activity_runs) == 1 + assert activity_runs[0].error == "ApplicationError: activity-failed" + + async def test_workflow_failure_marked( + self, + client: Client, + env: WorkflowEnvironment, # type:ignore[reportUnusedParameter] + ) -> None: + """A failing workflow run is marked with an error.""" + temporal_client, collector, _ = _make_client_and_collector( + client, add_temporal_runs=True + ) + + async with new_worker( + temporal_client, + FailingWorkflow, + workflow_failure_exception_types=[ApplicationError], + max_cached_workflows=0, + ) as worker: + handle = await temporal_client.start_workflow( + FailingWorkflow.run, + id=f"wf-fail-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + with pytest.raises(WorkflowFailureError): + await handle.result() + + expected = [ + "StartWorkflow:FailingWorkflow", + "RunWorkflow:FailingWorkflow", + ] + assert_trace_hierarchy(build_trace_trees(collector), expected) + # Verify the RunWorkflow run has an error + wf_runs = [r for r in collector.runs if r.name == "RunWorkflow:FailingWorkflow"] + assert len(wf_runs) == 1 + assert wf_runs[0].error == "ApplicationError: workflow-failed" + + async def test_benign_error_not_marked( + self, + client: Client, + env: WorkflowEnvironment, # type:ignore[reportUnusedParameter] + ) -> None: + """A benign ApplicationError does NOT mark the run as errored.""" + temporal_client, collector, _ = _make_client_and_collector( + client, add_temporal_runs=True + ) + + async with new_worker( + temporal_client, + BenignErrorWorkflow, + activities=[benign_failing_activity], + workflow_failure_exception_types=[ApplicationError], + max_cached_workflows=0, + ) as worker: + handle = await temporal_client.start_workflow( + BenignErrorWorkflow.run, + id=f"benign-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + with pytest.raises(WorkflowFailureError): + await handle.result() + + expected = [ + "StartWorkflow:BenignErrorWorkflow", + "RunWorkflow:BenignErrorWorkflow", + " StartActivity:benign_failing_activity", + " RunActivity:benign_failing_activity", + " benign_failing_activity", + ] + assert_trace_hierarchy(build_trace_trees(collector), expected) + # The RunActivity run for benign error should NOT have error set + activity_runs = [ + r for r in collector.runs if r.name == "RunActivity:benign_failing_activity" + ] + assert len(activity_runs) == 1 + assert activity_runs[0].error is None + + +# --------------------------------------------------------------------------- +# TestComprehensiveTracing +# --------------------------------------------------------------------------- + + +class TestComprehensiveTracing: + @pytest.mark.requires_local_server + async def test_comprehensive_with_temporal_runs( + self, client: Client, env: WorkflowEnvironment + ) -> None: + """Full trace hierarchy with worker restart mid-workflow. + + user_pipeline only wraps start_workflow (completing before the worker + starts), so poll/signal/query traces are naturally separate root traces. + """ + if env.supports_time_skipping: + pytest.skip("Time-skipping server doesn't persist headers.") + + task_queue = f"comprehensive-{uuid.uuid4()}" + workflow_id = f"comprehensive-{uuid.uuid4()}" + collector = InMemoryRunCollector() + mock_ls = make_mock_ls_client(collector) + temporal_client_1 = _make_temporal_client( + client, mock_ls, add_temporal_runs=True + ) + + @traceable(name="user_pipeline") + async def user_pipeline() -> WorkflowHandle[Any, Any]: + return await temporal_client_1.start_workflow( + ComprehensiveWorkflow.run, + id=workflow_id, + task_queue=task_queue, + ) + + with tracing_context(client=mock_ls, enabled=True): + # Start workflow — no worker yet, just a server RPC + handle = await user_pipeline() + + # Phase 1: worker picks up workflow, poll until signal wait + async with new_worker( + temporal_client_1, + ComprehensiveWorkflow, + TraceableActivityWorkflow, + activities=[nested_traceable_activity, traceable_activity], + nexus_service_handlers=[NexusService()], + task_queue=task_queue, + max_cached_workflows=0, + ) as worker: + await env.create_nexus_endpoint( + make_nexus_endpoint_name(worker.task_queue), + worker.task_queue, + ) + assert await _poll_query( + handle, + ComprehensiveWorkflow.is_waiting_for_signal, + expected=True, + ), "Workflow never reached signal wait point" + # Raw-client query (no LangSmith interceptor) — root-level trace + raw_handle = client.get_workflow_handle(workflow_id) + await raw_handle.query(ComprehensiveWorkflow.is_waiting_for_signal) + + # Phase 2: fresh worker, signal to resume, complete + temporal_client_2 = _make_temporal_client( + client, mock_ls, add_temporal_runs=True + ) + async with new_worker( + temporal_client_2, + ComprehensiveWorkflow, + TraceableActivityWorkflow, + activities=[nested_traceable_activity, traceable_activity], + nexus_service_handlers=[NexusService()], + task_queue=task_queue, + max_cached_workflows=0, + ): + handle_2 = temporal_client_2.get_workflow_handle(workflow_id) + await handle_2.query(ComprehensiveWorkflow.my_query) + await handle_2.signal(ComprehensiveWorkflow.my_signal, "hello") + await handle_2.execute_update( + ComprehensiveWorkflow.my_unvalidated_update, "test" + ) + await handle_2.execute_update(ComprehensiveWorkflow.my_update, "finish") + result = await handle_2.result() + + assert result == "comprehensive-done" + + trace_trees = build_trace_trees(collector) + + # user_pipeline trace: StartWorkflow + full workflow execution tree + workflow_trace_trees = find_trace_trees(trace_trees, "user_pipeline") + assert len(workflow_trace_trees) == 1 + expected_workflow = [ + "user_pipeline", + " StartWorkflow:ComprehensiveWorkflow", + " RunWorkflow:ComprehensiveWorkflow", + " StartActivity:nested_traceable_activity", + " RunActivity:nested_traceable_activity", + " nested_traceable_activity", + " outer_chain", + " inner_llm_call", + # step-wrapped activity + " step_with_activity", + " StartActivity:nested_traceable_activity", + " RunActivity:nested_traceable_activity", + " nested_traceable_activity", + " outer_chain", + " inner_llm_call", + " StartActivity:nested_traceable_activity", + " RunActivity:nested_traceable_activity", + " nested_traceable_activity", + " outer_chain", + " inner_llm_call", + " outer_chain", + " inner_llm_call", + " StartChildWorkflow:TraceableActivityWorkflow", + " RunWorkflow:TraceableActivityWorkflow", + " StartActivity:traceable_activity", + " RunActivity:traceable_activity", + " traceable_activity", + " inner_llm_call", + # step-wrapped child workflow + " step_with_child_workflow", + " StartChildWorkflow:TraceableActivityWorkflow", + " RunWorkflow:TraceableActivityWorkflow", + " StartActivity:traceable_activity", + " RunActivity:traceable_activity", + " traceable_activity", + " inner_llm_call", + " StartNexusOperation:NexusService/run_operation", + " RunStartNexusOperationHandler:NexusService/run_operation", + " StartWorkflow:TraceableActivityWorkflow", + " RunWorkflow:TraceableActivityWorkflow", + " StartActivity:traceable_activity", + " RunActivity:traceable_activity", + " traceable_activity", + " inner_llm_call", + # step-wrapped nexus operation + " step_with_nexus", + " StartNexusOperation:NexusService/run_operation", + " RunStartNexusOperationHandler:NexusService/run_operation", + " StartWorkflow:TraceableActivityWorkflow", + " RunWorkflow:TraceableActivityWorkflow", + " StartActivity:traceable_activity", + " RunActivity:traceable_activity", + " traceable_activity", + " inner_llm_call", + # post-signal + " StartActivity:nested_traceable_activity", + " RunActivity:nested_traceable_activity", + " nested_traceable_activity", + " outer_chain", + " inner_llm_call", + ] + assert_trace_hierarchy(workflow_trace_trees, expected_workflow) + + # poll_query trace (separate root, variable number of iterations) + poll_trace_trees = find_trace_trees(trace_trees, "poll_query") + assert len(poll_trace_trees) == 1 + poll = poll_trace_trees[0] + assert poll.name == "poll_query" + poll_children = poll.children + for poll_child in poll_children: + assert poll_child.name == "QueryWorkflow:is_waiting_for_signal" + assert [child.name for child in poll_child.children] == [ + "HandleQuery:is_waiting_for_signal" + ] + assert not poll_child.children[0].children + + # Raw-client query — no parent context, appears as root + raw_query_trace_trees = [ + trace for trace in trace_trees if trace.name.startswith("HandleQuery:") + ] + assert len(raw_query_trace_trees) == 1 + + # Phase 2: each operation is its own root trace + query_trace_trees = find_trace_trees(trace_trees, "QueryWorkflow:my_query") + assert len(query_trace_trees) == 1 + assert_trace_hierarchy( + query_trace_trees, + [ + "QueryWorkflow:my_query", + " HandleQuery:my_query", + ], + ) + + signal_trace_trees = find_trace_trees(trace_trees, "SignalWorkflow:my_signal") + assert len(signal_trace_trees) == 1 + assert_trace_hierarchy( + signal_trace_trees, + [ + "SignalWorkflow:my_signal", + " HandleSignal:my_signal", + ], + ) + + update_trace_trees = find_trace_trees( + trace_trees, "StartWorkflowUpdate:my_update" + ) + assert len(update_trace_trees) == 1 + assert_trace_hierarchy( + update_trace_trees, + [ + "StartWorkflowUpdate:my_update", + " ValidateUpdate:my_update", + " HandleUpdate:my_update", + ], + ) + + # Update without a validator — no ValidateUpdate trace + unvalidated_trace_trees = find_trace_trees( + trace_trees, "StartWorkflowUpdate:my_unvalidated_update" + ) + assert len(unvalidated_trace_trees) == 1 + assert_trace_hierarchy( + unvalidated_trace_trees, + [ + "StartWorkflowUpdate:my_unvalidated_update", + " HandleUpdate:my_unvalidated_update", + ], + ) + + @pytest.mark.requires_local_server + async def test_comprehensive_without_temporal_runs( + self, client: Client, env: WorkflowEnvironment + ) -> None: + """Same workflow with add_temporal_runs=False and worker restart. + + Only @traceable runs appear. Context propagation via headers still works. + user_pipeline only wraps start_workflow, so poll traces are separate roots. + """ + if env.supports_time_skipping: + pytest.skip("Time-skipping server doesn't persist headers.") + + task_queue = f"comprehensive-no-runs-{uuid.uuid4()}" + workflow_id = f"comprehensive-no-runs-{uuid.uuid4()}" + collector = InMemoryRunCollector() + mock_ls = make_mock_ls_client(collector) + temporal_client_1 = _make_temporal_client( + client, mock_ls, add_temporal_runs=False + ) + + @traceable(name="user_pipeline") + async def user_pipeline() -> WorkflowHandle[Any, Any]: + return await temporal_client_1.start_workflow( + ComprehensiveWorkflow.run, + id=workflow_id, + task_queue=task_queue, + ) + + with tracing_context(client=mock_ls, enabled=True): + handle = await user_pipeline() + + # Phase 1: worker picks up workflow, poll until signal wait + async with new_worker( + temporal_client_1, + ComprehensiveWorkflow, + TraceableActivityWorkflow, + activities=[nested_traceable_activity, traceable_activity], + nexus_service_handlers=[NexusService()], + task_queue=task_queue, + max_cached_workflows=0, + ) as worker: + await env.create_nexus_endpoint( + make_nexus_endpoint_name(worker.task_queue), + worker.task_queue, + ) + # Raw-client query — no interceptor, produces nothing + raw_handle = client.get_workflow_handle(workflow_id) + await raw_handle.query(ComprehensiveWorkflow.is_waiting_for_signal) + assert await _poll_query( + handle, + ComprehensiveWorkflow.is_waiting_for_signal, + expected=True, + ), "Workflow never reached signal wait point" + + # Phase 2: fresh worker, signal to resume, complete + temporal_client_2 = _make_temporal_client( + client, mock_ls, add_temporal_runs=False + ) + async with new_worker( + temporal_client_2, + ComprehensiveWorkflow, + TraceableActivityWorkflow, + activities=[nested_traceable_activity, traceable_activity], + nexus_service_handlers=[NexusService()], + task_queue=task_queue, + max_cached_workflows=0, + ): + handle_2 = temporal_client_2.get_workflow_handle(workflow_id) + await handle_2.signal(ComprehensiveWorkflow.my_signal, "hello") + await handle_2.execute_update( + ComprehensiveWorkflow.my_unvalidated_update, "test" + ) + await handle_2.execute_update(ComprehensiveWorkflow.my_update, "finish") + result = await handle_2.result() + + assert result == "comprehensive-done" + + trace_trees = build_trace_trees(collector) + + # Main workflow trace (only @traceable runs, nested under user_pipeline) + workflow_trace_trees = find_trace_trees(trace_trees, "user_pipeline") + assert len(workflow_trace_trees) == 1 + expected_workflow = [ + "user_pipeline", + " nested_traceable_activity", + " outer_chain", + " inner_llm_call", + " step_with_activity", + " nested_traceable_activity", + " outer_chain", + " inner_llm_call", + " nested_traceable_activity", + " outer_chain", + " inner_llm_call", + " outer_chain", + " inner_llm_call", + " traceable_activity", + " inner_llm_call", + " step_with_child_workflow", + " traceable_activity", + " inner_llm_call", + " traceable_activity", + " inner_llm_call", + " step_with_nexus", + " traceable_activity", + " inner_llm_call", + # post-signal + " nested_traceable_activity", + " outer_chain", + " inner_llm_call", + ] + assert_trace_hierarchy(workflow_trace_trees, expected_workflow) + + # Poll query — separate root, just the @traceable wrapper, no Temporal children + poll_trace_trees = find_trace_trees(trace_trees, "poll_query") + assert len(poll_trace_trees) == 1 + assert_trace_hierarchy(poll_trace_trees, ["poll_query"]) + + +# --------------------------------------------------------------------------- +# TestBackgroundIOIntegration — _RootReplaySafeRunTreeFactory + sync @traceable +# --------------------------------------------------------------------------- + + +@traceable(name="sync_inner_llm_call") +def _sync_inner_llm_call(prompt: str) -> str: + """Sync @traceable simulating an LLM call.""" + return f"sync-response to: {prompt}" + + +@traceable(name="sync_outer_chain") +def _sync_outer_chain(prompt: str) -> str: + """Sync @traceable that calls another sync @traceable.""" + return _sync_inner_llm_call(prompt) + + +@traceable(name="async_calls_sync") +async def _async_calls_sync(prompt: str) -> str: + """Async @traceable that calls a sync @traceable — the interesting mixed case.""" + return _sync_inner_llm_call(prompt) + + +@workflow.defn +class FactoryTraceableWorkflow: + """Workflow exercising _RootReplaySafeRunTreeFactory with async, sync, and mixed @traceable. + + Covers three code paths through create_child: + - async→async nesting + - sync→sync nesting (sync @traceable entry to factory) + - async→sync nesting (cross-boundary case) + """ + + @workflow.run + async def run(self) -> str: + r1 = await _outer_chain("async") + r2 = _sync_outer_chain("sync") + r3 = await _async_calls_sync("mixed") + # Activity with nested @traceable + await workflow.execute_activity( + nested_traceable_activity, + start_to_close_timeout=timedelta(seconds=10), + ) + return f"{r1}|{r2}|{r3}" + + +class TestBackgroundIOIntegration: + """Integration tests for workflows using add_temporal_runs=False without external context. + + Exercises the _RootReplaySafeRunTreeFactory path with sync, async, and mixed @traceable + nesting. Verifies root-run creation, correct nesting hierarchy, and replay safety. + """ + + async def test_factory_traceable_no_external_context( + self, + client: Client, + env: WorkflowEnvironment, # type:ignore[reportUnusedParameter] + ) -> None: + """Exercises _RootReplaySafeRunTreeFactory: add_temporal_runs=False, no external context. + + Uses a workflow with async→async, sync→sync, and async→sync @traceable + nesting, plus an activity with nested @traceable. Verifies: + - Each top-level @traceable becomes a root run (factory creates root children) + - Nested @traceable calls nest correctly under their parent + - Activity @traceable also produces correct hierarchy + - No phantom factory run appears in collected runs + - No duplicate run IDs after replay (max_cached_workflows=0) + """ + temporal_client, collector, _ = _make_client_and_collector( + client, add_temporal_runs=False + ) + + async with new_worker( + temporal_client, + FactoryTraceableWorkflow, + activities=[nested_traceable_activity], + max_cached_workflows=0, + ) as worker: + handle = await temporal_client.start_workflow( + FactoryTraceableWorkflow.run, + id=f"factory-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + result = await handle.result() + + assert ( + result + == "response to: async|sync-response to: sync|sync-response to: mixed" + ) + + expected = [ + "outer_chain", + " inner_llm_call", + "sync_outer_chain", + " sync_inner_llm_call", + "async_calls_sync", + " sync_inner_llm_call", + "nested_traceable_activity", + " outer_chain", + " inner_llm_call", + ] + assert_trace_hierarchy(build_trace_trees(collector), expected) + + # Verify no duplicate run IDs (replay safety with max_cached_workflows=0) + run_ids = [r.id for r in collector.runs] + assert len(run_ids) == len(set(run_ids)), ( + f"Duplicate run IDs found (replay issue): {run_ids}" + ) + + async def test_factory_passes_project_name_to_children( + self, + client: Client, + env: WorkflowEnvironment, # type:ignore[reportUnusedParameter] + ) -> None: + """Factory children inherit project_name (session_name) from plugin config.""" + temporal_client, _collector, mock_ls_client = _make_client_and_collector( + client, add_temporal_runs=False, project_name="my-ls-project" + ) + + async with new_worker( + temporal_client, + FactoryTraceableWorkflow, + activities=[nested_traceable_activity], + max_cached_workflows=0, + ) as worker: + handle = await temporal_client.start_workflow( + FactoryTraceableWorkflow.run, + id=f"factory-proj-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.result() + + # Verify create_run calls include session_name from project_name + for call in mock_ls_client.create_run.call_args_list: + session = call.kwargs.get("session_name") + assert session == "my-ls-project", ( + f"Expected session_name='my-ls-project', got {session!r} " + f"in create_run call: {call.kwargs.get('name')}" + ) + + async def test_mixed_sync_async_traceable_with_temporal_runs( + self, + client: Client, + env: WorkflowEnvironment, # type:ignore[reportUnusedParameter] + ) -> None: + """Exercises _ReplaySafeRunTree.create_child with mixed sync/async @traceable. + + With add_temporal_runs=True, the interceptor creates a real + _ReplaySafeRunTree as parent. This test verifies create_child + propagation works at every level regardless of sync/async, with + correct parent-child hierarchy and no duplicate run IDs after replay. + """ + temporal_client, collector, _ = _make_client_and_collector( + client, add_temporal_runs=True + ) + + async with new_worker( + temporal_client, + FactoryTraceableWorkflow, + activities=[nested_traceable_activity], + max_cached_workflows=0, + ) as worker: + handle = await temporal_client.start_workflow( + FactoryTraceableWorkflow.run, + id=f"mixed-temporal-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + result = await handle.result() + + assert ( + result + == "response to: async|sync-response to: sync|sync-response to: mixed" + ) + + # With add_temporal_runs=True, Temporal operations get their own runs. + # @traceable calls nest under the RunWorkflow run. + expected = [ + "StartWorkflow:FactoryTraceableWorkflow", + "RunWorkflow:FactoryTraceableWorkflow", + " outer_chain", + " inner_llm_call", + " sync_outer_chain", + " sync_inner_llm_call", + " async_calls_sync", + " sync_inner_llm_call", + " StartActivity:nested_traceable_activity", + " RunActivity:nested_traceable_activity", + " nested_traceable_activity", + " outer_chain", + " inner_llm_call", + ] + assert_trace_hierarchy(build_trace_trees(collector), expected) + + # Verify no duplicate run IDs (replay safety with max_cached_workflows=0) + run_ids = [r.id for r in collector.runs] + assert len(run_ids) == len(set(run_ids)), ( + f"Duplicate run IDs found (replay issue): {run_ids}" + ) + + +# --- Nexus service with direct @traceable call in handler --- + + +@traceable(name="nexus_direct_traceable") +async def _nexus_direct_traceable(input: str) -> str: + """A @traceable function called directly from a nexus handler.""" + return await _inner_llm_call(input) + + +@nexusrpc.handler.service_handler +class DirectTraceableNexusService: + """Nexus service that calls @traceable directly (not via activity).""" + + @nexusrpc.handler.sync_operation + async def direct_traceable_op( + self, + ctx: nexusrpc.handler.StartOperationContext, # type:ignore[reportUnusedParameter] + input: str, + ) -> str: + return await _nexus_direct_traceable(input) + + +@workflow.defn +class NexusDirectTraceableWorkflow: + """Workflow that calls a nexus operation whose handler uses @traceable directly.""" + + @workflow.run + async def run(self) -> str: + nexus_client = workflow.create_nexus_client( + endpoint=make_nexus_endpoint_name(workflow.info().task_queue), + service=DirectTraceableNexusService, + ) + return await nexus_client.execute_operation( + operation=DirectTraceableNexusService.direct_traceable_op, + input="nexus-input", + ) + + +# --------------------------------------------------------------------------- +# TestNexusInboundTracing +# --------------------------------------------------------------------------- + + +class TestNexusInboundTracing: + """Verifies nexus handlers receive tracing_context for @traceable collection.""" + + @pytest.mark.requires_local_server + async def test_nexus_direct_traceable_without_temporal_runs( + self, + client: Client, + env: WorkflowEnvironment, + ) -> None: + """@traceable in nexus handler works with add_temporal_runs=False. + + The worker must be started OUTSIDE tracing_context so that nexus handler + tasks inherit a clean contextvars state. Only the client call gets + tracing_context — the interceptor's tracing_context setup (or lack + thereof) is the only thing that should provide context to the handler. + """ + if env.supports_time_skipping: + pytest.skip("Time-skipping server doesn't persist headers.") + + task_queue = f"nexus-direct-{uuid.uuid4()}" + collector = InMemoryRunCollector() + mock_ls = make_mock_ls_client(collector) + temporal_client = _make_temporal_client( + client, mock_ls, add_temporal_runs=False + ) + + # Worker starts OUTSIDE tracing_context — nexus handler tasks get clean context + async with new_worker( + temporal_client, + NexusDirectTraceableWorkflow, + nexus_service_handlers=[DirectTraceableNexusService()], + task_queue=task_queue, + max_cached_workflows=0, + ) as worker: + await env.create_nexus_endpoint( + make_nexus_endpoint_name(worker.task_queue), + worker.task_queue, + ) + # Only the client call gets tracing context, not the worker + with tracing_context(client=mock_ls, enabled=True): + handle = await temporal_client.start_workflow( + NexusDirectTraceableWorkflow.run, + id=f"nexus-direct-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + result = await handle.result() + + assert result == "response to: nexus-input" + + # @traceable runs from inside the nexus handler should be collected + # via the interceptor's tracing_context setup. + expected = [ + "nexus_direct_traceable", + " inner_llm_call", + ] + assert_trace_hierarchy(build_trace_trees(collector), expected) + + +# --------------------------------------------------------------------------- +# TestBuiltinQueryFiltering +# --------------------------------------------------------------------------- + + +@workflow.defn +class QueryFilteringWorkflow: + """Workflow with a user query and a signal to complete.""" + + def __init__(self) -> None: + self._complete = False + + @workflow.run + async def run(self) -> str: + await workflow.wait_condition(lambda: self._complete) + return "done" + + @workflow.signal + def complete(self) -> None: + self._complete = True + + @workflow.query + def my_query(self) -> str: + return "query-result" + + +class TestBuiltinQueryFiltering: + """Verifies __temporal_ prefixed queries are not traced.""" + + async def test_temporal_prefixed_query_not_traced( + self, + client: Client, + ) -> None: + """__temporal_workflow_metadata query should not produce a trace, + but user-defined queries should still be traced. + + Uses add_temporal_runs=False on the query client to suppress + client-side QueryWorkflow traces, isolating the test to + worker-side HandleQuery traces only. + """ + + task_queue = f"query-filter-{uuid.uuid4()}" + collector = InMemoryRunCollector() + mock_ls = make_mock_ls_client(collector) + + # Worker client: add_temporal_runs=True so HandleQuery traces are created + worker_client = _make_temporal_client(client, mock_ls, add_temporal_runs=True) + # Query client: add_temporal_runs=False to suppress client-side traces + query_client = _make_temporal_client(client, mock_ls, add_temporal_runs=False) + + async with new_worker( + worker_client, + QueryFilteringWorkflow, + task_queue=task_queue, + max_cached_workflows=0, + ) as worker: + handle = await query_client.start_workflow( + QueryFilteringWorkflow.run, + id=f"query-filter-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + # Wait for workflow to start by polling the user query + assert await _poll_query( + handle, + QueryFilteringWorkflow.my_query, + expected="query-result", + ), "Workflow never started" + + collector.clear() + + # Built-in queries — should NOT be traced + await handle.query("__temporal_workflow_metadata") + + # User query — should be traced + await handle.query(QueryFilteringWorkflow.my_query) + + await handle.signal(QueryFilteringWorkflow.complete) + assert await handle.result() == "done" + + # Built-in queries should be absent; only user query and signal remain. + assert_trace_hierarchy( + build_trace_trees(collector), + [ + "HandleQuery:my_query", + "HandleSignal:complete", + ], + ) diff --git a/tests/contrib/langsmith/test_interceptor.py b/tests/contrib/langsmith/test_interceptor.py new file mode 100644 index 000000000..4c18c3f9f --- /dev/null +++ b/tests/contrib/langsmith/test_interceptor.py @@ -0,0 +1,1193 @@ +"""Tests for LangSmith interceptor points and helper functions.""" + +from __future__ import annotations + +import asyncio +from concurrent.futures import ThreadPoolExecutor +from datetime import datetime, timezone +from typing import Any +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from temporalio.api.common.v1 import Payload +from temporalio.contrib.langsmith import LangSmithInterceptor +from temporalio.contrib.langsmith._interceptor import ( + HEADER_KEY, + _extract_context, + _inject_context, + _LangSmithWorkflowInboundInterceptor, + _maybe_run, + _ReplaySafeRunTree, +) +from temporalio.worker import HandleQueryInput + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +# Common patch targets (interceptor module) +_MOD = "temporalio.contrib.langsmith._interceptor" +_PATCH_RUNTREE = f"{_MOD}.RunTree" +_PATCH_IN_WORKFLOW = f"{_MOD}.temporalio.workflow.in_workflow" +_PATCH_IS_REPLAYING = f"{_MOD}.temporalio.workflow.unsafe.is_replaying_history_events" +_PATCH_WF_NOW = f"{_MOD}.temporalio.workflow.now" +_PATCH_WF_INFO = f"{_MOD}.temporalio.workflow.info" +_PATCH_TRACING_CTX = f"{_MOD}.tracing_context" +_PATCH_EXTRACT_NEXUS = f"{_MOD}._extract_nexus_context" +_PATCH_INJECT_NEXUS = f"{_MOD}._inject_nexus_context" +_PATCH_GET_CURRENT_RUN = f"{_MOD}.get_current_run_tree" + + +def _make_mock_run() -> MagicMock: + """Create a mock RunTree with working to_headers() for _inject_context.""" + mock_run = MagicMock() + mock_run.to_headers.return_value = {"langsmith-trace": "test-trace-id"} + return mock_run + + +def _mock_workflow_info(**overrides: Any) -> MagicMock: + """Create a mock workflow Info object.""" + info = MagicMock() + info.workflow_id = overrides.get("workflow_id", "test-wf-id") + info.run_id = overrides.get("run_id", "test-run-id") + info.workflow_type = overrides.get("workflow_type", "TestWorkflow") + return info + + +def _mock_activity_info(**overrides: Any) -> MagicMock: + """Create a mock activity Info object.""" + info = MagicMock() + info.workflow_id = overrides.get("workflow_id", "test-wf-id") + info.workflow_run_id = overrides.get("workflow_run_id", "test-run-id") + info.activity_id = overrides.get("activity_id", "test-activity-id") + info.activity_type = overrides.get("activity_type", "test_activity") + return info + + +def _make_executor() -> ThreadPoolExecutor: + """Create a single-worker executor for tests.""" + return ThreadPoolExecutor(max_workers=1) + + +def _get_runtree_name(MockRunTree: MagicMock) -> str: + """Extract the 'name' kwarg from RunTree constructor call.""" + MockRunTree.assert_called_once() + return MockRunTree.call_args.kwargs["name"] + + +def _get_runtree_metadata(MockRunTree: MagicMock) -> dict[str, Any]: + """Extract metadata from RunTree constructor kwargs. + + The design stores metadata in the 'extra' kwarg as {"metadata": {...}}. + """ + MockRunTree.assert_called_once() + kwargs = MockRunTree.call_args.kwargs + extra = kwargs.get("extra", {}) + if extra and "metadata" in extra: + return extra["metadata"] + # Alternatively, metadata might be passed directly + return kwargs.get("metadata", {}) + + +# =================================================================== +# TestBuiltinQueryFiltering +# =================================================================== + + +class _RecordingWorkflowInboundInterceptor: + def __init__(self) -> None: + self.queries: list[HandleQueryInput] = [] + + async def handle_query(self, input: HandleQueryInput) -> str: + self.queries.append(input) + return "forwarded" + + +class TestBuiltinQueryFiltering: + async def test_builtin_queries_bypass_tracing(self) -> None: + next_interceptor = _RecordingWorkflowInboundInterceptor() + interceptor = _LangSmithWorkflowInboundInterceptor( + next_interceptor # type: ignore[arg-type] + ) + + with patch.object(interceptor, "_workflow_maybe_run") as maybe_run: + for query in ( + "__temporal_workflow_metadata", + "__stack_trace", + "__enhanced_stack_trace", + ): + assert ( + await interceptor.handle_query( + HandleQueryInput(id="id", query=query, args=[], headers={}) + ) + == "forwarded" + ) + + assert [input.query for input in next_interceptor.queries] == [ + "__temporal_workflow_metadata", + "__stack_trace", + "__enhanced_stack_trace", + ] + maybe_run.assert_not_called() + + +# =================================================================== +# TestContextPropagation +# =================================================================== + + +class TestContextPropagation: + """Tests for _inject_context / _extract_context roundtrip.""" + + @patch(_PATCH_RUNTREE) + def test_inject_extract_roundtrip(self, MockRunTree: Any) -> None: + """Inject a mock run tree's headers, then extract. Verify roundtrip.""" + mock_run = MagicMock() + mock_run.to_headers.return_value = { + "langsmith-trace": "test-trace-id", + "parent": "abc-123", + } + + headers: dict[str, Payload] = {} + result = _inject_context(headers, mock_run) + + assert HEADER_KEY in result + + # Mock from_headers for extraction (real one needs valid LangSmith header format) + mock_extracted = MagicMock() + MockRunTree.from_headers.return_value = mock_extracted + + extracted = _extract_context(result, _make_executor(), MagicMock()) + # extracted should be a _ReplaySafeRunTree wrapping the reconstructed run + assert isinstance(extracted, _ReplaySafeRunTree) + assert extracted._run is mock_extracted + MockRunTree.from_headers.assert_called_once() + + def test_extract_missing_header(self) -> None: + """When the _temporal-langsmith-context header is absent, returns None.""" + headers: dict[str, Payload] = {} + result = _extract_context(headers, _make_executor(), MagicMock()) + assert result is None + + def test_inject_preserves_existing_headers(self) -> None: + """Injecting LangSmith context does not overwrite other existing headers.""" + mock_run = MagicMock() + mock_run.to_headers.return_value = {"langsmith-trace": "val"} + + existing_payload = Payload(data=b"existing") + headers: dict[str, Payload] = {"my-header": existing_payload} + result = _inject_context(headers, mock_run) + + assert "my-header" in result + assert result["my-header"] is existing_payload + assert HEADER_KEY in result + + +# =================================================================== +# TestReplaySafety +# =================================================================== + + +class TestReplaySafety: + """Tests for replay-safe tracing behavior.""" + + @patch(_PATCH_RUNTREE) + @patch(_PATCH_IS_REPLAYING, return_value=True) + @patch(_PATCH_IN_WORKFLOW, return_value=True) + def test_replay_noop_post_end_patch( + self, _mock_in_wf: Any, _mock_replaying: Any, MockRunTree: Any + ) -> None: + """During replay, RunTree is created but post/end/patch are no-ops.""" + mock_run = _make_mock_run() + MockRunTree.return_value = mock_run + mock_client = MagicMock() + with _maybe_run( + mock_client, + "TestRun", + add_temporal_runs=True, + executor=_make_executor(), + ): + pass + # RunTree IS created (wrapped in _ReplaySafeRunTree) + MockRunTree.assert_called_once() + # But post/end/patch are no-ops during replay + mock_run.post.assert_not_called() + mock_run.end.assert_not_called() + mock_run.patch.assert_not_called() + + @patch(_PATCH_WF_NOW, return_value=datetime.now(timezone.utc)) + @patch(_PATCH_RUNTREE) + @patch(_PATCH_IS_REPLAYING, return_value=False) + @patch(_PATCH_IN_WORKFLOW, return_value=True) + def test_create_trace_when_not_replaying( + self, _mock_in_wf: Any, _mock_replaying: Any, MockRunTree: Any, _mock_now: Any + ) -> None: + """When not replaying (but in workflow), _maybe_run creates a _ReplaySafeRunTree.""" + mock_run = _make_mock_run() + MockRunTree.return_value = mock_run + mock_client = MagicMock() + with _maybe_run( + mock_client, + "TestRun", + add_temporal_runs=True, + executor=_make_executor(), + ): + pass + MockRunTree.assert_called_once() + assert MockRunTree.call_args.kwargs["name"] == "TestRun" + + @patch(_PATCH_RUNTREE) + @patch(_PATCH_IN_WORKFLOW, return_value=False) + def test_create_trace_outside_workflow( + self, _mock_in_wf: Any, MockRunTree: Any + ) -> None: + """Outside workflow (client/activity), RunTree IS created.""" + mock_run = _make_mock_run() + MockRunTree.return_value = mock_run + mock_client = MagicMock() + with _maybe_run( + mock_client, + "TestRun", + add_temporal_runs=True, + executor=_make_executor(), + ): + pass + MockRunTree.assert_called_once() + + +# =================================================================== +# TestErrorHandling +# =================================================================== + + +class TestErrorHandling: + """Tests for _maybe_run error handling.""" + + @patch(_PATCH_RUNTREE) + @patch(_PATCH_IN_WORKFLOW, return_value=False) + def test_exception_marks_run_errored( + self, _mock_in_wf: Any, MockRunTree: Any + ) -> None: + """RuntimeError marks the run as errored and re-raises.""" + mock_run = _make_mock_run() + MockRunTree.return_value = mock_run + mock_client = MagicMock() + with pytest.raises(RuntimeError, match="boom"): + with _maybe_run( + mock_client, + "TestRun", + add_temporal_runs=True, + executor=_make_executor(), + ): + raise RuntimeError("boom") + # run.end should have been called with error containing "boom" + mock_run.end.assert_called() + end_kwargs = mock_run.end.call_args.kwargs + assert end_kwargs["error"] == "RuntimeError: boom" + mock_run.patch.assert_called() + + @patch(_PATCH_RUNTREE) + @patch(_PATCH_IN_WORKFLOW, return_value=False) + def test_benign_application_error_not_marked( + self, _mock_in_wf: Any, MockRunTree: Any + ) -> None: + """Benign ApplicationError does not mark the run as errored.""" + from temporalio.exceptions import ApplicationError, ApplicationErrorCategory + + mock_run = _make_mock_run() + MockRunTree.return_value = mock_run + mock_client = MagicMock() + with pytest.raises(ApplicationError): + with _maybe_run( + mock_client, + "TestRun", + add_temporal_runs=True, + executor=_make_executor(), + ): + raise ApplicationError( + "benign", + category=ApplicationErrorCategory.BENIGN, + ) + # run.end should NOT have been called with error= + end_calls = mock_run.end.call_args_list + for c in end_calls: + assert "error" not in (c.kwargs or {}) + + @patch(_PATCH_RUNTREE) + @patch(_PATCH_IN_WORKFLOW, return_value=False) + def test_non_benign_application_error_marked( + self, _mock_in_wf: Any, MockRunTree: Any + ) -> None: + """Non-benign ApplicationError marks the run as errored.""" + from temporalio.exceptions import ApplicationError + + mock_run = _make_mock_run() + MockRunTree.return_value = mock_run + mock_client = MagicMock() + with pytest.raises(ApplicationError): + with _maybe_run( + mock_client, + "TestRun", + add_temporal_runs=True, + executor=_make_executor(), + ): + raise ApplicationError("bad", non_retryable=True) + mock_run.end.assert_called() + end_kwargs = mock_run.end.call_args.kwargs + assert end_kwargs["error"] == "ApplicationError: bad" + mock_run.patch.assert_called() + + @patch(_PATCH_RUNTREE) + @patch(_PATCH_IN_WORKFLOW, return_value=False) + def test_success_completes_normally( + self, _mock_in_wf: Any, MockRunTree: Any + ) -> None: + """On success, run.end(outputs={"status": "ok"}) and run.patch() are called.""" + mock_run = _make_mock_run() + MockRunTree.return_value = mock_run + mock_client = MagicMock() + with _maybe_run( + mock_client, + "TestRun", + add_temporal_runs=True, + executor=_make_executor(), + ): + pass + mock_run.end.assert_called_once() + end_kwargs = mock_run.end.call_args.kwargs + assert end_kwargs.get("outputs") == {"status": "ok"} + mock_run.patch.assert_called() + + @patch(_PATCH_RUNTREE) + @patch(_PATCH_IN_WORKFLOW, return_value=False) + def test_cancelled_error_propagates_without_marking_run( + self, _mock_in_wf: Any, MockRunTree: Any + ) -> None: + """CancelledError (BaseException) propagates without marking run as errored. + + _maybe_run catches Exception only, so CancelledError bypasses error marking. + """ + mock_run = _make_mock_run() + MockRunTree.return_value = mock_run + mock_client = MagicMock() + with pytest.raises(asyncio.CancelledError): + with _maybe_run( + mock_client, + "TestRun", + add_temporal_runs=True, + executor=_make_executor(), + ): + raise asyncio.CancelledError() + # run.end should NOT have been called with error= + end_calls = mock_run.end.call_args_list + for c in end_calls: + assert "error" not in (c.kwargs or {}) + + +# =================================================================== +# TestClientOutboundInterceptor +# =================================================================== + + +class TestClientOutboundInterceptor: + """Tests for _LangSmithClientOutboundInterceptor.""" + + def _make_client_interceptor( + self, *, add_temporal_runs: bool = True + ) -> tuple[Any, MagicMock]: + """Create a client outbound interceptor with a mock next.""" + config = LangSmithInterceptor( + client=MagicMock(), add_temporal_runs=add_temporal_runs + ) + mock_next = MagicMock() + mock_next.start_workflow = AsyncMock() + mock_next.query_workflow = AsyncMock() + mock_next.signal_workflow = AsyncMock() + mock_next.start_workflow_update = AsyncMock() + mock_next.start_update_with_start_workflow = AsyncMock() + interceptor = config.intercept_client(mock_next) + return interceptor, mock_next + + @pytest.mark.parametrize( + "method,input_attrs,expected_name", + [ + ( + "start_workflow", + {"workflow": "MyWorkflow", "start_signal": None}, + "StartWorkflow:MyWorkflow", + ), + ( + "start_workflow", + {"workflow": "MyWorkflow", "start_signal": "my_signal"}, + "SignalWithStartWorkflow:MyWorkflow", + ), + ("query_workflow", {"query": "get_status"}, "QueryWorkflow:get_status"), + ("signal_workflow", {"signal": "my_signal"}, "SignalWorkflow:my_signal"), + ( + "start_workflow_update", + {"update": "my_update"}, + "StartWorkflowUpdate:my_update", + ), + ], + ids=["start_workflow", "signal_with_start", "query", "signal", "update"], + ) + @pytest.mark.asyncio + @patch(_PATCH_RUNTREE) + async def test_creates_trace_and_injects_headers( + self, + MockRunTree: Any, + method: str, + input_attrs: dict[str, Any], + expected_name: str, + ) -> None: + """Each client method creates the correct trace and injects headers.""" + mock_run = _make_mock_run() + MockRunTree.return_value = mock_run + interceptor, mock_next = self._make_client_interceptor() + mock_input = MagicMock() + for k, v in input_attrs.items(): + setattr(mock_input, k, v) + mock_input.headers = {} + + with patch(_PATCH_GET_CURRENT_RUN, return_value=mock_run): + await getattr(interceptor, method)(mock_input) + + assert _get_runtree_name(MockRunTree) == expected_name + assert HEADER_KEY in mock_input.headers + getattr(mock_next, method).assert_called_once() + + @pytest.mark.asyncio + @patch(_PATCH_RUNTREE) + async def test_start_update_with_start_workflow(self, MockRunTree: Any) -> None: + """start_update_with_start_workflow injects headers into BOTH start and update inputs.""" + mock_run = _make_mock_run() + MockRunTree.return_value = mock_run + interceptor, mock_next = self._make_client_interceptor() + mock_input = MagicMock() + mock_input.start_workflow_input = MagicMock() + mock_input.start_workflow_input.workflow = "MyWorkflow" + mock_input.start_workflow_input.headers = {} + mock_input.update_workflow_input = MagicMock() + mock_input.update_workflow_input.headers = {} + + with patch(_PATCH_GET_CURRENT_RUN, return_value=mock_run): + await interceptor.start_update_with_start_workflow(mock_input) + + assert ( + _get_runtree_name(MockRunTree) == "StartUpdateWithStartWorkflow:MyWorkflow" + ) + assert HEADER_KEY in mock_input.start_workflow_input.headers + assert HEADER_KEY in mock_input.update_workflow_input.headers + mock_next.start_update_with_start_workflow.assert_called_once() + + @pytest.mark.asyncio + @patch(_PATCH_GET_CURRENT_RUN, return_value=None) + @patch(_PATCH_RUNTREE) + async def test_add_temporal_runs_false_skips_trace( + self, MockRunTree: Any, mock_get_current: Any + ) -> None: + """With add_temporal_runs=False and no ambient context, no run is created + and no headers are injected. + + _inject_current_context() is called unconditionally, but + _get_current_run_for_propagation() returns None so headers are unchanged. + """ + interceptor, mock_next = self._make_client_interceptor(add_temporal_runs=False) + mock_input = MagicMock() + mock_input.workflow = "MyWorkflow" + mock_input.start_signal = None + mock_input.headers = {} + + await interceptor.start_workflow(mock_input) + + # RunTree should NOT be created + MockRunTree.assert_not_called() + # _inject_current_context was called but found no ambient context + mock_get_current.assert_called_once() + # Headers should NOT have been modified (no ambient context) + assert HEADER_KEY not in mock_input.headers + # super() should still be called + mock_next.start_workflow.assert_called_once() + + @pytest.mark.asyncio + @patch(_PATCH_RUNTREE) + async def test_add_temporal_runs_false_with_ambient_context( + self, MockRunTree: Any + ) -> None: + """With add_temporal_runs=False but user-provided ambient context, + no run is created but the ambient context IS injected into headers. + + This verifies that context propagation works even without plugin-created + runs — if the user wraps the call in langsmith.trace(), that context + gets propagated through Temporal headers. + """ + mock_ambient_run = _make_mock_run() + interceptor, mock_next = self._make_client_interceptor(add_temporal_runs=False) + mock_input = MagicMock() + mock_input.workflow = "MyWorkflow" + mock_input.start_signal = None + mock_input.headers = {} + + with patch(_PATCH_GET_CURRENT_RUN, return_value=mock_ambient_run): + await interceptor.start_workflow(mock_input) + + # RunTree should NOT be created (no Temporal run) + MockRunTree.assert_not_called() + # But headers SHOULD be injected from the ambient context + assert HEADER_KEY in mock_input.headers + mock_next.start_workflow.assert_called_once() + + +# =================================================================== +# TestActivityInboundInterceptor +# =================================================================== + + +class TestActivityInboundInterceptor: + """Tests for _LangSmithActivityInboundInterceptor.""" + + def _make_activity_interceptor( + self, *, add_temporal_runs: bool = True + ) -> tuple[Any, MagicMock]: + config = LangSmithInterceptor( + client=MagicMock(), add_temporal_runs=add_temporal_runs + ) + mock_next = MagicMock() + mock_next.execute_activity = AsyncMock(return_value="activity_result") + interceptor = config.intercept_activity(mock_next) + return interceptor, mock_next + + @pytest.mark.asyncio + @patch(_PATCH_TRACING_CTX) + @patch(_PATCH_RUNTREE) + @patch("temporalio.activity.info") + async def test_execute_activity_creates_run_with_context_and_metadata( + self, mock_info_fn: Any, MockRunTree: Any, mock_tracing_ctx: Any + ) -> None: + """Activity execution creates a correctly named run with metadata and parent context.""" + mock_info_fn.return_value = _mock_activity_info( + activity_type="do_thing", + workflow_id="wf-123", + workflow_run_id="run-456", + activity_id="act-789", + ) + mock_run = _make_mock_run() + MockRunTree.return_value = mock_run + interceptor, mock_next = self._make_activity_interceptor() + + mock_input = MagicMock() + mock_input.headers = {} + + result = await interceptor.execute_activity(mock_input) + + # Verify trace name and run_type + assert _get_runtree_name(MockRunTree) == "RunActivity:do_thing" + assert MockRunTree.call_args.kwargs.get("run_type") == "tool" + # Verify metadata + metadata = _get_runtree_metadata(MockRunTree) + assert metadata["temporalWorkflowID"] == "wf-123" + assert metadata["temporalRunID"] == "run-456" + assert metadata["temporalActivityID"] == "act-789" + # Verify tracing_context sets parent (wrapped in _ReplaySafeRunTree) + mock_tracing_ctx.assert_called() + ctx_kwargs = mock_tracing_ctx.call_args.kwargs + parent = ctx_kwargs.get("parent") + assert isinstance(parent, _ReplaySafeRunTree) + assert parent._run is mock_run + # Verify super() called and result passed through + mock_next.execute_activity.assert_called_once() + assert result == "activity_result" + + @pytest.mark.asyncio + @patch(_PATCH_RUNTREE) + @patch("temporalio.activity.info") + async def test_execute_activity_no_header( + self, mock_info_fn: Any, MockRunTree: Any + ) -> None: + """When no LangSmith header is present, activity still executes (no parent, no crash).""" + mock_info_fn.return_value = _mock_activity_info() + mock_run = _make_mock_run() + MockRunTree.return_value = mock_run + interceptor, _mock_next = self._make_activity_interceptor() + + mock_input = MagicMock() + mock_input.headers = {} # No LangSmith header + + result = await interceptor.execute_activity(mock_input) + + # Should still create a run (just without a parent) + MockRunTree.assert_called_once() + assert MockRunTree.call_args.kwargs.get("parent_run") is None + assert result == "activity_result" + + +# =================================================================== +# TestWorkflowInboundInterceptor +# =================================================================== + + +class TestWorkflowInboundInterceptor: + """Tests for _LangSmithWorkflowInboundInterceptor.""" + + def _make_workflow_interceptors( + self, *, add_temporal_runs: bool = True + ) -> tuple[Any, MagicMock]: + """Create workflow inbound interceptor and a mock next.""" + config = LangSmithInterceptor( + client=MagicMock(), add_temporal_runs=add_temporal_runs + ) + mock_next = MagicMock() + mock_next.execute_workflow = AsyncMock(return_value="wf_result") + mock_next.handle_signal = AsyncMock() + mock_next.handle_query = AsyncMock(return_value="query_result") + mock_next.handle_update_validator = MagicMock() + mock_next.handle_update_handler = AsyncMock(return_value="update_result") + + # Get the workflow interceptor class + wf_class_input = MagicMock() + wf_interceptor_cls = config.workflow_interceptor_class(wf_class_input) + assert wf_interceptor_cls is not None + + # Instantiate with mock next + wf_interceptor = wf_interceptor_cls(mock_next) + + # Initialize with mock outbound + mock_outbound = MagicMock() + wf_interceptor.init(mock_outbound) + + return wf_interceptor, mock_next + + @pytest.mark.asyncio + @patch(_PATCH_WF_NOW, return_value=datetime.now(timezone.utc)) + @patch(_PATCH_RUNTREE) + @patch(_PATCH_IS_REPLAYING, return_value=False) + @patch(_PATCH_IN_WORKFLOW, return_value=True) + @patch(_PATCH_WF_INFO) + async def test_execute_workflow( + self, + mock_wf_info: Any, + _mock_in_wf: Any, + _mock_replaying: Any, + MockRunTree: Any, + _mock_now: Any, + ) -> None: + """execute_workflow creates a run named RunWorkflow:{workflow_type}.""" + mock_wf_info.return_value = _mock_workflow_info(workflow_type="MyWorkflow") + mock_run = _make_mock_run() + MockRunTree.return_value = mock_run + interceptor, mock_next = self._make_workflow_interceptors() + + mock_input = MagicMock() + mock_input.headers = {} + + result = await interceptor.execute_workflow(mock_input) + + # Verify trace name + assert _get_runtree_name(MockRunTree) == "RunWorkflow:MyWorkflow" + # Verify metadata includes workflow ID and run ID + metadata = _get_runtree_metadata(MockRunTree) + assert metadata == { + "temporalWorkflowID": "test-wf-id", + "temporalRunID": "test-run-id", + } + # Verify super() called and result passed through + mock_next.execute_workflow.assert_called_once() + assert result == "wf_result" + + @pytest.mark.parametrize( + "method,input_attr,input_val,expected_name", + [ + ("handle_signal", "signal", "my_signal", "HandleSignal:my_signal"), + ("handle_query", "query", "get_status", "HandleQuery:get_status"), + ( + "handle_update_validator", + "update", + "my_update", + "ValidateUpdate:my_update", + ), + ("handle_update_handler", "update", "my_update", "HandleUpdate:my_update"), + ], + ids=["signal", "query", "validator", "update_handler"], + ) + @pytest.mark.asyncio + @patch(_PATCH_WF_NOW, return_value=datetime.now(timezone.utc)) + @patch(_PATCH_RUNTREE) + @patch(_PATCH_IS_REPLAYING, return_value=False) + @patch(_PATCH_IN_WORKFLOW, return_value=True) + @patch(_PATCH_WF_INFO) + async def test_handler_creates_trace( + self, + mock_wf_info: Any, + _mock_in_wf: Any, + _mock_replaying: Any, + MockRunTree: Any, + _mock_now: Any, + method: str, + input_attr: str, + input_val: str, + expected_name: str, + ) -> None: + """Each workflow handler creates the correct trace name.""" + mock_wf_info.return_value = _mock_workflow_info() + mock_run = _make_mock_run() + MockRunTree.return_value = mock_run + interceptor, mock_next = self._make_workflow_interceptors() + + mock_input = MagicMock() + setattr(mock_input, input_attr, input_val) + mock_input.headers = {} + + result = getattr(interceptor, method)(mock_input) + if asyncio.iscoroutine(result): + await result + + assert _get_runtree_name(MockRunTree) == expected_name + getattr(mock_next, method).assert_called_once() + + +# =================================================================== +# TestWorkflowOutboundInterceptor +# =================================================================== + + +class TestWorkflowOutboundInterceptor: + """Tests for _LangSmithWorkflowOutboundInterceptor.""" + + def _make_outbound_interceptor( + self, *, add_temporal_runs: bool = True + ) -> tuple[Any, MagicMock, Any]: + """Create outbound interceptor with mock next and ambient run. + + Returns (outbound_interceptor, mock_next, mock_current_run). + """ + config = LangSmithInterceptor( + client=MagicMock(), add_temporal_runs=add_temporal_runs + ) + + # Create mock next for inbound + mock_inbound_next = MagicMock() + mock_inbound_next.execute_workflow = AsyncMock() + mock_inbound_next.handle_signal = AsyncMock() + mock_inbound_next.handle_query = AsyncMock() + mock_inbound_next.handle_update_validator = MagicMock() + mock_inbound_next.handle_update_handler = AsyncMock() + + # Create inbound interceptor + wf_class_input = MagicMock() + wf_interceptor_cls = config.workflow_interceptor_class(wf_class_input) + inbound = wf_interceptor_cls(mock_inbound_next) + + # Create mock outbound next + mock_outbound_next = MagicMock() + mock_outbound_next.start_activity = MagicMock() + mock_outbound_next.start_local_activity = MagicMock() + mock_outbound_next.start_child_workflow = AsyncMock() + mock_outbound_next.signal_child_workflow = AsyncMock() + mock_outbound_next.signal_external_workflow = AsyncMock() + mock_outbound_next.continue_as_new = MagicMock() + mock_outbound_next.start_nexus_operation = AsyncMock() + + # Initialize inbound (which should create the outbound) + inbound.init(mock_outbound_next) + + # Create the outbound directly for unit testing + from temporalio.contrib.langsmith._interceptor import ( + _LangSmithWorkflowOutboundInterceptor, + ) + + outbound = _LangSmithWorkflowOutboundInterceptor(mock_outbound_next, config) + + # Simulate active workflow execution via ambient context + mock_current_run = _make_mock_run() + + return outbound, mock_outbound_next, mock_current_run + + @pytest.mark.parametrize( + "method,input_attr,input_val,expected_name", + [ + ("start_activity", "activity", "do_thing", "StartActivity:do_thing"), + ( + "start_local_activity", + "activity", + "local_thing", + "StartActivity:local_thing", + ), + ( + "start_child_workflow", + "workflow", + "ChildWorkflow", + "StartChildWorkflow:ChildWorkflow", + ), + ( + "signal_child_workflow", + "signal", + "child_signal", + "SignalChildWorkflow:child_signal", + ), + ( + "signal_external_workflow", + "signal", + "ext_signal", + "SignalExternalWorkflow:ext_signal", + ), + ], + ids=[ + "activity", + "local_activity", + "child_workflow", + "signal_child", + "signal_external", + ], + ) + @pytest.mark.asyncio + @patch(_PATCH_WF_NOW, return_value=datetime.now(timezone.utc)) + @patch(_PATCH_RUNTREE) + @patch(_PATCH_IS_REPLAYING, return_value=False) + @patch(_PATCH_IN_WORKFLOW, return_value=True) + async def test_creates_trace_and_injects_headers( + self, + _mock_in_wf: Any, + _mock_replaying: Any, + MockRunTree: Any, + _mock_now: Any, + method: str, + input_attr: str, + input_val: str, + expected_name: str, + ) -> None: + """Each outbound method creates the correct trace and injects headers.""" + mock_run = _make_mock_run() + MockRunTree.return_value = mock_run + outbound, mock_next, mock_current_run = self._make_outbound_interceptor() + + mock_input = MagicMock() + setattr(mock_input, input_attr, input_val) + mock_input.headers = {} + + with patch(_PATCH_GET_CURRENT_RUN, return_value=mock_current_run): + result = getattr(outbound, method)(mock_input) + if asyncio.iscoroutine(result): + await result + + assert _get_runtree_name(MockRunTree) == expected_name + assert HEADER_KEY in mock_input.headers + getattr(mock_next, method).assert_called_once() + + @patch(_PATCH_RUNTREE) + @patch(_PATCH_IS_REPLAYING, return_value=False) + @patch(_PATCH_IN_WORKFLOW, return_value=True) + def test_continue_as_new( + self, _mock_in_wf: Any, _mock_replaying: Any, MockRunTree: Any + ) -> None: + """continue_as_new does NOT create a new trace, but injects context from ambient run.""" + outbound, mock_next, mock_current_run = self._make_outbound_interceptor() + + mock_input = MagicMock() + mock_input.headers = {} + + with patch(_PATCH_GET_CURRENT_RUN, return_value=mock_current_run): + outbound.continue_as_new(mock_input) + + # No new RunTree should be created for continue_as_new + MockRunTree.assert_not_called() + # But headers SHOULD be modified (context from ambient run) + assert HEADER_KEY in mock_input.headers + mock_next.continue_as_new.assert_called_once() + + @pytest.mark.asyncio + @patch(_PATCH_WF_NOW, return_value=datetime.now(timezone.utc)) + @patch(_PATCH_RUNTREE) + @patch(_PATCH_IS_REPLAYING, return_value=False) + @patch(_PATCH_IN_WORKFLOW, return_value=True) + async def test_start_nexus_operation( + self, + _mock_in_wf: Any, + _mock_replaying: Any, + MockRunTree: Any, + _mock_now: Any, + ) -> None: + """start_nexus_operation creates a trace named StartNexusOperation:{service}/{operation}.""" + mock_run = _make_mock_run() + MockRunTree.return_value = mock_run + outbound, mock_next, mock_current_run = self._make_outbound_interceptor() + + mock_input = MagicMock() + mock_input.service = "MyService" + mock_input.operation_name = "do_op" + mock_input.headers = {} + + with patch(_PATCH_GET_CURRENT_RUN, return_value=mock_current_run): + await outbound.start_nexus_operation(mock_input) + + assert _get_runtree_name(MockRunTree) == "StartNexusOperation:MyService/do_op" + # Nexus uses string headers, so context injection uses _inject_nexus_context + # The headers dict should be modified + mock_next.start_nexus_operation.assert_called_once() + + +# =================================================================== +# TestNexusInboundInterceptor +# =================================================================== + + +class TestNexusInboundInterceptor: + """Tests for _LangSmithNexusOperationInboundInterceptor.""" + + def _make_nexus_interceptor( + self, *, add_temporal_runs: bool = True + ) -> tuple[Any, MagicMock]: + config = LangSmithInterceptor( + client=MagicMock(), add_temporal_runs=add_temporal_runs + ) + mock_next = MagicMock() + mock_next.execute_nexus_operation_start = AsyncMock() + mock_next.execute_nexus_operation_cancel = AsyncMock() + interceptor = config.intercept_nexus_operation(mock_next) + return interceptor, mock_next + + @pytest.mark.asyncio + @patch(_PATCH_RUNTREE) + @patch(_PATCH_EXTRACT_NEXUS) + async def test_execute_nexus_operation_start( + self, mock_extract_nexus: Any, MockRunTree: Any + ) -> None: + """Creates a run named RunStartNexusOperationHandler:{service}/{operation}. + + Uses _extract_nexus_context (not _extract_context) for Nexus string headers. + """ + mock_extract_nexus.return_value = None # no parent + mock_run = _make_mock_run() + MockRunTree.return_value = mock_run + interceptor, mock_next = self._make_nexus_interceptor() + + mock_input = MagicMock() + mock_input.ctx = MagicMock() + mock_input.ctx.service = "MyService" + mock_input.ctx.operation = "start_op" + mock_input.ctx.headers = {} + + await interceptor.execute_nexus_operation_start(mock_input) + + # Verify _extract_nexus_context was called (not _extract_context) + mock_extract_nexus.assert_called_once() + assert mock_extract_nexus.call_args[0][0] is mock_input.ctx.headers + # Verify trace name + assert ( + _get_runtree_name(MockRunTree) + == "RunStartNexusOperationHandler:MyService/start_op" + ) + # Verify run_type is "tool" for Nexus operations + assert MockRunTree.call_args.kwargs.get("run_type") == "tool" + mock_next.execute_nexus_operation_start.assert_called_once() + + @pytest.mark.asyncio + @patch(_PATCH_RUNTREE) + @patch(_PATCH_EXTRACT_NEXUS) + async def test_execute_nexus_operation_cancel( + self, mock_extract_nexus: Any, MockRunTree: Any + ) -> None: + """Creates a run named RunCancelNexusOperationHandler:{service}/{operation}. + + Uses _extract_nexus_context for context extraction. + """ + mock_extract_nexus.return_value = None + mock_run = _make_mock_run() + MockRunTree.return_value = mock_run + interceptor, mock_next = self._make_nexus_interceptor() + + mock_input = MagicMock() + mock_input.ctx = MagicMock() + mock_input.ctx.service = "MyService" + mock_input.ctx.operation = "cancel_op" + mock_input.ctx.headers = {} + + await interceptor.execute_nexus_operation_cancel(mock_input) + + mock_extract_nexus.assert_called_once() + assert mock_extract_nexus.call_args[0][0] is mock_input.ctx.headers + assert ( + _get_runtree_name(MockRunTree) + == "RunCancelNexusOperationHandler:MyService/cancel_op" + ) + assert MockRunTree.call_args.kwargs.get("run_type") == "tool" + mock_next.execute_nexus_operation_cancel.assert_called_once() + + +# =================================================================== +# TestLazyClientPrevention +# =================================================================== + + +class TestLazyClientPrevention: + """Tests that RunTree always receives ls_client= to prevent lazy Client creation.""" + + @patch(_PATCH_IN_WORKFLOW, return_value=False) + @patch(_PATCH_RUNTREE) + def test_runtree_always_receives_ls_client( + self, MockRunTree: Any, _mock_in_wf: Any + ) -> None: + """Every RunTree() created by _maybe_run receives ls_client= (pre-created client).""" + mock_client = MagicMock() + mock_run = _make_mock_run() + MockRunTree.return_value = mock_run + + with _maybe_run( + mock_client, + "TestRun", + add_temporal_runs=True, + executor=_make_executor(), + ): + pass + + MockRunTree.assert_called_once() + call_kwargs = MockRunTree.call_args.kwargs + assert "ls_client" in call_kwargs + assert call_kwargs["ls_client"] is mock_client + + +# =================================================================== +# TestAddTemporalRunsToggle +# =================================================================== + + +class TestAddTemporalRunsToggle: + """Tests for the add_temporal_runs toggle.""" + + @patch(_PATCH_RUNTREE) + @patch(_PATCH_IN_WORKFLOW, return_value=False) + def test_false_skips_traces(self, _mock_in_wf: Any, MockRunTree: Any) -> None: + """With add_temporal_runs=False, _maybe_run yields None (no run created). + + Callers are responsible for propagating context even when the run is None. + See test_false_still_propagates_context for the full behavior. + """ + mock_client = MagicMock() + with _maybe_run( + mock_client, + "TestRun", + add_temporal_runs=False, + executor=_make_executor(), + ) as run: + assert run is None + MockRunTree.assert_not_called() + + @pytest.mark.asyncio + @patch(_PATCH_TRACING_CTX) + @patch(_PATCH_RUNTREE) + @patch(_PATCH_IS_REPLAYING, return_value=False) + @patch(_PATCH_IN_WORKFLOW, return_value=True) + @patch(_PATCH_WF_INFO) + @patch(f"{_MOD}.temporalio.activity.info") + async def test_false_still_propagates_context( + self, + mock_act_info: Any, + mock_wf_info: Any, + _mock_in_wf: Any, + _mock_replaying: Any, + MockRunTree: Any, + mock_tracing_ctx: Any, + ) -> None: + """With add_temporal_runs=False, no runs are created but context still propagates. + + 1. Workflow outbound: injects the ambient run's context into headers even + though no StartActivity run is created. + 2. Activity inbound: sets tracing_context(parent=extracted_parent) + unconditionally (before _maybe_run), so @traceable code nests correctly + even without a RunActivity run. + """ + from temporalio.contrib.langsmith._interceptor import ( + _LangSmithWorkflowOutboundInterceptor, + ) + + mock_wf_info.return_value = _mock_workflow_info() + mock_act_info.return_value = _mock_activity_info() + + # --- Workflow outbound: context propagation without run creation --- + config = LangSmithInterceptor(client=MagicMock(), add_temporal_runs=False) + + # Create inbound interceptor + wf_class_input = MagicMock() + wf_interceptor_cls = config.workflow_interceptor_class(wf_class_input) + mock_inbound_next = MagicMock() + mock_inbound_next.execute_workflow = AsyncMock() + inbound = wf_interceptor_cls(mock_inbound_next) + + # Create outbound interceptor + mock_outbound_next = MagicMock() + mock_outbound_next.start_activity = MagicMock() + inbound.init(mock_outbound_next) + outbound = _LangSmithWorkflowOutboundInterceptor(mock_outbound_next, config) + + # Simulate an ambient parent context (as if from active workflow execution) + mock_parent = _make_mock_run() + + mock_input = MagicMock() + mock_input.activity = "do_thing" + mock_input.headers = {} + + with patch(_PATCH_GET_CURRENT_RUN, return_value=mock_parent): + outbound.start_activity(mock_input) + + # No RunTree should be created (add_temporal_runs=False) + MockRunTree.assert_not_called() + # But headers SHOULD be injected from the inbound's parent context + assert HEADER_KEY in mock_input.headers + mock_outbound_next.start_activity.assert_called_once() + + # --- Activity inbound: tracing_context with extracted parent --- + MockRunTree.reset_mock() + mock_tracing_ctx.reset_mock() + + mock_act_next = MagicMock() + mock_act_next.execute_activity = AsyncMock(return_value="result") + act_interceptor = config.intercept_activity(mock_act_next) + + mock_act_input = MagicMock() + mock_extracted_parent = _make_mock_run() + + with patch(f"{_MOD}._extract_context", return_value=mock_extracted_parent): + await act_interceptor.execute_activity(mock_act_input) + + # No RunTree should be created (add_temporal_runs=False) + MockRunTree.assert_not_called() + # tracing_context SHOULD be called with the client and extracted parent + # (unconditionally, before _maybe_run) + mock_tracing_ctx.assert_called_once_with( + client=config._client, + project_name=None, + parent=mock_extracted_parent, + ) + mock_act_next.execute_activity.assert_called_once() + + @pytest.mark.asyncio + @patch(_PATCH_TRACING_CTX) + @patch(_PATCH_RUNTREE) + @patch(f"{_MOD}.temporalio.activity.info") + async def test_false_activity_no_parent_no_context( + self, + mock_act_info: Any, + MockRunTree: Any, + mock_tracing_ctx: Any, + ) -> None: + """With add_temporal_runs=False and no parent in headers, tracing_context + is still called with the client (so @traceable can use it), but no parent. + """ + mock_act_info.return_value = _mock_activity_info() + config = LangSmithInterceptor(client=MagicMock(), add_temporal_runs=False) + + mock_act_next = MagicMock() + mock_act_next.execute_activity = AsyncMock(return_value="result") + act_interceptor = config.intercept_activity(mock_act_next) + + mock_act_input = MagicMock() + + with patch(f"{_MOD}._extract_context", return_value=None): + await act_interceptor.execute_activity(mock_act_input) + + MockRunTree.assert_not_called() + # tracing_context called with client (no parent) + mock_tracing_ctx.assert_called_once_with( + client=config._client, project_name=None, parent=None + ) + mock_act_next.execute_activity.assert_called_once() diff --git a/tests/contrib/langsmith/test_plugin.py b/tests/contrib/langsmith/test_plugin.py new file mode 100644 index 000000000..6e3cb2e86 --- /dev/null +++ b/tests/contrib/langsmith/test_plugin.py @@ -0,0 +1,245 @@ +"""Tests for LangSmithPlugin construction, configuration, and end-to-end usage.""" + +from __future__ import annotations + +import uuid +from typing import Any +from unittest.mock import MagicMock + +import pytest +from langsmith import traceable, tracing_context + +from temporalio.client import Client, WorkflowHandle +from temporalio.contrib.langsmith import LangSmithInterceptor, LangSmithPlugin +from temporalio.testing import WorkflowEnvironment +from tests.contrib.langsmith.conftest import ( + build_trace_trees, + find_trace_trees, +) +from tests.contrib.langsmith.test_integration import ( + ComprehensiveWorkflow, + NexusService, + TraceableActivityWorkflow, + _make_client_and_collector, + _poll_query, + nested_traceable_activity, + traceable_activity, +) +from tests.helpers import new_worker +from tests.helpers.nexus import make_nexus_endpoint_name +from tests.helpers.trace import assert_trace_hierarchy + + +class TestPluginConstruction: + """Tests for LangSmithPlugin construction.""" + + def test_construction_stores_all_config(self) -> None: + """All constructor kwargs are stored on the interceptor.""" + mock_client = MagicMock() + plugin = LangSmithPlugin( + client=mock_client, + project_name="my-project", + add_temporal_runs=False, + default_metadata={"env": "prod"}, + default_tags=["v1"], + ) + assert plugin.interceptors is not None + assert len(plugin.interceptors) > 0 + interceptor = plugin.interceptors[0] + assert isinstance(interceptor, LangSmithInterceptor) + assert interceptor._client is mock_client + assert interceptor._project_name == "my-project" + assert interceptor._add_temporal_runs is False + assert interceptor._default_metadata == {"env": "prod"} + assert interceptor._default_tags == ["v1"] + + +class TestPluginIntegration: + """End-to-end test using LangSmithPlugin as a Temporal client plugin.""" + + @pytest.mark.requires_local_server + async def test_comprehensive_plugin_trace_hierarchy( + self, client: Client, env: WorkflowEnvironment + ) -> None: + """Plugin wired to a real Temporal worker produces the full trace hierarchy. + + user_pipeline only wraps start_workflow, so poll/query/signal/update + traces are naturally separate root traces. + """ + if env.supports_time_skipping: + pytest.skip("Time-skipping server doesn't persist headers.") + + temporal_client, collector, mock_ls_client = _make_client_and_collector( + client, add_temporal_runs=True + ) + + task_queue = f"plugin-comprehensive-{uuid.uuid4()}" + workflow_id = f"plugin-comprehensive-{uuid.uuid4()}" + + @traceable(name="user_pipeline") + async def user_pipeline() -> WorkflowHandle[Any, Any]: + return await temporal_client.start_workflow( + ComprehensiveWorkflow.run, + id=workflow_id, + task_queue=task_queue, + ) + + with tracing_context(client=mock_ls_client, enabled=True): + handle = await user_pipeline() + + async with new_worker( + temporal_client, + ComprehensiveWorkflow, + TraceableActivityWorkflow, + activities=[nested_traceable_activity, traceable_activity], + nexus_service_handlers=[NexusService()], + task_queue=task_queue, + max_cached_workflows=0, + ) as worker: + await env.create_nexus_endpoint( + make_nexus_endpoint_name(worker.task_queue), + worker.task_queue, + ) + assert await _poll_query( + handle, + ComprehensiveWorkflow.is_waiting_for_signal, + expected=True, + ), "Workflow never reached signal wait point" + await handle.query(ComprehensiveWorkflow.my_query) + await handle.signal(ComprehensiveWorkflow.my_signal, "hello") + await handle.execute_update( + ComprehensiveWorkflow.my_unvalidated_update, "test" + ) + await handle.execute_update(ComprehensiveWorkflow.my_update, "finish") + result = await handle.result() + + assert result == "comprehensive-done" + + trace_trees = build_trace_trees(collector) + + # user_pipeline trace: StartWorkflow + full workflow execution tree + workflow_trace_trees = find_trace_trees(trace_trees, "user_pipeline") + assert len(workflow_trace_trees) == 1 + expected_workflow = [ + "user_pipeline", + " StartWorkflow:ComprehensiveWorkflow", + " RunWorkflow:ComprehensiveWorkflow", + " StartActivity:nested_traceable_activity", + " RunActivity:nested_traceable_activity", + " nested_traceable_activity", + " outer_chain", + " inner_llm_call", + # step-wrapped activity + " step_with_activity", + " StartActivity:nested_traceable_activity", + " RunActivity:nested_traceable_activity", + " nested_traceable_activity", + " outer_chain", + " inner_llm_call", + " StartActivity:nested_traceable_activity", + " RunActivity:nested_traceable_activity", + " nested_traceable_activity", + " outer_chain", + " inner_llm_call", + " outer_chain", + " inner_llm_call", + " StartChildWorkflow:TraceableActivityWorkflow", + " RunWorkflow:TraceableActivityWorkflow", + " StartActivity:traceable_activity", + " RunActivity:traceable_activity", + " traceable_activity", + " inner_llm_call", + # step-wrapped child workflow + " step_with_child_workflow", + " StartChildWorkflow:TraceableActivityWorkflow", + " RunWorkflow:TraceableActivityWorkflow", + " StartActivity:traceable_activity", + " RunActivity:traceable_activity", + " traceable_activity", + " inner_llm_call", + " StartNexusOperation:NexusService/run_operation", + " RunStartNexusOperationHandler:NexusService/run_operation", + " StartWorkflow:TraceableActivityWorkflow", + " RunWorkflow:TraceableActivityWorkflow", + " StartActivity:traceable_activity", + " RunActivity:traceable_activity", + " traceable_activity", + " inner_llm_call", + # step-wrapped nexus operation + " step_with_nexus", + " StartNexusOperation:NexusService/run_operation", + " RunStartNexusOperationHandler:NexusService/run_operation", + " StartWorkflow:TraceableActivityWorkflow", + " RunWorkflow:TraceableActivityWorkflow", + " StartActivity:traceable_activity", + " RunActivity:traceable_activity", + " traceable_activity", + " inner_llm_call", + # post-signal + " StartActivity:nested_traceable_activity", + " RunActivity:nested_traceable_activity", + " nested_traceable_activity", + " outer_chain", + " inner_llm_call", + ] + assert_trace_hierarchy(workflow_trace_trees, expected_workflow) + + # poll_query trace (separate root, variable number of iterations) + poll_trace_trees = find_trace_trees(trace_trees, "poll_query") + assert len(poll_trace_trees) == 1 + poll = poll_trace_trees[0] + assert poll.name == "poll_query" + poll_children = poll.children + for poll_child in poll_children: + assert poll_child.name == "QueryWorkflow:is_waiting_for_signal" + assert [child.name for child in poll_child.children] == [ + "HandleQuery:is_waiting_for_signal" + ] + assert not poll_child.children[0].children + + # Each remaining operation is its own root trace + query_trace_trees = find_trace_trees(trace_trees, "QueryWorkflow:my_query") + assert len(query_trace_trees) == 1 + assert_trace_hierarchy( + query_trace_trees, + [ + "QueryWorkflow:my_query", + " HandleQuery:my_query", + ], + ) + + signal_trace_trees = find_trace_trees(trace_trees, "SignalWorkflow:my_signal") + assert len(signal_trace_trees) == 1 + assert_trace_hierarchy( + signal_trace_trees, + [ + "SignalWorkflow:my_signal", + " HandleSignal:my_signal", + ], + ) + + update_trace_trees = find_trace_trees( + trace_trees, "StartWorkflowUpdate:my_update" + ) + assert len(update_trace_trees) == 1 + assert_trace_hierarchy( + update_trace_trees, + [ + "StartWorkflowUpdate:my_update", + " ValidateUpdate:my_update", + " HandleUpdate:my_update", + ], + ) + + # Update without a validator — no ValidateUpdate trace + unvalidated_trace_trees = find_trace_trees( + trace_trees, "StartWorkflowUpdate:my_unvalidated_update" + ) + assert len(unvalidated_trace_trees) == 1 + assert_trace_hierarchy( + unvalidated_trace_trees, + [ + "StartWorkflowUpdate:my_unvalidated_update", + " HandleUpdate:my_unvalidated_update", + ], + ) diff --git a/tests/contrib/langsmith/test_tracing_env_override.py b/tests/contrib/langsmith/test_tracing_env_override.py new file mode 100644 index 000000000..c8aaa2fb0 --- /dev/null +++ b/tests/contrib/langsmith/test_tracing_env_override.py @@ -0,0 +1,244 @@ +"""Tests that LangSmithPlugin defers to ``langsmith.utils.tracing_is_enabled()``. + +Tracing requires the env to explicitly say so (``LANGSMITH_TRACING=true`` etc); +it is off when the env is unset or set to ``false``. Tests verify that +``LANGSMITH_TRACING=false`` produces zero runs and ``LANGSMITH_TRACING=true`` +produces runs. +""" + +from __future__ import annotations + +import uuid +from datetime import timedelta + +import pytest +from langsmith import traceable + +from temporalio import activity, workflow +from temporalio.client import Client +from temporalio.testing import WorkflowEnvironment +from tests.contrib.langsmith.test_integration import ( + DirectTraceableNexusService, + NexusDirectTraceableWorkflow, + _make_client_and_collector, +) +from tests.helpers import new_worker +from tests.helpers.nexus import make_nexus_endpoint_name + +# --------------------------------------------------------------------------- +# Sample workflow / activity +# --------------------------------------------------------------------------- + + +@traceable(name="inner_call") +async def _inner_call(prompt: str) -> str: + return f"response to: {prompt}" + + +@traceable +@activity.defn +async def env_override_activity() -> str: + result = await _inner_call("hello") + return result + + +@workflow.defn +class EnvOverrideWorkflow: + @workflow.run + async def run(self) -> str: + return await workflow.execute_activity( + env_override_activity, + start_to_close_timeout=timedelta(seconds=10), + ) + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + + +class TestTracingEnvOverride: + """LangSmithPlugin must respect LANGSMITH_TRACING=false.""" + + async def test_no_runs_when_tracing_disabled_with_temporal_runs( + self, + client: Client, + env: WorkflowEnvironment, # type:ignore[reportUnusedParameter] + monkeypatch: pytest.MonkeyPatch, + ) -> None: + """With LANGSMITH_TRACING=false and add_temporal_runs=True, no runs.""" + monkeypatch.setenv("LANGSMITH_TRACING", "false") + monkeypatch.delenv("LANGCHAIN_TRACING_V2", raising=False) + + temporal_client, collector, _ = _make_client_and_collector( + client, add_temporal_runs=True + ) + + async with new_worker( + temporal_client, + EnvOverrideWorkflow, + activities=[env_override_activity], + max_cached_workflows=0, + ) as worker: + handle = await temporal_client.start_workflow( + EnvOverrideWorkflow.run, + id=f"env-override-temporal-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + result = await handle.result() + + assert result == "response to: hello" + assert len(collector.runs) == 0, ( + f"Expected zero LangSmith runs when LANGSMITH_TRACING=false, " + f"but got {len(collector.runs)}: " + f"{[r.name for r in collector.runs]}" + ) + + async def test_no_runs_when_tracing_disabled_without_temporal_runs( + self, + client: Client, + env: WorkflowEnvironment, # type:ignore[reportUnusedParameter] + monkeypatch: pytest.MonkeyPatch, + ) -> None: + """With LANGSMITH_TRACING=false and add_temporal_runs=False, no runs.""" + monkeypatch.setenv("LANGSMITH_TRACING", "false") + monkeypatch.delenv("LANGCHAIN_TRACING_V2", raising=False) + + temporal_client, collector, _ = _make_client_and_collector( + client, add_temporal_runs=False + ) + + async with new_worker( + temporal_client, + EnvOverrideWorkflow, + activities=[env_override_activity], + max_cached_workflows=0, + ) as worker: + handle = await temporal_client.start_workflow( + EnvOverrideWorkflow.run, + id=f"env-override-no-temporal-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + result = await handle.result() + + assert result == "response to: hello" + assert len(collector.runs) == 0, ( + f"Expected zero LangSmith runs when LANGSMITH_TRACING=false, " + f"but got {len(collector.runs)}: " + f"{[r.name for r in collector.runs]}" + ) + + async def test_no_runs_when_langchain_tracing_v2_disabled( + self, + client: Client, + env: WorkflowEnvironment, # type:ignore[reportUnusedParameter] + monkeypatch: pytest.MonkeyPatch, + ) -> None: + """LANGCHAIN_TRACING_V2=false also suppresses runs (legacy env var).""" + monkeypatch.setenv("LANGCHAIN_TRACING_V2", "false") + monkeypatch.delenv("LANGSMITH_TRACING", raising=False) + + temporal_client, collector, _ = _make_client_and_collector( + client, add_temporal_runs=True + ) + + async with new_worker( + temporal_client, + EnvOverrideWorkflow, + activities=[env_override_activity], + max_cached_workflows=0, + ) as worker: + handle = await temporal_client.start_workflow( + EnvOverrideWorkflow.run, + id=f"env-override-v2-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + result = await handle.result() + + assert result == "response to: hello" + assert len(collector.runs) == 0, ( + f"Expected zero LangSmith runs when LANGCHAIN_TRACING_V2=false, " + f"but got {len(collector.runs)}: " + f"{[r.name for r in collector.runs]}" + ) + + @pytest.mark.requires_local_server + async def test_no_runs_when_tracing_disabled_for_nexus_start( + self, + client: Client, + env: WorkflowEnvironment, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + """With LANGSMITH_TRACING=false, nexus start handler emits no runs.""" + if env.supports_time_skipping: + pytest.skip("Time-skipping server doesn't persist headers.") + + monkeypatch.setenv("LANGSMITH_TRACING", "false") + monkeypatch.delenv("LANGCHAIN_TRACING_V2", raising=False) + + temporal_client, collector, _ = _make_client_and_collector( + client, add_temporal_runs=True + ) + + task_queue = f"env-override-nexus-{uuid.uuid4()}" + async with new_worker( + temporal_client, + NexusDirectTraceableWorkflow, + nexus_service_handlers=[DirectTraceableNexusService()], + task_queue=task_queue, + max_cached_workflows=0, + ) as worker: + await env.create_nexus_endpoint( + make_nexus_endpoint_name(worker.task_queue), + worker.task_queue, + ) + handle = await temporal_client.start_workflow( + NexusDirectTraceableWorkflow.run, + id=f"env-override-nexus-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + result = await handle.result() + + assert result == "response to: nexus-input" + assert len(collector.runs) == 0, ( + f"Expected zero LangSmith runs when LANGSMITH_TRACING=false " + f"(nexus start path), but got {len(collector.runs)}: " + f"{[r.name for r in collector.runs]}" + ) + + # NOTE: test_no_runs_when_tracing_disabled_for_nexus_cancel is not + # included — cancelling an in-flight nexus operation requires non-trivial + # orchestration (long-running handler + external cancel signal). Flagged + # for a follow-up ticket. + + async def test_runs_emitted_when_tracing_enabled( + self, + client: Client, + env: WorkflowEnvironment, # type:ignore[reportUnusedParameter] + monkeypatch: pytest.MonkeyPatch, + ) -> None: + """Positive control: with LANGSMITH_TRACING=true, runs ARE emitted.""" + monkeypatch.setenv("LANGSMITH_TRACING", "true") + monkeypatch.delenv("LANGCHAIN_TRACING_V2", raising=False) + + temporal_client, collector, _ = _make_client_and_collector( + client, add_temporal_runs=True + ) + + async with new_worker( + temporal_client, + EnvOverrideWorkflow, + activities=[env_override_activity], + max_cached_workflows=0, + ) as worker: + handle = await temporal_client.start_workflow( + EnvOverrideWorkflow.run, + id=f"env-enabled-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + result = await handle.result() + + assert result == "response to: hello" + assert len(collector.runs) > 0, ( + "Expected LangSmith runs when LANGSMITH_TRACING=true, but got none" + ) diff --git a/tests/contrib/openai_agents/research_agents/research_manager.py b/tests/contrib/openai_agents/research_agents/research_manager.py index de721f9b9..98ab550f5 100644 --- a/tests/contrib/openai_agents/research_agents/research_manager.py +++ b/tests/contrib/openai_agents/research_agents/research_manager.py @@ -2,8 +2,9 @@ import asyncio -from agents import Runner, custom_span, gen_trace_id, trace +from agents import Runner, custom_span +import temporalio.workflow from tests.contrib.openai_agents.research_agents.planner_agent import ( WebSearchItem, WebSearchPlan, @@ -23,8 +24,7 @@ def __init__(self): self.writer_agent = new_writer_agent() async def run(self, query: str) -> str: - trace_id = gen_trace_id() - with trace("Research trace", trace_id=trace_id): + with custom_span("Research manager"): search_plan = await self._plan_searches(query) search_results = await self._perform_searches(search_plan) report = await self._write_report(query, search_results) @@ -45,7 +45,7 @@ async def _perform_searches(self, search_plan: WebSearchPlan) -> list[str]: asyncio.create_task(self._search(item)) for item in search_plan.searches ] results = [] - for task in asyncio.as_completed(tasks): + for task in temporalio.workflow.as_completed(tasks): result = await task if result is not None: results.append(result) diff --git a/tests/contrib/openai_agents/test_openai.py b/tests/contrib/openai_agents/test_openai.py index 741c33353..25597ee55 100644 --- a/tests/contrib/openai_agents/test_openai.py +++ b/tests/contrib/openai_agents/test_openai.py @@ -3,15 +3,20 @@ import os import sys import uuid +from collections.abc import AsyncIterator, Callable, Sequence from dataclasses import dataclass from datetime import timedelta -from typing import Any, AsyncIterator, Optional, Union, no_type_check +from typing import ( + Any, + cast, +) import nexusrpc import pydantic import pytest from agents import ( Agent, + AgentBase, AgentOutputSchemaBase, CodeInterpreterTool, FileSearchTool, @@ -21,7 +26,6 @@ ImageGenerationTool, InputGuardrailTripwireTriggered, ItemHelpers, - LocalShellTool, MCPToolApprovalFunctionResult, MCPToolApprovalRequest, MessageOutputItem, @@ -51,24 +55,21 @@ HandoffOutputItem, ToolCallItem, ToolCallOutputItem, - TResponseOutputItem, TResponseStreamEvent, ) +from agents.mcp import MCPServer, MCPServerStdio +from agents.sandbox.capabilities.tools import SandboxApplyPatchTool +from agents.tool import CustomTool +from agents.tool_context import ToolContext from openai import APIStatusError, AsyncOpenAI, BaseModel from openai.types.responses import ( - EasyInputMessageParam, ResponseCodeInterpreterToolCall, + ResponseCustomToolCall, ResponseFileSearchToolCall, - ResponseFunctionToolCall, - ResponseFunctionToolCallParam, ResponseFunctionWebSearch, - ResponseInputTextParam, - ResponseOutputMessage, - ResponseOutputText, ) from openai.types.responses.response_file_search_tool_call import Result from openai.types.responses.response_function_web_search import ActionSearch -from openai.types.responses.response_input_item_param import Message from openai.types.responses.response_output_item import ( ImageGenerationCall, McpApprovalRequest, @@ -77,86 +78,43 @@ from openai.types.responses.response_prompt_param import ResponsePromptParam from pydantic import ConfigDict, Field, TypeAdapter -import temporalio.api.cloud.namespace.v1 from temporalio import activity, workflow from temporalio.client import Client, WorkflowFailureError, WorkflowHandle -from temporalio.common import RetryPolicy, SearchAttributeValueType +from temporalio.common import RetryPolicy from temporalio.contrib import openai_agents from temporalio.contrib.openai_agents import ( ModelActivityParameters, + StatefulMCPServerProvider, + StatelessMCPServerProvider, +) +from temporalio.contrib.openai_agents._invoke_model_activity import _build_tool +from temporalio.contrib.openai_agents._model_parameters import ModelSummaryProvider +from temporalio.contrib.openai_agents._openai_runner import ( + _coerce_run_config, + _convert_agent, +) +from temporalio.contrib.openai_agents._temporal_model_stub import ( + _TemporalModelStub, +) +from temporalio.contrib.openai_agents.testing import ( + AgentEnvironment, + ResponseBuilders, TestModel, TestModelProvider, ) -from temporalio.contrib.openai_agents._model_parameters import ModelSummaryProvider -from temporalio.contrib.openai_agents._temporal_model_stub import _extract_summary from temporalio.contrib.pydantic import pydantic_data_converter -from temporalio.exceptions import ApplicationError, CancelledError +from temporalio.exceptions import ApplicationError, CancelledError, TemporalError from temporalio.testing import WorkflowEnvironment +from temporalio.workflow import ActivityConfig from tests.contrib.openai_agents.research_agents.research_manager import ( ResearchManager, ) -from tests.helpers import assert_task_fail_eventually, new_worker -from tests.helpers.nexus import create_nexus_endpoint, make_nexus_endpoint_name - - -class StaticTestModel(TestModel): - __test__ = False - responses: list[ModelResponse] = [] - - def __init__( - self, - ) -> None: - self._responses = iter(self.responses) - super().__init__(lambda: next(self._responses)) - - -class ResponseBuilders: - @staticmethod - def model_response(output: TResponseOutputItem) -> ModelResponse: - return ModelResponse( - output=[output], - usage=Usage(), - response_id=None, - ) - - @staticmethod - def response_output_message(text: str) -> ResponseOutputMessage: - return ResponseOutputMessage( - id="", - content=[ - ResponseOutputText( - text=text, - annotations=[], - type="output_text", - ) - ], - role="assistant", - status="completed", - type="message", - ) - - @staticmethod - def tool_call(arguments: str, name: str) -> ModelResponse: - return ResponseBuilders.model_response( - ResponseFunctionToolCall( - arguments=arguments, - call_id="call", - name=name, - type="function_call", - id="id", - status="completed", - ) - ) - - @staticmethod - def output_message(text: str) -> ModelResponse: - return ResponseBuilders.model_response( - ResponseBuilders.response_output_message(text) - ) +from tests.helpers import assert_eventually, new_worker +from tests.helpers.nexus import make_nexus_endpoint_name -class TestHelloModel(StaticTestModel): - responses = [ResponseBuilders.output_message("test")] +def hello_mock_model(): + return TestModel.returning_responses([ResponseBuilders.output_message("test")]) @workflow.defn @@ -175,29 +133,26 @@ async def run(self, prompt: str) -> str: async def test_hello_world_agent(client: Client, use_local_model: bool): if not use_local_model and not os.environ.get("OPENAI_API_KEY"): pytest.skip("No openai API key") - new_config = client.config() - new_config["plugins"] = [ - openai_agents.OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30) - ), - model_provider=TestModelProvider(TestHelloModel()) - if use_local_model - else None, - ) - ] - client = Client(**new_config) - async with new_worker(client, HelloWorldAgent) as worker: - result = await client.execute_workflow( - HelloWorldAgent.run, - "Tell me about recursion in programming.", - id=f"hello-workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=5), - ) - if use_local_model: - assert result == "test" + model = hello_mock_model() if use_local_model else None + async with AgentEnvironment( + model=model, + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=30), + ), + ) as env: + client = env.applied_on_client(client) + + async with new_worker(client, HelloWorldAgent) as worker: + result = await client.execute_workflow( + HelloWorldAgent.run, + "Tell me about recursion in programming.", + id=f"hello-workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=60), + ) + if use_local_model: + assert result == "test" @dataclass @@ -216,7 +171,7 @@ async def get_weather(city: str) -> Weather: @activity.defn -async def get_weather_country(city: str, country: str) -> Weather: +async def get_weather_country(city: str, country: str) -> Weather: # type: ignore[reportUnusedParameter] """ Get the weather for a given city in a country. """ @@ -266,33 +221,41 @@ class WeatherService: class WeatherServiceHandler: @nexusrpc.handler.sync_operation async def get_weather_nexus_operation( - self, ctx: nexusrpc.handler.StartOperationContext, input: WeatherInput + self, + ctx: nexusrpc.handler.StartOperationContext, # type: ignore[reportUnusedParameter] + input: WeatherInput, # type: ignore[reportUnusedParameter] ) -> Weather: return Weather( city=input.city, temperature_range="14-20C", conditions="Sunny with wind." ) -class TestWeatherModel(StaticTestModel): - responses = [ - ResponseBuilders.tool_call('{"city":"Tokyo"}', "get_weather"), - ResponseBuilders.tool_call('{"input":{"city":"Tokyo"}}', "get_weather_object"), - ResponseBuilders.tool_call( - '{"city":"Tokyo","country":"Japan"}', "get_weather_country" - ), - ResponseBuilders.tool_call('{"city":"Tokyo"}', "get_weather_context"), - ResponseBuilders.tool_call('{"city":"Tokyo"}', "get_weather_method"), - ResponseBuilders.output_message("Test weather result"), - ] +def weather_mock_model(): + return TestModel.returning_responses( + [ + ResponseBuilders.tool_call('{"city":"Tokyo"}', "get_weather"), + ResponseBuilders.tool_call( + '{"input":{"city":"Tokyo"}}', "get_weather_object" + ), + ResponseBuilders.tool_call( + '{"city":"Tokyo","country":"Japan"}', "get_weather_country" + ), + ResponseBuilders.tool_call('{"city":"Tokyo"}', "get_weather_context"), + ResponseBuilders.tool_call('{"city":"Tokyo"}', "get_weather_method"), + ResponseBuilders.output_message("Test weather result"), + ] + ) -class TestNexusWeatherModel(StaticTestModel): - responses = [ - ResponseBuilders.tool_call( - '{"input":{"city":"Tokyo"}}', "get_weather_nexus_operation" - ), - ResponseBuilders.output_message("Test nexus weather result"), - ] +def nexus_weather_mock_model(): + return TestModel.returning_responses( + [ + ResponseBuilders.tool_call( + '{"input":{"city":"Tokyo"}}', "get_weather_nexus_operation" + ), + ResponseBuilders.output_message("Test nexus weather result"), + ] + ) @workflow.defn @@ -357,164 +320,161 @@ async def run(self, question: str) -> str: async def test_tool_workflow(client: Client, use_local_model: bool): if not use_local_model and not os.environ.get("OPENAI_API_KEY"): pytest.skip("No openai API key") - new_config = client.config() - new_config["plugins"] = [ - openai_agents.OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30) - ), - model_provider=TestModelProvider(TestWeatherModel()) - if use_local_model - else None, - ) - ] - client = Client(**new_config) - async with new_worker( - client, - ToolsWorkflow, - activities=[ - get_weather, - get_weather_object, - get_weather_country, - get_weather_context, - ActivityWeatherService().get_weather_method, - ], - ) as worker: - workflow_handle = await client.start_workflow( - ToolsWorkflow.run, - "What is the weather in Tokio?", - id=f"tools-workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=30), - ) - result = await workflow_handle.result() + model = weather_mock_model() if use_local_model else None + async with AgentEnvironment( + model=model, + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=30), + ), + ) as env: + client = env.applied_on_client(client) + + async with new_worker( + client, + ToolsWorkflow, + activities=[ + get_weather, + get_weather_object, + get_weather_country, + get_weather_context, + ActivityWeatherService().get_weather_method, + ], + ) as worker: + workflow_handle = await client.start_workflow( + ToolsWorkflow.run, + "What is the weather in Tokio?", + id=f"tools-workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=30), + ) + result = await workflow_handle.result() - if use_local_model: - assert result == "Test weather result" + if use_local_model: + assert result == "Test weather result" - events = [] - async for e in workflow_handle.fetch_history_events(): - if e.HasField("activity_task_completed_event_attributes"): - events.append(e) + events = [] + async for e in workflow_handle.fetch_history_events(): + if e.HasField("activity_task_completed_event_attributes"): + events.append(e) - assert len(events) == 11 - assert ( - "function_call" - in events[0] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "Sunny with wind" - in events[1] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "function_call" - in events[2] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "Sunny with wind" - in events[3] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "function_call" - in events[4] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "Sunny with wind" - in events[5] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "function_call" - in events[6] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "Stormy" - in events[7] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "function_call" - in events[8] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "Sunny with wind" - in events[9] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "Test weather result" - in events[10] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) + assert len(events) == 11 + assert ( + "function_call" + in events[0] + .activity_task_completed_event_attributes.result.payloads[0] + .data.decode() + ) + assert ( + "Sunny with wind" + in events[1] + .activity_task_completed_event_attributes.result.payloads[0] + .data.decode() + ) + assert ( + "function_call" + in events[2] + .activity_task_completed_event_attributes.result.payloads[0] + .data.decode() + ) + assert ( + "Sunny with wind" + in events[3] + .activity_task_completed_event_attributes.result.payloads[0] + .data.decode() + ) + assert ( + "function_call" + in events[4] + .activity_task_completed_event_attributes.result.payloads[0] + .data.decode() + ) + assert ( + "Sunny with wind" + in events[5] + .activity_task_completed_event_attributes.result.payloads[0] + .data.decode() + ) + assert ( + "function_call" + in events[6] + .activity_task_completed_event_attributes.result.payloads[0] + .data.decode() + ) + assert ( + "Stormy" + in events[7] + .activity_task_completed_event_attributes.result.payloads[0] + .data.decode() + ) + assert ( + "function_call" + in events[8] + .activity_task_completed_event_attributes.result.payloads[0] + .data.decode() + ) + assert ( + "Sunny with wind" + in events[9] + .activity_task_completed_event_attributes.result.payloads[0] + .data.decode() + ) + assert ( + "Test weather result" + in events[10] + .activity_task_completed_event_attributes.result.payloads[0] + .data.decode() + ) @activity.defn -async def get_weather_failure(city: str) -> Weather: +async def get_weather_failure(city: str) -> Weather: # type: ignore[reportUnusedParameter] """ Get the weather for a given city. """ raise ApplicationError("No weather", non_retryable=True) -class TestWeatherFailureModel(StaticTestModel): - responses = [ - ResponseBuilders.tool_call('{"city":"Tokyo"}', "get_weather_failure"), - ] +def weather_failure_mock_model(): + return TestModel.returning_responses( + [ + ResponseBuilders.tool_call('{"city":"Tokyo"}', "get_weather_failure"), + ] + ) async def test_tool_failure_workflow(client: Client): - new_config = client.config() - new_config["plugins"] = [ - openai_agents.OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30) - ), - model_provider=TestModelProvider(TestWeatherFailureModel()), - ) - ] - client = Client(**new_config) - - async with new_worker( - client, - ToolsWorkflow, - activities=[ - get_weather_failure, - ], - ) as worker: - workflow_handle = await client.start_workflow( - ToolsWorkflow.run, - "What is the weather in Tokio?", - id=f"tools-failure-workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=2), - ) - with pytest.raises(WorkflowFailureError) as e: - result = await workflow_handle.result() - cause = e.value.cause - assert isinstance(cause, ApplicationError) - assert "Workflow failure exception in Agents Framework" in cause.message + async with AgentEnvironment( + model=weather_failure_mock_model(), + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=30), + ), + ) as env: + client = env.applied_on_client(client) + + async with new_worker( + client, + ToolsWorkflow, + activities=[ + get_weather_failure, + ], + ) as worker: + workflow_handle = await client.start_workflow( + ToolsWorkflow.run, + "What is the weather in Tokio?", + id=f"tools-failure-workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=30), + ) + with pytest.raises(WorkflowFailureError) as e: + await workflow_handle.result() + cause = e.value.cause + assert isinstance(cause, ApplicationError) + assert "Workflow failure exception in Agents Framework" in cause.message @pytest.mark.parametrize("use_local_model", [True, False]) +@pytest.mark.requires_local_server async def test_nexus_tool_workflow( client: Client, env: WorkflowEnvironment, use_local_model: bool ): @@ -524,74 +484,71 @@ async def test_nexus_tool_workflow( if env.supports_time_skipping: pytest.skip("Nexus tests don't work with time-skipping server") - new_config = client.config() - new_config["plugins"] = [ - openai_agents.OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30) - ), - model_provider=TestModelProvider(TestNexusWeatherModel()) - if use_local_model - else None, - ) - ] - client = Client(**new_config) - - async with new_worker( - client, - NexusToolsWorkflow, - nexus_service_handlers=[WeatherServiceHandler()], - ) as worker: - await create_nexus_endpoint(worker.task_queue, client) + model = nexus_weather_mock_model() if use_local_model else None + async with AgentEnvironment( + model=model, + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=30), + ), + ) as agent_env: + client = agent_env.applied_on_client(client) + + async with new_worker( + client, + NexusToolsWorkflow, + nexus_service_handlers=[WeatherServiceHandler()], + ) as worker: + await env.create_nexus_endpoint( + make_nexus_endpoint_name(worker.task_queue), worker.task_queue + ) - workflow_handle = await client.start_workflow( - NexusToolsWorkflow.run, - "What is the weather in Tokio?", - id=f"nexus-tools-workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=30), - ) - result = await workflow_handle.result() + workflow_handle = await client.start_workflow( + NexusToolsWorkflow.run, + "What is the weather in Tokio?", + id=f"nexus-tools-workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=30), + ) + result = await workflow_handle.result() - if use_local_model: - assert result == "Test nexus weather result" + if use_local_model: + assert result == "Test nexus weather result" - events = [] - async for e in workflow_handle.fetch_history_events(): - if e.HasField("activity_task_completed_event_attributes") or e.HasField( - "nexus_operation_completed_event_attributes" - ): - events.append(e) + events = [] + async for e in workflow_handle.fetch_history_events(): + if e.HasField( + "activity_task_completed_event_attributes" + ) or e.HasField("nexus_operation_completed_event_attributes"): + events.append(e) - assert len(events) == 3 - assert ( - "function_call" - in events[0] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "Sunny with wind" - in events[ - 1 - ].nexus_operation_completed_event_attributes.result.data.decode() - ) - assert ( - "Test nexus weather result" - in events[2] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) + assert len(events) == 3 + assert ( + "function_call" + in events[0] + .activity_task_completed_event_attributes.result.payloads[0] + .data.decode() + ) + assert ( + "Sunny with wind" + in events[ + 1 + ].nexus_operation_completed_event_attributes.result.data.decode() + ) + assert ( + "Test nexus weather result" + in events[2] + .activity_task_completed_event_attributes.result.payloads[0] + .data.decode() + ) -@no_type_check -class TestResearchModel(StaticTestModel): +def research_mock_model(): responses = [ ResponseBuilders.output_message( '{"searches":[{"query":"best Caribbean surfing spots April","reason":"Identify locations with optimal surfing conditions in the Caribbean during April."},{"query":"top Caribbean islands for hiking April","reason":"Find Caribbean islands with excellent hiking opportunities that are ideal in April."},{"query":"Caribbean water sports destinations April","reason":"Locate Caribbean destinations offering a variety of water sports activities in April."},{"query":"surfing conditions Caribbean April","reason":"Understand the surfing conditions and which islands are suitable for surfing in April."},{"query":"Caribbean adventure travel hiking surfing","reason":"Explore adventure travel options that combine hiking and surfing in the Caribbean."},{"query":"best beaches for surfing Caribbean April","reason":"Identify which Caribbean beaches are renowned for surfing in April."},{"query":"Caribbean islands with national parks hiking","reason":"Find islands with national parks or reserves that offer hiking trails."},{"query":"Caribbean weather April surfing conditions","reason":"Research the weather conditions in April affecting surfing in the Caribbean."},{"query":"Caribbean water sports rentals April","reason":"Look for places where water sports equipment can be rented in the Caribbean during April."},{"query":"Caribbean multi-activity vacation packages","reason":"Look for vacation packages that offer a combination of surfing, hiking, and water sports."}]}' ) ] - for i in range(10): + for _ in range(10): responses.append( ModelResponse( output=[ @@ -599,7 +556,9 @@ class TestResearchModel(StaticTestModel): id="", status="completed", type="web_search_call", - action=ActionSearch(query="", type="search"), + action=ActionSearch.model_construct( + type="search", queries=[""] + ), ), ResponseBuilders.response_output_message("Granada"), ], @@ -612,6 +571,7 @@ class TestResearchModel(StaticTestModel): '{"follow_up_questions":[], "markdown_report":"report", "short_summary":"rep"}' ) ) + return TestModel.returning_responses(responses) @workflow.defn @@ -626,62 +586,59 @@ async def run(self, query: str): async def test_research_workflow(client: Client, use_local_model: bool): if not use_local_model and not os.environ.get("OPENAI_API_KEY"): pytest.skip("No openai API key") - new_config = client.config() - new_config["plugins"] = [ - openai_agents.OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=120), - schedule_to_close_timeout=timedelta(seconds=120), - ), - model_provider=TestModelProvider(TestResearchModel()) - if use_local_model - else None, - ) - ] - client = Client(**new_config) - async with new_worker( - client, - ResearchWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - ResearchWorkflow.run, - "Caribbean vacation spots in April, optimizing for surfing, hiking and water sports", - id=f"research-workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=120), - ) - result = await workflow_handle.result() + model = research_mock_model() if use_local_model else None + async with AgentEnvironment( + model=model, + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=120), + schedule_to_close_timeout=timedelta(seconds=120), + ), + ) as env: + client = env.applied_on_client(client) + + async with new_worker( + client, + ResearchWorkflow, + ) as worker: + workflow_handle = await client.start_workflow( + ResearchWorkflow.run, + "Caribbean vacation spots in April, optimizing for surfing, hiking and water sports", + id=f"research-workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=120), + ) + result = await workflow_handle.result() - if use_local_model: - assert result == "report" + if use_local_model: + assert result == "report" - events = [] - async for e in workflow_handle.fetch_history_events(): - if e.HasField("activity_task_completed_event_attributes"): - events.append(e) + events = [] + async for e in workflow_handle.fetch_history_events(): + if e.HasField("activity_task_completed_event_attributes"): + events.append(e) - assert len(events) == 12 - assert ( - '"type":"output_text"' - in events[0] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - for i in range(1, 11): + assert len(events) == 12 assert ( - "web_search_call" - in events[i] + '"type":"output_text"' + in events[0] .activity_task_completed_event_attributes.result.payloads[0] .data.decode() ) + for i in range(1, 11): + assert ( + "web_search_call" + in events[i] + .activity_task_completed_event_attributes.result.payloads[0] + .data.decode() + ) - assert ( - '"type":"output_text"' - in events[11] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) + assert ( + '"type":"output_text"' + in events[11] + .activity_task_completed_event_attributes.result.payloads[0] + .data.decode() + ) def orchestrator_agent() -> Agent: @@ -761,89 +718,88 @@ async def run(self, msg: str) -> str: return synthesizer_result.final_output -class AgentAsToolsModel(StaticTestModel): - responses = [ - ResponseBuilders.tool_call('{"input":"I am full"}', "translate_to_spanish"), - ResponseBuilders.output_message("Estoy lleno."), - ResponseBuilders.output_message( - 'The translation to Spanish is: "Estoy lleno."' - ), - ResponseBuilders.output_message( - 'The translation to Spanish is: "Estoy lleno."' - ), - ] +def agent_as_tools_mock_model(): + return TestModel.returning_responses( + [ + ResponseBuilders.tool_call('{"input":"I am full"}', "translate_to_spanish"), + ResponseBuilders.output_message("Estoy lleno."), + ResponseBuilders.output_message( + 'The translation to Spanish is: "Estoy lleno."' + ), + ResponseBuilders.output_message( + 'The translation to Spanish is: "Estoy lleno."' + ), + ] + ) @pytest.mark.parametrize("use_local_model", [True, False]) async def test_agents_as_tools_workflow(client: Client, use_local_model: bool): if not use_local_model and not os.environ.get("OPENAI_API_KEY"): pytest.skip("No openai API key") - new_config = client.config() - new_config["plugins"] = [ - openai_agents.OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30) - ), - model_provider=TestModelProvider(AgentAsToolsModel()) - if use_local_model - else None, - ) - ] - client = Client(**new_config) - async with new_worker( - client, - AgentsAsToolsWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - AgentsAsToolsWorkflow.run, - "Translate to Spanish: 'I am full'", - id=f"agents-as-tools-workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=30), - ) - result = await workflow_handle.result() + model = agent_as_tools_mock_model() if use_local_model else None + async with AgentEnvironment( + model=model, + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=30), + ), + ) as env: + client = env.applied_on_client(client) + + async with new_worker( + client, + AgentsAsToolsWorkflow, + ) as worker: + workflow_handle = await client.start_workflow( + AgentsAsToolsWorkflow.run, + "Translate to Spanish: 'I am full'", + id=f"agents-as-tools-workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=30), + ) + result = await workflow_handle.result() - if use_local_model: - assert result == 'The translation to Spanish is: "Estoy lleno."' + if use_local_model: + assert result == 'The translation to Spanish is: "Estoy lleno."' - events = [] - async for e in workflow_handle.fetch_history_events(): - if e.HasField("activity_task_completed_event_attributes"): - events.append(e) + events = [] + async for e in workflow_handle.fetch_history_events(): + if e.HasField("activity_task_completed_event_attributes"): + events.append(e) - assert len(events) == 4 - assert ( - "function_call" - in events[0] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "Estoy lleno" - in events[1] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "The translation to Spanish is:" - in events[2] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "The translation to Spanish is:" - in events[3] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) + assert len(events) == 4 + assert ( + "function_call" + in events[0] + .activity_task_completed_event_attributes.result.payloads[0] + .data.decode() + ) + assert ( + "Estoy lleno" + in events[1] + .activity_task_completed_event_attributes.result.payloads[0] + .data.decode() + ) + assert ( + "The translation to Spanish is:" + in events[2] + .activity_task_completed_event_attributes.result.payloads[0] + .data.decode() + ) + assert ( + "The translation to Spanish is:" + in events[3] + .activity_task_completed_event_attributes.result.payloads[0] + .data.decode() + ) class AirlineAgentContext(BaseModel): - passenger_name: Optional[str] = None - confirmation_number: Optional[str] = None - seat_number: Optional[str] = None - flight_number: Optional[str] = None + passenger_name: str | None = None + confirmation_number: str | None = None + seat_number: str | None = None + flight_number: str | None = None @function_tool( @@ -888,7 +844,10 @@ async def update_seat( async def on_seat_booking_handoff( context: RunContextWrapper[AirlineAgentContext], ) -> None: - flight_number = f"FLT-{workflow.random().randint(100, 999)}" + try: + flight_number = f"FLT-{workflow.random().randint(100, 999)}" + except TemporalError: + flight_number = "FLT-100" context.context.flight_number = flight_number @@ -950,23 +909,28 @@ class ProcessUserMessageInput(BaseModel): chat_length: int -class CustomerServiceModel(StaticTestModel): - responses = [ - ResponseBuilders.output_message("Hi there! How can I assist you today?"), - ResponseBuilders.tool_call("{}", "transfer_to_seat_booking_agent"), - ResponseBuilders.output_message( - "Could you please provide your confirmation number?" - ), - ResponseBuilders.output_message( - "Thanks! What seat number would you like to change to?" - ), - ResponseBuilders.tool_call( - '{"confirmation_number":"11111","new_seat":"window seat"}', "update_seat" - ), - ResponseBuilders.output_message( - "Your seat has been updated to a window seat. If there's anything else you need, feel free to let me know!" - ), - ] +def customer_service_mock_model(): + return TestModel.returning_responses( + [ + ResponseBuilders.output_message("Hi there! How can I assist you today?"), + ResponseBuilders.tool_call("{}", "transfer_to_seat_booking_agent"), + ResponseBuilders.output_message( + "Could you please provide your confirmation number?" + ), + ResponseBuilders.output_message( + "Thanks! What seat number would you like to change to?" + ), + ResponseBuilders.tool_call( + '{"confirmation_number":"11111","new_seat":"window seat"}', + "update_seat", + ), + ResponseBuilders.output_message( + "Your seat has been updated to a window seat. If there's anything else you need, feel free to let me know!" + ), + ResponseBuilders.tool_call("{}", "transfer_to_triage_agent"), + ResponseBuilders.output_message("You're welcome!"), + ] + ) @workflow.defn @@ -978,11 +942,8 @@ def __init__(self, input_items: list[TResponseInputItem] = []): self.input_items = input_items @workflow.run - async def run(self, input_items: list[TResponseInputItem] = []): - await workflow.wait_condition( - lambda: workflow.info().is_continue_as_new_suggested() - and workflow.all_handlers_finished() - ) + async def run(self, _input_items: list[TResponseInputItem] = []): + await workflow.wait_condition(lambda: False) workflow.continue_as_new(self.input_items) @workflow.query @@ -1040,95 +1001,110 @@ def validate_process_user_message(self, input: ProcessUserMessageInput) -> None: async def test_customer_service_workflow(client: Client, use_local_model: bool): if not use_local_model and not os.environ.get("OPENAI_API_KEY"): pytest.skip("No openai API key") - new_config = client.config() - new_config["plugins"] = [ - openai_agents.OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30) - ), - model_provider=TestModelProvider(CustomerServiceModel()) - if use_local_model - else None, - ) - ] - client = Client(**new_config) - - questions = ["Hello", "Book me a flight to PDX", "11111", "Any window seat"] - - async with new_worker( - client, - CustomerServiceWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - CustomerServiceWorkflow.run, - id=f"customer-service-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=30), - ) - history: list[Any] = [] - for q in questions: - message_input = ProcessUserMessageInput( - user_input=q, chat_length=len(history) - ) - new_history = await workflow_handle.execute_update( - CustomerServiceWorkflow.process_user_message, message_input - ) - history.extend(new_history) - print(*new_history, sep="\n") - - await workflow_handle.cancel() - - with pytest.raises(WorkflowFailureError) as err: - await workflow_handle.result() - assert isinstance(err.value.cause, CancelledError) - if use_local_model: - events = [] - async for e in WorkflowHandle( - client, - workflow_handle.id, - run_id=workflow_handle._first_execution_run_id, - ).fetch_history_events(): - if e.HasField("activity_task_completed_event_attributes"): - events.append(e) + model = customer_service_mock_model() if use_local_model else None + async with AgentEnvironment( + model=model, + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=30), + ), + ) as env: + client = env.applied_on_client(client) - assert len(events) == 6 - assert ( - "Hi there! How can I assist you today?" - in events[0] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "transfer_to_seat_booking_agent" - in events[1] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "Could you please provide your confirmation number?" - in events[2] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "Thanks! What seat number would you like to change to?" - in events[3] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "update_seat" - in events[4] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "Your seat has been updated to a window seat. If there's anything else you need, feel free to let me know!" - in events[5] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() + questions = [ + "Hello", + "Book me a flight to PDX", + "11111", + "Any window seat", + "Take me back to the triage agent to say goodbye", + ] + + async with new_worker( + client, + CustomerServiceWorkflow, + ) as worker: + workflow_handle = await client.start_workflow( + CustomerServiceWorkflow.run, + id=f"customer-service-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=60), ) + history: list[Any] = [] + for q in questions: + message_input = ProcessUserMessageInput( + user_input=q, chat_length=len(history) + ) + new_history = await workflow_handle.execute_update( + CustomerServiceWorkflow.process_user_message, message_input + ) + history.extend(new_history) + print(*new_history, sep="\n") + + await workflow_handle.cancel() + + with pytest.raises(WorkflowFailureError) as err: + await workflow_handle.result() + assert isinstance(err.value.cause, CancelledError) + + if use_local_model: + events = [] + async for e in WorkflowHandle( + client, + workflow_handle.id, + run_id=workflow_handle._first_execution_run_id, + ).fetch_history_events(): + if e.HasField("activity_task_completed_event_attributes"): + events.append(e) + + assert len(events) == 8 + assert ( + "Hi there! How can I assist you today?" + in events[0] + .activity_task_completed_event_attributes.result.payloads[0] + .data.decode() + ) + assert ( + "transfer_to_seat_booking_agent" + in events[1] + .activity_task_completed_event_attributes.result.payloads[0] + .data.decode() + ) + assert ( + "Could you please provide your confirmation number?" + in events[2] + .activity_task_completed_event_attributes.result.payloads[0] + .data.decode() + ) + assert ( + "Thanks! What seat number would you like to change to?" + in events[3] + .activity_task_completed_event_attributes.result.payloads[0] + .data.decode() + ) + assert ( + "update_seat" + in events[4] + .activity_task_completed_event_attributes.result.payloads[0] + .data.decode() + ) + assert ( + "Your seat has been updated to a window seat. If there's anything else you need, feel free to let me know!" + in events[5] + .activity_task_completed_event_attributes.result.payloads[0] + .data.decode() + ) + assert ( + "transfer_to_triage_agent" + in events[6] + .activity_task_completed_event_attributes.result.payloads[0] + .data.decode() + ) + assert ( + "You're welcome!" + in events[7] + .activity_task_completed_event_attributes.result.payloads[0] + .data.decode() + ) class InputGuardrailModel(OpenAIResponsesModel): @@ -1157,15 +1133,16 @@ def __init__( async def get_response( self, - system_instructions: Union[str, None], - input: Union[str, list[TResponseInputItem]], + system_instructions: str | None, + input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: Union[AgentOutputSchemaBase, None], + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], tracing: ModelTracing, - previous_response_id: Union[str, None], - prompt: Union[ResponsePromptParam, None] = None, + previous_response_id: str | None = None, + conversation_id: str | None = None, + prompt: ResponsePromptParam | None = None, ) -> ModelResponse: if ( system_instructions @@ -1193,8 +1170,8 @@ class MathHomeworkOutput(BaseModel): @input_guardrail async def math_guardrail( context: RunContextWrapper[None], - agent: Agent, - input: Union[str, list[TResponseInputItem]], + _agent: Agent, + input: str | list[TResponseInputItem], ) -> GuardrailFunctionOutput: """This is an input guardrail function, which happens to call an agent to check if the input is a math homework question. @@ -1251,49 +1228,50 @@ async def run(self, messages: list[str]) -> list[str]: async def test_input_guardrail(client: Client, use_local_model: bool): if not use_local_model and not os.environ.get("OPENAI_API_KEY"): pytest.skip("No openai API key") - new_config = client.config() - new_config["plugins"] = [ - openai_agents.OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30) - ), - model_provider=TestModelProvider( - InputGuardrailModel("", openai_client=AsyncOpenAI(api_key="Fake key")) - ) - if use_local_model - else None, - ) - ] - client = Client(**new_config) - async with new_worker( - client, - InputGuardrailWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - InputGuardrailWorkflow.run, - [ - "What's the capital of California?", - "Can you help me solve for x: 2x + 5 = 11", - ], - id=f"input-guardrail-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=10), - ) - result = await workflow_handle.result() + model = ( + InputGuardrailModel("", openai_client=AsyncOpenAI(api_key="Fake key")) + if use_local_model + else None + ) + async with AgentEnvironment( + model=model, + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=30), + ), + ) as env: + client = env.applied_on_client(client) + + async with new_worker( + client, + InputGuardrailWorkflow, + ) as worker: + workflow_handle = await client.start_workflow( + InputGuardrailWorkflow.run, + [ + "What's the capital of California?", + "Can you help me solve for x: 2x + 5 = 11", + ], + id=f"input-guardrail-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=60), + ) + result = await workflow_handle.result() - if use_local_model: - assert len(result) == 2 - assert result[0] == "The capital of California is Sacramento." - assert result[1] == "Sorry, I can't help you with your math homework." + if use_local_model: + assert len(result) == 2 + assert result[0] == "The capital of California is Sacramento." + assert result[1] == "Sorry, I can't help you with your math homework." -class OutputGuardrailModel(StaticTestModel): - responses = [ - ResponseBuilders.output_message( - '{"reasoning":"The phone number\'s area code (650) is associated with a region. However, the exact location is not definitive, but it\'s commonly linked to the San Francisco Peninsula in California, including cities like San Mateo, Palo Alto, and parts of Silicon Valley. It\'s important to note that area codes don\'t always guarantee a specific location due to mobile number portability.","response":"The area code 650 is typically associated with California, particularly the San Francisco Peninsula, including cities like Palo Alto and San Mateo.","user_name":null}' - ) - ] +def output_guardrail_mock_model(): + return TestModel.returning_responses( + [ + ResponseBuilders.output_message( + '{"reasoning":"The phone number\'s area code (650) is associated with a region. However, the exact location is not definitive, but it\'s commonly linked to the San Francisco Peninsula in California, including cities like San Mateo, Palo Alto, and parts of Silicon Valley. It\'s important to note that area codes don\'t always guarantee a specific location due to mobile number portability.","response":"The area code 650 is typically associated with California, particularly the San Francisco Peninsula, including cities like Palo Alto and San Mateo.","user_name":null}' + ) + ] + ) # The agent's output type @@ -1302,7 +1280,7 @@ class MessageOutput(BaseModel): description="Thoughts on how to respond to the user's message" ) response: str = Field(description="The response to the user's message") - user_name: Optional[str] = Field( + user_name: str | None = Field( description="The name of the user who sent the message, if known" ) model_config = ConfigDict(extra="forbid") @@ -1310,7 +1288,7 @@ class MessageOutput(BaseModel): @output_guardrail async def sensitive_data_check( - context: RunContextWrapper, agent: Agent, output: MessageOutput + _context: RunContextWrapper, _agent: Agent, output: MessageOutput ) -> GuardrailFunctionOutput: phone_number_in_response = "650" in output.response phone_number_in_reasoning = "650" in output.reasoning @@ -1350,40 +1328,39 @@ async def run(self) -> bool: async def test_output_guardrail(client: Client, use_local_model: bool): if not use_local_model and not os.environ.get("OPENAI_API_KEY"): pytest.skip("No openai API key") - new_config = client.config() - new_config["plugins"] = [ - openai_agents.OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30) - ), - model_provider=TestModelProvider(OutputGuardrailModel()) - if use_local_model - else None, - ) - ] - client = Client(**new_config) - async with new_worker( - client, - OutputGuardrailWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - OutputGuardrailWorkflow.run, - id=f"output-guardrail-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=10), - ) - result = await workflow_handle.result() + model = output_guardrail_mock_model() if use_local_model else None + async with AgentEnvironment( + model=model, + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=30), + ), + ) as env: + client = env.applied_on_client(client) + + async with new_worker( + client, + OutputGuardrailWorkflow, + ) as worker: + workflow_handle = await client.start_workflow( + OutputGuardrailWorkflow.run, + id=f"output-guardrail-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + result = await workflow_handle.result() - if use_local_model: - assert not result + if use_local_model: + assert not result -class WorkflowToolModel(StaticTestModel): - responses = [ - ResponseBuilders.tool_call("{}", "run_tool"), - ResponseBuilders.output_message("Workflow tool was used"), - ] +def workflow_tool_mock_model(): + return TestModel.returning_responses( + [ + ResponseBuilders.tool_call("{}", "run_tool"), + ResponseBuilders.output_message("Workflow tool was used"), + ] + ) @workflow.defn @@ -1393,6 +1370,7 @@ async def run(self) -> None: agent: Agent = Agent( name="Assistant", instructions="You are a helpful assistant.", + model="gpt-4o", tools=[function_tool(self.run_tool)], ) await Runner.run( @@ -1407,16 +1385,13 @@ async def run_tool(self): async def test_workflow_method_tools(client: Client): - new_config = client.config() - new_config["plugins"] = [ - openai_agents.OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30) - ), - model_provider=TestModelProvider(WorkflowToolModel()), - ) - ] - client = Client(**new_config) + async with AgentEnvironment( + model=workflow_tool_mock_model(), + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=30), + ), + ) as env: + client = env.applied_on_client(client) async with new_worker( client, @@ -1446,7 +1421,7 @@ async def test_response_serialization(): usage=Usage(), response_id="", ) - encoded = await pydantic_data_converter.encode([model_response]) + await pydantic_data_converter.encode([model_response]) async def assert_status_retry_behavior(status: int, client: Client, should_retry: bool): @@ -1462,40 +1437,37 @@ def status_error(status: int): body=None, ) - new_config = client.config() - new_config["plugins"] = [ - openai_agents.OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - retry_policy=RetryPolicy(maximum_attempts=2), - ), - model_provider=TestModelProvider(TestModel(lambda: status_error(status))), - ) - ] - client = Client(**new_config) - - async with new_worker( - client, - HelloWorldAgent, - ) as worker: - workflow_handle = await client.start_workflow( - HelloWorldAgent.run, - "Input", - id=f"workflow-tool-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=10), - ) - with pytest.raises(WorkflowFailureError) as e: - await workflow_handle.result() + async with AgentEnvironment( + model=TestModel(lambda: status_error(status)), + model_params=ModelActivityParameters( + retry_policy=RetryPolicy(maximum_attempts=2), + ), + ) as env: + client = env.applied_on_client(client) + + async with new_worker( + client, + HelloWorldAgent, + ) as worker: + workflow_handle = await client.start_workflow( + HelloWorldAgent.run, + "Input", + id=f"workflow-tool-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + with pytest.raises(WorkflowFailureError): + await workflow_handle.result() - found = False - async for event in workflow_handle.fetch_history_events(): - if event.HasField("activity_task_started_event_attributes"): - found = True - if should_retry: - assert event.activity_task_started_event_attributes.attempt == 2 - else: - assert event.activity_task_started_event_attributes.attempt == 1 - assert found + found = False + async for event in workflow_handle.fetch_history_events(): + if event.HasField("activity_task_started_event_attributes"): + found = True + if should_retry: + assert event.activity_task_started_event_attributes.attempt == 2 + else: + assert event.activity_task_started_event_attributes.attempt == 1 + assert found async def test_exception_handling(client: Client): @@ -1510,7 +1482,7 @@ async def test_exception_handling(client: Client): class CustomModelProvider(ModelProvider): - def get_model(self, model_name: Optional[str]) -> Model: + def get_model(self, model_name: str | None) -> Model: client = AsyncOpenAI(base_url="https://api.openai.com/v1") return OpenAIChatCompletionsModel(model="gpt-4o", openai_client=client) @@ -1519,43 +1491,38 @@ async def test_chat_completions_model(client: Client): if not os.environ.get("OPENAI_API_KEY"): pytest.skip("No openai API key") - new_config = client.config() - new_config["plugins"] = [ - openai_agents.OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30) - ), - model_provider=CustomModelProvider(), - ) - ] - client = Client(**new_config) - - async with new_worker( - client, - WorkflowToolWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - WorkflowToolWorkflow.run, - id=f"workflow-tool-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=10), - ) - await workflow_handle.result() + async with AgentEnvironment( + model_provider=CustomModelProvider(), + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=30), + ), + ) as env: + client = env.applied_on_client(client) + + async with new_worker( + client, + WorkflowToolWorkflow, + ) as worker: + workflow_handle = await client.start_workflow( + WorkflowToolWorkflow.run, + id=f"workflow-tool-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + await workflow_handle.result() class WaitModel(Model): async def get_response( self, - system_instructions: Union[str, None], - input: Union[str, list[TResponseInputItem]], + system_instructions: str | None, + input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: Union[AgentOutputSchemaBase, None], + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], tracing: ModelTracing, - *, - previous_response_id: Union[str, None], - prompt: Union[ResponsePromptParam, None] = None, + **kwargs, # type:ignore[reportMissingParameterType] ) -> ModelResponse: activity.logger.info("Waiting") await asyncio.sleep(1.0) @@ -1564,16 +1531,14 @@ async def get_response( def stream_response( self, - system_instructions: Optional[str], - input: Union[str, list[TResponseInputItem]], + system_instructions: str | None, + input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: Optional[AgentOutputSchemaBase], + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], tracing: ModelTracing, - *, - previous_response_id: Optional[str], - prompt: Optional[ResponsePromptParam], + **kwargs, # type:ignore[reportMissingParameterType] ) -> AsyncIterator[TResponseStreamEvent]: raise NotImplementedError() @@ -1592,98 +1557,58 @@ async def run(self, prompt: str) -> str: class CheckModelNameProvider(ModelProvider): - def get_model(self, model_name: Optional[str]) -> Model: + def get_model(self, model_name: str | None) -> Model: assert model_name == "test_model" - return TestHelloModel() + return hello_mock_model() async def test_alternative_model(client: Client): - new_config = client.config() - new_config["plugins"] = [ - openai_agents.OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30) - ), - model_provider=CheckModelNameProvider(), - ) - ] - client = Client(**new_config) - - async with new_worker( - client, - AlternateModelAgent, - ) as worker: - workflow_handle = await client.start_workflow( - AlternateModelAgent.run, - "Hello", - id=f"alternative-model-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=10), - ) - await workflow_handle.result() + async with AgentEnvironment( + model_provider=CheckModelNameProvider(), + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=30), + ), + ) as env: + client = env.applied_on_client(client) + + async with new_worker( + client, + AlternateModelAgent, + ) as worker: + workflow_handle = await client.start_workflow( + AlternateModelAgent.run, + "Hello", + id=f"alternative-model-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + await workflow_handle.result() async def test_heartbeat(client: Client, env: WorkflowEnvironment): if env.supports_time_skipping: pytest.skip("Relies on real timing, skip.") - new_config = client.config() - new_config["plugins"] = [ - openai_agents.OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - heartbeat_timeout=timedelta(seconds=0.5), - ), - model_provider=TestModelProvider(WaitModel()), - ) - ] - client = Client(**new_config) - - async with new_worker( - client, - HelloWorldAgent, - ) as worker: - workflow_handle = await client.start_workflow( - HelloWorldAgent.run, - "Tell me about recursion in programming.", - id=f"workflow-tool-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=5.0), - ) - await workflow_handle.result() - - -def test_summary_extraction(): - input: list[TResponseInputItem] = [ - EasyInputMessageParam( - content="First message", - role="user", - ) - ] - - assert _extract_summary(input) == "First message" - - input.append( - Message( - content=[ - ResponseInputTextParam( - text="Second message", - type="input_text", - ) - ], - role="user", - ) - ) - assert _extract_summary(input) == "Second message" - - input.append( - ResponseFunctionToolCallParam( - arguments="", - call_id="", - name="", - type="function_call", - ) - ) - assert _extract_summary(input) == "Second message" + async with AgentEnvironment( + model=WaitModel(), + model_params=ModelActivityParameters( + heartbeat_timeout=timedelta(seconds=0.5), + ), + ) as agent_env: + client = agent_env.applied_on_client(client) + + async with new_worker( + client, + HelloWorldAgent, + ) as worker: + workflow_handle = await client.start_workflow( + HelloWorldAgent.run, + "Tell me about recursion in programming.", + id=f"workflow-tool-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=5.0), + ) + await workflow_handle.result() @workflow.defn @@ -1702,84 +1627,89 @@ async def run(self) -> None: async def test_session(client: Client): - new_config = client.config() - new_config["plugins"] = [ - openai_agents.OpenAIAgentsPlugin( - model_provider=TestModelProvider(TestHelloModel()), - ) - ] - client = Client(**new_config) + async with AgentEnvironment(model=hello_mock_model()) as env: + client = env.applied_on_client(client) + + async with new_worker( + client, + SessionWorkflow, + ) as worker: + workflow_handle = await client.start_workflow( + SessionWorkflow.run, + id=f"session-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10.0), + retry_policy=RetryPolicy(maximum_attempts=1), + ) - async with new_worker( - client, - SessionWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - SessionWorkflow.run, - id=f"session-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=1.0), - retry_policy=RetryPolicy(maximum_attempts=1), - ) - await assert_task_fail_eventually( - workflow_handle, - message_contains="Temporal workflows don't support SQLite sessions", - ) + async def check(): + async for evt in workflow_handle.fetch_history_events(): + # Sometimes just creating the sqlite session takes too long for a workflow in CI, so check both + if evt.HasField("workflow_task_failed_event_attributes") and ( + "Temporal workflows don't support SQLite sessions" + in evt.workflow_task_failed_event_attributes.failure.message + or "Potential deadlock detected" + in evt.workflow_task_failed_event_attributes.failure.message + ): + return + + await assert_eventually(check) -async def test_lite_llm(client: Client, env: WorkflowEnvironment): +async def test_lite_llm(client: Client): if not os.environ.get("OPENAI_API_KEY"): pytest.skip("No openai API key") - if sys.version_info < (3, 10): - pytest.skip("Lite LLM does not import below 3.10") + if sys.version_info >= (3, 14): + pytest.skip("Lite LLM does not yet support Python 3.14") # type:ignore[reportUnreachable] - from agents.extensions.models.litellm_provider import LitellmProvider - - new_config = client.config() - new_config["plugins"] = [ - openai_agents.OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30) - ), - model_provider=LitellmProvider(), - ) - ] - client = Client(**new_config) + from agents.extensions.models.litellm_provider import ( + LitellmProvider, # type:ignore[reportUnreachable] + ) - async with new_worker( - client, - HelloWorldAgent, - ) as worker: - workflow_handle = await client.start_workflow( - HelloWorldAgent.run, - "Tell me about recursion in programming", - id=f"lite-llm-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=10), - ) - await workflow_handle.result() + async with AgentEnvironment( + model_provider=LitellmProvider(), + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=30), + ), + ) as agent_env: + client = agent_env.applied_on_client(client) # type:ignore[reportUnreachable] + + async with new_worker( + client, + HelloWorldAgent, + ) as worker: + workflow_handle = await client.start_workflow( # type:ignore[reportUnreachable] + HelloWorldAgent.run, + "Tell me about recursion in programming", + id=f"lite-llm-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + await workflow_handle.result() -class FileSearchToolModel(StaticTestModel): - responses = [ - ModelResponse( - output=[ - ResponseFileSearchToolCall( - queries=["side character in the Iliad"], - type="file_search_call", - id="id", - status="completed", - results=[ - Result(text="Some scene"), - Result(text="Other scene"), - ], - ), - ResponseBuilders.response_output_message("Patroclus"), - ], - usage=Usage(), - response_id=None, - ), - ] +def file_search_tool_mock_model(): + return TestModel.returning_responses( + [ + ModelResponse( + output=[ + ResponseFileSearchToolCall( + queries=["side character in the Iliad"], + type="file_search_call", + id="id", + status="completed", + results=[ + Result(text="Some scene"), + Result(text="Other scene"), + ], + ), + ResponseBuilders.response_output_message("Patroclus"), + ], + usage=Usage(), + response_id=None, + ), + ] + ) @workflow.defn @@ -1809,54 +1739,52 @@ async def run(self, question: str) -> str: @pytest.mark.parametrize("use_local_model", [True, False]) -async def test_file_search_tool(client: Client, use_local_model): +async def test_file_search_tool(client: Client, use_local_model: bool): if not use_local_model and not os.environ.get("OPENAI_API_KEY"): pytest.skip("No openai API key") - new_config = client.config() - new_config["plugins"] = [ - openai_agents.OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30) - ), - model_provider=TestModelProvider(FileSearchToolModel()) - if use_local_model - else None, - ) - ] - client = Client(**new_config) - - async with new_worker( - client, - FileSearchToolWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - FileSearchToolWorkflow.run, - "Tell me about a side character in the Iliad.", - id=f"file-search-tool-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=30), - ) - result = await workflow_handle.result() - if use_local_model: - assert result == "Patroclus" + model = file_search_tool_mock_model() if use_local_model else None + async with AgentEnvironment( + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=30) + ), + model=model, + ) as env: + client = env.applied_on_client(client) + + async with new_worker( + client, + FileSearchToolWorkflow, + ) as worker: + workflow_handle = await client.start_workflow( + FileSearchToolWorkflow.run, + "Tell me about a side character in the Iliad.", + id=f"file-search-tool-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=30), + ) + result = await workflow_handle.result() + if use_local_model: + assert result == "Patroclus" -class ImageGenerationModel(StaticTestModel): - responses = [ - ModelResponse( - output=[ - ImageGenerationCall( - type="image_generation_call", - id="id", - status="completed", - ), - ResponseBuilders.response_output_message("Patroclus"), - ], - usage=Usage(), - response_id=None, - ), - ] +def image_generation_mock_model(): + return TestModel.returning_responses( + [ + ModelResponse( + output=[ + ImageGenerationCall( + type="image_generation_call", + id="id", + status="completed", + ), + ResponseBuilders.response_output_message("Patroclus"), + ], + usage=Usage(), + response_id=None, + ), + ] + ) @workflow.defn @@ -1885,54 +1813,52 @@ async def run(self, question: str) -> str: # Can't currently validate against real server, we aren't verified for image generation @pytest.mark.parametrize("use_local_model", [True]) -async def test_image_generation_tool(client: Client, use_local_model): +async def test_image_generation_tool(client: Client, use_local_model: bool): if not use_local_model and not os.environ.get("OPENAI_API_KEY"): pytest.skip("No openai API key") - new_config = client.config() - new_config["plugins"] = [ - openai_agents.OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30) - ), - model_provider=TestModelProvider(ImageGenerationModel()) - if use_local_model - else None, - ) - ] - client = Client(**new_config) - - async with new_worker( - client, - ImageGenerationWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - ImageGenerationWorkflow.run, - "Create an image of a frog eating a pizza, comic book style.", - id=f"image-generation-tool-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=30), - ) - result = await workflow_handle.result() + model = image_generation_mock_model() if use_local_model else None + async with AgentEnvironment( + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=30) + ), + model=model, + ) as env: + client = env.applied_on_client(client) + + async with new_worker( + client, + ImageGenerationWorkflow, + ) as worker: + workflow_handle = await client.start_workflow( + ImageGenerationWorkflow.run, + "Create an image of a frog eating a pizza, comic book style.", + id=f"image-generation-tool-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=30), + ) + await workflow_handle.result() -class CodeInterpreterModel(StaticTestModel): - responses = [ - ModelResponse( - output=[ - ResponseCodeInterpreterToolCall( - container_id="", - code="some code", - type="code_interpreter_call", - id="id", - status="completed", - ), - ResponseBuilders.response_output_message("Over 9000"), - ], - usage=Usage(), - response_id=None, - ), - ] +def code_interpreter_mock_model(): + return TestModel.returning_responses( + [ + ModelResponse( + output=[ + ResponseCodeInterpreterToolCall( + container_id="", + code="some code", + type="code_interpreter_call", + id="id", + status="completed", + ), + ResponseBuilders.response_output_message("Over 9000"), + ], + usage=Usage(), + response_id=None, + ), + ] + ) @workflow.defn @@ -1962,63 +1888,62 @@ async def run(self, question: str) -> str: async def test_code_interpreter_tool(client: Client): - new_config = client.config() - new_config["plugins"] = [ - openai_agents.OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=60) - ), - model_provider=TestModelProvider(CodeInterpreterModel()), - ) - ] - client = Client(**new_config) - - async with new_worker( - client, - CodeInterpreterWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - CodeInterpreterWorkflow.run, - "What is the square root of273 * 312821 plus 1782?", - id=f"code-interpreter-tool-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=60), - ) - result = await workflow_handle.result() - assert result == "Over 9000" + async with AgentEnvironment( + model=code_interpreter_mock_model(), + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=60), + ), + ) as env: + client = env.applied_on_client(client) + + async with new_worker( + client, + CodeInterpreterWorkflow, + ) as worker: + workflow_handle = await client.start_workflow( + CodeInterpreterWorkflow.run, + "What is the square root of273 * 312821 plus 1782?", + id=f"code-interpreter-tool-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=60), + ) + result = await workflow_handle.result() + assert result == "Over 9000" -class HostedMCPModel(StaticTestModel): - responses = [ - ModelResponse( - output=[ - McpApprovalRequest( - arguments="", - name="", - server_label="gitmcp", - type="mcp_approval_request", - id="id", - ) - ], - usage=Usage(), - response_id=None, - ), - ModelResponse( - output=[ - McpCall( - arguments="", - name="", - server_label="", - type="mcp_call", - id="id", - output="Mcp output", - ), - ResponseBuilders.response_output_message("Some language"), - ], - usage=Usage(), - response_id=None, - ), - ] +def hosted_mcp_mock_model(): + return TestModel.returning_responses( + [ + ModelResponse( + output=[ + McpApprovalRequest( + arguments="", + name="", + server_label="gitmcp", + type="mcp_approval_request", + id="id", + ) + ], + usage=Usage(), + response_id=None, + ), + ModelResponse( + output=[ + McpCall( + arguments="", + name="", + server_label="", + type="mcp_call", + id="id", + output="Mcp output", + ), + ResponseBuilders.response_output_message("Some language"), + ], + usage=Usage(), + response_id=None, + ), + ] + ) @workflow.defn @@ -2057,51 +1982,111 @@ def approve(_: MCPToolApprovalRequest) -> MCPToolApprovalFunctionResult: async def test_hosted_mcp_tool(client: Client): - new_config = client.config() - new_config["plugins"] = [ - openai_agents.OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=120) + async with AgentEnvironment( + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=120) + ), + model=hosted_mcp_mock_model(), + ) as env: + client = env.applied_on_client(client) + + async with new_worker( + client, + HostedMCPWorkflow, + ) as worker: + workflow_handle = await client.start_workflow( + HostedMCPWorkflow.run, + "Which language is this repo written in?", + id=f"hosted-mcp-tool-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=120), + ) + result = await workflow_handle.result() + assert result == "Some language" + + +def custom_tool_mock_model(): + return TestModel.returning_responses( + [ + ModelResponse( + output=[ + ResponseCustomToolCall( + call_id="c1", + input="ping", + name="echo", + type="custom_tool_call", + ) + ], + usage=Usage(), + response_id=None, ), - model_provider=TestModelProvider(HostedMCPModel()), - ) - ] - client = Client(**new_config) + ResponseBuilders.output_message("done"), + ] + ) - async with new_worker( - client, - HostedMCPWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - HostedMCPWorkflow.run, - "Which language is this repo written in?", - id=f"hosted-mcp-tool-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=120), + +@workflow.defn +class CustomToolWorkflow: + @workflow.run + async def run(self) -> str: + captured: list[str] = [] + + async def echo(ctx: ToolContext[Any], input: str) -> str: # type: ignore[reportUnusedParameter] + captured.append(input) + return input + + agent = Agent[str]( + name="custom-tool-agent", + instructions="Use the echo tool.", + tools=[ + CustomTool( + name="echo", + description="Echo the input string back.", + on_invoke_tool=echo, + ) + ], ) - result = await workflow_handle.result() - assert result == "Some language" + result = await Runner.run(starting_agent=agent, input="say something") + return f"{result.final_output}:{captured[0]}" + + +async def test_custom_tool_workflow(client: Client): + async with AgentEnvironment(model=custom_tool_mock_model()) as env: + client = env.applied_on_client(client) + + async with new_worker(client, CustomToolWorkflow) as worker: + workflow_handle = await client.start_workflow( + CustomToolWorkflow.run, + id=f"custom-tool-workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=30), + ) + result = await workflow_handle.result() + assert result == "done:ping" class AssertDifferentModelProvider(ModelProvider): - model_names: set[Optional[str]] + model_names: set[str | None] def __init__(self, model: Model): self._model = model self.model_names = set() - def get_model(self, model_name: Union[str, None]) -> Model: + def get_model(self, model_name: str | None) -> Model: self.model_names.add(model_name) return self._model -class MultipleModelsModel(StaticTestModel): - responses = [ - ResponseBuilders.tool_call("{}", "transfer_to_underling"), - ResponseBuilders.output_message( - "I'm here to help! Was there a specific task you needed assistance with regarding the storeroom?" - ), - ] +MULTIPLE_MODELS_FINAL_RESPONSE = "I'm here to help! Was there a specific task you needed assistance with regarding the storeroom?" + + +def multiple_models_mock_model(): + return TestModel.returning_responses( + [ + ResponseBuilders.tool_call("{}", "transfer_to_underling"), + ResponseBuilders.output_message(MULTIPLE_MODELS_FINAL_RESPONSE), + ] + ) @workflow.defn @@ -2128,100 +2113,169 @@ async def run(self, use_run_config: bool): async def test_multiple_models(client: Client): - provider = AssertDifferentModelProvider(MultipleModelsModel()) - new_config = client.config() - new_config["plugins"] = [ - openai_agents.OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=120) - ), - model_provider=provider, - ) - ] - client = Client(**new_config) - - async with new_worker( - client, - MultipleModelWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - MultipleModelWorkflow.run, - False, - id=f"multiple-model-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=10), - ) - result = await workflow_handle.result() - assert provider.model_names == {None, "gpt-4o-mini"} + provider = AssertDifferentModelProvider(multiple_models_mock_model()) + async with AgentEnvironment( + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=120) + ), + model_provider=provider, + ) as env: + client = env.applied_on_client(client) + + async with new_worker( + client, + MultipleModelWorkflow, + ) as worker: + workflow_handle = await client.start_workflow( + MultipleModelWorkflow.run, + False, + id=f"multiple-model-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + await workflow_handle.result() + assert provider.model_names == {None, "gpt-4o-mini"} async def test_run_config_models(client: Client): - provider = AssertDifferentModelProvider(MultipleModelsModel()) - new_config = client.config() - new_config["plugins"] = [ - openai_agents.OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=120) - ), - model_provider=provider, + provider = AssertDifferentModelProvider(multiple_models_mock_model()) + async with AgentEnvironment( + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=120) + ), + model_provider=provider, + ) as env: + client = env.applied_on_client(client) + + async with new_worker( + client, + MultipleModelWorkflow, + ) as worker: + workflow_handle = await client.start_workflow( + MultipleModelWorkflow.run, + True, + id=f"run-config-model-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + await workflow_handle.result() + + # Only the model from the runconfig override is used + assert provider.model_names == {"gpt-4o"} + + +@workflow.defn +class DictRunConfigWorkflow: + """Same agents as MultipleModelWorkflow, but passes run_config as a plain + dict, which openai-agents >= 0.19 accepts at its public runner boundaries.""" + + @workflow.run + async def run(self) -> str: + underling = Agent[None]( + name="Underling", + instructions="You do all the work you are told.", ) - ] - client = Client(**new_config) - async with new_worker( - client, - MultipleModelWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - MultipleModelWorkflow.run, - True, - id=f"run-config-model-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=10), + starting_agent = Agent[None]( + name="Lazy Assistant", + model="gpt-4o-mini", + instructions="You delegate all your work to another agent.", + handoffs=[underling], ) - result = await workflow_handle.result() + # Typed as Any so this also type-checks against openai-agents + # versions whose run_config annotation does not include dict. + dict_run_config: Any = {"model": "gpt-4o"} + result = await Runner.run( + starting_agent=starting_agent, + input="Have you cleaned the store room yet?", + run_config=dict_run_config, + ) + return result.final_output + + +async def test_dict_run_config_models(client: Client): + # A dict run_config must behave identically to the equivalent + # RunConfig(model="gpt-4o") in test_run_config_models above. + provider = AssertDifferentModelProvider(multiple_models_mock_model()) + async with AgentEnvironment( + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=120) + ), + model_provider=provider, + ) as env: + client = env.applied_on_client(client) + + async with new_worker( + client, + DictRunConfigWorkflow, + ) as worker: + workflow_handle = await client.start_workflow( + DictRunConfigWorkflow.run, + id=f"dict-run-config-model-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + result = await workflow_handle.result() + + # Only the model from the runconfig override is used + assert provider.model_names == {"gpt-4o"} + assert result == MULTIPLE_MODELS_FINAL_RESPONSE + + +def test_coerce_run_config_validation(): + # Mirrors upstream agents' normalization: equivalent RunConfig out of a + # dict, and the same TypeErrors for invalid input. + coerced = _coerce_run_config({"model": "gpt-4o", "workflow_name": "wf"}) + assert isinstance(coerced, RunConfig) + assert coerced.model == "gpt-4o" + assert coerced.workflow_name == "wf" + + run_config = RunConfig(model="gpt-4o") + assert _coerce_run_config(run_config) is run_config + + with pytest.raises(TypeError, match="Unknown run_config settings: bogus_setting"): + _coerce_run_config({"model": "gpt-4o", "bogus_setting": True}) - # Only the model from the runconfig override is used - assert provider.model_names == {"gpt-4o"} + with pytest.raises( + TypeError, match="run_config must be a RunConfig instance or a dict, got int" + ): + _coerce_run_config(42) async def test_summary_provider(client: Client): class SummaryProvider(ModelSummaryProvider): def provide( self, - agent: Optional[Agent[Any]], - instructions: Optional[str], - input: Union[str, list[TResponseInputItem]], + agent: Agent[Any] | None, + instructions: str | None, + input: str | list[TResponseInputItem], ) -> str: return "My summary" - new_config = client.config() - new_config["plugins"] = [ - openai_agents.OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=120), - summary_override=SummaryProvider(), - ), - model_provider=TestModelProvider(TestHelloModel()), - ) - ] - client = Client(**new_config) - - async with new_worker( - client, - HelloWorldAgent, - ) as worker: - workflow_handle = await client.start_workflow( - HelloWorldAgent.run, - "Prompt", - id=f"summary-provider-model-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=10), - ) - result = await workflow_handle.result() - async for e in workflow_handle.fetch_history_events(): - if e.HasField("activity_task_scheduled_event_attributes"): - assert e.user_metadata.summary.data == b'"My summary"' + async with AgentEnvironment( + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=120), + summary_override=SummaryProvider(), + ), + model=hello_mock_model(), + ) as env: + client = env.applied_on_client(client) + + async with new_worker( + client, + HelloWorldAgent, + ) as worker: + workflow_handle = await client.start_workflow( + HelloWorldAgent.run, + "Prompt", + id=f"summary-provider-model-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + await workflow_handle.result() + async for e in workflow_handle.fetch_history_events(): + if e.HasField("activity_task_scheduled_event_attributes"): + assert e.user_metadata.summary.data == b'"My summary"' class OutputType(pydantic.BaseModel): @@ -2245,36 +2299,623 @@ async def run(self) -> OutputType: return result.final_output -class OutputTypeModel(StaticTestModel): - responses = [ - ResponseBuilders.output_message( - '{"answer": "My answer"}', - ), - ] +def output_type_mock_model(): + return TestModel.returning_responses( + [ + ResponseBuilders.output_message( + '{"answer": "My answer"}', + ), + ] + ) async def test_output_type(client: Client): + async with AgentEnvironment( + model=output_type_mock_model(), + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=120), + ), + ) as env: + client = env.applied_on_client(client) + + async with new_worker( + client, + OutputTypeWorkflow, + ) as worker: + workflow_handle = await client.start_workflow( + OutputTypeWorkflow.run, + id=f"output-type-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + result = await workflow_handle.result() + assert isinstance(result, OutputType) + assert result.answer == "My answer" + + +@workflow.defn +class McpServerWorkflow: + @workflow.run + async def run(self, caching: bool, factory_argument: Any | None) -> str: + from agents.mcp import MCPServer # type: ignore[reportUnusedImport] + + server: MCPServer = openai_agents.workflow.stateless_mcp_server( + "HelloServer", cache_tools_list=caching, factory_argument=factory_argument + ) + agent = Agent[str]( + name="MCP ServerWorkflow", + instructions="Use the tools to assist the customer.", + mcp_servers=[server], + ) + result = await Runner.run( + starting_agent=agent, input="Say hello to Tom and Tim." + ) + return result.final_output + + +@workflow.defn +class McpServerStatefulWorkflow: + @workflow.run + async def run(self, timeout: timedelta, factory_argument: Any | None) -> str: + async with openai_agents.workflow.stateful_mcp_server( + "HelloServer", + config=ActivityConfig( + schedule_to_start_timeout=timeout, + start_to_close_timeout=timedelta(seconds=30), + ), + factory_argument=factory_argument, + ) as server: + agent = Agent[str]( + name="MCP ServerWorkflow", + instructions="Use the tools to assist the customer.", + mcp_servers=[server], + ) + result = await Runner.run( + starting_agent=agent, input="Say hello to Tom and Tim." + ) + return result.final_output + + +def tracking_mcp_mock_model(): + return TestModel.returning_responses( + [ + ResponseBuilders.tool_call( + arguments='{"name":"Tom"}', + name="Say-Hello", + ), + ResponseBuilders.tool_call( + arguments='{"name":"Tim"}', + name="Say-Hello", + ), + ResponseBuilders.output_message("Hi Tom and Tim!"), + ] + ) + + +def get_tracking_server(name: str): + from agents.mcp import MCPServer # type: ignore + from mcp import GetPromptResult, ListPromptsResult # type: ignore + from mcp import Tool as MCPTool # type: ignore + from mcp.types import CallToolResult, TextContent # type: ignore + + class TrackingMCPServer(MCPServer): + calls: list[str] + + def __init__(self, name: str): + self._name = name + self.calls = [] + super().__init__() + + async def connect(self): + self.calls.append("connect") + + @property + def name(self) -> str: + return self._name + + async def cleanup(self): + self.calls.append("cleanup") + + async def list_tools( + self, + run_context: RunContextWrapper[Any] | None = None, + agent: AgentBase | None = None, + ) -> list[MCPTool]: + self.calls.append("list_tools") + return [ + MCPTool( + name="Say-Hello", + inputSchema={ + "type": "object", + "properties": { + "name": {"type": "string"}, + }, + "required": ["name"], + "$schema": "http://json-schema.org/draft-07/schema#", + }, + ) + ] + + async def call_tool( + self, + tool_name: str, + arguments: dict[str, Any] | None, + meta: dict[str, Any] | None = None, + ) -> CallToolResult: + self.calls.append("call_tool") + name = (arguments or {}).get("name") or "John Doe" + return CallToolResult( + content=[TextContent(type="text", text=f"Hello {name}")] + ) + + async def list_prompts(self) -> ListPromptsResult: + raise NotImplementedError() + + async def get_prompt( + self, name: str, arguments: dict[str, Any] | None = None + ) -> GetPromptResult: + raise NotImplementedError() + + return TrackingMCPServer(name) + + +@pytest.mark.parametrize("use_local_model", [True, False]) +@pytest.mark.parametrize("stateful", [True, False]) +@pytest.mark.parametrize("caching", [True, False]) +async def test_mcp_server( + client: Client, use_local_model: bool, stateful: bool, caching: bool +): + if not use_local_model and not os.environ.get("OPENAI_API_KEY"): + pytest.skip("No openai API key") + + if stateful and caching: + pytest.skip("Caching is only supported for stateless MCP servers") + + from agents.mcp import MCPServer # type: ignore + + from temporalio.contrib.openai_agents import ( + StatefulMCPServerProvider, + StatelessMCPServerProvider, + ) + + tracking_server = get_tracking_server(name="HelloServer") + server: StatefulMCPServerProvider | StatelessMCPServerProvider = ( + StatefulMCPServerProvider("HelloServer", lambda _: tracking_server) + if stateful + else StatelessMCPServerProvider("HelloServer", lambda _: tracking_server) + ) + + model = tracking_mcp_mock_model() if use_local_model else None + async with AgentEnvironment( + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=120) + ), + model=model, + mcp_server_providers=[server], + ) as env: + client = env.applied_on_client(client) + + async with new_worker( + client, McpServerStatefulWorkflow, McpServerWorkflow + ) as worker: + if stateful: + result = await client.execute_workflow( + McpServerStatefulWorkflow.run, + args=[timedelta(seconds=30), None], + id=f"mcp-server-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=30), + ) + else: + result = await client.execute_workflow( + McpServerWorkflow.run, + args=[caching, None], + id=f"mcp-server-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=30), + ) + if use_local_model: + assert result == "Hi Tom and Tim!" + if use_local_model: + print(tracking_server.calls) + if stateful: + assert tracking_server.calls == [ + "connect", + "list_tools", + "call_tool", + "list_tools", + "call_tool", + "list_tools", + "cleanup", + ] + assert len(cast(StatefulMCPServerProvider, server)._servers) == 0 + else: + if caching: + assert tracking_server.calls == [ + "connect", + "list_tools", + "cleanup", + "connect", + "call_tool", + "cleanup", + "connect", + "call_tool", + "cleanup", + ] + else: + assert tracking_server.calls == [ + "connect", + "list_tools", + "cleanup", + "connect", + "call_tool", + "cleanup", + "connect", + "list_tools", + "cleanup", + "connect", + "call_tool", + "cleanup", + "connect", + "list_tools", + "cleanup", + ] + + +@pytest.mark.parametrize("stateful", [True, False]) +async def test_mcp_server_factory_argument(client: Client, stateful: bool): + def factory(args: Any | None) -> MCPServer: + print("Invoking factory: ", args) + if args is not None: + assert args is not None + assert cast(dict[str, str], args).get("user") == "blah" + + return get_tracking_server("HelloServer") + + server: StatefulMCPServerProvider | StatelessMCPServerProvider = ( + StatefulMCPServerProvider("HelloServer", factory) + if stateful + else StatelessMCPServerProvider("HelloServer", factory) + ) + + async with AgentEnvironment( + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=120) + ), + model=tracking_mcp_mock_model(), + mcp_server_providers=[server], + ) as env: + client = env.applied_on_client(client) + + headers = {"user": "blah"} + async with new_worker( + client, McpServerStatefulWorkflow, McpServerWorkflow + ) as worker: + if stateful: + await client.execute_workflow( + McpServerStatefulWorkflow.run, + args=[timedelta(seconds=30), headers], + id=f"mcp-server-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=30), + ) + else: + await client.execute_workflow( + McpServerWorkflow.run, + args=[False, headers], + id=f"mcp-server-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=30), + ) + + +async def test_stateful_mcp_server_no_worker(client: Client): + server = StatefulMCPServerProvider( + "Filesystem-Server", + lambda _: MCPServerStdio( + name="Filesystem-Server", + params={ + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-filesystem", + os.path.dirname(os.path.abspath(__file__)), + ], + }, + ), + ) + + # Override the connect activity to not actually start a worker + @activity.defn(name="Filesystem-Server-stateful-connect") + async def connect() -> None: + await asyncio.sleep(30) + + def override_get_activities() -> Sequence[Callable]: + return (connect,) + + server.get_activities = override_get_activities # type:ignore + + async with AgentEnvironment( + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=120) + ), + model=tracking_mcp_mock_model(), + mcp_server_providers=[server], + ) as env: + client = env.applied_on_client(client) + + async with new_worker( + client, + McpServerStatefulWorkflow, + ) as worker: + workflow_handle = await client.start_workflow( + McpServerStatefulWorkflow.run, + args=[timedelta(seconds=1), None], + id=f"mcp-server-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=30), + ) + with pytest.raises(WorkflowFailureError) as err: + await workflow_handle.result() + assert isinstance(err.value.cause, ApplicationError) + assert ( + err.value.cause.message + == "MCP Stateful Server Worker failed to schedule activity." + ) + + +async def test_model_conversion_loops(): + agent = init_agents() + converted = _convert_agent(ModelActivityParameters(), agent, None) + seat_booking_handoff = converted.handoffs[1] + assert isinstance(seat_booking_handoff, Handoff) + context: RunContextWrapper[AirlineAgentContext] = RunContextWrapper( + context=AirlineAgentContext() # type: ignore + ) + seat_booking_agent = await seat_booking_handoff.on_invoke_handoff(context, "") + triage_agent = seat_booking_agent.handoffs[0] + assert isinstance(triage_agent, Agent) + assert isinstance(triage_agent.model, _TemporalModelStub) + seat_booking_agent = await seat_booking_handoff.on_invoke_handoff(context, "") + triage_agent = seat_booking_agent.handoffs[0] + assert isinstance(triage_agent, Agent) + assert isinstance(triage_agent.model, _TemporalModelStub) + seat_booking_agent = await seat_booking_handoff.on_invoke_handoff(context, "") + triage_agent = seat_booking_agent.handoffs[0] + assert isinstance(triage_agent, Agent) + assert isinstance(triage_agent.model, _TemporalModelStub) + + +def test_sandbox_apply_patch_tool_round_trips_through_activity_input(): + class FakeSandboxSession: + pass + + tool = SandboxApplyPatchTool(session=FakeSandboxSession()) # type: ignore[arg-type] + + stub = _TemporalModelStub( + model_name="gpt-5", + model_params=ModelActivityParameters(), + agent=None, + ) + + activity_input, _summary = stub._build_activity_input( + system_instructions=None, + input="hi", + model_settings=ModelSettings(), + tools=[tool], + output_schema=None, + handoffs=[], + tracing=ModelTracing.DISABLED, + previous_response_id=None, + conversation_id=None, + prompt=None, + ) + + tool_inputs = activity_input.get("tools") or [] + assert len(tool_inputs) == 1 + rebuilt = _build_tool(tool_inputs[0]) + assert isinstance(rebuilt, CustomTool) + assert rebuilt.name == tool.name + assert rebuilt.description == tool.description + assert rebuilt.format == tool.format + assert rebuilt.tool_config == tool.tool_config + + +def test_custom_tool_with_defer_loading_round_trips_through_activity_input(): + async def stub(_ctx: Any, _payload: str) -> str: + return "" + + tool = CustomTool( + name="deferred_tool", + description="A custom tool with defer_loading enabled", + on_invoke_tool=stub, + defer_loading=True, + ) + + stub_model = _TemporalModelStub( + model_name="gpt-5", + model_params=ModelActivityParameters(), + agent=None, + ) + + activity_input, _summary = stub_model._build_activity_input( + system_instructions=None, + input="hi", + model_settings=ModelSettings(), + tools=[tool], + output_schema=None, + handoffs=[], + tracing=ModelTracing.DISABLED, + previous_response_id=None, + conversation_id=None, + prompt=None, + ) + + tool_inputs = activity_input.get("tools") or [] + assert len(tool_inputs) == 1 + rebuilt = _build_tool(tool_inputs[0]) + assert isinstance(rebuilt, CustomTool) + assert rebuilt.tool_config == tool.tool_config + assert rebuilt.defer_loading is True + + +async def test_local_hello_world_agent(client: Client): + async with AgentEnvironment( + model=hello_mock_model(), + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=30), + use_local_activity=True, + ), + ) as env: + client = env.applied_on_client(client) + + async with new_worker(client, HelloWorldAgent) as worker: + handle = await client.start_workflow( + HelloWorldAgent.run, + "Tell me about recursion in programming.", + id=f"hello-workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=5), + ) + result = await handle.result() + assert result == "test" + local_activity_found = False + async for e in handle.fetch_history_events(): + if e.HasField("marker_recorded_event_attributes"): + local_activity_found = True + assert local_activity_found + + +async def test_split_workers(client: Client): new_config = client.config() - new_config["plugins"] = [ - openai_agents.OpenAIAgentsPlugin( + + workflow_plugin = openai_agents.OpenAIAgentsPlugin( + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=30) + ), + model_provider=TestModelProvider(hello_mock_model()), + register_activities=False, + ) + new_config["plugins"] = [workflow_plugin] + workflow_client = Client(**new_config) + + # Workflow worker + async with new_worker( + workflow_client, HelloWorldAgent, no_remote_activities=True + ) as worker: + activity_plugin = openai_agents.OpenAIAgentsPlugin( model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=120), + start_to_close_timeout=timedelta(seconds=30) ), - model_provider=TestModelProvider(OutputTypeModel()), + model_provider=TestModelProvider(hello_mock_model()), + ) + new_config["plugins"] = [activity_plugin] + activity_client = Client(**new_config) + # Activity Worker + async with new_worker(activity_client, task_queue=worker.task_queue): + result = await activity_client.execute_workflow( + HelloWorldAgent.run, + "Tell me about recursion in programming.", + id=f"hello-workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=120), + ) + assert result == "test" + + +def multiple_handoffs_mock_model(): + return TestModel.returning_responses( + [ + ResponseBuilders.tool_call("{}", "transfer_to_planner"), + ResponseBuilders.output_message( + "I'll analyze the requirements and create a plan." + ), + ] + ) + + +@workflow.defn +class MultipleHandoffsWorkflow: + @workflow.run + async def run(self, task: str) -> str: + planner = Agent[None]( + name="Planner", + instructions="You analyze requirements and create detailed plans.", + handoff_description="An agent that creates detailed plans and strategies", ) - ] - client = Client(**new_config) - async with new_worker( - client, - OutputTypeWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - OutputTypeWorkflow.run, - id=f"output-type-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=10), + writer = Agent[None]( + name="Writer", + instructions="You write documents and reports based on provided information.", + handoff_description="An agent that writes professional documents and reports", + ) + + specialists = [planner, writer] + handoffs_list: list[Agent[Any] | Handoff[None, Any]] = [ + handoff(agent=a) for a in specialists + ] + + triage = Agent[None]( + name="Triage", + instructions="Hand off to Planner when requested.", + handoffs=handoffs_list, ) - result = await workflow_handle.result() - assert isinstance(result, OutputType) - assert result.answer == "My answer" + + result = await Runner.run(starting_agent=triage, input=task) + return result.final_output + + +async def test_multiple_handoffs_workflow(client: Client): + model = multiple_handoffs_mock_model() + async with AgentEnvironment( + model=model, + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=30), + ), + ) as env: + client = env.applied_on_client(client) + + async with new_worker( + client, + MultipleHandoffsWorkflow, + ) as worker: + workflow_handle = await client.start_workflow( + MultipleHandoffsWorkflow.run, + "Create a project plan for building a web application", + id=f"multiple-handoffs-workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=30), + ) + result = await workflow_handle.result() + + assert result == "I'll analyze the requirements and create a plan." + + # Verify the correct handoff occurred + events = [] + async for e in workflow_handle.fetch_history_events(): + if e.HasField("activity_task_completed_event_attributes"): + events.append(e) + + # Should have 2 activity completions: + # 1. Triage agent makes handoff call to planner + # 2. Planner agent responds + assert len(events) == 2 + + # Verify handoff to planner was requested + first_event_data = ( + events[0] + .activity_task_completed_event_attributes.result.payloads[0] + .data.decode() + ) + assert "transfer_to_planner" in first_event_data + + # Verify that the planner agent was actually invoked (this would fail before the fix) + planner_response_data = ( + events[1] + .activity_task_completed_event_attributes.result.payloads[0] + .data.decode() + ) + assert ( + "I'll analyze the requirements and create a plan." + in planner_response_data + ) diff --git a/tests/contrib/openai_agents/test_openai_replay.py b/tests/contrib/openai_agents/test_openai_replay.py index d625343b8..6db463392 100644 --- a/tests/contrib/openai_agents/test_openai_replay.py +++ b/tests/contrib/openai_agents/test_openai_replay.py @@ -3,7 +3,7 @@ import pytest from temporalio.client import WorkflowHistory -from temporalio.contrib.openai_agents import ModelActivityParameters, OpenAIAgentsPlugin +from temporalio.contrib.openai_agents import OpenAIAgentsPlugin from temporalio.worker import Replayer from tests.contrib.openai_agents.test_openai import ( AgentsAsToolsWorkflow, diff --git a/tests/contrib/openai_agents/test_openai_sandbox.py b/tests/contrib/openai_agents/test_openai_sandbox.py new file mode 100644 index 000000000..3338f8d64 --- /dev/null +++ b/tests/contrib/openai_agents/test_openai_sandbox.py @@ -0,0 +1,941 @@ +"""Tests for sandbox validation in TemporalOpenAIRunner.""" + +import io +import uuid +from datetime import timedelta +from pathlib import Path +from typing import Any, Literal + +import pytest +from agents import Agent, FunctionTool, RunConfig, Runner, Tool +from agents.sandbox import Capability, Manifest, SandboxAgent, SandboxRunConfig +from agents.sandbox.errors import ( + ExecTransportError, + SandboxError, + WorkspaceArchiveReadError, +) +from agents.sandbox.session.base_sandbox_session import BaseSandboxSession +from agents.sandbox.session.sandbox_client import ( + BaseSandboxClient, + BaseSandboxClientOptions, +) +from agents.sandbox.session.sandbox_session import SandboxSession +from agents.sandbox.session.sandbox_session_state import SandboxSessionState +from agents.sandbox.snapshot import NoopSnapshot +from agents.sandbox.types import ExecResult +from pydantic import TypeAdapter +from pydantic_core import to_json + +from temporalio import workflow +from temporalio.client import Client +from temporalio.contrib.openai_agents import ( + ModelActivityParameters, + OpenAIAgentsPlugin, + SandboxClientProvider, +) +from temporalio.contrib.openai_agents._openai_runner import _has_sandbox_agent +from temporalio.contrib.openai_agents.sandbox._temporal_activity_models import ( + CreateSessionArgs, + ExecArgs, + HydrateWorkspaceArgs, + PersistWorkspaceResult, + PtyExecUpdateResult, + ReadArgs, + ReadResult, + ResumeSessionArgs, + RunningArgs, + StopArgs, + WriteArgs, +) +from temporalio.contrib.openai_agents.sandbox._temporal_activity_models import ( + ExecResult as ExecResultModel, +) +from temporalio.contrib.openai_agents.sandbox._temporal_sandbox_client import ( + TemporalSandboxClient, +) +from temporalio.contrib.openai_agents.testing import ( + AgentEnvironment, + ResponseBuilders, + TestModel, + TestModelProvider, +) +from temporalio.contrib.openai_agents.workflow import temporal_sandbox_client +from temporalio.exceptions import ApplicationError +from temporalio.workflow import ActivityConfig +from tests.helpers import new_worker + +# ── _has_sandbox_agent unit tests ── + + +def test_has_sandbox_agent_regular_agent(): + assert _has_sandbox_agent(Agent[None](name="regular")) is False + + +def test_has_sandbox_agent_sandbox_starting(): + assert _has_sandbox_agent(SandboxAgent[None](name="sandbox")) is True + + +def test_has_sandbox_agent_sandbox_direct_handoff(): + sandbox = SandboxAgent[None](name="sandbox") + regular = Agent[None](name="regular", handoffs=[sandbox]) + assert _has_sandbox_agent(regular) is True + + +def test_has_sandbox_agent_sandbox_deep_handoff(): + sandbox = SandboxAgent[None](name="sandbox") + middle = Agent[None](name="middle", handoffs=[sandbox]) + top = Agent[None](name="top", handoffs=[middle]) + assert _has_sandbox_agent(top) is True + + +def test_has_sandbox_agent_no_sandbox_in_chain(): + c = Agent[None](name="c") + b = Agent[None](name="b", handoffs=[c]) + a = Agent[None](name="a", handoffs=[b]) + assert _has_sandbox_agent(a) is False + + +def test_has_sandbox_agent_circular_no_sandbox(): + a: Agent[Any] = Agent[None](name="a") + b: Agent[Any] = Agent[None](name="b", handoffs=[a]) + a.handoffs = [b] + assert _has_sandbox_agent(a) is False + + +def test_has_sandbox_agent_circular_with_sandbox(): + sandbox = SandboxAgent[None](name="sandbox") + a: Agent[Any] = Agent[None](name="a", handoffs=[sandbox]) + b: Agent[Any] = Agent[None](name="b", handoffs=[a]) + a.handoffs = [b, sandbox] + assert _has_sandbox_agent(b) is True + + +# ── temporal_sandbox_client helper tests ── + + +def test_temporal_sandbox_client_returns_temporal_client(): + client = temporal_sandbox_client("my-backend") + assert isinstance(client, TemporalSandboxClient) + assert client._name == "my-backend" + assert client.backend_id == "my-backend" + + +def test_temporal_sandbox_client_with_config(): + config = ActivityConfig(start_to_close_timeout=timedelta(minutes=10)) + client = temporal_sandbox_client("my-backend", config=config) + assert isinstance(client, TemporalSandboxClient) + assert client._config == config + + +# ── Workflow validation tests ── + + +def _mock_model(): + return TestModel.returning_responses([ResponseBuilders.output_message("test")]) + + +@workflow.defn +class SandboxValidationWorkflow: + """Single workflow that validates all sandbox configuration error cases.""" + + @workflow.run + async def run(self) -> str: + # Case 1: SandboxAgent without run_config.sandbox + try: + agent = SandboxAgent[None](name="sandbox") + await Runner.run(starting_agent=agent, input="hello") + return "FAIL: no-config should have raised" + except ValueError as e: + assert "run_config.sandbox is not configured" in str(e) + + # Case 2: SandboxAgent reachable via handoff without run_config.sandbox + try: + sandbox = SandboxAgent[None](name="sandbox_target") + router = Agent[None](name="router", handoffs=[sandbox]) + await Runner.run(starting_agent=router, input="hello") + return "FAIL: handoff-no-config should have raised" + except ValueError as e: + assert "run_config.sandbox is not configured" in str(e) + + # Case 3: SandboxRunConfig with client=None + try: + agent = SandboxAgent[None](name="sandbox") + await Runner.run( + starting_agent=agent, + input="hello", + run_config=RunConfig( + sandbox=SandboxRunConfig(client=None), # type: ignore[arg-type] + ), + ) + return "FAIL: null-client should have raised" + except ValueError as e: + assert "run_config.sandbox.client must be set" in str(e) + + # Case 4: Non-TemporalSandboxClient in run_config.sandbox.client + try: + agent = SandboxAgent[None](name="sandbox") + await Runner.run( + starting_agent=agent, + input="hello", + run_config=RunConfig( + sandbox=SandboxRunConfig(client=object()), # type: ignore[arg-type] + ), + ) + return "FAIL: wrong-client should have raised" + except ValueError as e: + assert "temporal_sandbox_client(name)" in str(e) + + return "OK" + + +async def test_sandbox_validation_errors(client: Client): + """All sandbox configuration errors should be caught immediately in the workflow.""" + async with AgentEnvironment(model=_mock_model()) as env: + client = env.applied_on_client(client) + async with new_worker( + client, + SandboxValidationWorkflow, + workflow_failure_exception_types=[ValueError, AssertionError], + ) as worker: + result = await client.execute_workflow( + SandboxValidationWorkflow.run, + id=f"sandbox-validation-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + assert result == "OK" + + +# ── Mock sandbox infrastructure for delegation tests ── + + +class TestSessionState(SandboxSessionState): + """Concrete ``SandboxSessionState`` subclass for tests that don't need a real backend.""" + + __test__ = False + type: Literal["test"] = "test" # type: ignore + + +class _MockSandboxSession(BaseSandboxSession): + """Minimal mock session that tracks calls and returns canned results.""" + + def __init__(self, manifest: Manifest | None = None) -> None: + self.state = TestSessionState( + manifest=manifest or Manifest(), + snapshot=NoopSnapshot(id=str(uuid.uuid4())), + ) + self.exec_calls: list[tuple] = [] + self.read_calls: list[Path] = [] + self.write_calls: list[tuple[Path, bytes]] = [] + self.running_calls: int = 0 + self.start_calls: int = 0 + self.stop_calls: int = 0 + self.shutdown_calls: int = 0 + self.persist_workspace_calls: int = 0 + self.hydrate_workspace_calls: int = 0 + + async def start(self) -> None: + self.start_calls += 1 + + async def stop(self) -> None: + self.stop_calls += 1 + + async def shutdown(self) -> None: + self.shutdown_calls += 1 + + async def running(self) -> bool: + self.running_calls += 1 + return True + + async def _exec_internal( + self, + *command: str | Path, + timeout: float | None = None, + ) -> ExecResult: + self.exec_calls.append((command, timeout)) + return ExecResult(stdout=b"ok\n", stderr=b"", exit_code=0) + + async def read(self, path: Path, *, user: Any = None) -> io.IOBase: # type: ignore[reportUnusedParameter] + self.read_calls.append(path) + return io.BytesIO(b"file-content") + + async def write(self, path: Path, data: io.IOBase, *, user: Any = None) -> None: # type: ignore[reportUnusedParameter] + self.write_calls.append((path, data.read())) + + async def persist_workspace(self) -> io.IOBase: + self.persist_workspace_calls += 1 + return io.BytesIO(b"workspace-archive") + + async def hydrate_workspace(self, data: io.IOBase) -> None: + self.hydrate_workspace_calls += 1 + + def supports_pty(self) -> bool: + return False + + +class _MockSandboxClient(BaseSandboxClient[BaseSandboxClientOptions | None]): + """Mock client that tracks create/resume/delete calls and delegates to a mock session.""" + + backend_id = "mock" + supports_default_options = True + + def __init__(self, session: _MockSandboxSession | None = None) -> None: + self.inner_session = session or _MockSandboxSession() + self.session = self._wrap_session(self.inner_session) + self.create_calls: int = 0 + self.resume_calls: int = 0 + self.delete_calls: int = 0 + + async def create( + self, + *, + snapshot: Any = None, + manifest: Manifest | None = None, + options: BaseSandboxClientOptions | None = None, + ) -> SandboxSession: + self.create_calls += 1 + if manifest is not None: + self.inner_session.state.manifest = manifest + return self.session + + async def resume(self, state: SandboxSessionState) -> SandboxSession: + self.resume_calls += 1 + self.inner_session.state = state + return self.session + + async def delete(self, session: SandboxSession) -> SandboxSession: + self.delete_calls += 1 + return session + + def deserialize_session_state(self, payload: dict[str, Any]) -> SandboxSessionState: + return SandboxSessionState.model_validate(payload) + + +# ── SandboxClientProvider unit tests (delegation) ── + + +@pytest.fixture +def mock_client() -> _MockSandboxClient: + return _MockSandboxClient() + + +@pytest.fixture +def sandbox_activities(mock_client: _MockSandboxClient) -> SandboxClientProvider: + return SandboxClientProvider("mock", mock_client) + + +def _make_state(manifest: Manifest | None = None) -> TestSessionState: + return TestSessionState( + manifest=manifest or Manifest(), + snapshot=NoopSnapshot(id=str(uuid.uuid4())), + ) + + +def _activity_map( + sandbox_activities: SandboxClientProvider, +) -> dict[str, Any]: + """Build a short-name → callable dict from all() for easy test dispatch.""" + return { + act.__temporal_activity_definition.name: act # type: ignore[attr-defined, union-attr] + for act in sandbox_activities._get_activities() + } + + +async def test_activities_create_session_delegates( + sandbox_activities: SandboxClientProvider, + mock_client: _MockSandboxClient, +): + """create_session activity should delegate to the real client's create().""" + acts = _activity_map(sandbox_activities) + args = CreateSessionArgs( + snapshot_spec=None, + manifest=Manifest(), + client_options=None, + ) + result = await acts["mock-sandbox_client_create"](args) + assert mock_client.create_calls == 1 + assert result.state is not None + assert isinstance(result.supports_pty, bool) + + +async def test_activities_resume_session_delegates( + sandbox_activities: SandboxClientProvider, + mock_client: _MockSandboxClient, +): + """resume_session activity should delegate to the real client's resume().""" + acts = _activity_map(sandbox_activities) + state = _make_state() + args = ResumeSessionArgs(state=state) + result = await acts["mock-sandbox_client_resume"](args) + assert mock_client.resume_calls == 1 + assert result.state is not None + + +async def test_activities_exec_delegates( + sandbox_activities: SandboxClientProvider, + mock_client: _MockSandboxClient, +): + """exec activity should delegate to the real session's exec().""" + acts = _activity_map(sandbox_activities) + # First create a session so the activities cache is populated + await acts["mock-sandbox_client_create"]( + CreateSessionArgs(snapshot_spec=None, manifest=Manifest(), client_options=None) + ) + state = mock_client.inner_session.state + + args = ExecArgs(state=state, command=["echo", "hello"], timeout=10.0, shell=True) + result = await acts["mock-sandbox_session_exec"](args) + assert result.stdout == b"ok\n" + assert result.stderr == b"" + assert result.exit_code == 0 + assert len(mock_client.inner_session.exec_calls) == 1 + + +async def test_activities_read_delegates( + sandbox_activities: SandboxClientProvider, + mock_client: _MockSandboxClient, +): + """read activity should delegate to the real session's read().""" + acts = _activity_map(sandbox_activities) + await acts["mock-sandbox_client_create"]( + CreateSessionArgs(snapshot_spec=None, manifest=Manifest(), client_options=None) + ) + state = mock_client.inner_session.state + + args = ReadArgs(state=state, path="/tmp/test.txt") + result = await acts["mock-sandbox_session_read"](args) + assert result.data == b"file-content" + assert len(mock_client.inner_session.read_calls) == 1 + + +async def test_activities_write_delegates( + sandbox_activities: SandboxClientProvider, + mock_client: _MockSandboxClient, +): + """write activity should delegate to the real session's write().""" + acts = _activity_map(sandbox_activities) + await acts["mock-sandbox_client_create"]( + CreateSessionArgs(snapshot_spec=None, manifest=Manifest(), client_options=None) + ) + state = mock_client.inner_session.state + + args = WriteArgs(state=state, path="/tmp/out.txt", data=b"written-data") + await acts["mock-sandbox_session_write"](args) + assert len(mock_client.inner_session.write_calls) == 1 + assert mock_client.inner_session.write_calls[0][1] == b"written-data" + + +async def test_activities_running_delegates( + sandbox_activities: SandboxClientProvider, + mock_client: _MockSandboxClient, +): + """running activity should delegate to the real session's running().""" + acts = _activity_map(sandbox_activities) + await acts["mock-sandbox_client_create"]( + CreateSessionArgs(snapshot_spec=None, manifest=Manifest(), client_options=None) + ) + state = mock_client.inner_session.state + + args = RunningArgs(state=state) + result = await acts["mock-sandbox_session_running"](args) + assert result.is_running is True + assert mock_client.inner_session.running_calls == 1 + + +async def test_activities_client_delete_delegates( + sandbox_activities: SandboxClientProvider, + mock_client: _MockSandboxClient, +): + """client_delete activity should delegate to the real client's delete().""" + acts = _activity_map(sandbox_activities) + await acts["mock-sandbox_client_create"]( + CreateSessionArgs(snapshot_spec=None, manifest=Manifest(), client_options=None) + ) + state = mock_client.inner_session.state + + args = StopArgs(state=state) + await acts["mock-sandbox_client_delete"](args) + + assert mock_client.delete_calls == 1 + + +async def test_activities_session_shutdown_clears_cache( + sandbox_activities: SandboxClientProvider, + mock_client: _MockSandboxClient, +): + """session_shutdown activity should call session.shutdown() and evict from cache.""" + acts = _activity_map(sandbox_activities) + await acts["mock-sandbox_client_create"]( + CreateSessionArgs(snapshot_spec=None, manifest=Manifest(), client_options=None) + ) + state = mock_client.inner_session.state + session_key = str(state.session_id) + + # Session should be cached + assert session_key in sandbox_activities._sessions + + args = StopArgs(state=state) + await acts["mock-sandbox_session_shutdown"](args) + + assert mock_client.inner_session.shutdown_calls == 1 + # Session should be evicted from cache + assert session_key not in sandbox_activities._sessions + + +async def test_activities_session_shutdown_noop_for_unknown_session( + sandbox_activities: SandboxClientProvider, +): + """session_shutdown should be a no-op if the session isn't in the cache.""" + acts = _activity_map(sandbox_activities) + state = _make_state() + args = StopArgs(state=state) + # Should not raise + await acts["mock-sandbox_session_shutdown"](args) + + +async def test_activities_session_caching( + sandbox_activities: SandboxClientProvider, + mock_client: _MockSandboxClient, +): + """Multiple operations on the same session should reuse the cached session.""" + acts = _activity_map(sandbox_activities) + await acts["mock-sandbox_client_create"]( + CreateSessionArgs(snapshot_spec=None, manifest=Manifest(), client_options=None) + ) + state = mock_client.inner_session.state + + # Multiple exec calls should not trigger additional resume calls + await acts["mock-sandbox_session_exec"]( + ExecArgs(state=state, command=["cmd1"], shell=True) + ) + await acts["mock-sandbox_session_exec"]( + ExecArgs(state=state, command=["cmd2"], shell=True) + ) + assert mock_client.resume_calls == 0 + assert len(mock_client.inner_session.exec_calls) == 2 + + +async def test_activities_all_returns_all_activity_methods( + sandbox_activities: SandboxClientProvider, +): + """all() should return all 14 activity callables with prefixed names.""" + activities = sandbox_activities._get_activities() + assert len(activities) == 14 + # Verify they are all activity-decorated callables with prefixed names + activity_names = set() + for act in activities: + assert hasattr(act, "__temporal_activity_definition") + activity_names.add(act.__temporal_activity_definition.name) # type: ignore[union-attr] + expected = { + "mock-sandbox_client_create", + "mock-sandbox_client_resume", + "mock-sandbox_client_delete", + "mock-sandbox_session_exec", + "mock-sandbox_session_read", + "mock-sandbox_session_write", + "mock-sandbox_session_running", + "mock-sandbox_session_persist_workspace", + "mock-sandbox_session_hydrate_workspace", + "mock-sandbox_session_pty_exec_start", + "mock-sandbox_session_pty_write_stdin", + "mock-sandbox_session_start", + "mock-sandbox_session_stop", + "mock-sandbox_session_shutdown", + } + assert activity_names == expected + + +async def test_multiple_providers_register_distinct_activities(): + """Multiple SandboxClientProviders should produce distinct prefixed activity sets.""" + client1 = _MockSandboxClient() + client2 = _MockSandboxClient() + provider1 = SandboxClientProvider("daytona", client1) + provider2 = SandboxClientProvider("local", client2) + + activities1 = provider1._get_activities() + activities2 = provider2._get_activities() + + names1 = {a.__temporal_activity_definition.name for a in activities1} # type: ignore + names2 = {a.__temporal_activity_definition.name for a in activities2} # type: ignore + + # No overlap + assert names1.isdisjoint(names2) + # Both have 14 activities + assert len(names1) == 14 + assert len(names2) == 14 + # Verify prefixes + assert all( + n.startswith("daytona-sandbox_client_") + or n.startswith("daytona-sandbox_session_") + for n in names1 + ) + assert all( + n.startswith("local-sandbox_client_") or n.startswith("local-sandbox_session_") + for n in names2 + ) + + +# ── SandboxError retryable mapping tests ── + + +class _ExecRaisingSession(_MockSandboxSession): + """Mock session whose exec() raises a chosen SandboxError.""" + + def __init__(self, error: SandboxError) -> None: + super().__init__() + self._error = error + + async def _exec_internal( + self, + *command: str | Path, # type: ignore[reportUnusedParameter] + timeout: float | None = None, # type: ignore[reportUnusedParameter] + ) -> ExecResult: + raise self._error + + +async def _exec_with_error(error: SandboxError) -> None: + provider = SandboxClientProvider( + "mock", _MockSandboxClient(_ExecRaisingSession(error)) + ) + acts = _activity_map(provider) + state = ( + await acts["mock-sandbox_client_create"]( + CreateSessionArgs( + snapshot_spec=None, manifest=Manifest(), client_options=None + ) + ) + ).state + await acts["mock-sandbox_session_exec"]( + ExecArgs(state=state, command=["boom"], shell=True) + ) + + +async def test_exec_terminal_error_becomes_non_retryable_application_error(): + """retryable is False should map to a non-retryable ApplicationError.""" + with pytest.raises(ApplicationError) as exc_info: + await _exec_with_error(ExecTransportError(command=["boom"], retryable=False)) + assert exc_info.value.non_retryable is True + assert exc_info.value.type == "exec_transport_error" + + +async def test_exec_transient_error_propagates_unchanged(): + """retryable is True should let the original SandboxError propagate.""" + with pytest.raises(ExecTransportError): + await _exec_with_error(ExecTransportError(command=["boom"], retryable=True)) + + +async def test_exec_unclassified_error_propagates_unchanged(): + """retryable is None should let the original SandboxError propagate (not converted).""" + with pytest.raises(ExecTransportError): + await _exec_with_error(ExecTransportError(command=["boom"], retryable=None)) + + +class _ShutdownRaisingSession(_MockSandboxSession): + """Mock session whose shutdown() raises a chosen SandboxError.""" + + def __init__(self, error: SandboxError) -> None: + super().__init__() + self._error = error + + async def shutdown(self) -> None: + raise self._error + + +async def _create_shutdown_raising( + error: SandboxError, +) -> tuple[dict[str, Any], SandboxClientProvider, StopArgs, str]: + provider = SandboxClientProvider( + "mock", _MockSandboxClient(_ShutdownRaisingSession(error)) + ) + acts = _activity_map(provider) + state = ( + await acts["mock-sandbox_client_create"]( + CreateSessionArgs( + snapshot_spec=None, manifest=Manifest(), client_options=None + ) + ) + ).state + key = str(state.session_id) + assert key in provider._sessions + return acts, provider, StopArgs(state=state), key + + +async def test_shutdown_terminal_error_evicts_session_and_raises(): + """A terminal shutdown error maps to a non-retryable ApplicationError and + evicts the dead session from the cache.""" + acts, provider, args, key = await _create_shutdown_raising( + ExecTransportError(command=["shutdown"], retryable=False) + ) + + with pytest.raises(ApplicationError) as exc_info: + await acts["mock-sandbox_session_shutdown"](args) + assert exc_info.value.non_retryable is True + assert key not in provider._sessions + + +async def test_shutdown_retryable_error_keeps_session_cached(): + """A retryable shutdown error propagates unchanged and leaves the session + cached so the activity's retry can still shut it down.""" + acts, provider, args, key = await _create_shutdown_raising( + ExecTransportError(command=["shutdown"], retryable=True) + ) + + with pytest.raises(ExecTransportError): + await acts["mock-sandbox_session_shutdown"](args) + assert key in provider._sessions + + +class _RunningRaisingSession(_MockSandboxSession): + """Mock session whose running() raises a chosen SandboxError.""" + + def __init__(self, error: SandboxError) -> None: + super().__init__() + self._error = error + + async def running(self) -> bool: + raise self._error + + +async def test_running_terminal_error_becomes_non_retryable_application_error(): + """A terminal SandboxError from a non-exec activity also maps to a + non-retryable ApplicationError, with type set to its error_code.""" + error = WorkspaceArchiveReadError(path=Path("/workspace"), retryable=False) + provider = SandboxClientProvider( + "mock", _MockSandboxClient(_RunningRaisingSession(error)) + ) + acts = _activity_map(provider) + state = ( + await acts["mock-sandbox_client_create"]( + CreateSessionArgs( + snapshot_spec=None, manifest=Manifest(), client_options=None + ) + ) + ).state + + with pytest.raises(ApplicationError) as exc_info: + await acts["mock-sandbox_session_running"](RunningArgs(state=state)) + assert exc_info.value.non_retryable is True + assert exc_info.value.type == "workspace_archive_read_error" + + +# ── End-to-end test: Runner + SandboxAgent through Temporal activities ── + + +class _TestSandboxCapability(Capability): + """Minimal capability exposing exec, read, and write via FunctionTools.""" + + def __init__(self) -> None: + super().__init__(type="test_sandbox") + self._session: BaseSandboxSession | None = None + + def bind(self, session: BaseSandboxSession) -> None: + self._session = session + + def tools(self) -> list[Tool]: + session = self._session + + async def _run_cmd(ctx: Any, args: str) -> str: # type: ignore[reportUnusedParameter] + import json + + cmd = json.loads(args)["cmd"] + result = await session.exec(cmd, shell=True) # type: ignore[union-attr] + return result.stdout.decode() + + async def _read_file(ctx: Any, args: str) -> str: # type: ignore[reportUnusedParameter] + import json + + path = json.loads(args)["path"] + handle = await session.read(Path(path)) # type: ignore[union-attr] + return handle.read().decode() + + async def _write_file(ctx: Any, args: str) -> str: # type: ignore[reportUnusedParameter] + import json + + parsed = json.loads(args) + await session.write( # type: ignore[union-attr] + Path(parsed["path"]), io.BytesIO(parsed["data"].encode()) + ) + return "ok" + + return [ + FunctionTool( + name="run_command", + description="Run a shell command", + params_json_schema={ + "type": "object", + "properties": {"cmd": {"type": "string"}}, + "required": ["cmd"], + }, + on_invoke_tool=_run_cmd, + ), + FunctionTool( + name="read_file", + description="Read a file", + params_json_schema={ + "type": "object", + "properties": {"path": {"type": "string"}}, + "required": ["path"], + }, + on_invoke_tool=_read_file, + ), + FunctionTool( + name="write_file", + description="Write a file", + params_json_schema={ + "type": "object", + "properties": { + "path": {"type": "string"}, + "data": {"type": "string"}, + }, + "required": ["path", "data"], + }, + on_invoke_tool=_write_file, + ), + ] + + +class _TestSandboxClientOptions(BaseSandboxClientOptions): + type: str = "test" # type: ignore[reportIncompatibleVariableOverride] + + +@workflow.defn +class SandboxE2EWorkflow: + @workflow.run + async def run(self) -> str: + agent = SandboxAgent[None]( + name="sandbox-e2e", capabilities=[_TestSandboxCapability()] + ) + result = await Runner.run( + starting_agent=agent, + input="run a command", + run_config=RunConfig( + sandbox=SandboxRunConfig( + client=temporal_sandbox_client("mock"), + options=_TestSandboxClientOptions(), + ), + ), + ) + return result.final_output + + +async def test_sandbox_e2e_runner(client: Client): + """End-to-end: Runner.run() with SandboxAgent exercises the full sandbox + lifecycle (create, start, stop, shutdown, delete) through Temporal activities.""" + mock_session = _MockSandboxSession() + mock_sandbox_client = _MockSandboxClient(mock_session) + + mock_model = TestModel.returning_responses( + [ + ResponseBuilders.tool_call('{"cmd": "echo hello"}', "run_command"), + ResponseBuilders.tool_call('{"path": "/tmp/test.txt"}', "read_file"), + ResponseBuilders.tool_call( + '{"path": "/tmp/out.txt", "data": "hello"}', "write_file" + ), + ResponseBuilders.output_message("Done."), + ] + ) + + plugin = OpenAIAgentsPlugin( + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=30), + ), + model_provider=TestModelProvider(mock_model), + sandbox_clients=[SandboxClientProvider("mock", mock_sandbox_client)], + ) + + new_config = client.config() + new_config["plugins"] = [plugin] + test_client = Client(**new_config) + + async with new_worker( + test_client, + SandboxE2EWorkflow, + workflow_failure_exception_types=[Exception], + ) as worker: + result = await test_client.execute_workflow( + SandboxE2EWorkflow.run, + id=f"sandbox-e2e-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + + assert result == "Done." + # Full sandbox lifecycle exercised through Temporal activities + assert mock_sandbox_client.create_calls == 1, "client.create() not called" + assert mock_session.start_calls == 1, "session.start() not called" + assert len(mock_session.exec_calls) >= 1, "session.exec() not called" + assert len(mock_session.read_calls) >= 1, "session.read() not called" + assert len(mock_session.write_calls) >= 1, "session.write() not called" + assert mock_session.stop_calls >= 1, "session.stop() not called" + assert mock_session.shutdown_calls >= 1, "session.shutdown() not called" + assert mock_sandbox_client.delete_calls == 1, "client.delete() not called" + + +# ── JsonSafeBytes lossless serialization tests ── + +# Payloads that exercise edge cases for bytes → JSON → bytes roundtrip. +_BYTE_PAYLOADS = [ + pytest.param(b"", id="empty"), + pytest.param(b"hello world", id="ascii"), + pytest.param(b"\xc3\xa9\xc3\xa0", id="valid-utf8"), # éà + pytest.param(bytes(range(256)), id="all-byte-values"), + pytest.param(b"\xff\xfe\x80\x90\x00\x01", id="non-utf8-binary"), + pytest.param(b"ok\nWarning: \xff\xfe binary \x80\x90\x00\x01", id="mixed"), + pytest.param(b"\x00\x00\x00", id="null-bytes"), +] + + +def _roundtrip(model_cls: Any, **kwargs: Any) -> Any: + """Serialize a model to JSON via pydantic_core and deserialize back.""" + json_bytes = to_json(model_cls(**kwargs)) + return TypeAdapter(model_cls).validate_json(json_bytes) + + +@pytest.mark.parametrize("payload", _BYTE_PAYLOADS) +def test_exec_result_bytes_roundtrip(payload: bytes): + """ExecResult.stdout/stderr must survive a JSON roundtrip unchanged.""" + restored = _roundtrip(ExecResultModel, stdout=payload, stderr=payload, exit_code=1) + assert restored.stdout == payload + assert restored.stderr == payload + assert restored.exit_code == 1 + + +@pytest.mark.parametrize("payload", _BYTE_PAYLOADS) +def test_pty_exec_update_result_bytes_roundtrip(payload: bytes): + """PtyExecUpdateResult.output must survive a JSON roundtrip unchanged.""" + restored = _roundtrip( + PtyExecUpdateResult, + process_id=1, + output=payload, + exit_code=0, + original_token_count=None, + ) + assert restored.output == payload + + +@pytest.mark.parametrize("payload", _BYTE_PAYLOADS) +def test_read_result_bytes_roundtrip(payload: bytes): + """ReadResult.data must survive a JSON roundtrip unchanged.""" + restored = _roundtrip(ReadResult, data=payload) + assert restored.data == payload + + +@pytest.mark.parametrize("payload", _BYTE_PAYLOADS) +def test_persist_workspace_result_bytes_roundtrip(payload: bytes): + """PersistWorkspaceResult.data must survive a JSON roundtrip unchanged.""" + restored = _roundtrip(PersistWorkspaceResult, data=payload) + assert restored.data == payload + + +@pytest.mark.parametrize("payload", _BYTE_PAYLOADS) +def test_write_args_bytes_roundtrip(payload: bytes): + """WriteArgs.data must survive a JSON roundtrip unchanged (workflow → activity).""" + restored = _roundtrip(WriteArgs, state=_make_state(), path="/tmp/f", data=payload) + assert restored.data == payload + + +@pytest.mark.parametrize("payload", _BYTE_PAYLOADS) +def test_hydrate_workspace_args_bytes_roundtrip(payload: bytes): + """HydrateWorkspaceArgs.data must survive a JSON roundtrip unchanged.""" + restored = _roundtrip(HydrateWorkspaceArgs, state=_make_state(), data=payload) + assert restored.data == payload diff --git a/tests/contrib/openai_agents/test_openai_streaming.py b/tests/contrib/openai_agents/test_openai_streaming.py new file mode 100644 index 000000000..ab711cd86 --- /dev/null +++ b/tests/contrib/openai_agents/test_openai_streaming.py @@ -0,0 +1,350 @@ +"""Integration tests for OpenAI Agents streaming support. + +Streaming is opt-in via ``Runner.run_streamed``. Events flow back to the +workflow through ``RunResultStreaming.stream_events()`` (in batch after +each model activity completes) and to external consumers in real time +via the configured stream topic. +""" + +import asyncio +import logging +import uuid +from collections.abc import AsyncIterator +from datetime import timedelta +from typing import Any + +import pytest +from agents import ( + Agent, + AgentOutputSchemaBase, + Handoff, + Model, + ModelResponse, + ModelSettings, + ModelTracing, + Runner, + Tool, + TResponseInputItem, + Usage, +) +from agents.items import TResponseStreamEvent +from openai.types.responses import ( + Response, + ResponseCompletedEvent, + ResponseOutputMessage, + ResponseOutputText, + ResponseTextConfig, + ResponseTextDeltaEvent, + ResponseUsage, +) +from openai.types.responses.response_usage import ( + InputTokensDetails, + OutputTokensDetails, +) +from openai.types.shared.response_format_text import ResponseFormatText + +from temporalio import workflow +from temporalio.client import Client, WorkflowFailureError +from temporalio.contrib.openai_agents import ModelActivityParameters +from temporalio.contrib.openai_agents.testing import AgentEnvironment +from temporalio.contrib.workflow_streams import WorkflowStream, WorkflowStreamClient +from tests.helpers import new_worker + +logger = logging.getLogger(__name__) + + +class StreamingTestModel(Model): + """Test model that yields text deltas followed by a ResponseCompletedEvent.""" + + __test__ = False + + async def get_response( + self, + system_instructions: str | None, + input: str | list[TResponseInputItem], + model_settings: ModelSettings, + tools: list[Tool], + output_schema: AgentOutputSchemaBase | None, + handoffs: list[Handoff], + tracing: ModelTracing, + **kwargs: Any, + ) -> ModelResponse: + return ModelResponse( + output=[ + ResponseOutputMessage( + id="msg_test", + content=[ + ResponseOutputText( + text="Hello world!", + annotations=[], + type="output_text", + logprobs=[], + ) + ], + role="assistant", + status="completed", + type="message", + ) + ], + usage=Usage(), + response_id=None, + ) + + async def stream_response( + self, + system_instructions: str | None, + input: str | list[TResponseInputItem], + model_settings: ModelSettings, + tools: list[Tool], + output_schema: AgentOutputSchemaBase | None, + handoffs: list[Handoff], + tracing: ModelTracing, + **kwargs: Any, + ) -> AsyncIterator[TResponseStreamEvent]: + # Yield text deltas + yield ResponseTextDeltaEvent( + content_index=0, + delta="Hello ", + item_id="item1", + output_index=0, + sequence_number=0, + type="response.output_text.delta", + logprobs=[], + ) + yield ResponseTextDeltaEvent( + content_index=0, + delta="world!", + item_id="item1", + output_index=0, + sequence_number=1, + type="response.output_text.delta", + logprobs=[], + ) + + # Yield the final completed event + response = Response( + id="resp_test", + created_at=0, + error=None, + incomplete_details=None, + instructions=None, + metadata={}, + model="test", + object="response", + output=[ + ResponseOutputMessage( + id="msg_test", + content=[ + ResponseOutputText( + text="Hello world!", + annotations=[], + type="output_text", + logprobs=[], + ) + ], + role="assistant", + status="completed", + type="message", + ) + ], + parallel_tool_calls=True, + temperature=1.0, + tool_choice="auto", + tools=[], + top_p=1.0, + status="completed", + text=ResponseTextConfig(format=ResponseFormatText(type="text")), + truncation="disabled", + usage=ResponseUsage( + input_tokens=10, + output_tokens=5, + total_tokens=15, + input_tokens_details=InputTokensDetails.model_validate( + {"cached_tokens": 0, "cache_write_tokens": 0} + ), + output_tokens_details=OutputTokensDetails(reasoning_tokens=0), + ), + ) + yield ResponseCompletedEvent( + response=response, sequence_number=2, type="response.completed" + ) + + +@workflow.defn +class StreamingOpenAIWorkflow: + """Test workflow that opts into streaming via ``Runner.run_streamed``. + + Workflow code consumes events from ``stream_events()`` and exposes + the seen event types via a query so the test can verify both the + in-workflow iteration and the external stream subscriber observe the + same events. + """ + + @workflow.init + def __init__(self, prompt: str) -> None: + self.stream = WorkflowStream() + self.workflow_event_types: list[str] = [] + + @workflow.run + async def run(self, prompt: str) -> str: + agent = Agent[None]( + name="Assistant", + instructions="You are a test agent.", + ) + result = Runner.run_streamed(starting_agent=agent, input=prompt) + async for event in result.stream_events(): + raw = getattr(event, "data", None) + event_type = getattr(raw, "type", None) + if event_type is not None: + self.workflow_event_types.append(event_type) + return result.final_output + + @workflow.query + def get_workflow_event_types(self) -> list[str]: + return self.workflow_event_types + + +@workflow.defn +class StreamingRequiresTopicWorkflow: + """Workflow that opts into ``Runner.run_streamed`` while the model + plugin was configured without a ``streaming_topic``. + + The stub raises before scheduling the streaming activity; this + propagates out of ``Runner.run_streamed`` and fails the workflow. + """ + + @workflow.run + async def run(self, prompt: str) -> str: + agent = Agent[None]( + name="Assistant", + instructions="You are a test agent.", + ) + result = Runner.run_streamed(starting_agent=agent, input=prompt) + async for _ in result.stream_events(): + pass + return result.final_output + + +@pytest.mark.asyncio +async def test_streaming_publishes_raw_events(client: Client): + """Both the workflow consumer (via stream_events) and the stream + topic see the same native OpenAI events, in order, with no + normalization.""" + async with AgentEnvironment( + model=StreamingTestModel(), + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=30), + streaming_topic="events", + ), + ) as env: + client = env.applied_on_client(client) + workflow_id = f"openai-streaming-test-{uuid.uuid4()}" + + async with new_worker( + client, StreamingOpenAIWorkflow, max_cached_workflows=0 + ) as worker: + handle = await client.start_workflow( + StreamingOpenAIWorkflow.run, + "Hello", + id=workflow_id, + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=30), + ) + + stream = WorkflowStreamClient.create(client, workflow_id) + published: list[TResponseStreamEvent] = [] + + async def collect_events() -> None: + async for item in stream.subscribe( + ["events"], + from_offset=0, + # TResponseStreamEvent is a discriminated union + # (Annotated[..., Discriminator]); Pydantic decodes + # it via TypeAdapter at runtime, but the type + # checkers see ``Annotated`` rather than ``type``. + result_type=TResponseStreamEvent, # type: ignore[arg-type,call-overload] + poll_cooldown=timedelta(milliseconds=50), + ): + published.append(item.data) + if item.data.type == "response.completed": + break + + collect_task = asyncio.create_task(collect_events()) + result = await handle.result() + await asyncio.wait_for(collect_task, timeout=10.0) + + workflow_event_types = await handle.query( + StreamingOpenAIWorkflow.get_workflow_event_types + ) + + assert result == "Hello world!" + + published_types = [e.type for e in published] + assert published_types == [ + "response.output_text.delta", + "response.output_text.delta", + "response.completed", + ], f"Unexpected published event sequence: {published_types}" + + deltas = [e.delta for e in published if e.type == "response.output_text.delta"] + assert deltas == ["Hello ", "world!"] + + # Workflow-side iteration sees the same model events in the same order. + assert workflow_event_types == published_types + + +@pytest.mark.asyncio +async def test_streaming_requires_topic(client: Client): + """``Runner.run_streamed`` fails fast when the plugin has no topic + configured. The error is raised in ``stream_response`` before any + streaming activity is scheduled.""" + async with AgentEnvironment( + model=StreamingTestModel(), + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=30), + streaming_topic=None, + ), + ) as env: + client = env.applied_on_client(client) + async with new_worker( + client, StreamingRequiresTopicWorkflow, max_cached_workflows=0 + ) as worker: + with pytest.raises(WorkflowFailureError) as exc_info: + await client.execute_workflow( + StreamingRequiresTopicWorkflow.run, + "Hi", + id=f"openai-streaming-requires-topic-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=30), + ) + + assert "streaming_topic" in str(exc_info.value.cause) + + +@pytest.mark.asyncio +async def test_streaming_rejects_local_activity(client: Client): + """``Runner.run_streamed`` fails fast when the plugin is configured + with ``use_local_activity=True``. Local activities support neither + heartbeats nor the workflow-stream signal channel.""" + async with AgentEnvironment( + model=StreamingTestModel(), + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=30), + streaming_topic="events", + use_local_activity=True, + ), + ) as env: + client = env.applied_on_client(client) + async with new_worker( + client, StreamingRequiresTopicWorkflow, max_cached_workflows=0 + ) as worker: + with pytest.raises(WorkflowFailureError) as exc_info: + await client.execute_workflow( + StreamingRequiresTopicWorkflow.run, + "Hi", + id=f"openai-streaming-rejects-local-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=30), + ) + + assert "use_local_activity" in str(exc_info.value.cause) diff --git a/tests/contrib/openai_agents/test_openai_tracing.py b/tests/contrib/openai_agents/test_openai_tracing.py index c8ad366e6..77950c035 100644 --- a/tests/contrib/openai_agents/test_openai_tracing.py +++ b/tests/contrib/openai_agents/test_openai_tracing.py @@ -2,16 +2,29 @@ from datetime import timedelta from typing import Any -from agents import Span, Trace, TracingProcessor +import opentelemetry.trace +from agents import Span, Trace, TracingProcessor, custom_span, trace from agents.tracing import get_trace_provider +from opentelemetry.sdk.trace import ReadableSpan +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter +from temporalio import activity, workflow from temporalio.client import Client -from temporalio.contrib import openai_agents -from temporalio.contrib.openai_agents import ( - TestModelProvider, +from temporalio.contrib.openai_agents import _temporal_openai_agents +from temporalio.contrib.openai_agents.testing import ( + AgentEnvironment, ) -from tests.contrib.openai_agents.test_openai import ResearchWorkflow, TestResearchModel -from tests.helpers import new_worker +from temporalio.contrib.opentelemetry import create_tracer_provider +from temporalio.worker.workflow_sandbox import ( + SandboxedWorkflowRunner, + SandboxRestrictions, +) +from tests.contrib.openai_agents.test_openai import ( + ResearchWorkflow, + research_mock_model, +) +from tests.helpers import assert_eq_eventually, new_worker class MemoryTracingProcessor(TracingProcessor): @@ -38,103 +51,897 @@ def force_flush(self) -> None: pass +def test_otel_instrumentation_lifecycle_does_not_nest() -> None: + from openinference.instrumentation.openai_agents._processor import ( + OpenInferenceTracingProcessor, + ) + from opentelemetry import trace + + original = OpenInferenceTracingProcessor.on_trace_start + _temporal_openai_agents._install_otel_instrumentation(trace.get_tracer_provider()) + try: + installed_patch = OpenInferenceTracingProcessor.on_trace_start + assert installed_patch is not original + + _temporal_openai_agents._install_otel_instrumentation( + trace.get_tracer_provider() + ) + try: + assert OpenInferenceTracingProcessor.on_trace_start is installed_patch + finally: + _temporal_openai_agents._uninstall_otel_instrumentation() + + assert OpenInferenceTracingProcessor.on_trace_start is installed_patch + finally: + _temporal_openai_agents._uninstall_otel_instrumentation() + + assert OpenInferenceTracingProcessor.on_trace_start is original + + async def test_tracing(client: Client): - new_config = client.config() - new_config["plugins"] = [ - openai_agents.OpenAIAgentsPlugin( - model_provider=TestModelProvider(TestResearchModel()) + async with AgentEnvironment(model=research_mock_model()) as env: + client = env.applied_on_client(client) + provider = get_trace_provider() + + processor = MemoryTracingProcessor() + provider.set_processors([processor]) + + async with new_worker( + client, + ResearchWorkflow, + ) as worker: + with trace("Research workflow"): + workflow_handle = await client.start_workflow( + ResearchWorkflow.run, + "Caribbean vacation spots in April, optimizing for surfing, hiking and water sports", + id=f"research-workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=120), + ) + await workflow_handle.result() + print("\n".join([str({"name": t.name}) for t, _ in processor.trace_events])) + + # There are two traces, one is created in the client because it is needed to start the temporal spans + assert len(processor.trace_events) == 2 + assert ( + processor.trace_events[0][0].trace_id + == processor.trace_events[1][0].trace_id ) - ] - client = Client(**new_config) + assert processor.trace_events[0][1] + assert not processor.trace_events[1][1] - provider = get_trace_provider() + def paired_span(a: tuple[Span[Any], bool], b: tuple[Span[Any], bool]) -> None: + assert a[0].trace_id == b[0].trace_id + assert a[1] + assert not b[1] - processor = MemoryTracingProcessor() - provider.set_processors([processor]) + print( + "\n".join( + [ + str({"id": t.span_id, "data": t.span_data.export()}) + for t, _ in processor.span_events + ] + ) + ) - async with new_worker( - client, - ResearchWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - ResearchWorkflow.run, - "Caribbean vacation spots in April, optimizing for surfing, hiking and water sports", - id=f"research-workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=120), + # Start workflow traces + paired_span(processor.span_events[0], processor.span_events[1]) + assert ( + processor.span_events[0][0].span_data.export().get("name") + == "temporal:startWorkflow:ResearchWorkflow" ) - result = await workflow_handle.result() - # There is one closed root trace - assert len(processor.trace_events) == 2 - assert ( - processor.trace_events[0][0].trace_id == processor.trace_events[1][0].trace_id + # Execute workflow + paired_span(processor.span_events[2], processor.span_events[-1]) + assert ( + processor.span_events[2][0].span_data.export().get("name") + == "temporal:executeWorkflow" + ) + + # Research manager span + paired_span(processor.span_events[3], processor.span_events[-2]) + assert ( + processor.span_events[3][0].span_data.export().get("name") + == "Research manager" + ) + + # Initial planner spans - task wraps agent, agent wraps turn, turn wraps activity + paired_span(processor.span_events[4], processor.span_events[13]) + assert processor.span_events[4][0].span_data.export().get("name") == "task" + + paired_span(processor.span_events[5], processor.span_events[12]) + assert ( + processor.span_events[5][0].span_data.export().get("name") == "PlannerAgent" + ) + + paired_span(processor.span_events[6], processor.span_events[11]) + assert processor.span_events[6][0].span_data.export().get("name") == "turn" + + paired_span(processor.span_events[7], processor.span_events[10]) + assert ( + processor.span_events[7][0].span_data.export().get("name") + == "temporal:startActivity" + ) + + paired_span(processor.span_events[8], processor.span_events[9]) + assert ( + processor.span_events[8][0].span_data.export().get("name") + == "temporal:executeActivity" + ) + + for span, start in processor.span_events[14:-12]: + span_data = span.span_data.export() + + # All spans should be closed + if start: + assert any( + span.span_id == s.span_id and not s_start + for (s, s_start) in processor.span_events + ) + + # Start activity is always parented to a turn span, which is parented to an agent + if span_data.get("name") == "temporal:startActivity": + turn_spans = [ + s for (s, _) in processor.span_events if s.span_id == span.parent_id + ] + assert len(turn_spans) == 2 + assert ( + turn_spans[0] + .span_data.export() + .get("data", {}) + .get("sdk_span_type") + == "turn" + ) + agent_spans = [ + s + for (s, _) in processor.span_events + if s.span_id == turn_spans[0].parent_id + ] + assert len(agent_spans) == 2 + assert agent_spans[0].span_data.export()["type"] == "agent" + + # Execute is parented to start + if span_data.get("name") == "temporal:executeActivity": + parents = [ + s for (s, _) in processor.span_events if s.span_id == span.parent_id + ] + assert ( + len(parents) == 2 + and parents[0].span_data.export()["name"] + == "temporal:startActivity" + ) + + # Final writer spans - task wraps agent, agent wraps turn, turn wraps activity + paired_span(processor.span_events[-12], processor.span_events[-3]) + assert processor.span_events[-12][0].span_data.export().get("name") == "task" + + paired_span(processor.span_events[-11], processor.span_events[-4]) + assert ( + processor.span_events[-11][0].span_data.export().get("name") + == "WriterAgent" + ) + + paired_span(processor.span_events[-10], processor.span_events[-5]) + assert processor.span_events[-10][0].span_data.export().get("name") == "turn" + + paired_span(processor.span_events[-9], processor.span_events[-6]) + assert ( + processor.span_events[-9][0].span_data.export().get("name") + == "temporal:startActivity" + ) + + paired_span(processor.span_events[-8], processor.span_events[-7]) + assert ( + processor.span_events[-8][0].span_data.export().get("name") + == "temporal:executeActivity" + ) + + +@activity.defn +async def simple_no_context_activity() -> str: + return "success" + + +@workflow.defn +class TraceWorkflow: + def __init__(self) -> None: + self._proceed = False + self._ready = False + + @workflow.run + async def run(self): + # Workflow creates spans within existing trace context + with custom_span("Workflow span"): + await workflow.execute_activity( + simple_no_context_activity, + start_to_close_timeout=timedelta(seconds=10), + ) + self._ready = True + await workflow.wait_condition(lambda: self._proceed) + return "done" + + @workflow.query + def ready(self) -> bool: + return self._ready + + @workflow.signal + def proceed(self) -> None: + self._proceed = True + + +@workflow.defn +class SelfTracingWorkflow: + def __init__(self) -> None: + self._proceed = False + self._ready = False + + @workflow.run + async def run(self): + # Workflow starts its own trace + with trace("Workflow trace"): + with custom_span("Workflow span"): + await workflow.execute_activity( + simple_no_context_activity, + start_to_close_timeout=timedelta(seconds=10), + ) + self._ready = True + await workflow.wait_condition(lambda: self._proceed) + return "done" + + @workflow.query + def ready(self) -> bool: + return self._ready + + @workflow.signal + def proceed(self) -> None: + self._proceed = True + + +def print_otel_spans(spans: tuple[ReadableSpan, ...]): + print( + "\n".join( + [ + str( + { + "Name": span.name, + "Id": span.context.span_id if span.context else None, + "Parent": span.parent.span_id if span.parent else None, + } + ) + for span in spans + ] + ) ) - assert processor.trace_events[0][1] - assert not processor.trace_events[1][1] - def paired_span(a: tuple[Span[Any], bool], b: tuple[Span[Any], bool]) -> None: - assert a[0].trace_id == b[0].trace_id - assert a[1] - assert not b[1] - # Initial planner spans - There are only 3 because we don't make an actual model call - paired_span(processor.span_events[0], processor.span_events[5]) - assert processor.span_events[0][0].span_data.export().get("name") == "PlannerAgent" +def set_test_tracer_provider() -> InMemorySpanExporter: + exporter = InMemorySpanExporter() - paired_span(processor.span_events[1], processor.span_events[4]) - assert ( - processor.span_events[1][0].span_data.export().get("name") - == "temporal:startActivity" + provider = create_tracer_provider() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + opentelemetry.trace.set_tracer_provider(provider) + return exporter + + +async def test_external_trace_to_workflow_spans( + client: Client, + reset_otel_tracer_provider: Any, # type: ignore[reportUnusedParameter] +): + """Test: External trace -> workflow spans (with worker restart).""" + exporter = set_test_tracer_provider() + workflow_id = None + task_queue = str(uuid.uuid4()) + + # First worker: Start workflow with external trace context + async with AgentEnvironment( + model=research_mock_model(), + add_temporal_spans=False, + use_otel_instrumentation=True, + ) as env: + new_client = env.applied_on_client(client) + # Start external trace, then start workflow within that trace + # Start it outside of the worker to validate provider usage without worker's runcontext + with env.openai_agents_plugin.tracing_context(): + with trace("External trace"): + workflow_handle = await new_client.start_workflow( + TraceWorkflow.run, + id=f"external-trace-workflow-{uuid.uuid4()}", + task_queue=task_queue, + execution_timeout=timedelta(seconds=120), + ) + workflow_id = workflow_handle.id + + async with new_worker( + new_client, + TraceWorkflow, + activities=[simple_no_context_activity], + max_cached_workflows=0, + task_queue=task_queue, + ): + # Wait for workflow to be ready + async def ready() -> bool: + return await workflow_handle.query(TraceWorkflow.ready) + + await assert_eq_eventually(True, ready) + + # Second worker: Complete the workflow with fresh objects (new instrumentation) + async with AgentEnvironment( + model=research_mock_model(), + add_temporal_spans=False, + use_otel_instrumentation=True, + ) as env: + new_client = env.applied_on_client(client) + + async with new_worker( + new_client, + TraceWorkflow, + activities=[simple_no_context_activity], + max_cached_workflows=0, + task_queue=task_queue, + ): + workflow_handle = new_client.get_workflow_handle(workflow_id) + await workflow_handle.signal(TraceWorkflow.proceed) + result = await workflow_handle.result() + assert result == "done" + + spans = exporter.get_finished_spans() + print_otel_spans(spans) + + assert len(spans) >= 2 # External trace + workflow span + + # Find the spans + external_span = next((s for s in spans if s.name == "External trace"), None) + workflow_span = next((s for s in spans if s.name == "Workflow span"), None) + + assert external_span is not None, "External trace span should exist" + assert workflow_span is not None, "Workflow span should exist" + + # Verify parenting: External trace should be root, workflow span should be child of external trace + assert external_span.parent is None, ( + "External trace should have no parent (be root)" + ) + assert workflow_span.parent is not None, "Workflow span should have a parent" + assert external_span.context is not None, "External span should have context" + assert workflow_span.parent.span_id == external_span.context.span_id, ( + "Workflow span should be child of external trace" ) - paired_span(processor.span_events[2], processor.span_events[3]) - assert ( - processor.span_events[2][0].span_data.export().get("name") - == "temporal:executeActivity" + # Verify all spans have unique IDs + span_ids = [span.context.span_id for span in spans if span.context] + assert len(span_ids) == len(set(span_ids)), ( + f"All spans should have unique IDs, got: {span_ids}" + ) + + +async def test_external_trace_and_span_to_workflow_spans( + client: Client, + reset_otel_tracer_provider: Any, # type: ignore[reportUnusedParameter] +): + """Test: External trace + span -> workflow spans (with worker restart).""" + exporter = set_test_tracer_provider() + workflow_id = None + task_queue = str(uuid.uuid4()) + + # First worker: Start workflow with external trace + span context + async with AgentEnvironment( + model=research_mock_model(), + add_temporal_spans=False, + use_otel_instrumentation=True, + ) as env: + new_client = env.applied_on_client(client) + + # Start external trace + span, then start workflow within that context + # Start it outside of the worker to validate provider usage without worker's runcontext + with env.openai_agents_plugin.tracing_context(): + with trace("External trace"): + with custom_span("External span"): + workflow_handle = await new_client.start_workflow( + TraceWorkflow.run, + id=f"external-span-workflow-{uuid.uuid4()}", + task_queue=task_queue, + execution_timeout=timedelta(seconds=120), + ) + workflow_id = workflow_handle.id + + async with new_worker( + new_client, + TraceWorkflow, + activities=[simple_no_context_activity], + max_cached_workflows=0, + task_queue=task_queue, + ): + # Wait for workflow to be ready + async def ready() -> bool: + return await workflow_handle.query(TraceWorkflow.ready) + + await assert_eq_eventually(True, ready) + + # Second worker: Complete the workflow with fresh objects (new instrumentation) + async with AgentEnvironment( + model=research_mock_model(), + add_temporal_spans=False, + use_otel_instrumentation=True, + ) as env: + new_client = env.applied_on_client(client) + + async with new_worker( + new_client, + TraceWorkflow, + activities=[simple_no_context_activity], + max_cached_workflows=0, + task_queue=task_queue, + ): + workflow_handle = new_client.get_workflow_handle(workflow_id) + await workflow_handle.signal(TraceWorkflow.proceed) + result = await workflow_handle.result() + assert result == "done" + + spans = exporter.get_finished_spans() + + assert len(spans) >= 3 # External trace + external span + workflow span + + # Find the spans + external_trace_span = next((s for s in spans if s.name == "External trace"), None) + external_span = next((s for s in spans if s.name == "External span"), None) + workflow_span = next((s for s in spans if s.name == "Workflow span"), None) + + assert external_trace_span is not None, "External trace span should exist" + assert external_span is not None, "External span should exist" + assert workflow_span is not None, "Workflow span should exist" + + # Verify parenting: External span should be child of trace, workflow span should be child of external span + assert external_trace_span.parent is None, ( + "External trace should have no parent (be root)" + ) + assert external_span.parent is not None, "External span should have a parent" + assert external_trace_span.context is not None, ( + "External trace span should have context" + ) + assert external_span.parent.span_id == external_trace_span.context.span_id, ( + "External span should be child of external trace" + ) + assert workflow_span.parent is not None, "Workflow span should have a parent" + assert external_span.context is not None, "External span should have context" + assert workflow_span.parent.span_id == external_span.context.span_id, ( + "Workflow span should be child of external span" + ) + + # Verify all spans have unique IDs + span_ids = [span.context.span_id for span in spans if span.context] + assert len(span_ids) == len(set(span_ids)), ( + f"All spans should have unique IDs, got: {span_ids}" ) - for span, start in processor.span_events[6:-6]: - span_data = span.span_data.export() - # All spans should be closed - if start: - assert any( - span.span_id == s.span_id and not s_start - for (s, s_start) in processor.span_events +async def test_workflow_only_trace_to_spans( + client: Client, + reset_otel_tracer_provider: Any, # type: ignore[reportUnusedParameter] +): + """Test: Workflow-only trace -> spans (with worker restart).""" + exporter = set_test_tracer_provider() + workflow_id = None + task_queue = str(uuid.uuid4()) + + # First worker: Start workflow (no external trace context) + async with AgentEnvironment( + model=research_mock_model(), + add_temporal_spans=False, + use_otel_instrumentation=True, + ) as env: + new_client = env.applied_on_client(client) + + async with new_worker( + new_client, + SelfTracingWorkflow, + activities=[simple_no_context_activity], + max_cached_workflows=0, + task_queue=task_queue, + ) as worker: + # No external trace - workflow starts its own + workflow_handle = await new_client.start_workflow( + SelfTracingWorkflow.run, + id=f"self-tracing-workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=120), ) + workflow_id = workflow_handle.id - # Start activity is always parented to an agent - if span_data.get("name") == "temporal:startActivity": - parents = [ - s for (s, _) in processor.span_events if s.span_id == span.parent_id - ] - assert ( - len(parents) == 2 and parents[0].span_data.export()["type"] == "agent" + # Wait for workflow to be ready + async def ready() -> bool: + return await workflow_handle.query(SelfTracingWorkflow.ready) + + await assert_eq_eventually(True, ready) + + # Second worker: Complete the workflow with fresh objects (new instrumentation) + async with AgentEnvironment( + model=research_mock_model(), + add_temporal_spans=False, + use_otel_instrumentation=True, + ) as env: + new_client = env.applied_on_client(client) + + async with new_worker( + new_client, + SelfTracingWorkflow, + activities=[simple_no_context_activity], + max_cached_workflows=0, + task_queue=task_queue, + ) as worker: + workflow_handle = new_client.get_workflow_handle(workflow_id) + await workflow_handle.signal(SelfTracingWorkflow.proceed) + result = await workflow_handle.result() + assert result == "done" + + spans = exporter.get_finished_spans() + + assert len(spans) >= 2 # Workflow trace + workflow span + + # Find the spans + workflow_trace_span = next((s for s in spans if s.name == "Workflow trace"), None) + workflow_span = next((s for s in spans if s.name == "Workflow span"), None) + + assert workflow_trace_span is not None, "Workflow trace span should exist" + assert workflow_span is not None, "Workflow span should exist" + + # Verify parenting: Workflow trace should be root, workflow span should be child of workflow trace + assert workflow_trace_span.parent is None, ( + "Workflow trace should have no parent (be root)" + ) + assert workflow_span.parent is not None, "Workflow span should have a parent" + assert workflow_trace_span.context is not None, ( + "Workflow trace span should have context" + ) + assert workflow_span.parent.span_id == workflow_trace_span.context.span_id, ( + "Workflow span should be child of workflow trace" + ) + + +@workflow.defn +class SimpleWorkflow: + @workflow.run + async def run(self) -> str: + # Use custom_span without starting a trace - should be a no-op + with custom_span("Should not appear"): + with custom_span("Neither should this"): + return "done" + + +async def test_custom_span_without_trace_context( + client: Client, + reset_otel_tracer_provider: Any, # type: ignore[reportUnusedParameter] +): + """Test that custom_span() without a trace context emits no spans. + + This validates our hypothesis about why the main test fails: + If no OpenAI trace is started, custom_span() calls should be no-ops. + """ + exporter = set_test_tracer_provider() + + async with AgentEnvironment( + model=research_mock_model(), use_otel_instrumentation=True + ) as env: + client = env.applied_on_client(client) + + async with new_worker(client, SimpleWorkflow) as worker: + result = await client.execute_workflow( + SimpleWorkflow.run, + id=f"simple-workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, ) + assert result == "done" - # Execute is parented to start - if span_data.get("name") == "temporal:executeActivity": - parents = [ - s for (s, _) in processor.span_events if s.span_id == span.parent_id - ] - assert ( - len(parents) == 2 - and parents[0].span_data.export()["name"] == "temporal:startActivity" + spans = exporter.get_finished_spans() + + # Should have no custom spans since no trace was started + custom_spans = [ + span + for span in spans + if "Should not appear" in span.name or "Neither should this" in span.name + ] + + assert len(custom_spans) == 0, ( + f"Expected no custom spans without trace context, but found: {[s.name for s in custom_spans]}" + ) + + # Should have no spans at all since no trace was started and spans should be dropped + assert len(spans) == 0, ( + f"Expected no spans without trace context, but found: {[s.name for s in spans]}" + ) + + +async def test_otel_tracing_in_runner( + client: Client, + reset_otel_tracer_provider: Any, # type: ignore[reportUnusedParameter] +): + """Test the tracing when executing an actual OpenAI Runner.""" + exporter = set_test_tracer_provider() + + # Test the new ergonomic API - just pass exporters to AgentEnvironment + async with AgentEnvironment( + model=research_mock_model(), + add_temporal_spans=False, + use_otel_instrumentation=True, + ) as env: + client = env.applied_on_client(client) + + async with new_worker( + client, + ResearchWorkflow, + max_cached_workflows=0, + ) as worker: + with trace("Research workflow"): + workflow_handle = await client.start_workflow( + ResearchWorkflow.run, + "Caribbean vacation spots in April, optimizing for surfing, hiking and water sports", + id=f"research-workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=120), + ) + await workflow_handle.result() + + spans = exporter.get_finished_spans() + print("OTEL tracing in runner spans:") + print_otel_spans(spans) + + # Verify basic span capture + assert len(spans) > 0, "Should have captured some spans from the research workflow" + + # Categorize spans that users expect to see in their agents workflow + research_manager_spans = [span for span in spans if "Research manager" in span.name] + search_web_spans = [span for span in spans if "Search the web" in span.name] + agent_execution_spans = [ + span + for span in spans + if any( + agent_name in span.name.lower() + for agent_name in ["planner", "search", "writer"] + ) + and "workflow" not in span.name.lower() + ] + + all_span_names = [span.name for span in spans] + unique_span_names = list(set(all_span_names)) + + # Assert users get visibility into their workflow coordination + assert len(research_manager_spans) > 0, ( + f"Expected 'Research manager' spans for workflow coordination visibility, " + f"but only found: {unique_span_names}" + ) + + # Assert users can see their search phases + assert len(search_web_spans) > 0, ( + f"Expected 'Search the web' spans for search phase visibility, " + f"but only found: {unique_span_names}" + ) + + # Assert users can see individual agent executions + assert len(agent_execution_spans) > 0, ( + f"Expected agent execution spans (planner, search, writer) for individual agent visibility, " + f"but only found: {unique_span_names}" + ) + + # Validate span hierarchy integrity + span_ids = {span.context.span_id for span in spans if span.context} + for span in spans: + if span.parent: + assert span.parent.span_id in span_ids, ( + f"Span '{span.name}' has invalid parent reference - parent span doesn't exist" ) - # Final writer spans - There are only 3 because we don't make an actual model call - paired_span(processor.span_events[-6], processor.span_events[-1]) - assert processor.span_events[-6][0].span_data.export().get("name") == "WriterAgent" + # Validate logical parent-child relationships match user code structure + workflow_trace_spans = [span for span in spans if "Research workflow" in span.name] + assert len(workflow_trace_spans) == 1, ( + f"Expected exactly one 'Research workflow' trace, got {len(workflow_trace_spans)}" + ) + workflow_span = workflow_trace_spans[0] + assert workflow_span.context is not None - paired_span(processor.span_events[-5], processor.span_events[-2]) - assert ( - processor.span_events[-5][0].span_data.export().get("name") - == "temporal:startActivity" + # Research manager should be child of workflow trace + research_span = research_manager_spans[0] + assert research_span.context is not None + assert research_span.parent is not None, ( + "Research manager span should have a parent" + ) + assert research_span.parent.span_id == workflow_span.context.span_id, ( + "Expected 'Research manager' to be child of 'Research workflow' trace" + ) + + # Search the web should be child of research manager + search_span = search_web_spans[0] + assert search_span.context is not None + assert search_span.parent is not None, "Search the web span should have a parent" + assert search_span.parent.span_id == research_span.context.span_id, ( + "Expected 'Search the web' to be child of 'Research manager' span" ) - paired_span(processor.span_events[-4], processor.span_events[-3]) + # All search agent spans should be descendants of "Search the web" + # (the SDK now inserts a "task" span between "Search the web" and the agent) + span_by_id = {span.context.span_id: span for span in spans if span.context} + search_agent_spans = [span for span in spans if "Search agent" in span.name] + + def is_descendant_of(child: ReadableSpan, ancestor_span_id: int) -> bool: + """Check if child is a descendant of the span with ancestor_span_id.""" + current: ReadableSpan | None = child + while current and current.parent: + if current.parent.span_id == ancestor_span_id: + return True + current = span_by_id.get(current.parent.span_id) + return False + + for search_agent_span in search_agent_spans: + assert search_agent_span.parent is not None, ( + f"Search agent span '{search_agent_span.name}' should have a parent" + ) + assert is_descendant_of(search_agent_span, search_span.context.span_id), ( + f"Expected all 'Search agent' spans to be descendants of 'Search the web' span" + ) + + # PlannerAgent and WriterAgent should be descendants of research manager + planner_spans = [span for span in spans if "PlannerAgent" in span.name] + writer_spans = [span for span in spans if "WriterAgent" in span.name] + + for planner_span in planner_spans: + assert planner_span.parent is not None, "PlannerAgent span should have a parent" + assert is_descendant_of(planner_span, research_span.context.span_id), ( + "Expected 'PlannerAgent' to be descendant of 'Research manager' span" + ) + + for writer_span in writer_spans: + assert writer_span.parent is not None, "WriterAgent span should have a parent" + assert is_descendant_of(writer_span, research_span.context.span_id), ( + "Expected 'WriterAgent' to be descendant of 'Research manager' span" + ) + + +@workflow.defn +class OtelSpanWorkflow: + def __init__(self) -> None: + self._proceed = False + self._ready = False + + @workflow.run + async def run(self): + # Start an SDK custom_span first to establish OTEL context + with custom_span("Workflow SDK span"): + # Workflow starts OTEL span directly using opentelemetry.trace + tracer = opentelemetry.trace.get_tracer(__name__) + with tracer.start_as_current_span("Direct OTEL span"): + await workflow.execute_activity( + simple_no_context_activity, + start_to_close_timeout=timedelta(seconds=10), + ) + self._ready = True + await workflow.wait_condition(lambda: self._proceed) + return "done" + + @workflow.query + def ready(self) -> bool: + return self._ready + + @workflow.signal + def proceed(self) -> None: + self._proceed = True + + +async def test_sdk_trace_to_otel_span_parenting( + client: Client, + reset_otel_tracer_provider: Any, # type: ignore[reportUnusedParameter] +): + """Test that OTEL spans started in workflow are properly parented to client SDK trace.""" + exporter = set_test_tracer_provider() + workflow_id = None + task_queue = str(uuid.uuid4()) + + # First worker: Start workflow with client SDK trace context + async with AgentEnvironment( + model=research_mock_model(), + add_temporal_spans=False, + use_otel_instrumentation=True, + ) as env: + new_client = env.applied_on_client(client) + + async with new_worker( + new_client, + OtelSpanWorkflow, + activities=[simple_no_context_activity], + max_cached_workflows=0, + task_queue=task_queue, + workflow_runner=SandboxedWorkflowRunner( + SandboxRestrictions.default.with_passthrough_modules("opentelemetry") + ), + ) as worker: + # Start SDK trace in client, then start workflow within that trace + with trace("Client SDK trace"): + workflow_handle = await new_client.start_workflow( + OtelSpanWorkflow.run, + id=f"sdk-trace-otel-span-workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=120), + ) + workflow_id = workflow_handle.id + + # Wait for workflow to be ready + async def ready() -> bool: + return await workflow_handle.query(OtelSpanWorkflow.ready) + + await assert_eq_eventually(True, ready) + + # Second worker: Complete the workflow with fresh objects (new instrumentation) + async with AgentEnvironment( + model=research_mock_model(), + add_temporal_spans=False, + use_otel_instrumentation=True, + ) as env: + new_client = env.applied_on_client(client) + + async with new_worker( + new_client, + OtelSpanWorkflow, + activities=[simple_no_context_activity], + max_cached_workflows=0, + task_queue=task_queue, + workflow_runner=SandboxedWorkflowRunner( + SandboxRestrictions.default.with_passthrough_modules("opentelemetry") + ), + ) as worker: + workflow_handle = new_client.get_workflow_handle(workflow_id) + await workflow_handle.signal(OtelSpanWorkflow.proceed) + result = await workflow_handle.result() + assert result == "done" + + spans = exporter.get_finished_spans() + print("SDK trace to OTEL span parenting:") + print_otel_spans(spans) + + assert len(spans) >= 3 # Client SDK trace + Workflow SDK span + Direct OTEL span + + # Find the spans + client_sdk_trace_span = next( + (s for s in spans if s.name == "Client SDK trace"), None + ) + workflow_sdk_span = next((s for s in spans if s.name == "Workflow SDK span"), None) + direct_otel_span = next((s for s in spans if s.name == "Direct OTEL span"), None) + + assert client_sdk_trace_span is not None, "Client SDK trace span should exist" + assert workflow_sdk_span is not None, "Workflow SDK span should exist" + assert direct_otel_span is not None, "Direct OTEL span should exist" + + # Verify parenting chain: Client SDK trace -> Workflow SDK span -> Direct OTEL span + assert client_sdk_trace_span.parent is None, ( + "Client SDK trace should have no parent (be root)" + ) + + assert workflow_sdk_span.parent is not None, ( + "Workflow SDK span should have a parent" + ) + assert client_sdk_trace_span.context is not None, ( + "Client SDK trace span should have context" + ) + assert workflow_sdk_span.parent.span_id == client_sdk_trace_span.context.span_id, ( + "Workflow SDK span should be child of Client SDK trace" + ) + + assert direct_otel_span.parent is not None, "Direct OTEL span should have a parent" + assert workflow_sdk_span.context is not None, ( + "Workflow SDK span should have context" + ) + assert direct_otel_span.parent.span_id == workflow_sdk_span.context.span_id, ( + "Direct OTEL span should be child of Workflow SDK span" + ) + + # Verify all spans belong to the same trace + assert workflow_sdk_span.context is not None, ( + "Workflow SDK span should have context" + ) + assert direct_otel_span.context is not None, "Direct OTEL span should have context" assert ( - processor.span_events[-4][0].span_data.export().get("name") - == "temporal:executeActivity" + client_sdk_trace_span.context.trace_id + == workflow_sdk_span.context.trace_id + == direct_otel_span.context.trace_id + ), "All spans should belong to the same trace" + + # Verify all spans have unique IDs + span_ids = [span.context.span_id for span in spans if span.context] + assert len(span_ids) == len(set(span_ids)), ( + f"All spans should have unique IDs, got: {span_ids}" ) diff --git a/tests/contrib/opentelemetry/test_opentelemetry.py b/tests/contrib/opentelemetry/test_opentelemetry.py new file mode 100644 index 000000000..1bab931ac --- /dev/null +++ b/tests/contrib/opentelemetry/test_opentelemetry.py @@ -0,0 +1,1036 @@ +from __future__ import annotations + +import asyncio +import gc +import logging +import queue +import threading +import uuid +from collections.abc import Callable, Generator, Iterable +from concurrent.futures import ThreadPoolExecutor +from contextlib import contextmanager +from dataclasses import dataclass +from datetime import timedelta +from typing import Any, cast + +import nexusrpc +import opentelemetry.context +import pytest +from opentelemetry import baggage, context +from opentelemetry.sdk.trace import ReadableSpan, TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter +from opentelemetry.trace import StatusCode, get_tracer + +from temporalio import activity, nexus, workflow +from temporalio.client import Client, WithStartWorkflowOperation, WorkflowUpdateStage +from temporalio.common import RetryPolicy, WorkflowIDConflictPolicy +from temporalio.contrib.opentelemetry import ( + TracingInterceptor, + TracingWorkflowInboundInterceptor, +) +from temporalio.contrib.opentelemetry import workflow as otel_workflow +from temporalio.exceptions import ( + ApplicationError, + ApplicationErrorCategory, + NexusOperationError, +) +from temporalio.testing import WorkflowEnvironment +from temporalio.worker import UnsandboxedWorkflowRunner, Worker +from tests.helpers import LogCapturer +from tests.helpers.nexus import make_nexus_endpoint_name + + +@dataclass +class TracingActivityParam: + heartbeat: bool = True + fail_until_attempt: int | None = None + + +@activity.defn +async def tracing_activity(param: TracingActivityParam) -> None: + if param.heartbeat and not activity.info().is_local: + activity.heartbeat() + if param.fail_until_attempt and activity.info().attempt < param.fail_until_attempt: + raise RuntimeError("intentional failure") + + +@dataclass +class TracingWorkflowParam: + actions: list[TracingWorkflowAction] + + +@dataclass +class TracingWorkflowAction: + fail_on_non_replay: bool = False + child_workflow: TracingWorkflowActionChildWorkflow | None = None + activity: TracingWorkflowActionActivity | None = None + continue_as_new: TracingWorkflowActionContinueAsNew | None = None + wait_until_signal_count: int = 0 + wait_and_do_update: bool = False + wait_and_do_start_with_update: bool = False + start_and_cancel_nexus_operation: bool = False + + +@dataclass +class TracingWorkflowActionChildWorkflow: + id: str + param: TracingWorkflowParam + signal: bool = False + external_signal: bool = False + fail_on_non_replay_before_complete: bool = False + + +@dataclass +class TracingWorkflowActionActivity: + param: TracingActivityParam + local: bool = False + fail_on_non_replay_before_complete: bool = False + + +@dataclass +class TracingWorkflowActionContinueAsNew: + param: TracingWorkflowParam + + +@workflow.defn +class ExpectCancelNexusWorkflow: + @workflow.run + async def run(self, _input: str): + try: + await asyncio.wait_for(asyncio.Future(), 2) + except asyncio.TimeoutError: + raise ApplicationError("expected cancellation") + + +@nexusrpc.handler.service_handler +class InterceptedNexusService: + @nexus.workflow_run_operation + async def intercepted_operation( + self, ctx: nexus.WorkflowRunOperationContext, input: str + ) -> nexus.WorkflowHandle[None]: + return await ctx.start_workflow( + ExpectCancelNexusWorkflow.run, + input, + id=f"wf-{uuid.uuid4()}-{ctx.request_id}", + ) + + +ready_for_update: asyncio.Semaphore +ready_for_update_with_start: asyncio.Semaphore + + +@workflow.defn +class TracingWorkflow: + def __init__(self) -> None: + self._signal_count = 0 + self._did_update = False + self._did_update_with_start = False + + @workflow.run + async def run(self, param: TracingWorkflowParam) -> None: + otel_workflow.completed_span("MyCustomSpan", attributes={"foo": "bar"}) + for action in param.actions: + if action.fail_on_non_replay: + await self._raise_on_non_replay() + if action.child_workflow: + child_handle = await workflow.start_child_workflow( + TracingWorkflow.run, + action.child_workflow.param, + id=action.child_workflow.id, + ) + if action.child_workflow.fail_on_non_replay_before_complete: + await self._raise_on_non_replay() + if action.child_workflow.signal: + await child_handle.signal(TracingWorkflow.signal) + if action.child_workflow.external_signal: + external_handle: workflow.ExternalWorkflowHandle[ + TracingWorkflow + ] = workflow.get_external_workflow_handle_for( + TracingWorkflow.run, workflow_id=child_handle.id + ) + await external_handle.signal(TracingWorkflow.signal) + await child_handle + if action.activity: + retry_policy = RetryPolicy(initial_interval=timedelta(milliseconds=1)) + activity_handle = ( + workflow.start_local_activity( + tracing_activity, + action.activity.param, + start_to_close_timeout=timedelta(seconds=10), + retry_policy=retry_policy, + ) + if action.activity.local + else workflow.start_activity( + tracing_activity, + action.activity.param, + start_to_close_timeout=timedelta(seconds=10), + retry_policy=retry_policy, + ) + ) + if action.activity.fail_on_non_replay_before_complete: + await self._raise_on_non_replay() + await activity_handle + if action.continue_as_new: + workflow.continue_as_new(action.continue_as_new.param) + if action.wait_until_signal_count: + await workflow.wait_condition( + lambda: self._signal_count >= action.wait_until_signal_count + ) + if action.wait_and_do_update: + ready_for_update.release() + await workflow.wait_condition(lambda: self._did_update) + if action.wait_and_do_start_with_update: + ready_for_update_with_start.release() + await workflow.wait_condition(lambda: self._did_update_with_start) + if action.start_and_cancel_nexus_operation: + nexus_client = workflow.create_nexus_client( + endpoint=make_nexus_endpoint_name(workflow.info().task_queue), + service=InterceptedNexusService, + ) + + nexus_handle = await nexus_client.start_operation( + operation=InterceptedNexusService.intercepted_operation, + input="nexus-workflow", + ) + nexus_handle.cancel() + + try: + await nexus_handle + except NexusOperationError: + pass + + async def _raise_on_non_replay(self) -> None: + replaying = workflow.unsafe.is_replaying() + # We sleep to force a task rollover + await asyncio.sleep(0.01) + if not replaying: + raise RuntimeError("Intentional task failure") + + @workflow.query + def query(self) -> str: + # We're gonna do a custom span here + return "some query" + + @workflow.signal + def signal(self) -> None: + self._signal_count += 1 + + @workflow.update + def update(self) -> None: + self._did_update = True + + @workflow.update + def update_with_start(self) -> None: + self._did_update_with_start = True + + @update.validator + def update_validator(self) -> None: + pass + + +async def test_opentelemetry_tracing(client: Client, env: WorkflowEnvironment): + # TODO(cretz): Fix + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/1424" + ) + global ready_for_update + ready_for_update = asyncio.Semaphore(0) + # Create a tracer that has an in-memory exporter + exporter = InMemorySpanExporter() + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + tracer = get_tracer(__name__, tracer_provider=provider) + # Create new client with tracer interceptor + client_config = client.config() + client_config["interceptors"] = [TracingInterceptor(tracer)] + client = Client(**client_config) + + task_queue = f"task_queue_{uuid.uuid4()}" + async with Worker( + client, + task_queue=task_queue, + workflows=[TracingWorkflow], + activities=[tracing_activity], + # Needed so we can wait to send update at the right time + workflow_runner=UnsandboxedWorkflowRunner(), + ): + # Run workflow with various actions + workflow_id = f"workflow_{uuid.uuid4()}" + handle = await client.start_workflow( + TracingWorkflow.run, + TracingWorkflowParam( + actions=[ + # First fail on replay + TracingWorkflowAction(fail_on_non_replay=True), + # Wait for a signal + TracingWorkflowAction(wait_until_signal_count=1), + # Exec activity that fails task before complete + TracingWorkflowAction( + activity=TracingWorkflowActionActivity( + param=TracingActivityParam(fail_until_attempt=2), + fail_on_non_replay_before_complete=True, + ), + ), + # Wait for update + TracingWorkflowAction(wait_and_do_update=True), + # Exec child workflow that fails task before complete + TracingWorkflowAction( + child_workflow=TracingWorkflowActionChildWorkflow( + id=f"{workflow_id}_child", + # Exec activity and finish after two signals + param=TracingWorkflowParam( + actions=[ + TracingWorkflowAction( + activity=TracingWorkflowActionActivity( + param=TracingActivityParam(), + local=True, + ), + ), + # Wait for the two signals + TracingWorkflowAction(wait_until_signal_count=2), + ] + ), + signal=True, + external_signal=True, + fail_on_non_replay_before_complete=True, + ) + ), + # Continue as new and run one local activity + TracingWorkflowAction( + continue_as_new=TracingWorkflowActionContinueAsNew( + param=TracingWorkflowParam( + # Do a local activity in the continue as new + actions=[ + TracingWorkflowAction( + activity=TracingWorkflowActionActivity( + param=TracingActivityParam(), + local=True, + ), + ) + ] + ) + ) + ), + ], + ), + id=workflow_id, + task_queue=task_queue, + ) + # Send query, then signal to move it along + assert "some query" == await handle.query(TracingWorkflow.query) + await handle.signal(TracingWorkflow.signal) + # Wait to send the update until after the things that fail tasks are over, as failing a task while the update + # is running can mean we execute it twice, which will mess up our spans. + async with ready_for_update: + await handle.execute_update(TracingWorkflow.update) + await handle.result() + + # Dump debug with attributes, but do string assertion test without + logging.debug( + "Spans:\n%s", + "\n".join(dump_spans(exporter.get_finished_spans(), with_attributes=False)), + ) + assert dump_spans(exporter.get_finished_spans(), with_attributes=False) == [ + "StartWorkflow:TracingWorkflow", + " RunWorkflow:TracingWorkflow", + " MyCustomSpan", + " HandleSignal:signal (links: SignalWorkflow:signal)", + " StartActivity:tracing_activity", + " RunActivity:tracing_activity", + " RunActivity:tracing_activity", + " ValidateUpdate:update (links: StartWorkflowUpdate:update)", + " HandleUpdate:update (links: StartWorkflowUpdate:update)", + " StartChildWorkflow:TracingWorkflow", + " RunWorkflow:TracingWorkflow", + " MyCustomSpan", + " StartActivity:tracing_activity", + " RunActivity:tracing_activity", + " HandleSignal:signal (links: SignalChildWorkflow:signal)", + " HandleSignal:signal (links: SignalExternalWorkflow:signal)", + " CompleteWorkflow:TracingWorkflow", + " SignalChildWorkflow:signal", + " SignalExternalWorkflow:signal", + " RunWorkflow:TracingWorkflow", + " MyCustomSpan", + " StartActivity:tracing_activity", + " RunActivity:tracing_activity", + " CompleteWorkflow:TracingWorkflow", + "QueryWorkflow:query", + " HandleQuery:query (links: StartWorkflow:TracingWorkflow)", + "SignalWorkflow:signal", + "StartWorkflowUpdate:update", + ] + + +async def test_opentelemetry_tracing_update_with_start( + client: Client, env: WorkflowEnvironment +): + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/1424" + ) + global ready_for_update_with_start + ready_for_update_with_start = asyncio.Semaphore(0) + # Create a tracer that has an in-memory exporter + exporter = InMemorySpanExporter() + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + tracer = get_tracer(__name__, tracer_provider=provider) + # Create new client with tracer interceptor + client_config = client.config() + client_config["interceptors"] = [TracingInterceptor(tracer)] + client = Client(**client_config) + + task_queue = f"task_queue_{uuid.uuid4()}" + async with Worker( + client, + task_queue=task_queue, + workflows=[TracingWorkflow], + activities=[tracing_activity], + # Needed so we can wait to send update at the right time + workflow_runner=UnsandboxedWorkflowRunner(), + ): + # Run workflow with various actions + workflow_id = f"workflow_{uuid.uuid4()}" + workflow_params = TracingWorkflowParam( + actions=[ + # Wait for update + TracingWorkflowAction(wait_and_do_start_with_update=True), + ] + ) + handle = await client.start_workflow( + TracingWorkflow.run, + workflow_params, + id=workflow_id, + task_queue=task_queue, + ) + async with ready_for_update_with_start: + start_op = WithStartWorkflowOperation( + TracingWorkflow.run, + workflow_params, + id=handle.id, + task_queue=task_queue, + id_conflict_policy=WorkflowIDConflictPolicy.USE_EXISTING, + ) + await client.start_update_with_start_workflow( + TracingWorkflow.update_with_start, + start_workflow_operation=start_op, + id=handle.id, + wait_for_stage=WorkflowUpdateStage.ACCEPTED, + ) + await handle.result() + + # issue update with start again to trigger a new workflow + workflow_id = f"workflow_{uuid.uuid4()}" + start_op = WithStartWorkflowOperation( + TracingWorkflow.run, + TracingWorkflowParam(actions=[]), + id=workflow_id, + task_queue=task_queue, + id_conflict_policy=WorkflowIDConflictPolicy.USE_EXISTING, + ) + await client.execute_update_with_start_workflow( + update=TracingWorkflow.update_with_start, + start_workflow_operation=start_op, + id=workflow_id, + ) + + # Dump debug with attributes, but do string assertion test without + logging.debug( + "Spans:\n%s", + "\n".join(dump_spans(exporter.get_finished_spans(), with_attributes=False)), + ) + assert dump_spans(exporter.get_finished_spans(), with_attributes=False) == [ + "StartWorkflow:TracingWorkflow", + " RunWorkflow:TracingWorkflow", + " MyCustomSpan", + " HandleUpdate:update_with_start (links: StartUpdateWithStartWorkflow:TracingWorkflow)", + " CompleteWorkflow:TracingWorkflow", + "StartUpdateWithStartWorkflow:TracingWorkflow", + "StartUpdateWithStartWorkflow:TracingWorkflow", + " HandleUpdate:update_with_start (links: StartUpdateWithStartWorkflow:TracingWorkflow)", + " RunWorkflow:TracingWorkflow", + " MyCustomSpan", + " CompleteWorkflow:TracingWorkflow", + ] + + +@pytest.mark.requires_local_server +async def test_opentelemetry_tracing_nexus(client: Client, env: WorkflowEnvironment): + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/1424" + ) + global ready_for_update_with_start + ready_for_update_with_start = asyncio.Semaphore(0) + # Create a tracer that has an in-memory exporter + exporter = InMemorySpanExporter() + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + tracer = get_tracer(__name__, tracer_provider=provider) + # Create new client with tracer interceptor + client_config = client.config() + client_config["interceptors"] = [TracingInterceptor(tracer)] + client = Client(**client_config) + + task_queue = f"task-queue-{uuid.uuid4()}" + await env.create_nexus_endpoint(make_nexus_endpoint_name(task_queue), task_queue) + async with Worker( + client, + task_queue=task_queue, + workflows=[TracingWorkflow, ExpectCancelNexusWorkflow], + activities=[tracing_activity], + nexus_service_handlers=[InterceptedNexusService()], + # Needed so we can wait to send update at the right time + workflow_runner=UnsandboxedWorkflowRunner(), + ): + # Run workflow with various actions + workflow_id = f"workflow_{uuid.uuid4()}" + workflow_params = TracingWorkflowParam( + actions=[ + TracingWorkflowAction(start_and_cancel_nexus_operation=True), + ] + ) + handle = await client.start_workflow( + TracingWorkflow.run, + workflow_params, + id=workflow_id, + task_queue=task_queue, + ) + await handle.result() + + # Dump debug with attributes, but do string assertion test without + logging.debug( + "Spans:\n%s", + "\n".join(dump_spans(exporter.get_finished_spans(), with_attributes=False)), + ) + assert dump_spans(exporter.get_finished_spans(), with_attributes=False) == [ + "StartWorkflow:TracingWorkflow", + " RunWorkflow:TracingWorkflow", + " MyCustomSpan", + " StartNexusOperation:InterceptedNexusService/intercepted_operation", + " RunStartNexusOperationHandler:InterceptedNexusService/intercepted_operation", + " StartWorkflow:ExpectCancelNexusWorkflow", + " RunWorkflow:ExpectCancelNexusWorkflow", + " RunCancelNexusOperationHandler:InterceptedNexusService/intercepted_operation", + " CompleteWorkflow:TracingWorkflow", + ] + + +def dump_spans( + spans: Iterable[ReadableSpan], + *, + parent_id: int | None = None, + with_attributes: bool = True, + indent_depth: int = 0, +) -> list[str]: + ret: list[str] = [] + for span in spans: + if (not span.parent and parent_id is None) or ( + span.parent and span.parent.span_id == parent_id + ): + span_str = f"{' ' * indent_depth}{span.name}" + if with_attributes: + span_str += f" (attributes: {dict(span.attributes or {})})" + # Add links + if span.links: + span_links: list[str] = [] + for link in span.links: + for link_span in spans: + if ( + link_span.context is not None + and link_span.context.span_id == link.context.span_id + ): + span_links.append(link_span.name) + span_str += f" (links: {', '.join(span_links)})" + # Signals can duplicate in rare situations, so we make sure not to + # re-add + if "Signal" in span_str and span_str in ret: + continue + ret.append(span_str) + ret += dump_spans( + spans, + parent_id=span.context.span_id if span.context else None, + with_attributes=with_attributes, + indent_depth=indent_depth + 1, + ) + return ret + + +@workflow.defn +class SimpleWorkflow: + @workflow.run + async def run(self) -> str: + return "done" + + +async def test_opentelemetry_always_create_workflow_spans(client: Client): + # Create a tracer that has an in-memory exporter + exporter = InMemorySpanExporter() + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + tracer = get_tracer(__name__, tracer_provider=provider) + + # Create a worker with an interceptor without always create + async with Worker( + client, + task_queue=f"task_queue_{uuid.uuid4()}", + workflows=[SimpleWorkflow], + interceptors=[TracingInterceptor(tracer)], + ) as worker: + assert "done" == await client.execute_workflow( + SimpleWorkflow.run, + id=f"workflow_{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + # Confirm the spans are not there + spans = exporter.get_finished_spans() + logging.debug("Spans:\n%s", "\n".join(dump_spans(spans, with_attributes=False))) + assert len(spans) == 0 + + # Now create a worker with an interceptor with always create + async with Worker( + client, + task_queue=f"task_queue_{uuid.uuid4()}", + workflows=[SimpleWorkflow], + interceptors=[TracingInterceptor(tracer, always_create_workflow_spans=True)], + ) as worker: + assert "done" == await client.execute_workflow( + SimpleWorkflow.run, + id=f"workflow_{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + # Confirm the spans are not there + spans = exporter.get_finished_spans() + logging.debug("Spans:\n%s", "\n".join(dump_spans(spans, with_attributes=False))) + assert len(spans) > 0 + assert spans[0].name == "RunWorkflow:SimpleWorkflow" + + +attempted = False + + +@activity.defn +def benign_activity() -> str: + global attempted + if attempted: + return "done" + attempted = True + raise ApplicationError( + category=ApplicationErrorCategory.BENIGN, message="Benign Error" + ) + + +@workflow.defn +class BenignWorkflow: + @workflow.run + async def run(self) -> str: + return await workflow.execute_activity( + benign_activity, schedule_to_close_timeout=timedelta(seconds=1) + ) + + +async def test_opentelemetry_benign_exception(client: Client): + # Create a tracer that has an in-memory exporter + exporter = InMemorySpanExporter() + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + tracer = get_tracer(__name__, tracer_provider=provider) + + # Create new client with tracer interceptor + client_config = client.config() + client_config["interceptors"] = [TracingInterceptor(tracer)] + client = Client(**client_config) + + async with Worker( + client, + task_queue=f"task_queue_{uuid.uuid4()}", + workflows=[BenignWorkflow], + activities=[benign_activity], + activity_executor=ThreadPoolExecutor(max_workers=1), + ) as worker: + assert "done" == await client.execute_workflow( + BenignWorkflow.run, + id=f"workflow_{uuid.uuid4()}", + task_queue=worker.task_queue, + retry_policy=RetryPolicy( + maximum_attempts=2, initial_interval=timedelta(milliseconds=10) + ), + ) + spans = exporter.get_finished_spans() + assert all(span.status.status_code == StatusCode.UNSET for span in spans) + + +@contextmanager +def baggage_values(values: dict[str, str]) -> Generator[None]: + ctx = context.get_current() + for key, value in values.items(): + ctx = baggage.set_baggage(key, value, context=ctx) + + token = context.attach(ctx) + try: + yield + finally: + context.detach(token) + + +@pytest.fixture +def client_with_tracing(client: Client) -> Client: + tracer = get_tracer(__name__, tracer_provider=TracerProvider()) + client_config = client.config() + client_config["interceptors"] = [TracingInterceptor(tracer)] + return Client(**client_config) + + +def get_baggage_value(key: str) -> str: + return cast("str", baggage.get_baggage(key)) + + +@activity.defn +async def read_baggage_activity() -> dict[str, str]: + return { + "user_id": get_baggage_value("user.id"), + "tenant_id": get_baggage_value("tenant.id"), + } + + +@workflow.defn +class ReadBaggageTestWorkflow: + @workflow.run + async def run(self) -> dict[str, str]: + return await workflow.execute_activity( + read_baggage_activity, + start_to_close_timeout=timedelta(seconds=10), + ) + + +async def test_opentelemetry_baggage_propagation_basic(client_with_tracing: Client): + task_queue = f"task_queue_{uuid.uuid4()}" + async with Worker( + client_with_tracing, + task_queue=task_queue, + workflows=[ReadBaggageTestWorkflow], + activities=[read_baggage_activity], + ): + with baggage_values({"user.id": "test-user-123", "tenant.id": "some-corp"}): + result = await client_with_tracing.execute_workflow( + ReadBaggageTestWorkflow.run, + id=f"workflow_{uuid.uuid4()}", + task_queue=task_queue, + ) + + assert result["user_id"] == "test-user-123", ( + "user.id baggage should propagate to activity" + ) + assert result["tenant_id"] == "some-corp", ( + "tenant.id baggage should propagate to activity" + ) + + +@activity.defn +async def read_baggage_local_activity() -> dict[str, str]: + return { + "user_id": get_baggage_value("user.id"), + "tenant_id": get_baggage_value("tenant.id"), + } + + +@workflow.defn +class LocalActivityBaggageTestWorkflow: + @workflow.run + async def run(self) -> dict[str, str]: + return await workflow.execute_local_activity( + read_baggage_local_activity, + start_to_close_timeout=timedelta(seconds=10), + ) + + +async def test_opentelemetry_baggage_propagation_local_activity( + client_with_tracing: Client, +): + task_queue = f"task_queue_{uuid.uuid4()}" + async with Worker( + client_with_tracing, + task_queue=task_queue, + workflows=[LocalActivityBaggageTestWorkflow], + activities=[read_baggage_local_activity], + ): + with baggage_values( + { + "user.id": "test-user-456", + "tenant.id": "local-corp", + } + ): + result = await client_with_tracing.execute_workflow( + LocalActivityBaggageTestWorkflow.run, + id=f"workflow_{uuid.uuid4()}", + task_queue=task_queue, + ) + + assert result["user_id"] == "test-user-456" + assert result["tenant_id"] == "local-corp" + + +retry_attempt_baggage_values: list[str] = [] + + +@activity.defn +async def failing_baggage_activity() -> None: + retry_attempt_baggage_values.append(get_baggage_value("user.id")) + if activity.info().attempt < 2: + raise RuntimeError("Intentional failure") + + +@workflow.defn +class RetryBaggageTestWorkflow: + @workflow.run + async def run(self) -> None: + await workflow.execute_activity( + failing_baggage_activity, + start_to_close_timeout=timedelta(seconds=10), + retry_policy=RetryPolicy(initial_interval=timedelta(milliseconds=1)), + ) + + +async def test_opentelemetry_baggage_propagation_with_retries( + client_with_tracing: Client, +) -> None: + global retry_attempt_baggage_values + retry_attempt_baggage_values = [] + + task_queue = f"task_queue_{uuid.uuid4()}" + async with Worker( + client_with_tracing, + task_queue=task_queue, + workflows=[RetryBaggageTestWorkflow], + activities=[failing_baggage_activity], + ): + with baggage_values({"user.id": "test-user-retry"}): + await client_with_tracing.execute_workflow( + RetryBaggageTestWorkflow.run, + id=f"workflow_{uuid.uuid4()}", + task_queue=task_queue, + ) + + # Verify baggage was present on all attempts + assert len(retry_attempt_baggage_values) == 2 + assert all(v == "test-user-retry" for v in retry_attempt_baggage_values) + + +@activity.defn +async def context_clear_noop_activity() -> None: + pass + + +@activity.defn +async def context_clear_exception_activity() -> None: + raise Exception("Simulated exception") + + +@workflow.defn +class ContextClearWorkflow: + @workflow.run + async def run(self) -> None: + await workflow.execute_activity( + context_clear_noop_activity, + start_to_close_timeout=timedelta(seconds=10), + retry_policy=RetryPolicy( + maximum_attempts=1, initial_interval=timedelta(milliseconds=1) + ), + ) + + +@pytest.mark.parametrize( + "activity,expect_failure", + [ + (context_clear_noop_activity, not True), + (context_clear_exception_activity, True), + ], +) +async def test_opentelemetry_context_restored_after_activity( + client_with_tracing: Client, + activity: Callable[[], None], + expect_failure: bool, +) -> None: + attach_count = 0 + detach_count = 0 + original_attach = context.attach + original_detach = context.detach + + def tracked_attach(ctx): # type:ignore[reportMissingParameterType] + nonlocal attach_count + attach_count += 1 + return original_attach(ctx) + + def tracked_detach(token): # type:ignore[reportMissingParameterType] + nonlocal detach_count + detach_count += 1 + return original_detach(token) + + context.attach = tracked_attach + context.detach = tracked_detach + + try: + task_queue = f"task_queue_{uuid.uuid4()}" + async with Worker( + client_with_tracing, + task_queue=task_queue, + workflows=[ContextClearWorkflow], + activities=[activity], + ): + with baggage_values({"user.id": "test-123"}): + try: + await client_with_tracing.execute_workflow( + ContextClearWorkflow.run, + id=f"workflow_{uuid.uuid4()}", + task_queue=task_queue, + ) + assert not expect_failure, ( + "This test should have raised an exception" + ) + except Exception: + assert expect_failure, "This test is not expeced to raise" + + assert attach_count == detach_count, ( + f"Context leak detected: {attach_count} attaches vs {detach_count} detaches. " + ) + assert attach_count > 0, "Expected at least one context attach/detach" + + finally: + context.attach = original_attach + context.detach = original_detach + + +@activity.defn +async def simple_no_context_activity() -> str: + return "success" + + +@workflow.defn +class SimpleNoContextWorkflow: + @workflow.run + async def run(self) -> str: + return await workflow.execute_activity( + simple_no_context_activity, + start_to_close_timeout=timedelta(seconds=10), + ) + + +async def test_opentelemetry_interceptor_works_if_no_context( + client_with_tracing: Client, +): + task_queue = f"task_queue_{uuid.uuid4()}" + async with Worker( + client_with_tracing, + task_queue=task_queue, + workflows=[SimpleNoContextWorkflow], + activities=[simple_no_context_activity], + ): + result = await client_with_tracing.execute_workflow( + SimpleNoContextWorkflow.run, + id=f"workflow_{uuid.uuid4()}", + task_queue=task_queue, + ) + + assert result == "success" + + +# TODO(cretz): Additional tests to write +# * query without interceptor (no headers) +# * workflow without interceptor (no headers) but query with interceptor (headers) +# * workflow failure and wft failure +# * signal with start +# * signal failure and wft failure from signal + + +async def test_opentelemetry_standalone_activity_tracing( + client: Client, env: WorkflowEnvironment +): + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + exporter = InMemorySpanExporter() + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + tracer = get_tracer(__name__, tracer_provider=provider) + client_config = client.config() + client_config["interceptors"] = [TracingInterceptor(tracer)] + client = Client(**client_config) + + task_queue = f"task_queue_{uuid.uuid4()}" + activity_id = f"activity_{uuid.uuid4()}" + async with Worker( + client, + task_queue=task_queue, + activities=[tracing_activity], + ): + handle = await client.start_activity( + tracing_activity, + TracingActivityParam(heartbeat=False), + id=activity_id, + task_queue=task_queue, + schedule_to_close_timeout=timedelta(seconds=10), + ) + await handle.result() + + finished_spans = exporter.get_finished_spans() + assert dump_spans(finished_spans, with_attributes=False) == [ + "StartActivity:tracing_activity", + " RunActivity:tracing_activity", + ] + start_activity_span = next( + s for s in finished_spans if s.name == "StartActivity:tracing_activity" + ) + assert start_activity_span.attributes is not None + assert start_activity_span.attributes["temporalActivityID"] == activity_id + assert start_activity_span.attributes["temporalActivityType"] == "tracing_activity" + + +def test_opentelemetry_safe_detach(): + class _fake_self: + def _load_workflow_context_carrier(*_args): + return None + + def _set_on_context(self, ctx: Any): + return opentelemetry.context.set_value("test-key", "test-value", ctx) + + def _completed_span(*args: Any, **_kwargs: Any): + pass + + # create a context manager and force enter to happen on this thread + context_manager = TracingWorkflowInboundInterceptor._top_level_workflow_context( + _fake_self(), # type: ignore + success_is_complete=True, + ) + context_manager.__enter__() + + # move reference to context manager into queue + q: queue.Queue = queue.Queue() + q.put(context_manager) + del context_manager + + def worker(): + # pull reference from queue and delete the last reference + context_manager = q.get() + del context_manager + # force gc + gc.collect() + + with LogCapturer().logs_captured(opentelemetry.context.logger) as capturer: + # run forced gc on other thread so exit happens there + t = threading.Thread(target=worker) + t.start() + t.join(timeout=5) + + def otel_context_error(record: logging.LogRecord) -> bool: + return ( + record.name == "opentelemetry.context" + and "Failed to detach context" in record.message + ) + + assert capturer.find(otel_context_error) is None, ( + "Detach from context message should not be logged" + ) diff --git a/tests/contrib/opentelemetry/test_opentelemetry_plugin.py b/tests/contrib/opentelemetry/test_opentelemetry_plugin.py new file mode 100644 index 000000000..337270d0c --- /dev/null +++ b/tests/contrib/opentelemetry/test_opentelemetry_plugin.py @@ -0,0 +1,650 @@ +import logging +import uuid +from datetime import timedelta +from typing import Any + +import nexusrpc +import opentelemetry.trace +import pytest +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) +from opentelemetry.trace import ( + get_tracer, +) + +import temporalio.contrib.opentelemetry.workflow +from temporalio import activity, nexus, workflow +from temporalio.client import Client, WorkflowFailureError +from temporalio.contrib.opentelemetry import OpenTelemetryPlugin, create_tracer_provider +from temporalio.exceptions import ApplicationError +from temporalio.testing import WorkflowEnvironment + +# Import the dump_spans function from the original opentelemetry test +from tests.contrib.opentelemetry.test_opentelemetry import dump_spans +from tests.helpers import new_worker +from tests.helpers.nexus import make_nexus_endpoint_name + +logger = logging.getLogger(__name__) + + +@activity.defn +async def simple_no_context_activity() -> str: + with get_tracer(__name__).start_as_current_span("Activity"): + pass + return "success" + + +@workflow.defn +class SimpleNexusWorkflow: + @workflow.run + async def run(self, input: str) -> str: + return f"nexus-result-{input}" + + +@nexusrpc.handler.service_handler +class ComprehensiveNexusService: + @nexus.workflow_run_operation + async def test_operation( + self, ctx: nexus.WorkflowRunOperationContext, input: str + ) -> nexus.WorkflowHandle[str]: + return await ctx.start_workflow( + SimpleNexusWorkflow.run, + input, + id=f"nexus-wf-{ctx.request_id}", + ) + + +@workflow.defn +class BasicTraceWorkflow: + @workflow.run + async def run(self): + tracer = get_tracer(__name__) + temporalio.contrib.opentelemetry.workflow.completed_span("Completed Span") + with tracer.start_as_current_span("Hello World"): + await workflow.execute_activity( + simple_no_context_activity, + start_to_close_timeout=timedelta(seconds=10), + ) + await workflow.execute_activity( + simple_no_context_activity, + start_to_close_timeout=timedelta(seconds=10), + ) + span = tracer.start_span("Not context") + with tracer.start_as_current_span("Inner"): + await workflow.execute_activity( + simple_no_context_activity, + start_to_close_timeout=timedelta(seconds=10), + ) + span.end() + return + + +async def test_otel_tracing_basic(client: Client, reset_otel_tracer_provider: Any): # type: ignore[reportUnusedParameter] + exporter = InMemorySpanExporter() + provider = create_tracer_provider() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + opentelemetry.trace.set_tracer_provider(provider) + + new_config = client.config() + new_config["plugins"] = [OpenTelemetryPlugin()] + new_client = Client(**new_config) + + async with new_worker( + new_client, + BasicTraceWorkflow, + activities=[simple_no_context_activity], + max_cached_workflows=0, + ) as worker: + tracer = get_tracer(__name__) + + with tracer.start_as_current_span("Research workflow"): + workflow_handle = await new_client.start_workflow( + BasicTraceWorkflow.run, + id=f"research-workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=120), + ) + await workflow_handle.result() + + spans = exporter.get_finished_spans() + + expected_hierarchy = [ + "Research workflow", + " Completed Span", + " Hello World", + " Activity", + " Activity", + " Inner", + " Activity", + " Not context", + ] + + # Verify the span hierarchy matches expectations + actual_hierarchy = dump_spans(spans, with_attributes=False) + assert actual_hierarchy == expected_hierarchy, ( + f"Span hierarchy mismatch.\nExpected:\n{expected_hierarchy}\nActual:\n{actual_hierarchy}" + ) + + +@workflow.defn +class ComprehensiveWorkflow: + def __init__(self) -> None: + self._signal_count = 0 + self._update_completed = False + self._nexus_result: str = "" + + @workflow.run + async def run(self, actions: list[str]) -> dict[str, str]: + results = {} + tracer = get_tracer(__name__) + with tracer.start_as_current_span("MainWorkflow"): + for action in actions: + if action == "activity": + with tracer.start_as_current_span("ActivitySection"): + result = await workflow.execute_activity( + simple_no_context_activity, + start_to_close_timeout=timedelta(seconds=10), + ) + results["activity"] = result + + elif action == "local_activity": + with tracer.start_as_current_span("LocalActivitySection"): + result = await workflow.execute_local_activity( + simple_no_context_activity, + start_to_close_timeout=timedelta(seconds=10), + ) + results["local_activity"] = result + + elif action == "child_workflow": + with tracer.start_as_current_span("ChildWorkflowSection"): + child_handle = await workflow.start_child_workflow( + BasicTraceWorkflow.run, + id=f"child-{workflow.info().workflow_id}", + ) + + await child_handle + results["child_workflow"] = "completed" + + elif action == "timer": + with tracer.start_as_current_span("TimerSection"): + await workflow.sleep(0.01) + results["timer"] = "completed" + + elif action == "wait_signal": + with tracer.start_as_current_span("WaitSignalSection"): + await workflow.wait_condition(lambda: self._signal_count > 0) + results["wait_signal"] = ( + f"received_{self._signal_count}_signals" + ) + + elif action == "wait_update": + with tracer.start_as_current_span("WaitUpdateSection"): + await workflow.wait_condition(lambda: self._update_completed) + results["wait_update"] = "update_received" + + elif action == "nexus": + with tracer.start_as_current_span("NexusSection"): + nexus_client = workflow.create_nexus_client( + endpoint=make_nexus_endpoint_name( + workflow.info().task_queue + ), + service=ComprehensiveNexusService, + ) + nexus_handle = await nexus_client.start_operation( + operation=ComprehensiveNexusService.test_operation, + input="test-input", + ) + nexus_result = await nexus_handle + results["nexus"] = nexus_result + + elif action == "continue_as_new": + with tracer.start_as_current_span("ContinueAsNewSection"): + if ( + len(results) > 0 + ): # Only continue as new if we've done some work + workflow.continue_as_new( + [] + ) # Empty actions to finish quickly + results["continue_as_new"] = "prepared" + + return results + + @workflow.query + def get_status(self) -> dict[str, Any]: + return { + "signal_count": self._signal_count, + "update_completed": self._update_completed, + } + + @workflow.signal + def notify(self, message: str) -> None: # type: ignore[reportUnusedParameter] + self._signal_count += 1 + + @workflow.update + def update_status(self, status: str) -> str: + self._update_completed = True + return f"updated_to_{status}" + + @update_status.validator + def validate_update_status(self, status: str) -> None: + if not status: + raise ValueError("Status cannot be empty") + + +@pytest.mark.requires_local_server +async def test_opentelemetry_comprehensive_tracing( + client: Client, + env: WorkflowEnvironment, + reset_otel_tracer_provider: Any, # type: ignore[reportUnusedParameter] +): + """Test OpenTelemetry v2 integration across all workflow operations.""" + if env.supports_time_skipping: + pytest.skip("Fails on java test server.") + + exporter = InMemorySpanExporter() + provider = create_tracer_provider() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + opentelemetry.trace.set_tracer_provider(provider) + + new_config = client.config() + new_config["plugins"] = [OpenTelemetryPlugin(add_temporal_spans=True)] + new_client = Client(**new_config) + + async with new_worker( + new_client, + ComprehensiveWorkflow, + BasicTraceWorkflow, # For child workflow + SimpleNexusWorkflow, # For Nexus operation + activities=[simple_no_context_activity], + nexus_service_handlers=[ComprehensiveNexusService()], + max_cached_workflows=0, + ) as worker: + # Create Nexus endpoint for this task queue + await env.create_nexus_endpoint( + make_nexus_endpoint_name(worker.task_queue), worker.task_queue + ) + + with get_tracer(__name__).start_as_current_span("ComprehensiveTest") as span: + span.set_attribute("test.type", "comprehensive") + + # Start workflow with various actions + workflow_handle = await new_client.start_workflow( + ComprehensiveWorkflow.run, + [ + "activity", + "local_activity", + "child_workflow", + "timer", + "nexus", + "wait_signal", + "wait_update", + ], + id=f"comprehensive-workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=120), + ) + + logger.info(f"Comprehensive workflow query") + + # Test query + status = await workflow_handle.query(ComprehensiveWorkflow.get_status) + assert status["signal_count"] == 0 + + logger.info(f"Comprehensive workflow signal") + + # Test signal + await workflow_handle.signal(ComprehensiveWorkflow.notify, "test-signal-1") + await workflow_handle.signal(ComprehensiveWorkflow.notify, "test-signal-2") + + logger.info(f"Comprehensive workflow update") + + # Test update + update_result = await workflow_handle.execute_update( + ComprehensiveWorkflow.update_status, "active" + ) + assert update_result == "updated_to_active" + + logger.info(f"Comprehensive workflow get result") + + # Get final result + result = await workflow_handle.result() + + # Verify results + expected_keys = { + "activity", + "local_activity", + "child_workflow", + "timer", + "nexus", + "wait_signal", + "wait_update", + } + assert all(key in result for key in expected_keys) + assert result["activity"] == "success" + assert result["local_activity"] == "success" + assert result["child_workflow"] == "completed" + assert result["timer"] == "completed" + assert result["nexus"] == "nexus-result-test-input" + assert result["wait_signal"] == "received_2_signals" + assert result["wait_update"] == "update_received" + + spans = exporter.get_finished_spans() + + # Note: Even though we call signal twice, dump_spans() deduplicates signal spans + # as they "can duplicate in rare situations" according to the original test + + expected_hierarchy = [ + "ComprehensiveTest", + " StartWorkflow:ComprehensiveWorkflow", + " RunWorkflow:ComprehensiveWorkflow", + " MainWorkflow", + " ActivitySection", + " StartActivity:simple_no_context_activity", + " RunActivity:simple_no_context_activity", + " Activity", + " LocalActivitySection", + " StartActivity:simple_no_context_activity", + " RunActivity:simple_no_context_activity", + " Activity", + " ChildWorkflowSection", + " StartChildWorkflow:BasicTraceWorkflow", + " RunWorkflow:BasicTraceWorkflow", + " Completed Span", + " Hello World", + " StartActivity:simple_no_context_activity", + " RunActivity:simple_no_context_activity", + " Activity", + " StartActivity:simple_no_context_activity", + " RunActivity:simple_no_context_activity", + " Activity", + " Inner", + " StartActivity:simple_no_context_activity", + " RunActivity:simple_no_context_activity", + " Activity", + " Not context", + " TimerSection", + " NexusSection", + " StartNexusOperation:ComprehensiveNexusService/test_operation", + " RunStartNexusOperationHandler:ComprehensiveNexusService/test_operation", + " StartWorkflow:SimpleNexusWorkflow", + " RunWorkflow:SimpleNexusWorkflow", + " WaitSignalSection", + " WaitUpdateSection", + " QueryWorkflow:get_status", + " HandleQuery:get_status", + " SignalWorkflow:notify", + " HandleSignal:notify", + " StartWorkflowUpdate:update_status", + " ValidateUpdate:update_status", + " HandleUpdate:update_status", + ] + + # Verify the span hierarchy matches expectations + actual_hierarchy = dump_spans(spans, with_attributes=False) + assert actual_hierarchy == expected_hierarchy, ( + f"Span hierarchy mismatch.\nExpected:\n{expected_hierarchy}\nActual:\n{actual_hierarchy}" + ) + + +async def test_otel_tracing_with_added_spans( + client: Client, + reset_otel_tracer_provider: Any, # type: ignore[reportUnusedParameter] +): + exporter = InMemorySpanExporter() + provider = create_tracer_provider() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + opentelemetry.trace.set_tracer_provider(provider) + + plugin = OpenTelemetryPlugin(add_temporal_spans=True) + new_config = client.config() + new_config["plugins"] = [plugin] + new_client = Client(**new_config) + + async with new_worker( + new_client, + BasicTraceWorkflow, + activities=[simple_no_context_activity], + max_cached_workflows=0, + ) as worker: + with get_tracer(__name__).start_as_current_span("Research workflow"): + workflow_handle = await new_client.start_workflow( + BasicTraceWorkflow.run, + id=f"research-workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=120), + ) + await workflow_handle.result() + + spans = exporter.get_finished_spans() + + expected_hierarchy = [ + "Research workflow", + " StartWorkflow:BasicTraceWorkflow", + " RunWorkflow:BasicTraceWorkflow", + " Completed Span", + " Hello World", + " StartActivity:simple_no_context_activity", + " RunActivity:simple_no_context_activity", + " Activity", + " StartActivity:simple_no_context_activity", + " RunActivity:simple_no_context_activity", + " Activity", + " Inner", + " StartActivity:simple_no_context_activity", + " RunActivity:simple_no_context_activity", + " Activity", + " Not context", + ] + + # Verify the span hierarchy matches expectations + actual_hierarchy = dump_spans(spans, with_attributes=False) + assert actual_hierarchy == expected_hierarchy, ( + f"Span hierarchy mismatch.\nExpected:\n{expected_hierarchy}\nActual:\n{actual_hierarchy}" + ) + + +task_fail_once_workflow_has_failed = False + + +@workflow.defn(sandboxed=False) +class FailingTaskWorkflow: + @workflow.run + async def run(self): + tracer = get_tracer(__name__) + with tracer.start_as_current_span("FailingWorkflowSpan"): + with tracer.start_as_current_span("FailingWorkflow CompletedSpan"): + pass + global task_fail_once_workflow_has_failed + if not task_fail_once_workflow_has_failed: + task_fail_once_workflow_has_failed = True + raise RuntimeError("Intentional workflow task failure") + task_fail_once_workflow_has_failed = False + + return + + +async def test_otel_tracing_workflow_task_failure( + client: Client, + reset_otel_tracer_provider: Any, # type: ignore[reportUnusedParameter] +): + """Test OpenTelemetry behavior when a workflow task fails.""" + exporter = InMemorySpanExporter() + provider = create_tracer_provider() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + opentelemetry.trace.set_tracer_provider(provider) + + new_config = client.config() + new_config["plugins"] = [OpenTelemetryPlugin(add_temporal_spans=True)] + new_client = Client(**new_config) + + async with new_worker( + new_client, + FailingTaskWorkflow, + max_cached_workflows=0, + ) as worker: + with get_tracer(__name__).start_as_current_span("FailingWorkflowTest"): + workflow_handle = await new_client.start_workflow( + FailingTaskWorkflow.run, + id=f"failing-workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=30), + ) + + await workflow_handle.result() + + spans = exporter.get_finished_spans() + + # Verify the span hierarchy includes the failure, but only once + # Spans which completed during the failed task will duplicate + expected_hierarchy = [ + "FailingWorkflowTest", + " StartWorkflow:FailingTaskWorkflow", + " RunWorkflow:FailingTaskWorkflow", + " FailingWorkflowSpan", + " FailingWorkflow CompletedSpan", + " FailingWorkflow CompletedSpan", + ] + + actual_hierarchy = dump_spans(spans, with_attributes=False) + assert actual_hierarchy == expected_hierarchy, ( + f"Span hierarchy mismatch.\nExpected:\n{expected_hierarchy}\nActual:\n{actual_hierarchy}" + ) + + +@workflow.defn +class FailingWorkflow: + @workflow.run + async def run(self): + tracer = get_tracer(__name__) + with tracer.start_as_current_span("FailingWorkflowSpan"): + raise ApplicationError("Intentional workflow failure", non_retryable=True) + + +async def test_otel_tracing_workflow_failure( + client: Client, + reset_otel_tracer_provider: Any, # type: ignore[reportUnusedParameter] +): + """Test OpenTelemetry behavior when a workflow task fails.""" + exporter = InMemorySpanExporter() + provider = create_tracer_provider() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + opentelemetry.trace.set_tracer_provider(provider) + + new_config = client.config() + new_config["plugins"] = [OpenTelemetryPlugin(add_temporal_spans=True)] + new_client = Client(**new_config) + + async with new_worker( + new_client, + FailingWorkflow, + max_cached_workflows=0, + ) as worker: + with get_tracer(__name__).start_as_current_span("FailingWorkflowTest"): + workflow_handle = await new_client.start_workflow( + FailingWorkflow.run, + id=f"failing-workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=30), + ) + + with pytest.raises(WorkflowFailureError): + await workflow_handle.result() + + spans = exporter.get_finished_spans() + + # Verify the span hierarchy includes the failure when it fails the whole workflow + expected_hierarchy = [ + "FailingWorkflowTest", + " StartWorkflow:FailingWorkflow", + " RunWorkflow:FailingWorkflow", + " FailingWorkflowSpan", + ] + + actual_hierarchy = dump_spans(spans, with_attributes=False) + assert actual_hierarchy == expected_hierarchy, ( + f"Span hierarchy mismatch.\nExpected:\n{expected_hierarchy}\nActual:\n{actual_hierarchy}" + ) + + +async def test_otel_standalone_activity_tracing( + client: Client, + env: WorkflowEnvironment, + reset_otel_tracer_provider: Any, # type: ignore[reportUnusedParameter] +): + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + exporter = InMemorySpanExporter() + provider = create_tracer_provider() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + opentelemetry.trace.set_tracer_provider(provider) + + new_config = client.config() + new_config["plugins"] = [OpenTelemetryPlugin(add_temporal_spans=True)] + new_client = Client(**new_config) + + activity_id = f"activity_{uuid.uuid4()}" + async with new_worker( + new_client, + activities=[simple_no_context_activity], + ) as worker: + handle = await new_client.start_activity( + simple_no_context_activity, + id=activity_id, + task_queue=worker.task_queue, + schedule_to_close_timeout=timedelta(seconds=10), + ) + await handle.result() + + finished_spans = exporter.get_finished_spans() + assert dump_spans(finished_spans, with_attributes=False) == [ + "StartActivity:simple_no_context_activity", + " RunActivity:simple_no_context_activity", + " Activity", + ] + start_activity_span = next( + s + for s in finished_spans + if s.name == "StartActivity:simple_no_context_activity" + and s.attributes is not None + and s.attributes.get("temporalActivityID") == activity_id + ) + assert start_activity_span.attributes is not None + assert ( + start_activity_span.attributes["temporalActivityType"] + == "simple_no_context_activity" + ) + + +def test_replay_safe_span_delegates_extra_attributes(): + """Test that _ReplaySafeSpan delegates attribute access to the underlying span. + + Concrete span implementations (e.g. opentelemetry.sdk.trace.Span) expose + attributes beyond the Span ABC such as .attributes, .name, .kind, and + .resource. _ReplaySafeSpan must forward these so that instrumentation + libraries that rely on them work correctly. + """ + from opentelemetry.sdk.trace import TracerProvider as SdkTracerProvider + + from temporalio.contrib.opentelemetry._tracer_provider import _ReplaySafeSpan + + provider = SdkTracerProvider() + tracer = provider.get_tracer("test") + inner_span = tracer.start_span("test-span") + + wrapper = _ReplaySafeSpan(inner_span) + + # These properties exist on the SDK span but not on the Span ABC + assert wrapper.name == "test-span" + assert wrapper.kind is not None + assert wrapper.resource is not None + assert wrapper.attributes is not None or wrapper.attributes == {} + + # Verify that AttributeError is still raised for truly missing attributes + with pytest.raises(AttributeError): + _ = wrapper.nonexistent_attribute_xyz + + inner_span.end() diff --git a/tests/contrib/pydantic/activities.py b/tests/contrib/pydantic/activities.py index ad1b86507..7cd4abd30 100644 --- a/tests/contrib/pydantic/activities.py +++ b/tests/contrib/pydantic/activities.py @@ -1,5 +1,4 @@ from datetime import datetime -from typing import List from uuid import UUID from temporalio import activity @@ -8,8 +7,8 @@ @activity.defn async def pydantic_objects_activity( - models: List[PydanticModels], -) -> List[PydanticModels]: + models: list[PydanticModels], +) -> list[PydanticModels]: return models diff --git a/tests/contrib/pydantic/models.py b/tests/contrib/pydantic/models.py index e2fdcfe2c..dddeb5d7e 100644 --- a/tests/contrib/pydantic/models.py +++ b/tests/contrib/pydantic/models.py @@ -1,18 +1,14 @@ import dataclasses import uuid +from collections.abc import Sequence from datetime import date, datetime, time, timedelta, timezone from ipaddress import IPv4Address from pathlib import Path from typing import ( Annotated, Any, - Dict, Generic, - List, - Sequence, - Tuple, TypeVar, - Union, cast, ) @@ -114,7 +110,7 @@ class ChildModel(BaseModel): class ParentModel(BaseModel): child: ChildModel - children: List[ChildModel] + children: list[ChildModel] def _check_instance(self) -> None: assert isinstance(self.child, ChildModel) @@ -166,7 +162,7 @@ def make_field_features_object() -> FieldFeaturesModel: class AnnotatedFieldsModel(BaseModel): max_length_str: Annotated[str, Len(max_length=10)] - custom_json: Annotated[Dict[str, Any], WithJsonSchema({"extra": "data"})] + custom_json: Annotated[dict[str, Any], WithJsonSchema({"extra": "data"})] def _check_instance(self) -> None: assert isinstance(self.max_length_str, str) @@ -188,7 +184,7 @@ def make_annotated_fields_object() -> AnnotatedFieldsModel: class GenericModel(BaseModel, Generic[T]): value: T - values: List[T] + values: list[T] def _check_instance(self) -> None: assert isinstance(self.value, str) @@ -206,8 +202,8 @@ def make_generic_string_object() -> GenericModel[str]: class UnionModel(BaseModel): - simple_union_field: Union[str, int] - proxied_union_field: Union[datetime, Path] + simple_union_field: str | int + proxied_union_field: datetime | Path def _check_instance(self) -> None: assert isinstance(self.simple_union_field, str) @@ -231,9 +227,9 @@ class PydanticDatetimeModel(BaseModel): ) annotated_datetime: Annotated[datetime, Field(), WithJsonSchema({"extra": "data"})] annotated_list_of_datetime: Annotated[ - List[datetime], Field(), WithJsonSchema({"extra": "data"}) + list[datetime], Field(), WithJsonSchema({"extra": "data"}) ] - datetime_short_sequence: ShortSequence[List[datetime]] + datetime_short_sequence: ShortSequence[list[datetime]] def _check_instance(self): _assert_datetime_validity(self.datetime_field) @@ -275,9 +271,9 @@ class PydanticDateModel(BaseModel): date_field_with_default: date = Field(default_factory=lambda: date(2000, 1, 2)) annotated_date: Annotated[date, Field(), WithJsonSchema({"extra": "data"})] annotated_list_of_date: Annotated[ - List[date], Field(), WithJsonSchema({"extra": "data"}) + list[date], Field(), WithJsonSchema({"extra": "data"}) ] - date_short_sequence: ShortSequence[List[date]] + date_short_sequence: ShortSequence[list[date]] def _check_instance(self): _assert_date_validity(self.date_field) @@ -317,9 +313,9 @@ class PydanticTimedeltaModel(BaseModel): timedelta, Field(), WithJsonSchema({"extra": "data"}) ] annotated_list_of_timedelta: Annotated[ - List[timedelta], Field(), WithJsonSchema({"extra": "data"}) + list[timedelta], Field(), WithJsonSchema({"extra": "data"}) ] - timedelta_short_sequence: ShortSequence[List[timedelta]] + timedelta_short_sequence: ShortSequence[list[timedelta]] def _check_instance(self): _assert_timedelta_validity(self.timedelta_field) @@ -370,24 +366,24 @@ def _assert_timedelta_validity(td: timedelta): assert issubclass(td.__class__, timedelta) -PydanticModels = Union[ - StandardTypesModel, - StrictStandardTypesModel, - ComplexTypesModel, - SpecialTypesModel, - StrictSpecialTypesModel, - ParentModel, - FieldFeaturesModel, - AnnotatedFieldsModel, - GenericModel[Any], - UnionModel, - PydanticDatetimeModel, - PydanticDateModel, - PydanticTimedeltaModel, -] - - -def make_list_of_pydantic_objects() -> List[PydanticModels]: +PydanticModels = ( + StandardTypesModel + | StrictStandardTypesModel + | ComplexTypesModel + | SpecialTypesModel + | StrictSpecialTypesModel + | ParentModel + | FieldFeaturesModel + | AnnotatedFieldsModel + | GenericModel[Any] + | UnionModel + | PydanticDatetimeModel + | PydanticDateModel + | PydanticTimedeltaModel +) + + +def make_list_of_pydantic_objects() -> list[PydanticModels]: objects = [ make_standard_types_object(), make_strict_standard_types_object(), @@ -414,12 +410,12 @@ class MyDataClass: data_class_int_field: int -def make_dataclass_objects() -> List[MyDataClass]: +def make_dataclass_objects() -> list[MyDataClass]: return [MyDataClass(data_class_int_field=7)] -ComplexCustomType = Tuple[List[MyDataClass], List[PydanticModels]] -ComplexCustomUnionType = List[Union[MyDataClass, PydanticModels]] +ComplexCustomType = tuple[list[MyDataClass], list[PydanticModels]] +ComplexCustomUnionType = list[MyDataClass | PydanticModels] class PydanticModelWithStrictField(BaseModel): diff --git a/tests/contrib/pydantic/models_2.py b/tests/contrib/pydantic/models_2.py index 2c9520b32..4794f95a4 100644 --- a/tests/contrib/pydantic/models_2.py +++ b/tests/contrib/pydantic/models_2.py @@ -2,19 +2,12 @@ import decimal import fractions import re +from collections.abc import Hashable, Sequence from enum import Enum, IntEnum +from re import Pattern from typing import ( Any, - Dict, - Hashable, - List, NamedTuple, - Optional, - Pattern, - Sequence, - Set, - Tuple, - Union, cast, ) @@ -214,12 +207,12 @@ class Point(NamedTuple): class ComplexTypesModel(BaseModel): - list_field: List[str] - dict_field: Dict[str, int] - set_field: Set[int] - tuple_field: Tuple[str, int] - union_field: Union[str, int] - optional_field: Optional[str] + list_field: list[str] + dict_field: dict[str, int] + set_field: set[int] + tuple_field: tuple[str, int] + union_field: str | int + optional_field: str | None named_tuple_field: Point def _check_instance(self) -> None: diff --git a/tests/contrib/pydantic/test_pydantic.py b/tests/contrib/pydantic/test_pydantic.py index c70eee56f..7d70dfd19 100644 --- a/tests/contrib/pydantic/test_pydantic.py +++ b/tests/contrib/pydantic/test_pydantic.py @@ -2,6 +2,7 @@ import datetime import os import pathlib +import typing import uuid import pydantic @@ -9,13 +10,21 @@ from pydantic import BaseModel from temporalio.client import Client -from temporalio.contrib.pydantic import pydantic_data_converter +from temporalio.contrib.pydantic import ( + PydanticJSONPlainPayloadConverter, + PydanticPayloadConverter, + pydantic_data_converter, +) from temporalio.worker import Worker from temporalio.worker.workflow_sandbox._restrictions import ( RestrictionContext, SandboxMatcher, _RestrictedProxy, ) +from tests.contrib.pydantic.activities import ( + misc_objects_activity, + pydantic_objects_activity, +) from tests.contrib.pydantic.models import ( PydanticModels, PydanticModelWithStrictField, @@ -35,10 +44,159 @@ RoundTripPydanticObjectsWorkflow, _test_pydantic_model_with_strict_field, clone_objects, - misc_objects_activity, - pydantic_objects_activity, ) +_MANY_TYPE_HINTS = tuple( + typing.cast(type, typing.cast(object, typing.Annotated[list[int], index])) + for index in range(1025) +) +_UNHASHABLE_TYPE_HINT = typing.cast( + type, typing.cast(object, typing.Annotated[list[int], []]) +) + + +@pytest.mark.parametrize( + ( + "converter_kwargs", + "type_hints", + "expected_type_adapter_constructions", + ), + [ + # Default caches repeated hints + ({}, (list[int], list[int]), 1), + # Zero disables caching + ({"max_cached_type_adapters": 0}, (list[int], list[int]), 2), + # Unhashable hints bypass the cache + ({}, (_UNHASHABLE_TYPE_HINT, _UNHASHABLE_TYPE_HINT), 2), + # Default bound is 1024: 1025 distinct hints evict the first + ({}, _MANY_TYPE_HINTS + (_MANY_TYPE_HINTS[0],), 1026), + # None is unbounded: no eviction + ( + {"max_cached_type_adapters": None}, + _MANY_TYPE_HINTS + (_MANY_TYPE_HINTS[0],), + 1025, + ), + # Explicit bound evicts least recently used + ( + {"max_cached_type_adapters": 1}, + (list[int], _MANY_TYPE_HINTS[0], list[int]), + 3, + ), + ], +) +def test_type_adapter_reuse( + monkeypatch: pytest.MonkeyPatch, + converter_kwargs: dict[str, typing.Any], + type_hints: tuple[type, ...], + expected_type_adapter_constructions: int, +): + actual_type_adapter = pydantic.TypeAdapter + type_adapter_constructions = 0 + + def counting_type_adapter( + type_hint: typing.Any, + ) -> pydantic.TypeAdapter[typing.Any]: + nonlocal type_adapter_constructions + type_adapter_constructions += 1 + return actual_type_adapter(type_hint) + + monkeypatch.setattr( + "temporalio.contrib.pydantic.TypeAdapter", counting_type_adapter + ) + converter = PydanticJSONPlainPayloadConverter(**converter_kwargs) + payload = converter.to_payload([1]) + assert payload is not None + for type_hint in type_hints: + assert converter.from_payload(payload, type_hint) == [1] + assert type_adapter_constructions == expected_type_adapter_constructions + + +@pytest.mark.parametrize( + ("max_cached_type_adapters", "expected_type_adapter_constructions"), + [(None, 1), (0, 2)], +) +def test_composite_converter_forwards_type_adapter_cache_size( + monkeypatch: pytest.MonkeyPatch, + max_cached_type_adapters: int | None, + expected_type_adapter_constructions: int, +): + actual_type_adapter = pydantic.TypeAdapter + type_adapter_constructions = 0 + + def counting_type_adapter( + type_hint: typing.Any, + ) -> pydantic.TypeAdapter[typing.Any]: + nonlocal type_adapter_constructions + type_adapter_constructions += 1 + return actual_type_adapter(type_hint) + + monkeypatch.setattr( + "temporalio.contrib.pydantic.TypeAdapter", counting_type_adapter + ) + converter = PydanticPayloadConverter( + max_cached_type_adapters=max_cached_type_adapters + ) + payloads = converter.to_payloads([[1], [2]]) + assert converter.from_payloads(payloads, [list[int], list[int]]) == [[1], [2]] + assert type_adapter_constructions == expected_type_adapter_constructions + + +def test_type_adapter_reuse_across_threads_with_deferred_build(): + import concurrent.futures + + class DeferredModel(BaseModel): + model_config = pydantic.ConfigDict(defer_build=True) + value: int + + converter = PydanticJSONPlainPayloadConverter() + payload = converter.to_payload(DeferredModel(value=1)) + assert payload is not None + + def decode() -> DeferredModel: + return converter.from_payload(payload, DeferredModel) + + with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor: + results = list(executor.map(lambda _: decode(), range(64))) + assert all(result == DeferredModel(value=1) for result in results) + + +def test_type_adapter_cache_distinguishes_reimported_classes(): + # The workflow sandbox re-imports user modules, producing distinct class + # objects with identical names. Class hints hash by identity, so each + # world's class must get its own cache slot and validate to itself. + import types + + source = "from pydantic import BaseModel\n\nclass Foo(BaseModel):\n name: str\n" + + def load_module() -> types.ModuleType: + module = types.ModuleType("test_reimported_models") + exec(compile(source, "test_reimported_models.py", "exec"), module.__dict__) + return module + + foo_outside = load_module().Foo + foo_sandbox = load_module().Foo + assert foo_outside is not foo_sandbox + assert hash(foo_outside) != hash(foo_sandbox) + + # Worst case: one converter shared across both worlds (the real sandbox + # creates a separate converter per workflow instance). + converter = PydanticJSONPlainPayloadConverter() + payload = converter.to_payload(foo_outside(name="x")) + assert payload is not None + decoded_outside = converter.from_payload(payload, foo_outside) + decoded_sandbox = converter.from_payload(payload, foo_sandbox) + assert type(decoded_outside) is foo_outside + assert type(decoded_sandbox) is foo_sandbox + + +@pytest.mark.parametrize( + "converter_type", + [PydanticJSONPlainPayloadConverter, PydanticPayloadConverter], +) +def test_type_adapter_cache_rejects_negative_size(converter_type: type): + with pytest.raises(ValueError, match="max_cached_type_adapters cannot be negative"): + converter_type(max_cached_type_adapters=-1) + async def test_instantiation_outside_sandbox(): make_list_of_pydantic_objects() diff --git a/tests/contrib/pydantic/workflows.py b/tests/contrib/pydantic/workflows.py index a4d656b20..4733dec4c 100644 --- a/tests/contrib/pydantic/workflows.py +++ b/tests/contrib/pydantic/workflows.py @@ -1,6 +1,5 @@ import dataclasses from datetime import datetime, timedelta -from typing import List from uuid import UUID from pydantic import BaseModel, create_model @@ -22,11 +21,11 @@ ) -def clone_objects(objects: List[PydanticModels]) -> List[PydanticModels]: +def clone_objects(objects: list[PydanticModels]) -> list[PydanticModels]: new_objects = [] for o in objects: fields = {} - for name, f in o.model_fields.items(): + for name, f in o.model_fields.items(): # type: ignore[reportDeprecated] fields[name] = (f.annotation, f) model = create_model(o.__class__.__name__, **fields) # type: ignore new_objects.append(model(**o.model_dump(by_alias=True))) @@ -45,7 +44,7 @@ async def run(self) -> None: @workflow.defn class RoundTripPydanticObjectsWorkflow: @workflow.run - async def run(self, objects: List[PydanticModels]) -> List[PydanticModels]: + async def run(self, objects: list[PydanticModels]) -> list[PydanticModels]: return await workflow.execute_activity( pydantic_objects_activity, objects, @@ -86,7 +85,7 @@ async def run( @workflow.defn class CloneObjectsWorkflow: @workflow.run - async def run(self, objects: List[PydanticModels]) -> List[PydanticModels]: + async def run(self, objects: list[PydanticModels]) -> list[PydanticModels]: return clone_objects(objects) @@ -98,7 +97,7 @@ async def run( input: ComplexCustomUnionType, ) -> ComplexCustomUnionType: data_classes = [] - pydantic_objects: List[PydanticModels] = [] + pydantic_objects: list[PydanticModels] = [] for o in input: if dataclasses.is_dataclass(o): data_classes.append(o) @@ -171,5 +170,5 @@ async def run( @workflow.defn class NoTypeAnnotationsWorkflow: @workflow.run - async def run(self, arg): + async def run(self, arg): # type: ignore[reportMissingParameterType] return arg diff --git a/tests/contrib/strands/common.py b/tests/contrib/strands/common.py new file mode 100644 index 000000000..5ece12a1e --- /dev/null +++ b/tests/contrib/strands/common.py @@ -0,0 +1,10 @@ +from temporalio.api.enums.v1 import EventType +from temporalio.client import WorkflowHistory + + +def get_activities(history: WorkflowHistory) -> list[str]: + return [ + event.activity_task_scheduled_event_attributes.activity_type.name + for event in history.events + if event.event_type == EventType.EVENT_TYPE_ACTIVITY_TASK_SCHEDULED + ] diff --git a/tests/contrib/strands/echo_mcp_server.py b/tests/contrib/strands/echo_mcp_server.py new file mode 100644 index 000000000..9f70075ac --- /dev/null +++ b/tests/contrib/strands/echo_mcp_server.py @@ -0,0 +1,13 @@ +from mcp.server.fastmcp import FastMCP + +mcp = FastMCP("echo-server") + + +@mcp.tool() +def echo(message: str) -> str: + """Return the input message unchanged.""" + return message + + +if __name__ == "__main__": + mcp.run() diff --git a/tests/contrib/strands/mock_model.py b/tests/contrib/strands/mock_model.py new file mode 100644 index 000000000..5cbb0f89f --- /dev/null +++ b/tests/contrib/strands/mock_model.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +import json +from collections.abc import AsyncIterable +from typing import Any + +from strands.models import Model +from strands.types.streaming import StreamEvent + + +class MockModel(Model): + """Scripted Strands ``Model`` for tests. + + Each entry in ``responses`` is consumed by one ``stream()`` call. A ``str`` + yields a text turn; a ``dict`` of ``{name, input}`` yields a tool-use turn. + """ + + def __init__(self, responses: list[str | dict[str, Any]]) -> None: + self._responses = list(responses) + self._tool_call_index = 0 + + def update_config(self, **_model_config: Any) -> None: + return None + + def get_config(self) -> dict[str, Any]: + return {} + + def structured_output(self, *_args: Any, **_kwargs: Any): + raise NotImplementedError + + async def stream(self, *_args: Any, **_kwargs: Any) -> AsyncIterable[StreamEvent]: + if not self._responses: + raise AssertionError("MockModel script exhausted") + response = self._responses.pop(0) + + yield {"messageStart": {"role": "assistant"}} + + if isinstance(response, str): + yield {"contentBlockDelta": {"delta": {"text": response}}} + yield {"contentBlockStop": {}} + yield {"messageStop": {"stopReason": "end_turn"}} + else: + self._tool_call_index += 1 + yield { + "contentBlockStart": { + "start": { + "toolUse": { + "name": response["name"], + "toolUseId": f"mock-tool-{self._tool_call_index}", + }, + }, + }, + } + yield { + "contentBlockDelta": { + "delta": {"toolUse": {"input": json.dumps(response["input"])}}, + }, + } + yield {"contentBlockStop": {}} + yield {"messageStop": {"stopReason": "tool_use"}} diff --git a/tests/contrib/strands/test_hooks.py b/tests/contrib/strands/test_hooks.py new file mode 100644 index 000000000..7bcf0172f --- /dev/null +++ b/tests/contrib/strands/test_hooks.py @@ -0,0 +1,106 @@ +from datetime import timedelta +from uuid import uuid4 + +from strands import tool +from strands.hooks import HookProvider, HookRegistry +from strands.hooks.events import AfterToolCallEvent + +from temporalio import activity, workflow +from temporalio.client import Client +from temporalio.contrib.strands import StrandsPlugin, TemporalAgent +from temporalio.contrib.strands.workflow import activity_as_hook +from temporalio.worker import Replayer, Worker +from tests.contrib.strands.common import get_activities +from tests.contrib.strands.mock_model import MockModel + +# Module-level sink: written by the audit activity, read in assertions. +# Activity bodies run in worker context, not the sandbox, so a plain list is fine. +_AUDIT_LOG: list[str] = [] + + +@activity.defn +async def audit_tool(tool_name: str) -> None: + _AUDIT_LOG.append(tool_name) + + +@tool +def echo(text: str) -> str: + return text + + +class AuditHook(HookProvider): + def __init__(self) -> None: + self.fired_events: list[str] = [] + + def register_hooks(self, registry: HookRegistry, **kwargs: object) -> None: + registry.add_callback(AfterToolCallEvent, self._sync_log) + registry.add_callback( + AfterToolCallEvent, + activity_as_hook( + audit_tool, + activity_input=lambda event: event.tool_use["name"], + start_to_close_timeout=timedelta(seconds=10), + ), + ) + + def _sync_log(self, event: AfterToolCallEvent) -> None: + # Deterministic in-workflow mutation: appends to per-workflow state. + self.fired_events.append(event.tool_use["name"]) + + +@workflow.defn +class HooksWorkflow: + def __init__(self) -> None: + self.hook = AuditHook() + self.agent = TemporalAgent( + model="mock", + start_to_close_timeout=timedelta(seconds=15), + tools=[echo], + hooks=[self.hook], + ) + + @workflow.run + async def run(self, prompt: str) -> list[str]: + await self.agent.invoke_async(prompt) + return self.hook.fired_events + + +async def test_hooks(client: Client): + _AUDIT_LOG.clear() + task_queue = f"test_hooks-{uuid4()}" + plugin = StrandsPlugin( + models={ + "mock": lambda: MockModel( + [ + {"name": "echo", "input": {"text": "hi"}}, + "Done!", + ] + ) + } + ) + + async with Worker( + client, + task_queue=task_queue, + workflows=[HooksWorkflow], + activities=[audit_tool], + plugins=[plugin], + max_cached_workflows=0, + ): + handle = await client.start_workflow( + HooksWorkflow.run, + "Say hi", + id=f"test_hooks_{uuid4()}", + task_queue=task_queue, + ) + assert await handle.result() == ["echo"] + + assert _AUDIT_LOG == ["echo"] + + history = await handle.fetch_history() + assert "audit_tool" in get_activities(history) + + await Replayer( + workflows=[HooksWorkflow], + plugins=[plugin], + ).replay_workflow(history) diff --git a/tests/contrib/strands/test_interrupt.py b/tests/contrib/strands/test_interrupt.py new file mode 100644 index 000000000..b5c76add0 --- /dev/null +++ b/tests/contrib/strands/test_interrupt.py @@ -0,0 +1,105 @@ +from datetime import timedelta +from uuid import uuid4 + +from strands import tool +from strands.hooks import HookProvider, HookRegistry +from strands.hooks.events import BeforeToolCallEvent +from strands.types.interrupt import InterruptResponseContent + +from temporalio import workflow +from temporalio.client import Client +from temporalio.contrib.strands import StrandsPlugin, TemporalAgent +from temporalio.worker import Replayer, Worker +from tests.contrib.strands.common import get_activities +from tests.contrib.strands.mock_model import MockModel + + +@tool +def delete_thing(name: str) -> str: + return f"deleted {name}" + + +class ApprovalHook(HookProvider): + def register_hooks(self, registry: HookRegistry, **kwargs: object) -> None: + registry.add_callback(BeforeToolCallEvent, self._gate) + + def _gate(self, event: BeforeToolCallEvent) -> None: + if event.tool_use["name"] != "delete_thing": + return + approval = event.interrupt( + "approval", + reason=f"approve delete of {event.tool_use['input']['name']}?", + ) + if approval != "approve": + event.cancel_tool = "denied" + + +@workflow.defn +class InterruptWorkflow: + def __init__(self) -> None: + self.agent = TemporalAgent( + model="mock", + start_to_close_timeout=timedelta(seconds=15), + tools=[delete_thing], + hooks=[ApprovalHook()], + ) + self._approval: str | None = None + + @workflow.signal + def approve(self, response: str) -> None: + self._approval = response + + @workflow.run + async def run(self, prompt: str) -> str: + result = await self.agent.invoke_async(prompt) + while result.stop_reason == "interrupt": + await workflow.wait_condition(lambda: self._approval is not None) + response = self._approval + self._approval = None + responses: list[InterruptResponseContent] = [ + {"interruptResponse": {"interruptId": i.id, "response": response}} + for i in (result.interrupts or []) + ] + result = await self.agent.invoke_async(responses) + return str(result) + + +async def test_interrupt(client: Client): + task_queue = f"test_interrupt-{uuid4()}" + plugin = StrandsPlugin( + models={ + "mock": lambda: MockModel( + [ + {"name": "delete_thing", "input": {"name": "foo"}}, + "Done!", + ] + ) + } + ) + + async with Worker( + client, + task_queue=task_queue, + workflows=[InterruptWorkflow], + plugins=[plugin], + max_cached_workflows=0, + ): + handle = await client.start_workflow( + InterruptWorkflow.run, + "delete foo", + id=f"test_interrupt_{uuid4()}", + task_queue=task_queue, + ) + await handle.signal(InterruptWorkflow.approve, "approve") + assert await handle.result() == "Done!\n" + + history = await handle.fetch_history() + assert get_activities(history) == [ + "invoke_model", + "invoke_model", + ] + + await Replayer( + workflows=[InterruptWorkflow], + plugins=[plugin], + ).replay_workflow(history) diff --git a/tests/contrib/strands/test_interrupt_exception.py b/tests/contrib/strands/test_interrupt_exception.py new file mode 100644 index 000000000..e7492f286 --- /dev/null +++ b/tests/contrib/strands/test_interrupt_exception.py @@ -0,0 +1,191 @@ +from datetime import timedelta +from uuid import uuid4 + +from strands import tool +from strands.interrupt import Interrupt, InterruptException +from strands.types.interrupt import InterruptResponseContent + +from temporalio import activity, workflow +from temporalio.client import Client +from temporalio.contrib.strands import StrandsPlugin, TemporalAgent +from temporalio.contrib.strands.workflow import activity_as_tool +from temporalio.worker import Replayer, Worker +from tests.contrib.strands.common import get_activities +from tests.contrib.strands.mock_model import MockModel + + +@tool +def in_workflow_delete(name: str) -> str: + raise InterruptException( + Interrupt(id=f"delete:{name}", name="approval", reason=f"delete {name}?") + ) + + +# Counts attempts so the activity raises on the first invocation and succeeds on +# the second — modeling a real "approval flipped an external flag" check. +_activity_delete_calls = 0 + + +@activity.defn +async def activity_delete(name: str) -> str: + global _activity_delete_calls + _activity_delete_calls += 1 + if _activity_delete_calls == 1: + raise InterruptException( + Interrupt(id=f"delete:{name}", name="approval", reason=f"delete {name}?") + ) + return f"deleted {name}" + + +@workflow.defn +class InWorkflowToolInterruptWorkflow: + def __init__(self) -> None: + self.agent = TemporalAgent( + model="mock", + start_to_close_timeout=timedelta(seconds=15), + tools=[in_workflow_delete], + ) + self._approval: str | None = None + + @workflow.signal + def approve(self, response: str) -> None: + self._approval = response + + @workflow.run + async def run(self, prompt: str) -> str: + result = await self.agent.invoke_async(prompt) + while result.stop_reason == "interrupt": + await workflow.wait_condition(lambda: self._approval is not None) + response, self._approval = self._approval, None + responses: list[InterruptResponseContent] = [ + {"interruptResponse": {"interruptId": i.id, "response": response}} + for i in (result.interrupts or []) + ] + result = await self.agent.invoke_async(responses) + return str(result) + + +@workflow.defn +class ActivityToolInterruptWorkflow: + def __init__(self) -> None: + self.agent = TemporalAgent( + model="mock", + start_to_close_timeout=timedelta(seconds=15), + tools=[ + activity_as_tool( + activity_delete, + start_to_close_timeout=timedelta(seconds=15), + ) + ], + ) + self._approval: str | None = None + + @workflow.signal + def approve(self, response: str) -> None: + self._approval = response + + @workflow.run + async def run(self, prompt: str) -> str: + result = await self.agent.invoke_async(prompt) + while result.stop_reason == "interrupt": + await workflow.wait_condition(lambda: self._approval is not None) + response, self._approval = self._approval, None + responses: list[InterruptResponseContent] = [ + {"interruptResponse": {"interruptId": i.id, "response": response}} + for i in (result.interrupts or []) + ] + result = await self.agent.invoke_async(responses) + return str(result) + + +async def test_in_workflow_tool_interrupt(client: Client): + task_queue = f"test_in_workflow_tool_interrupt-{uuid4()}" + plugin = StrandsPlugin( + models={ + "mock": lambda: MockModel( + [ + {"name": "in_workflow_delete", "input": {"name": "foo"}}, + "Done!", + ] + ) + } + ) + + async with Worker( + client, + task_queue=task_queue, + workflows=[InWorkflowToolInterruptWorkflow], + plugins=[plugin], + max_cached_workflows=0, + ): + handle = await client.start_workflow( + InWorkflowToolInterruptWorkflow.run, + "delete foo", + id=f"test_in_workflow_tool_interrupt_{uuid4()}", + task_queue=task_queue, + ) + await handle.signal(InWorkflowToolInterruptWorkflow.approve, "approve") + assert await handle.result() == "Done!\n" + + history = await handle.fetch_history() + # No activity call for the in-workflow @tool — only model calls. + assert get_activities(history) == ["invoke_model", "invoke_model"] + + await Replayer( + workflows=[InWorkflowToolInterruptWorkflow], + plugins=[plugin], + ).replay_workflow(history) + + +async def test_activity_tool_interrupt(client: Client): + global _activity_delete_calls + _activity_delete_calls = 0 + task_queue = f"test_activity_tool_interrupt-{uuid4()}" + plugin = StrandsPlugin( + models={ + "mock": lambda: MockModel( + [ + {"name": "activity_delete", "input": {"name": "foo"}}, + "Done!", + ] + ) + } + ) + + # Activity-side InterruptException relies on the failure converter installed + # via the data converter, which _ActivityWorker reads from the client config. + # Re-create the client with the plugin attached so that converter takes effect. + config = client.config() + config["plugins"] = [*config["plugins"], plugin] + client = Client(**config) + + async with Worker( + client, + task_queue=task_queue, + workflows=[ActivityToolInterruptWorkflow], + activities=[activity_delete], + max_cached_workflows=0, + ): + handle = await client.start_workflow( + ActivityToolInterruptWorkflow.run, + "delete foo", + id=f"test_activity_tool_interrupt_{uuid4()}", + task_queue=task_queue, + ) + await handle.signal(ActivityToolInterruptWorkflow.approve, "approve") + assert await handle.result() == "Done!\n" + + history = await handle.fetch_history() + # activity_delete appears twice: once for the call that raised + # InterruptException, once for the resume call that returned successfully. + assert get_activities(history) == [ + "invoke_model", + "activity_delete", + "activity_delete", + "invoke_model", + ] + + await Replayer( + workflows=[ActivityToolInterruptWorkflow], + plugins=[plugin], + ).replay_workflow(history) diff --git a/tests/contrib/strands/test_invocation_state.py b/tests/contrib/strands/test_invocation_state.py new file mode 100644 index 000000000..84b5531a4 --- /dev/null +++ b/tests/contrib/strands/test_invocation_state.py @@ -0,0 +1,83 @@ +from collections.abc import AsyncIterable +from datetime import timedelta +from typing import Any +from uuid import uuid4 + +from strands.models import Model +from strands.types.streaming import StreamEvent + +from temporalio import workflow +from temporalio.client import Client +from temporalio.contrib.strands import StrandsPlugin, TemporalAgent +from temporalio.worker import Worker + +# Worker-side sink: the recording model writes the invocation_state it +# received here so the test body can inspect it after the workflow completes. +_RECEIVED: list[dict[str, Any]] = [] + + +class _RecordingModel(Model): + def update_config(self, **_model_config: Any) -> None: + return None + + def get_config(self) -> dict[str, Any]: + return {} + + def structured_output(self, *_args: Any, **_kwargs: Any) -> Any: + raise NotImplementedError + + async def stream( + self, + *_args: Any, + invocation_state: dict[str, Any] | None = None, + **_kwargs: Any, + ) -> AsyncIterable[StreamEvent]: + _RECEIVED.append(invocation_state or {}) + yield {"messageStart": {"role": "assistant"}} + yield {"contentBlockDelta": {"delta": {"text": "ok"}}} + yield {"contentBlockStop": {}} + yield {"messageStop": {"stopReason": "end_turn"}} + + +@workflow.defn +class _InvocationStateWorkflow: + def __init__(self) -> None: + self.agent = TemporalAgent( + model="recording", + start_to_close_timeout=timedelta(seconds=15), + ) + + @workflow.run + async def run(self, prompt: str) -> str: + result = await self.agent.invoke_async( + prompt, + invocation_state={"user_key": "user_value", "non_json": object()}, + ) + return str(result) + + +async def test_invocation_state_round_trip(client: Client): + _RECEIVED.clear() + task_queue = f"test_invocation_state-{uuid4()}" + plugin = StrandsPlugin(models={"recording": lambda: _RecordingModel()}) + + async with Worker( + client, + task_queue=task_queue, + workflows=[_InvocationStateWorkflow], + plugins=[plugin], + max_cached_workflows=0, + ): + await client.execute_workflow( + _InvocationStateWorkflow.run, + "hi", + id=f"test_invocation_state_{uuid4()}", + task_queue=task_queue, + ) + + # The serializable key crosses the activity boundary; the non-serializable + # one is dropped before dispatch (with a debug log). + assert _RECEIVED, "model.stream() was not called" + received = _RECEIVED[0] + assert received.get("user_key") == "user_value" + assert "non_json" not in received diff --git a/tests/contrib/strands/test_mcp.py b/tests/contrib/strands/test_mcp.py new file mode 100644 index 000000000..9849e6dda --- /dev/null +++ b/tests/contrib/strands/test_mcp.py @@ -0,0 +1,326 @@ +import asyncio +import sys +from datetime import timedelta +from pathlib import Path +from uuid import uuid4 + +from mcp import StdioServerParameters, stdio_client +from strands.tools.mcp.mcp_client import MCPClient + +from temporalio import workflow +from temporalio.client import Client +from temporalio.contrib.strands import ( + StrandsPlugin, + TemporalAgent, + TemporalMCPClient, + _temporal_mcp_client, +) +from temporalio.worker import Replayer, Worker +from tests.contrib.strands.common import get_activities +from tests.contrib.strands.mock_model import MockModel + + +def _echo_client_factory() -> MCPClient: + return MCPClient( + lambda: stdio_client( + StdioServerParameters( + command=sys.executable, + args=[str(Path(__file__).parent / "echo_mcp_server.py")], + ) + ) + ) + + +@workflow.defn +class MCPWorkflow: + def __init__(self) -> None: + echo = TemporalMCPClient( + server="echo", + cache_tools=True, + start_to_close_timeout=timedelta(seconds=30), + ) + self.agent = TemporalAgent( + model="mock", + start_to_close_timeout=timedelta(seconds=30), + tools=[echo], + ) + + @workflow.run + async def run(self, prompt: str) -> str: + result = await self.agent.invoke_async(prompt) + return str(result) + + +async def test_mcp(client: Client): + task_queue = f"test_mcp-{uuid4()}" + plugin = StrandsPlugin( + models={ + "mock": lambda: MockModel( + [ + {"name": "echo", "input": {"message": "hello"}}, + "Done!", + ] + ) + }, + mcp_clients={ + "echo": lambda: MCPClient( + lambda: stdio_client( + StdioServerParameters( + command=sys.executable, + args=[str(Path(__file__).parent / "echo_mcp_server.py")], + ) + ) + ), + }, + ) + + async with Worker( + client, + task_queue=task_queue, + workflows=[MCPWorkflow], + plugins=[plugin], + max_cached_workflows=0, + ): + handle = await client.start_workflow( + MCPWorkflow.run, + "echo hello", + id=f"test_mcp_{uuid4()}", + task_queue=task_queue, + ) + assert await handle.result() == "Done!\n" + + history = await handle.fetch_history() + assert get_activities(history) == [ + "echo-list-tools", + "invoke_model", + "echo-call-tool", + "invoke_model", + ] + + await Replayer( + workflows=[MCPWorkflow], + plugins=[plugin], + ).replay_workflow(history) + + +@workflow.defn +class MCPReuseWorkflow: + def __init__(self) -> None: + echo = TemporalMCPClient( + server="echo_cached", + cache_tools=True, + start_to_close_timeout=timedelta(seconds=30), + ) + self.agent = TemporalAgent( + model="mock", + start_to_close_timeout=timedelta(seconds=30), + tools=[echo], + ) + + @workflow.run + async def run(self, prompt: str) -> str: + result = await self.agent.invoke_async(prompt) + return str(result) + + +async def test_mcp_reuses_connection(client: Client): + """Successive MCP tool calls reuse one cached worker-side connection.""" + task_queue = f"test_mcp_reuses_connection-{uuid4()}" + # Count how often the worker opens a connection. One lazily-opened + # connection serves the list-tools discovery and both tool calls (1); + # reconnecting per call would make it more. + factory_calls = [0] + + def counting_factory() -> MCPClient: + factory_calls[0] += 1 + return _echo_client_factory() + + plugin = StrandsPlugin( + models={ + "mock": lambda: MockModel( + [ + {"name": "echo", "input": {"message": "one"}}, + {"name": "echo", "input": {"message": "two"}}, + "Done!", + ] + ) + }, + mcp_clients={"echo_cached": counting_factory}, + ) + + async with Worker( + client, + task_queue=task_queue, + workflows=[MCPReuseWorkflow], + plugins=[plugin], + max_cached_workflows=0, + ): + handle = await client.start_workflow( + MCPReuseWorkflow.run, + "echo twice", + id=f"test_mcp_reuses_connection_{uuid4()}", + task_queue=task_queue, + ) + assert await handle.result() == "Done!\n" + + # The worker context has exited, so its run_context finally evicted the + # cached connection. + assert "echo_cached" not in _temporal_mcp_client._CONNECTIONS + assert factory_calls[0] == 1 + + history = await handle.fetch_history() + assert get_activities(history) == [ + "echo_cached-list-tools", + "invoke_model", + "echo_cached-call-tool", + "invoke_model", + "echo_cached-call-tool", + "invoke_model", + ] + + await Replayer( + workflows=[MCPReuseWorkflow], + plugins=[plugin], + ).replay_workflow(history) + + +@workflow.defn +class MCPIdleWorkflow: + def __init__(self) -> None: + echo = TemporalMCPClient( + server="echo_idle", + cache_tools=True, + start_to_close_timeout=timedelta(seconds=30), + ) + self.agent = TemporalAgent( + model="mock", + start_to_close_timeout=timedelta(seconds=30), + tools=[echo], + ) + + @workflow.run + async def run(self, prompt: str) -> str: + result = await self.agent.invoke_async(prompt) + return str(result) + + +async def test_mcp_connection_idle_timeout(client: Client): + """A short idle timeout evicts the cached connection while the worker runs.""" + task_queue = f"test_mcp_connection_idle_timeout-{uuid4()}" + factory_calls = [0] + + def counting_factory() -> MCPClient: + factory_calls[0] += 1 + return _echo_client_factory() + + plugin = StrandsPlugin( + models={ + "mock": lambda: MockModel( + [ + {"name": "echo", "input": {"message": "hello"}}, + "Done!", + ] + ) + }, + mcp_clients={"echo_idle": counting_factory}, + # Short window so the cached call connection is evicted mid-run rather + # than only at worker shutdown. The idle timer only arms once the call + # releases the connection, so this can't tear it down mid-call. + mcp_connection_idle_timeout=timedelta(milliseconds=100), + ) + + async with Worker( + client, + task_queue=task_queue, + workflows=[MCPIdleWorkflow], + plugins=[plugin], + max_cached_workflows=0, + ): + handle = await client.start_workflow( + MCPIdleWorkflow.run, + "echo hello", + id=f"test_mcp_connection_idle_timeout_{uuid4()}", + task_queue=task_queue, + ) + assert await handle.result() == "Done!\n" + + # A connection was opened lazily (on the first list-tools/call-tool). + # How many times depends on whether the short idle timer fires between + # activities, so this only asserts that at least one was opened; the + # eviction-while-alive behavior is what the polling loop below checks. + assert factory_calls[0] >= 1 + + # Still inside the worker context: the short idle timer evicts the + # cached call connection on its own. Asserting eviction here -- with the + # worker alive -- proves it came from the idle timer, not shutdown. + for _ in range(100): + if "echo_idle" not in _temporal_mcp_client._CONNECTIONS: + break + await asyncio.sleep(0.1) + assert "echo_idle" not in _temporal_mcp_client._CONNECTIONS + + +@workflow.defn +class MCPNoCacheWorkflow: + def __init__(self) -> None: + echo = TemporalMCPClient( + server="echo_nocache", + cache_tools=False, + start_to_close_timeout=timedelta(seconds=30), + ) + self.agent = TemporalAgent( + model="mock", + start_to_close_timeout=timedelta(seconds=30), + tools=[echo], + ) + + @workflow.run + async def run(self, prompt: str) -> str: + result = await self.agent.invoke_async(prompt) + return str(result) + + +async def test_mcp_lists_tools_each_turn_when_uncached(client: Client): + """With cache_tools=False the tool list is re-fetched on every model call.""" + task_queue = f"test_mcp_lists_tools_each_turn_when_uncached-{uuid4()}" + plugin = StrandsPlugin( + models={ + "mock": lambda: MockModel( + [ + {"name": "echo", "input": {"message": "one"}}, + {"name": "echo", "input": {"message": "two"}}, + "Done!", + ] + ) + }, + mcp_clients={"echo_nocache": _echo_client_factory}, + ) + + async with Worker( + client, + task_queue=task_queue, + workflows=[MCPNoCacheWorkflow], + plugins=[plugin], + max_cached_workflows=0, + ): + handle = await client.start_workflow( + MCPNoCacheWorkflow.run, + "echo twice", + id=f"test_mcp_lists_tools_each_turn_when_uncached_{uuid4()}", + task_queue=task_queue, + ) + assert await handle.result() == "Done!\n" + + history = await handle.fetch_history() + activities = get_activities(history) + # One list-tools per model call -- the tools are re-listed every turn rather + # than once for the workflow. + assert activities.count("echo_nocache-list-tools") == activities.count( + "invoke_model" + ) + assert activities.count("echo_nocache-list-tools") == 3 + + await Replayer( + workflows=[MCPNoCacheWorkflow], + plugins=[plugin], + ).replay_workflow(history) diff --git a/tests/contrib/strands/test_model.py b/tests/contrib/strands/test_model.py new file mode 100644 index 000000000..c96d4c0d7 --- /dev/null +++ b/tests/contrib/strands/test_model.py @@ -0,0 +1,51 @@ +from datetime import timedelta +from uuid import uuid4 + +from temporalio import workflow +from temporalio.client import Client +from temporalio.contrib.strands import StrandsPlugin, TemporalAgent +from temporalio.worker import Replayer, Worker +from tests.contrib.strands.common import get_activities +from tests.contrib.strands.mock_model import MockModel + + +@workflow.defn +class ModelWorkflow: + def __init__(self) -> None: + self.agent = TemporalAgent( + model="mock", + start_to_close_timeout=timedelta(seconds=15), + ) + + @workflow.run + async def run(self, prompt: str) -> str: + result = await self.agent.invoke_async(prompt) + return str(result) + + +async def test_model(client: Client): + task_queue = f"test_model-{uuid4()}" + plugin = StrandsPlugin(models={"mock": lambda: MockModel(["Done!"])}) + + async with Worker( + client, + task_queue=task_queue, + workflows=[ModelWorkflow], + plugins=[plugin], + max_cached_workflows=0, + ): + handle = await client.start_workflow( + ModelWorkflow.run, + "Hello", + id=f"test_model_{uuid4()}", + task_queue=task_queue, + ) + assert await handle.result() == "Done!\n" + + history = await handle.fetch_history() + assert get_activities(history) == ["invoke_model"] + + await Replayer( + workflows=[ModelWorkflow], + plugins=[plugin], + ).replay_workflow(history) diff --git a/tests/contrib/strands/test_model_streaming.py b/tests/contrib/strands/test_model_streaming.py new file mode 100644 index 000000000..1a0b84447 --- /dev/null +++ b/tests/contrib/strands/test_model_streaming.py @@ -0,0 +1,78 @@ +import asyncio +from datetime import timedelta +from uuid import uuid4 + +from strands.types.streaming import StreamEvent + +from temporalio import workflow +from temporalio.client import Client +from temporalio.contrib.strands import StrandsPlugin, TemporalAgent +from temporalio.contrib.workflow_streams import WorkflowStream, WorkflowStreamClient +from temporalio.worker import Replayer, Worker +from tests.contrib.strands.common import get_activities +from tests.contrib.strands.mock_model import MockModel + + +@workflow.defn +class StreamingModelWorkflow: + def __init__(self) -> None: + self.stream = WorkflowStream() + self.agent = TemporalAgent( + model="mock", + start_to_close_timeout=timedelta(seconds=15), + streaming_topic="events", + ) + + @workflow.run + async def run(self, prompt: str) -> str: + result = await self.agent.invoke_async(prompt) + return str(result) + + +async def test_model_streaming(client: Client): + task_queue = f"test_model_streaming-{uuid4()}" + plugin = StrandsPlugin(models={"mock": lambda: MockModel(["Done!"])}) + workflow_id = f"test_model_streaming_{uuid4()}" + + async with Worker( + client, + task_queue=task_queue, + workflows=[StreamingModelWorkflow], + plugins=[plugin], + max_cached_workflows=0, + ): + handle = await client.start_workflow( + StreamingModelWorkflow.run, + "Hello", + id=workflow_id, + task_queue=task_queue, + ) + + stream = WorkflowStreamClient.create(client, workflow_id) + events: list[StreamEvent] = [] + + async def collect() -> None: + async for item in stream.subscribe( + ["events"], + from_offset=0, + result_type=StreamEvent, + poll_cooldown=timedelta(milliseconds=50), + ): + events.append(item.data) + if len(events) >= 4: + break + + collect_task = asyncio.create_task(collect()) + assert await handle.result() == "Done!\n" + await asyncio.wait_for(collect_task, timeout=10.0) + + history = await handle.fetch_history() + assert get_activities(history) == ["invoke_model_streaming"] + + assert any("messageStart" in e for e in events) + assert any("messageStop" in e for e in events) + + await Replayer( + workflows=[StreamingModelWorkflow], + plugins=[plugin], + ).replay_workflow(history) diff --git a/tests/contrib/strands/test_structured_output.py b/tests/contrib/strands/test_structured_output.py new file mode 100644 index 000000000..eafe2b364 --- /dev/null +++ b/tests/contrib/strands/test_structured_output.py @@ -0,0 +1,74 @@ +from datetime import timedelta +from uuid import uuid4 + +from pydantic import BaseModel, Field + +from temporalio import workflow +from temporalio.client import Client +from temporalio.contrib.strands import StrandsPlugin, TemporalAgent +from temporalio.worker import Replayer, Worker +from tests.contrib.strands.mock_model import MockModel + + +class PersonInfo(BaseModel): + name: str = Field(description="Name of the person") + age: int = Field(description="Age of the person") + occupation: str = Field(description="Occupation of the person") + + +@workflow.defn +class StructuredOutputWorkflow: + def __init__(self) -> None: + self.agent = TemporalAgent( + model="mock", + start_to_close_timeout=timedelta(seconds=15), + structured_output_model=PersonInfo, + ) + + @workflow.run + async def run(self, prompt: str) -> PersonInfo: + result = await self.agent.invoke_async(prompt) + assert isinstance(result.structured_output, PersonInfo) + return result.structured_output + + +async def test_structured_output(client: Client): + task_queue = f"test_structured_output-{uuid4()}" + plugin = StrandsPlugin( + models={ + "mock": lambda: MockModel( + [ + { + "name": "PersonInfo", + "input": { + "name": "John Smith", + "age": 30, + "occupation": "software engineer", + }, + }, + ] + ) + } + ) + + async with Worker( + client, + task_queue=task_queue, + workflows=[StructuredOutputWorkflow], + plugins=[plugin], + max_cached_workflows=0, + ): + handle = await client.start_workflow( + StructuredOutputWorkflow.run, + "John Smith is a 30 year-old software engineer", + id=f"test_structured_output_{uuid4()}", + task_queue=task_queue, + ) + assert await handle.result() == PersonInfo( + name="John Smith", age=30, occupation="software engineer" + ) + + await Replayer( + workflows=[StructuredOutputWorkflow], + plugins=[plugin], + ).replay_workflow(await handle.fetch_history()) diff --git a/tests/contrib/strands/test_tool.py b/tests/contrib/strands/test_tool.py new file mode 100644 index 000000000..d13fcbec1 --- /dev/null +++ b/tests/contrib/strands/test_tool.py @@ -0,0 +1,118 @@ +from datetime import timedelta +from pathlib import Path +from uuid import uuid4 + +from strands import tool +from strands_tools import ( # pyright: ignore[reportMissingTypeStubs] + calculator, + file_read, +) + +from temporalio import activity, workflow +from temporalio.client import Client +from temporalio.contrib.strands import StrandsPlugin, TemporalAgent +from temporalio.contrib.strands.workflow import activity_as_tool +from temporalio.worker import Replayer, Worker +from tests.contrib.strands.common import get_activities +from tests.contrib.strands.mock_model import MockModel + + +@tool +def letter_counter(word: str, letter: str) -> int: + return word.lower().count(letter.lower()) + + +@activity.defn(name="read_file") +async def read_file_activity(path: str) -> str: + result = file_read.file_read( + { + "toolUseId": "read_file", + "name": "file_read", + "input": {"path": path, "mode": "view"}, + } + ) + text = result["content"][0].get("text") + assert text is not None + return text + + +@workflow.defn +class ToolWorkflow: + def __init__(self) -> None: + self.agent = TemporalAgent( + model="mock", + start_to_close_timeout=timedelta(seconds=15), + tools=[ + calculator, + activity_as_tool( + read_file_activity, + start_to_close_timeout=timedelta(seconds=15), + ), + letter_counter, + ], + ) + + @workflow.run + async def run(self, prompt: str) -> str: + result = await self.agent.invoke_async(prompt) + return str(result) + + +async def test_tool(client: Client, tmp_path: Path): + task_queue = f"test_tool-{uuid4()}" + fixture = tmp_path / "greeting.txt" + fixture.write_text("hello\n") + + plugin = StrandsPlugin( + models={ + "mock": lambda: MockModel( + [ + {"name": "read_file", "input": {"path": str(fixture)}}, + { + "name": "calculator", + "input": {"expression": "3111696 / 74088"}, + }, + { + "name": "letter_counter", + "input": {"word": "strawberry", "letter": "R"}, + }, + "Done!", + ] + ) + } + ) + + async with Worker( + client, + task_queue=task_queue, + workflows=[ToolWorkflow], + activities=[read_file_activity], + plugins=[plugin], + max_cached_workflows=0, + ): + handle = await client.start_workflow( + ToolWorkflow.run, + "I have 3 requests:\n" + f"1. Read the file at {fixture}\n" + "2. Calculate 3111696 / 74088\n" + '3. Tell me how many letter R\'s are in the word "strawberry"', + id=f"test_tool_{uuid4()}", + task_queue=task_queue, + ) + assert await handle.result() == "Done!\n" + + history = await handle.fetch_history() + assert get_activities(history) == [ + "invoke_model", + "read_file", + "invoke_model", + # calculator (in-workflow) + "invoke_model", + # letter_counter (in-workflow) + "invoke_model", + ] + + await Replayer( + workflows=[ToolWorkflow], + plugins=[plugin], + ).replay_workflow(history) diff --git a/tests/contrib/test_opentelemetry.py b/tests/contrib/test_opentelemetry.py deleted file mode 100644 index e42e6b977..000000000 --- a/tests/contrib/test_opentelemetry.py +++ /dev/null @@ -1,394 +0,0 @@ -from __future__ import annotations - -import asyncio -import logging -import uuid -from dataclasses import dataclass -from datetime import timedelta -from typing import Iterable, List, Optional - -from opentelemetry.sdk.trace import ReadableSpan, TracerProvider -from opentelemetry.sdk.trace.export import SimpleSpanProcessor -from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter -from opentelemetry.trace import get_tracer - -from temporalio import activity, workflow -from temporalio.client import Client -from temporalio.common import RetryPolicy -from temporalio.contrib.opentelemetry import TracingInterceptor -from temporalio.contrib.opentelemetry import workflow as otel_workflow -from temporalio.testing import WorkflowEnvironment -from temporalio.worker import UnsandboxedWorkflowRunner, Worker - -# Passing through because Python 3.9 has an import bug at -# https://github.com/python/cpython/issues/91351 -with workflow.unsafe.imports_passed_through(): - import pytest - - -@dataclass -class TracingActivityParam: - heartbeat: bool = True - fail_until_attempt: Optional[int] = None - - -@activity.defn -async def tracing_activity(param: TracingActivityParam) -> None: - if param.heartbeat and not activity.info().is_local: - activity.heartbeat() - if param.fail_until_attempt and activity.info().attempt < param.fail_until_attempt: - raise RuntimeError("intentional failure") - - -@dataclass -class TracingWorkflowParam: - actions: List[TracingWorkflowAction] - - -@dataclass -class TracingWorkflowAction: - fail_on_non_replay: bool = False - child_workflow: Optional[TracingWorkflowActionChildWorkflow] = None - activity: Optional[TracingWorkflowActionActivity] = None - continue_as_new: Optional[TracingWorkflowActionContinueAsNew] = None - wait_until_signal_count: int = 0 - wait_and_do_update: bool = False - - -@dataclass -class TracingWorkflowActionChildWorkflow: - id: str - param: TracingWorkflowParam - signal: bool = False - external_signal: bool = False - fail_on_non_replay_before_complete: bool = False - - -@dataclass -class TracingWorkflowActionActivity: - param: TracingActivityParam - local: bool = False - fail_on_non_replay_before_complete: bool = False - - -@dataclass -class TracingWorkflowActionContinueAsNew: - param: TracingWorkflowParam - - -ready_for_update: asyncio.Semaphore - - -@workflow.defn -class TracingWorkflow: - def __init__(self) -> None: - self._signal_count = 0 - self._did_update = False - - @workflow.run - async def run(self, param: TracingWorkflowParam) -> None: - otel_workflow.completed_span("MyCustomSpan", attributes={"foo": "bar"}) - for action in param.actions: - if action.fail_on_non_replay: - await self._raise_on_non_replay() - if action.child_workflow: - child_handle = await workflow.start_child_workflow( - TracingWorkflow.run, - action.child_workflow.param, - id=action.child_workflow.id, - ) - if action.child_workflow.fail_on_non_replay_before_complete: - await self._raise_on_non_replay() - if action.child_workflow.signal: - await child_handle.signal(TracingWorkflow.signal) - if action.child_workflow.external_signal: - external_handle: workflow.ExternalWorkflowHandle[ - TracingWorkflow - ] = workflow.get_external_workflow_handle_for( - TracingWorkflow.run, workflow_id=child_handle.id - ) - await external_handle.signal(TracingWorkflow.signal) - await child_handle - if action.activity: - retry_policy = RetryPolicy(initial_interval=timedelta(milliseconds=1)) - activity_handle = ( - workflow.start_local_activity( - tracing_activity, - action.activity.param, - start_to_close_timeout=timedelta(seconds=10), - retry_policy=retry_policy, - ) - if action.activity.local - else workflow.start_activity( - tracing_activity, - action.activity.param, - start_to_close_timeout=timedelta(seconds=10), - retry_policy=retry_policy, - ) - ) - if action.activity.fail_on_non_replay_before_complete: - await self._raise_on_non_replay() - await activity_handle - if action.continue_as_new: - workflow.continue_as_new(action.continue_as_new.param) - if action.wait_until_signal_count: - await workflow.wait_condition( - lambda: self._signal_count >= action.wait_until_signal_count - ) - if action.wait_and_do_update: - ready_for_update.release() - await workflow.wait_condition(lambda: self._did_update) - - async def _raise_on_non_replay(self) -> None: - replaying = workflow.unsafe.is_replaying() - # We sleep to force a task rollover - await asyncio.sleep(0.01) - if not replaying: - raise RuntimeError("Intentional task failure") - - @workflow.query - def query(self) -> str: - # We're gonna do a custom span here - return "some query" - - @workflow.signal - def signal(self) -> None: - self._signal_count += 1 - - @workflow.update - def update(self) -> None: - self._did_update = True - - @update.validator - def update_validator(self) -> None: - pass - - -async def test_opentelemetry_tracing(client: Client, env: WorkflowEnvironment): - # TODO(cretz): Fix - if env.supports_time_skipping: - pytest.skip( - "Java test server: https://github.com/temporalio/sdk-java/issues/1424" - ) - global ready_for_update - ready_for_update = asyncio.Semaphore(0) - # Create a tracer that has an in-memory exporter - exporter = InMemorySpanExporter() - provider = TracerProvider() - provider.add_span_processor(SimpleSpanProcessor(exporter)) - tracer = get_tracer(__name__, tracer_provider=provider) - # Create new client with tracer interceptor - client_config = client.config() - client_config["interceptors"] = [TracingInterceptor(tracer)] - client = Client(**client_config) - - task_queue = f"task_queue_{uuid.uuid4()}" - async with Worker( - client, - task_queue=task_queue, - workflows=[TracingWorkflow], - activities=[tracing_activity], - # Needed so we can wait to send update at the right time - workflow_runner=UnsandboxedWorkflowRunner(), - ): - # Run workflow with various actions - workflow_id = f"workflow_{uuid.uuid4()}" - handle = await client.start_workflow( - TracingWorkflow.run, - TracingWorkflowParam( - actions=[ - # First fail on replay - TracingWorkflowAction(fail_on_non_replay=True), - # Wait for a signal - TracingWorkflowAction(wait_until_signal_count=1), - # Exec activity that fails task before complete - TracingWorkflowAction( - activity=TracingWorkflowActionActivity( - param=TracingActivityParam(fail_until_attempt=2), - fail_on_non_replay_before_complete=True, - ), - ), - # Wait for update - TracingWorkflowAction(wait_and_do_update=True), - # Exec child workflow that fails task before complete - TracingWorkflowAction( - child_workflow=TracingWorkflowActionChildWorkflow( - id=f"{workflow_id}_child", - # Exec activity and finish after two signals - param=TracingWorkflowParam( - actions=[ - TracingWorkflowAction( - activity=TracingWorkflowActionActivity( - param=TracingActivityParam(), - local=True, - ), - ), - # Wait for the two signals - TracingWorkflowAction(wait_until_signal_count=2), - ] - ), - signal=True, - external_signal=True, - fail_on_non_replay_before_complete=True, - ) - ), - # Continue as new and run one local activity - TracingWorkflowAction( - continue_as_new=TracingWorkflowActionContinueAsNew( - param=TracingWorkflowParam( - # Do a local activity in the continue as new - actions=[ - TracingWorkflowAction( - activity=TracingWorkflowActionActivity( - param=TracingActivityParam(), - local=True, - ), - ) - ] - ) - ) - ), - ], - ), - id=workflow_id, - task_queue=task_queue, - ) - # Send query, then signal to move it along - assert "some query" == await handle.query(TracingWorkflow.query) - await handle.signal(TracingWorkflow.signal) - # Wait to send the update until after the things that fail tasks are over, as failing a task while the update - # is running can mean we execute it twice, which will mess up our spans. - async with ready_for_update: - await handle.execute_update(TracingWorkflow.update) - await handle.result() - - # Dump debug with attributes, but do string assertion test without - logging.debug( - "Spans:\n%s", - "\n".join(dump_spans(exporter.get_finished_spans(), with_attributes=False)), - ) - assert dump_spans(exporter.get_finished_spans(), with_attributes=False) == [ - "StartWorkflow:TracingWorkflow", - " RunWorkflow:TracingWorkflow", - " MyCustomSpan", - " HandleSignal:signal (links: SignalWorkflow:signal)", - " StartActivity:tracing_activity", - " RunActivity:tracing_activity", - " RunActivity:tracing_activity", - " ValidateUpdate:update (links: StartWorkflowUpdate:update)", - " HandleUpdate:update (links: StartWorkflowUpdate:update)", - " StartChildWorkflow:TracingWorkflow", - " RunWorkflow:TracingWorkflow", - " MyCustomSpan", - " StartActivity:tracing_activity", - " RunActivity:tracing_activity", - " HandleSignal:signal (links: SignalChildWorkflow:signal)", - " HandleSignal:signal (links: SignalExternalWorkflow:signal)", - " CompleteWorkflow:TracingWorkflow", - " SignalChildWorkflow:signal", - " SignalExternalWorkflow:signal", - " RunWorkflow:TracingWorkflow", - " MyCustomSpan", - " StartActivity:tracing_activity", - " RunActivity:tracing_activity", - " CompleteWorkflow:TracingWorkflow", - "QueryWorkflow:query", - " HandleQuery:query (links: StartWorkflow:TracingWorkflow)", - "SignalWorkflow:signal", - "StartWorkflowUpdate:update", - ] - - -def dump_spans( - spans: Iterable[ReadableSpan], - *, - parent_id: Optional[int] = None, - with_attributes: bool = True, - indent_depth: int = 0, -) -> List[str]: - ret: List[str] = [] - for span in spans: - if (not span.parent and parent_id is None) or ( - span.parent and span.parent.span_id == parent_id - ): - span_str = f"{' ' * indent_depth}{span.name}" - if with_attributes: - span_str += f" (attributes: {dict(span.attributes or {})})" - # Add links - if span.links: - span_links: List[str] = [] - for link in span.links: - for link_span in spans: - if link_span.context.span_id == link.context.span_id: - span_links.append(link_span.name) - span_str += f" (links: {', '.join(span_links)})" - # Signals can duplicate in rare situations, so we make sure not to - # re-add - if "Signal" in span_str and span_str in ret: - continue - ret.append(span_str) - ret += dump_spans( - spans, - parent_id=span.context.span_id, - with_attributes=with_attributes, - indent_depth=indent_depth + 1, - ) - return ret - - -@workflow.defn -class SimpleWorkflow: - @workflow.run - async def run(self) -> str: - return "done" - - -async def test_opentelemetry_always_create_workflow_spans(client: Client): - # Create a tracer that has an in-memory exporter - exporter = InMemorySpanExporter() - provider = TracerProvider() - provider.add_span_processor(SimpleSpanProcessor(exporter)) - tracer = get_tracer(__name__, tracer_provider=provider) - - # Create a worker with an interceptor without always create - async with Worker( - client, - task_queue=f"task_queue_{uuid.uuid4()}", - workflows=[SimpleWorkflow], - interceptors=[TracingInterceptor(tracer)], - ) as worker: - assert "done" == await client.execute_workflow( - SimpleWorkflow.run, - id=f"workflow_{uuid.uuid4()}", - task_queue=worker.task_queue, - ) - # Confirm the spans are not there - spans = exporter.get_finished_spans() - logging.debug("Spans:\n%s", "\n".join(dump_spans(spans, with_attributes=False))) - assert len(spans) == 0 - - # Now create a worker with an interceptor with always create - async with Worker( - client, - task_queue=f"task_queue_{uuid.uuid4()}", - workflows=[SimpleWorkflow], - interceptors=[TracingInterceptor(tracer, always_create_workflow_spans=True)], - ) as worker: - assert "done" == await client.execute_workflow( - SimpleWorkflow.run, - id=f"workflow_{uuid.uuid4()}", - task_queue=worker.task_queue, - ) - # Confirm the spans are not there - spans = exporter.get_finished_spans() - logging.debug("Spans:\n%s", "\n".join(dump_spans(spans, with_attributes=False))) - assert len(spans) > 0 - assert spans[0].name == "RunWorkflow:SimpleWorkflow" - - -# TODO(cretz): Additional tests to write -# * query without interceptor (no headers) -# * workflow without interceptor (no headers) but query with interceptor (headers) -# * workflow failure and wft failure -# * signal with start -# * signal failure and wft failure from signal diff --git a/tests/contrib/workflow_streams/__init__.py b/tests/contrib/workflow_streams/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/contrib/workflow_streams/test_payload_roundtrip.py b/tests/contrib/workflow_streams/test_payload_roundtrip.py new file mode 100644 index 000000000..545d3a405 --- /dev/null +++ b/tests/contrib/workflow_streams/test_payload_roundtrip.py @@ -0,0 +1,137 @@ +"""Regression guards for the workflow_streams Payload wire format. + +1. The default JSON converter does not handle ``Payload`` embedded in a + dataclass — serialization fails with ``TypeError``. This rules out a + naive nested-Payload wire format. +2. A proto-serialized ``Payload`` inside a dataclass does round-trip. + This is the wire format used: base64 of ``Payload.SerializeToString()`` + inside ``PublishEntry``/``_WorkflowStreamWireItem``, surfacing + ``Payload`` (or a decoded value via ``result_type=``) at the user API. +""" + +from __future__ import annotations + +import base64 +import uuid +from dataclasses import dataclass, field + +import pytest + +from temporalio import workflow +from temporalio.api.common.v1 import Payload +from temporalio.client import Client +from tests.helpers import new_worker + + +@dataclass +class NestedPayloadEnvelope: + items: list[Payload] = field(default_factory=list) + + +@dataclass +class SerializedEntry: + topic: str + data: str # base64(Payload.SerializeToString()) + + +@dataclass +class SerializedEnvelope: + items: list[SerializedEntry] = field(default_factory=list) + + +@workflow.defn +class NestedPayloadWorkflow: + def __init__(self) -> None: + self._received: NestedPayloadEnvelope | None = None + + @workflow.signal + def receive(self, envelope: NestedPayloadEnvelope) -> None: + self._received = envelope + + @workflow.query + def decoded_strings(self) -> list[str]: + assert self._received is not None + conv = workflow.payload_converter() + return [conv.from_payload(p, str) for p in self._received.items] + + @workflow.run + async def run(self) -> None: + await workflow.wait_condition(lambda: self._received is not None) + + +@workflow.defn +class SerializedPayloadWorkflow: + def __init__(self) -> None: + self._received: SerializedEnvelope | None = None + + @workflow.signal + def receive(self, envelope: SerializedEnvelope) -> None: + self._received = envelope + + @workflow.query + def decoded_strings(self) -> list[str]: + assert self._received is not None + conv = workflow.payload_converter() + out: list[str] = [] + for entry in self._received.items: + p = Payload() + p.ParseFromString(base64.b64decode(entry.data)) + out.append(conv.from_payload(p, str)) + return out + + @workflow.query + def topics(self) -> list[str]: + assert self._received is not None + return [e.topic for e in self._received.items] + + @workflow.run + async def run(self) -> None: + await workflow.wait_condition(lambda: self._received is not None) + + +@pytest.mark.asyncio +async def test_nested_payload_in_dataclass_fails(client: Client) -> None: + """Confirm the load-bearing negative result: Payload inside dataclass doesn't serialize.""" + conv = client.data_converter.payload_converter + payloads = [conv.to_payloads([v])[0] for v in ["hello", "world"]] + envelope = NestedPayloadEnvelope(items=payloads) + + async with new_worker(client, NestedPayloadWorkflow) as worker: + handle = await client.start_workflow( + NestedPayloadWorkflow.run, + id=f"nested-payload-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + with pytest.raises(TypeError, match="Payload is not JSON serializable"): + await handle.signal(NestedPayloadWorkflow.receive, envelope) + await handle.terminate() + + +@pytest.mark.asyncio +async def test_serialized_payload_fallback_round_trips(client: Client) -> None: + """Proto-serialize Payload -> base64 -> dataclass round-trips through signal.""" + conv = client.data_converter.payload_converter + originals = ["hello", "world", "payload"] + payloads = [conv.to_payloads([v])[0] for v in originals] + envelope = SerializedEnvelope( + items=[ + SerializedEntry( + topic=f"t{i}", + data=base64.b64encode(p.SerializeToString()).decode("ascii"), + ) + for i, p in enumerate(payloads) + ] + ) + + async with new_worker(client, SerializedPayloadWorkflow) as worker: + handle = await client.start_workflow( + SerializedPayloadWorkflow.run, + id=f"serialized-payload-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.signal(SerializedPayloadWorkflow.receive, envelope) + decoded = await handle.query(SerializedPayloadWorkflow.decoded_strings) + assert decoded == originals + topics = await handle.query(SerializedPayloadWorkflow.topics) + assert topics == ["t0", "t1", "t2"] + await handle.result() diff --git a/tests/contrib/workflow_streams/test_workflow_streams.py b/tests/contrib/workflow_streams/test_workflow_streams.py new file mode 100644 index 000000000..e7cedd038 --- /dev/null +++ b/tests/contrib/workflow_streams/test_workflow_streams.py @@ -0,0 +1,2790 @@ +"""E2E integration tests for temporalio.contrib.workflow_streams.""" + +from __future__ import annotations + +import asyncio +import sys +import uuid +from dataclasses import dataclass +from datetime import timedelta +from typing import Any, cast +from unittest.mock import patch + +if sys.version_info >= (3, 11): + from asyncio import timeout as _async_timeout # pyright: ignore[reportUnreachable] +else: + from async_timeout import ( # pyright: ignore[reportUnreachable] + timeout as _async_timeout, + ) + +import google.protobuf.duration_pb2 +import nexusrpc +import nexusrpc.handler +import pytest + +import temporalio.api.nexus.v1 +import temporalio.api.operatorservice.v1 +import temporalio.api.workflowservice.v1 +from temporalio import activity, nexus, workflow +from temporalio.client import ( + Client, + WorkflowExecutionStatus, + WorkflowHandle, + WorkflowUpdateFailedError, + WorkflowUpdateStage, +) +from temporalio.common import RawValue +from temporalio.contrib.workflow_streams import ( + PollInput, + PollResult, + PublishEntry, + PublishInput, + TopicHandle, + WorkflowStream, + WorkflowStreamClient, + WorkflowStreamItem, + WorkflowStreamState, + WorkflowTopicHandle, +) +from temporalio.contrib.workflow_streams._types import _encode_payload +from temporalio.converter import DataConverter +from temporalio.exceptions import ApplicationError +from temporalio.nexus import WorkflowRunOperationContext, workflow_run_operation +from temporalio.testing import WorkflowEnvironment +from temporalio.worker import Worker +from tests.helpers import assert_eq_eventually, new_worker +from tests.helpers.nexus import make_nexus_endpoint_name + + +def _wire_bytes(data: bytes) -> str: + """Build a PublishEntry.data string from raw bytes. + + Mirrors what :class:`WorkflowStreamClient` produces on the encode path: + default payload converter turns the bytes into a ``Payload``, which + is then proto-serialized and base64-encoded for the wire. + """ + payload = DataConverter.default.payload_converter.to_payloads([data])[0] + return _encode_payload(payload) + + +# --------------------------------------------------------------------------- +# Test workflows (must be module-level, not local classes) +# --------------------------------------------------------------------------- + + +@workflow.defn +class BasicWorkflowStreamWorkflow: + @workflow.init + def __init__(self) -> None: + self.stream = WorkflowStream() + self._closed = False + + @workflow.signal + def close(self) -> None: + self._closed = True + + @workflow.run + async def run(self) -> None: + await workflow.wait_condition(lambda: self._closed) + + +@workflow.defn +class ActivityPublishWorkflow: + @workflow.init + def __init__(self, count: int) -> None: + self.stream = WorkflowStream() + self._closed = False + + @workflow.signal + def close(self) -> None: + self._closed = True + + @workflow.run + async def run(self, count: int) -> None: + await workflow.execute_activity( + "publish_items", + count, + start_to_close_timeout=timedelta(seconds=30), + heartbeat_timeout=timedelta(seconds=10), + ) + self.stream.topic("status", type=bytes).publish(b"activity_done") + await workflow.wait_condition(lambda: self._closed) + + +@dataclass +class AgentEvent: + kind: str + payload: dict[str, Any] + + +@workflow.defn +class StructuredPublishWorkflow: + @workflow.init + def __init__(self, count: int) -> None: + self.stream = WorkflowStream() + self._closed = False + + @workflow.signal + def close(self) -> None: + self._closed = True + + @workflow.run + async def run(self, count: int) -> None: + for i in range(count): + self.stream.topic("events", type=AgentEvent).publish( + AgentEvent(kind="tick", payload={"i": i}) + ) + await workflow.wait_condition(lambda: self._closed) + + +@workflow.defn +class TopicHandlePublishWorkflow: + """Workflow that publishes via the workflow-side topic handle.""" + + @workflow.init + def __init__(self, count: int) -> None: + self.stream = WorkflowStream() + self.events = self.stream.topic("events", type=AgentEvent) + self._closed = False + + @workflow.signal + def close(self) -> None: + self._closed = True + + @workflow.run + async def run(self, count: int) -> None: + for i in range(count): + self.events.publish(AgentEvent(kind="tick", payload={"i": i})) + await workflow.wait_condition(lambda: self._closed) + + +@workflow.defn +class WorkflowSidePublishWorkflow: + @workflow.init + def __init__(self, count: int) -> None: + self.stream = WorkflowStream() + self._closed = False + + @workflow.signal + def close(self) -> None: + self._closed = True + + @workflow.run + async def run(self, count: int) -> None: + for i in range(count): + self.stream.topic("events", type=bytes).publish(f"item-{i}".encode()) + await workflow.wait_condition(lambda: self._closed) + + +@workflow.defn +class MultiTopicWorkflow: + @workflow.init + def __init__(self, count: int) -> None: + self.stream = WorkflowStream() + self._closed = False + + @workflow.signal + def close(self) -> None: + self._closed = True + + @workflow.run + async def run(self, count: int) -> None: + await workflow.execute_activity( + "publish_multi_topic", + count, + start_to_close_timeout=timedelta(seconds=30), + heartbeat_timeout=timedelta(seconds=10), + ) + await workflow.wait_condition(lambda: self._closed) + + +@workflow.defn +class InterleavedWorkflow: + @workflow.init + def __init__(self, count: int) -> None: + self.stream = WorkflowStream() + self._closed = False + + @workflow.signal + def close(self) -> None: + self._closed = True + + @workflow.run + async def run(self, count: int) -> None: + self.stream.topic("status", type=bytes).publish(b"started") + await workflow.execute_activity( + "publish_items", + count, + start_to_close_timeout=timedelta(seconds=30), + heartbeat_timeout=timedelta(seconds=10), + ) + self.stream.topic("status", type=bytes).publish(b"done") + await workflow.wait_condition(lambda: self._closed) + + +@workflow.defn +class PriorityWorkflow: + @workflow.init + def __init__(self) -> None: + self.stream = WorkflowStream() + self._closed = False + + @workflow.signal + def close(self) -> None: + self._closed = True + + @workflow.run + async def run(self) -> None: + await workflow.execute_activity( + "publish_with_priority", + start_to_close_timeout=timedelta(seconds=30), + heartbeat_timeout=timedelta(seconds=10), + ) + await workflow.wait_condition(lambda: self._closed) + + +@workflow.defn +class FlushOnExitWorkflow: + @workflow.init + def __init__(self, count: int) -> None: + self.stream = WorkflowStream() + self._closed = False + + @workflow.signal + def close(self) -> None: + self._closed = True + + @workflow.run + async def run(self, count: int) -> None: + await workflow.execute_activity( + "publish_batch_test", + count, + start_to_close_timeout=timedelta(seconds=30), + heartbeat_timeout=timedelta(seconds=10), + ) + await workflow.wait_condition(lambda: self._closed) + + +@workflow.defn +class MaxBatchWorkflow: + @workflow.init + def __init__(self, count: int) -> None: + self.stream = WorkflowStream() + self._closed = False + + @workflow.signal + def close(self) -> None: + self._closed = True + + @workflow.query + def publisher_sequences(self) -> dict[str, int]: + return {pid: ps.sequence for pid, ps in self.stream._publishers.items()} + + @workflow.run + async def run(self, count: int) -> None: + await workflow.execute_activity( + "publish_with_max_batch", + count, + start_to_close_timeout=timedelta(seconds=30), + heartbeat_timeout=timedelta(seconds=10), + ) + self.stream.topic("status", type=bytes).publish(b"activity_done") + await workflow.wait_condition(lambda: self._closed) + + +@workflow.defn +class LateWorkflowStreamWorkflow: + """Calls WorkflowStream() from @workflow.run, not from @workflow.init. + + The constructor inspects the caller's frame and requires the + function name to be ``__init__``; called from ``run``, it must + raise ``RuntimeError``. The workflow returns the error message so + the test can assert on it without forcing a workflow task failure. + """ + + @workflow.run + async def run(self) -> str: + try: + WorkflowStream() + except RuntimeError as e: + return str(e) + return "no error raised" + + +@workflow.defn +class DoubleInitWorkflow: + """Calls WorkflowStream() twice from @workflow.init. + + The first call succeeds; the second must raise RuntimeError because + the workflow stream signal handler is already registered. The workflow + stashes the error message so the test can assert on it without + forcing a workflow task failure. + """ + + @workflow.init + def __init__(self) -> None: + self.stream = WorkflowStream() + self._closed = False + self.double_init_error: str | None = None + try: + WorkflowStream() + except RuntimeError as e: + self.double_init_error = str(e) + + @workflow.signal + def close(self) -> None: + self._closed = True + + @workflow.query + def get_double_init_error(self) -> str | None: + return self.double_init_error + + @workflow.run + async def run(self) -> None: + await workflow.wait_condition(lambda: self._closed) + + +# --------------------------------------------------------------------------- +# Activities +# --------------------------------------------------------------------------- + + +@activity.defn(name="publish_items") +async def publish_items(count: int) -> None: + client = WorkflowStreamClient.from_within_activity( + batch_interval=timedelta(milliseconds=500) + ) + async with client: + for i in range(count): + activity.heartbeat() + client.topic("events", type=bytes).publish(f"item-{i}".encode()) + + +@activity.defn(name="publish_multi_topic") +async def publish_multi_topic(count: int) -> None: + topics = ["a", "b", "c"] + client = WorkflowStreamClient.from_within_activity( + batch_interval=timedelta(milliseconds=500) + ) + async with client: + for i in range(count): + activity.heartbeat() + topic = topics[i % len(topics)] + client.topic(topic, type=bytes).publish(f"{topic}-{i}".encode()) + + +@activity.defn(name="publish_with_priority") +async def publish_with_priority() -> None: + # Long batch_interval AND long post-publish hold ensure that only a + # working force_flush wakeup can deliver items before __aexit__ flushes. + # The hold is deliberately much longer than the test's collect timeout + # so a regression (force_flush no-op) surfaces as a missing item rather + # than flaking on slow CI. + client = WorkflowStreamClient.from_within_activity( + batch_interval=timedelta(seconds=60) + ) + async with client: + client.topic("events", type=bytes).publish(b"normal-0") + client.topic("events", type=bytes).publish(b"normal-1") + client.topic("events", type=bytes).publish(b"priority", force_flush=True) + for _ in range(100): + activity.heartbeat() + await asyncio.sleep(0.1) + + +@activity.defn(name="publish_batch_test") +async def publish_batch_test(count: int) -> None: + client = WorkflowStreamClient.from_within_activity( + batch_interval=timedelta(seconds=60) + ) + async with client: + for i in range(count): + activity.heartbeat() + client.topic("events", type=bytes).publish(f"item-{i}".encode()) + + +@activity.defn(name="publish_with_max_batch") +async def publish_with_max_batch(count: int) -> None: + client = WorkflowStreamClient.from_within_activity( + batch_interval=timedelta(seconds=60), max_batch_size=3 + ) + async with client: + for i in range(count): + activity.heartbeat() + client.topic("events", type=bytes).publish(f"item-{i}".encode()) + # Yield so the flusher task can run when max_batch_size triggers + # _flush_event. Real workloads (e.g. agents awaiting LLM streams) + # yield constantly; a tight loop with no awaits would never let + # the flusher fire and would collapse back to exit-only flushing. + await asyncio.sleep(0) + # Long batch_interval ensures only max_batch_size triggers flushes. + # Context manager exit flushes any remainder. + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +async def _is_different_run( + old_handle: WorkflowHandle[Any, Any], + new_handle: WorkflowHandle[Any, Any], +) -> bool: + """Check if new_handle points to a different run than old_handle.""" + try: + desc = await new_handle.describe() + return desc.run_id != old_handle.result_run_id + except Exception: + return False + + +async def collect_items( + client: Client, + handle: WorkflowHandle[Any, Any], + topics: list[str] | None, + from_offset: int, + expected_count: int, + timeout: float = 15.0, + *, + result_type: type | None = bytes, +) -> list[WorkflowStreamItem]: + """Subscribe and collect exactly expected_count items, with timeout. + + Default ``result_type=bytes`` matches the bytes-oriented tests that + compare ``item.data`` against literal byte strings. Pass + ``result_type=None`` for the converter's default ``Any`` decoding, + or ``result_type=RawValue`` for a ``RawValue``-wrapped ``Payload``. + """ + stream = WorkflowStreamClient.create(client, handle.id) + items: list[WorkflowStreamItem] = [] + try: + async with _async_timeout(timeout): + async for item in stream.subscribe( + topics=topics, + from_offset=from_offset, + poll_cooldown=timedelta(0), + result_type=result_type, + ): + items.append(item) + if len(items) >= expected_count: + break + except asyncio.TimeoutError: + pass + return items + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_activity_publish_and_subscribe(client: Client) -> None: + """Activity publishes items, external client subscribes and receives them.""" + count = 10 + async with new_worker( + client, + ActivityPublishWorkflow, + activities=[publish_items], + ) as worker: + handle = await client.start_workflow( + ActivityPublishWorkflow.run, + count, + id=f"workflow-stream-basic-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + # Collect activity items + the "activity_done" status item + items = await collect_items(client, handle, None, 0, count + 1) + assert len(items) == count + 1 + + # Check activity items + for i in range(count): + assert items[i].topic == "events" + assert items[i].data == f"item-{i}".encode() + + # Check workflow-side status item + assert items[count].topic == "status" + assert items[count].data == b"activity_done" + + await handle.signal(ActivityPublishWorkflow.close) + + +@pytest.mark.asyncio +async def test_structured_type_round_trip(client: Client) -> None: + """Workflow publishes dataclass values; subscriber decodes via result_type.""" + count = 4 + async with new_worker(client, StructuredPublishWorkflow) as worker: + handle = await client.start_workflow( + StructuredPublishWorkflow.run, + count, + id=f"workflow-stream-structured-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + items = await collect_items( + client, handle, None, 0, count, result_type=AgentEvent + ) + assert len(items) == count + for i, item in enumerate(items): + assert isinstance(item.data, AgentEvent) + assert item.data == AgentEvent(kind="tick", payload={"i": i}) + + await handle.signal(StructuredPublishWorkflow.close) + + +@pytest.mark.asyncio +async def test_subscribe_default_decode_and_raw_value(client: Client) -> None: + """No ``result_type`` decodes via Any; ``result_type=RawValue`` yields a ``Payload``.""" + count = 2 + async with new_worker(client, StructuredPublishWorkflow) as worker: + handle = await client.start_workflow( + StructuredPublishWorkflow.run, + count, + id=f"workflow-stream-default-decode-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + any_items = await collect_items( + client, handle, None, 0, count, result_type=None + ) + assert len(any_items) == count + for i, item in enumerate(any_items): + # Default JSON converter decodes a dataclass to a plain dict. + assert item.data == {"kind": "tick", "payload": {"i": i}} + + raw_items = await collect_items( + client, handle, None, 0, count, result_type=RawValue + ) + assert len(raw_items) == count + for item in raw_items: + assert isinstance(item.data, RawValue) + assert item.data.payload.data # non-empty serialized JSON bytes + + await handle.signal(StructuredPublishWorkflow.close) + + +@pytest.mark.asyncio +async def test_subscribe_with_payload_result_type_rejected(client: Client) -> None: + """``subscribe(result_type=Payload)`` raises — there is no Payload decode path. + + Mirrors the topic-handle rejection (``stream.topic(name, type=Payload)``) + so the direct ``subscribe`` API can't smuggle in the same ambiguity that + the topic-handle layer already guards against. Users wanting raw payloads + pass ``result_type=RawValue``. + """ + from temporalio.api.common.v1 import Payload + + handle = client.get_workflow_handle("nonexistent-workflow-id") + stream = WorkflowStreamClient(handle) + with pytest.raises(RuntimeError, match="result_type=Payload"): + async for _ in stream.subscribe(result_type=Payload): + pass + + +@pytest.mark.asyncio +async def test_topic_handle_workflow_side_publish_and_subscribe( + client: Client, +) -> None: + """Workflow publishes via WorkflowStream.topic; client subscribes via TopicHandle.""" + count = 3 + async with new_worker(client, TopicHandlePublishWorkflow) as worker: + handle = await client.start_workflow( + TopicHandlePublishWorkflow.run, + count, + id=f"workflow-stream-topic-handle-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + stream = WorkflowStreamClient.create(client, handle.id) + events = stream.topic("events", type=AgentEvent) + assert isinstance(events, TopicHandle) + assert events.name == "events" + assert events.type is AgentEvent + + items: list[WorkflowStreamItem] = [] + async with _async_timeout(15.0): + async for item in events.subscribe(poll_cooldown=timedelta(0)): + items.append(item) + if len(items) >= count: + break + assert [item.data for item in items] == [ + AgentEvent(kind="tick", payload={"i": i}) for i in range(count) + ] + + await handle.signal(TopicHandlePublishWorkflow.close) + + +@workflow.defn +class TopicHandleUniquenessWorkflow: + """Probes the WorkflowStream.topic uniqueness check in @workflow.init. + + Returns a tuple (idempotent_ok, error_message) so the test can assert + both branches: same-type rebind is silent, different-type rebind raises. + """ + + @workflow.init + def __init__(self) -> None: + from temporalio.api.common.v1 import Payload + + self.stream = WorkflowStream() + first = self.stream.topic("events", type=AgentEvent) + self._idempotent_ok = ( + isinstance( + self.stream.topic("events", type=AgentEvent), WorkflowTopicHandle + ) + and first.type is AgentEvent + ) + try: + self.stream.topic("events", type=bytes) + except RuntimeError as exc: + self._error = str(exc) + else: + self._error = "" + try: + self.stream.topic("misused", type=Payload) + except RuntimeError as exc: + self._payload_error = str(exc) + else: + self._payload_error = "" + + @workflow.run + async def run(self) -> tuple[bool, str, str]: + return (self._idempotent_ok, self._error, self._payload_error) + + +@pytest.mark.asyncio +async def test_topic_handle_uniqueness_on_workflow_stream(client: Client) -> None: + """Same-type rebind is idempotent; different-type rebind raises in @workflow.init. + + Also covers the workflow-side rejection of ``type=Payload`` — + binding a topic to ``Payload`` itself has no decode path, so + ``WorkflowStream.topic`` raises in ``@workflow.init``. + """ + async with new_worker(client, TopicHandleUniquenessWorkflow) as worker: + idempotent_ok, error, payload_error = await client.execute_workflow( + TopicHandleUniquenessWorkflow.run, + id=f"workflow-stream-handle-unique-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + assert idempotent_ok is True + assert "already bound to type" in error + assert "events" in error + assert "type=Payload" in payload_error + + +@pytest.mark.asyncio +async def test_topic_handle_client_uniqueness(client: Client) -> None: + """Re-binding a topic name to a different type on a client raises.""" + handle = client.get_workflow_handle("nonexistent-workflow-id") + stream = WorkflowStreamClient(handle) + + first = stream.topic("events", type=AgentEvent) + assert first.name == "events" + assert first.type is AgentEvent + + # Same type is idempotent. + again = stream.topic("events", type=AgentEvent) + assert again.type is AgentEvent + + # Different type raises. + with pytest.raises(RuntimeError, match="already bound to type"): + stream.topic("events", type=bytes) + + # Different topic with a different type is fine. + other = stream.topic("other", type=bytes) + assert other.type is bytes + + # Any escape hatch coexists on a different topic. Omitting ``type`` + # is the documented form (defaults to ``typing.Any``); we also + # exercise the explicit ``type=Any`` path with the cast required + # because ``Any`` is a typing special form rather than a class. + raw = stream.topic("forwarded") + assert raw.type is Any + explicit = stream.topic( + "forwarded-explicit", type=cast(type[Any], cast(object, Any)) + ) + assert explicit.type is Any + + # Binding to Payload itself is rejected — subscribers would have + # no decode path. Pre-built Payload values can still be published + # via a normally-typed handle (zero-copy fast path). + from temporalio.api.common.v1 import Payload + + with pytest.raises(RuntimeError, match="type=Payload"): + stream.topic("misused", type=Payload) + + +@pytest.mark.asyncio +async def test_topic_handle_payload_passthrough(client: Client) -> None: + """Pre-built Payloads pass through topic.publish regardless of bound type.""" + count = 2 + async with new_worker(client, BasicWorkflowStreamWorkflow) as worker: + handle = await client.start_workflow( + BasicWorkflowStreamWorkflow.run, + id=f"workflow-stream-handle-payload-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + stream = WorkflowStreamClient.create( + client, handle.id, batch_interval=timedelta(milliseconds=50) + ) + events = stream.topic("events", type=bytes) + async with stream: + converter = DataConverter.default.payload_converter + for i in range(count): + payload = converter.to_payloads([f"raw-{i}".encode()])[0] + events.publish(payload) + await stream.flush() + + items = await collect_items(client, handle, ["events"], 0, count) + assert [item.data for item in items] == [ + f"raw-{i}".encode() for i in range(count) + ] + + await handle.signal(BasicWorkflowStreamWorkflow.close) + + +@pytest.mark.asyncio +async def test_topic_filtering(client: Client) -> None: + """Publish to multiple topics, subscribe with filter.""" + count = 9 # 3 per topic + async with new_worker( + client, + MultiTopicWorkflow, + activities=[publish_multi_topic], + ) as worker: + handle = await client.start_workflow( + MultiTopicWorkflow.run, + count, + id=f"workflow-stream-filter-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + # Subscribe to topic "a" only — should get 3 items + a_items = await collect_items(client, handle, ["a"], 0, 3) + assert len(a_items) == 3 + assert all(item.topic == "a" for item in a_items) + + # Subscribe to ["a", "c"] — should get 6 items + ac_items = await collect_items(client, handle, ["a", "c"], 0, 6) + assert len(ac_items) == 6 + assert all(item.topic in ("a", "c") for item in ac_items) + + # Subscribe to all (None) — should get all 9 + all_items = await collect_items(client, handle, None, 0, 9) + assert len(all_items) == 9 + + await handle.signal(MultiTopicWorkflow.close) + + +@pytest.mark.asyncio +async def test_subscribe_from_offset_and_per_item_offsets(client: Client) -> None: + """Subscribe from zero and non-zero offsets; each item carries its global offset.""" + count = 5 + async with new_worker( + client, + WorkflowSidePublishWorkflow, + ) as worker: + handle = await client.start_workflow( + WorkflowSidePublishWorkflow.run, + count, + id=f"workflow-stream-offset-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + # Subscribe from offset 0 — all items, offsets 0..count-1 + all_items = await collect_items(client, handle, None, 0, count) + assert len(all_items) == count + for i, item in enumerate(all_items): + assert item.offset == i + assert item.data == f"item-{i}".encode() + + # Subscribe from offset 3 — items 3, 4 with offsets 3, 4 + later_items = await collect_items(client, handle, None, 3, 2) + assert len(later_items) == 2 + assert later_items[0].offset == 3 + assert later_items[0].data == b"item-3" + assert later_items[1].offset == 4 + assert later_items[1].data == b"item-4" + + await handle.signal(WorkflowSidePublishWorkflow.close) + + +@pytest.mark.asyncio +async def test_per_item_offsets_with_topic_filter(client: Client) -> None: + """Per-item offsets are global (not per-topic) even when filtering.""" + count = 9 # 3 per topic (a, b, c round-robin) + async with new_worker( + client, + MultiTopicWorkflow, + activities=[publish_multi_topic], + ) as worker: + handle = await client.start_workflow( + MultiTopicWorkflow.run, + count, + id=f"workflow-stream-item-offset-filter-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + # Subscribe to topic "a" only — items are at global offsets 0, 3, 6 + a_items = await collect_items(client, handle, ["a"], 0, 3) + assert len(a_items) == 3 + assert a_items[0].offset == 0 + assert a_items[1].offset == 3 + assert a_items[2].offset == 6 + + # Subscribe to topic "b" — items are at global offsets 1, 4, 7 + b_items = await collect_items(client, handle, ["b"], 0, 3) + assert len(b_items) == 3 + assert b_items[0].offset == 1 + assert b_items[1].offset == 4 + assert b_items[2].offset == 7 + + await handle.signal(MultiTopicWorkflow.close) + + +@pytest.mark.asyncio +async def test_poll_truncated_offset_returns_application_error(client: Client) -> None: + """Polling a truncated offset raises ApplicationError (not ValueError) + and does not crash the workflow task.""" + async with new_worker( + client, + TruncateWorkflow, + ) as worker: + handle = await client.start_workflow( + TruncateWorkflow.run, + 5, + id=f"workflow-stream-trunc-error-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + # Truncate up to offset 3 via update — completion is explicit. + await handle.execute_update("truncate", 3) + + # Poll from offset 1 (truncated) — should get ApplicationError, + # NOT crash the workflow task. Catching WorkflowUpdateFailedError is + # sufficient to prove the handler raised ApplicationError: Temporal's + # update protocol completes the update with this error only when the + # handler raises ApplicationError. A bare ValueError (or any other + # exception) would fail the workflow task instead, causing + # execute_update to hang — not raise. The follow-up collect_items + # below proves the workflow task wasn't poisoned. + with pytest.raises(WorkflowUpdateFailedError) as exc_info: + await handle.execute_update( + "__temporal_workflow_stream_poll", + PollInput(topics=[], from_offset=1), + result_type=PollResult, + ) + cause = exc_info.value.cause + assert isinstance(cause, ApplicationError) + assert cause.type == "TruncatedOffset" + + # Workflow should still be usable — poll from valid offset 3 + items = await collect_items(client, handle, None, 3, 2) + assert len(items) == 2 + assert items[0].offset == 3 + + await handle.signal("close") + + +@pytest.mark.asyncio +async def test_truncate_past_end_raises_application_error(client: Client) -> None: + """truncate() with an offset past the log end raises ApplicationError + (type=TruncateOutOfRange) — the update surfaces as a clean failure + without poisoning the workflow task.""" + async with new_worker( + client, + TruncateWorkflow, + ) as worker: + handle = await client.start_workflow( + TruncateWorkflow.run, + 2, + id=f"workflow-stream-trunc-oor-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + # Only 2 items exist; asking to truncate to offset 5 is out of range. + with pytest.raises(WorkflowUpdateFailedError) as exc_info: + await handle.execute_update("truncate", 5) + cause = exc_info.value.cause + assert isinstance(cause, ApplicationError) + assert cause.type == "TruncateOutOfRange" + + # Workflow task wasn't poisoned — a valid poll still completes. + items = await collect_items(client, handle, None, 0, 2) + assert len(items) == 2 + + await handle.signal("close") + + +@pytest.mark.asyncio +async def test_subscribe_recovers_from_truncation(client: Client) -> None: + """subscribe() auto-recovers when offset falls behind truncation.""" + async with new_worker( + client, + TruncateWorkflow, + ) as worker: + handle = await client.start_workflow( + TruncateWorkflow.run, + 5, + id=f"workflow-stream-trunc-recover-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + # Truncate first 3. The update returns after the handler completes. + await handle.execute_update("truncate", 3) + + # subscribe from offset 1 (truncated) — should auto-recover + # and deliver items from base_offset (3) + stream = WorkflowStreamClient(handle) + items: list[WorkflowStreamItem] = [] + try: + async with _async_timeout(5): + async for item in stream.subscribe( + from_offset=1, poll_cooldown=timedelta(0), result_type=bytes + ): + items.append(item) + if len(items) >= 2: + break + except asyncio.TimeoutError: + pass + assert len(items) == 2 + assert items[0].offset == 3 + + await handle.signal("close") + + +@pytest.mark.asyncio +async def test_truncate_during_waiting_poll_raises_truncated_offset( + client: Client, +) -> None: + """A truncate that advances ``base_offset`` past a waiting poll's + ``from_offset`` must wake the poll and raise ``TruncatedOffset``. + + Reproduces the bug where ``_on_poll`` captured ``log_offset`` once + before ``wait_condition`` and then sliced ``self._log[log_offset:]`` + against the post-truncate state. With the old predicate + ``len(self._log) > log_offset`` the wait would either never fire + (truncation shrinks the log below the captured offset) or fire on a + later publish and silently emit the wrong items at offsets the + subscriber had already moved past. + """ + async with new_worker(client, TruncateRaceWorkflow) as worker: + handle = await client.start_workflow( + TruncateRaceWorkflow.run, + id=f"workflow-stream-trunc-race-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + # Seed: 5 items at offsets 0..4. base_offset stays 0. + await handle.execute_update(TruncateRaceWorkflow.publish, 5) + + # Park a poll from offset=10 — past the current end of the log. + # With wait_for_stage=ACCEPTED the handler has begun executing + # and is parked at workflow.wait_condition by the time the + # client gets the handle back. + poll_handle = await handle.start_update( + "__temporal_workflow_stream_poll", + PollInput(topics=[], from_offset=10), + result_type=PollResult, + wait_for_stage=WorkflowUpdateStage.ACCEPTED, + ) + + # In one workflow activation: publish 7 more items (log grows to + # 12 entries at offsets 0..11) and then truncate to 11. Result: + # base_offset=11, log=[item @11]. The waiting poll's + # from_offset=10 is now strictly less than base_offset, so the + # fixed predicate must wake it and the post-wait recompute must + # raise TruncatedOffset. Both halves of the fix are exercised: + # without the predicate change the wait stays asleep through + # this activation; without the post-wait recompute the slice + # silently returns wrong items / next_offset. + await handle.execute_update(TruncateRaceWorkflow.publish_then_truncate, (7, 11)) + + with pytest.raises(WorkflowUpdateFailedError) as exc_info: + await poll_handle.result() + cause = exc_info.value.cause + assert isinstance(cause, ApplicationError) + assert cause.type == "TruncatedOffset" + + await handle.signal(TruncateRaceWorkflow.close) + + +@pytest.mark.asyncio +async def test_workflow_and_activity_publish_interleaved(client: Client) -> None: + """Workflow publishes status events around activity publishing.""" + count = 5 + async with new_worker( + client, + InterleavedWorkflow, + activities=[publish_items], + ) as worker: + handle = await client.start_workflow( + InterleavedWorkflow.run, + count, + id=f"workflow-stream-interleave-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + # Total: 1 (started) + count (activity) + 1 (done) = count + 2 + items = await collect_items(client, handle, None, 0, count + 2) + assert len(items) == count + 2 + + # First item is workflow-side "started" + assert items[0].topic == "status" + assert items[0].data == b"started" + + # Middle items are from activity + for i in range(count): + assert items[i + 1].topic == "events" + assert items[i + 1].data == f"item-{i}".encode() + + # Last item is workflow-side "done" + assert items[count + 1].topic == "status" + assert items[count + 1].data == b"done" + + await handle.signal(InterleavedWorkflow.close) + + +@pytest.mark.asyncio +async def test_priority_flush(client: Client) -> None: + """Priority publish triggers immediate flush without waiting for timer.""" + async with new_worker( + client, + PriorityWorkflow, + activities=[publish_with_priority], + ) as worker: + handle = await client.start_workflow( + PriorityWorkflow.run, + id=f"workflow-stream-priority-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + # If priority works, items arrive within milliseconds of the publish. + # The activity holds for ~10s after priority publish; this timeout + # gives plenty of margin for workflow/worker scheduling on slow CI + # while staying well below the activity hold so a regression (no + # priority wakeup) surfaces as a missing item, not a pass via + # __aexit__ flush. + items = await collect_items(client, handle, None, 0, 3, timeout=5.0) + assert len(items) == 3 + assert items[2].data == b"priority" + + await handle.signal(PriorityWorkflow.close) + + +@pytest.mark.asyncio +async def test_iterator_cancellation(client: Client) -> None: + """Cancelling a subscription iterator after it has yielded an item + completes cleanly.""" + async with new_worker( + client, + BasicWorkflowStreamWorkflow, + ) as worker: + handle = await client.start_workflow( + BasicWorkflowStreamWorkflow.run, + id=f"workflow-stream-cancel-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + # Seed one item so the iterator provably reaches an active state + # before we cancel — no sleep-based wait. + await handle.signal( + "__temporal_workflow_stream_publish", + PublishInput( + items=[PublishEntry(topic="events", data=_wire_bytes(b"seed"))] + ), + ) + + stream_client = WorkflowStreamClient.create(client, handle.id) + first_item = asyncio.Event() + items: list[WorkflowStreamItem] = [] + + async def subscribe_and_collect() -> None: + async for item in stream_client.subscribe( + from_offset=0, poll_cooldown=timedelta(0), result_type=bytes + ): + items.append(item) + first_item.set() + + task = asyncio.create_task(subscribe_and_collect()) + # Bounded wait so a subscribe regression fails fast instead of hanging. + async with _async_timeout(5): + await first_item.wait() + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + + assert len(items) == 1 + assert items[0].data == b"seed" + + await handle.signal(BasicWorkflowStreamWorkflow.close) + + +@pytest.mark.asyncio +async def test_context_manager_flushes_on_exit(client: Client) -> None: + """Context manager exit flushes all buffered items.""" + count = 5 + async with new_worker( + client, + FlushOnExitWorkflow, + activities=[publish_batch_test], + ) as worker: + handle = await client.start_workflow( + FlushOnExitWorkflow.run, + count, + id=f"workflow-stream-flush-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + # Despite 60s batch interval, all items arrive because __aexit__ flushes + items = await collect_items(client, handle, None, 0, count, timeout=15.0) + assert len(items) == count + for i in range(count): + assert items[i].data == f"item-{i}".encode() + + await handle.signal(FlushOnExitWorkflow.close) + + +@pytest.mark.asyncio +async def test_explicit_flush_barrier(client: Client) -> None: + """``await client.flush()`` is a synchronization point. + + Verifies the documented contract: + 1. Returns immediately when the buffer is empty. + 2. After it returns, items published before the call are durable + on the workflow side (observable via ``get_offset()``) — even + when the timer-driven flush would not yet have fired. + 3. Calling it again after a successful flush is a no-op. + + Uses a 60s ``batch_interval`` so a regression where ``flush()`` + silently relies on the background timer surfaces as a hang + against the test's 5s timeout, not a slow pass. + """ + async with new_worker( + client, + BasicWorkflowStreamWorkflow, + ) as worker: + handle = await client.start_workflow( + BasicWorkflowStreamWorkflow.run, + id=f"workflow-stream-flush-barrier-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + stream = WorkflowStreamClient.create( + client, handle.id, batch_interval=timedelta(seconds=60) + ) + + async with _async_timeout(5): + # 1. Empty-buffer flush is a no-op (must not block). + assert await stream.get_offset() == 0 + await stream.flush() + assert await stream.get_offset() == 0 + + # 2. Flush makes prior publishes visible without waiting on + # the 60s batch timer. + stream.topic("events", type=bytes).publish(b"a") + stream.topic("events", type=bytes).publish(b"b") + stream.topic("events", type=bytes).publish(b"c") + await stream.flush() + assert await stream.get_offset() == 3 + + # 3. Second flush with no new items is a no-op. + await stream.flush() + assert await stream.get_offset() == 3 + + await handle.signal(BasicWorkflowStreamWorkflow.close) + + +@pytest.mark.asyncio +async def test_concurrent_subscribers(client: Client) -> None: + """Two subscribers on different topics make interleaved progress. + + Publishes A-0, waits for subscriber A to observe it; publishes B-0, + waits for subscriber B to observe it. At this point both subscribers + have received exactly one item and are polling for their second, + so both subscriptions are provably in flight at the same time. + Then publishes A-1, B-1 the same way. A sequential execution (A drains + then B starts) cannot satisfy the ordering because B's first item + isn't published until after A has already received its first. + """ + async with new_worker( + client, + BasicWorkflowStreamWorkflow, + ) as worker: + handle = await client.start_workflow( + BasicWorkflowStreamWorkflow.run, + id=f"workflow-stream-concurrent-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + stream = WorkflowStreamClient(handle) + a_items: list[WorkflowStreamItem] = [] + b_items: list[WorkflowStreamItem] = [] + a_got = [asyncio.Event(), asyncio.Event()] + b_got = [asyncio.Event(), asyncio.Event()] + + async def collect( + topic: str, + collected: list[WorkflowStreamItem], + events: list[asyncio.Event], + ) -> None: + async for item in stream.subscribe( + topics=[topic], + from_offset=0, + poll_cooldown=timedelta(0), + result_type=bytes, + ): + collected.append(item) + events[len(collected) - 1].set() + if len(collected) >= len(events): + break + + a_task = asyncio.create_task(collect("a", a_items, a_got)) + b_task = asyncio.create_task(collect("b", b_items, b_got)) + + async def publish(topic: str, data: bytes) -> None: + await handle.signal( + "__temporal_workflow_stream_publish", + PublishInput(items=[PublishEntry(topic=topic, data=_wire_bytes(data))]), + ) + + try: + async with _async_timeout(10): + await publish("a", b"a-0") + await a_got[0].wait() + await publish("b", b"b-0") + await b_got[0].wait() + # Both subscribers are now mid-subscription, each having + # seen one item and polling for the next. + await publish("a", b"a-1") + await a_got[1].wait() + await publish("b", b"b-1") + await b_got[1].wait() + + await asyncio.gather(a_task, b_task) + finally: + a_task.cancel() + b_task.cancel() + + assert [i.data for i in a_items] == [b"a-0", b"a-1"] + assert [i.data for i in b_items] == [b"b-0", b"b-1"] + + await handle.signal(BasicWorkflowStreamWorkflow.close) + + +@pytest.mark.asyncio +async def test_max_batch_size(client: Client) -> None: + """max_batch_size triggers auto-flush without waiting for timer.""" + count = 7 # with max_batch_size=3: flushes at 3, 6, then remainder 1 on exit + async with new_worker( + client, + MaxBatchWorkflow, + activities=[publish_with_max_batch], + max_cached_workflows=0, + ) as worker: + handle = await client.start_workflow( + MaxBatchWorkflow.run, + count, + id=f"workflow-stream-maxbatch-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + # count items from activity + 1 "activity_done" from workflow + items = await collect_items(client, handle, None, 0, count + 1, timeout=15.0) + assert len(items) == count + 1 + for i in range(count): + assert items[i].data == f"item-{i}".encode() + + # max_batch_size actually engages: at least one flush fires during + # the publish loop, so 7 items ship as >=2 signals. Without this + # assertion the test would pass even if max_batch_size were ignored + # and all 7 items went out in a single exit-time flush (batch_count + # == 1). Note: max_batch_size is a *trigger* threshold, not a cap — + # the flusher may take more items from the buffer than max_batch_size + # if more were added while a prior signal was in flight, so the exact + # batch count depends on interleaving. Asserting >= 2 is the + # non-flaky way to verify the mechanism is live. + seqs = await handle.query(MaxBatchWorkflow.publisher_sequences) + assert len(seqs) == 1, f"expected one publisher, got {seqs}" + (batch_count,) = seqs.values() + assert batch_count >= 2, ( + f"expected >=2 batches with max_batch_size=3 and 7 items, got " + f"{batch_count} — max_batch_size did not trigger a mid-loop flush" + ) + + await handle.signal(MaxBatchWorkflow.close) + + +@pytest.mark.asyncio +async def test_replay_safety(client: Client) -> None: + """Workflow stream broker survives workflow replay (max_cached_workflows=0).""" + async with new_worker( + client, + InterleavedWorkflow, + activities=[publish_items], + max_cached_workflows=0, + ) as worker: + handle = await client.start_workflow( + InterleavedWorkflow.run, + 5, + id=f"workflow-stream-replay-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + # 1 (started) + 5 (activity) + 1 (done) = 7 + items = await collect_items(client, handle, None, 0, 7) + # Full ordered sequence — endpoint-only checks would miss mid-stream + # replay corruption (reordering, duplication, dropped items). + assert [i.data for i in items] == [ + b"started", + b"item-0", + b"item-1", + b"item-2", + b"item-3", + b"item-4", + b"done", + ] + assert [i.offset for i in items] == list(range(7)) + await handle.signal(InterleavedWorkflow.close) + + +@pytest.mark.asyncio +async def test_flush_retry_preserves_items_after_failures( + client: Client, +) -> None: + """After flush failures, a subsequent successful flush delivers all items + in publish order, exactly once. + + Exercises the retry code path behaviorally: simulated delivery failures + must not drop items, must not duplicate them on retry, and must not + reorder items published during the failed state. + """ + async with new_worker(client, BasicWorkflowStreamWorkflow) as worker: + handle = await client.start_workflow( + BasicWorkflowStreamWorkflow.run, + id=f"workflow-stream-flush-retry-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + stream = WorkflowStreamClient(handle) + real_signal = handle.signal + fail_remaining = 2 + + async def maybe_failing_signal(*args: Any, **kwargs: Any) -> Any: + nonlocal fail_remaining + if fail_remaining > 0: + fail_remaining -= 1 + raise RuntimeError("simulated delivery failure") + return await real_signal(*args, **kwargs) + + with patch.object(handle, "signal", side_effect=maybe_failing_signal): + stream.topic("events", type=bytes).publish(b"item-0") + stream.topic("events", type=bytes).publish(b"item-1") + with pytest.raises(RuntimeError): + await stream._flush() + + # Publish more during the failed state — must not overtake the + # pending retry on eventual delivery. + stream.topic("events", type=bytes).publish(b"item-2") + with pytest.raises(RuntimeError): + await stream._flush() + + # Third flush succeeds, delivering the pending retry batch. + await stream._flush() + # Fourth flush delivers the buffered "item-2". + await stream._flush() + + items = await collect_items(client, handle, None, 0, 3) + assert [i.data for i in items] == [b"item-0", b"item-1", b"item-2"] + + await handle.signal(BasicWorkflowStreamWorkflow.close) + + +@pytest.mark.asyncio +async def test_flush_raises_after_max_retry_duration(client: Client) -> None: + """When max_retry_duration is exceeded, flush raises TimeoutError and the + client can resume publishing without losing subsequent items.""" + async with new_worker(client, BasicWorkflowStreamWorkflow) as worker: + handle = await client.start_workflow( + BasicWorkflowStreamWorkflow.run, + id=f"workflow-stream-retry-expiry-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + # Inject a controllable clock into the client module. The client's + # retry check compares `time.monotonic() - _pending_since` against + # `max_retry_duration`, so advancing the clock between flushes makes + # the timeout fire deterministically regardless of wall-clock speed + # or clock resolution. + stream = WorkflowStreamClient( + handle, max_retry_duration=timedelta(milliseconds=100) + ) + real_signal = handle.signal + fail_signals = True + + async def maybe_failing_signal(*args: Any, **kwargs: Any) -> Any: + if fail_signals: + raise RuntimeError("simulated failure") + return await real_signal(*args, **kwargs) + + clock = [0.0] + with ( + patch( + "temporalio.contrib.workflow_streams._client.time.monotonic", + side_effect=lambda: clock[0], + ), + patch.object(handle, "signal", side_effect=maybe_failing_signal), + ): + stream.topic("events", type=bytes).publish(b"lost") + + # First flush fails and enters the pending-retry state. + with pytest.raises(RuntimeError): + await stream._flush() + + # Advance the clock well past max_retry_duration. + clock[0] = 10.0 + + # Next flush raises TimeoutError — the pending batch is abandoned. + with pytest.raises(TimeoutError, match="max_retry_duration"): + await stream._flush() + + # Stop failing signals; subsequent publishes must succeed. + fail_signals = False + stream.topic("events", type=bytes).publish(b"kept") + await stream._flush() + + items = await collect_items(client, handle, None, 0, 1) + assert len(items) == 1 + assert items[0].data == b"kept" + + await handle.signal(BasicWorkflowStreamWorkflow.close) + + +@pytest.mark.asyncio +async def test_dedup_rejects_duplicate_signal(client: Client) -> None: + """Workflow deduplicates signals with the same publisher_id + sequence.""" + async with new_worker( + client, + BasicWorkflowStreamWorkflow, + ) as worker: + handle = await client.start_workflow( + BasicWorkflowStreamWorkflow.run, + id=f"workflow-stream-dedup-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + # Send a batch with publisher_id and sequence + await handle.signal( + "__temporal_workflow_stream_publish", + PublishInput( + items=[PublishEntry(topic="events", data=_wire_bytes(b"item-0"))], + publisher_id="test-pub", + sequence=1, + ), + ) + + # Send the same sequence again — should be deduped + await handle.signal( + "__temporal_workflow_stream_publish", + PublishInput( + items=[PublishEntry(topic="events", data=_wire_bytes(b"duplicate"))], + publisher_id="test-pub", + sequence=1, + ), + ) + + # Send a new sequence — should go through + await handle.signal( + "__temporal_workflow_stream_publish", + PublishInput( + items=[PublishEntry(topic="events", data=_wire_bytes(b"item-1"))], + publisher_id="test-pub", + sequence=2, + ), + ) + + # Should have 2 items, not 3 (collect_items' update call acts as barrier) + items = await collect_items(client, handle, None, 0, 2) + assert len(items) == 2 + assert items[0].data == b"item-0" + assert items[1].data == b"item-1" + + # Verify offset is 2 (not 3) + stream_client = WorkflowStreamClient(handle) + offset = await stream_client.get_offset() + assert offset == 2 + + await handle.signal(BasicWorkflowStreamWorkflow.close) + + +@pytest.mark.asyncio +async def test_double_init_raises(client: Client) -> None: + """Instantiating WorkflowStream twice from @workflow.init raises RuntimeError. + + The first WorkflowStream() registers the __temporal_workflow_stream_publish signal handler; the + second call detects the existing handler and raises rather than + silently overwriting it. + """ + async with new_worker(client, DoubleInitWorkflow) as worker: + handle = await client.start_workflow( + DoubleInitWorkflow.run, + id=f"workflow-stream-double-init-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + err = await handle.query(DoubleInitWorkflow.get_double_init_error) + assert err is not None + assert "already registered" in err + await handle.signal(DoubleInitWorkflow.close) + + +@pytest.mark.asyncio +async def test_workflow_stream_outside_init_raises(client: Client) -> None: + """Constructing WorkflowStream outside @workflow.init raises RuntimeError. + + The workflow calls WorkflowStream() from @workflow.run; the caller-frame + guard must reject the call because the caller's function name is + ``run``, not ``__init__``. + """ + async with new_worker(client, LateWorkflowStreamWorkflow) as worker: + result = await client.execute_workflow( + LateWorkflowStreamWorkflow.run, + id=f"workflow-stream-late-init-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + assert "must be constructed directly from the workflow's" in result + assert "'run'" in result + + +@pytest.mark.asyncio +async def test_truncate_stream(client: Client) -> None: + """WorkflowStream.truncate discards prefix and adjusts base_offset.""" + async with new_worker( + client, + TruncateWorkflow, + ) as worker: + handle = await client.start_workflow( + TruncateWorkflow.run, + 5, + id=f"workflow-stream-truncate-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + # Verify all 5 items + items = await collect_items(client, handle, None, 0, 5) + assert len(items) == 5 + + # Truncate up to offset 3 (discard items 0, 1, 2). The update + # returns after the handler completes. + await handle.execute_update("truncate", 3) + + # Offset should still be 5 (truncation moves base_offset, not tail) + stream_client = WorkflowStreamClient(handle) + offset = await stream_client.get_offset() + assert offset == 5 + + # Reading from offset 3 should work (items 3, 4) + items_after = await collect_items(client, handle, None, 3, 2) + assert len(items_after) == 2 + assert items_after[0].data == b"item-3" + assert items_after[1].data == b"item-4" + + await handle.signal("close") + + +@pytest.mark.asyncio +async def test_ttl_pruning_in_get_stream_state(client: Client) -> None: + """WorkflowStream.get_state prunes publishers whose last-seen time exceeds the + TTL while retaining newer publishers. The log itself is unaffected. + + Uses a wall-clock gap between publishes so that workflow.time() + advances between the two publishers' tasks. workflow.time() can't be + cleanly injected from outside, so a short real sleep is the mechanism. + """ + async with new_worker( + client, + TTLTestWorkflow, + ) as worker: + handle = await client.start_workflow( + TTLTestWorkflow.run, + id=f"workflow-stream-ttl-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + # pub-old arrives first. + await handle.signal( + "__temporal_workflow_stream_publish", + PublishInput( + items=[PublishEntry(topic="events", data=_wire_bytes(b"old"))], + publisher_id="pub-old", + sequence=1, + ), + ) + + # Sanity: pub-old is recorded (generous TTL retains it). + state_before = await handle.query(TTLTestWorkflow.get_state_with_ttl, 9999.0) + assert "pub-old" in state_before.publishers + + # Let workflow.time() advance by real wall-clock time. Use a + # generous gap (1.0s) relative to the TTL (0.5s) so the test + # tolerates CI scheduling delays — pub-old must be >=0.5s past, + # pub-new must be <0.5s past, at the moment of the query. + await asyncio.sleep(1.0) + + # pub-new arrives after the gap. + await handle.signal( + "__temporal_workflow_stream_publish", + PublishInput( + items=[PublishEntry(topic="events", data=_wire_bytes(b"new"))], + publisher_id="pub-new", + sequence=1, + ), + ) + + # TTL=0.5s prunes pub-old (~1.0s old) but keeps pub-new (~0s). + state = await handle.query(TTLTestWorkflow.get_state_with_ttl, 0.5) + assert "pub-old" not in state.publishers + assert "pub-new" in state.publishers + # Log contents are not touched by publisher pruning. + assert len(state.log) == 2 + + await handle.signal("close") + + +# --------------------------------------------------------------------------- +# Truncate and TTL test workflows +# --------------------------------------------------------------------------- + + +@workflow.defn +class TruncateWorkflow: + """Test scaffolding that exposes WorkflowStream.truncate via a user-authored + update. + + The contrib module does not define a built-in external truncate API — + truncation is a workflow-internal decision (typically driven by + consumer progress or a retention policy). Workflows that want external + control wire up their own signal or update. We use an update here so + callers get explicit completion (signals are fire-and-forget). + + The ``truncate`` update is ``async`` and opens with + ``await asyncio.sleep(0)`` — the documented recipe from the + contrib/stream README for sync-shaped handlers that read ``WorkflowStream`` + state. The yield lets any buffered ``__temporal_workflow_stream_publish`` signal in + the same activation apply before the handler inspects ``self._log``. + This keeps the test workflow aligned with the pattern users are + directed to follow. + + ``prepub_count`` seeds the log with N byte-payload items during + ``@workflow.init`` as test convenience, so the error-path tests + have deterministic log content without an extra round trip to + publish from the client. + """ + + @workflow.init + def __init__(self, prepub_count: int = 0) -> None: + self.stream = WorkflowStream() + self._closed = False + for i in range(prepub_count): + self.stream.topic("events", type=bytes).publish(f"item-{i}".encode()) + + @workflow.signal + def close(self) -> None: + self._closed = True + + @workflow.update + async def truncate(self, up_to_offset: int) -> None: + # Recipe from README.md "Gotcha" section: yield once so any + # buffered __temporal_workflow_stream_publish in the same activation applies + # before we read self._log. asyncio.sleep(0) is a pure asyncio + # yield — no Temporal timer, no history event. + await asyncio.sleep(0) + self.stream.truncate(up_to_offset) + + @workflow.run + async def run(self, _prepub_count: int = 0) -> None: + # _prepub_count is consumed in @workflow.init above. @workflow.run + # must accept the same positional args, but the names are free + # to differ. + del _prepub_count + await workflow.wait_condition(lambda: self._closed) + + +@workflow.defn +class TruncateRaceWorkflow: + """Workflow that exposes ``publish`` and ``publish_then_truncate`` + updates so a test can deterministically interleave a waiting + ``__temporal_workflow_stream_poll`` update against a truncate that + advances ``base_offset`` past the poll's ``from_offset``. + + The ``publish_then_truncate`` handler runs publish loop and truncate + in a single workflow activation (no awaits between them), so a poll + parked at ``wait_condition`` sees the post-truncate state on its + next predicate evaluation rather than firing on an intermediate + publish. + """ + + @workflow.init + def __init__(self) -> None: + self.stream = WorkflowStream() + self._closed = False + + @workflow.signal + def close(self) -> None: + self._closed = True + + @workflow.update + async def publish(self, count: int) -> None: + await asyncio.sleep(0) + topic = self.stream.topic("events", type=bytes) + for i in range(count): + topic.publish(f"item-{i}".encode()) + + @workflow.update + async def publish_then_truncate(self, args: tuple[int, int]) -> None: + await asyncio.sleep(0) + publish_count, truncate_to = args + topic = self.stream.topic("events", type=bytes) + for i in range(publish_count): + topic.publish(f"prepub-{i}".encode()) + self.stream.truncate(truncate_to) + + @workflow.run + async def run(self) -> None: + await workflow.wait_condition(lambda: self._closed) + + +@workflow.defn +class TTLTestWorkflow: + """Workflow that exposes WorkflowStream.get_state via query for TTL testing.""" + + @workflow.init + def __init__(self) -> None: + self.stream = WorkflowStream() + self._closed = False + + @workflow.signal + def close(self) -> None: + self._closed = True + + @workflow.query + def get_state_with_ttl(self, ttl_seconds: float) -> WorkflowStreamState: + # Query arg is passed as float because the default JSON payload + # converter does not serialize ``timedelta``; convert here. + return self.stream.get_state(publisher_ttl=timedelta(seconds=ttl_seconds)) + + @workflow.run + async def run(self) -> None: + await workflow.wait_condition(lambda: self._closed) + + +# --------------------------------------------------------------------------- +# Continue-as-new workflow and test +# --------------------------------------------------------------------------- + + +@dataclass +class CANWorkflowInputTyped: + """Uses proper typing.""" + + stream_state: WorkflowStreamState | None = None + + +@workflow.defn +class ContinueAsNewTypedWorkflow: + """CAN workflow using properly-typed stream_state.""" + + @workflow.init + def __init__(self, input: CANWorkflowInputTyped) -> None: + self.stream = WorkflowStream(prior_state=input.stream_state) + self._should_continue = False + self._closed = False + + @workflow.signal + def close(self) -> None: + self._closed = True + + @workflow.signal + def trigger_continue(self) -> None: + self._should_continue = True + + @workflow.query + def publisher_sequences(self) -> dict[str, int]: + return {pid: ps.sequence for pid, ps in self.stream._publishers.items()} + + @workflow.run + async def run(self, _input: CANWorkflowInputTyped) -> None: + # _input is consumed in @workflow.init above. @workflow.run must + # accept the same positional args, but the names are free to differ. + del _input + while True: + await workflow.wait_condition(lambda: self._should_continue or self._closed) + if self._closed: + return + if self._should_continue: + self._should_continue = False + self.stream.detach_pollers() + await workflow.wait_condition(workflow.all_handlers_finished) + workflow.continue_as_new( + args=[ + CANWorkflowInputTyped( + stream_state=self.stream.get_state(), + ) + ] + ) + + +@pytest.mark.asyncio +async def test_continue_as_new_properly_typed(client: Client) -> None: + """CAN preserves the log, global offsets, AND publisher dedup state + when stream_state is properly typed as ``WorkflowStreamState | None``.""" + async with new_worker( + client, + ContinueAsNewTypedWorkflow, + ) as worker: + handle = await client.start_workflow( + ContinueAsNewTypedWorkflow.run, + CANWorkflowInputTyped(), + id=f"workflow-stream-can-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + # Publish 3 items with an explicit publisher_id/sequence so dedup + # state is seeded and we can verify it survives CAN. + await handle.signal( + "__temporal_workflow_stream_publish", + PublishInput( + items=[ + PublishEntry(topic="events", data=_wire_bytes(b"item-0")), + PublishEntry(topic="events", data=_wire_bytes(b"item-1")), + PublishEntry(topic="events", data=_wire_bytes(b"item-2")), + ], + publisher_id="pub", + sequence=1, + ), + ) + + items_before = await collect_items(client, handle, None, 0, 3) + assert len(items_before) == 3 + + await handle.signal(ContinueAsNewTypedWorkflow.trigger_continue) + + new_handle = client.get_workflow_handle(handle.id) + await assert_eq_eventually( + True, + lambda: _is_different_run(handle, new_handle), + ) + + # Log contents and offsets preserved across CAN. + items_after = await collect_items(client, new_handle, None, 0, 3) + assert [i.data for i in items_after] == [b"item-0", b"item-1", b"item-2"] + assert [i.offset for i in items_after] == [0, 1, 2] + + # Dedup state preserved: the carried publisher_sequences dict has + # pub -> 1 after CAN. + seqs_after_can = await new_handle.query( + ContinueAsNewTypedWorkflow.publisher_sequences + ) + assert seqs_after_can == {"pub": 1} + + # Re-sending publisher_id="pub", sequence=1 must be rejected by + # dedup — both the log and the publisher_sequences entry stay put. + await new_handle.signal( + "__temporal_workflow_stream_publish", + PublishInput( + items=[ + PublishEntry(topic="events", data=_wire_bytes(b"dup")), + ], + publisher_id="pub", + sequence=1, + ), + ) + seqs_after_dup = await new_handle.query( + ContinueAsNewTypedWorkflow.publisher_sequences + ) + assert seqs_after_dup == {"pub": 1} + + # A fresh sequence from the same publisher is accepted, advances + # publisher_sequences to 2, and the new item gets offset 3. + await new_handle.signal( + "__temporal_workflow_stream_publish", + PublishInput( + items=[ + PublishEntry(topic="events", data=_wire_bytes(b"item-3")), + ], + publisher_id="pub", + sequence=2, + ), + ) + seqs_after_accept = await new_handle.query( + ContinueAsNewTypedWorkflow.publisher_sequences + ) + assert seqs_after_accept == {"pub": 2} + items_all = await collect_items(client, new_handle, None, 0, 4) + assert [i.data for i in items_all] == [ + b"item-0", + b"item-1", + b"item-2", + b"item-3", + ] + assert items_all[3].offset == 3 + + await new_handle.signal(ContinueAsNewTypedWorkflow.close) + + +@workflow.defn +class ContinueAsNewHelperWorkflow: + """CAN workflow that uses the packaged ``WorkflowStream.continue_as_new`` helper.""" + + @workflow.init + def __init__(self, input: CANWorkflowInputTyped) -> None: + self.stream = WorkflowStream(prior_state=input.stream_state) + self._should_continue = False + self._closed = False + + @workflow.signal + def close(self) -> None: + self._closed = True + + @workflow.signal + def trigger_continue(self) -> None: + self._should_continue = True + + @workflow.run + async def run(self, _input: CANWorkflowInputTyped) -> None: + del _input + while True: + await workflow.wait_condition(lambda: self._should_continue or self._closed) + if self._closed: + return + if self._should_continue: + self._should_continue = False + await self.stream.continue_as_new( + lambda state: [CANWorkflowInputTyped(stream_state=state)], + ) + + +@pytest.mark.asyncio +async def test_continue_as_new_helper(client: Client) -> None: + """The ``WorkflowStream.continue_as_new`` helper preserves log and dedup state + just like the explicit detach_pollers/wait/CAN recipe.""" + async with new_worker( + client, + ContinueAsNewHelperWorkflow, + ) as worker: + handle = await client.start_workflow( + ContinueAsNewHelperWorkflow.run, + CANWorkflowInputTyped(), + id=f"workflow-stream-can-helper-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + await handle.signal( + "__temporal_workflow_stream_publish", + PublishInput( + items=[ + PublishEntry(topic="events", data=_wire_bytes(b"item-0")), + PublishEntry(topic="events", data=_wire_bytes(b"item-1")), + ], + publisher_id="pub", + sequence=1, + ), + ) + + items_before = await collect_items(client, handle, None, 0, 2) + assert [i.data for i in items_before] == [b"item-0", b"item-1"] + + await handle.signal(ContinueAsNewHelperWorkflow.trigger_continue) + + new_handle = client.get_workflow_handle(handle.id) + await assert_eq_eventually( + True, + lambda: _is_different_run(handle, new_handle), + ) + + items_after = await collect_items(client, new_handle, None, 0, 2) + assert [i.data for i in items_after] == [b"item-0", b"item-1"] + assert [i.offset for i in items_after] == [0, 1] + + await new_handle.signal(ContinueAsNewHelperWorkflow.close) + + +@pytest.mark.asyncio +async def test_follow_continue_as_new_describes_polled_run(client: Client) -> None: + """Regression test for continue-as-new detection. + + ``_follow_continue_as_new`` must describe the *specific run the poll was + admitted to* — a rolled-over run is closed with status CONTINUED_AS_NEW, + whereas the latest (successor) run reports RUNNING. The previous + implementation described the latest run, so the check never fired and a poll + failure during a rollover stopped the subscription instead of following it. + + Driving the exact poll-failure race deterministically is impractical (the + workflow drains in-flight polls before continuing-as-new), so this asserts + the helper's decision directly against a real post-rollover run. + """ + async with new_worker(client, ContinueAsNewHelperWorkflow) as worker: + handle = await client.start_workflow( + ContinueAsNewHelperWorkflow.run, + CANWorkflowInputTyped(), + id=f"workflow-stream-can-follow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.signal( + "__temporal_workflow_stream_publish", + PublishInput( + items=[PublishEntry(topic="events", data=_wire_bytes(b"item-0"))], + publisher_id="pub", + sequence=1, + ), + ) + old_run_id = handle.result_run_id + + await handle.signal(ContinueAsNewHelperWorkflow.trigger_continue) + new_handle = client.get_workflow_handle(handle.id) + await assert_eq_eventually(True, lambda: _is_different_run(handle, new_handle)) + + # The fix's premise: the polled (old) run reports CONTINUED_AS_NEW; the + # latest run reports RUNNING. + old_desc = await client.get_workflow_handle( + handle.id, run_id=old_run_id + ).describe() + assert old_desc.status == WorkflowExecutionStatus.CONTINUED_AS_NEW + latest_desc = await client.get_workflow_handle(handle.id).describe() + assert latest_desc.status == WorkflowExecutionStatus.RUNNING + + # The client follows the rollover when it describes the polled run, but + # the previous latest-run behavior (polled_run_id unset) would not. + following = WorkflowStreamClient.create(client, handle.id) + following._polled_run_id = old_run_id + assert await following._follow_continue_as_new() is True + + latest_only = WorkflowStreamClient.create(client, handle.id) + latest_only._polled_run_id = None # describes the latest run, as the bug did + assert await latest_only._follow_continue_as_new() is False + + await new_handle.signal(ContinueAsNewHelperWorkflow.close) + + +@pytest.mark.asyncio +async def test_subscribe_captures_polled_run_id(client: Client) -> None: + """The ``subscribe`` loop must record the run each poll was admitted to. + + ``_follow_continue_as_new`` relies on ``_polled_run_id`` to describe the + *polled* run rather than the latest one (see + ``test_follow_continue_as_new_describes_polled_run``). This verifies the + real subscribe flow populates that field, rather than setting it by hand. + """ + async with new_worker(client, BasicWorkflowStreamWorkflow) as worker: + handle = await client.start_workflow( + BasicWorkflowStreamWorkflow.run, + id=f"workflow-stream-polled-run-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.signal( + "__temporal_workflow_stream_publish", + PublishInput( + items=[PublishEntry(topic="events", data=_wire_bytes(b"item-0"))], + publisher_id="pub", + sequence=1, + ), + ) + + stream = WorkflowStreamClient.create(client, handle.id) + received: list[WorkflowStreamItem] = [] + + async def consume() -> None: + async for item in stream.subscribe( + from_offset=0, poll_cooldown=timedelta(0), result_type=bytes + ): + received.append(item) + + async def received_count() -> int: + return len(received) + + task = asyncio.create_task(consume()) + try: + await assert_eq_eventually(1, received_count) + # The poll was admitted to the run we started; subscribe must have + # captured exactly that run id, not left it unset. + assert stream._polled_run_id == handle.result_run_id + finally: + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + await handle.signal(BasicWorkflowStreamWorkflow.close) + + +@workflow.defn +class DrainingGateWorkflow: + """CAN workflow that detaches pollers and then *holds* in the draining state + until released, so a subscriber deterministically hits the draining poll + rejection before the rollover completes.""" + + @workflow.init + def __init__(self, input: CANWorkflowInputTyped) -> None: + self.stream = WorkflowStream(prior_state=input.stream_state) + self._should_continue = False + self._release = False + self._closed = False + + @workflow.signal + def close(self) -> None: + self._closed = True + + @workflow.signal + def trigger_continue(self) -> None: + self._should_continue = True + + @workflow.signal + def release(self) -> None: + self._release = True + + @workflow.run + async def run(self, _input: CANWorkflowInputTyped) -> None: + del _input + await workflow.wait_condition(lambda: self._should_continue or self._closed) + if self._closed: + return + # Detach but stay open until released, so new polls are rejected with + # StreamDraining for a deterministic window. + self.stream.detach_pollers() + await workflow.wait_condition(lambda: self._release) + await workflow.wait_condition(workflow.all_handlers_finished) + workflow.continue_as_new( + args=[CANWorkflowInputTyped(stream_state=self.stream.get_state())] + ) + + +@pytest.mark.asyncio +async def test_subscribe_retries_while_draining(client: Client) -> None: + """A poll rejected because the stream is draining for continue-as-new must + be retried, not surfaced as an error: the subscription stays alive through + the rollover and resumes on the successor run.""" + async with new_worker(client, DrainingGateWorkflow) as worker: + handle = await client.start_workflow( + DrainingGateWorkflow.run, + CANWorkflowInputTyped(), + id=f"workflow-stream-draining-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.signal( + "__temporal_workflow_stream_publish", + PublishInput( + items=[PublishEntry(topic="events", data=_wire_bytes(b"item-0"))], + publisher_id="pub", + sequence=1, + ), + ) + + stream = WorkflowStreamClient.create(client, handle.id) + received: list[WorkflowStreamItem] = [] + + async def consume() -> None: + async for item in stream.subscribe( + from_offset=0, poll_cooldown=timedelta(0), result_type=bytes + ): + received.append(item) + + async def received_count() -> int: + return len(received) + + task = asyncio.create_task(consume()) + new_handle = client.get_workflow_handle(handle.id) + try: + await assert_eq_eventually(1, received_count) + + # Detach; the subscriber's polls are now rejected with StreamDraining. + await handle.signal(DrainingGateWorkflow.trigger_continue) + # The subscription must keep retrying, not error out. + await asyncio.sleep(1.0) + assert not task.done(), "draining rejection must not end the subscription" + + # Release: the workflow continues-as-new; the subscription resumes on + # the successor run and receives an item published there. + await handle.signal(DrainingGateWorkflow.release) + await assert_eq_eventually( + True, lambda: _is_different_run(handle, new_handle) + ) + await new_handle.signal( + "__temporal_workflow_stream_publish", + PublishInput( + items=[PublishEntry(topic="events", data=_wire_bytes(b"item-1"))], + publisher_id="pub", + sequence=2, + ), + ) + + await assert_eq_eventually(2, received_count) + assert [i.data for i in received] == [b"item-0", b"item-1"] + assert [i.offset for i in received] == [0, 1] + finally: + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + await new_handle.signal(DrainingGateWorkflow.close) + + +# --------------------------------------------------------------------------- +# Cross-workflow workflow stream (Scenario 1) +# --------------------------------------------------------------------------- + + +@dataclass +class CrossWorkflowInput: + broker_workflow_id: str + expected_count: int + + +@workflow.defn +class BrokerWorkflow: + @workflow.init + def __init__(self, count: int) -> None: + self.stream = WorkflowStream() + self._closed = False + + @workflow.signal + def close(self) -> None: + self._closed = True + + @workflow.run + async def run(self, count: int) -> None: + for i in range(count): + self.stream.topic("events", type=bytes).publish(f"broker-{i}".encode()) + await workflow.wait_condition(lambda: self._closed) + + +@workflow.defn +class SubscriberWorkflow: + @workflow.run + async def run(self, input: CrossWorkflowInput) -> list[str]: + return await workflow.execute_activity( + "subscribe_to_broker", + input, + start_to_close_timeout=timedelta(seconds=30), + heartbeat_timeout=timedelta(seconds=10), + ) + + +@activity.defn(name="subscribe_to_broker") +async def subscribe_to_broker(input: CrossWorkflowInput) -> list[str]: + client = WorkflowStreamClient.create( + client=activity.client(), + workflow_id=input.broker_workflow_id, + ) + items: list[str] = [] + async with _async_timeout(15.0): + async for item in client.subscribe( + topics=["events"], + from_offset=0, + poll_cooldown=timedelta(0), + result_type=bytes, + ): + items.append(item.data.decode()) + activity.heartbeat() + if len(items) >= input.expected_count: + break + return items + + +@pytest.mark.asyncio +async def test_cross_workflow_stream(client: Client) -> None: + """Workflow B's activity subscribes to events published by Workflow A.""" + count = 5 + task_queue = str(uuid.uuid4()) + + async with new_worker( + client, + BrokerWorkflow, + SubscriberWorkflow, + activities=[subscribe_to_broker], + task_queue=task_queue, + ): + broker_id = f"workflow-stream-broker-{uuid.uuid4()}" + broker_handle = await client.start_workflow( + BrokerWorkflow.run, + count, + id=broker_id, + task_queue=task_queue, + ) + + sub_handle = await client.start_workflow( + SubscriberWorkflow.run, + CrossWorkflowInput( + broker_workflow_id=broker_id, + expected_count=count, + ), + id=f"workflow-stream-subscriber-{uuid.uuid4()}", + task_queue=task_queue, + ) + + result = await sub_handle.result() + assert result == [f"broker-{i}" for i in range(count)] + + # Also verify external subscription still works + external_items = await collect_items( + client, broker_handle, ["events"], 0, count + ) + assert len(external_items) == count + + await broker_handle.signal(BrokerWorkflow.close) + + +# --------------------------------------------------------------------------- +# Standalone activity (started directly via Client, no parent workflow) +# --------------------------------------------------------------------------- + + +@dataclass +class StandalonePublishInput: + broker_workflow_id: str + count: int + + +@activity.defn(name="standalone_publish_to_broker") +async def standalone_publish_to_broker(input: StandalonePublishInput) -> None: + """Publish to a broker workflow from a standalone activity. + + Same usage as in any external program: build a Client (here taken + via ``activity.client()``), pass an explicit workflow id to + ``WorkflowStreamClient.create``. ``from_within_activity`` is not usable + here because the activity has no parent workflow. + """ + assert activity.info().workflow_id is None, ( + "test bug: this activity should be standalone" + ) + client = WorkflowStreamClient.create( + client=activity.client(), + workflow_id=input.broker_workflow_id, + batch_interval=timedelta(milliseconds=500), + ) + async with client: + for i in range(input.count): + activity.heartbeat() + client.topic("events", type=bytes).publish(f"standalone-{i}".encode()) + + +@activity.defn(name="standalone_subscribe_to_broker") +async def standalone_subscribe_to_broker(input: CrossWorkflowInput) -> list[str]: + assert activity.info().workflow_id is None, ( + "test bug: this activity should be standalone" + ) + client = WorkflowStreamClient.create( + client=activity.client(), + workflow_id=input.broker_workflow_id, + ) + items: list[str] = [] + async with _async_timeout(15.0): + async for item in client.subscribe( + topics=["events"], + from_offset=0, + poll_cooldown=timedelta(0), + result_type=bytes, + ): + items.append(item.data.decode()) + activity.heartbeat() + if len(items) >= input.expected_count: + break + return items + + +@activity.defn(name="standalone_from_within_activity_misuse") +async def standalone_from_within_activity_misuse() -> str: + """Calling from_within_activity in a standalone activity must raise a clear error.""" + try: + WorkflowStreamClient.from_within_activity() + except RuntimeError as e: + return str(e) + return "" + + +@pytest.mark.asyncio +async def test_standalone_activity_publish( + client: Client, env: WorkflowEnvironment +) -> None: + """Activity started directly via Client.start_activity publishes via create().""" + if env.supports_time_skipping: + pytest.skip( + "Java test server does not support Client.start_activity: " + "https://github.com/temporalio/sdk-java/issues/2741" + ) + count = 5 + task_queue = str(uuid.uuid4()) + + async with new_worker( + client, + BasicWorkflowStreamWorkflow, + activities=[standalone_publish_to_broker], + task_queue=task_queue, + ): + broker_id = f"workflow-stream-standalone-broker-{uuid.uuid4()}" + broker_handle = await client.start_workflow( + BasicWorkflowStreamWorkflow.run, + id=broker_id, + task_queue=task_queue, + ) + + activity_handle = await client.start_activity( + standalone_publish_to_broker, + StandalonePublishInput(broker_workflow_id=broker_id, count=count), + id=f"standalone-publish-{uuid.uuid4()}", + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=30), + heartbeat_timeout=timedelta(seconds=10), + ) + await activity_handle.result() + + items = await collect_items(client, broker_handle, ["events"], 0, count) + assert [i.data for i in items] == [ + f"standalone-{i}".encode() for i in range(count) + ] + + await broker_handle.signal(BasicWorkflowStreamWorkflow.close) + + +@pytest.mark.asyncio +async def test_standalone_activity_subscribe( + client: Client, env: WorkflowEnvironment +) -> None: + """Standalone activity subscribes to a broker workflow via create().""" + if env.supports_time_skipping: + pytest.skip( + "Java test server does not support Client.start_activity: " + "https://github.com/temporalio/sdk-java/issues/2741" + ) + count = 5 + task_queue = str(uuid.uuid4()) + + async with new_worker( + client, + BrokerWorkflow, + activities=[standalone_subscribe_to_broker], + task_queue=task_queue, + ): + broker_id = f"workflow-stream-standalone-sub-broker-{uuid.uuid4()}" + broker_handle = await client.start_workflow( + BrokerWorkflow.run, + count, + id=broker_id, + task_queue=task_queue, + ) + + activity_handle = await client.start_activity( + standalone_subscribe_to_broker, + CrossWorkflowInput( + broker_workflow_id=broker_id, + expected_count=count, + ), + id=f"standalone-subscribe-{uuid.uuid4()}", + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=30), + heartbeat_timeout=timedelta(seconds=10), + ) + result = await activity_handle.result() + assert result == [f"broker-{i}" for i in range(count)] + + await broker_handle.signal(BrokerWorkflow.close) + + +@pytest.mark.asyncio +async def test_from_within_activity_in_standalone_activity_raises( + client: Client, env: WorkflowEnvironment +) -> None: + """from_within_activity() raises a clear error pointing at create() when used in a + standalone activity (one without a parent workflow).""" + if env.supports_time_skipping: + pytest.skip( + "Java test server does not support Client.start_activity: " + "https://github.com/temporalio/sdk-java/issues/2741" + ) + task_queue = str(uuid.uuid4()) + + async with new_worker( + client, + activities=[standalone_from_within_activity_misuse], + task_queue=task_queue, + ): + activity_handle = await client.start_activity( + standalone_from_within_activity_misuse, + id=f"standalone-misuse-{uuid.uuid4()}", + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=10), + ) + msg = await activity_handle.result() + assert "no parent workflow" in msg + assert "WorkflowStreamClient.create" in msg + + +# --------------------------------------------------------------------------- +# Cross-namespace workflow stream via Nexus (Scenario 2) +# --------------------------------------------------------------------------- + + +@dataclass +class StartBrokerInput: + count: int + broker_id: str + + +@dataclass +class NexusCallerInput: + count: int + broker_id: str + endpoint: str + + +@workflow.defn +class NexusBrokerWorkflow: + @workflow.init + def __init__(self, count: int) -> None: + self.stream = WorkflowStream() + self._closed = False + + @workflow.signal + def close(self) -> None: + self._closed = True + + @workflow.run + async def run(self, count: int) -> str: + for i in range(count): + self.stream.topic("events", type=bytes).publish(f"nexus-{i}".encode()) + await workflow.wait_condition(lambda: self._closed) + return "done" + + +@nexusrpc.service +class WorkflowStreamNexusService: + start_broker: nexusrpc.Operation[StartBrokerInput, str] + + +@nexusrpc.handler.service_handler(service=WorkflowStreamNexusService) +class WorkflowStreamNexusHandler: + @workflow_run_operation + async def start_broker( + self, ctx: WorkflowRunOperationContext, input: StartBrokerInput + ) -> nexus.WorkflowHandle[str]: + return await ctx.start_workflow( + NexusBrokerWorkflow.run, + input.count, + id=input.broker_id, + ) + + +@workflow.defn +class NexusCallerWorkflow: + @workflow.run + async def run(self, input: NexusCallerInput) -> str: + nc = workflow.create_nexus_client( + service=WorkflowStreamNexusService, + endpoint=input.endpoint, + ) + return await nc.execute_operation( + WorkflowStreamNexusService.start_broker, + StartBrokerInput(count=input.count, broker_id=input.broker_id), + ) + + +async def create_cross_namespace_endpoint( + client: Client, + endpoint_name: str, + target_namespace: str, + task_queue: str, +) -> None: + await client.operator_service.create_nexus_endpoint( + temporalio.api.operatorservice.v1.CreateNexusEndpointRequest( + spec=temporalio.api.nexus.v1.EndpointSpec( + name=endpoint_name, + target=temporalio.api.nexus.v1.EndpointTarget( + worker=temporalio.api.nexus.v1.EndpointTarget.Worker( + namespace=target_namespace, + task_queue=task_queue, + ) + ), + ) + ) + ) + + +@pytest.mark.asyncio +async def test_poll_more_ready_when_response_exceeds_size_limit( + client: Client, +) -> None: + """Poll response sets more_ready=True when items exceed ~1MB wire size.""" + async with new_worker( + client, + BasicWorkflowStreamWorkflow, + ) as worker: + handle = await client.start_workflow( + BasicWorkflowStreamWorkflow.run, + id=f"workflow-stream-more-ready-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + # Publish items that total well over 1MB in the poll response. + # Send in separate signals to stay under the RPC size limit. + # Each item is ~200KB; 8 items = ~1.6MB wire (base64 inflates ~33%). + chunk = b"x" * 200_000 + for _ in range(8): + await handle.signal( + "__temporal_workflow_stream_publish", + PublishInput( + items=[PublishEntry(topic="big", data=_wire_bytes(chunk))] + ), + ) + + # First poll from offset 0 — should get some items but not all. + # (The update acts as a barrier for all prior publish signals.) + result1: PollResult = await handle.execute_update( + "__temporal_workflow_stream_poll", + PollInput(topics=[], from_offset=0), + result_type=PollResult, + ) + assert result1.more_ready is True + assert len(result1.items) < 8 + assert result1.next_offset < 8 + + # Continue polling until we have all items + all_items = list(result1.items) + offset = result1.next_offset + last_result: PollResult = result1 + while len(all_items) < 8: + last_result = await handle.execute_update( + "__temporal_workflow_stream_poll", + PollInput(topics=[], from_offset=offset), + result_type=PollResult, + ) + all_items.extend(last_result.items) + offset = last_result.next_offset + assert len(all_items) == 8 + # The final poll that drained the log should set more_ready=False + assert last_result.more_ready is False + + await handle.signal(BasicWorkflowStreamWorkflow.close) + + +@pytest.mark.asyncio +async def test_subscribe_iterates_through_more_ready(client: Client) -> None: + """Subscriber correctly yields all items when polls are size-truncated.""" + async with new_worker( + client, + BasicWorkflowStreamWorkflow, + ) as worker: + handle = await client.start_workflow( + BasicWorkflowStreamWorkflow.run, + id=f"workflow-stream-more-ready-iter-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + # Publish 8 x 200KB items (~2MB+ wire, exceeds 1MB cap) + chunk = b"x" * 200_000 + for _ in range(8): + await handle.signal( + "__temporal_workflow_stream_publish", + PublishInput( + items=[PublishEntry(topic="big", data=_wire_bytes(chunk))] + ), + ) + + # subscribe() should seamlessly iterate through all 8 items + items = await collect_items(client, handle, None, 0, 8, timeout=10.0) + assert len(items) == 8 + for item in items: + assert item.data == chunk + + await handle.signal(BasicWorkflowStreamWorkflow.close) + + +@pytest.mark.asyncio +@pytest.mark.requires_local_server +async def test_cross_namespace_nexus_stream( + client: Client, env: WorkflowEnvironment +) -> None: + """Nexus operation starts a workflow stream broker in another namespace; test subscribes.""" + if env.supports_time_skipping: + pytest.skip("Nexus not supported with time-skipping server") + + count = 5 + handler_ns = f"handler-ns-{uuid.uuid4().hex[:8]}" + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + broker_id = f"nexus-broker-{uuid.uuid4()}" + + # Register the handler namespace with the dev server + await client.workflow_service.register_namespace( + temporalio.api.workflowservice.v1.RegisterNamespaceRequest( + namespace=handler_ns, + workflow_execution_retention_period=google.protobuf.duration_pb2.Duration( + seconds=86400, + ), + ) + ) + + handler_client = await env.connect_client( + namespace=handler_ns, + ) + + # Create endpoint targeting the handler namespace + await create_cross_namespace_endpoint( + client, + endpoint_name, + target_namespace=handler_ns, + task_queue=task_queue, + ) + + # Handler worker in handler namespace + async with Worker( + handler_client, + task_queue=task_queue, + workflows=[NexusBrokerWorkflow], + nexus_service_handlers=[WorkflowStreamNexusHandler()], + ): + # Caller worker in default namespace + caller_tq = str(uuid.uuid4()) + async with new_worker( + client, + NexusCallerWorkflow, + task_queue=caller_tq, + ): + # Start caller — invokes Nexus op which starts broker in handler ns + caller_handle = await client.start_workflow( + NexusCallerWorkflow.run, + NexusCallerInput( + count=count, + broker_id=broker_id, + endpoint=endpoint_name, + ), + id=f"nexus-caller-{uuid.uuid4()}", + task_queue=caller_tq, + ) + + # Wait for the broker workflow to be started by the Nexus operation + broker_handle = handler_client.get_workflow_handle(broker_id) + + async def broker_started() -> bool: + try: + await broker_handle.describe() + return True + except Exception: + return False + + await assert_eq_eventually( + True, broker_started, timeout=timedelta(seconds=15) + ) + + # Subscribe to broker events from the handler namespace + items = await collect_items( + handler_client, broker_handle, ["events"], 0, count + ) + assert len(items) == count + for i in range(count): + assert items[i].topic == "events" + assert items[i].data == f"nexus-{i}".encode() + + # Clean up — signal broker to close so caller can complete + await broker_handle.signal("close") + result = await caller_handle.result() + assert result == "done" diff --git a/tests/helpers/__init__.py b/tests/helpers/__init__.py index 79d3687fd..fe37296e9 100644 --- a/tests/helpers/__init__.py +++ b/tests/helpers/__init__.py @@ -1,11 +1,20 @@ import asyncio +import logging +import logging.handlers +import queue import socket +import threading import time import uuid -from contextlib import closing +from collections.abc import Awaitable, Callable, Iterator, Sequence +from contextlib import closing, contextmanager from dataclasses import dataclass from datetime import datetime, timedelta, timezone -from typing import Any, Awaitable, Callable, Optional, Sequence, Type, TypeVar, Union +from typing import ( + Any, + TypeVar, + cast, +) from temporalio.api.common.v1 import WorkflowExecution from temporalio.api.enums.v1 import EventType as EventType @@ -35,13 +44,13 @@ def new_worker( client: Client, - *workflows: Type, + *workflows: type, activities: Sequence[Callable] = [], - task_queue: Optional[str] = None, + task_queue: str | None = None, workflow_runner: WorkflowRunner = SandboxedWorkflowRunner(), max_cached_workflows: int = 1000, - workflow_failure_exception_types: Sequence[Type[BaseException]] = [], - **kwargs, + workflow_failure_exception_types: Sequence[type[BaseException]] = [], + **kwargs, # type:ignore[reportMissingParameterType] ) -> Worker: return Worker( client, @@ -63,6 +72,7 @@ async def assert_eventually( *, timeout: timedelta = timedelta(seconds=10), interval: timedelta = timedelta(milliseconds=200), + retry_on_rpc_cancelled: bool = True, ) -> T: start_sec = time.monotonic() while True: @@ -72,6 +82,11 @@ async def assert_eventually( except AssertionError: if timedelta(seconds=time.monotonic() - start_sec) >= timeout: raise + except RPCError as e: + if retry_on_rpc_cancelled and e.status == RPCStatusCode.CANCELLED: + continue + else: + raise await asyncio.sleep(interval.total_seconds()) @@ -89,7 +104,7 @@ async def check() -> None: async def assert_task_fail_eventually( - handle: WorkflowHandle, *, message_contains: Optional[str] = None + handle: WorkflowHandle, *, message_contains: str | None = None ) -> None: async def check() -> None: async for evt in handle.fetch_history_events(): @@ -125,7 +140,7 @@ async def ensure_search_attributes_present( resp = await client.operator_service.list_search_attributes( ListSearchAttributesRequest(namespace=client.namespace) ) - if not set(key.name for key in keys).issubset(resp.custom_attributes.keys()): + if not {key.name for key in keys}.issubset(resp.custom_attributes.keys()): await client.operator_service.add_search_attributes( AddSearchAttributesRequest( namespace=client.namespace, @@ -139,7 +154,7 @@ async def ensure_search_attributes_present( resp = await client.operator_service.list_search_attributes( ListSearchAttributesRequest(namespace=client.namespace) ) - assert set(key.name for key in keys).issubset(resp.custom_attributes.keys()) + assert {key.name for key in keys}.issubset(resp.custom_attributes.keys()) def find_free_port() -> int: @@ -175,7 +190,7 @@ async def admitted_update_task( handle: WorkflowHandle, update_method: UpdateMethodMultiParam, id: str, - **kwargs, + **kwargs, # type:ignore[reportMissingParameterType] ) -> asyncio.Task: """ Return an asyncio.Task for an update after waiting for it to be admitted. @@ -239,10 +254,48 @@ async def check() -> PendingActivityInfo: return await assert_eventually(check, timeout=timeout) +async def assert_event_subsequence( + wf_handle: WorkflowHandle, + expected_events: list[EventType.ValueType], + timeout: timedelta = timedelta(seconds=5), +) -> None: + """ + Given a workflow handle and a sequence of event types, assert that the workflow's history + contains that subsequence of events in the order specified. + """ + + async def check(): + history = await wf_handle.fetch_history() + + _all_events = iter(history.events) + _expected_events = iter(expected_events) + + previous_expected_event_type_name = None + for expected_event_type in _expected_events: + expected_event_type_name = EventType.Name(expected_event_type).removeprefix( + "EVENT_TYPE_" + ) + has_expected = next( + (e for e in _all_events if e.event_type == expected_event_type), + None, + ) + if not has_expected: + if previous_expected_event_type_name is not None: + prefix = f"After {previous_expected_event_type_name}, " + else: + prefix = "" + raise AssertionError( + f"{prefix}expected {expected_event_type_name} in workflow {wf_handle.id}" + ) + previous_expected_event_type_name = expected_event_type_name + + await assert_eventually(check, timeout=timeout) + + async def get_pending_activity_info( handle: WorkflowHandle, activity_id: str, -) -> Optional[PendingActivityInfo]: +) -> PendingActivityInfo | None: """Get pending activity info by ID, or None if not found.""" desc = await handle.describe() for act in desc.raw_description.pending_activities: @@ -251,8 +304,28 @@ async def get_pending_activity_info( return None +_wait_for_pause_events: dict[str, threading.Event] = {} + + +def wait_for_pause_event(activity_id: str) -> None: + event = _wait_for_pause_events.get(activity_id) + if event is not None: + event.wait() + + +async def async_wait_for_pause_event(activity_id: str) -> None: + event = _wait_for_pause_events.get(activity_id) + if event is not None: + await asyncio.get_running_loop().run_in_executor(None, event.wait) + + async def pause_and_assert(client: Client, handle: WorkflowHandle, activity_id: str): - """Pause the given activity and assert it becomes paused.""" + """Pause the given activity and assert it becomes paused. + + Registers an event before calling the pause API so cooperating test + activities (those that catch the pause-induced cancel via + wait_for_pause_release) hang until we have observed paused=true. + """ desc = await handle.describe() req = PauseActivityRequest( namespace=client.namespace, @@ -262,14 +335,19 @@ async def pause_and_assert(client: Client, handle: WorkflowHandle, activity_id: ), id=activity_id, ) - await client.workflow_service.pause_activity(req) - # Assert eventually paused - async def check_paused() -> bool: - info = await assert_pending_activity_exists_eventually(handle, activity_id) - return info.paused + _wait_for_pause_events[activity_id] = threading.Event() + try: + await client.workflow_service.pause_activity(req) + + async def check_paused() -> None: + info = await assert_pending_activity_exists_eventually(handle, activity_id) + assert info.paused, f"Activity {activity_id} not yet paused" - await assert_eventually(check_paused) + await assert_eventually(check_paused) + finally: + _wait_for_pause_events[activity_id].set() + del _wait_for_pause_events[activity_id] async def unpause_and_assert(client: Client, handle: WorkflowHandle, activity_id: str): @@ -286,9 +364,9 @@ async def unpause_and_assert(client: Client, handle: WorkflowHandle, activity_id await client.workflow_service.unpause_activity(req) # Assert eventually not paused - async def check_unpaused() -> bool: + async def check_unpaused() -> None: info = await assert_pending_activity_exists_eventually(handle, activity_id) - return not info.paused + assert not info.paused, f"Activity {activity_id} still paused" await assert_eventually(check_unpaused) @@ -304,14 +382,14 @@ async def print_history(handle: WorkflowHandle): @dataclass class InterleavedHistoryEvent: handle: WorkflowHandle - event: Union[HistoryEvent, str] - number: Optional[int] + event: HistoryEvent | str + number: int | None time: datetime async def print_interleaved_histories( handles: list[WorkflowHandle], - extra_events: Optional[list[tuple[WorkflowHandle, str, datetime]]] = None, + extra_events: list[tuple[WorkflowHandle, str, datetime]] | None = None, ) -> None: """ Print the interleaved history events from multiple workflow handles in columns. @@ -401,3 +479,56 @@ def _format_row(items: list[str], truncate: bool = False) -> str: padding = len(f" *: {elapsed_ms:>4} ") summary_row[col_idx] = f"{' ' * padding}[{summary}]"[: col_width - 3] print(_format_row(summary_row)) + + +class LogCapturer: + def __init__(self) -> None: + self.log_queue: queue.Queue[logging.LogRecord] = queue.Queue() + + @contextmanager + def logs_captured(self, *loggers: logging.Logger, level: int = logging.INFO): + handler = logging.handlers.QueueHandler(self.log_queue) + + prev_levels = [l.level for l in loggers] + for l in loggers: + l.setLevel(level) + l.addHandler(handler) + try: + yield self + finally: + for i, l in enumerate(loggers): + l.removeHandler(handler) + l.setLevel(prev_levels[i]) + + def find_log(self, starts_with: str) -> logging.LogRecord | None: + return self.find(lambda l: l.message.startswith(starts_with)) + + def find( + self, pred: Callable[[logging.LogRecord], bool] + ) -> logging.LogRecord | None: + for record in cast(list[logging.LogRecord], self.log_queue.queue): + if pred(record): + return record + return None + + def find_all( + self, pred: Callable[[logging.LogRecord], bool] + ) -> list[logging.LogRecord]: + return [ + record + for record in cast(list[logging.LogRecord], self.log_queue.queue) + if pred(record) + ] + + +class LogHandler: + @staticmethod + @contextmanager + def apply(logger: logging.Logger, handler: logging.Handler) -> Iterator[None]: + level = logger.level + logger.addHandler(handler) + try: + yield + finally: + logger.removeHandler(handler) + logger.level = level diff --git a/tests/helpers/cache_eviction.py b/tests/helpers/cache_eviction.py new file mode 100644 index 000000000..191d51078 --- /dev/null +++ b/tests/helpers/cache_eviction.py @@ -0,0 +1,68 @@ +import asyncio +from datetime import timedelta + +from temporalio import activity, workflow + + +@activity.defn +async def wait_forever_activity() -> None: + await asyncio.Future() + + +@workflow.defn +class WaitForeverWorkflow: + @workflow.run + async def run(self) -> None: + await asyncio.Future() + + +@workflow.defn +class CacheEvictionTearDownWorkflow: + def __init__(self) -> None: + self._signal_count = 0 + + @workflow.run + async def run(self) -> None: + # Start several things in background. This is just to show that eviction + # can work even with these things running. + tasks = [ + asyncio.create_task( + workflow.execute_activity( + wait_forever_activity, start_to_close_timeout=timedelta(hours=1) + ) + ), + asyncio.create_task( + workflow.execute_child_workflow(WaitForeverWorkflow.run) + ), + asyncio.create_task(asyncio.sleep(1000)), + asyncio.shield( + workflow.execute_activity( + wait_forever_activity, start_to_close_timeout=timedelta(hours=1) + ) + ), + asyncio.create_task(workflow.wait_condition(lambda: False)), + ] + gather_fut = asyncio.gather(*tasks, return_exceptions=True) + # Let's also start something in the background that we never wait on + asyncio.create_task(asyncio.sleep(1000)) + try: + # Wait for signal count to reach 2 + await asyncio.sleep(0.01) + await workflow.wait_condition(lambda: self._signal_count > 1) + finally: + # This finally, on eviction, is actually called but the command + # should be ignored + await asyncio.sleep(0.01) + await workflow.wait_condition(lambda: self._signal_count > 2) + # Cancel gather tasks and wait on them, but ignore the errors + for task in tasks: + task.cancel() + await gather_fut + + @workflow.signal + async def signal(self) -> None: + self._signal_count += 1 + + @workflow.query + def signal_count(self) -> int: + return self._signal_count diff --git a/tests/helpers/external_coroutine.py b/tests/helpers/external_coroutine.py index 511db1ae3..9af6e85ec 100644 --- a/tests/helpers/external_coroutine.py +++ b/tests/helpers/external_coroutine.py @@ -2,17 +2,15 @@ File used in conjunction with external_stack_trace.py to test filenames in multi-file workflows. """ -from typing import List - from temporalio import workflow -async def never_completing_coroutine(status: List[str]) -> None: +async def never_completing_coroutine(status: list[str]) -> None: status[0] = "waiting" # external coroutine test await workflow.wait_condition(lambda: False) -async def wait_on_timer(status: List[str]) -> None: +async def wait_on_timer(status: list[str]) -> None: status[0] = "waiting" # multifile test print("Coroutine executed, waiting.") await workflow.wait_condition(lambda: False) diff --git a/tests/helpers/fork.py b/tests/helpers/fork.py new file mode 100644 index 000000000..d465a4808 --- /dev/null +++ b/tests/helpers/fork.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +import asyncio +import multiprocessing +import multiprocessing.context +from dataclasses import dataclass +from typing import Any + +import pytest + + +@dataclass +class _ForkTestResult: + status: str + err_name: str | None + err_msg: str | None + + def __eq__(self, value: object) -> bool: + if not isinstance(value, _ForkTestResult): + return False + + valid_err_msg = False + + if self.err_msg and value.err_msg: + valid_err_msg = ( + self.err_msg in value.err_msg or value.err_msg in self.err_msg + ) + + return ( + value.status == self.status + and value.err_name == value.err_name + and valid_err_msg + ) + + @staticmethod + def assertion_error(message: str) -> _ForkTestResult: + return _ForkTestResult( + status="error", err_name="AssertionError", err_msg=message + ) + + +class _TestFork: # type:ignore[reportUnusedClass] + _expected: _ForkTestResult # type:ignore[reportUninitializedInstanceVariable] + + async def coro(self) -> Any: + raise NotImplementedError() + + def entry(self): + event_loop = asyncio.new_event_loop() + asyncio.set_event_loop(event_loop) + try: + event_loop.run_until_complete(self.coro()) + payload = _ForkTestResult(status="ok", err_name=None, err_msg=None) + except BaseException as err: + payload = _ForkTestResult( + status="error", err_name=err.__class__.__name__, err_msg=str(err) + ) + + self._child_conn.send(payload) + self._child_conn.close() + + def run(self, mp_fork_context: multiprocessing.context.BaseContext | None): + process_factory = getattr(mp_fork_context, "Process", None) + + if not mp_fork_context or not process_factory: + pytest.skip("fork context not available") + + self._parent_conn, self._child_conn = mp_fork_context.Pipe(duplex=False) # type:ignore[reportUninitializedInstanceVariable] + # start fork + child_process = process_factory(target=self.entry, args=(), daemon=False) + child_process.start() + # close parent's handle on child_conn + self._child_conn.close() + + # get run info from pipe + payload = self._parent_conn.recv() + self._parent_conn.close() + + assert payload == self._expected diff --git a/tests/helpers/metrics.py b/tests/helpers/metrics.py new file mode 100644 index 000000000..d5869d46b --- /dev/null +++ b/tests/helpers/metrics.py @@ -0,0 +1,30 @@ +from collections.abc import Mapping + + +class PromMetricMatcher: + def __init__(self, prom_lines: list[str]) -> None: + self._prom_lines = prom_lines + + # Intentionally naive metric checker + def matches_metric_line( + self, line: str, name: str, at_least_labels: Mapping[str, str], value: int + ) -> bool: + # Must have metric name + if not line.startswith(name + "{"): + return False + # Must have labels (don't escape for this test) + for k, v in at_least_labels.items(): + if f'{k}="{v}"' not in line: + return False + return line.endswith(f" {value}") + + def assert_metric_exists( + self, name: str, at_least_labels: Mapping[str, str], value: int + ) -> None: + assert any( + self.matches_metric_line(line, name, at_least_labels, value) + for line in self._prom_lines + ) + + def assert_description_exists(self, name: str, description: str) -> None: + assert f"# HELP {name} {description}" in self._prom_lines diff --git a/tests/helpers/nexus.py b/tests/helpers/nexus.py index 6e8c62e59..0af468b44 100644 --- a/tests/helpers/nexus.py +++ b/tests/helpers/nexus.py @@ -1,19 +1,9 @@ -import dataclasses -from dataclasses import dataclass -from typing import Any, Mapping, Optional -from urllib.parse import urlparse +from collections.abc import Sequence -import temporalio.api.failure.v1 -import temporalio.api.nexus.v1 -import temporalio.api.operatorservice.v1 -import temporalio.workflow -from temporalio.client import Client -from temporalio.converter import FailureConverter, PayloadConverter -from temporalio.testing import WorkflowEnvironment - -with temporalio.workflow.unsafe.imports_passed_through(): - import httpx - from google.protobuf import json_format +import temporalio.api.common.v1 +import temporalio.api.enums.v1 +import temporalio.api.history.v1 +from temporalio.client import WorkflowHistory def make_nexus_endpoint_name(task_queue: str) -> str: @@ -21,110 +11,92 @@ def make_nexus_endpoint_name(task_queue: str) -> str: return f"nexus-endpoint-{task_queue}" -# TODO(nexus-preview): How do we recommend that users create endpoints in their own tests? -# See https://github.com/temporalio/sdk-typescript/pull/1708/files?show-viewed-files=true&file-filters%5B%5D=&w=0#r2082549085 -async def create_nexus_endpoint( - task_queue: str, client: Client -) -> temporalio.api.operatorservice.v1.CreateNexusEndpointResponse: - name = make_nexus_endpoint_name(task_queue) - return await client.operator_service.create_nexus_endpoint( - temporalio.api.operatorservice.v1.CreateNexusEndpointRequest( - spec=temporalio.api.nexus.v1.EndpointSpec( - name=name, - target=temporalio.api.nexus.v1.EndpointTarget( - worker=temporalio.api.nexus.v1.EndpointTarget.Worker( - namespace=client.namespace, - task_queue=task_queue, - ) - ), - ) +def events_of_type( + history: WorkflowHistory, + event_type: temporalio.api.enums.v1.EventType.ValueType, +) -> list[temporalio.api.history.v1.HistoryEvent]: + return [event for event in history.events if event.event_type == event_type] + + +def links_from_workflow_execution_started_event( + event: temporalio.api.history.v1.HistoryEvent, +) -> list[temporalio.api.common.v1.Link]: + callback_links = [ + link + for callback in event.workflow_execution_started_event_attributes.completion_callbacks + for link in callback.links + ] + if callback_links: + return list(callback_links) + return list(event.links) + + +def workflow_event_link_event_type( + workflow_event: temporalio.api.common.v1.Link.WorkflowEvent, +) -> temporalio.api.enums.v1.EventType.ValueType: + if workflow_event.HasField("request_id_ref"): + return workflow_event.request_id_ref.event_type + return workflow_event.event_ref.event_type + + +def expected_nexus_operation_link( + *, + namespace: str, + operation_id: str, + run_id: str, +) -> temporalio.api.common.v1.Link: + return temporalio.api.common.v1.Link( + nexus_operation=temporalio.api.common.v1.Link.NexusOperation( + namespace=namespace, + operation_id=operation_id, + run_id=run_id, ) ) -@dataclass -class ServiceClient: - server_address: str # E.g. http://127.0.0.1:7243 - endpoint: str - service: str - - async def start_operation( - self, - operation: str, - body: Optional[dict[str, Any]] = None, - headers: Mapping[str, str] = {}, - ) -> httpx.Response: - """ - Start a Nexus operation. - """ - # TODO(nexus-preview): Support callback URL as query param - async with httpx.AsyncClient() as http_client: - return await http_client.post( - f"http://{self.server_address}/nexus/endpoints/{self.endpoint}/services/{self.service}/{operation}", - json=body, - headers=headers, - ) - - async def cancel_operation( - self, - operation: str, - token: str, - ) -> httpx.Response: - async with httpx.AsyncClient() as http_client: - return await http_client.post( - f"http://{self.server_address}/nexus/endpoints/{self.endpoint}/services/{self.service}/{operation}/cancel", - # Token can also be sent as "Nexus-Operation-Token" header - params={"token": token}, +def expected_workflow_event_link( + *, + namespace: str, + workflow_id: str, + run_id: str, + event_type: temporalio.api.enums.v1.EventType.ValueType, + event_id: int = 0, + request_id: str | None = None, +) -> temporalio.api.common.v1.Link: + if request_id is not None: + return temporalio.api.common.v1.Link( + workflow_event=temporalio.api.common.v1.Link.WorkflowEvent( + namespace=namespace, + workflow_id=workflow_id, + run_id=run_id, + request_id_ref=temporalio.api.common.v1.Link.WorkflowEvent.RequestIdReference( + request_id=request_id, + event_type=event_type, + ), ) + ) - @staticmethod - def default_server_address(env: WorkflowEnvironment) -> str: - # TODO(nexus-preview): nexus tests are making http requests directly but this is - # not officially supported. - parsed = urlparse(env.client.service_client.config.target_host) - host = parsed.hostname or "127.0.0.1" - http_port = getattr(env, "_http_port", 7243) - return f"{host}:{http_port}" - - -def dataclass_as_dict(dataclass: Any) -> dict[str, Any]: - """ - Return a shallow dict of the dataclass's fields. - - dataclasses.as_dict goes too far (attempts to pickle values) - """ - return { - field.name: getattr(dataclass, field.name) - for field in dataclasses.fields(dataclass) - } - - -@dataclass -class Failure: - """A Nexus Failure object, with details parsed into an exception. - - https://github.com/nexus-rpc/api/blob/main/SPEC.md#failure - """ + return temporalio.api.common.v1.Link( + workflow_event=temporalio.api.common.v1.Link.WorkflowEvent( + namespace=namespace, + workflow_id=workflow_id, + run_id=run_id, + event_ref=temporalio.api.common.v1.Link.WorkflowEvent.EventReference( + event_id=event_id, + event_type=event_type, + ), + ) + ) - message: str = "" - metadata: Optional[dict[str, str]] = None - details: Optional[dict[str, Any]] = None - exception_from_details: Optional[BaseException] = dataclasses.field( - init=False, default=None - ) +def assert_links_match( + links: Sequence[temporalio.api.common.v1.Link], + *expected_links: temporalio.api.common.v1.Link, +) -> None: + actual = sorted(list(links), key=_link_sort_key) + expected = sorted(list(expected_links), key=_link_sort_key) + assert actual == expected - def __post_init__(self) -> None: - if self.metadata and (error_type := self.metadata.get("type")): - self.exception_from_details = self._instantiate_exception( - error_type, self.details - ) - def _instantiate_exception( - self, error_type: str, details: Optional[dict[str, Any]] - ) -> BaseException: - proto = { - "temporal.api.failure.v1.Failure": temporalio.api.failure.v1.Failure, - }[error_type]() - json_format.ParseDict(self.details, proto, ignore_unknown_fields=True) - return FailureConverter.default.from_failure(proto, PayloadConverter.default) +def _link_sort_key(link: temporalio.api.common.v1.Link) -> bytes: + return link.SerializeToString(deterministic=True) diff --git a/tests/helpers/trace.py b/tests/helpers/trace.py new file mode 100644 index 000000000..913bd27da --- /dev/null +++ b/tests/helpers/trace.py @@ -0,0 +1,111 @@ +"""Helpers for asserting indented trace hierarchies.""" + +from __future__ import annotations + +from collections.abc import Sequence +from dataclasses import dataclass, field + + +@dataclass +class TraceNode: + name: str + children: list[TraceNode] = field(default_factory=list) + + +def format_trace_hierarchy(roots: Sequence[TraceNode]) -> list[str]: + """Render a trace forest as indented lines.""" + lines: list[str] = [] + + def render(node: TraceNode, depth: int) -> None: + lines.append(" " * depth + node.name) + for child in node.children: + render(child, depth + 1) + + for root in roots: + render(root, 0) + return lines + + +def assert_trace_hierarchy( + actual: Sequence[TraceNode], expected: Sequence[str] +) -> None: + """Assert a trace hierarchy, allowing matching Start/Run siblings to swap. + + Activity execution can begin before the asynchronously published Start span + reaches an in-memory exporter. The two spans retain the same parent, so this + accepts only that sibling transposition and preserves all other ordering. + """ + actual_tree = TraceNode("", list(actual)) + expected_tree = _parse_trace(expected) + assert _nodes_match(actual_tree, expected_tree), ( + "Trace hierarchy differed.\n" + f"Actual:\n{chr(10).join(format_trace_hierarchy(actual))}\n" + f"Expected:\n{chr(10).join(expected)}" + ) + + +def _parse_trace(trace: Sequence[str]) -> TraceNode: + root = TraceNode("") + stack: list[tuple[int, TraceNode]] = [(-1, root)] + for line in trace: + name = line.lstrip() + indent = len(line) - len(name) + assert name and indent % 2 == 0, f"Invalid trace line: {line!r}" + depth = indent // 2 + while stack[-1][0] >= depth: + stack.pop() + assert depth == stack[-1][0] + 1, f"Invalid trace nesting: {line!r}" + node = TraceNode(name) + stack[-1][1].children.append(node) + stack.append((depth, node)) + return root + + +def _nodes_match(actual: TraceNode, expected: TraceNode) -> bool: + if actual.name != expected.name or len(actual.children) != len(expected.children): + return False + + index = 0 + while index < len(actual.children): + actual_child = actual.children[index] + expected_child = expected.children[index] + if _nodes_match(actual_child, expected_child): + index += 1 + continue + + if index + 1 == len(actual.children): + return False + actual_pair = actual.children[index : index + 2] + expected_pair = expected.children[index : index + 2] + if not _is_start_run_pair(*actual_pair) or not _is_start_run_pair( + *expected_pair + ): + return False + expected_by_name = {node.name: node for node in expected_pair} + if any(node.name not in expected_by_name for node in actual_pair): + return False + if not all( + _nodes_match(node, expected_by_name[node.name]) for node in actual_pair + ): + return False + index += 2 + + return True + + +def _is_start_run_pair(first: TraceNode, second: TraceNode) -> bool: + first_start = _start_run_suffix(first.name) + second_start = _start_run_suffix(second.name) + first_run = _run_suffix(first.name) + second_run = _run_suffix(second.name) + return (first_start is not None and first_start == second_run) or ( + second_start is not None and second_start == first_run + ) + + +def _start_run_suffix(name: str) -> str | None: + return name.removeprefix("Start") if name.startswith("Start") else None + + +def _run_suffix(name: str) -> str | None: + return name.removeprefix("Run") if name.startswith("Run") else None diff --git a/tests/helpers/worker.py b/tests/helpers/worker.py index ea307d263..b21be3b36 100644 --- a/tests/helpers/worker.py +++ b/tests/helpers/worker.py @@ -9,9 +9,10 @@ import asyncio import uuid from abc import ABC, abstractmethod +from collections.abc import Sequence from dataclasses import dataclass from datetime import timedelta -from typing import Any, Optional, Sequence, Tuple +from typing import Any import temporalio.converter from temporalio import workflow @@ -23,32 +24,32 @@ @dataclass class KSWorkflowParams: - actions: Optional[Sequence[KSAction]] = None - action_signal: Optional[str] = None + actions: Sequence[KSAction] | None = None + action_signal: str | None = None @dataclass class KSAction: - result: Optional[KSResultAction] = None - error: Optional[KSErrorAction] = None - continue_as_new: Optional[KSContinueAsNewAction] = None - sleep: Optional[KSSleepAction] = None - query_handler: Optional[KSQueryHandlerAction] = None - signal: Optional[KSSignalAction] = None - execute_activity: Optional[KSExecuteActivityAction] = None + result: KSResultAction | None = None + error: KSErrorAction | None = None + continue_as_new: KSContinueAsNewAction | None = None + sleep: KSSleepAction | None = None + query_handler: KSQueryHandlerAction | None = None + signal: KSSignalAction | None = None + execute_activity: KSExecuteActivityAction | None = None @dataclass class KSResultAction: - value: Optional[Any] = None - run_id: Optional[bool] = None + value: Any | None = None + run_id: bool | None = None @dataclass class KSErrorAction: - message: Optional[str] = None - details: Optional[Any] = None - attempt: Optional[bool] = None + message: str | None = None + details: Any | None = None + attempt: bool | None = None @dataclass @@ -74,18 +75,18 @@ class KSSignalAction: @dataclass class KSExecuteActivityAction: name: str - task_queue: Optional[str] = None - args: Optional[Sequence[Any]] = None - count: Optional[int] = None - index_as_arg: Optional[bool] = None - schedule_to_close_timeout_ms: Optional[int] = None - start_to_close_timeout_ms: Optional[int] = None - schedule_to_start_timeout_ms: Optional[int] = None - cancel_after_ms: Optional[int] = None - wait_for_cancellation: Optional[bool] = None - heartbeat_timeout_ms: Optional[int] = None - retry_max_attempts: Optional[int] = None - non_retryable_error_types: Optional[Sequence[str]] = None + task_queue: str | None = None + args: Sequence[Any] | None = None + count: int | None = None + index_as_arg: bool | None = None + schedule_to_close_timeout_ms: int | None = None + start_to_close_timeout_ms: int | None = None + schedule_to_start_timeout_ms: int | None = None + cancel_after_ms: int | None = None + wait_for_cancellation: bool | None = None + heartbeat_timeout_ms: int | None = None + retry_max_attempts: int | None = None + non_retryable_error_types: Sequence[str] | None = None @workflow.defn(name="kitchen_sink") @@ -110,7 +111,7 @@ async def run(self, params: KSWorkflowParams) -> Any: async def handle_action( self, params: KSWorkflowParams, action: KSAction - ) -> Tuple[bool, Any]: + ) -> tuple[bool, Any]: if action.result: if action.result.run_id: return (True, workflow.info().run_id) @@ -131,7 +132,7 @@ async def handle_action( elif action.signal: signal_event = asyncio.Event() - def signal_handler(arg: Optional[Any] = None) -> None: + def signal_handler(_arg: Any | None = None) -> None: signal_event.set() workflow.set_signal_handler(action.signal.name, signal_handler) @@ -140,12 +141,9 @@ def signal_handler(arg: Optional[Any] = None) -> None: opt = action.execute_activity config = workflow.ActivityConfig( task_queue=opt.task_queue, - retry_policy=RetryPolicy( - initial_interval=timedelta(milliseconds=1), - backoff_coefficient=1.01, - maximum_interval=timedelta(milliseconds=2), - maximum_attempts=opt.retry_max_attempts or 1, - non_retryable_error_types=opt.non_retryable_error_types or [], + retry_policy=kitchen_sink_retry_policy( + maximum_attempts=opt.retry_max_attempts, + non_retryable_error_types=opt.non_retryable_error_types, ), ) if opt.schedule_to_close_timeout_ms: @@ -207,6 +205,19 @@ async def run_activity(index: int) -> None: return False, None +def kitchen_sink_retry_policy( + maximum_attempts: int | None = None, + non_retryable_error_types: Sequence[str] | None = None, +) -> RetryPolicy: + return RetryPolicy( + initial_interval=timedelta(milliseconds=1), + backoff_coefficient=1.01, + maximum_interval=timedelta(milliseconds=2), + maximum_attempts=maximum_attempts or 1, + non_retryable_error_types=non_retryable_error_types, + ) + + async def cancel_after(task: asyncio.Task, after: float) -> None: await asyncio.sleep(after) task.cancel() diff --git a/tests/nexus/test_dynamic_creation_of_user_handler_classes.py b/tests/nexus/test_dynamic_creation_of_user_handler_classes.py index 0eef14b84..214e02ab9 100644 --- a/tests/nexus/test_dynamic_creation_of_user_handler_classes.py +++ b/tests/nexus/test_dynamic_creation_of_user_handler_classes.py @@ -1,16 +1,18 @@ import uuid -import httpx +import nexusrpc import nexusrpc.handler import pytest -from nexusrpc.handler import sync_operation from temporalio import nexus, workflow from temporalio.client import Client -from temporalio.nexus._util import get_operation_factory from temporalio.testing import WorkflowEnvironment from temporalio.worker import Worker -from tests.helpers.nexus import ServiceClient, create_nexus_endpoint +from tests.helpers.nexus import make_nexus_endpoint_name + +# Cloud CI's namespace credentials cannot manage Nexus endpoints. +# See https://github.com/temporalio/sdk-python/issues/1704. +pytestmark = pytest.mark.requires_local_server @workflow.defn @@ -20,11 +22,6 @@ async def run(self, input: int) -> int: return input + 1 -@nexusrpc.service -class MyService: - increment: nexusrpc.Operation[int, int] - - class MyIncrementOperationHandler(nexusrpc.handler.OperationHandler[int, int]): async def start( self, @@ -44,26 +41,16 @@ async def cancel( ) -> None: raise NotImplementedError - async def fetch_info( - self, - ctx: nexusrpc.handler.FetchOperationInfoContext, - token: str, - ) -> nexusrpc.OperationInfo: - raise NotImplementedError - - async def fetch_result( - self, - ctx: nexusrpc.handler.FetchOperationResultContext, - token: str, - ) -> int: - raise NotImplementedError - -@nexusrpc.handler.service_handler -class MyServiceHandlerWithWorkflowRunOperation: - @nexusrpc.handler._decorators.operation_handler - def increment(self) -> nexusrpc.handler.OperationHandler[int, int]: - return MyIncrementOperationHandler() +@workflow.defn +class IncrementCallerWorkflow: + @workflow.run + async def run(self, input: int, task_queue: str) -> int: + client = workflow.create_nexus_client( + service="MyService", + endpoint=make_nexus_endpoint_name(task_queue), + ) + return await client.execute_operation("increment", input, output_type=int) async def test_run_nexus_service_from_programmatically_created_service_handler( @@ -78,8 +65,8 @@ async def test_run_nexus_service_from_programmatically_created_service_handler( service_handler = nexusrpc.handler._core.ServiceHandler( service=nexusrpc.ServiceDefinition( name="MyService", - operations={ - "increment": nexusrpc.Operation[int, int]( + operation_definitions={ + "increment": nexusrpc.OperationDefinition[int, int]( name="increment", method_name="increment", input_type=int, @@ -92,85 +79,17 @@ async def test_run_nexus_service_from_programmatically_created_service_handler( }, ) - service_name = service_handler.service.name - - endpoint = (await create_nexus_endpoint(task_queue, client)).endpoint.id + await env.create_nexus_endpoint(make_nexus_endpoint_name(task_queue), task_queue) async with Worker( client, task_queue=task_queue, nexus_service_handlers=[service_handler], + workflows=[IncrementCallerWorkflow, MyWorkflow], ): - server_address = ServiceClient.default_server_address(env) - async with httpx.AsyncClient() as http_client: - response = await http_client.post( - f"http://{server_address}/nexus/endpoints/{endpoint}/services/{service_name}/increment", - json=1, - ) - assert response.status_code == 201 - - -def make_incrementer_user_service_definition_and_service_handler_classes( - op_names: list[str], -) -> tuple[type, type]: - # - # service contract - # - - ops = {name: nexusrpc.Operation[int, int] for name in op_names} - service_cls: type = nexusrpc.service(type("ServiceContract", (), ops)) - - # - # service handler - # - @sync_operation - async def _increment_op( - self, - ctx: nexusrpc.handler.StartOperationContext, - input: int, - ) -> int: - return input + 1 - - op_handler_factories = {} - for name in op_names: - op_handler_factory, _ = get_operation_factory(_increment_op) - assert op_handler_factory - op_handler_factories[name] = op_handler_factory - - handler_cls: type = nexusrpc.handler.service_handler(service=service_cls)( - type("ServiceImpl", (), op_handler_factories) - ) - - return service_cls, handler_cls - - -@pytest.mark.skip( - reason="Dynamic creation of service contract using type() is not supported" -) -async def test_dynamic_creation_of_user_handler_classes( - client: Client, env: WorkflowEnvironment -): - task_queue = str(uuid.uuid4()) - - service_cls, handler_cls = ( - make_incrementer_user_service_definition_and_service_handler_classes( - ["increment"] + result = await client.execute_workflow( + IncrementCallerWorkflow.run, + args=[5, task_queue], + id=str(uuid.uuid4()), + task_queue=task_queue, ) - ) - - assert (service_defn := nexusrpc.get_service_definition(service_cls)) - service_name = service_defn.name - - endpoint = (await create_nexus_endpoint(task_queue, client)).endpoint.id - async with Worker( - client, - task_queue=task_queue, - nexus_service_handlers=[handler_cls()], - ): - server_address = ServiceClient.default_server_address(env) - async with httpx.AsyncClient() as http_client: - response = await http_client.post( - f"http://{server_address}/nexus/endpoints/{endpoint}/services/{service_name}/increment", - json=1, - ) - assert response.status_code == 200 - assert response.json() == 2 + assert result == 6 diff --git a/tests/nexus/test_handler.py b/tests/nexus/test_handler.py deleted file mode 100644 index c805a967c..000000000 --- a/tests/nexus/test_handler.py +++ /dev/null @@ -1,1126 +0,0 @@ -""" -See https://github.com/nexus-rpc/api/blob/main/SPEC.md - -This file contains test coverage for Nexus StartOperation and CancelOperation -operations issued by a caller directly via HTTP. - -The response to StartOperation may indicate a protocol-level failure (400 -BAD_REQUEST, 520 UPSTREAM_TIMEOUT, etc). In this case the body is a valid -Failure object. - - -(https://github.com/nexus-rpc/api/blob/main/SPEC.md#predefined-handler-errors) - -""" - -import asyncio -import concurrent.futures -import logging -import pprint -import uuid -from collections.abc import Mapping -from concurrent.futures.thread import ThreadPoolExecutor -from dataclasses import dataclass -from types import MappingProxyType -from typing import Any, Callable, Optional, Union - -import httpx -import nexusrpc -import pytest -from nexusrpc import ( - HandlerError, - HandlerErrorType, - OperationError, - OperationErrorState, - OperationInfo, -) -from nexusrpc.handler import ( - CancelOperationContext, - FetchOperationInfoContext, - FetchOperationResultContext, - OperationHandler, - StartOperationContext, - StartOperationResultSync, - service_handler, - sync_operation, -) -from nexusrpc.handler._decorators import operation_handler - -from temporalio import nexus, workflow -from temporalio.client import Client -from temporalio.common import WorkflowIDReusePolicy -from temporalio.exceptions import ApplicationError -from temporalio.nexus import ( - WorkflowRunOperationContext, - workflow_run_operation, -) -from temporalio.testing import WorkflowEnvironment -from temporalio.worker import Worker -from tests.helpers.nexus import ( - Failure, - ServiceClient, - create_nexus_endpoint, - dataclass_as_dict, -) - - -@dataclass -class Input: - value: str - - -@dataclass -class Output: - value: str - - -@dataclass -class NonSerializableOutput: - callable: Callable[[], Any] = lambda: None - - -# TODO(nexus-prelease): Test attaching multiple callers to the same operation. -# TODO(nexus-preview): type check nexus implementation under mypy -# TODO(nexus-preview): test malformed inbound_links and outbound_links - -# TODO(nexus-prerelease): 2025-07-02T23:29:20.000489Z WARN temporal_sdk_core::worker::nexus: Nexus task not found on completion. This may happen if the operation has already been cancelled but completed anyway. details=Status { code: NotFound, message: "Nexus task not found or already expired", details: b"\x08\x05\x12'Nexus task not found or already expired\x1aB\n@type.googleapis.com/temporal.api.errordetails.v1.NotFoundFailure", metadata: MetadataMap { headers: {"content-type": "application/grpc"} }, source: None } - - -@nexusrpc.service -class MyService: - echo: nexusrpc.Operation[Input, Output] - echo_renamed: nexusrpc.Operation[Input, Output] = nexusrpc.Operation( - name="echo-renamed" - ) - hang: nexusrpc.Operation[Input, Output] - log: nexusrpc.Operation[Input, Output] - workflow_run_operation_happy_path: nexusrpc.Operation[Input, Output] - sync_operation_with_non_async_def: nexusrpc.Operation[Input, Output] - operation_returning_unwrapped_result_at_runtime_error: nexusrpc.Operation[ - Input, Output - ] - non_retryable_application_error: nexusrpc.Operation[Input, Output] - retryable_application_error: nexusrpc.Operation[Input, Output] - check_operation_timeout_header: nexusrpc.Operation[Input, Output] - workflow_run_op_link_test: nexusrpc.Operation[Input, Output] - handler_error_internal: nexusrpc.Operation[Input, Output] - operation_error_failed: nexusrpc.Operation[Input, Output] - idempotency_check: nexusrpc.Operation[None, Output] - non_serializable_output: nexusrpc.Operation[Input, NonSerializableOutput] - - -@workflow.defn -class MyWorkflow: - @workflow.run - async def run(self, input: Input) -> Output: - return Output(value=f"from workflow: {input.value}") - - -@workflow.defn -class WorkflowWithoutTypeAnnotations: - @workflow.run - async def run(self, input): # type: ignore - return Output(value=f"from workflow without type annotations: {input}") - - -@workflow.defn -class MyLinkTestWorkflow: - @workflow.run - async def run(self, input: Input) -> Output: - return Output(value=f"from link test workflow: {input.value}") - - -# The service_handler decorator is applied by the test -class MyServiceHandler: - @sync_operation - async def echo(self, ctx: StartOperationContext, input: Input) -> Output: - assert ctx.headers["test-header-key"] == "test-header-value" - ctx.outbound_links.extend(ctx.inbound_links) - assert nexus.in_operation() - return Output( - value=f"from start method on {self.__class__.__name__}: {input.value}" - ) - - # The name override is present in the service definition. But the test below submits - # the same operation name in the request whether using a service definition or now. - # The name override here is necessary when the test is not using the service - # definition. It should be permitted when the service definition is in effect, as - # long as the name override is the same as that in the service definition. - @sync_operation(name="echo-renamed") - async def echo_renamed(self, ctx: StartOperationContext, input: Input) -> Output: - return await self.echo(ctx, input) - - @sync_operation - async def hang(self, ctx: StartOperationContext, input: Input) -> Output: - await asyncio.Future() - return Output(value="won't reach here") - - @sync_operation - async def non_retryable_application_error( - self, ctx: StartOperationContext, input: Input - ) -> Output: - raise ApplicationError( - "non-retryable application error", - "details arg", - # TODO(nexus-preview): what values of `type` should be tested? - type="TestFailureType", - non_retryable=True, - ) - - @sync_operation - async def retryable_application_error( - self, ctx: StartOperationContext, input: Input - ) -> Output: - raise ApplicationError( - "retryable application error", - "details arg", - type="TestFailureType", - non_retryable=False, - ) - - @sync_operation - async def handler_error_internal( - self, ctx: StartOperationContext, input: Input - ) -> Output: - raise HandlerError( - message="deliberate internal handler error", - type=HandlerErrorType.INTERNAL, - retryable_override=False, - ) from RuntimeError("cause message") - - @sync_operation - async def operation_error_failed( - self, ctx: StartOperationContext, input: Input - ) -> Output: - raise OperationError( - message="deliberate operation error", - state=OperationErrorState.FAILED, - ) - - @sync_operation - async def check_operation_timeout_header( - self, ctx: StartOperationContext, input: Input - ) -> Output: - assert "operation-timeout" in ctx.headers - return Output( - value=f"from start method on {self.__class__.__name__}: {input.value}" - ) - - @sync_operation - async def log(self, ctx: StartOperationContext, input: Input) -> Output: - nexus.logger.info( - "Logging from start method", extra={"input_value": input.value} - ) - return Output(value=f"logged: {input.value}") - - @workflow_run_operation - async def workflow_run_operation_happy_path( - self, ctx: WorkflowRunOperationContext, input: Input - ) -> nexus.WorkflowHandle[Output]: - assert nexus.in_operation() - return await ctx.start_workflow( - MyWorkflow.run, - input, - id=str(uuid.uuid4()), - id_reuse_policy=WorkflowIDReusePolicy.REJECT_DUPLICATE, - ) - - @sync_operation - async def sync_operation_with_non_async_def( - self, ctx: StartOperationContext, input: Input - ) -> Output: - return Output( - value=f"from start method on {self.__class__.__name__}: {input.value}" - ) - - @workflow_run_operation - async def workflow_run_op_link_test( - self, ctx: WorkflowRunOperationContext, input: Input - ) -> nexus.WorkflowHandle[Output]: - assert any( - link.url == "http://inbound-link/" for link in ctx.inbound_links - ), "Inbound link not found" - assert ctx.request_id == "test-request-id-123", "Request ID mismatch" - ctx.outbound_links.extend(ctx.inbound_links) - - return await ctx.start_workflow( - MyLinkTestWorkflow.run, - input, - id=str(uuid.uuid4()), - ) - - class OperationHandlerReturningUnwrappedResult(OperationHandler[Input, Output]): - async def start( # type: ignore[override] # intentional test error - self, - ctx: StartOperationContext, - input: Input, - # This return type is a type error, but VSCode doesn't flag it unless - # "python.analysis.typeCheckingMode" is set to "strict" - ) -> Output: - # Invalid: start method must wrap result as StartOperationResultSync - # or StartOperationResultAsync - return Output(value="unwrapped result error") - - async def fetch_info( - self, ctx: FetchOperationInfoContext, token: str - ) -> OperationInfo: - raise NotImplementedError - - async def fetch_result( - self, ctx: FetchOperationResultContext, token: str - ) -> Output: - raise NotImplementedError - - async def cancel(self, ctx: CancelOperationContext, token: str) -> None: - raise NotImplementedError - - @operation_handler - def operation_returning_unwrapped_result_at_runtime_error( - self, - ) -> OperationHandler[Input, Output]: - return MyServiceHandler.OperationHandlerReturningUnwrappedResult() - - @sync_operation - async def idempotency_check( - self, ctx: StartOperationContext, input: None - ) -> Output: - return Output(value=f"request_id: {ctx.request_id}") - - @sync_operation - async def non_serializable_output( - self, ctx: StartOperationContext, input: Input - ) -> NonSerializableOutput: - return NonSerializableOutput() - - -# Immutable dicts that can be used as dataclass field defaults - -SUCCESSFUL_RESPONSE_HEADERS = MappingProxyType( - { - "content-type": "application/json", - } -) - -UNSUCCESSFUL_RESPONSE_HEADERS = MappingProxyType( - { - "content-type": "application/json", - "temporal-nexus-failure-source": "worker", - } -) - - -@dataclass -class SuccessfulResponse: - status_code: int - body_json: Optional[Union[dict[str, Any], Callable[[dict[str, Any]], bool]]] = None - headers: Mapping[str, str] = SUCCESSFUL_RESPONSE_HEADERS - - -@dataclass -class UnsuccessfulResponse: - status_code: int - failure_message: Union[str, Callable[[str], bool]] - # Is the Nexus Failure expected to have the details field populated? - failure_details: bool = True - # Expected value of inverse of non_retryable attribute of exception. - retryable_exception: bool = True - body_json: Optional[Callable[[dict[str, Any]], bool]] = None - headers: Mapping[str, str] = UNSUCCESSFUL_RESPONSE_HEADERS - - -class _TestCase: - operation: str - service_defn: str = "MyService" - input: Input = Input("") - headers: dict[str, str] = {} - expected: SuccessfulResponse - expected_without_service_definition: Optional[SuccessfulResponse] = None - skip = "" - - @classmethod - def check_response( - cls, - response: httpx.Response, - with_service_definition: bool, - ) -> None: - assert response.status_code == cls.expected.status_code, ( - f"expected status code {cls.expected.status_code} " - f"but got {response.status_code} for response content" - f"{pprint.pformat(response.content.decode())}" - ) - if not with_service_definition and cls.expected_without_service_definition: - expected = cls.expected_without_service_definition - else: - expected = cls.expected - if expected.body_json is not None: - body = response.json() - assert isinstance(body, dict) - if isinstance(expected.body_json, dict): - assert body == expected.body_json - else: - assert expected.body_json(body) - assert response.headers.items() >= cls.expected.headers.items() - - -class _FailureTestCase(_TestCase): - expected: UnsuccessfulResponse # type: ignore[assignment] - - @classmethod - def check_response( - cls, response: httpx.Response, with_service_definition: bool - ) -> None: - super().check_response(response, with_service_definition) - failure = Failure(**response.json()) - - if isinstance(cls.expected.failure_message, str): - assert failure.message == cls.expected.failure_message - else: - assert cls.expected.failure_message(failure.message) - - -class SyncHandlerHappyPath(_TestCase): - operation = "echo" - input = Input("hello") - # TODO(nexus-prerelease): why is application/json randomly scattered around these tests? - headers = { - "Content-Type": "application/json", - "Test-Header-Key": "test-header-value", - "Nexus-Link": '; type="test"', - } - expected = SuccessfulResponse( - status_code=200, - body_json={"value": "from start method on MyServiceHandler: hello"}, - ) - # TODO(nexus-prerelease): headers should be lower-cased - assert ( - headers.get("Nexus-Link") == '; type="test"' - ), "Nexus-Link header not echoed correctly." - - -class SyncHandlerHappyPathRenamed(SyncHandlerHappyPath): - operation = "echo-renamed" - - -class SyncHandlerHappyPathNonAsyncDef(_TestCase): - operation = "sync_operation_with_non_async_def" - input = Input("hello") - expected = SuccessfulResponse( - status_code=200, - body_json={"value": "from start method on MyServiceHandler: hello"}, - ) - - -class AsyncHandlerHappyPath(_TestCase): - operation = "workflow_run_operation_happy_path" - input = Input("hello") - headers = {"Operation-Timeout": "777s"} - expected = SuccessfulResponse( - status_code=201, - ) - - -class WorkflowRunOpLinkTestHappyPath(_TestCase): - # TODO(nexus-prerelease): fix this test - skip = "Yields invalid link" - operation = "workflow_run_op_link_test" - input = Input("link-test-input") - headers = { - "Nexus-Link": '; type="test"', - "Nexus-Request-Id": "test-request-id-123", - } - expected = SuccessfulResponse( - status_code=201, - ) - - @classmethod - def check_response( - cls, response: httpx.Response, with_service_definition: bool - ) -> None: - super().check_response(response, with_service_definition) - nexus_link = response.headers.get("nexus-link") - assert nexus_link is not None, "nexus-link header not found in response" - assert nexus_link.startswith( - " Output: - assert ctx.headers["test-header-key"] == "test-header-value" - ctx.outbound_links.extend(ctx.inbound_links) - return Output( - value=f"from start method on {self.__class__.__name__}: {input.value}" - ) - - -@service_handler(service=EchoService) -class DefaultCancelHandler: - @sync_operation - async def echo(self, ctx: StartOperationContext, input: Input) -> Output: - return Output( - value=f"from start method on {self.__class__.__name__}: {input.value}" - ) - - -@service_handler(service=EchoService) -class SyncCancelHandler: - class SyncCancel(OperationHandler[Input, Output]): - async def start( - self, - ctx: StartOperationContext, - input: Input, - # This return type is a type error, but VSCode doesn't flag it unless - # "python.analysis.typeCheckingMode" is set to "strict" - ) -> StartOperationResultSync[Output]: - # Invalid: start method must wrap result as StartOperationResultSync - # or StartOperationResultAsync - return StartOperationResultSync(Output(value="Hello")) # type: ignore - - def cancel(self, ctx: CancelOperationContext, token: str) -> None: - return None # type: ignore - - def fetch_info( - self, ctx: FetchOperationInfoContext, token: str - ) -> OperationInfo: - raise NotImplementedError - - def fetch_result(self, ctx: FetchOperationResultContext, token: str) -> Output: - raise NotImplementedError - - @operation_handler - def echo(self) -> OperationHandler[Input, Output]: - return SyncCancelHandler.SyncCancel() - - -class SyncHandlerNoExecutor(_InstantiationCase): - handler = SyncStartHandler - executor = False - exception = RuntimeError - match = "you have not supplied an executor" - - -class DefaultCancel(_InstantiationCase): - handler = DefaultCancelHandler - executor = False - exception = None - - -class SyncCancel(_InstantiationCase): - handler = SyncCancelHandler - executor = False - exception = RuntimeError - match = "you have not supplied an executor" - - -@pytest.mark.parametrize( - "test_case", - [SyncHandlerNoExecutor, DefaultCancel, SyncCancel], -) -async def test_handler_instantiation( - test_case: type[_InstantiationCase], client: Client -): - task_queue = str(uuid.uuid4()) - - if test_case.exception is not None: - with pytest.raises(test_case.exception, match=test_case.match): - Worker( - client, - task_queue=task_queue, - nexus_service_handlers=[test_case.handler()], - nexus_task_executor=ThreadPoolExecutor() - if test_case.executor - else None, - ) - else: - Worker( - client, - task_queue=task_queue, - nexus_service_handlers=[test_case.handler()], - nexus_task_executor=ThreadPoolExecutor() if test_case.executor else None, - ) - - -async def test_cancel_operation_with_invalid_token(env: WorkflowEnvironment): - if env.supports_time_skipping: - pytest.skip("Nexus tests don't work with time-skipping server") - - """Verify that canceling an operation with an invalid token fails correctly.""" - task_queue = str(uuid.uuid4()) - endpoint = (await create_nexus_endpoint(task_queue, env.client)).endpoint.id - service_client = ServiceClient( - server_address=ServiceClient.default_server_address(env), - endpoint=endpoint, - service=MyService.__name__, - ) - - decorator = service_handler(service=MyService) - user_service_handler = decorator(MyServiceHandler)() - - async with Worker( - env.client, - task_queue=task_queue, - nexus_service_handlers=[user_service_handler], - nexus_task_executor=concurrent.futures.ThreadPoolExecutor(), - ): - cancel_response = await service_client.cancel_operation( - "workflow_run_operation_happy_path", - token="this-is-not-a-valid-token", - ) - assert cancel_response.status_code == 404 - failure = Failure(**cancel_response.json()) - assert "failed to decode operation token" in failure.message.lower() - - -async def test_request_id_is_received_by_sync_operation( - env: WorkflowEnvironment, -): - if env.supports_time_skipping: - pytest.skip("Nexus tests don't work with time-skipping server") - - task_queue = str(uuid.uuid4()) - endpoint = (await create_nexus_endpoint(task_queue, env.client)).endpoint.id - service_client = ServiceClient( - server_address=ServiceClient.default_server_address(env), - endpoint=endpoint, - service=MyService.__name__, - ) - - decorator = service_handler(service=MyService) - user_service_handler = decorator(MyServiceHandler)() - - async with Worker( - env.client, - task_queue=task_queue, - nexus_service_handlers=[user_service_handler], - nexus_task_executor=concurrent.futures.ThreadPoolExecutor(), - ): - request_id = str(uuid.uuid4()) - resp = await service_client.start_operation( - "idempotency_check", None, {"Nexus-Request-Id": request_id} - ) - assert resp.status_code == 200 - assert resp.json() == {"value": f"request_id: {request_id}"} - - -@workflow.defn -class EchoWorkflow: - @workflow.run - async def run(self, input: Input) -> Output: - return Output(value=input.value) - - -@service_handler -class ServiceHandlerForRequestIdTest: - @workflow_run_operation - async def operation_backed_by_a_workflow( - self, ctx: WorkflowRunOperationContext, input: Input - ) -> nexus.WorkflowHandle[Output]: - return await ctx.start_workflow( - EchoWorkflow.run, - input, - id=input.value, - id_reuse_policy=WorkflowIDReusePolicy.REJECT_DUPLICATE, - ) - - @workflow_run_operation - async def operation_that_executes_a_workflow_before_starting_the_backing_workflow( - self, ctx: WorkflowRunOperationContext, input: Input - ) -> nexus.WorkflowHandle[Output]: - await nexus.client().start_workflow( - EchoWorkflow.run, - input, - id=input.value, - task_queue=nexus.info().task_queue, - ) - # This should fail. It will not fail if the Nexus request ID was incorrectly - # propagated to both StartWorkflow requests. - return await ctx.start_workflow( - EchoWorkflow.run, - input, - id=input.value, - id_reuse_policy=WorkflowIDReusePolicy.REJECT_DUPLICATE, - ) - - -async def test_request_id_becomes_start_workflow_request_id(env: WorkflowEnvironment): - if env.supports_time_skipping: - pytest.skip("Nexus tests don't work with time-skipping server") - - # We send two Nexus requests that would start a workflow with the same workflow ID, - # using reuse_policy=REJECT_DUPLICATE. This would fail if they used different - # request IDs. However, when we use the same request ID, it does not fail, - # demonstrating that the Nexus Start Operation request ID has become the - # StartWorkflow request ID. - task_queue = str(uuid.uuid4()) - endpoint = (await create_nexus_endpoint(task_queue, env.client)).endpoint.id - service_client = ServiceClient( - server_address=ServiceClient.default_server_address(env), - endpoint=endpoint, - service=ServiceHandlerForRequestIdTest.__name__, - ) - - async def start_two_workflows_with_conflicting_workflow_ids( - request_ids: tuple[tuple[str, int, str], tuple[str, int, str]], - ): - workflow_id = str(uuid.uuid4()) - for request_id, status_code, error_message in request_ids: - resp = await service_client.start_operation( - "operation_backed_by_a_workflow", - dataclass_as_dict(Input(workflow_id)), - {"Nexus-Request-Id": request_id}, - ) - assert resp.status_code == status_code, ( - f"expected status code {status_code} " - f"but got {resp.status_code} for response content " - f"{pprint.pformat(resp.content.decode())}" - ) - if not error_message: - assert status_code == 201 - op_info = resp.json() - assert op_info["token"] - assert op_info["state"] == nexusrpc.OperationState.RUNNING.value - else: - assert status_code >= 400 - failure = Failure(**resp.json()) - assert failure.message == error_message - - async def start_two_workflows_in_a_single_operation( - request_id: str, status_code: int, error_message: str - ): - resp = await service_client.start_operation( - "operation_that_executes_a_workflow_before_starting_the_backing_workflow", - dataclass_as_dict(Input("test-workflow-id")), - {"Nexus-Request-Id": request_id}, - ) - assert resp.status_code == status_code - if error_message: - failure = Failure(**resp.json()) - assert failure.message == error_message - - async with Worker( - env.client, - task_queue=task_queue, - nexus_service_handlers=[ServiceHandlerForRequestIdTest()], - nexus_task_executor=concurrent.futures.ThreadPoolExecutor(), - ): - request_id_1, request_id_2 = str(uuid.uuid4()), str(uuid.uuid4()) - # Reusing the same request ID does not fail - await start_two_workflows_with_conflicting_workflow_ids( - ((request_id_1, 201, ""), (request_id_1, 201, "")) - ) - # Using a different request ID does fail - # TODO(nexus-prerelease) I think that this should be a 409 per the spec. Go and - # Java are not doing that. - await start_two_workflows_with_conflicting_workflow_ids( - ( - (request_id_1, 201, ""), - (request_id_2, 500, "Workflow execution already started"), - ) - ) - # Two workflows started in the same operation should fail, since the Nexus - # request ID should be propagated to the backing workflow only. - await start_two_workflows_in_a_single_operation( - request_id_1, 500, "Workflow execution already started" - ) diff --git a/tests/nexus/test_handler_async_operation.py b/tests/nexus/test_handler_async_operation.py deleted file mode 100644 index df245d0ff..000000000 --- a/tests/nexus/test_handler_async_operation.py +++ /dev/null @@ -1,264 +0,0 @@ -""" -Test that the Nexus SDK can be used to define an operation that responds asynchronously. -""" - -from __future__ import annotations - -import asyncio -import concurrent.futures -import dataclasses -import uuid -from collections.abc import Coroutine -from dataclasses import dataclass, field -from typing import Any, Type, Union - -import nexusrpc -import nexusrpc.handler -import pytest -from nexusrpc import OperationInfo -from nexusrpc.handler import ( - CancelOperationContext, - FetchOperationInfoContext, - FetchOperationResultContext, - OperationHandler, - StartOperationContext, - StartOperationResultAsync, - service_handler, -) -from nexusrpc.handler._decorators import operation_handler - -from temporalio.testing import WorkflowEnvironment -from temporalio.worker import Worker -from tests.helpers.nexus import ServiceClient, create_nexus_endpoint - - -@dataclass -class Input: - value: str - - -@dataclass -class Output: - value: str - - -@dataclass -class AsyncOperationWithAsyncDefs(OperationHandler[Input, Output]): - executor: TaskExecutor - - async def start( - self, ctx: StartOperationContext, input: Input - ) -> StartOperationResultAsync: - async def task() -> Output: - await asyncio.sleep(0.1) - return Output("Hello from async operation!") - - task_id = str(uuid.uuid4()) - await self.executor.add_task(task_id, task()) - return StartOperationResultAsync(token=task_id) - - async def fetch_info( - self, ctx: FetchOperationInfoContext, token: str - ) -> OperationInfo: - # status = self.executor.get_task_status(task_id=token) - # return OperationInfo(token=token, status=status) - raise NotImplementedError( - "Not possible to test this currently since the server's Nexus implementation does not support fetch_info" - ) - - async def fetch_result( - self, ctx: FetchOperationResultContext, token: str - ) -> Output: - # return await self.executor.get_task_result(task_id=token) - raise NotImplementedError( - "Not possible to test this currently since the server's Nexus implementation does not support fetch_result" - ) - - async def cancel(self, ctx: CancelOperationContext, token: str) -> None: - self.executor.request_cancel_task(task_id=token) - - -@dataclass -class AsyncOperationWithNonAsyncDefs(OperationHandler[Input, Output]): - executor: TaskExecutor - - def start( - self, ctx: StartOperationContext, input: Input - ) -> StartOperationResultAsync: - async def task() -> Output: - await asyncio.sleep(0.1) - return Output("Hello from async operation!") - - task_id = str(uuid.uuid4()) - self.executor.add_task_sync(task_id, task()) - return StartOperationResultAsync(token=task_id) - - def fetch_info(self, ctx: FetchOperationInfoContext, token: str) -> OperationInfo: - # status = self.executor.get_task_status(task_id=token) - # return OperationInfo(token=token, status=status) - raise NotImplementedError( - "Not possible to test this currently since the server's Nexus implementation does not support fetch_info" - ) - - def fetch_result(self, ctx: FetchOperationResultContext, token: str) -> Output: - # return self.executor.get_task_result_sync(task_id=token) - raise NotImplementedError( - "Not possible to test this currently since the server's Nexus implementation does not support fetch_result" - ) - - def cancel(self, ctx: CancelOperationContext, token: str) -> None: - self.executor.request_cancel_task(task_id=token) - - -@dataclass -@service_handler -class MyServiceHandlerWithAsyncDefs: - executor: TaskExecutor - - @operation_handler - def async_operation(self) -> OperationHandler[Input, Output]: - return AsyncOperationWithAsyncDefs(self.executor) - - -@dataclass -@service_handler -class MyServiceHandlerWithNonAsyncDefs: - executor: TaskExecutor - - @operation_handler - def async_operation(self) -> OperationHandler[Input, Output]: - return AsyncOperationWithNonAsyncDefs(self.executor) - - -@pytest.mark.parametrize( - "service_handler_cls", - [ - MyServiceHandlerWithAsyncDefs, - MyServiceHandlerWithNonAsyncDefs, - ], -) -async def test_async_operation_lifecycle( - env: WorkflowEnvironment, - service_handler_cls: Union[ - Type[MyServiceHandlerWithAsyncDefs], - Type[MyServiceHandlerWithNonAsyncDefs], - ], -): - if env.supports_time_skipping: - pytest.skip("Nexus tests don't work with time-skipping server") - - task_executor = await TaskExecutor.connect() - task_queue = str(uuid.uuid4()) - endpoint = (await create_nexus_endpoint(task_queue, env.client)).endpoint.id - service_client = ServiceClient( - ServiceClient.default_server_address(env), - endpoint, - service_handler_cls.__name__, - ) - - async with Worker( - env.client, - task_queue=task_queue, - nexus_service_handlers=[service_handler_cls(task_executor)], - nexus_task_executor=concurrent.futures.ThreadPoolExecutor(), - ): - start_response = await service_client.start_operation( - "async_operation", - body=dataclass_as_dict(Input(value="Hello from test")), - ) - assert start_response.status_code == 201 - assert start_response.json()["token"] - assert start_response.json()["state"] == "running" - - # Cancel it - cancel_response = await service_client.cancel_operation( - "async_operation", - token=start_response.json()["token"], - ) - assert cancel_response.status_code == 202 - - # get_info and get_result not implemented by server - - -@dataclass -class TaskExecutor: - """ - This class represents the task execution platform being used by the team operating the - Nexus operation. - """ - - event_loop: asyncio.AbstractEventLoop - tasks: dict[str, asyncio.Task[Any]] = field(default_factory=dict) - - @classmethod - async def connect(cls) -> TaskExecutor: - return cls(event_loop=asyncio.get_running_loop()) - - async def add_task(self, task_id: str, coro: Coroutine[Any, Any, Any]) -> None: - """ - Add a task to the task execution platform. - """ - if task_id in self.tasks: - raise RuntimeError(f"Task with id {task_id} already exists") - - # This function is async def because in reality this step will often write to - # durable storage. - self.tasks[task_id] = asyncio.create_task(coro) - - def add_task_sync(self, task_id: str, coro: Coroutine[Any, Any, Any]) -> None: - """ - Add a task to the task execution platform from a sync context. - """ - asyncio.run_coroutine_threadsafe( - self.add_task(task_id, coro), self.event_loop - ).result() - - def get_task_status(self, task_id: str) -> nexusrpc.OperationState: - task = self.tasks[task_id] - if not task.done(): - return nexusrpc.OperationState.RUNNING - elif task.cancelled(): - return nexusrpc.OperationState.CANCELED - elif task.exception(): - return nexusrpc.OperationState.FAILED - else: - return nexusrpc.OperationState.SUCCEEDED - - async def get_task_result(self, task_id: str) -> Any: - """ - Get the result of a task from the task execution platform. - """ - task = self.tasks.get(task_id) - if not task: - raise RuntimeError(f"Task not found with id {task_id}") - return await task - - def get_task_result_sync(self, task_id: str) -> Any: - """ - Get the result of a task from the task execution platform from a sync context. - """ - return asyncio.run_coroutine_threadsafe( - self.get_task_result(task_id), self.event_loop - ).result() - - def request_cancel_task(self, task_id: str) -> None: - """ - Request cancellation of a task on the task execution platform. - """ - task = self.tasks.get(task_id) - if not task: - raise RuntimeError(f"Task not found with id {task_id}") - task.cancel() - # Not implemented: cancellation confirmation, deletion on cancellation - - -def dataclass_as_dict(dataclass: Any) -> dict[str, Any]: - """ - Return a shallow dict of the dataclass's fields. - - dataclasses.as_dict goes too far (attempts to pickle values) - """ - return { - field.name: getattr(dataclass, field.name) - for field in dataclasses.fields(dataclass) - } diff --git a/tests/nexus/test_handler_interface_implementation.py b/tests/nexus/test_handler_interface_implementation.py index d51d58dca..f20f25a04 100644 --- a/tests/nexus/test_handler_interface_implementation.py +++ b/tests/nexus/test_handler_interface_implementation.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Any, Optional +from typing import Any import nexusrpc import nexusrpc.handler @@ -14,7 +14,7 @@ class _InterfaceImplementationTestCase: Interface: type[Any] Impl: type[Any] - error_message: Optional[str] + error_message: str | None class ValidImpl(_InterfaceImplementationTestCase): @@ -44,11 +44,29 @@ async def op( error_message = None +class MissingWorkflowRunDecorator(_InterfaceImplementationTestCase): + """Missing @workflow_run_operation decorator raises appropriate error.""" + + @nexusrpc.service + class Interface: + my_workflow_op: nexusrpc.Operation[str, int] + + class Impl: + # Method exists but MISSING @workflow_run_operation decorator + async def my_workflow_op( + self, _ctx: WorkflowRunOperationContext, _input: str + ) -> nexus.WorkflowHandle[int]: + raise NotImplementedError + + error_message = "does not implement an operation with method name 'my_workflow_op'" + + @pytest.mark.parametrize( "test_case", [ ValidImpl, ValidWorkflowRunImpl, + MissingWorkflowRunDecorator, ], ) def test_service_decorator_enforces_interface_conformance( @@ -56,7 +74,9 @@ def test_service_decorator_enforces_interface_conformance( ): if test_case.error_message: with pytest.raises(Exception) as ei: - nexusrpc.handler.service_handler(test_case.Interface)(test_case.Impl) + nexusrpc.handler.service_handler(service=test_case.Interface)( + test_case.Impl + ) err = ei.value assert test_case.error_message in str(err) else: diff --git a/tests/nexus/test_handler_operation_definitions.py b/tests/nexus/test_handler_operation_definitions.py index 8e41c1efa..f47e3f47f 100644 --- a/tests/nexus/test_handler_operation_definitions.py +++ b/tests/nexus/test_handler_operation_definitions.py @@ -3,8 +3,9 @@ and input/output types. """ +import warnings from dataclasses import dataclass -from typing import Any, Type +from typing import Any import nexusrpc.handler import pytest @@ -26,7 +27,7 @@ class Output: @dataclass class _TestCase: - Service: Type[Any] + Service: type[Any] expected_operations: dict[str, nexusrpc.Operation] @@ -35,14 +36,13 @@ class NotCalled(_TestCase): class Service: @workflow_run_operation async def my_workflow_run_operation_handler( - self, ctx: WorkflowRunOperationContext, input: Input + self, _ctx: WorkflowRunOperationContext, _input: Input ) -> nexus.WorkflowHandle[Output]: raise NotImplementedError expected_operations = { "my_workflow_run_operation_handler": nexusrpc.Operation( name="my_workflow_run_operation_handler", - method_name="my_workflow_run_operation_handler", input_type=Input, output_type=Output, ), @@ -54,7 +54,7 @@ class CalledWithoutArgs(_TestCase): class Service: @workflow_run_operation async def my_workflow_run_operation_handler( - self, ctx: WorkflowRunOperationContext, input: Input + self, _ctx: WorkflowRunOperationContext, _input: Input ) -> nexus.WorkflowHandle[Output]: raise NotImplementedError @@ -66,14 +66,13 @@ class CalledWithNameOverride(_TestCase): class Service: @workflow_run_operation(name="operation-name") async def workflow_run_operation_with_name_override( - self, ctx: WorkflowRunOperationContext, input: Input + self, _ctx: WorkflowRunOperationContext, _input: Input ) -> nexus.WorkflowHandle[Output]: raise NotImplementedError expected_operations = { "workflow_run_operation_with_name_override": nexusrpc.Operation( name="operation-name", - method_name="workflow_run_operation_with_name_override", input_type=Input, output_type=Output, ), @@ -90,7 +89,7 @@ async def workflow_run_operation_with_name_override( ) @pytest.mark.asyncio async def test_collected_operation_names( - test_case: Type[_TestCase], + test_case: type[_TestCase], ): service_defn = nexusrpc.get_service_definition(test_case.Service) assert isinstance(service_defn, nexusrpc.ServiceDefinition) @@ -101,3 +100,101 @@ async def test_collected_operation_names( assert actual_op.name == expected_op.name assert actual_op.input_type == expected_op.input_type assert actual_op.output_type == expected_op.output_type + + +def test_unsafe_narrow_context_annotations_warn_and_drop_input_type(): + """Unsafe context annotations warn and prevent input type inference. + + Decorators construct a specific context type at runtime. If a handler annotates a + narrower or unrelated context type, the decorator cannot safely call it, so we + should warn and avoid using the handler annotation to infer operation input type. + """ + + with pytest.warns( + UserWarning, + match="Expected parameter 1 .* TemporalStartOperationContext", + ): + + class MyTemporalOpCtx(nexus.TemporalStartOperationContext): + def custom_method(self): + raise NotImplementedError + + class TemporalOperationHandler: + @nexus.temporal_operation # type: ignore[arg-type] + async def op( + self, + _ctx: MyTemporalOpCtx, + _client: nexus.TemporalNexusClient, + _input: Input, + ) -> nexus.TemporalOperationResult[Output]: + raise NotImplementedError + + _, temporal_op = get_operation_factory(TemporalOperationHandler.op) + assert isinstance(temporal_op, nexusrpc.Operation) + assert temporal_op.input_type is None + assert temporal_op.output_type == Output + + with pytest.warns( + UserWarning, + match="Expected parameter 1 .* WorkflowRunOperationContext", + ): + + class MyWorkflowRunOpCtx(nexus.WorkflowRunOperationContext): + def custom_method(self): + raise NotImplementedError + + class WorkflowRunOperationHandler: + @workflow_run_operation # type: ignore[arg-type] + async def op( + self, + _ctx: MyWorkflowRunOpCtx, + _input: Input, + ) -> nexus.WorkflowHandle[Output]: + raise NotImplementedError + + _, workflow_op = get_operation_factory(WorkflowRunOperationHandler.op) + assert isinstance(workflow_op, nexusrpc.Operation) + assert workflow_op.input_type is None + assert workflow_op.output_type == Output + + +def test_safe_broader_context_annotations_preserve_input_type_without_warnings(): + """Safe context annotations preserve input type inference without warnings. + + A handler can safely annotate a context parameter with the exact runtime context + type or a broader base type. These cases should keep handler-derived operation + input metadata intact. + """ + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + + class TemporalOperationHandler: + @nexus.temporal_operation + async def op( + self, + _ctx: nexusrpc.handler.StartOperationContext, + _client: nexus.TemporalNexusClient, + _input: Input, + ) -> nexus.TemporalOperationResult[Output]: + raise NotImplementedError + + class WorkflowRunStartContextHandler: + @workflow_run_operation + async def op( + self, + _ctx: nexusrpc.handler.StartOperationContext, + _input: Input, + ) -> nexus.WorkflowHandle[Output]: + raise NotImplementedError + + assert not caught + + for method in ( + TemporalOperationHandler.op, + WorkflowRunStartContextHandler.op, + ): + _, op = get_operation_factory(method) + assert isinstance(op, nexusrpc.Operation) + assert op.input_type == Input + assert op.output_type == Output diff --git a/tests/nexus/test_link_conversion.py b/tests/nexus/test_link_conversion.py index 4170f515b..15fc8d77f 100644 --- a/tests/nexus/test_link_conversion.py +++ b/tests/nexus/test_link_conversion.py @@ -1,6 +1,7 @@ import urllib.parse from typing import Any +import nexusrpc import pytest import temporalio.api.common.v1 @@ -39,8 +40,9 @@ def test_query_params_to_event_reference( query_param_str: str, expected_event_ref: dict[str, Any] ): + query_params = urllib.parse.parse_qs(query_param_str) event_ref = temporalio.nexus._link_conversion._query_params_to_event_reference( - query_param_str + query_params ) for k, v in expected_event_ref.items(): assert getattr(event_ref, k) == v @@ -72,6 +74,343 @@ def test_event_reference_to_query_params( assert query_params == expected_query_params +@pytest.mark.parametrize( + ["query_param_str", "expected_event_ref"], + [ + ( + "eventType=NexusOperationScheduled&referenceType=RequestIdReference&requestID=req-123", + { + "event_type": temporalio.api.enums.v1.EventType.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED, + "request_id": "req-123", + }, + ), + # event ID is optional in query params; we leave it unset in the ref if missing + ( + "eventType=NexusOperationScheduled&referenceType=RequestIdReference", + { + "event_type": temporalio.api.enums.v1.EventType.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED, + "request_id": "", + }, + ), + # Older server sends EVENT_TYPE_CONSTANT_CASE event type name + ( + "eventType=EVENT_TYPE_NEXUS_OPERATION_SCHEDULED&referenceType=RequestIdReference&requestID=req-123", + { + "event_type": temporalio.api.enums.v1.EventType.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED, + "request_id": "req-123", + }, + ), + ], +) +def test_query_params_to_request_id_reference( + query_param_str: str, expected_event_ref: dict[str, Any] +): + query_params = urllib.parse.parse_qs(query_param_str) + event_ref = temporalio.nexus._link_conversion._query_params_to_request_id_reference( + query_params + ) + for k, v in expected_event_ref.items(): + assert getattr(event_ref, k) == v + + +@pytest.mark.parametrize( + ["event_ref", "expected_query_param_str"], + [ + # We always send PascalCase event type names (no EventType prefix) + ( + { + "event_type": temporalio.api.enums.v1.EventType.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED, + "request_id": "req-123", + }, + "eventType=NexusOperationScheduled&referenceType=RequestIdReference&requestID=req-123", + ), + ], +) +def test_request_id_reference_to_query_params( + event_ref: dict[str, Any], expected_query_param_str: str +): + query_params_str = ( + temporalio.nexus._link_conversion._request_id_reference_to_query_params( + temporalio.api.common.v1.Link.WorkflowEvent.RequestIdReference(**event_ref) + ) + ) + query_params = urllib.parse.parse_qs(query_params_str) + expected_query_params = urllib.parse.parse_qs(expected_query_param_str) + assert query_params == expected_query_params + + +@pytest.mark.parametrize( + ["wf_event_link", "expected_link"], + [ + ( + temporalio.api.common.v1.Link( + workflow_event=temporalio.api.common.v1.Link.WorkflowEvent( + namespace="ns", + workflow_id="wid", + run_id="rid", + request_id_ref=temporalio.api.common.v1.Link.WorkflowEvent.RequestIdReference( + event_type=temporalio.api.enums.v1.event_type_pb2.EVENT_TYPE_WORKFLOW_TASK_COMPLETED, + request_id="req-123", + ), + ) + ), + nexusrpc.Link( + type=temporalio.api.common.v1.Link.WorkflowEvent.DESCRIPTOR.full_name, + url="temporal:///namespaces/ns/workflows/wid/rid/history?referenceType=RequestIdReference&requestID=req-123&eventType=WorkflowTaskCompleted", + ), + ), + ( + temporalio.api.common.v1.Link( + workflow_event=temporalio.api.common.v1.Link.WorkflowEvent( + namespace="ns2", + workflow_id="wid2", + run_id="rid2", + event_ref=temporalio.api.common.v1.Link.WorkflowEvent.EventReference( + event_id=42, + event_type=temporalio.api.enums.v1.event_type_pb2.EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED, + ), + ) + ), + nexusrpc.Link( + type=temporalio.api.common.v1.Link.WorkflowEvent.DESCRIPTOR.full_name, + url="temporal:///namespaces/ns2/workflows/wid2/rid2/history?eventID=42&eventType=WorkflowExecutionCompleted&referenceType=EventReference", + ), + ), + ( + temporalio.api.common.v1.Link( + workflow_event=temporalio.api.common.v1.Link.WorkflowEvent( + namespace="ns2", + workflow_id="wid/2", + run_id="rid2", + event_ref=temporalio.api.common.v1.Link.WorkflowEvent.EventReference( + event_id=42, + event_type=temporalio.api.enums.v1.event_type_pb2.EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED, + ), + ) + ), + nexusrpc.Link( + type=temporalio.api.common.v1.Link.WorkflowEvent.DESCRIPTOR.full_name, + url="temporal:///namespaces/ns2/workflows/wid%2F2/rid2/history?eventID=42&eventType=WorkflowExecutionCompleted&referenceType=EventReference", + ), + ), + ], +) +def test_link_conversion_workflow_event_to_link_and_back( + wf_event_link: temporalio.api.common.v1.Link, expected_link: nexusrpc.Link +): + actual_link = temporalio.nexus._link_conversion.workflow_event_to_nexus_link( + wf_event_link.workflow_event + ) + assert expected_link == actual_link + + actual_event = temporalio.nexus._link_conversion.nexus_link_to_workflow_event_link( + actual_link + ) + assert wf_event_link == actual_event + + +@pytest.mark.parametrize( + ["workflow_link", "expected_link"], + [ + ( + temporalio.api.common.v1.Link( + workflow=temporalio.api.common.v1.Link.Workflow( + namespace="ns", + workflow_id="wid", + run_id="rid", + reason="query", + ) + ), + nexusrpc.Link( + type=temporalio.api.common.v1.Link.Workflow.DESCRIPTOR.full_name, + url="temporal:///namespaces/ns/workflows/wid/rid?reason=query", + ), + ), + ( + temporalio.api.common.v1.Link( + workflow=temporalio.api.common.v1.Link.Workflow( + namespace="ns2", + workflow_id="wid/2", + run_id="rid2", + ) + ), + nexusrpc.Link( + type=temporalio.api.common.v1.Link.Workflow.DESCRIPTOR.full_name, + url="temporal:///namespaces/ns2/workflows/wid%2F2/rid2", + ), + ), + ], +) +def test_link_conversion_workflow_to_link_and_back( + workflow_link: temporalio.api.common.v1.Link, expected_link: nexusrpc.Link +): + actual_link = temporalio.nexus._link_conversion.workflow_to_nexus_link( + workflow_link.workflow + ) + assert expected_link == actual_link + + actual_workflow = temporalio.nexus._link_conversion.nexus_link_to_workflow_link( + actual_link + ) + assert workflow_link == actual_workflow + + assert ( + expected_link + == temporalio.nexus._link_conversion.temporal_link_to_nexus_link(workflow_link) + ) + assert ( + workflow_link + == temporalio.nexus._link_conversion.nexus_link_to_temporal_link(expected_link) + ) + + +@pytest.mark.parametrize( + ["operation_link", "expected_link"], + [ + ( + temporalio.api.common.v1.Link( + nexus_operation=temporalio.api.common.v1.Link.NexusOperation( + namespace="ns", + operation_id="op-id", + run_id="run-id", + ) + ), + nexusrpc.Link( + type=temporalio.api.common.v1.Link.NexusOperation.DESCRIPTOR.full_name, + url="temporal:///namespaces/ns/nexus-operations/op-id/run-id/details", + ), + ), + ( + temporalio.api.common.v1.Link( + nexus_operation=temporalio.api.common.v1.Link.NexusOperation( + namespace="ns", + operation_id="op-id", + ) + ), + nexusrpc.Link( + type=temporalio.api.common.v1.Link.NexusOperation.DESCRIPTOR.full_name, + url="temporal:///namespaces/ns/nexus-operations/op-id//details", + ), + ), + ( + temporalio.api.common.v1.Link( + nexus_operation=temporalio.api.common.v1.Link.NexusOperation( + namespace="ns", + operation_id="op/id", + ) + ), + nexusrpc.Link( + type=temporalio.api.common.v1.Link.NexusOperation.DESCRIPTOR.full_name, + url="temporal:///namespaces/ns/nexus-operations/op%2Fid//details", + ), + ), + ( + temporalio.api.common.v1.Link( + nexus_operation=temporalio.api.common.v1.Link.NexusOperation( + namespace="ns", + operation_id="op/id", + run_id="run/id", + ) + ), + nexusrpc.Link( + type=temporalio.api.common.v1.Link.NexusOperation.DESCRIPTOR.full_name, + url="temporal:///namespaces/ns/nexus-operations/op%2Fid/run%2Fid/details", + ), + ), + ], +) +def test_link_conversion_nexus_operation_to_link_and_back( + operation_link: temporalio.api.common.v1.Link, + expected_link: nexusrpc.Link, +): + actual_link = temporalio.nexus._link_conversion.nexus_operation_to_nexus_link( + operation_link.nexus_operation + ) + assert expected_link == actual_link + + actual_operation = ( + temporalio.nexus._link_conversion.nexus_link_to_nexus_operation_link( + actual_link + ) + ) + assert operation_link == actual_operation + + assert ( + expected_link + == temporalio.nexus._link_conversion.temporal_link_to_nexus_link(operation_link) + ) + assert ( + operation_link + == temporalio.nexus._link_conversion.nexus_link_to_temporal_link(expected_link) + ) + + +def test_nexus_operation_link_with_unparseable_url_is_ignored(): + # The canonical path is /nexus-operations/{op_id}/{run_id}/details; a URL missing the + # run-id/details suffix (e.g. the legacy ?runID= form) does not parse. + link = nexusrpc.Link( + type=temporalio.api.common.v1.Link.NexusOperation.DESCRIPTOR.full_name, + url="temporal:///namespaces/ns/nexus-operations/op-id?runID=run-id", + ) + assert temporalio.nexus._link_conversion.nexus_link_to_temporal_link(link) is None + + +@pytest.mark.parametrize( + ["link", "expected_link"], + [ + ( + nexusrpc.Link( + type=temporalio.api.common.v1.Link.Activity.DESCRIPTOR.full_name, + url="temporal:///namespaces/ns/activities/act-id/run-id/details", + ), + temporalio.api.common.v1.Link( + activity=temporalio.api.common.v1.Link.Activity( + namespace="ns", + activity_id="act-id", + run_id="run-id", + ), + ), + ), + ( + nexusrpc.Link( + type=temporalio.api.common.v1.Link.Activity.DESCRIPTOR.full_name, + url="temporal:///namespaces/ns%2F/activities/act-id%2F/run-id%3E/details", + ), + temporalio.api.common.v1.Link( + activity=temporalio.api.common.v1.Link.Activity( + namespace="ns/", + activity_id="act-id/", + run_id="run-id>", + ), + ), + ), + ], +) +def test_link_conversion_nexus_link_to_activity_link( + link: nexusrpc.Link, + expected_link: temporalio.api.common.v1.Link, +): + from_activity_link = temporalio.nexus._link_conversion.activity_link_to_nexus_link( + expected_link.activity + ) + assert link == from_activity_link + + from_temporal_link = temporalio.nexus._link_conversion.temporal_link_to_nexus_link( + expected_link + ) + assert link == from_temporal_link + + actual_activity = temporalio.nexus._link_conversion.nexus_link_to_activity_link( + link + ) + assert expected_link == actual_activity + + actual_temporal_link = ( + temporalio.nexus._link_conversion.nexus_link_to_temporal_link(link) + ) + assert expected_link == actual_temporal_link + + def test_link_conversion_utilities(): p2c = temporalio.nexus._link_conversion._event_type_pascal_case_to_constant_case c2p = temporalio.nexus._link_conversion._event_type_constant_case_to_pascal_case diff --git a/tests/nexus/test_link_propagation.py b/tests/nexus/test_link_propagation.py new file mode 100644 index 000000000..a7b78426c --- /dev/null +++ b/tests/nexus/test_link_propagation.py @@ -0,0 +1,642 @@ +"""Unit tests for Nexus link propagation. + +These exercise link propagation when a Nexus operation handler signals or starts a +workflow or activity against a mocked workflow service. End-to-end signal backlinks +require a server with EnableCHASMSignalBacklinks enabled and are not covered here. +""" + +from __future__ import annotations + +from collections.abc import Generator +from datetime import timedelta +from typing import Any +from unittest import mock + +import nexusrpc +import nexusrpc.handler +import pytest +from nexusrpc.handler import ( + OperationHandler, + StartOperationContext, + StartOperationResultAsync, + service_handler, + sync_operation, +) +from nexusrpc.handler._decorators import operation_handler + +import temporalio.api.common.v1 +import temporalio.api.enums.v1 +import temporalio.api.nexus.v1 +import temporalio.api.workflowservice.v1 +import temporalio.common +import temporalio.converter +import temporalio.nexus._link_conversion +import temporalio.nexus._operation_context +import temporalio.nexus._token +from temporalio.client._impl import _ClientImpl +from temporalio.client._interceptor import ( + SignalWorkflowInput, + StartActivityInput, + StartWorkflowInput, +) +from temporalio.nexus._operation_context import _TemporalStartOperationContext +from temporalio.worker._nexus import _NexusTaskCancellation, _NexusWorker + +NAMESPACE = "test-namespace" +WORKFLOW_ID = "wf-target" + + +def _workflow_event_link( + workflow_id: str, + run_id: str, + event_type: temporalio.api.enums.v1.EventType.ValueType, +) -> temporalio.api.common.v1.Link: + return temporalio.api.common.v1.Link( + workflow_event=temporalio.api.common.v1.Link.WorkflowEvent( + namespace=NAMESPACE, + workflow_id=workflow_id, + run_id=run_id, + event_ref=temporalio.api.common.v1.Link.WorkflowEvent.EventReference( + event_type=event_type, + ), + ) + ) + + +def _inbound_nexus_link() -> temporalio.api.common.v1.Link: + return _workflow_event_link( + "caller-wf", + "caller-run", + temporalio.api.enums.v1.EventType.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED, + ) + + +@pytest.fixture +def nexus_ctx() -> Generator[_TemporalStartOperationContext]: + """Install a Nexus start-operation context with a single inbound link. + + The inbound link is provided in nexusrpc.Link form, exactly as the worker populates it from + the inbound Nexus task. Yields the temporal context so tests can inspect outbound_links. + """ + inbound = temporalio.nexus._link_conversion.workflow_event_to_nexus_link( + _inbound_nexus_link().workflow_event + ) + nexus_context = nexusrpc.handler.StartOperationContext( + service="svc", + operation="op", + headers={}, + request_id="req-id", + callback_url="https://callback.example", + inbound_links=[inbound], + callback_headers={}, + task_cancellation=_NexusTaskCancellation(), + ) + ctx = temporalio.nexus._operation_context._TemporalStartOperationContext( + nexus_context=nexus_context, + client=mock.MagicMock(namespace=NAMESPACE), + info=lambda: temporalio.nexus.Info( + endpoint="endpoint", namespace=NAMESPACE, task_queue="tq" + ), + _runtime_metric_meter=mock.MagicMock(), + _worker_shutdown_event=mock.MagicMock(), + ) + token = temporalio.nexus._operation_context._temporal_start_operation_context.set( + ctx + ) + try: + yield ctx + finally: + temporalio.nexus._operation_context._temporal_start_operation_context.reset( + token + ) + + +def _make_client_impl(workflow_service: Any) -> _ClientImpl: + client = mock.MagicMock() + client.namespace = NAMESPACE + client.identity = "test-identity" + client.workflow_service = workflow_service + client.data_converter = temporalio.converter.DataConverter.default + return _ClientImpl(client) + + +def _signal_input() -> SignalWorkflowInput: + return SignalWorkflowInput( + id=WORKFLOW_ID, + run_id=None, + signal="test-signal", + args=[], + headers={}, + rpc_metadata={}, + rpc_timeout=None, + ) + + +def _start_input(start_signal: str | None = None) -> StartWorkflowInput: + return StartWorkflowInput( + workflow="TestWorkflow", + args=[], + id=WORKFLOW_ID, + task_queue="tq", + execution_timeout=None, + run_timeout=None, + task_timeout=None, + id_reuse_policy=temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy=temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, + retry_policy=None, + cron_schedule="", + memo=None, + search_attributes=None, + start_delay=None, + headers={}, + start_signal=start_signal, + start_signal_args=[], + static_summary=None, + static_details=None, + ret_type=None, + rpc_metadata={}, + rpc_timeout=None, + request_eager_start=False, + priority=temporalio.common.Priority.default, + callbacks=[], + links=[], + request_id=None, + versioning_override=None, + ) + + +def _start_activity_input() -> StartActivityInput: + return StartActivityInput( + activity_type="TestActivity", + args=[], + id="activity-target", + task_queue="tq", + result_type=None, + schedule_to_close_timeout=None, + start_to_close_timeout=timedelta(seconds=10), + schedule_to_start_timeout=None, + heartbeat_timeout=None, + id_reuse_policy=temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy=temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy=None, + priority=temporalio.common.Priority.default, + search_attributes=None, + summary=None, + start_delay=None, + headers={}, + rpc_metadata={}, + rpc_timeout=None, + ) + + +def _outbound_link_urls(ctx: Any) -> list[str]: + return [link.url for link in ctx.nexus_context.outbound_links] + + +# ── signal ──────────────────────────────────────────────────────────────────────────────── + + +async def test_signal_forwards_inbound_links_and_captures_response_backlink( + nexus_ctx: _TemporalStartOperationContext, +) -> None: + response_link = _workflow_event_link( + WORKFLOW_ID, + "target-run", + temporalio.api.enums.v1.EventType.EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED, + ) + workflow_service = mock.MagicMock() + workflow_service.signal_workflow_execution = mock.AsyncMock( + return_value=temporalio.api.workflowservice.v1.SignalWorkflowExecutionResponse( + link=response_link + ) + ) + impl = _make_client_impl(workflow_service) + + await impl.signal_workflow(_signal_input()) + + # Forward: the request carries the single inbound link. + sent = workflow_service.signal_workflow_execution.call_args.args[0] + assert len(sent.links) == 1 + assert sent.links[0] == _inbound_nexus_link() + + # Backward: the response link is added to the operation's outbound links (as a Nexus link). + assert len(nexus_ctx.nexus_context.outbound_links) == 1 + assert "wf-target" in _outbound_link_urls(nexus_ctx)[0] + + +async def test_signal_against_older_server_captures_no_backlink( + nexus_ctx: _TemporalStartOperationContext, +) -> None: + workflow_service = mock.MagicMock() + workflow_service.signal_workflow_execution = mock.AsyncMock( + return_value=temporalio.api.workflowservice.v1.SignalWorkflowExecutionResponse() + ) + impl = _make_client_impl(workflow_service) + + await impl.signal_workflow(_signal_input()) + + # Forward direction still works regardless of server version. + sent = workflow_service.signal_workflow_execution.call_args.args[0] + assert len(sent.links) == 1 + + # Backward: no backlink because the server returned no link. + assert nexus_ctx.nexus_context.outbound_links == [] + + +async def test_multiple_signals_accumulate_all_backlinks( + nexus_ctx: _TemporalStartOperationContext, +) -> None: + first = _workflow_event_link( + "callee-a", + "run-a", + temporalio.api.enums.v1.EventType.EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED, + ) + second = _workflow_event_link( + "callee-b", + "run-b", + temporalio.api.enums.v1.EventType.EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED, + ) + workflow_service = mock.MagicMock() + workflow_service.signal_workflow_execution = mock.AsyncMock( + side_effect=[ + temporalio.api.workflowservice.v1.SignalWorkflowExecutionResponse( + link=first + ), + temporalio.api.workflowservice.v1.SignalWorkflowExecutionResponse( + link=second + ), + ] + ) + impl = _make_client_impl(workflow_service) + + await impl.signal_workflow(_signal_input()) + await impl.signal_workflow(_signal_input()) + + urls = _outbound_link_urls(nexus_ctx) + assert len(urls) == 2 + assert "callee-a" in urls[0] + assert "callee-b" in urls[1] + + +async def test_signal_outside_nexus_context_does_not_touch_links() -> None: + workflow_service = mock.MagicMock() + workflow_service.signal_workflow_execution = mock.AsyncMock( + return_value=temporalio.api.workflowservice.v1.SignalWorkflowExecutionResponse() + ) + impl = _make_client_impl(workflow_service) + + await impl.signal_workflow(_signal_input()) + + sent = workflow_service.signal_workflow_execution.call_args.args[0] + assert len(sent.links) == 0 + + +# ── signal-with-start ─────────────────────────────────────────────────────────────────────── + + +async def test_signal_with_start_forwards_inbound_links_and_captures_backlink( + nexus_ctx: _TemporalStartOperationContext, +) -> None: + response_link = _workflow_event_link( + WORKFLOW_ID, + "target-run", + temporalio.api.enums.v1.EventType.EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED, + ) + workflow_service = mock.MagicMock() + workflow_service.signal_with_start_workflow_execution = mock.AsyncMock( + return_value=temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse( + run_id="target-run", + signal_link=response_link, + ) + ) + impl = _make_client_impl(workflow_service) + + await impl.start_workflow(_start_input(start_signal="test-signal")) + + # Forward: the SignalWithStart request carries the inbound link. + sent = workflow_service.signal_with_start_workflow_execution.call_args.args[0] + assert len(sent.links) == 1 + assert sent.links[0] == _inbound_nexus_link() + + # Backward: response.signal_link is captured as an outbound Nexus link. + assert len(nexus_ctx.nexus_context.outbound_links) == 1 + assert "wf-target" in _outbound_link_urls(nexus_ctx)[0] + + +async def test_signal_with_start_against_older_server_captures_no_backlink( + nexus_ctx: _TemporalStartOperationContext, +) -> None: + workflow_service = mock.MagicMock() + workflow_service.signal_with_start_workflow_execution = mock.AsyncMock( + return_value=temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse( + run_id="target-run", + ) + ) + impl = _make_client_impl(workflow_service) + + await impl.start_workflow(_start_input(start_signal="test-signal")) + + sent = workflow_service.signal_with_start_workflow_execution.call_args.args[0] + assert len(sent.links) == 1 + assert nexus_ctx.nexus_context.outbound_links == [] + + +# ── start ───────────────────────────────────────────────────────────────────────────────── + + +async def test_start_forwards_inbound_links_and_captures_backlink( + nexus_ctx: _TemporalStartOperationContext, +) -> None: + server_link = _workflow_event_link( + WORKFLOW_ID, + "target-run", + temporalio.api.enums.v1.EventType.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED, + ) + workflow_service = mock.MagicMock() + workflow_service.start_workflow_execution = mock.AsyncMock( + return_value=temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse( + run_id="target-run", + link=server_link, + ) + ) + impl = _make_client_impl(workflow_service) + + await impl.start_workflow(_start_input()) + + # Forward: the start request carries the single inbound link. + sent = workflow_service.start_workflow_execution.call_args.args[0] + assert len(sent.links) == 1 + assert sent.links[0] == _inbound_nexus_link() + + # Backward: a plain start captures a backlink + assert len(nexus_ctx.nexus_context.outbound_links) == 1 + assert "wf-target" in _outbound_link_urls(nexus_ctx)[0] + + +async def test_start_against_older_server_captures_no_backlink( + nexus_ctx: _TemporalStartOperationContext, +) -> None: + workflow_service = mock.MagicMock() + workflow_service.start_workflow_execution = mock.AsyncMock( + return_value=temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse( + run_id="target-run", + ) + ) + impl = _make_client_impl(workflow_service) + + await impl.start_workflow(_start_input()) + + # Forward direction still works regardless of server version. + sent = workflow_service.start_workflow_execution.call_args.args[0] + assert len(sent.links) == 1 + assert sent.links[0] == _inbound_nexus_link() + + # Backward: a plain start fabricates a backlink when the server doesn't return one. + assert len(nexus_ctx.nexus_context.outbound_links) == 1 + assert "wf-target" in _outbound_link_urls(nexus_ctx)[0] + + +async def test_start_outside_nexus_context_does_not_touch_links() -> None: + workflow_service = mock.MagicMock() + workflow_service.start_workflow_execution = mock.AsyncMock( + return_value=temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse( + run_id="target-run", + ) + ) + impl = _make_client_impl(workflow_service) + + # Should not raise even though there is no Nexus context. + handle = await impl.start_workflow(_start_input()) + assert handle.result_run_id == "target-run" + sent = workflow_service.start_workflow_execution.call_args.args[0] + assert len(sent.links) == 0 + + +# ── start: on-conflict options ────────────────────────────────────────────────────────────── +# +# A start issued from inside a Nexus operation handler enables all on_conflict_options so that, +# under a WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING policy, a detected conflict attaches the +# request id, completion callbacks, and links to the existing run. + + +def _start_response() -> ( + temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse +): + return temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse( + run_id="target-run", + ) + + +@pytest.mark.usefixtures("nexus_ctx") +async def test_start_from_nexus_context_sets_all_on_conflict_options() -> None: + workflow_service = mock.MagicMock() + workflow_service.start_workflow_execution = mock.AsyncMock( + return_value=_start_response() + ) + impl = _make_client_impl(workflow_service) + + await impl.start_workflow(_start_input()) + + sent = workflow_service.start_workflow_execution.call_args.args[0] + assert sent.HasField("on_conflict_options") + assert sent.on_conflict_options.attach_request_id + assert sent.on_conflict_options.attach_completion_callbacks + assert sent.on_conflict_options.attach_links + + +@pytest.mark.usefixtures("nexus_ctx") +async def test_backing_workflow_start_sets_on_conflict_options_without_duplicating_links() -> ( + None +): + workflow_service = mock.MagicMock() + workflow_service.start_workflow_execution = mock.AsyncMock( + return_value=_start_response() + ) + impl = _make_client_impl(workflow_service) + + # The nexus-backing workflow carries its inbound links via input.links (start_workflow + # forwards them as links=), so the build path must enable on_conflict_options but must not + # also re-add the context's request links. + start_input = _start_input() + start_input.links = [_inbound_nexus_link()] + with temporalio.nexus._operation_context._nexus_backing_start_context(): + await impl.start_workflow(start_input) + + sent = workflow_service.start_workflow_execution.call_args.args[0] + assert sent.HasField("on_conflict_options") + assert sent.on_conflict_options.attach_request_id + assert sent.on_conflict_options.attach_completion_callbacks + assert sent.on_conflict_options.attach_links + # The single inbound link appears exactly once, not duplicated. + assert len(sent.links) == 1 + assert sent.links[0] == _inbound_nexus_link() + + +async def test_start_outside_nexus_context_leaves_on_conflict_options_unset() -> None: + workflow_service = mock.MagicMock() + workflow_service.start_workflow_execution = mock.AsyncMock( + return_value=_start_response() + ) + impl = _make_client_impl(workflow_service) + + await impl.start_workflow(_start_input()) + + sent = workflow_service.start_workflow_execution.call_args.args[0] + assert not sent.HasField("on_conflict_options") + + +# ── activity start ────────────────────────────────────────────────────────────────────────── + + +@pytest.mark.usefixtures("nexus_ctx") +async def test_activity_start_forwards_inbound_links() -> None: + impl = _make_client_impl(mock.MagicMock()) + + req = await impl._build_start_activity_execution_request(_start_activity_input()) + + assert len(req.links) == 1 + assert req.links[0] == _inbound_nexus_link() + assert req.request_id == "req-id" + assert len(req.completion_callbacks) == 0 + + +async def test_activity_start_without_server_link_synthesizes_backlink( + nexus_ctx: _TemporalStartOperationContext, +) -> None: + workflow_service = mock.MagicMock() + workflow_service.start_activity_execution = mock.AsyncMock( + return_value=temporalio.api.workflowservice.v1.StartActivityExecutionResponse( + run_id="activity-run", + ) + ) + impl = _make_client_impl(workflow_service) + + await impl.start_activity(_start_activity_input()) + + assert len(nexus_ctx.nexus_context.outbound_links) == 1 + link = nexus_ctx.nexus_context.outbound_links[0] + assert link.url == ( + "temporal:///namespaces/test-namespace/" + "activities/activity-target/activity-run/details" + ) + assert link.type == temporalio.api.common.v1.Link.Activity.DESCRIPTOR.full_name + + +@pytest.mark.usefixtures("nexus_ctx") +async def test_backing_activity_start_gets_nexus_request_fields() -> None: + impl = _make_client_impl(mock.MagicMock()) + + with temporalio.nexus._operation_context._nexus_backing_start_context(): + req = await impl._build_start_activity_execution_request( + _start_activity_input() + ) + + assert len(req.links) == 0 + assert req.request_id == "req-id" + assert len(req.completion_callbacks) == 1 + operation_token = temporalio.nexus._token.OperationToken.decode( + req.completion_callbacks[0].nexus.header["nexus-operation-token"] + ) + assert operation_token.type is temporalio.nexus._token.OperationTokenType.ACTIVITY + assert operation_token.namespace == NAMESPACE + assert operation_token.activity_id == "activity-target" + assert list(req.completion_callbacks[0].links) == [_inbound_nexus_link()] + + +# ── handler-level: backlinks land on the StartOperationResponse ────────────────────────────── + +# A response link that a handler stashes on ctx.outbound_links, mimicking what a signal RPC inside +# the handler would do via _add_response_link. +_BACKLINK = temporalio.nexus._link_conversion.workflow_event_to_nexus_link( + temporalio.api.common.v1.Link.WorkflowEvent( + namespace=NAMESPACE, + workflow_id="callee-wf", + run_id="callee-run-id", + event_ref=temporalio.api.common.v1.Link.WorkflowEvent.EventReference( + event_type=temporalio.api.enums.v1.EventType.EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED, + ), + ) +) + + +class _AsyncBacklinkOperation(OperationHandler): + """Stashes a backlink then returns an async result, simulating a signaling handler.""" + + async def start( + self, ctx: StartOperationContext, input: str + ) -> StartOperationResultAsync: + ctx.outbound_links.append(_BACKLINK) + return StartOperationResultAsync(token=input) + + async def cancel(self, ctx: Any, token: str) -> None: ... + + +@service_handler +class _BacklinkStashingService: + @sync_operation + async def sync_op(self, ctx: StartOperationContext, _input: str) -> str: + # Stash a backlink and return a sync result. + ctx.outbound_links.append(_BACKLINK) + return "result" + + @operation_handler + def async_op(self) -> OperationHandler[str, str]: + return _AsyncBacklinkOperation() + + +def _make_nexus_worker() -> _NexusWorker: + return _NexusWorker( + bridge_worker=lambda: mock.MagicMock(), + client=mock.MagicMock(namespace=NAMESPACE), + namespace=NAMESPACE, + task_queue="tq", + service_handlers=[_BacklinkStashingService()], + data_converter=temporalio.converter.DataConverter.default, + interceptors=[], + metric_meter=mock.MagicMock(), + executor=None, + ) + + +def _start_request( + operation: str, input: str +) -> temporalio.api.nexus.v1.StartOperationRequest: + [payload] = ( + temporalio.converter.DataConverter.default.payload_converter.to_payloads( + [input] + ) + ) + return temporalio.api.nexus.v1.StartOperationRequest( + service="_BacklinkStashingService", + operation=operation, + payload=payload, + ) + + +async def test_sync_response_includes_signal_backlinks() -> None: + worker = _make_nexus_worker() + response = await worker._start_operation( + _start_request("sync_op", "input"), + headers={}, + cancellation=_NexusTaskCancellation(), + request_deadline=None, + endpoint="endpoint", + ) + assert response.HasField("sync_success") + assert len(response.sync_success.links) == 1 + assert "callee-wf" in response.sync_success.links[0].url + + +async def test_async_response_includes_signal_backlinks() -> None: + worker = _make_nexus_worker() + response = await worker._start_operation( + _start_request("async_op", "op-token"), + headers={}, + cancellation=_NexusTaskCancellation(), + request_deadline=None, + endpoint="endpoint", + ) + assert response.HasField("async_success") + assert response.async_success.operation_token == "op-token" + assert len(response.async_success.links) == 1 + assert "callee-wf" in response.async_success.links[0].url diff --git a/tests/nexus/test_nexus_client_updates.py b/tests/nexus/test_nexus_client_updates.py new file mode 100644 index 000000000..97dd251da --- /dev/null +++ b/tests/nexus/test_nexus_client_updates.py @@ -0,0 +1,108 @@ +"""Tests for Nexus worker client updates.""" + +import uuid + +import nexusrpc +import pytest +from nexusrpc.handler import StartOperationContext, service_handler, sync_operation + +import temporalio.nexus +from temporalio import workflow +from temporalio.client import Client +from temporalio.testing import WorkflowEnvironment +from temporalio.worker import Worker + +# Cloud CI's namespace credentials cannot manage Nexus endpoints. +# See https://github.com/temporalio/sdk-python/issues/1704. +pytestmark = pytest.mark.requires_local_server + + +@nexusrpc.service +class ClientTestService: + capture_client: nexusrpc.Operation[None, str] + + +captured_clients: list[Client] = [] + + +@service_handler(service=ClientTestService) +class ClientTestServiceHandler: + @sync_operation + async def capture_client(self, _ctx: StartOperationContext, _input: None) -> str: + captured_clients.append(temporalio.nexus.client()) + return "done" + + +@workflow.defn +class ClientTestCallerWorkflow: + @workflow.run + async def run(self, endpoint_name: str) -> str: + nexus_client = workflow.create_nexus_client( + service=ClientTestService, + endpoint=endpoint_name, + ) + return await nexus_client.execute_operation( + ClientTestService.capture_client, + None, + ) + + +async def test_nexus_client_updates_when_worker_client_changes( + env: WorkflowEnvironment, +): + """Test that Nexus operations get the updated client when worker.client is changed.""" + # Create a second client (simulating a new client after cert rotation) + # Must use the same runtime + client2 = await env.connect_client( + data_converter=env.client.data_converter, + runtime=env.client.service_client.config.runtime, + ) + + # Clear any previous captures + captured_clients.clear() + + caller_task_queue = f"caller-{uuid.uuid4()}" + handler_task_queue = f"handler-{uuid.uuid4()}" + + # Create Nexus endpoint + endpoint_name = f"test-endpoint-{uuid.uuid4()}" + await env.create_nexus_endpoint(endpoint_name, handler_task_queue) + + # Caller worker + caller_worker = Worker( + env.client, + task_queue=caller_task_queue, + workflows=[ClientTestCallerWorkflow], + ) + + # Handler worker + handler_worker = Worker( + env.client, + task_queue=handler_task_queue, + nexus_service_handlers=[ClientTestServiceHandler()], + ) + + async with caller_worker, handler_worker: + # Execute operation with original client + await env.client.execute_workflow( + ClientTestCallerWorkflow.run, + endpoint_name, + id=f"wf-{uuid.uuid4()}", + task_queue=caller_task_queue, + ) + + # Update handler worker's client + handler_worker.client = client2 + + # Execute operation again - should get the new client + await client2.execute_workflow( + ClientTestCallerWorkflow.run, + endpoint_name, + id=f"wf-{uuid.uuid4()}", + task_queue=caller_task_queue, + ) + + # Should have captured both clients + assert len(captured_clients) == 2 + assert captured_clients[0] is env.client + assert captured_clients[1] is client2 # This will fail before the fix diff --git a/tests/nexus/test_nexus_type_errors.py b/tests/nexus/test_nexus_type_errors.py new file mode 100644 index 000000000..946e34035 --- /dev/null +++ b/tests/nexus/test_nexus_type_errors.py @@ -0,0 +1,916 @@ +""" +This file exists to test for type-checker false positives and false negatives. +It doesn't contain any test functions. +""" + +from dataclasses import dataclass +from datetime import timedelta +from typing import Any, TypeAlias +from unittest.mock import Mock + +import nexusrpc + +import temporalio.nexus +from temporalio import activity, workflow +from temporalio.client import Client, NexusOperationHandle +from temporalio.nexus import TemporalOperationStartHandlerFunc +from temporalio.service import ServiceClient + + +@dataclass +class MyInput: + pass + + +@dataclass +class MyOutput: + pass + + +@workflow.defn +class MyNoArgProcWorkflow: + @workflow.run + async def run(self) -> None: + pass + + +@workflow.defn +class MyOneArgProcWorkflow: + @workflow.run + async def run(self, _input: MyInput) -> None: + pass + + +@workflow.defn +class MyTwoArgProcWorkflow: + @workflow.run + async def run(self, _input: MyInput, _arg2: int) -> None: + pass + + +@workflow.defn +class MyThreeArgProcWorkflow: + @workflow.run + async def run(self, _input: MyInput, _arg2: int, _arg3: int) -> None: + pass + + +@workflow.defn +class MyFourArgProcWorkflow: + @workflow.run + async def run(self, _input: MyInput, _arg2: int, _arg3: int, _arg4: int) -> None: + pass + + +@workflow.defn +class MyFiveArgProcWorkflow: + @workflow.run + async def run( + self, _input: MyInput, _arg2: int, _arg3: int, _arg4: int, _arg5: int + ) -> None: + pass + + +@activity.defn +async def my_no_arg_activity() -> None: + pass + + +@activity.defn +async def my_one_arg_activity(_input: MyInput) -> None: + pass + + +@activity.defn +async def my_two_arg_activity(_input: MyInput, _arg2: int) -> None: + pass + + +@activity.defn +async def my_three_arg_activity(_input: MyInput, _arg2: int, _arg3: int) -> None: + pass + + +@activity.defn +async def my_four_arg_activity( + _input: MyInput, _arg2: int, _arg3: int, _arg4: int +) -> None: + pass + + +@activity.defn +async def my_five_arg_activity( + _input: MyInput, _arg2: int, _arg3: int, _arg4: int, _arg5: int +) -> None: + pass + + +@activity.defn +def my_sync_no_arg_activity() -> None: + pass + + +@activity.defn +def my_sync_one_arg_activity(_input: MyInput) -> None: + pass + + +@activity.defn +def my_sync_two_arg_activity(_input: MyInput, _arg2: int) -> None: + pass + + +@activity.defn +def my_sync_three_arg_activity(_input: MyInput, _arg2: int, _arg3: int) -> None: + pass + + +@activity.defn +def my_sync_four_arg_activity( + _input: MyInput, _arg2: int, _arg3: int, _arg4: int +) -> None: + pass + + +@activity.defn +def my_sync_five_arg_activity( + _input: MyInput, _arg2: int, _arg3: int, _arg4: int, _arg5: int +) -> None: + pass + + +@nexusrpc.service +class MyService: + my_sync_operation: nexusrpc.Operation[MyInput, MyOutput] + my_workflow_run_operation: nexusrpc.Operation[MyInput, MyOutput] + my_temporal_operation: nexusrpc.Operation[int, None] + + +@nexusrpc.service +class MyNoInputService: + my_no_input_operation: nexusrpc.Operation[None, MyOutput] + + +@nexusrpc.handler.service_handler(service=MyService) +class MyServiceHandler: + @nexusrpc.handler.sync_operation + async def my_sync_operation( + self, _ctx: nexusrpc.handler.StartOperationContext, _input: MyInput + ) -> MyOutput: + raise NotImplementedError + + @temporalio.nexus.workflow_run_operation + async def my_workflow_run_operation( + self, _ctx: temporalio.nexus.WorkflowRunOperationContext, _input: MyInput + ) -> temporalio.nexus.WorkflowHandle[MyOutput]: + raise NotImplementedError + + @temporalio.nexus.temporal_operation + async def my_temporal_operation( + self, + _ctx: temporalio.nexus.TemporalStartOperationContext, + client: temporalio.nexus.TemporalNexusClient, + input: int, + ) -> temporalio.nexus.TemporalOperationResult[None]: + """ + Typed proc workflow and activity starts from a generic Temporal Nexus + operation handler infer TemporalOperationResult[None] for 0 to 5 + workflow or activity parameters. + """ + if input == 0: + result_0: temporalio.nexus.TemporalOperationResult[ + None + ] = await client.start_workflow(MyNoArgProcWorkflow.run, id="proc-0") + return result_0 + if input == 1: + result_1: temporalio.nexus.TemporalOperationResult[ + None + ] = await client.start_workflow( + MyOneArgProcWorkflow.run, MyInput(), id="proc-1" + ) + return result_1 + if input == 2: + result_2: temporalio.nexus.TemporalOperationResult[ + None + ] = await client.start_workflow( + MyTwoArgProcWorkflow.run, args=[MyInput(), 2], id="proc-2" + ) + return result_2 + if input == 3: + result_3: temporalio.nexus.TemporalOperationResult[ + None + ] = await client.start_workflow( + MyThreeArgProcWorkflow.run, + args=[MyInput(), 2, 3], + id="proc-3", + ) + return result_3 + if input == 4: + result_4: temporalio.nexus.TemporalOperationResult[ + None + ] = await client.start_workflow( + MyFourArgProcWorkflow.run, + args=[MyInput(), 2, 3, 4], + id="proc-4", + ) + return result_4 + if input == 5: + result_5: temporalio.nexus.TemporalOperationResult[ + None + ] = await client.start_workflow( + MyFiveArgProcWorkflow.run, + args=[MyInput(), 2, 3, 4, 5], + id="proc-5", + ) + return result_5 + + # Typed activity starts infer TemporalOperationResult[None] for 0 to 5 + # activity parameters. Activities require a start_to_close_timeout. + if input == 6: + activity_result_0: temporalio.nexus.TemporalOperationResult[ + None + ] = await client.start_activity( + my_no_arg_activity, + id="activity-0", + start_to_close_timeout=timedelta(seconds=5), + ) + return activity_result_0 + if input == 7: + activity_result_1: temporalio.nexus.TemporalOperationResult[ + None + ] = await client.start_activity( + my_one_arg_activity, + MyInput(), + id="activity-1", + start_to_close_timeout=timedelta(seconds=5), + ) + return activity_result_1 + if input == 8: + activity_result_2: temporalio.nexus.TemporalOperationResult[ + None + ] = await client.start_activity( + my_two_arg_activity, + args=[MyInput(), 2], + id="activity-2", + start_to_close_timeout=timedelta(seconds=5), + ) + return activity_result_2 + if input == 9: + activity_result_3: temporalio.nexus.TemporalOperationResult[ + None + ] = await client.start_activity( + my_three_arg_activity, + args=[MyInput(), 2, 3], + id="activity-3", + start_to_close_timeout=timedelta(seconds=5), + ) + return activity_result_3 + if input == 10: + activity_result_4: temporalio.nexus.TemporalOperationResult[ + None + ] = await client.start_activity( + my_four_arg_activity, + args=[MyInput(), 2, 3, 4], + id="activity-4", + start_to_close_timeout=timedelta(seconds=5), + ) + return activity_result_4 + if input == 11: + activity_result_5: temporalio.nexus.TemporalOperationResult[ + None + ] = await client.start_activity( + my_five_arg_activity, + args=[MyInput(), 2, 3, 4, 5], + id="activity-5", + start_to_close_timeout=timedelta(seconds=5), + ) + return activity_result_5 + if input == 12: + sync_activity_result_0: temporalio.nexus.TemporalOperationResult[ + None + ] = await client.start_activity( + my_sync_no_arg_activity, + id="sync-activity-0", + start_to_close_timeout=timedelta(seconds=5), + ) + return sync_activity_result_0 + if input == 13: + sync_activity_result_1: temporalio.nexus.TemporalOperationResult[ + None + ] = await client.start_activity( + my_sync_one_arg_activity, + MyInput(), + id="sync-activity-1", + start_to_close_timeout=timedelta(seconds=5), + ) + return sync_activity_result_1 + if input == 14: + sync_activity_result_2: temporalio.nexus.TemporalOperationResult[ + None + ] = await client.start_activity( + my_sync_two_arg_activity, + args=[MyInput(), 2], + id="sync-activity-2", + start_to_close_timeout=timedelta(seconds=5), + ) + return sync_activity_result_2 + if input == 15: + sync_activity_result_3: temporalio.nexus.TemporalOperationResult[ + None + ] = await client.start_activity( + my_sync_three_arg_activity, + args=[MyInput(), 2, 3], + id="sync-activity-3", + start_to_close_timeout=timedelta(seconds=5), + ) + return sync_activity_result_3 + if input == 16: + sync_activity_result_4: temporalio.nexus.TemporalOperationResult[ + None + ] = await client.start_activity( + my_sync_four_arg_activity, + args=[MyInput(), 2, 3, 4], + id="sync-activity-4", + start_to_close_timeout=timedelta(seconds=5), + ) + return sync_activity_result_4 + if input == 17: + sync_activity_result_5: temporalio.nexus.TemporalOperationResult[ + None + ] = await client.start_activity( + my_sync_five_arg_activity, + args=[MyInput(), 2, 3, 4, 5], + id="sync-activity-5", + start_to_close_timeout=timedelta(seconds=5), + ) + return sync_activity_result_5 + if input == 18: + string_activity_result: temporalio.nexus.TemporalOperationResult[ + None + ] = await client.start_activity( + "string-activity", + id="string-activity", + result_type=type(None), + start_to_close_timeout=timedelta(seconds=5), + ) + return string_activity_result + if input == 19: + # assert-type-error-pyright: 'No overloads for "start_activity" match' + return await client.start_activity( # type: ignore + my_one_arg_activity, + # assert-type-error-pyright: 'Argument of type .+ cannot be assigned to parameter' + "wrong-input-type", # type: ignore + id="activity-wrong-input", + start_to_close_timeout=timedelta(seconds=5), + ) + + # assert-type-error-pyright: 'No overloads for "start_workflow" match' + return await client.start_workflow( # type: ignore + MyOneArgProcWorkflow.run, + # assert-type-error-pyright: 'Argument of type .+ cannot be assigned to parameter' + "wrong-input-type", # type: ignore + id="proc-wrong-input", + ) + + +@nexusrpc.handler.service_handler(service=MyService) +class MyServiceHandler2: + @nexusrpc.handler.sync_operation + async def my_sync_operation( + self, _ctx: nexusrpc.handler.StartOperationContext, _input: MyInput + ) -> MyOutput: + raise NotImplementedError + + @temporalio.nexus.workflow_run_operation + async def my_workflow_run_operation( + self, _ctx: temporalio.nexus.WorkflowRunOperationContext, _input: MyInput + ) -> temporalio.nexus.WorkflowHandle[MyOutput]: + raise NotImplementedError + + @temporalio.nexus.temporal_operation + async def my_temporal_operation( + self, + _ctx: temporalio.nexus.TemporalStartOperationContext, + _client: temporalio.nexus.TemporalNexusClient, + _input: int, + ) -> temporalio.nexus.TemporalOperationResult[None]: + raise NotImplementedError + + +@nexusrpc.handler.service_handler +class MyServiceHandlerWithoutServiceDefinition: + @nexusrpc.handler.sync_operation + async def my_sync_operation( + self, _ctx: nexusrpc.handler.StartOperationContext, _input: MyInput + ) -> MyOutput: + raise NotImplementedError + + @temporalio.nexus.workflow_run_operation + async def my_workflow_run_operation( + self, _ctx: temporalio.nexus.WorkflowRunOperationContext, _input: MyInput + ) -> temporalio.nexus.WorkflowHandle[MyOutput]: + raise NotImplementedError + + @temporalio.nexus.temporal_operation + async def my_temporal_operation( + self, + _ctx: temporalio.nexus.TemporalStartOperationContext, + _client: temporalio.nexus.TemporalNexusClient, + _input: int, + ) -> temporalio.nexus.TemporalOperationResult[None]: + raise NotImplementedError + + +_handler: TemporalOperationStartHandlerFunc[ + MyServiceHandler, + int, + None, +] = MyServiceHandler.my_temporal_operation + +_BadHandler: TypeAlias = temporalio.nexus.TemporalOperationStartHandlerFunc[ + MyServiceHandler, + str, + None, +] + +_bad_handler: TemporalOperationStartHandlerFunc[ + MyServiceHandler, + str, + None, + # assert-type-error-pyright: 'is not assignable to declared type' +] = MyServiceHandler.my_temporal_operation # type: ignore + + +class MyUnsafeContextAnnotationServiceHandler: + # A temporal operation receives TemporalStartOperationContext at runtime, so + # requiring an arbitrary user subclass is not safe. + class MyCustomTemporalStartOperationContext( + temporalio.nexus.TemporalStartOperationContext + ): + def custom_state(self) -> str: + raise NotImplementedError + + # assert-type-error-pyright: 'cannot be assigned to parameter "start".+temporal_operation' + @temporalio.nexus.temporal_operation # type: ignore + async def my_temporal_operation_with_workflow_run_context( + self, + _ctx: MyCustomTemporalStartOperationContext, + _client: temporalio.nexus.TemporalNexusClient, + _input: int, + ) -> temporalio.nexus.TemporalOperationResult[None]: + raise NotImplementedError + + # A workflow run operation receives WorkflowRunOperationContext at runtime, + # so requiring an arbitrary user subclass is not safe. + class MyCustomWorkflowRunOperationContext( + temporalio.nexus.WorkflowRunOperationContext + ): + def custom_state(self) -> str: + raise NotImplementedError + + # assert-type-error-pyright: 'cannot be assigned to parameter "start".+workflow_run_operation' + @temporalio.nexus.workflow_run_operation # type: ignore + async def my_workflow_run_operation_with_custom_context( + self, + _ctx: MyCustomWorkflowRunOperationContext, + _input: MyInput, + ) -> temporalio.nexus.WorkflowHandle[MyOutput]: + raise NotImplementedError + + +@workflow.defn +class MyWorkflow1: + @workflow.run + async def test_invoke_by_operation_definition_happy_path(self) -> None: + """ + When a nexus client calls an operation by referencing an operation definition on + a service definition, the output type is inferred correctly. + """ + nexus_client = workflow.create_nexus_client( + service=MyService, + endpoint="fake-endpoint", + ) + input = MyInput() + + # sync operation + _output_1: MyOutput = await nexus_client.execute_operation( + MyService.my_sync_operation, input + ) + _handle_1: workflow.NexusOperationHandle[ + MyOutput + ] = await nexus_client.start_operation(MyService.my_sync_operation, input) + _output_1_1: MyOutput = await _handle_1 + + # workflow run operation + _output_2: MyOutput = await nexus_client.execute_operation( + MyService.my_workflow_run_operation, input + ) + _handle_2: workflow.NexusOperationHandle[ + MyOutput + ] = await nexus_client.start_operation( + MyService.my_workflow_run_operation, input + ) + _output_2_1: MyOutput = await _handle_2 + + # temporal operation + _output_3: None = await nexus_client.execute_operation( # type: ignore + MyService.my_temporal_operation, 0 + ) + _handle_3: workflow.NexusOperationHandle[ + None + ] = await nexus_client.start_operation(MyService.my_temporal_operation, 0) + _output_3_1: None = await _handle_3 # type: ignore + + +@workflow.defn +class MyWorkflow2: + @workflow.run + async def test_invoke_by_operation_handler_happy_path(self) -> None: + """ + When a nexus client calls an operation by referencing an operation handler on a + service handler, the output type is inferred correctly. + """ + nexus_client = workflow.create_nexus_client( + service=MyServiceHandler, # MyService would also work + endpoint="fake-endpoint", + ) + input = MyInput() + + # sync operation + _output_1: MyOutput = await nexus_client.execute_operation( + MyServiceHandler.my_sync_operation, input + ) + _handle_1: workflow.NexusOperationHandle[ + MyOutput + ] = await nexus_client.start_operation( + MyServiceHandler.my_sync_operation, input + ) + _output_1_1: MyOutput = await _handle_1 + + # workflow run operation + _output_2: MyOutput = await nexus_client.execute_operation( + MyServiceHandler.my_workflow_run_operation, input + ) + _handle_2: workflow.NexusOperationHandle[ + MyOutput + ] = await nexus_client.start_operation( + MyServiceHandler.my_workflow_run_operation, input + ) + _output_2_1: MyOutput = await _handle_2 + + # temporal operation + _output_3: None = await nexus_client.execute_operation( # type: ignore + MyServiceHandler.my_temporal_operation, 0 + ) + _handle_3: workflow.NexusOperationHandle[ + None + ] = await nexus_client.start_operation( + MyServiceHandler.my_temporal_operation, 0 + ) + _output_3_1: None = await _handle_3 # type: ignore + + +@workflow.defn +class MyWorkflow3: + @workflow.run + async def test_invoke_by_operation_definition_wrong_input_type(self) -> None: + """ + When a nexus client calls an operation by referencing an operation definition on + a service definition, there is a type error if the input type is wrong. + """ + nexus_client = workflow.create_nexus_client( + service=MyService, + endpoint="fake-endpoint", + ) + # assert-type-error-pyright: 'No overloads for "execute_operation" match' + await nexus_client.execute_operation( # type: ignore + MyService.my_sync_operation, + # assert-type-error-pyright: 'Argument of type .+ cannot be assigned to parameter "input"' + "wrong-input-type", # type: ignore + ) + # assert-type-error-pyright: 'No overloads for "execute_operation" match' + await nexus_client.execute_operation( # type: ignore + MyService.my_temporal_operation, + # assert-type-error-pyright: 'Argument of type .+ cannot be assigned to parameter "input"' + "wrong-input-type", # type: ignore + ) + + +@workflow.defn +class MyWorkflow4: + @workflow.run + async def test_invoke_by_operation_handler_wrong_input_type(self) -> None: + """ + When a nexus client calls an operation by referencing an operation handler on a + service handler, there is a type error if the input type is wrong. + """ + nexus_client = workflow.create_nexus_client( + service=MyServiceHandler, + endpoint="fake-endpoint", + ) + # assert-type-error-pyright: 'No overloads for "execute_operation" match' + await nexus_client.execute_operation( # type: ignore + MyServiceHandler.my_sync_operation, # type: ignore[arg-type] + # assert-type-error-pyright: 'Argument of type .+ cannot be assigned to parameter "input"' + "wrong-input-type", # type: ignore + ) + # assert-type-error-pyright: 'No overloads for "execute_operation" match' + await nexus_client.execute_operation( # type: ignore + MyServiceHandler.my_temporal_operation, # type: ignore[arg-type] + # assert-type-error-pyright: 'Argument of type .+ cannot be assigned to parameter "input"' + "wrong-input-type", # type: ignore + ) + + +@workflow.defn +class MyWorkflow5: + @workflow.run + async def test_invoke_by_operation_handler_method_on_wrong_service(self) -> None: + """ + When a nexus client calls an operation by referencing an operation handler method + on a service handler, there is a type error if the method does not belong to the + service for which the client was created. + + (This form of type safety is not available when referencing an operation definition) + """ + nexus_client = workflow.create_nexus_client( + service=MyServiceHandler, + endpoint="fake-endpoint", + ) + # assert-type-error-pyright: 'No overloads for "execute_operation" match' + await nexus_client.execute_operation( # type: ignore + # assert-type-error-pyright: 'Argument of type .+ cannot be assigned to parameter "operation"' + MyServiceHandler2.my_sync_operation, # type: ignore + MyInput(), + ) + + # assert-type-error-pyright: 'No overloads for "execute_operation" match' + await nexus_client.execute_operation( # type: ignore + # assert-type-error-pyright: 'Argument of type .+ cannot be assigned to parameter "operation"' + MyServiceHandler2.my_temporal_operation, # type: ignore + 0, + ) + + +async def standalone_operation_type_tests(): + client = Client(service_client=Mock(spec=ServiceClient)) + nexus_client = client.create_nexus_client( + MyService, + endpoint="fake-endpoint", + ) + no_input_nexus_client = client.create_nexus_client( + MyNoInputService, + endpoint="fake-endpoint", + ) + handler_nexus_client = client.create_nexus_client( + MyServiceHandler, + endpoint="fake-endpoint", + ) + + # execute with an operation definition infers output type + _op_defn_output: MyOutput = await nexus_client.execute_operation( + MyService.my_sync_operation, + MyInput(), + id="op-1", + schedule_to_start_timeout=timedelta(seconds=1), + start_to_close_timeout=timedelta(seconds=2), + ) + + # result_type is not allowed when an operation is provided + await nexus_client.execute_operation( + # assert-type-error-pyright: 'cannot be assigned to parameter "operation" of type "str"' + MyService.my_sync_operation, # type: ignore + MyInput(), + id="op-1", + result_type=str, + ) + + # string operation name and result_type infers output type + _str_op_result_type_output: MyOutput = await nexus_client.execute_operation( + "my_sync_operation", MyInput(), id="op-1", result_type=MyOutput + ) + + # execute with workflow run handler infers output type + _workflow_run_output: MyOutput = await handler_nexus_client.execute_operation( + MyServiceHandler.my_workflow_run_operation, + MyInput(), + id="op-1", + ) + + # execute with temporal operation handler infers output type + _temporal_output: None = await handler_nexus_client.execute_operation( # type: ignore[func-returns-value] + MyServiceHandler.my_temporal_operation, + 0, + id="op-1", + ) + + # omitting arg for string operation names is not supported + # assert-type-error-pyright: 'No overloads for "execute_operation" match' + await nexus_client.execute_operation( # type: ignore + "my_sync_operation", + id="op-1", + result_type=MyOutput, + ) + # assert-type-error-pyright: 'No overloads for "start_operation" match' + await nexus_client.start_operation( # type: ignore + "my_sync_operation", + id="op-1", + result_type=MyOutput, + ) + + # omitting arg for callable operations is not supported + # assert-type-error-pyright: 'No overloads for "execute_operation" match' + await nexus_client.execute_operation( # type: ignore + MyServiceHandler.my_sync_operation, + id="op-1", + result_type=MyOutput, + ) + # assert-type-error-pyright: 'No overloads for "start_operation" match' + await nexus_client.start_operation( # type: ignore + MyServiceHandler.my_sync_operation, + id="op-1", + result_type=MyOutput, + ) + + # no-input operation definitions must still be called with explicit None + _no_input_op_defn_output: MyOutput = await no_input_nexus_client.execute_operation( + MyNoInputService.my_no_input_operation, + None, + id="op-1", + ) + _no_input_op_defn_handle: NexusOperationHandle[ + MyOutput + ] = await no_input_nexus_client.start_operation( + MyNoInputService.my_no_input_operation, + None, + id="op-1", + ) + _no_input_op_defn_handle_output: MyOutput = await _no_input_op_defn_handle.result() + + # omitting arg for no-input operation definitions is not supported + # assert-type-error-pyright: 'No overloads for "execute_operation" match' + await no_input_nexus_client.execute_operation( # type: ignore + MyNoInputService.my_no_input_operation, + id="op-1", + ) + # assert-type-error-pyright: 'No overloads for "start_operation" match' + await no_input_nexus_client.start_operation( # type: ignore + MyNoInputService.my_no_input_operation, + id="op-1", + ) + + # execute with an operation definition and a wrong input type produces a type error + # assert-type-error-pyright: 'No overloads for "execute_operation" match' + await nexus_client.execute_operation( # type: ignore + MyService.my_sync_operation, + # assert-type-error-pyright: 'Argument of type .+ cannot be assigned to parameter "arg"' + "wrong-input-type", # type: ignore + id="op-1", + ) + + # start with an operation definition and a wrong input type produces a type error + # assert-type-error-pyright: 'No overloads for "start_operation" match' + await nexus_client.start_operation( # type: ignore + MyService.my_sync_operation, + # assert-type-error-pyright: 'Argument of type .+ cannot be assigned to parameter "arg"' + "wrong-input-type", # type: ignore + id="op-1", + ) + + # starting with an operation definition infers output type on the handle and + # result from handle + _defn_handle: NexusOperationHandle[MyOutput] = await nexus_client.start_operation( + MyService.my_sync_operation, + MyInput(), + id="op-1", + schedule_to_start_timeout=timedelta(seconds=1), + start_to_close_timeout=timedelta(seconds=2), + ) + _defn_handle_output: MyOutput = await _defn_handle.result() + + # result_type is not allowed when an operation is provided + await nexus_client.start_operation( + # assert-type-error-pyright: 'cannot be assigned to parameter "operation" of type "str"' + MyServiceHandler.my_sync_operation, # type: ignore + MyInput(), + id="op-1", + result_type=str, + ) + + # starting with string operation name and result_type infers output type on the handle + # and result from the handle + _str_op_result_type_handle: NexusOperationHandle[ + MyOutput + ] = await nexus_client.start_operation( + "my_sync_operation", MyInput(), id="op-1", result_type=MyOutput + ) + _str_op_result_type_handle_output: MyOutput = ( + await _str_op_result_type_handle.result() + ) + + # starting with workflow run handler infers output type on the handle + # and result from the handle + _workflow_run_handle: NexusOperationHandle[ + MyOutput + ] = await handler_nexus_client.start_operation( + MyServiceHandler.my_workflow_run_operation, + MyInput(), + id="op-1", + ) + + # starting with temporal operation handler infers output type on the handle + # and result from the handle + _workflow_run_handle_output: MyOutput = await _workflow_run_handle.result() + _temporal_handle: NexusOperationHandle[ + None + ] = await handler_nexus_client.start_operation( + MyServiceHandler.my_temporal_operation, + 0, + id="op-1", + ) + _temporal_handle_output: None = await _temporal_handle.result() # type: ignore[func-returns-value] + + # workflow run and temporal operation handlers reject wrong input types + # assert-type-error-pyright: 'No overloads for "execute_operation" match' + await handler_nexus_client.execute_operation( # type: ignore + MyServiceHandler.my_workflow_run_operation, # type: ignore[arg-type] + # assert-type-error-pyright: 'Argument of type .+ cannot be assigned to parameter "arg"' + "wrong-input-type", # type: ignore + id="op-1", + ) + # assert-type-error-pyright: 'No overloads for "start_operation" match' + await handler_nexus_client.start_operation( # type: ignore + MyServiceHandler.my_workflow_run_operation, # type: ignore[arg-type] + # assert-type-error-pyright: 'Argument of type .+ cannot be assigned to parameter "arg"' + "wrong-input-type", # type: ignore + id="op-1", + ) + # assert-type-error-pyright: 'No overloads for "execute_operation" match' + await handler_nexus_client.execute_operation( # type: ignore + MyServiceHandler.my_temporal_operation, # type: ignore[arg-type] + # assert-type-error-pyright: 'Argument of type .+ cannot be assigned to parameter "arg"' + "wrong-input-type", # type: ignore + id="op-1", + ) + # assert-type-error-pyright: 'No overloads for "start_operation" match' + await handler_nexus_client.start_operation( # type: ignore + MyServiceHandler.my_temporal_operation, # type: ignore[arg-type] + # assert-type-error-pyright: 'Argument of type .+ cannot be assigned to parameter "arg"' + "wrong-input-type", # type: ignore + id="op-1", + ) + + # getting a handle with a string produces a handle to Any + _str_op_handle: NexusOperationHandle[Any] = client.get_nexus_operation_handle( + "op-1" + ) + + # getting a handle with an explicit type produces handle of that type + _result_type_get_handle: NexusOperationHandle[MyOutput] = ( + client.get_nexus_operation_handle("op-1", result_type=MyOutput) + ) + + # getting a handle with an operation definition produces a handle of the operation + # output type + _op_defn_get_handle: NexusOperationHandle[MyOutput] = ( + client.get_nexus_operation_handle("op-1", operation=MyService.my_sync_operation) + ) + + # providing both operation and result_type to get_nexus_operation_handle + # produces a no overload found error + # assert-type-error-pyright: 'No overloads for "get_nexus_operation_handle" match' + _result_type_op_defn_get_handle: NexusOperationHandle[MyOutput] = ( + client.get_nexus_operation_handle( # type: ignore + "op-1", + operation=MyService.my_sync_operation, + result_type=str, + ) + ) + + # mismatched types on get_nexus_operation_handle produce a type error + # assert-type-error-pyright: 'Type "NexusOperationHandle\[str\]" is not assignable to declared type "NexusOperationHandle\[MyOutput\]"' + _mismatch_handle: NexusOperationHandle[MyOutput] = ( + client.get_nexus_operation_handle( # type: ignore + "op-1", + result_type=str, # type: ignore + ) + ) + + # functions with invalid signatures produce a type error + class InvalidServiceHandler: + async def invalid(self, _ctx: str, _input: str) -> str: + raise NotImplementedError() + + # assert-type-error-pyright: 'No overloads for "start_operation" match' + _invalid_handle: NexusOperationHandle[str] = await nexus_client.start_operation( + InvalidServiceHandler.invalid, # type: ignore + "foo", + id="invalid", + ) + + # assert-type-error-pyright: 'No overloads for "execute_operation" match' + _invalid_result: str = await nexus_client.execute_operation( + InvalidServiceHandler.invalid, # type: ignore + "foo", + id="invalid", + ) diff --git a/tests/nexus/test_nexus_worker_shutdown.py b/tests/nexus/test_nexus_worker_shutdown.py new file mode 100644 index 000000000..2a94027d5 --- /dev/null +++ b/tests/nexus/test_nexus_worker_shutdown.py @@ -0,0 +1,350 @@ +"""Tests for Nexus worker shutdown support.""" + +import asyncio +import concurrent.futures +import threading +import uuid +from dataclasses import dataclass +from datetime import timedelta +from typing import Literal + +import nexusrpc +import pytest +from nexusrpc.handler import ( + StartOperationContext, + service_handler, + sync_operation, +) + +from temporalio import nexus, workflow +from temporalio.testing import WorkflowEnvironment +from temporalio.worker import Worker +from tests.helpers.nexus import ( + make_nexus_endpoint_name, +) + +# Cloud CI's namespace credentials cannot manage Nexus endpoints. +# See https://github.com/temporalio/sdk-python/issues/1704. +pytestmark = pytest.mark.requires_local_server + + +@nexusrpc.service +class ShutdownTestService: + wait_for_shutdown: nexusrpc.Operation[None, str] + hang_until_cancelled: nexusrpc.Operation[None, str] + wait_for_shutdown_sync: nexusrpc.Operation[None, str] + check_shutdown: nexusrpc.Operation[None, str] + + +@service_handler(service=ShutdownTestService) +class ShutdownTestServiceHandler: + def __init__( + self, + operation_started: asyncio.Event | None = None, + sync_operation_started: threading.Event | None = None, + ) -> None: + self.operation_started = operation_started + self.sync_operation_started = sync_operation_started + self.shutdown_check_before: bool | None = None + self.shutdown_check_after: bool | None = None + + @sync_operation + async def wait_for_shutdown(self, _ctx: StartOperationContext, _input: None) -> str: + assert self.operation_started + self.operation_started.set() + await nexus.wait_for_worker_shutdown() + return "Worker graceful shutdown" + + @sync_operation + async def hang_until_cancelled( + self, _ctx: StartOperationContext, _input: None + ) -> str: + assert self.operation_started + self.operation_started.set() + try: + while True: + await asyncio.sleep(0.1) + except asyncio.CancelledError: + return "Properly cancelled" + + @sync_operation + def wait_for_shutdown_sync(self, _ctx: StartOperationContext, _input: None) -> str: + assert self.sync_operation_started + self.sync_operation_started.set() + nexus.wait_for_worker_shutdown_sync(30) + return "Worker graceful shutdown sync" + + @sync_operation + async def check_shutdown(self, _ctx: StartOperationContext, _input: None) -> str: + assert self.operation_started + self.shutdown_check_before = nexus.is_worker_shutdown() + self.operation_started.set() + await nexus.wait_for_worker_shutdown() + self.shutdown_check_after = nexus.is_worker_shutdown() + return "done" + + +@dataclass +class ShutdownTestCallerInput: + operation: Literal[ + "wait_for_shutdown", + "hang_until_cancelled", + "wait_for_shutdown_sync", + "check_shutdown", + ] + task_queue: str + + +@workflow.defn +class ShutdownTestCallerWorkflow: + @workflow.run + async def run(self, input: ShutdownTestCallerInput) -> str: + nexus_client = workflow.create_nexus_client( + service=ShutdownTestService, + endpoint=make_nexus_endpoint_name(input.task_queue), + ) + match input.operation: + case "wait_for_shutdown": + return await nexus_client.execute_operation( + ShutdownTestService.wait_for_shutdown, None + ) + case "hang_until_cancelled": + return await nexus_client.execute_operation( + ShutdownTestService.hang_until_cancelled, None + ) + case "wait_for_shutdown_sync": + return await nexus_client.execute_operation( + ShutdownTestService.wait_for_shutdown_sync, None + ) + case "check_shutdown": + return await nexus_client.execute_operation( + ShutdownTestService.check_shutdown, None + ) + + +async def test_nexus_worker_shutdown(env: WorkflowEnvironment): + """Test that Nexus operations are cancelled when worker shuts down without graceful timeout.""" + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work with time-skipping server") + + # Use separate task queues for caller and handler workers + handler_task_queue = str(uuid.uuid4()) + caller_task_queue = str(uuid.uuid4()) + await env.create_nexus_endpoint( + make_nexus_endpoint_name(handler_task_queue), handler_task_queue + ) + + operation_started = asyncio.Event() + + with concurrent.futures.ThreadPoolExecutor() as executor: + async with Worker( + env.client, + task_queue=caller_task_queue, + workflows=[ShutdownTestCallerWorkflow], + ): + # Handler worker (will be shut down without graceful timeout) + handler_worker = Worker( + env.client, + task_queue=handler_task_queue, + nexus_service_handlers=[ShutdownTestServiceHandler(operation_started)], + nexus_task_executor=executor, + # No graceful shutdown timeout - operations should be cancelled immediately + ) + + handler_worker_task = asyncio.create_task(handler_worker.run()) + + # Start the operation that will hang via workflow + handle = await env.client.start_workflow( + ShutdownTestCallerWorkflow.run, + ShutdownTestCallerInput( + operation="hang_until_cancelled", + task_queue=handler_task_queue, + ), + id=str(uuid.uuid4()), + task_queue=caller_task_queue, + ) + + # Wait for operation to start + await operation_started.wait() + + # Shutdown the handler worker - this should cancel the operation + await handler_worker.shutdown() + + # The handler worker task should complete + await handler_worker_task + + result = await handle.result() + assert result == "Properly cancelled" + + +async def test_nexus_worker_shutdown_graceful(env: WorkflowEnvironment): + """Test that async Nexus operations complete gracefully during shutdown.""" + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work with time-skipping server") + + # Use separate task queues for caller and handler workers + handler_task_queue = str(uuid.uuid4()) + caller_task_queue = str(uuid.uuid4()) + await env.create_nexus_endpoint( + make_nexus_endpoint_name(handler_task_queue), handler_task_queue + ) + + operation_started = asyncio.Event() + + with concurrent.futures.ThreadPoolExecutor() as executor: + async with Worker( + env.client, + task_queue=caller_task_queue, + workflows=[ShutdownTestCallerWorkflow], + ): + # Handler worker (will be shut down) + handler_worker = Worker( + env.client, + task_queue=handler_task_queue, + nexus_service_handlers=[ShutdownTestServiceHandler(operation_started)], + nexus_task_executor=executor, + graceful_shutdown_timeout=timedelta(seconds=5), + ) + + handler_worker_task = asyncio.create_task(handler_worker.run()) + + # Start the operation that waits for shutdown via workflow + handle = await env.client.start_workflow( + ShutdownTestCallerWorkflow.run, + ShutdownTestCallerInput( + operation="wait_for_shutdown", + task_queue=handler_task_queue, + ), + id=str(uuid.uuid4()), + task_queue=caller_task_queue, + ) + + # Wait for operation to start + await operation_started.wait() + + # Shutdown the handler worker - this should signal the shutdown event + await handler_worker.shutdown() + + # The handler worker task should complete + await handler_worker_task + + # The operation should have completed successfully + result = await handle.result() + assert result == "Worker graceful shutdown" + + +async def test_sync_nexus_operation_worker_shutdown_graceful(env: WorkflowEnvironment): + """Test that sync (ThreadPoolExecutor) Nexus operations complete gracefully during shutdown.""" + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work with time-skipping server") + + # Use separate task queues for caller and handler workers + handler_task_queue = str(uuid.uuid4()) + caller_task_queue = str(uuid.uuid4()) + await env.create_nexus_endpoint( + make_nexus_endpoint_name(handler_task_queue), handler_task_queue + ) + + sync_operation_started = threading.Event() + + with concurrent.futures.ThreadPoolExecutor() as executor: + async with Worker( + env.client, + task_queue=caller_task_queue, + workflows=[ShutdownTestCallerWorkflow], + ): + # Handler worker (will be shut down) + handler_worker = Worker( + env.client, + task_queue=handler_task_queue, + nexus_service_handlers=[ + ShutdownTestServiceHandler( + sync_operation_started=sync_operation_started + ) + ], + nexus_task_executor=executor, + graceful_shutdown_timeout=timedelta(seconds=5), + ) + + handler_worker_task = asyncio.create_task(handler_worker.run()) + + # Start the operation that waits for shutdown synchronously via workflow + handle = await env.client.start_workflow( + ShutdownTestCallerWorkflow.run, + ShutdownTestCallerInput( + operation="wait_for_shutdown_sync", + task_queue=handler_task_queue, + ), + id=str(uuid.uuid4()), + task_queue=caller_task_queue, + ) + + # Wait for operation to start + await asyncio.to_thread(sync_operation_started.wait) + + # Shutdown the handler worker - this should signal the shutdown event + await handler_worker.shutdown() + + # The handler worker task should complete + await handler_worker_task + + # The operation should have completed successfully + result = await handle.result() + assert result == "Worker graceful shutdown sync" + + +async def test_is_worker_shutdown(env: WorkflowEnvironment): + """Test that is_worker_shutdown returns correct values.""" + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work with time-skipping server") + + # Use separate task queues for caller and handler workers + handler_task_queue = str(uuid.uuid4()) + caller_task_queue = str(uuid.uuid4()) + await env.create_nexus_endpoint( + make_nexus_endpoint_name(handler_task_queue), handler_task_queue + ) + + operation_started = asyncio.Event() + handler = ShutdownTestServiceHandler(operation_started) + + with concurrent.futures.ThreadPoolExecutor() as executor: + async with Worker( + env.client, + task_queue=caller_task_queue, + workflows=[ShutdownTestCallerWorkflow], + ): + # Handler worker (will be shut down) + handler_worker = Worker( + env.client, + task_queue=handler_task_queue, + nexus_service_handlers=[handler], + nexus_task_executor=executor, + graceful_shutdown_timeout=timedelta(seconds=5), + ) + + handler_worker_task = asyncio.create_task(handler_worker.run()) + + # Start the operation via workflow + handle = await env.client.start_workflow( + ShutdownTestCallerWorkflow.run, + ShutdownTestCallerInput( + operation="check_shutdown", + task_queue=handler_task_queue, + ), + id=str(uuid.uuid4()), + task_queue=caller_task_queue, + ) + + # Wait for operation to start + await operation_started.wait() + + # Shutdown the handler worker + await handler_worker.shutdown() + + await handler_worker_task + result = await handle.result() + + assert handler.shutdown_check_before is False + assert handler.shutdown_check_after is True + assert result == "done" diff --git a/tests/nexus/test_operation_token.py b/tests/nexus/test_operation_token.py new file mode 100644 index 000000000..58d4a7859 --- /dev/null +++ b/tests/nexus/test_operation_token.py @@ -0,0 +1,279 @@ +import base64 +import json +from typing import Any + +import pytest + +from temporalio.nexus._token import ( + OperationToken, + OperationTokenType, + WorkflowHandle, + _base64url_decode_no_padding, +) + + +def _encode_json_token(value: Any) -> str: + return _encode_bytes(json.dumps(value, separators=(",", ":")).encode("utf-8")) + + +def _encode_bytes(value: bytes) -> str: + return base64.urlsafe_b64encode(value).decode("utf-8").rstrip("=") + + +def test_operation_token_encode_decode_round_trip(): + token = OperationToken( + type=OperationTokenType.WORKFLOW, + namespace="default", + workflow_id="workflow-id", + version=0, + ).encode() + + assert "=" not in token + assert OperationToken.decode(token) == OperationToken( + type=OperationTokenType.WORKFLOW, + namespace="default", + workflow_id="workflow-id", + version=0, + ) + + +def test_operation_token_activity_encode_decode_round_trip(): + token = OperationToken( + type=OperationTokenType.ACTIVITY, + namespace="default", + activity_id="activity-id", + run_id="run-id", + version=0, + ).encode() + + assert "=" not in token + assert OperationToken.decode(token) == OperationToken( + type=OperationTokenType.ACTIVITY, + namespace="default", + activity_id="activity-id", + run_id="run-id", + version=0, + ) + + +def test_operation_token_activity_encode_uses_activity_id_and_omits_workflow_id(): + token = OperationToken( + type=OperationTokenType.ACTIVITY, + namespace="default", + activity_id="activity-id", + ).encode() + + assert json.loads(_base64url_decode_no_padding(token)) == { + "t": 2, + "ns": "default", + "aid": "activity-id", + } + + +def test_workflow_handle_to_from_token_round_trip(): + handle = WorkflowHandle[str](namespace="default", workflow_id="workflow-id") + + assert WorkflowHandle[str].from_token(handle.to_token()) == handle + + +@pytest.mark.parametrize( + ("token", "expected"), + [ + ( + _encode_json_token({"t": 1, "ns": "default", "wid": "workflow-id"}), + OperationToken( + type=OperationTokenType.WORKFLOW, + namespace="default", + workflow_id="workflow-id", + ), + ), + ( + _encode_json_token({"t": 1, "ns": "", "wid": "workflow-id"}), + OperationToken( + type=OperationTokenType.WORKFLOW, + namespace="", + workflow_id="workflow-id", + ), + ), + ( + _encode_json_token( + {"t": 1, "ns": "default", "wid": "workflow-id", "v": None} + ), + OperationToken( + type=OperationTokenType.WORKFLOW, + namespace="default", + workflow_id="workflow-id", + ), + ), + ( + _encode_json_token({"t": 1, "ns": "default", "wid": "workflow-id", "v": 0}), + OperationToken( + type=OperationTokenType.WORKFLOW, + namespace="default", + workflow_id="workflow-id", + version=0, + ), + ), + # Activity tokens + ( + _encode_json_token( + {"t": 2, "ns": "default", "aid": "activity-id", "rid": "run-id"} + ), + OperationToken( + type=OperationTokenType.ACTIVITY, + namespace="default", + activity_id="activity-id", + run_id="run-id", + ), + ), + ( + _encode_json_token( + {"t": 2, "ns": "", "aid": "activity-id", "rid": "run-id"} + ), + OperationToken( + type=OperationTokenType.ACTIVITY, + namespace="", + activity_id="activity-id", + run_id="run-id", + ), + ), + ( + _encode_json_token( + { + "t": 2, + "ns": "default", + "aid": "activity-id", + "rid": "run-id", + "v": None, + } + ), + OperationToken( + type=OperationTokenType.ACTIVITY, + namespace="default", + activity_id="activity-id", + run_id="run-id", + ), + ), + ( + _encode_json_token( + {"t": 2, "ns": "default", "aid": "activity-id", "rid": "run-id", "v": 0} + ), + OperationToken( + type=OperationTokenType.ACTIVITY, + namespace="default", + activity_id="activity-id", + run_id="run-id", + version=0, + ), + ), + ], +) +def test_operation_token_decode_accepts_valid_tokens( + token: str, + expected: OperationToken, +): + assert OperationToken.decode(token) == expected + + +@pytest.mark.parametrize( + ("token", "message"), + [ + ("", "invalid token: token is empty"), + ("not+a-base64url-token", "failed to decode token as base64url"), + (_encode_bytes(b"not json"), "failed to unmarshal operation token"), + (_encode_json_token(["not", "a", "dict"]), "expected dict"), + ( + _encode_json_token({"ns": "default", "wid": "workflow-id"}), + "expected token type to be an int", + ), + ( + _encode_json_token({"t": "1", "ns": "default", "wid": "workflow-id"}), + "expected token type to be an int", + ), + ( + _encode_json_token({"t": 999, "ns": "default", "wid": "workflow-id"}), + "unknown token type", + ), + ( + _encode_json_token({"t": 1, "ns": "default"}), + "expected non-empty workflow id for token type `WORKFLOW`", + ), + ( + _encode_json_token({"t": 1, "ns": "default", "wid": 123}), + "expected workflow id to be a string", + ), + ( + _encode_json_token({"t": 1, "ns": "default", "wid": ""}), + "expected non-empty workflow id", + ), + ( + _encode_json_token({"t": 1, "wid": "workflow-id"}), + "expected namespace to be a string", + ), + ( + _encode_json_token({"t": 1, "ns": 123, "wid": "workflow-id"}), + "expected namespace to be a string", + ), + ( + _encode_json_token( + {"t": 1, "ns": "default", "wid": "workflow-id", "v": "0"} + ), + "expected version to be an int or null", + ), + # Activity tokens + ( + _encode_json_token({"t": 2, "ns": "default"}), + "expected non-empty activity id for token type `ACTIVITY`", + ), + ( + _encode_json_token({"t": 2, "ns": "default", "aid": ""}), + "expected non-empty activity id for token type `ACTIVITY`", + ), + ( + _encode_json_token({"t": 2, "ns": "default", "aid": 123}), + "expected activity id to be a string", + ), + ( + _encode_json_token( + {"t": 2, "ns": "default", "aid": "activity-id", "rid": 123} + ), + "expected run_id to be a string", + ), + ( + _encode_json_token({"t": 2, "aid": "activity-id", "rid": "run-id"}), + "expected namespace to be a string", + ), + ( + _encode_json_token( + { + "t": 2, + "ns": "default", + "aid": "activity-id", + "rid": "run-id", + "v": "0", + } + ), + "expected version to be an int or null", + ), + ], +) +def test_operation_token_decode_rejects_invalid_tokens(token: str, message: str): + with pytest.raises(TypeError, match=message): + OperationToken.decode(token) + + +def test_workflow_handle_from_token_accepts_version_zero(): + token = _encode_json_token({"t": 1, "ns": "default", "wid": "workflow-id", "v": 0}) + + assert WorkflowHandle[str].from_token(token) == WorkflowHandle[str]( + namespace="default", + workflow_id="workflow-id", + version=0, + ) + + +def test_workflow_handle_from_token_rejects_unsupported_version(): + token = _encode_json_token({"t": 1, "ns": "default", "wid": "workflow-id", "v": 1}) + + with pytest.raises(TypeError, match="'v' field, if present, must be 0"): + WorkflowHandle[str].from_token(token) diff --git a/tests/nexus/test_signal_link_propagation_e2e.py b/tests/nexus/test_signal_link_propagation_e2e.py new file mode 100644 index 000000000..9e51d4b93 --- /dev/null +++ b/tests/nexus/test_signal_link_propagation_e2e.py @@ -0,0 +1,560 @@ +"""End-to-end (server-based) tests for Nexus signal-backlink propagation. + +These exercise, against a real server, the bidirectional +link propagation that occurs when a Nexus operation handler signals (or signal-with-starts) a +workflow: + +- Forward: the caller's ``NexusOperationScheduled`` event is referenced by the callee's + ``WorkflowExecutionSignaled`` event (attached to the signal RPC the handler issues). +- Backward: a backlink pointing at the callee's ``WorkflowExecutionSignaled`` event lands on + the caller's ``NexusOperationCompleted`` event (sync handler) or ``NexusOperationStarted`` + event (async handler). + +The backward direction is produced server-side (temporalio/temporal#9897) and is gated by +``history.enableCHASMSignalBacklinks=true`` (added to the local dev-server args in +``tests/conftest.py``). The server populates the backlink's reference via ``RequestIdReference`` +rather than ``EventReference``, so backlink assertions tolerate both oneof variants of +``common.v1.Link.WorkflowEvent.reference`` (see ``workflow_event_link_event_type``). When run +against a server that does not emit the backlink, the backward assertions are skipped. + +The forward/backward description above applies to operations scheduled by a caller workflow. The +file also covers the same handlers invoked as standalone (client-initiated) operations via +``client.create_nexus_client``; that case has no caller workflow, so the forward link is a +``NexusOperation`` link to the operation execution itself and only the forward direction is +asserted (see the standalone tests near the end of the file for details). +""" + +from __future__ import annotations + +import uuid +from dataclasses import dataclass +from datetime import timedelta + +import pytest +from nexusrpc import Operation, service +from nexusrpc.handler import ( + OperationHandler, + StartOperationContext, + StartOperationResultAsync, + service_handler, + sync_operation, +) +from nexusrpc.handler._decorators import operation_handler + +import temporalio.api.enums.v1 +import temporalio.api.history.v1 +import temporalio.common +from temporalio import nexus, workflow +from temporalio.client import Client, WorkflowHistory +from temporalio.service import RPCError, RPCStatusCode +from temporalio.testing import WorkflowEnvironment +from temporalio.worker import Worker +from tests.helpers import assert_eventually +from tests.helpers.nexus import ( + events_of_type, + make_nexus_endpoint_name, + workflow_event_link_event_type, +) + +# Cloud CI's namespace credentials cannot manage Nexus endpoints. +# See https://github.com/temporalio/sdk-python/issues/1704. +pytestmark = pytest.mark.requires_local_server + +EventType = temporalio.api.enums.v1.EventType + + +# ── Service definition ────────────────────────────────────────────────────────────────────── + + +@dataclass +class OpInput: + mode: str + callee_id: str + + +@service +class SignalingService: + op: Operation[OpInput, str] + + +# ── Callee workflow ─────────────────────────────────────────────────────────────────────── + + +@workflow.defn +class CalleeWorkflow: + def __init__(self) -> None: + self._received: list[str] = [] + self._expected = 1 + + @workflow.run + async def run(self, expected_signals: int) -> str: + self._expected = expected_signals + await workflow.wait_condition(lambda: len(self._received) >= self._expected) + return ",".join(self._received) + + @workflow.signal + def ping(self, msg: str) -> None: + self._received.append(msg) + + +# ── Caller workflow ─────────────────────────────────────────────────────────────────────── + + +@workflow.defn +class CallerWorkflow: + @workflow.run + async def run(self, mode: str, callee_id: str, task_queue: str) -> str: + client = workflow.create_nexus_client( + service=SignalingService, + endpoint=make_nexus_endpoint_name(task_queue), + ) + return await client.execute_operation( + SignalingService.op, OpInput(mode=mode, callee_id=callee_id) + ) + + +# ── Nexus service handler ───────────────────────────────────────────────────────────────── + +MODE_SYNC = "sync" +MODE_ASYNC = "async" + + +class _AsyncSignalingOperation(OperationHandler[OpInput, str]): + """Signal-with-starts the callee then returns an async result. + + The backlink stashed on ``ctx.outbound_links`` by the signal-with-start RPC is carried on + the start-operation response, landing on the caller's ``NexusOperationStarted`` event. + """ + + async def start( + self, ctx: StartOperationContext, input: OpInput + ) -> StartOperationResultAsync: + await _signal_with_start(input.callee_id, "async-signal") + return StartOperationResultAsync(token=f"async-op-{uuid.uuid4()}") + + async def cancel(self, ctx, token: str) -> None: # type: ignore[no-untyped-def] + raise NotImplementedError + + +@service_handler(service=SignalingService) +class SignalingServiceHandler: + @sync_operation + async def op(self, _ctx: StartOperationContext, input: OpInput) -> str: + # Synchronous path: signal-with-start the callee (first signal) then plain-signal it + # (second signal). Both backlinks are carried on the sync start-operation response and + # land on the caller's NexusOperationCompleted event. + await _signal_with_start(input.callee_id, "first") + await ( + nexus.client() + .get_workflow_handle(input.callee_id) + .signal(CalleeWorkflow.ping, "second") + ) + return "ok:sync" + + +# A separate service exposing only the async operation, so the caller can address it by name. +@service +class AsyncSignalingService: + op: Operation[OpInput, str] + + +@service_handler(service=AsyncSignalingService) +class AsyncSignalingServiceHandler: + @operation_handler + def op(self) -> OperationHandler[OpInput, str]: + return _AsyncSignalingOperation() + + +# A service whose handler issues a plain start_workflow (no signal, not the nexus-backing +# workflow) against an already-running callee under a USE_EXISTING conflict policy. +@service +class StartConflictService: + op: Operation[OpInput, str] + + +@service_handler(service=StartConflictService) +class StartConflictServiceHandler: + @sync_operation + async def op(self, _ctx: StartOperationContext, input: OpInput) -> str: + # Plain start_workflow from inside a Nexus operation handler, targeting the + # already-running callee with WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING. The build path + # enables on_conflict_options, so the detected conflict attaches this request's request + # id (and links) to the existing run. + await nexus.client().start_workflow( + CalleeWorkflow.run, + 1, + id=input.callee_id, + task_queue=nexus.info().task_queue, + id_conflict_policy=temporalio.common.WorkflowIDConflictPolicy.USE_EXISTING, + ) + return "ok:conflict" + + +async def _signal_with_start(callee_id: str, payload: str) -> None: + # signal-with-start exercises the SignalWithStartWorkflowExecutionResponse.signal_link + # backlink path in temporalio.client._impl. + await nexus.client().start_workflow( + CalleeWorkflow.run, + 2 if payload == "first" else 1, + id=callee_id, + task_queue=nexus.info().task_queue, + start_signal="ping", + start_signal_args=[payload], + ) + + +@workflow.defn +class AsyncSignalCallerWorkflow: + @workflow.run + async def run(self, callee_id: str, task_queue: str) -> str: + client = workflow.create_nexus_client( + service=AsyncSignalingService, + endpoint=make_nexus_endpoint_name(task_queue), + ) + handle = await client.start_operation( + AsyncSignalingService.op, OpInput(mode=MODE_ASYNC, callee_id=callee_id) + ) + # Do not await the result: the async op never completes (no completion is delivered). + # Returning the token confirms the operation reached the Started state, whose history + # event carries the backlink. + return handle.operation_token or "async-started" + + +# ── Assertion helpers ─────────────────────────────────────────────────────────────────────── + + +def _assert_forward_link( + callee_history: WorkflowHistory, + caller_id: str, + expected_count: int, +) -> None: + signaled = events_of_type( + callee_history, EventType.EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED + ) + assert len(signaled) == expected_count, ( + f"expected {expected_count} WorkflowExecutionSignaled events, got {len(signaled)}" + ) + for event in signaled: + assert len(event.links) >= 1, ( + "expected at least one link on each WorkflowExecutionSignaled event" + ) + we = event.links[0].workflow_event + assert we.workflow_id == caller_id, ( + "forward link should reference the caller workflow" + ) + assert we.event_ref.event_type == EventType.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED + + +def _assert_backlink( + event: temporalio.api.history.v1.HistoryEvent, callee_id: str +) -> bool: + """Assert the event carries a signal-event backlink to the callee. + + Returns False (and asserts nothing) if no backlink is present, so the test soft-passes + against a server that does not emit backlinks. + """ + if len(event.links) < 1: + return False + we = event.links[0].workflow_event + assert we.workflow_id == callee_id, "backlink should reference the callee workflow" + assert ( + workflow_event_link_event_type(we) + == EventType.EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED + ) + return True + + +# ── Tests ───────────────────────────────────────────────────────────────────────────────── + + +async def test_sync_signal_operation_links( + client: Client, + env: WorkflowEnvironment, +) -> None: + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work with time-skipping server") + + task_queue = str(uuid.uuid4()) + await env.create_nexus_endpoint(make_nexus_endpoint_name(task_queue), task_queue) + callee_id = f"callee-{uuid.uuid4()}" + caller_id = f"caller-{uuid.uuid4()}" + + async with Worker( + client, + task_queue=task_queue, + nexus_service_handlers=[SignalingServiceHandler()], + workflows=[CallerWorkflow, CalleeWorkflow], + ): + caller_handle = await client.start_workflow( + CallerWorkflow.run, + args=[MODE_SYNC, callee_id, task_queue], + id=caller_id, + task_queue=task_queue, + ) + assert await caller_handle.result() == "ok:sync" + + callee_result = await client.get_workflow_handle(callee_id).result() + assert callee_result == "first,second" + + caller_history = await caller_handle.fetch_history() + callee_history = await client.get_workflow_handle(callee_id).fetch_history() + + # Forward: both signal events on the callee reference the caller's scheduled event. + _assert_forward_link(callee_history, caller_id, expected_count=2) + + # Backward: the single NexusOperationCompleted carries backlinks to the callee. + completed = events_of_type( + caller_history, EventType.EVENT_TYPE_NEXUS_OPERATION_COMPLETED + ) + assert len(completed) == 1, ( + f"expected exactly one NexusOperationCompleted event, got {len(completed)}" + ) + if not _assert_backlink(completed[0], callee_id): + pytest.skip( + "server did not emit a signal backlink " + "(history.enableCHASMSignalBacklinks not enabled)" + ) + + +async def test_async_signal_operation_links( + client: Client, + env: WorkflowEnvironment, +) -> None: + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work with time-skipping server") + + task_queue = str(uuid.uuid4()) + await env.create_nexus_endpoint(make_nexus_endpoint_name(task_queue), task_queue) + callee_id = f"async-callee-{uuid.uuid4()}" + caller_id = f"async-caller-{uuid.uuid4()}" + + async with Worker( + client, + task_queue=task_queue, + nexus_service_handlers=[AsyncSignalingServiceHandler()], + workflows=[AsyncSignalCallerWorkflow, CalleeWorkflow], + ): + caller_handle = await client.start_workflow( + AsyncSignalCallerWorkflow.run, + args=[callee_id, task_queue], + id=caller_id, + task_queue=task_queue, + ) + # Caller returns once the async operation reaches Started; result is the op token. + assert await caller_handle.result() + + callee_result = await client.get_workflow_handle(callee_id).result() + assert callee_result == "async-signal" + + caller_history = await caller_handle.fetch_history() + callee_history = await client.get_workflow_handle(callee_id).fetch_history() + + # Forward: the single signal event on the callee references the caller's scheduled event. + _assert_forward_link(callee_history, caller_id, expected_count=1) + + # Backward: the backlink lands on NexusOperationStarted for the async response path. + started = events_of_type( + caller_history, EventType.EVENT_TYPE_NEXUS_OPERATION_STARTED + ) + assert len(started) == 1, ( + f"expected exactly one NexusOperationStarted event, got {len(started)}" + ) + if not _assert_backlink(started[0], callee_id): + pytest.skip( + "server did not emit a signal backlink " + "(history.enableCHASMSignalBacklinks not enabled)" + ) + + +# ── Standalone (client-initiated) operations ───────────────────────────────────────────────── +# +# The tests above drive the operation from a caller workflow. The tests below invoke the same +# handlers directly via ``client.create_nexus_client`` (a standalone Nexus operation, with no +# caller workflow). The forward link still propagates, but as a ``NexusOperation`` link +# referencing the operation execution itself rather than a ``WorkflowEvent`` link to a caller's +# ``NexusOperationScheduled`` event. The backward (response) link lands on the standalone +# operation's own execution, which is a CHASM ``nexusoperation.operation`` archetype and is not +# retrievable via the workflow history API, so only the forward direction is asserted here. + + +def _assert_standalone_forward_link( + callee_history: WorkflowHistory, + operation_id: str, + expected_count: int, +) -> None: + signaled = events_of_type( + callee_history, EventType.EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED + ) + assert len(signaled) == expected_count, ( + f"expected {expected_count} WorkflowExecutionSignaled events, got {len(signaled)}" + ) + for event in signaled: + assert len(event.links) >= 1, ( + "expected at least one link on each WorkflowExecutionSignaled event" + ) + link = event.links[0] + assert link.HasField("nexus_operation"), ( + "standalone forward link should be a NexusOperation link" + ) + assert link.nexus_operation.operation_id == operation_id, ( + "forward link should reference the standalone Nexus operation" + ) + + +async def test_standalone_sync_signal_operation_links( + client: Client, + env: WorkflowEnvironment, +) -> None: + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work with time-skipping server") + + task_queue = str(uuid.uuid4()) + await env.create_nexus_endpoint(make_nexus_endpoint_name(task_queue), task_queue) + callee_id = f"standalone-callee-{uuid.uuid4()}" + operation_id = f"standalone-op-{uuid.uuid4()}" + + async with Worker( + client, + task_queue=task_queue, + nexus_service_handlers=[SignalingServiceHandler()], + workflows=[CalleeWorkflow], + ): + nexus_client = client.create_nexus_client( + service=SignalingService, + endpoint=make_nexus_endpoint_name(task_queue), + ) + handle = await nexus_client.start_operation( + SignalingService.op, + OpInput(mode=MODE_SYNC, callee_id=callee_id), + id=operation_id, + schedule_to_close_timeout=timedelta(seconds=20), + ) + assert await handle.result() == "ok:sync" + + callee_result = await client.get_workflow_handle(callee_id).result() + assert callee_result == "first,second" + + callee_history = await client.get_workflow_handle(callee_id).fetch_history() + + # Forward: both signal events on the callee reference the standalone Nexus operation. + _assert_standalone_forward_link(callee_history, operation_id, expected_count=2) + + +async def test_standalone_async_signal_operation_links( + client: Client, + env: WorkflowEnvironment, +) -> None: + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work with time-skipping server") + + task_queue = str(uuid.uuid4()) + await env.create_nexus_endpoint(make_nexus_endpoint_name(task_queue), task_queue) + callee_id = f"standalone-async-callee-{uuid.uuid4()}" + operation_id = f"standalone-async-op-{uuid.uuid4()}" + + async with Worker( + client, + task_queue=task_queue, + nexus_service_handlers=[AsyncSignalingServiceHandler()], + workflows=[CalleeWorkflow], + ): + nexus_client = client.create_nexus_client( + service=AsyncSignalingService, + endpoint=make_nexus_endpoint_name(task_queue), + ) + # The async operation never completes (no completion is delivered), so we do not await + # its result; the handler signals the callee during start. + await nexus_client.start_operation( + AsyncSignalingService.op, + OpInput(mode=MODE_ASYNC, callee_id=callee_id), + id=operation_id, + schedule_to_close_timeout=timedelta(seconds=20), + ) + + async def _callee_result() -> str: + # The callee is signal-with-started from the handler; tolerate the brief window + # before it exists. + try: + return await client.get_workflow_handle(callee_id).result() + except RPCError as err: + if err.status == RPCStatusCode.NOT_FOUND: + raise AssertionError("callee not created yet") + raise + + assert await assert_eventually(_callee_result) == "async-signal" + + callee_history = await client.get_workflow_handle(callee_id).fetch_history() + + # Forward: the single signal event on the callee references the standalone operation. + _assert_standalone_forward_link(callee_history, operation_id, expected_count=1) + + +# ── on-conflict options for a plain start from a handler ────────────────────────────────────── +# +# When a Nexus operation handler issues a plain start_workflow (no signal, not the nexus-backing +# workflow) against an already-running workflow under a USE_EXISTING conflict policy, the SDK +# enables on_conflict_options so the server attaches this request's request id (and links) to the +# existing run. The attachment surfaces as a WorkflowExecutionOptionsUpdated event on the +# existing workflow's history. Older servers may not emit it, so the assertion soft-skips. + + +async def test_start_from_handler_attaches_on_conflict_options( + client: Client, + env: WorkflowEnvironment, +) -> None: + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work with time-skipping server") + + task_queue = str(uuid.uuid4()) + await env.create_nexus_endpoint(make_nexus_endpoint_name(task_queue), task_queue) + callee_id = f"conflict-callee-{uuid.uuid4()}" + operation_id = f"conflict-op-{uuid.uuid4()}" + + async with Worker( + client, + task_queue=task_queue, + nexus_service_handlers=[StartConflictServiceHandler()], + workflows=[CalleeWorkflow], + ): + # Start the callee first so the handler's start_workflow hits a conflict. + callee_handle = await client.start_workflow( + CalleeWorkflow.run, + 1, + id=callee_id, + task_queue=task_queue, + ) + + nexus_client = client.create_nexus_client( + service=StartConflictService, + endpoint=make_nexus_endpoint_name(task_queue), + ) + handle = await nexus_client.start_operation( + StartConflictService.op, + OpInput(mode=MODE_SYNC, callee_id=callee_id), + id=operation_id, + schedule_to_close_timeout=timedelta(seconds=20), + ) + # USE_EXISTING resolves the conflict to the existing run; the operation succeeds. + assert await handle.result() == "ok:conflict" + + # Release the callee so it terminates cleanly, then read its history. + await callee_handle.signal(CalleeWorkflow.ping, "done") + assert await callee_handle.result() == "done" + callee_history = await callee_handle.fetch_history() + + updated = events_of_type( + callee_history, EventType.EVENT_TYPE_WORKFLOW_EXECUTION_OPTIONS_UPDATED + ) + if not updated: + pytest.skip( + "server did not emit a WorkflowExecutionOptionsUpdated event " + "(on_conflict_options not honored by this server version)" + ) + # The conflict resolution attached this start request's request id to the existing run, + # which only happens because on_conflict_options.attach_request_id was set on the request. + assert any( + e.workflow_execution_options_updated_event_attributes.attached_request_id + for e in updated + ), ( + "expected a WorkflowExecutionOptionsUpdated event carrying an attached request id" + ) diff --git a/tests/nexus/test_standalone_operations.py b/tests/nexus/test_standalone_operations.py new file mode 100644 index 000000000..26a8316b4 --- /dev/null +++ b/tests/nexus/test_standalone_operations.py @@ -0,0 +1,1070 @@ +"""Integration tests for standalone Nexus operations (client-side). + +Tests the client-side Nexus operation API: start, result, describe, cancel, +terminate, list, count, get_nexus_operation_handle, ID conflict policies, +and interceptor integration. +""" + +from __future__ import annotations + +import asyncio +import uuid +from dataclasses import dataclass +from datetime import timedelta +from typing import Any, Literal + +import nexusrpc +import pytest +from nexusrpc.handler import ( + StartOperationContext, + service_handler, + sync_operation, +) + +import temporalio.api.enums.v1 +from temporalio import nexus, workflow +from temporalio.client import ( + CancelNexusOperationInput, + Client, + CountNexusOperationsInput, + DescribeNexusOperationInput, + GetNexusOperationResultInput, + Interceptor, + ListNexusOperationsInput, + NexusOperationExecutionDescription, + NexusOperationFailureError, + NexusOperationHandle, + OutboundInterceptor, + StartNexusOperationInput, + TerminateNexusOperationInput, + WorkflowHistory, + WorkflowUpdateStage, +) +from temporalio.common import ( + NexusOperationExecutionStatus, + NexusOperationIDConflictPolicy, + NexusOperationIDReusePolicy, + PendingNexusOperationExecutionState, + WorkflowIDConflictPolicy, + WorkflowIDReusePolicy, +) +from temporalio.exceptions import ( + ApplicationError, + CancelledError, + NexusOperationAlreadyStartedError, + TerminatedError, +) +from temporalio.nexus import WorkflowRunOperationContext, workflow_run_operation +from temporalio.testing import WorkflowEnvironment +from temporalio.worker import Worker +from tests.helpers import assert_eventually +from tests.helpers.nexus import ( + assert_links_match, + expected_nexus_operation_link, + expected_workflow_event_link, + links_from_workflow_execution_started_event, + make_nexus_endpoint_name, +) + +# --------------------------------------------------------------------------- +# Data types +# --------------------------------------------------------------------------- + + +# Cloud CI's namespace credentials cannot manage Nexus endpoints. +# See https://github.com/temporalio/sdk-python/issues/1704. +pytestmark = pytest.mark.requires_local_server + + +@dataclass +class EchoInput: + value: str + + +@dataclass +class EchoOutput: + value: str + + +@dataclass +class RaiseErrInput: + err_type: Literal["handler_err", "application_err"] + + +# --------------------------------------------------------------------------- +# Service definition +# --------------------------------------------------------------------------- + + +@nexusrpc.service +class StandaloneTestService: + echo_sync: nexusrpc.Operation[EchoInput, EchoOutput] + echo_async: nexusrpc.Operation[EchoInput, EchoOutput] + blocking_async: nexusrpc.Operation[EchoInput, EchoOutput] + raise_err: nexusrpc.Operation[RaiseErrInput, None] + + +@nexusrpc.service(name="StandaloneTestService") +class NamedService: + echo_sync: nexusrpc.Operation[EchoInput, EchoOutput] + + +# --------------------------------------------------------------------------- +# Handler workflows +# --------------------------------------------------------------------------- + + +@workflow.defn +class EchoHandlerWorkflow: + @workflow.run + async def run(self, input: EchoInput) -> EchoOutput: + return EchoOutput(value=input.value) + + +@workflow.defn +class BlockingHandlerWorkflow: + """A workflow that blocks until it receives a signal or is cancelled/terminated.""" + + def __init__(self) -> None: + self._proceed = False + + @workflow.run + async def run(self, input: EchoInput) -> EchoOutput: + await workflow.wait_condition(lambda: self._proceed) + return EchoOutput(value=input.value) + + @workflow.update + def unblock(self) -> None: + self._proceed = True + + +# --------------------------------------------------------------------------- +# Service handler +# --------------------------------------------------------------------------- + + +@service_handler(service=StandaloneTestService) +class StandaloneTestServiceHandler: + def __init__(self) -> None: + self.started_blocking = asyncio.Event() + + @sync_operation + async def echo_sync( + self, _ctx: StartOperationContext, input: EchoInput + ) -> EchoOutput: + return EchoOutput(value=input.value) + + @workflow_run_operation + async def echo_async( + self, ctx: WorkflowRunOperationContext, input: EchoInput + ) -> nexus.WorkflowHandle[EchoOutput]: + return await ctx.start_workflow( + EchoHandlerWorkflow.run, + input, + id=str(uuid.uuid4()), + ) + + @workflow_run_operation + async def blocking_async( + self, ctx: WorkflowRunOperationContext, input: EchoInput + ) -> nexus.WorkflowHandle[EchoOutput]: + handle = await ctx.start_workflow( + BlockingHandlerWorkflow.run, + input, + id=f"blocking_async-{input.value}", + id_reuse_policy=WorkflowIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy=WorkflowIDConflictPolicy.FAIL, + ) + self.started_blocking.set() + return handle + + @sync_operation + async def raise_err( + self, _ctx: StartOperationContext, input: RaiseErrInput + ) -> None: + match input.err_type: + case "handler_err": + raise nexusrpc.HandlerError( + "test handler error", + type=nexusrpc.HandlerErrorType.INTERNAL, + retryable_override=False, + ) + case "application_err": + raise ApplicationError("test application error", non_retryable=True) + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + + +async def test_start_sync_operation_and_get_result( + client: Client, env: WorkflowEnvironment +): + """Start a sync nexus operation, call handle.result(), verify return value.""" + if env.supports_time_skipping: + pytest.skip( + "Standalone Nexus Operation tests don't work with time-skipping server" + ) + + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + + async with Worker( + client, + task_queue=task_queue, + nexus_service_handlers=[StandaloneTestServiceHandler()], + workflows=[EchoHandlerWorkflow, BlockingHandlerWorkflow], + ): + await env.create_nexus_endpoint(endpoint_name, task_queue) + + nexus_client = client.create_nexus_client( + service=StandaloneTestService, endpoint=endpoint_name + ) + # Use execute_with_retry to retry the full start+result cycle + # (endpoint propagation may cause the first attempt to time out) + handle = await nexus_client.start_operation( + StandaloneTestService.echo_sync, + EchoInput(value="hello"), + id=str(uuid.uuid4()), + schedule_to_close_timeout=timedelta(seconds=10), + ) + result = await handle.result() + assert isinstance(result, EchoOutput) + assert result.value == "hello" + + # test value is cached + second_result = await handle.result() + assert result is second_result + + +async def test_start_async_operation_and_poll_result( + client: Client, env: WorkflowEnvironment +): + """Start a workflow_run operation, poll result, verify.""" + if env.supports_time_skipping: + pytest.skip( + "Standalone Nexus Operation tests don't work with time-skipping server" + ) + + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + + async with Worker( + client, + task_queue=task_queue, + nexus_service_handlers=[StandaloneTestServiceHandler()], + workflows=[EchoHandlerWorkflow, BlockingHandlerWorkflow], + ): + await env.create_nexus_endpoint(endpoint_name, task_queue) + + nexus_client = client.create_nexus_client( + service=StandaloneTestService, endpoint=endpoint_name + ) + handle = await nexus_client.start_operation( + StandaloneTestService.echo_async, + EchoInput(value="async-hello"), + id=str(uuid.uuid4()), + schedule_to_close_timeout=timedelta(seconds=30), + ) + result = await handle.result() + assert isinstance(result, EchoOutput) + assert result.value == "async-hello" + + +async def test_started_workflow_has_link_to_standalone_nexus_operation( + client: Client, env: WorkflowEnvironment +): + """Start a workflow_run operation and verify its workflow links back to the Nexus op.""" + if env.supports_time_skipping: + pytest.skip( + "Standalone Nexus Operation tests don't work with time-skipping server" + ) + + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + service_handler = StandaloneTestServiceHandler() + + async with Worker( + client, + task_queue=task_queue, + nexus_service_handlers=[service_handler], + workflows=[EchoHandlerWorkflow, BlockingHandlerWorkflow], + ): + await env.create_nexus_endpoint(endpoint_name, task_queue) + + nexus_client = client.create_nexus_client( + service=StandaloneTestService, endpoint=endpoint_name + ) + op_id = str(uuid.uuid4()) + input_value = f"link-test-{uuid.uuid4()}" + workflow_id = f"blocking_async-{input_value}" + + handle = await nexus_client.start_operation( + StandaloneTestService.blocking_async, + EchoInput(value=input_value), + id=op_id, + id_reuse_policy=NexusOperationIDReusePolicy.REJECT_DUPLICATE, + id_conflict_policy=NexusOperationIDConflictPolicy.FAIL, + schedule_to_close_timeout=timedelta(seconds=30), + ) + + await service_handler.started_blocking.wait() + workflow_history = await _assert_workflow_started_with_nexus_operation_link( + client, workflow_id, handle + ) + await assert_eventually( + lambda: _assert_nexus_operation_has_link_to_started_workflow( + client, workflow_history, handle + ) + ) + + workflow_handle = client.get_workflow_handle(workflow_id) + await workflow_handle.start_update( + BlockingHandlerWorkflow.unblock, + wait_for_stage=WorkflowUpdateStage.COMPLETED, + ) + result = await handle.result() + assert isinstance(result, EchoOutput) + assert result.value == input_value + + +async def test_execute_operation(client: Client, env: WorkflowEnvironment): + """Use execute_operation convenience method, verify it returns result directly.""" + if env.supports_time_skipping: + pytest.skip( + "Standalone Nexus Operation tests don't work with time-skipping server" + ) + + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + + async with Worker( + client, + task_queue=task_queue, + nexus_service_handlers=[StandaloneTestServiceHandler()], + workflows=[EchoHandlerWorkflow, BlockingHandlerWorkflow], + ): + await env.create_nexus_endpoint(endpoint_name, task_queue) + + nexus_client = client.create_nexus_client( + service=StandaloneTestService, endpoint=endpoint_name + ) + result = await nexus_client.execute_operation( + StandaloneTestService.echo_sync, + EchoInput(value="execute"), + id=str(uuid.uuid4()), + id_reuse_policy=NexusOperationIDReusePolicy.REJECT_DUPLICATE, + id_conflict_policy=NexusOperationIDConflictPolicy.FAIL, + schedule_to_close_timeout=timedelta(seconds=10), + ) + assert isinstance(result, EchoOutput) + assert result.value == "execute" + + +async def test_execute_operation_named_service( + client: Client, env: WorkflowEnvironment +): + """Verify that the name on the service decorator is respected by the standalone nexus client""" + if env.supports_time_skipping: + pytest.skip( + "Standalone Nexus Operation tests don't work with time-skipping server" + ) + + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + + async with Worker( + client, + task_queue=task_queue, + # Register the standalone test service handler + nexus_service_handlers=[StandaloneTestServiceHandler()], + workflows=[EchoHandlerWorkflow, BlockingHandlerWorkflow], + ): + await env.create_nexus_endpoint(endpoint_name, task_queue) + + # Create client using the service that is uses the name "StandaloneTestService" + nexus_client = client.create_nexus_client( + service=NamedService, endpoint=endpoint_name + ) + result = await nexus_client.execute_operation( + NamedService.echo_sync, + EchoInput(value="execute"), + id=str(uuid.uuid4()), + id_reuse_policy=NexusOperationIDReusePolicy.REJECT_DUPLICATE, + id_conflict_policy=NexusOperationIDConflictPolicy.FAIL, + schedule_to_close_timeout=timedelta(seconds=10), + ) + assert isinstance(result, EchoOutput) + assert result.value == "execute" + + +async def test_errors(client: Client, env: WorkflowEnvironment): + """Execute operations that raise errors""" + if env.supports_time_skipping: + pytest.skip( + "Standalone Nexus Operation tests don't work with time-skipping server" + ) + + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + + async with Worker( + client, + task_queue=task_queue, + nexus_service_handlers=[StandaloneTestServiceHandler()], + workflows=[EchoHandlerWorkflow, BlockingHandlerWorkflow], + ): + await env.create_nexus_endpoint(endpoint_name, task_queue) + + nexus_client = client.create_nexus_client( + service=StandaloneTestService, endpoint=endpoint_name + ) + + handle = await nexus_client.start_operation( + StandaloneTestService.raise_err, + RaiseErrInput("handler_err"), + id=str(uuid.uuid4()), + id_reuse_policy=NexusOperationIDReusePolicy.REJECT_DUPLICATE, + id_conflict_policy=NexusOperationIDConflictPolicy.FAIL, + schedule_to_close_timeout=timedelta(seconds=30), + ) + + with pytest.raises(NexusOperationFailureError) as err: + await handle.result() + + assert err.value.__cause__ + assert isinstance(err.value.__cause__, nexusrpc.HandlerError) + + # test that the error is cached + with pytest.raises(NexusOperationFailureError) as second_err: + await handle.result() + assert err.value is second_err.value + + handle = await nexus_client.start_operation( + StandaloneTestService.raise_err, + RaiseErrInput("application_err"), + id=str(uuid.uuid4()), + id_reuse_policy=NexusOperationIDReusePolicy.REJECT_DUPLICATE, + id_conflict_policy=NexusOperationIDConflictPolicy.FAIL, + schedule_to_close_timeout=timedelta(seconds=30), + ) + + with pytest.raises(NexusOperationFailureError) as err: + await handle.result() + + assert err.value.__cause__ + assert isinstance(err.value.__cause__, nexusrpc.HandlerError) + assert err.value.__cause__.__cause__ + assert isinstance(err.value.__cause__.__cause__, ApplicationError) + + +async def test_describe_operation(client: Client, env: WorkflowEnvironment): + """Start op, get result first, then describe, verify fields populated.""" + if env.supports_time_skipping: + pytest.skip( + "Standalone Nexus Operation tests don't work with time-skipping server" + ) + + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + + async with Worker( + client, + task_queue=task_queue, + nexus_service_handlers=[StandaloneTestServiceHandler()], + workflows=[EchoHandlerWorkflow, BlockingHandlerWorkflow], + ): + await env.create_nexus_endpoint(endpoint_name, task_queue) + + nexus_client = client.create_nexus_client( + service=StandaloneTestService, endpoint=endpoint_name + ) + # Start an async operation and get its result first, then describe + handle = await nexus_client.start_operation( + StandaloneTestService.echo_async, + EchoInput(value="describe-me"), + id=str(uuid.uuid4()), + id_reuse_policy=NexusOperationIDReusePolicy.REJECT_DUPLICATE, + id_conflict_policy=NexusOperationIDConflictPolicy.FAIL, + schedule_to_close_timeout=timedelta(seconds=30), + summary=StandaloneTestService.echo_async.name, + ) + await handle.result() + + desc = await handle.describe() + assert desc.operation_id == handle.operation_id + assert desc.endpoint == endpoint_name + assert desc.service == "StandaloneTestService" + assert desc.operation == "echo_async" + assert desc.status == NexusOperationExecutionStatus.COMPLETED + assert desc.state == PendingNexusOperationExecutionState.UNSPECIFIED + assert desc.attempt >= 1 + assert desc.blocked_reason is None + assert desc.last_attempt_failure is None + summary = await desc.static_summary() + assert summary == StandaloneTestService.echo_async.name + + +async def test_cancel_operation(client: Client, env: WorkflowEnvironment): + """Start blocking async op, cancel it, verify awaiting result raises NexusOperationFailureError + from a CancelledError. + """ + if env.supports_time_skipping: + pytest.skip( + "Standalone Nexus Operation tests don't work with time-skipping server" + ) + + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + + async with Worker( + client, + task_queue=task_queue, + nexus_service_handlers=[StandaloneTestServiceHandler()], + workflows=[EchoHandlerWorkflow, BlockingHandlerWorkflow], + ): + await env.create_nexus_endpoint(endpoint_name, task_queue) + + nexus_client = client.create_nexus_client( + service=StandaloneTestService, endpoint=endpoint_name + ) + handle = await nexus_client.start_operation( + StandaloneTestService.blocking_async, + EchoInput(value="cancel-me"), + id=str(uuid.uuid4()), + id_reuse_policy=NexusOperationIDReusePolicy.REJECT_DUPLICATE, + id_conflict_policy=NexusOperationIDConflictPolicy.FAIL, + schedule_to_close_timeout=timedelta(seconds=30), + ) + + # Cancel the operation + await handle.cancel() + + with pytest.raises(NexusOperationFailureError) as err: + await handle.result() + + assert err.value.__cause__ + assert isinstance(err.value.__cause__, CancelledError) + + +async def test_terminate_operation(client: Client, env: WorkflowEnvironment): + """Start blocking async op, terminate it, verify awaiting the result raises NexusOperationFailureError + from a TerminatedError. + """ + if env.supports_time_skipping: + pytest.skip( + "Standalone Nexus Operation tests don't work with time-skipping server" + ) + + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + + async with Worker( + client, + task_queue=task_queue, + nexus_service_handlers=[StandaloneTestServiceHandler()], + workflows=[EchoHandlerWorkflow, BlockingHandlerWorkflow], + ): + await env.create_nexus_endpoint(endpoint_name, task_queue) + + nexus_client = client.create_nexus_client( + service=StandaloneTestService, endpoint=endpoint_name + ) + handle = await nexus_client.start_operation( + StandaloneTestService.blocking_async, + EchoInput(value="terminate-me"), + id=str(uuid.uuid4()), + id_reuse_policy=NexusOperationIDReusePolicy.REJECT_DUPLICATE, + id_conflict_policy=NexusOperationIDConflictPolicy.FAIL, + schedule_to_close_timeout=timedelta(seconds=30), + ) + + # Terminate the operation + await handle.terminate(reason="test termination") + + with pytest.raises(NexusOperationFailureError) as err: + await handle.result() + + assert err.value.__cause__ + assert isinstance(err.value.__cause__, TerminatedError) + + +async def test_list_operations(client: Client, env: WorkflowEnvironment): + """Start multiple ops, list them, verify iteration yields correct results.""" + if env.supports_time_skipping: + pytest.skip( + "Standalone Nexus Operation tests don't work with time-skipping server" + ) + + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + + async with Worker( + client, + task_queue=task_queue, + nexus_service_handlers=[StandaloneTestServiceHandler()], + workflows=[EchoHandlerWorkflow, BlockingHandlerWorkflow], + ): + await env.create_nexus_endpoint(endpoint_name, task_queue) + + nexus_client = client.create_nexus_client( + service=StandaloneTestService, endpoint=endpoint_name + ) + + # Start several blocking operations so they remain visible + op_ids: list[str] = [] + for i in range(3): + op_id = str(uuid.uuid4()) + op_ids.append(op_id) + await nexus_client.start_operation( + StandaloneTestService.blocking_async, + EchoInput(value=f"list-{i}"), + id=op_id, + id_reuse_policy=NexusOperationIDReusePolicy.REJECT_DUPLICATE, + id_conflict_policy=NexusOperationIDConflictPolicy.FAIL, + schedule_to_close_timeout=timedelta(seconds=30), + ) + + # Poll until all 3 operations appear (visibility is eventually consistent) + query = f'Endpoint = "{endpoint_name}"' + + async def check_ids() -> None: + found_ids: set[str] = set() + async for op_exec in client.list_nexus_operations(query): + found_ids.add(op_exec.operation_id) + assert all(op_id in found_ids for op_id in op_ids) + + await assert_eventually(check_ids) + + +async def test_count_operations(client: Client, env: WorkflowEnvironment): + """Start ops, count, verify count.""" + if env.supports_time_skipping: + pytest.skip( + "Standalone Nexus Operation tests don't work with time-skipping server" + ) + + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + + async with Worker( + client, + task_queue=task_queue, + nexus_service_handlers=[StandaloneTestServiceHandler()], + workflows=[EchoHandlerWorkflow, BlockingHandlerWorkflow], + ): + await env.create_nexus_endpoint(endpoint_name, task_queue) + + nexus_client = client.create_nexus_client( + service=StandaloneTestService, endpoint=endpoint_name + ) + + # Start some blocking operations + for i in range(2): + await nexus_client.start_operation( + StandaloneTestService.blocking_async, + EchoInput(value=f"count-{i}"), + id=str(uuid.uuid4()), + id_reuse_policy=NexusOperationIDReusePolicy.REJECT_DUPLICATE, + id_conflict_policy=NexusOperationIDConflictPolicy.FAIL, + schedule_to_close_timeout=timedelta(seconds=30), + ) + + # Poll until count >= 2 (visibility is eventually consistent) + query = f'Endpoint = "{endpoint_name}"' + + async def check_count() -> None: + count_result = await client.count_nexus_operations(query) + assert count_result.count >= 2 + + await assert_eventually(check_count) + + +async def test_get_nexus_operation_handle(client: Client, env: WorkflowEnvironment): + """Start op, get result, then get handle by ID and get result again.""" + if env.supports_time_skipping: + pytest.skip( + "Standalone Nexus Operation tests don't work with time-skipping server" + ) + + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + + async with Worker( + client, + task_queue=task_queue, + nexus_service_handlers=[StandaloneTestServiceHandler()], + workflows=[EchoHandlerWorkflow, BlockingHandlerWorkflow], + ): + await env.create_nexus_endpoint(endpoint_name, task_queue) + + nexus_client = client.create_nexus_client( + service=StandaloneTestService, endpoint=endpoint_name + ) + + op_id = str(uuid.uuid4()) + original_handle = await nexus_client.start_operation( + StandaloneTestService.echo_async, + EchoInput(value="handle-test"), + id=op_id, + id_reuse_policy=NexusOperationIDReusePolicy.REJECT_DUPLICATE, + id_conflict_policy=NexusOperationIDConflictPolicy.FAIL, + schedule_to_close_timeout=timedelta(seconds=30), + ) + # Get result from the original handle first + original_result = await original_handle.result() + assert isinstance(original_result, EchoOutput) + assert original_result.value == "handle-test" + + # Get a fresh handle by ID and get result again + handle = client.get_nexus_operation_handle( + op_id, operation=StandaloneTestService.echo_async + ) + result = await handle.result() + assert isinstance(result, EchoOutput) + assert result.value == "handle-test" + + +async def test_id_conflict_policy_use_existing( + client: Client, env: WorkflowEnvironment +): + """Start op, re-start with USE_EXISTING, verify same op/run ID and expected result""" + if env.supports_time_skipping: + pytest.skip( + "Standalone Nexus Operation tests don't work with time-skipping server" + ) + + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + + service_handler = StandaloneTestServiceHandler() + + async with Worker( + client, + task_queue=task_queue, + nexus_service_handlers=[service_handler], + workflows=[EchoHandlerWorkflow, BlockingHandlerWorkflow], + ): + await env.create_nexus_endpoint(endpoint_name, task_queue) + + nexus_client = client.create_nexus_client( + service=StandaloneTestService, endpoint=endpoint_name + ) + + op_id = str(uuid.uuid4()) + + # First start + handle = await nexus_client.start_operation( + StandaloneTestService.blocking_async, + EchoInput(value=task_queue), + id=op_id, + id_reuse_policy=NexusOperationIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy=NexusOperationIDConflictPolicy.USE_EXISTING, + schedule_to_close_timeout=timedelta(seconds=30), + ) + + # Second start with same ID and USE_EXISTING + handle2 = await nexus_client.start_operation( + StandaloneTestService.blocking_async, + EchoInput(value="second"), + id=op_id, + id_reuse_policy=NexusOperationIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy=NexusOperationIDConflictPolicy.USE_EXISTING, + schedule_to_close_timeout=timedelta(seconds=30), + ) + assert handle.operation_id == handle2.operation_id + assert handle.run_id == handle2.run_id + + # Let the nexus operation run and start the blocking workflow + await service_handler.started_blocking.wait() + + expected_wf_id = f"blocking_async-{task_queue}" + wf_handle = env.client.get_workflow_handle(expected_wf_id) + + await wf_handle.start_update( + BlockingHandlerWorkflow.unblock, + wait_for_stage=WorkflowUpdateStage.COMPLETED, + ) + + first_result = await handle.result() + second_result = await handle2.result() + assert first_result.value == task_queue + assert first_result.value == second_result.value + + +async def test_id_conflict_policy_fail(client: Client, env: WorkflowEnvironment): + """Start op, re-start with FAIL, verify raises NexusOperationAlreadyStartedError.""" + if env.supports_time_skipping: + pytest.skip( + "Standalone Nexus Operation tests don't work with time-skipping server" + ) + + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + + async with Worker( + client, + task_queue=task_queue, + nexus_service_handlers=[StandaloneTestServiceHandler()], + workflows=[EchoHandlerWorkflow, BlockingHandlerWorkflow], + ): + await env.create_nexus_endpoint(endpoint_name, task_queue) + + nexus_client = client.create_nexus_client( + service=StandaloneTestService, endpoint=endpoint_name + ) + + op_id = str(uuid.uuid4()) + + # First start + await nexus_client.start_operation( + StandaloneTestService.blocking_async, + EchoInput(value="first"), + id=op_id, + id_reuse_policy=NexusOperationIDReusePolicy.REJECT_DUPLICATE, + id_conflict_policy=NexusOperationIDConflictPolicy.FAIL, + schedule_to_close_timeout=timedelta(seconds=30), + ) + + # Second start with same ID and FAIL should raise + with pytest.raises(NexusOperationAlreadyStartedError): + await nexus_client.start_operation( + StandaloneTestService.blocking_async, + EchoInput(value="second"), + id=op_id, + id_reuse_policy=NexusOperationIDReusePolicy.REJECT_DUPLICATE, + id_conflict_policy=NexusOperationIDConflictPolicy.FAIL, + schedule_to_close_timeout=timedelta(seconds=30), + ) + + +# --------------------------------------------------------------------------- +# Interceptor test +# --------------------------------------------------------------------------- + + +class _RecordingOutboundInterceptor(OutboundInterceptor): + """Outbound interceptor that records calls to nexus operation methods.""" + + def __init__( + self, next: OutboundInterceptor, parent: _RecordingInterceptor + ) -> None: + super().__init__(next) + self._parent = parent + + async def start_nexus_operation( + self, input: StartNexusOperationInput + ) -> NexusOperationHandle[Any]: + self._parent.start_calls.append(input) + return await super().start_nexus_operation(input) + + async def describe_nexus_operation( + self, input: DescribeNexusOperationInput + ) -> NexusOperationExecutionDescription: + self._parent.describe_calls.append(input) + return await super().describe_nexus_operation(input) + + async def get_nexus_operation_result( + self, input: GetNexusOperationResultInput + ) -> Any: + self._parent.result_calls.append(input) + return await super().get_nexus_operation_result(input) + + async def cancel_nexus_operation(self, input: CancelNexusOperationInput) -> None: + self._parent.cancel_calls.append(input) + return await super().cancel_nexus_operation(input) + + async def terminate_nexus_operation( + self, input: TerminateNexusOperationInput + ) -> None: + self._parent.terminate_calls.append(input) + return await super().terminate_nexus_operation(input) + + def list_nexus_operations(self, input: ListNexusOperationsInput): + self._parent.list_calls.append(input) + return super().list_nexus_operations(input) + + async def count_nexus_operations(self, input: CountNexusOperationsInput): + self._parent.count_calls.append(input) + return await super().count_nexus_operations(input) + + +class _RecordingInterceptor(Interceptor): + """Client interceptor that records nexus operation calls.""" + + def __init__(self) -> None: + super().__init__() + self.start_calls: list[StartNexusOperationInput] = [] + self.describe_calls: list[DescribeNexusOperationInput] = [] + self.result_calls: list[GetNexusOperationResultInput] = [] + self.cancel_calls: list[CancelNexusOperationInput] = [] + self.terminate_calls: list[TerminateNexusOperationInput] = [] + self.list_calls: list[ListNexusOperationsInput] = [] + self.count_calls: list[CountNexusOperationsInput] = [] + + def intercept_client(self, next: OutboundInterceptor) -> OutboundInterceptor: + return _RecordingOutboundInterceptor(next, self) + + +async def test_interceptor_receives_inputs(client: Client, env: WorkflowEnvironment): + """Custom OutboundInterceptor records calls, verify correct input types.""" + if env.supports_time_skipping: + pytest.skip( + "Standalone Nexus Operation tests don't work with time-skipping server" + ) + + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + + interceptor = _RecordingInterceptor() + intercepted_client = Client( + service_client=client.service_client, + namespace=client.namespace, + interceptors=[interceptor], + ) + + async with Worker( + client, + task_queue=task_queue, + nexus_service_handlers=[StandaloneTestServiceHandler()], + workflows=[EchoHandlerWorkflow, BlockingHandlerWorkflow], + ): + await env.create_nexus_endpoint(endpoint_name, task_queue) + + nexus_client = intercepted_client.create_nexus_client( + service=StandaloneTestService, endpoint=endpoint_name + ) + + op_id = str(uuid.uuid4()) + + # Start operation -- should trigger start interceptor (with retry) + handle = await nexus_client.start_operation( + StandaloneTestService.blocking_async, + EchoInput(value=f"interceptor-test-{op_id}"), + id=op_id, + id_reuse_policy=NexusOperationIDReusePolicy.REJECT_DUPLICATE, + id_conflict_policy=NexusOperationIDConflictPolicy.FAIL, + schedule_to_close_timeout=timedelta(seconds=30), + schedule_to_start_timeout=timedelta(seconds=5), + start_to_close_timeout=timedelta(seconds=20), + ) + assert len(interceptor.start_calls) >= 1 + start_input = interceptor.start_calls[-1] + assert isinstance(start_input, StartNexusOperationInput) + assert start_input.id == op_id + assert start_input.operation == "blocking_async" + assert start_input.endpoint == endpoint_name + assert start_input.service == "StandaloneTestService" + assert start_input.schedule_to_start_timeout == timedelta(seconds=5) + assert start_input.start_to_close_timeout == timedelta(seconds=20) + + # Describe + await handle.describe() + assert len(interceptor.describe_calls) == 1 + desc_input = interceptor.describe_calls[0] + assert isinstance(desc_input, DescribeNexusOperationInput) + assert desc_input.operation_id == op_id + + # Cancel + await handle.cancel() + assert len(interceptor.cancel_calls) == 1 + cancel_input = interceptor.cancel_calls[0] + assert isinstance(cancel_input, CancelNexusOperationInput) + assert cancel_input.operation_id == op_id + + # GetResult + with pytest.raises(NexusOperationFailureError): + await handle.result() + assert len(interceptor.result_calls) == 1 + result_input = interceptor.result_calls[0] + assert isinstance(result_input, GetNexusOperationResultInput) + assert result_input.operation_id == op_id + assert result_input.result_type == EchoOutput + + # Start another so we can terminate it + previous_start_count = len(interceptor.start_calls) + op_id = str(uuid.uuid4()) + handle = await nexus_client.start_operation( + StandaloneTestService.blocking_async, + EchoInput(value=f"interceptor-test-{op_id}"), + id=op_id, + id_reuse_policy=NexusOperationIDReusePolicy.REJECT_DUPLICATE, + id_conflict_policy=NexusOperationIDConflictPolicy.FAIL, + schedule_to_close_timeout=timedelta(seconds=30), + ) + assert len(interceptor.start_calls) > previous_start_count + + # Terminate + await handle.terminate() + assert len(interceptor.terminate_calls) == 1 + terminate_input = interceptor.terminate_calls[0] + assert isinstance(terminate_input, TerminateNexusOperationInput) + assert terminate_input.operation_id == op_id + + query = f'`OperationId`="{op_id}"' + + # List Operations + # Iterate over list to ensure call is made + async for _ in intercepted_client.list_nexus_operations(query): + pass + assert len(interceptor.list_calls) >= 1 + list_input = interceptor.list_calls[-1] + assert isinstance(list_input, ListNexusOperationsInput) + assert list_input.query == query + + # Count Operations + await intercepted_client.count_nexus_operations(query) + assert len(interceptor.count_calls) >= 1 + count_input = interceptor.count_calls[-1] + assert isinstance(count_input, CountNexusOperationsInput) + assert count_input.query == query + + +async def _assert_workflow_started_with_nexus_operation_link( + client: Client, + workflow_id: str, + operation_handle: NexusOperationHandle[Any], +) -> WorkflowHistory: + history = await client.get_workflow_handle(workflow_id).fetch_history() + started_event = next( + ( + e + for e in history.events + if ( + e.event_type + == temporalio.api.enums.v1.EventType.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED + ) + ), + None, + ) + assert started_event is not None + + assert operation_handle.run_id is not None + assert_links_match( + links_from_workflow_execution_started_event(started_event), + expected_nexus_operation_link( + namespace=client.namespace, + operation_id=operation_handle.operation_id, + run_id=operation_handle.run_id, + ), + ) + return history + + +async def _assert_nexus_operation_has_link_to_started_workflow( + client: Client, + workflow_history: WorkflowHistory, + operation_handle: NexusOperationHandle[Any], +) -> None: + desc = await operation_handle.describe() + assert_links_match( + desc.raw_description.links, + expected_workflow_event_link( + namespace=client.namespace, + workflow_id=workflow_history.workflow_id, + run_id=workflow_history.run_id, + event_type=temporalio.api.enums.v1.EventType.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED, + event_id=workflow_history.events[0].event_id, + ), + ) diff --git a/tests/nexus/test_temporal_extstore.py b/tests/nexus/test_temporal_extstore.py new file mode 100644 index 000000000..f23a69259 --- /dev/null +++ b/tests/nexus/test_temporal_extstore.py @@ -0,0 +1,274 @@ +"""Integration tests for external storage with Nexus task processing. + +Mirrors sdk-typescript's test-integration-extstore-nexus. A caller workflow +invokes a Nexus operation whose input and/or result is large enough to be +offloaded to external storage. Verifies that the offloaded input is retrieved +before the handler runs, that a large synchronous result is offloaded when +completing the task (and retrieved by the caller), and that a transient +storage-driver failure fails the Nexus task retryably and then recovers. +""" + +from __future__ import annotations + +import dataclasses +import uuid +from collections.abc import Sequence +from datetime import timedelta +from typing import Any + +import nexusrpc +import pytest +from nexusrpc.handler import ( + StartOperationContext, + service_handler, + sync_operation, +) + +import temporalio.converter +from temporalio import workflow +from temporalio.api.common.v1 import Payload +from temporalio.client import Client, WorkflowFailureError +from temporalio.converter import ( + ExternalStorage, + StorageDriverClaim, + StorageDriverRetrieveContext, + StorageDriverStoreContext, +) +from temporalio.exceptions import ApplicationError, NexusOperationError +from temporalio.testing import WorkflowEnvironment +from temporalio.types import MethodAsyncSingleParam +from temporalio.worker import UnsandboxedWorkflowRunner, Worker +from tests.helpers.nexus import make_nexus_endpoint_name +from tests.test_extstore import InMemoryTestDriver + +# Cloud CI's namespace credentials cannot manage Nexus endpoints. +# See https://github.com/temporalio/sdk-python/issues/1704. +pytestmark = pytest.mark.requires_local_server + +PAYLOAD_SIZE = 4096 +PAYLOAD_SIZE_THRESHOLD = 1024 +_STORE_FAILURE_MESSAGE = "external storage store failed" + + +@nexusrpc.service +class ExtStoreNexusService: + size_op: nexusrpc.Operation[str, int] + big_result_op: nexusrpc.Operation[int, str] + + +@service_handler(service=ExtStoreNexusService) +class ExtStoreNexusServiceHandler: + @sync_operation + async def size_op(self, _ctx: StartOperationContext, data: str) -> int: + return len(data) + + @sync_operation + async def big_result_op(self, _ctx: StartOperationContext, size: int) -> str: + return "x" * size + + +@workflow.defn +class SizeOpCallerWorkflow: + """Calls ``size_op`` with a large input (offloaded) and returns its length.""" + + @workflow.run + async def run(self, task_queue: str) -> int: + nexus_client = workflow.create_nexus_client( + service=ExtStoreNexusService, + endpoint=make_nexus_endpoint_name(task_queue), + ) + return await nexus_client.execute_operation( + ExtStoreNexusService.size_op, "x" * PAYLOAD_SIZE + ) + + +@workflow.defn +class BigResultOpCallerWorkflow: + """Calls ``big_result_op`` (whose result is offloaded) and returns its length.""" + + @workflow.run + async def run(self, task_queue: str) -> int: + nexus_client = workflow.create_nexus_client( + service=ExtStoreNexusService, + endpoint=make_nexus_endpoint_name(task_queue), + ) + result = await nexus_client.execute_operation( + ExtStoreNexusService.big_result_op, PAYLOAD_SIZE + ) + return len(result) + + +class TransientFailureDriver(InMemoryTestDriver): + """In-memory driver that fails the first store and/or retrieve call, then + behaves normally. Simulates a transient storage-driver outage.""" + + def __init__( + self, + *, + fail_first_store: bool = False, + fail_first_retrieve: bool = False, + driver_name: str = "test-driver", + ): + super().__init__(driver_name=driver_name) + self._fail_first_store = fail_first_store + self._fail_first_retrieve = fail_first_retrieve + self.store_attempts = 0 + self.retrieve_attempts = 0 + + async def store( + self, + context: StorageDriverStoreContext, + payloads: Sequence[Payload], + ) -> list[StorageDriverClaim]: + self.store_attempts += 1 + if self._fail_first_store and self.store_attempts == 1: + raise RuntimeError("transient store failure") + return await super().store(context, payloads) + + async def retrieve( + self, + context: StorageDriverRetrieveContext, + claims: Sequence[StorageDriverClaim], + ) -> list[Payload]: + self.retrieve_attempts += 1 + if self._fail_first_retrieve and self.retrieve_attempts == 1: + raise RuntimeError("transient retrieve failure") + return await super().retrieve(context, claims) + + +def _client_with_extstore( + env: WorkflowEnvironment, driver: InMemoryTestDriver +) -> Client: + config = env.client.config() + config["data_converter"] = dataclasses.replace( + temporalio.converter.default(), + external_storage=ExternalStorage( + drivers=[driver], + payload_size_threshold=PAYLOAD_SIZE_THRESHOLD, + ), + ) + return Client(**config) + + +async def _run_caller( + env: WorkflowEnvironment, + driver: InMemoryTestDriver, + workflow_run: MethodAsyncSingleParam[Any, str, int], +) -> int: + client = _client_with_extstore(env, driver) + task_queue = str(uuid.uuid4()) + async with Worker( + client, + task_queue=task_queue, + workflows=[SizeOpCallerWorkflow, BigResultOpCallerWorkflow], + nexus_service_handlers=[ExtStoreNexusServiceHandler()], + workflow_runner=UnsandboxedWorkflowRunner(), + ): + await env.create_nexus_endpoint( + make_nexus_endpoint_name(task_queue), task_queue + ) + return await client.execute_workflow( + workflow_run, + task_queue, + id=str(uuid.uuid4()), + task_queue=task_queue, + execution_timeout=timedelta(seconds=30), + ) + + +def _cause_chain(err: BaseException) -> list[BaseException]: + chain: list[BaseException] = [] + e: BaseException | None = err + while e is not None: + chain.append(e) + e = e.__cause__ + return chain + + +async def test_nexus_operation_input_offloaded_and_retrieved(env: WorkflowEnvironment): + """The offloaded operation input is retrieved before the handler runs.""" + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work with the Java test server") + + driver = InMemoryTestDriver() + result = await _run_caller(env, driver, SizeOpCallerWorkflow.run) + + assert result == PAYLOAD_SIZE + assert driver._store_calls >= 1 + assert driver._retrieve_calls >= 1 + + +async def test_nexus_operation_sync_result_offloaded_and_retrieved( + env: WorkflowEnvironment, +): + """A large synchronous result is offloaded and retrieved by the caller.""" + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work with the Java test server") + + driver = InMemoryTestDriver() + result = await _run_caller(env, driver, BigResultOpCallerWorkflow.run) + + assert result == PAYLOAD_SIZE + assert driver._store_calls >= 1 + assert driver._retrieve_calls >= 1 + + +async def test_nexus_operation_transient_retrieve_failure_recovers( + env: WorkflowEnvironment, +): + """A transient retrieve failure fails the task retryably; it then recovers.""" + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work with the Java test server") + + driver = TransientFailureDriver(fail_first_retrieve=True) + result = await _run_caller(env, driver, SizeOpCallerWorkflow.run) + + assert result == PAYLOAD_SIZE + assert driver.retrieve_attempts >= 2 + + +async def test_nexus_operation_transient_store_failure_recovers( + env: WorkflowEnvironment, +): + """A transient store failure fails the task retryably; it then recovers.""" + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work with the Java test server") + + driver = TransientFailureDriver(fail_first_store=True) + result = await _run_caller(env, driver, BigResultOpCallerWorkflow.run) + + assert result == PAYLOAD_SIZE + assert driver.store_attempts >= 2 + + +class PermanentFailStoreDriver(InMemoryTestDriver): + """Store always fails non-retryably, so the Nexus operation fails permanently.""" + + async def store( + self, + context: StorageDriverStoreContext, + payloads: Sequence[Payload], + ) -> list[StorageDriverClaim]: + raise ApplicationError(_STORE_FAILURE_MESSAGE, non_retryable=True) + + +async def test_nexus_operation_store_failure_fails_operation( + env: WorkflowEnvironment, +): + """A non-retryable store failure fails the operation and surfaces the driver + error to the caller (deterministically, with no retries).""" + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work with the Java test server") + + driver = PermanentFailStoreDriver() + with pytest.raises(WorkflowFailureError) as exc_info: + await _run_caller(env, driver, BigResultOpCallerWorkflow.run) + + causes = _cause_chain(exc_info.value) + assert [type(c) for c in causes] == [ + WorkflowFailureError, + NexusOperationError, + nexusrpc.HandlerError, + ApplicationError, + ] + assert _STORE_FAILURE_MESSAGE in str(causes[-1]) diff --git a/tests/nexus/test_temporal_operation.py b/tests/nexus/test_temporal_operation.py new file mode 100644 index 000000000..85948deb3 --- /dev/null +++ b/tests/nexus/test_temporal_operation.py @@ -0,0 +1,1659 @@ +import asyncio +import uuid +from dataclasses import dataclass +from datetime import timedelta + +import nexusrpc +import pytest +from nexusrpc import HandlerErrorType, Operation, service +from nexusrpc.handler import ( + CancelOperationContext, + OperationTaskCancellation, + operation_handler, + service_handler, +) +from typing_extensions import override + +import temporalio.exceptions +from temporalio import activity, nexus, workflow +from temporalio.api.activity.v1 import ActivityExecutionInfo +from temporalio.api.common.v1 import Link +from temporalio.client import ( + ActivityExecutionStatus, + Client, + NexusOperationFailureError, + WorkflowExecutionStatus, + WorkflowFailureError, +) +from temporalio.common import ( + NexusOperationExecutionStatus, + RetryPolicy, + WorkflowIDConflictPolicy, +) +from temporalio.nexus._token import OperationToken, OperationTokenType +from temporalio.testing import WorkflowEnvironment +from temporalio.worker import Worker +from tests.helpers import EventType, assert_event_subsequence, assert_eventually +from tests.helpers.nexus import ( + assert_links_match, + expected_nexus_operation_link, + make_nexus_endpoint_name, +) + +# Cloud CI's namespace credentials cannot manage Nexus endpoints. +# See https://github.com/temporalio/sdk-python/issues/1704. +pytestmark = pytest.mark.requires_local_server + + +@dataclass +class Input: + value: str + task_queue: str + update_value: str = "" + update_id: str = "" + expect_sync_response: bool = False + + +def test_temporal_operation_result_validates_single_result_kind() -> None: + assert nexus.TemporalOperationResult.sync(None).value is None + assert nexus.TemporalOperationResult.async_token("token").token == "token" + + with pytest.raises(ValueError, match="exactly one of value or token"): + nexus.TemporalOperationResult() + + with pytest.raises(ValueError, match="exactly one of value or token"): + nexus.TemporalOperationResult(value="value", token="token") + + +def test_temporal_operation_result_validates_token() -> None: + with pytest.raises(ValueError, match="non-empty string"): + nexus.TemporalOperationResult.async_token("") + + with pytest.raises(ValueError, match="non-empty string"): + nexus.TemporalOperationResult(token="") + + with pytest.raises(ValueError, match="non-empty string"): + nexus.TemporalOperationResult(token=123) # type: ignore + + +@workflow.defn +class EchoWorkflow: + @workflow.run + async def run(self, input: Input) -> str: + return input.value + + +@activity.defn +async def echo_activity(input: Input) -> str: + return input.value + + +@activity.defn +async def raise_error_activity() -> None: + raise temporalio.exceptions.ApplicationError( + "test-activity-error-message", + type="test-activity-error-type", + non_retryable=True, + ) + + +@activity.defn +async def wait_for_cancel_activity() -> None: + # Heartbeat in a loop so the activity receives cancellation. Letting the + # resulting CancelledError bubble out transitions the activity to CANCELED. + while True: + await asyncio.sleep(0.3) + activity.heartbeat() + + +@service +class TestService: + echo: Operation[Input, str] + blocking: Operation[None, None] + double_start: Operation[Input, None] + concurrent_start: Operation[Input, str] + retry_after_failed_start: Operation[Input, str] + sync_result: Operation[Input, str] + custom_cancel: Operation[str, None] + update_op: Operation[Input, str] + echo_activity: Operation[Input, str] + error_activity: Operation[Input, None] + blocking_activity: Operation[str, None] + custom_cancel_activity: Operation[str, None] + double_start_activity: Operation[Input, None] + mixed_start: Operation[Input, None] + + +@service_handler(service=TestService) +class TestServiceHandler: + # tell Pytest this is not a test class + __test__ = False + + def __init__(self) -> None: + self.started_custom_cancel_workflow = asyncio.Event() + self.started_custom_cancel_activity = asyncio.Event() + self.custom_cancel_activity_called = asyncio.Event() + + @nexus.temporal_operation + async def echo( + self, + _ctx: nexus.TemporalStartOperationContext, + client: nexus.TemporalNexusClient, + input: Input, + ) -> nexus.TemporalOperationResult[str]: + return await client.start_workflow( + EchoWorkflow.run, input, id=f"echo-{input.value}" + ) + + @nexus.temporal_operation + async def blocking( + self, + _ctx: nexus.TemporalStartOperationContext, + client: nexus.TemporalNexusClient, + _input: None, + ) -> nexus.TemporalOperationResult[None]: + return await client.start_workflow( + BlockingWorkflow.run, id=f"blocking-{uuid.uuid4()}" + ) + + @nexus.temporal_operation + async def double_start( + self, + _ctx: nexus.TemporalStartOperationContext, + client: nexus.TemporalNexusClient, + input: Input, + ) -> nexus.TemporalOperationResult[None]: + await client.start_workflow( + EchoWorkflow.run, input, id=f"double-start-{uuid.uuid4()}" + ) + await client.start_workflow( + EchoWorkflow.run, input, id=f"double-start-{uuid.uuid4()}" + ) + return nexus.TemporalOperationResult.sync(None) + + @nexus.temporal_operation + async def concurrent_start( + self, + _ctx: nexus.TemporalStartOperationContext, + client: nexus.TemporalNexusClient, + input: Input, + ) -> nexus.TemporalOperationResult[str]: + results = await asyncio.gather( + client.start_workflow( + EchoWorkflow.run, + input, + id=f"concurrent-start-1-{uuid.uuid4()}", + ), + client.start_workflow( + EchoWorkflow.run, + input, + id=f"concurrent-start-2-{uuid.uuid4()}", + ), + return_exceptions=True, + ) + + async_results: list[nexus.TemporalOperationResult[str]] = [] + handler_errors: list[nexusrpc.HandlerError] = [] + for result in results: + if isinstance(result, nexus.TemporalOperationResult): + async_results.append(result) + elif isinstance(result, nexusrpc.HandlerError): + handler_errors.append(result) + elif isinstance(result, BaseException): + raise result + else: + raise RuntimeError(f"Unexpected concurrent start result: {result}") + + if ( + len(async_results) == 1 + and len(handler_errors) == 1 + and handler_errors[0].type == HandlerErrorType.BAD_REQUEST + ): + return async_results[0] + + raise RuntimeError( + "Expected one async workflow start and one BAD_REQUEST HandlerError, " + f"got {len(async_results)} starts and {len(handler_errors)} handler errors" + ) + + @nexus.temporal_operation + async def retry_after_failed_start( + self, + _ctx: nexus.TemporalStartOperationContext, + client: nexus.TemporalNexusClient, + input: Input, + ) -> nexus.TemporalOperationResult[str]: + try: + await client.start_workflow( + BlockingWorkflow.run, + id=input.value, + id_conflict_policy=WorkflowIDConflictPolicy.FAIL, + ) + except temporalio.exceptions.WorkflowAlreadyStartedError: + return await client.start_workflow( + EchoWorkflow.run, + input, + id=f"retry-after-failed-start-{uuid.uuid4()}", + ) + + raise RuntimeError("Expected first workflow start to fail") + + @nexus.temporal_operation + async def sync_result( + self, + _ctx: nexus.TemporalStartOperationContext, + _client: nexus.TemporalNexusClient, + input: Input, + ) -> nexus.TemporalOperationResult[str]: + return nexus.TemporalOperationResult.sync(input.value) + + @operation_handler + def custom_cancel(self) -> nexus.TemporalOperationHandler[str, None]: + event = self.started_custom_cancel_workflow + + class CustomCancelNexusOpHandler(nexus.TemporalOperationHandler[str, None]): + @override + async def start_operation( + self, + ctx: nexus.TemporalStartOperationContext, + client: nexus.TemporalNexusClient, + input: str, + ) -> nexus.TemporalOperationResult[None]: + result = await client.start_workflow(BlockingWorkflow.run, id=input) + event.set() + return result + + @override + async def cancel_workflow_run( + self, + ctx: nexus.TemporalCancelOperationContext, + options: nexus.CancelWorkflowRunOptions, + ): + # get a handle to the workflow + handle = nexus.client().get_workflow_handle(options.workflow_id) + + # cancel the workflow + await handle.cancel() + + return CustomCancelNexusOpHandler() + + @nexus.temporal_operation + async def update_op( + self, + _ctx: nexus.TemporalStartOperationContext, + client: nexus.TemporalNexusClient, + input: Input, + ) -> nexus.TemporalOperationResult[str]: + # input.value carries the target workflow_id, input.update_value has actual update + return await client.start_workflow_update( + input.value, + UpdatableWorkflow.do_update, + input.update_value, + update_id=input.update_id, + ) + + @nexus.temporal_operation + async def echo_activity( + self, + _ctx: nexus.TemporalStartOperationContext, + client: nexus.TemporalNexusClient, + input: Input, + ) -> nexus.TemporalOperationResult[str]: + return await client.start_activity( + echo_activity, + input, + id=f"echo_activity-{uuid.uuid4()}", + start_to_close_timeout=timedelta(seconds=5), + ) + + @nexus.temporal_operation + async def error_activity( + self, + _ctx: nexus.TemporalStartOperationContext, + client: nexus.TemporalNexusClient, + _input: Input, + ) -> nexus.TemporalOperationResult[None]: + # The activity raises immediately. With a single permitted attempt it + # fails the backing activity, which in turn fails the Nexus operation. + return await client.start_activity( + raise_error_activity, + id=f"error_activity-{uuid.uuid4()}", + start_to_close_timeout=timedelta(seconds=5), + retry_policy=RetryPolicy(maximum_attempts=1), + ) + + @nexus.temporal_operation + async def blocking_activity( + self, + _ctx: nexus.TemporalStartOperationContext, + client: nexus.TemporalNexusClient, + input: str, + ) -> nexus.TemporalOperationResult[None]: + return await client.start_activity( + wait_for_cancel_activity, + id=input, + start_to_close_timeout=timedelta(seconds=30), + heartbeat_timeout=timedelta(seconds=1), + ) + + @nexus.temporal_operation + async def double_start_activity( + self, + _ctx: nexus.TemporalStartOperationContext, + client: nexus.TemporalNexusClient, + _input: Input, + ) -> nexus.TemporalOperationResult[None]: + # Keep the first activity running so its callback cannot race the + # handler error raised by the second start. + await client.start_activity( + wait_for_cancel_activity, + id=f"double-start-activity-{uuid.uuid4()}", + start_to_close_timeout=timedelta(seconds=30), + heartbeat_timeout=timedelta(seconds=1), + ) + await client.start_activity( + wait_for_cancel_activity, + id=f"double-start-activity-{uuid.uuid4()}", + start_to_close_timeout=timedelta(seconds=30), + heartbeat_timeout=timedelta(seconds=1), + ) + return nexus.TemporalOperationResult.sync(None) + + @nexus.temporal_operation + async def mixed_start( + self, + _ctx: nexus.TemporalStartOperationContext, + client: nexus.TemporalNexusClient, + input: Input, + ) -> nexus.TemporalOperationResult[None]: + # Starting a workflow reserves the single async start, so the subsequent + # start_activity must hit the same guard and raise a BAD_REQUEST error. + await client.start_workflow( + EchoWorkflow.run, input, id=f"mixed-start-{uuid.uuid4()}" + ) + await client.start_activity( + echo_activity, + input, + id=f"mixed-start-{uuid.uuid4()}", + start_to_close_timeout=timedelta(seconds=5), + ) + return nexus.TemporalOperationResult.sync(None) + + @operation_handler + def custom_cancel_activity(self) -> nexus.TemporalOperationHandler[str, None]: + started = self.started_custom_cancel_activity + cancel_called = self.custom_cancel_activity_called + + class CustomCancelActivityNexusOpHandler( + nexus.TemporalOperationHandler[str, None] + ): + @override + async def start_operation( + self, + ctx: nexus.TemporalStartOperationContext, + client: nexus.TemporalNexusClient, + input: str, + ) -> nexus.TemporalOperationResult[None]: + result = await client.start_activity( + wait_for_cancel_activity, + id=input, + start_to_close_timeout=timedelta(seconds=30), + heartbeat_timeout=timedelta(seconds=1), + ) + started.set() + return result + + @override + async def cancel_activity( + self, + ctx: nexus.TemporalCancelOperationContext, + options: nexus.CancelActivityOptions, + ): + # record that the custom override ran + cancel_called.set() + + # get a handle to the activity and cancel it + handle = nexus.client().get_activity_handle(options.activity_id) + await handle.cancel() + + return CustomCancelActivityNexusOpHandler() + + +@workflow.defn +class EchoWorkflowCaller: + @workflow.run + async def run(self, input: Input) -> str: + client = workflow.create_nexus_client( + service=TestService, endpoint=make_nexus_endpoint_name(input.task_queue) + ) + return await client.execute_operation(TestService.echo, input) + + +async def test_temporal_operation_start_workflow( + client: Client, env: WorkflowEnvironment +): + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) + async with Worker( + env.client, + task_queue=task_queue, + nexus_service_handlers=[TestServiceHandler()], + workflows=[EchoWorkflow, EchoWorkflowCaller], + ): + wf_handle = await client.start_workflow( + EchoWorkflowCaller.run, + Input(value="test", task_queue=task_queue), + task_queue=task_queue, + id=str(uuid.uuid4()), + ) + result = await wf_handle.result() + assert result == "test" + + await assert_event_subsequence( + wf_handle, + [ + EventType.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED, + EventType.EVENT_TYPE_NEXUS_OPERATION_STARTED, + EventType.EVENT_TYPE_NEXUS_OPERATION_COMPLETED, + ], + ) + + +async def test_temporal_operation_update_workflow( + client: Client, env: WorkflowEnvironment +) -> None: + if ( + env.supports_time_skipping + ): # time skipping server uses different dynamic configs + pytest.skip("Update workflow tests don't work with time-skipping server") + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) + async with Worker( + env.client, + task_queue=task_queue, + nexus_service_handlers=[TestServiceHandler()], + workflows=[UpdatableWorkflow, UpdateWorkflowCaller], + ): + update_workflow_id = f"updatable-workflow-{uuid.uuid4()}" + target_handle = await client.start_workflow( + UpdatableWorkflow.run, id=update_workflow_id, task_queue=task_queue + ) + + async def check_simple_update_and_links(): + """Run an update, check state changes from pending to created, verify forward and back links are correct""" + wf_handle = await client.start_workflow( + UpdateWorkflowCaller.run, + Input( + value=update_workflow_id, + task_queue=task_queue, + update_value="Created", + ), + task_queue=task_queue, + id=f"update-workflow-caller-created-{uuid.uuid4()}", + ) + result = await wf_handle.result() + assert result == "Updated workflow status from Pending to Created" + # assert expected events are in expected sequence in caller history + await assert_event_subsequence( + wf_handle, + [ + EventType.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED, + EventType.EVENT_TYPE_NEXUS_OPERATION_STARTED, + EventType.EVENT_TYPE_NEXUS_OPERATION_COMPLETED, + ], + ) + # now, check the links + caller_history = await wf_handle.fetch_history() + handler_history = await target_handle.fetch_history() + scheduled_event = next( + e + for e in caller_history.events + if e.event_type == EventType.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED + ) + caller_request_id = ( + scheduled_event.nexus_operation_scheduled_event_attributes.request_id + ) + assert target_handle.result_run_id is not None + # from caller ns to target ns + expected_forward_link = Link( + workflow_event=Link.WorkflowEvent( + namespace=client.namespace, + workflow_id=update_workflow_id, + run_id=target_handle.result_run_id, + request_id_ref=Link.WorkflowEvent.RequestIdReference( + request_id=caller_request_id, + event_type=EventType.EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_ACCEPTED, + ), + ) + ) + assert wf_handle.result_run_id is not None + # from target ns back to caller ns + expected_backward_link = Link( + workflow_event=Link.WorkflowEvent( + namespace=client.namespace, + workflow_id=wf_handle.id, + run_id=wf_handle.result_run_id, + event_ref=Link.WorkflowEvent.EventReference( + event_id=scheduled_event.event_id, + event_type=EventType.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED, + ), + ) + ) + caller_links = [ + link + for e in caller_history.events + if e.event_type == EventType.EVENT_TYPE_NEXUS_OPERATION_STARTED + for link in e.links + ] + handler_links = [ + link + for e in handler_history.events + if e.event_type + == EventType.EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_ACCEPTED + for link in e.links + ] + assert expected_forward_link in caller_links + assert expected_backward_link in handler_links + + async def check_sequential_updates_consistent(): + """Run updates back-to-back, verify update isnt re-processed""" + stable_update_id = "sequential-update" + wf_handle = await client.start_workflow( + UpdateWorkflowCaller.run, + Input( + value=update_workflow_id, + task_queue=task_queue, + update_value="Processed", + update_id=stable_update_id, + ), + task_queue=task_queue, + id="sequential-update-workflow-caller-processed-0", + ) + result = await wf_handle.result() + assert result == "Updated workflow status from Created to Processed" + + # same update_id -> wont be processed again, receives a sync result + wf_handle = await client.start_workflow( + UpdateWorkflowCaller.run, + Input( + value=update_workflow_id, + task_queue=task_queue, + update_value="Processed", + update_id=stable_update_id, + expect_sync_response=True, + ), + task_queue=task_queue, + id="sequential-update-workflow-caller-processed-1", + ) + result = await wf_handle.result() + assert result == "Updated workflow status from Created to Processed" + + async def check_parallel_updates_idempotent_and_finish(): + """Run multiple updates in parallel, verify they are idempotent and finish with same result""" + stable_id = "parallel-updates-id" + num_parallel = 3 + gate = asyncio.Event() + + async def run_update(i: int) -> str: + await gate.wait() + wf_handle = await client.start_workflow( + UpdateWorkflowCaller.run, + Input( + value=update_workflow_id, + task_queue=task_queue, + update_value="Completed", + update_id=stable_id, + ), + task_queue=task_queue, + id=f"parallel-update-workflow-caller-completed-{i}", + ) + return await wf_handle.result() + + tasks = [asyncio.create_task(run_update(i)) for i in range(num_parallel)] + gate.set() + results = await asyncio.gather(*tasks) + + for result in results: + assert result == "Updated workflow status from Processed to Completed" + + async def check_updates_on_completed_workflows_fail(): + """The handler workflow already finished at this point, further updaes should just fail""" + wf_handle = await client.start_workflow( + UpdateWorkflowCaller.run, + Input( + value=update_workflow_id, + task_queue=task_queue, + update_value="dummy, will fail anyway", + ), + task_queue=task_queue, + id=f"{uuid.uuid4()}", + ) + with pytest.raises(WorkflowFailureError): + await wf_handle.result() + + await check_simple_update_and_links() + await check_sequential_updates_consistent() + await check_parallel_updates_idempotent_and_finish() + await check_updates_on_completed_workflows_fail() + + +async def test_temporal_operation_update_workflow_delayed( + client: Client, env: WorkflowEnvironment +) -> None: + if ( + env.supports_time_skipping + ): # time skipping server uses different dynamic configs + pytest.skip("Update workflow tests don't work with time-skipping server") + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) + + update_workflow_id = f"another-updatable-workflow-{uuid.uuid4()}" + + # start both caller and handler without starting worker + wf_handle = await client.start_workflow( + UpdateWorkflowCaller.run, + Input( + value=update_workflow_id, + task_queue=task_queue, + update_value="Completed", + ), + task_queue=task_queue, + id=f"update-workflow-caller-created-{uuid.uuid4()}", + ) + target_handle = await client.start_workflow( + UpdatableWorkflow.run, id=update_workflow_id, task_queue=task_queue + ) + + # now, start the worker, it should process both the handler + # and the caller and finish the enqueued update + async with Worker( + env.client, + task_queue=task_queue, + nexus_service_handlers=[TestServiceHandler()], + workflows=[UpdatableWorkflow, UpdateWorkflowCaller], + ): + result = await wf_handle.result() + assert result == "Updated workflow status from Pending to Completed" + + await assert_event_subsequence( + wf_handle, + [ + EventType.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED, + EventType.EVENT_TYPE_NEXUS_OPERATION_STARTED, + EventType.EVENT_TYPE_NEXUS_OPERATION_COMPLETED, + ], + ) + + caller_history = await wf_handle.fetch_history() + handler_history = await target_handle.fetch_history() + scheduled_event = next( + e + for e in caller_history.events + if e.event_type == EventType.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED + ) + caller_request_id = ( + scheduled_event.nexus_operation_scheduled_event_attributes.request_id + ) + assert target_handle.result_run_id is not None + expected_forward_link = Link( + workflow_event=Link.WorkflowEvent( + namespace=client.namespace, + workflow_id=update_workflow_id, + run_id=target_handle.result_run_id, + request_id_ref=Link.WorkflowEvent.RequestIdReference( + request_id=caller_request_id, + event_type=EventType.EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_ACCEPTED, + ), + ) + ) + assert wf_handle.result_run_id is not None + expected_backward_link = Link( + workflow_event=Link.WorkflowEvent( + namespace=client.namespace, + workflow_id=wf_handle.id, + run_id=wf_handle.result_run_id, + event_ref=Link.WorkflowEvent.EventReference( + event_id=scheduled_event.event_id, + event_type=EventType.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED, + ), + ) + ) + caller_links = [ + link + for e in caller_history.events + if e.event_type == EventType.EVENT_TYPE_NEXUS_OPERATION_STARTED + for link in e.links + ] + handler_links = [ + link + for e in handler_history.events + if e.event_type == EventType.EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_ACCEPTED + for link in e.links + ] + assert expected_forward_link in caller_links + assert expected_backward_link in handler_links + + +async def test_temporal_operation_cancel_rejects_unknown_tokens(): + class FakeNexusTaskCancellation(OperationTaskCancellation): + def is_cancelled(self) -> bool: + return False + + def cancellation_reason(self) -> str | None: + return None + + def wait_until_cancelled_sync(self, timeout: float | None = None) -> bool: + return False + + async def wait_until_cancelled(self) -> None: + return None + + def cancel(self, _reason: str) -> bool: + return False + + cancel_ctx = CancelOperationContext( + service="TestService", + operation="echo", + headers={}, + task_cancellation=FakeNexusTaskCancellation(), + ) + + service_handler = TestServiceHandler() + + # Use a factory style operation form the handler to allow calling cancel directly + op_handler = service_handler.custom_cancel() + + # Invalid token type + token = OperationToken(type=30, namespace="default") # type: ignore + with pytest.raises(nexusrpc.HandlerError) as err: + await op_handler.cancel(cancel_ctx, token.encode()) + assert err.value.type == HandlerErrorType.INTERNAL + assert not err.value.retryable + underlying = err.value.__cause__ + assert isinstance(underlying, TypeError) + assert "unknown token type, got 30" in str(underlying) + + # Workflow ID missing from workflow type + token = OperationToken(type=OperationTokenType.WORKFLOW, namespace="default") + with pytest.raises(nexusrpc.HandlerError) as err: + await op_handler.cancel(cancel_ctx, token.encode()) + assert err.value.type == HandlerErrorType.INTERNAL + assert not err.value.retryable + underlying = err.value.__cause__ + assert isinstance(underlying, TypeError) + assert "expected non-empty workflow id for token type `WORKFLOW`" in str(underlying) + + # Activity ID missing from activity type + token = OperationToken(type=OperationTokenType.ACTIVITY, namespace="default") + with pytest.raises(nexusrpc.HandlerError) as err: + await op_handler.cancel(cancel_ctx, token.encode()) + assert err.value.type == HandlerErrorType.INTERNAL + assert not err.value.retryable + underlying = err.value.__cause__ + assert isinstance(underlying, TypeError) + assert "expected non-empty activity id for token type `ACTIVITY`" in str(underlying) + + activity_op_handler = service_handler.custom_cancel_activity() + for run_id in (None, ""): + token = OperationToken( + type=OperationTokenType.ACTIVITY, + namespace="default", + activity_id="activity-id", + run_id=run_id, + ) + with pytest.raises(nexusrpc.HandlerError) as err: + await activity_op_handler.cancel(cancel_ctx, token.encode()) + assert err.value.type == HandlerErrorType.INTERNAL + assert not err.value.retryable + assert not service_handler.custom_cancel_activity_called.is_set() + + +@workflow.defn +class BlockingWorkflow: + def __init__(self) -> None: + self.done: bool = False + + @workflow.run + async def run(self) -> None: + await workflow.wait_condition(lambda: self.done) + + @workflow.update + async def unblock(self): + self.done = True + + +@workflow.defn +class CancelBlockingWorkflowCaller: + op_started = False + + @workflow.run + async def run(self, input: Input) -> None: + client = workflow.create_nexus_client( + service=TestService, endpoint=make_nexus_endpoint_name(input.task_queue) + ) + op_handle = await client.start_operation(TestService.blocking, None) + self.op_started = True + return await op_handle + + @workflow.update + async def wait_operation_started(self): + await workflow.wait_condition(lambda: self.op_started) + + +async def test_temporal_operation_cancel_workflow( + client: Client, env: WorkflowEnvironment +): + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) + async with Worker( + env.client, + task_queue=task_queue, + nexus_service_handlers=[TestServiceHandler()], + workflows=[BlockingWorkflow, CancelBlockingWorkflowCaller], + ): + wf_handle = await client.start_workflow( + CancelBlockingWorkflowCaller.run, + Input(value="test", task_queue=task_queue), + task_queue=task_queue, + id=f"blocking-{uuid.uuid4()}", + ) + + await wf_handle.execute_update( + CancelBlockingWorkflowCaller.wait_operation_started + ) + + await wf_handle.cancel() + + await assert_event_subsequence( + wf_handle, + [ + EventType.EVENT_TYPE_NEXUS_OPERATION_CANCEL_REQUESTED, + EventType.EVENT_TYPE_NEXUS_OPERATION_CANCEL_REQUEST_COMPLETED, + EventType.EVENT_TYPE_NEXUS_OPERATION_CANCELED, + ], + ) + + +async def test_customized_temporal_operation_cancel_workflow( + client: Client, env: WorkflowEnvironment +): + if env.supports_time_skipping: + pytest.skip( + "Standalone Nexus Operation tests don't work with time-skipping server" + ) + + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) + + service_handler = TestServiceHandler() + async with Worker( + env.client, + task_queue=task_queue, + nexus_service_handlers=[service_handler], + workflows=[BlockingWorkflow, CancelBlockingWorkflowCaller], + ): + nexus_client = client.create_nexus_client(TestService, endpoint_name) + + wf_id = f"custom-cancel-{uuid.uuid4()}" + op_handle = await nexus_client.start_operation( + TestService.custom_cancel, wf_id, id=str(uuid.uuid4()) + ) + + await service_handler.started_custom_cancel_workflow.wait() + + await op_handle.cancel() + + async def check_cancelled(): + wf_handle = client.get_workflow_handle(wf_id) + wf_desc = await wf_handle.describe() + assert wf_desc.status is WorkflowExecutionStatus.CANCELED + op_desc = await op_handle.describe() + assert op_desc.status is NexusOperationExecutionStatus.CANCELED + + await assert_eventually(check_cancelled) + + +@workflow.defn +class DoubleStartWorkflowCaller: + @workflow.run + async def run(self, input: Input) -> None: + client = workflow.create_nexus_client( + service=TestService, endpoint=make_nexus_endpoint_name(input.task_queue) + ) + op_handle = await client.start_operation(TestService.double_start, input) + return await op_handle + + +@workflow.defn +class ConcurrentStartWorkflowCaller: + @workflow.run + async def run(self, input: Input) -> str: + client = workflow.create_nexus_client( + service=TestService, endpoint=make_nexus_endpoint_name(input.task_queue) + ) + return await client.execute_operation(TestService.concurrent_start, input) + + +@workflow.defn +class FailedStartRollbackWorkflowCaller: + @workflow.run + async def run(self, input: Input) -> str: + client = workflow.create_nexus_client( + service=TestService, endpoint=make_nexus_endpoint_name(input.task_queue) + ) + return await client.execute_operation( + TestService.retry_after_failed_start, + input, + ) + + +async def test_temporal_operation_double_start_raises_handler_err( + client: Client, env: WorkflowEnvironment +): + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) + async with Worker( + env.client, + task_queue=task_queue, + nexus_service_handlers=[TestServiceHandler()], + workflows=[EchoWorkflow, DoubleStartWorkflowCaller], + ): + with pytest.raises(WorkflowFailureError) as err: + await client.execute_workflow( + DoubleStartWorkflowCaller.run, + Input(value="test", task_queue=task_queue), + task_queue=task_queue, + id=f"double-start-{uuid.uuid4()}", + ) + + assert isinstance(err.value.cause, temporalio.exceptions.NexusOperationError) + assert isinstance(err.value.cause.cause, nexusrpc.HandlerError) + assert err.value.cause.cause.type == HandlerErrorType.BAD_REQUEST + assert ( + "Only one async operation can be started per operation handler invocation" + in err.value.cause.cause.message + ) + + +async def test_temporal_operation_concurrent_start_raises_handler_err( + client: Client, env: WorkflowEnvironment +): + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) + async with Worker( + env.client, + task_queue=task_queue, + nexus_service_handlers=[TestServiceHandler()], + workflows=[EchoWorkflow, ConcurrentStartWorkflowCaller], + ): + result = await client.execute_workflow( + ConcurrentStartWorkflowCaller.run, + Input(value="test", task_queue=task_queue), + task_queue=task_queue, + id=f"concurrent-start-{uuid.uuid4()}", + ) + + assert result == "test" + + +async def test_temporal_operation_failed_start_allows_retry( + client: Client, env: WorkflowEnvironment +): + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + conflict_id = f"failed-start-rollback-{uuid.uuid4()}" + await env.create_nexus_endpoint(endpoint_name, task_queue) + async with Worker( + env.client, + task_queue=task_queue, + nexus_service_handlers=[TestServiceHandler()], + workflows=[ + BlockingWorkflow, + EchoWorkflow, + FailedStartRollbackWorkflowCaller, + ], + ): + conflict_handle = await client.start_workflow( + BlockingWorkflow.run, + id=conflict_id, + task_queue=task_queue, + ) + + try: + result = await client.execute_workflow( + FailedStartRollbackWorkflowCaller.run, + Input(value=conflict_id, task_queue=task_queue), + task_queue=task_queue, + id=f"failed-start-rollback-caller-{uuid.uuid4()}", + ) + assert result == conflict_id + finally: + await conflict_handle.cancel() + + +async def test_temporal_operation_mixed_start_raises_handler_err( + client: Client, env: WorkflowEnvironment +): + if env.supports_time_skipping: + pytest.skip( + "Standalone Nexus Operation tests don't work with time-skipping server" + ) + + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) + async with Worker( + env.client, + task_queue=task_queue, + nexus_service_handlers=[TestServiceHandler()], + workflows=[EchoWorkflow], + activities=[echo_activity], + ): + nexus_client = client.create_nexus_client(TestService, endpoint_name) + + with pytest.raises(NexusOperationFailureError) as err: + await nexus_client.execute_operation( + TestService.mixed_start, + Input(value="test", task_queue=task_queue), + id=str(uuid.uuid4()), + ) + + assert isinstance(err.value.cause, nexusrpc.HandlerError) + assert err.value.cause.type == HandlerErrorType.BAD_REQUEST + assert ( + "Only one async operation can be started per operation handler invocation" + in err.value.cause.message + ) + + +@workflow.defn +class SyncResultCaller: + @workflow.run + async def run(self, input: Input) -> str: + client = workflow.create_nexus_client( + service=TestService, endpoint=make_nexus_endpoint_name(input.task_queue) + ) + return await client.execute_operation(TestService.sync_result, input) + + +async def test_temporal_operation_sync_result(client: Client, env: WorkflowEnvironment): + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) + async with Worker( + env.client, + task_queue=task_queue, + nexus_service_handlers=[TestServiceHandler()], + workflows=[SyncResultCaller], + ): + wf_handle = await client.start_workflow( + SyncResultCaller.run, + Input(value="test", task_queue=task_queue), + task_queue=task_queue, + id=str(uuid.uuid4()), + ) + result = await wf_handle.result() + assert result == "test" + + # Sync results do not produce a NEXUS_OPERATION_STARTED event, + await assert_event_subsequence( + wf_handle, + [ + EventType.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED, + EventType.EVENT_TYPE_NEXUS_OPERATION_COMPLETED, + ], + ) + + +async def test_temporal_operation_start_activity( + client: Client, env: WorkflowEnvironment +): + if env.supports_time_skipping: + pytest.skip( + "Standalone Nexus Operation tests don't work with time-skipping server" + ) + + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) + async with Worker( + env.client, + task_queue=task_queue, + nexus_service_handlers=[TestServiceHandler()], + activities=[echo_activity], + ): + nexus_client = client.create_nexus_client(TestService, endpoint_name) + + result = await nexus_client.execute_operation( + TestService.echo_activity, + Input(value="test", task_queue=task_queue), + id=str(uuid.uuid4()), + ) + assert result == "test" + + +async def test_temporal_operation_backing_activity_does_not_duplicate_links( + client: Client, env: WorkflowEnvironment +): + if env.supports_time_skipping: + pytest.skip( + "Standalone Nexus Operation tests don't work with time-skipping server" + ) + + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) + activity_id = f"link-activity-{uuid.uuid4()}" + + @service_handler + class LinkActivityHandler: + @nexus.temporal_operation + async def echo_activity( + self, + _ctx: nexus.TemporalStartOperationContext, + client: nexus.TemporalNexusClient, + input: Input, + ) -> nexus.TemporalOperationResult[str]: + return await client.start_activity( + echo_activity, + input, + id=activity_id, + start_to_close_timeout=timedelta(seconds=5), + ) + + async with Worker( + env.client, + task_queue=task_queue, + nexus_service_handlers=[LinkActivityHandler()], + activities=[echo_activity], + ): + nexus_client = client.create_nexus_client(LinkActivityHandler, endpoint_name) + operation_handle = await nexus_client.start_operation( + LinkActivityHandler.echo_activity, + Input(value="test", task_queue=task_queue), + id=str(uuid.uuid4()), + ) + + assert await operation_handle.result() == "test" + activity_description = await client.get_activity_handle(activity_id).describe() + assert isinstance(activity_description.raw_info, ActivityExecutionInfo) + assert operation_handle.run_id is not None + callback_links = [ + link + for callback in activity_description.raw_callbacks + for link in callback.info.callback.links + ] + assert_links_match( + [*activity_description.raw_info.links, *callback_links], + expected_nexus_operation_link( + namespace=client.namespace, + operation_id=operation_handle.operation_id, + run_id=operation_handle.run_id, + ), + ) + + +async def test_temporal_operation_start_activity_raises_error( + client: Client, env: WorkflowEnvironment +): + if env.supports_time_skipping: + pytest.skip( + "Standalone Nexus Operation tests don't work with time-skipping server" + ) + + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) + async with Worker( + env.client, + task_queue=task_queue, + nexus_service_handlers=[TestServiceHandler()], + activities=[raise_error_activity], + ): + nexus_client = client.create_nexus_client(TestService, endpoint_name) + + with pytest.raises(NexusOperationFailureError) as err: + await nexus_client.execute_operation( + TestService.error_activity, + Input(value="test", task_queue=task_queue), + id=str(uuid.uuid4()), + ) + + operation_err = err.value.__cause__ + assert isinstance(operation_err, temporalio.exceptions.ApplicationError) + assert operation_err.type == "OperationError" + assert "nexus operation completed unsuccessfully" in str(operation_err) + + application_err = operation_err.__cause__ + assert isinstance(application_err, temporalio.exceptions.ApplicationError) + + assert application_err.type == "test-activity-error-type" + assert "test-activity-error-message" in str(application_err) + assert application_err.__cause__ is None + + +async def test_temporal_operation_cancel_activity( + client: Client, env: WorkflowEnvironment +): + if env.supports_time_skipping: + pytest.skip( + "Standalone Nexus Operation tests don't work with time-skipping server" + ) + + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) + async with Worker( + env.client, + task_queue=task_queue, + nexus_service_handlers=[TestServiceHandler()], + activities=[wait_for_cancel_activity], + ): + nexus_client = client.create_nexus_client(TestService, endpoint_name) + + activity_id = f"blocking-activity-{uuid.uuid4()}" + op_handle = await nexus_client.start_operation( + TestService.blocking_activity, activity_id, id=str(uuid.uuid4()) + ) + + await op_handle.cancel() + + activity_handle = client.get_activity_handle(activity_id) + + async def check_cancelled(): + op_desc = await op_handle.describe() + assert op_desc.status is NexusOperationExecutionStatus.CANCELED + activity_desc = await activity_handle.describe() + assert activity_desc.status is ActivityExecutionStatus.CANCELED + + await assert_eventually(check_cancelled) + + +async def test_customized_temporal_operation_cancel_activity( + client: Client, env: WorkflowEnvironment +): + if env.supports_time_skipping: + pytest.skip( + "Standalone Nexus Operation tests don't work with time-skipping server" + ) + + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) + + service_handler = TestServiceHandler() + async with Worker( + env.client, + task_queue=task_queue, + nexus_service_handlers=[service_handler], + activities=[wait_for_cancel_activity], + ): + nexus_client = client.create_nexus_client(TestService, endpoint_name) + + activity_id = f"custom-cancel-activity-{uuid.uuid4()}" + op_handle = await nexus_client.start_operation( + TestService.custom_cancel_activity, activity_id, id=str(uuid.uuid4()) + ) + await service_handler.started_custom_cancel_activity.wait() + + await op_handle.cancel() + + activity_handle = client.get_activity_handle(activity_id) + + async def check_cancelled(): + assert service_handler.custom_cancel_activity_called.is_set() + op_desc = await op_handle.describe() + assert op_desc.status is NexusOperationExecutionStatus.CANCELED + activity_desc = await activity_handle.describe() + assert activity_desc.status is ActivityExecutionStatus.CANCELED + + await assert_eventually(check_cancelled) + + +async def test_temporal_operation_double_start_activity_raises_handler_err( + client: Client, env: WorkflowEnvironment +): + if env.supports_time_skipping: + pytest.skip( + "Standalone Nexus Operation tests don't work with time-skipping server" + ) + + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) + async with Worker( + env.client, + task_queue=task_queue, + nexus_service_handlers=[TestServiceHandler()], + activities=[wait_for_cancel_activity], + ): + nexus_client = client.create_nexus_client(TestService, endpoint_name) + + with pytest.raises(NexusOperationFailureError) as err: + await nexus_client.execute_operation( + TestService.double_start_activity, + Input(value="test", task_queue=task_queue), + id=str(uuid.uuid4()), + ) + + assert isinstance(err.value.cause, nexusrpc.HandlerError) + assert err.value.cause.type == HandlerErrorType.BAD_REQUEST + assert ( + "Only one async operation can be started per operation handler invocation" + in err.value.cause.message + ) + + +@dataclass +class TemporalOperationOverloadTestValue: + value: int + + +@workflow.defn +class TemporalOperationOverloadTestWorkflow: + @workflow.run + async def run( + self, input: TemporalOperationOverloadTestValue + ) -> TemporalOperationOverloadTestValue: + return TemporalOperationOverloadTestValue(value=input.value * 2) + + +@workflow.defn +class TemporalOperationOverloadTestWorkflowNoParam: + @workflow.run + async def run(self) -> TemporalOperationOverloadTestValue: + return TemporalOperationOverloadTestValue(value=0) + + +@service_handler +class TemporalOperationOverloadTestServiceHandler: + @nexus.temporal_operation + async def no_param( + self, + _ctx: nexus.TemporalStartOperationContext, + client: nexus.TemporalNexusClient, + _input: TemporalOperationOverloadTestValue, + ) -> nexus.TemporalOperationResult[TemporalOperationOverloadTestValue]: + return await client.start_workflow( + TemporalOperationOverloadTestWorkflowNoParam.run, + id=str(uuid.uuid4()), + ) + + @nexus.temporal_operation + async def single_param( + self, + _ctx: nexus.TemporalStartOperationContext, + client: nexus.TemporalNexusClient, + input: TemporalOperationOverloadTestValue, + ) -> nexus.TemporalOperationResult[TemporalOperationOverloadTestValue]: + return await client.start_workflow( + TemporalOperationOverloadTestWorkflow.run, + input, + id=str(uuid.uuid4()), + ) + + @nexus.temporal_operation + async def multi_param( + self, + _ctx: nexus.TemporalStartOperationContext, + client: nexus.TemporalNexusClient, + input: TemporalOperationOverloadTestValue, + ) -> nexus.TemporalOperationResult[TemporalOperationOverloadTestValue]: + return await client.start_workflow( + TemporalOperationOverloadTestWorkflow.run, + args=[input], + id=str(uuid.uuid4()), + ) + + @nexus.temporal_operation + async def by_name( + self, + _ctx: nexus.TemporalStartOperationContext, + client: nexus.TemporalNexusClient, + input: TemporalOperationOverloadTestValue, + ) -> nexus.TemporalOperationResult[TemporalOperationOverloadTestValue]: + return await client.start_workflow( + "TemporalOperationOverloadTestWorkflow", + input, + id=str(uuid.uuid4()), + result_type=TemporalOperationOverloadTestValue, + ) + + @nexus.temporal_operation + async def by_name_multi_param( + self, + _ctx: nexus.TemporalStartOperationContext, + client: nexus.TemporalNexusClient, + input: TemporalOperationOverloadTestValue, + ) -> nexus.TemporalOperationResult[TemporalOperationOverloadTestValue]: + return await client.start_workflow( + "TemporalOperationOverloadTestWorkflow", + args=[input], + id=str(uuid.uuid4()), + result_type=TemporalOperationOverloadTestValue, + ) + + +@workflow.defn +class TemporalOperationOverloadTestCallerWorkflow: + @workflow.run + async def run( + self, op: str, input: TemporalOperationOverloadTestValue + ) -> TemporalOperationOverloadTestValue: + client = workflow.create_nexus_client( + service=TemporalOperationOverloadTestServiceHandler, + endpoint=make_nexus_endpoint_name(workflow.info().task_queue), + ) + + if op == "no_param": + return await client.execute_operation( + TemporalOperationOverloadTestServiceHandler.no_param, input + ) + elif op == "single_param": + return await client.execute_operation( + TemporalOperationOverloadTestServiceHandler.single_param, input + ) + elif op == "multi_param": + return await client.execute_operation( + TemporalOperationOverloadTestServiceHandler.multi_param, input + ) + elif op == "by_name": + return await client.execute_operation( + TemporalOperationOverloadTestServiceHandler.by_name, input + ) + elif op == "by_name_multi_param": + return await client.execute_operation( + TemporalOperationOverloadTestServiceHandler.by_name_multi_param, input + ) + else: + raise ValueError(f"Unknown op: {op}") + + +@pytest.mark.parametrize( + "op", + [ + "no_param", + "single_param", + "multi_param", + "by_name", + "by_name_multi_param", + ], +) +async def test_temporal_operation_overloads( + client: Client, env: WorkflowEnvironment, op: str +): + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) + async with Worker( + client, + task_queue=task_queue, + workflows=[ + TemporalOperationOverloadTestCallerWorkflow, + TemporalOperationOverloadTestWorkflow, + TemporalOperationOverloadTestWorkflowNoParam, + ], + nexus_service_handlers=[TemporalOperationOverloadTestServiceHandler()], + ): + result = await client.execute_workflow( + TemporalOperationOverloadTestCallerWorkflow.run, + args=[op, TemporalOperationOverloadTestValue(value=2)], + id=str(uuid.uuid4()), + task_queue=task_queue, + ) + assert result == ( + TemporalOperationOverloadTestValue(value=0) + if op == "no_param" + else TemporalOperationOverloadTestValue(value=4) + ) + + +async def test_temporal_operation_includes_token_in_callback( + client: Client, env: WorkflowEnvironment +): + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) + async with Worker( + env.client, + task_queue=task_queue, + nexus_service_handlers=[TestServiceHandler()], + workflows=[EchoWorkflow, EchoWorkflowCaller], + ): + input_value = f"test-{uuid.uuid4()}" + wf_handle = await client.start_workflow( + EchoWorkflowCaller.run, + Input(value=input_value, task_queue=task_queue), + task_queue=task_queue, + id=str(uuid.uuid4()), + ) + result = await wf_handle.result() + assert result == input_value + + target_handle = client.get_workflow_handle(f"echo-{input_value}") + + desc = await target_handle.describe() + token = desc.raw_description.callbacks[0].callback.nexus.header[ + "nexus-operation-token" + ] + + expected_token = OperationToken( + type=OperationTokenType.WORKFLOW, + namespace=client.namespace, + workflow_id=target_handle.id, + ).encode() + + assert token == expected_token + + +@workflow.defn +class UpdateWorkflowCaller: + """Simple caller workflow that triggers a workflow update via nexus op""" + + @workflow.run + async def run(self, input: Input) -> str: + client = workflow.create_nexus_client( + service=TestService, + endpoint=make_nexus_endpoint_name(input.task_queue), + ) + op_handle = await client.start_operation(TestService.update_op, input) + if input.expect_sync_response: + if op_handle.operation_token: + raise RuntimeError("unexpected operation token on a sync operation") + else: + if not op_handle.operation_token: + raise RuntimeError( + "unexpected empty operation token on an async operation" + ) + return await op_handle + + +@workflow.defn +class UpdatableWorkflow: + """Workflow that accepts updates and exits when it receives a specific status""" + + def __init__(self) -> None: + self.order_status = "Pending" + + @workflow.run + async def run(self) -> None: + await workflow.wait_condition(lambda: self.order_status == "Completed") + # some more order processing etc + + @workflow.update + async def do_update(self, value: str) -> str: + status = self.order_status + await workflow.sleep( + timedelta(seconds=1) + ) # small sleep to ensure updates are async and backlinks are to STARTED events + self.order_status = value + update_result = f"Updated workflow status from {status} to {value}" + return update_result + + +async def test_temporal_operation_includes_activity_token_in_callback( + client: Client, env: WorkflowEnvironment +): + if env.supports_time_skipping: + pytest.skip( + "Standalone Nexus Operation tests don't work with time-skipping server" + ) + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) + + @service_handler + class ActivityTokenHandler: + @nexus.temporal_operation + async def echo_activity( + self, + _ctx: nexus.TemporalStartOperationContext, + client: nexus.TemporalNexusClient, + input: Input, + ) -> nexus.TemporalOperationResult[str]: + return await client.start_activity( + echo_activity, + input, + id=input.value, + start_to_close_timeout=timedelta(seconds=10), + start_delay=timedelta(milliseconds=100), + ) + + async with Worker( + client, + task_queue=task_queue, + nexus_service_handlers=[ActivityTokenHandler()], + activities=[echo_activity], + ): + input_value = f"test-{uuid.uuid4()}" + + nexus_client = client.create_nexus_client(ActivityTokenHandler, endpoint_name) + + result = await nexus_client.execute_operation( + ActivityTokenHandler.echo_activity, + Input(value=input_value, task_queue=task_queue), + id=str(uuid.uuid4()), + ) + assert result == input_value + + activity_handle = client.get_activity_handle(input_value) + + desc = await activity_handle.describe() + token = desc.raw_callbacks[0].info.callback.nexus.header[ + "nexus-operation-token" + ] + + expected_token = OperationToken( + type=OperationTokenType.ACTIVITY, + namespace=client.namespace, + activity_id=input_value, + ).encode() + + assert token == expected_token diff --git a/tests/nexus/test_temporal_system_nexus.py b/tests/nexus/test_temporal_system_nexus.py new file mode 100644 index 000000000..ff6b36e41 --- /dev/null +++ b/tests/nexus/test_temporal_system_nexus.py @@ -0,0 +1,430 @@ +from __future__ import annotations + +import dataclasses +import uuid +from collections.abc import Sequence +from datetime import timedelta +from typing import Any, cast + +import pytest +from google.protobuf.descriptor import FieldDescriptor +from google.protobuf.message import Message + +import temporalio.api.common.v1 +import temporalio.api.workflowservice.v1.request_response_pb2 as workflowservice_pb2 +import temporalio.converter +import temporalio.nexus.system as nexus_system +from temporalio import workflow +from temporalio.bridge._visitor import PayloadVisitor +from temporalio.bridge._visitor_functions import VisitorFunctions +from temporalio.bridge.proto.workflow_completion.workflow_completion_pb2 import ( + WorkflowActivationCompletion, +) +from temporalio.client import Client +from temporalio.converter import ExternalStorage, PayloadCodec +from temporalio.testing import WorkflowEnvironment +from temporalio.worker import ( + Interceptor, + StartNexusOperationInput, + Worker, + WorkflowInboundInterceptor, + WorkflowInterceptorClassInput, + WorkflowOutboundInterceptor, +) +from temporalio.worker._workflow_instance import UnsandboxedWorkflowRunner +from tests.test_extstore import InMemoryTestDriver + +interceptor_traces: list[tuple[str, object]] = [] +SYSTEM_NEXUS_PAYLOAD_METADATA_KEY = "__temporal_system_payload" + + +@workflow.defn +class ExternalHandleSignalWithStartWorkflowCaller: + @workflow.run + async def run(self, task_queue: str) -> str: + started_handle = await workflow.signal_with_start_workflow( + "test-workflow", + "workflow-input", + id="system-nexus-workflow-id", + task_queue=task_queue, + signal="test-signal", + signal_args=["signal-input"], + memo={"memo-key": "memo-value"}, + static_summary="summary-value", + static_details="details-value", + ) + return started_handle.id + + +class RejectOuterSystemNexusCodec(PayloadCodec): + def __init__(self) -> None: + self.encode_count = 0 + + async def encode( + self, payloads: Sequence[temporalio.api.common.v1.Payload] + ) -> list[temporalio.api.common.v1.Payload]: + encoded: list[temporalio.api.common.v1.Payload] = [] + for payload in payloads: + if ( + payload.metadata.get("encoding") == b"binary/protobuf" + and payload.metadata.get("messageType") + == b"temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest" + ): + raise RuntimeError( + "outer system nexus envelope should not be codec encoded" + ) + self.encode_count += 1 + encoded.append( + temporalio.api.common.v1.Payload( + metadata={**payload.metadata, "test-codec": b"true"}, + data=payload.data, + ) + ) + return encoded + + async def decode( + self, payloads: Sequence[temporalio.api.common.v1.Payload] + ) -> list[temporalio.api.common.v1.Payload]: + decoded: list[temporalio.api.common.v1.Payload] = [] + for payload in payloads: + if ( + payload.metadata.get("encoding") == b"binary/protobuf" + and payload.metadata.get("messageType") + == b"temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest" + ): + raise RuntimeError( + "outer system nexus envelope should not be codec decoded" + ) + decoded.append(payload) + return decoded + + +class TracingWorkflowInterceptor(Interceptor): + def workflow_interceptor_class( + self, input: WorkflowInterceptorClassInput + ) -> type[WorkflowInboundInterceptor] | None: + return _TracingWorkflowInboundInterceptor + + +class _TracingWorkflowInboundInterceptor(WorkflowInboundInterceptor): + def init(self, outbound: WorkflowOutboundInterceptor) -> None: + super().init(_TracingWorkflowOutboundInterceptor(outbound)) + + +class _TracingWorkflowOutboundInterceptor(WorkflowOutboundInterceptor): + async def start_nexus_operation( + self, input: StartNexusOperationInput[Any, Any] + ) -> workflow.NexusOperationHandle[Any]: + interceptor_traces.append(("workflow.start_nexus_operation", input)) + return await super().start_nexus_operation(input) + + +def _assert_stored_payloads_include( + driver: InMemoryTestDriver, expected_payload_data: set[bytes] +) -> None: + stored_payload_data: set[bytes] = set() + for stored_payload_bytes in driver._storage.values(): + stored_payload = temporalio.api.common.v1.Payload() + stored_payload.ParseFromString(stored_payload_bytes) + assert stored_payload.metadata["test-codec"] == b"true" + stored_payload_data.add(stored_payload.data) + assert expected_payload_data.issubset(stored_payload_data) + + +def _assert_start_nexus_operation_interceptor_trace() -> None: + assert len(interceptor_traces) == 1 + trace_name, trace_value = interceptor_traces.pop() + assert trace_name == "workflow.start_nexus_operation" + trace_input = cast(StartNexusOperationInput[Any, Any], trace_value) + request = cast( + workflowservice_pb2.SignalWithStartWorkflowExecutionRequest, + trace_input.input, + ) + assert request.workflow_id == "system-nexus-workflow-id" + assert request.signal_name == "test-signal" + assert request.workflow_type.name == "test-workflow" + + +class _MarkingPayloadVisitor(VisitorFunctions): + def __init__(self) -> None: + self.visited_payload_count = 0 + self.system_envelope_count = 0 + + async def visit_payload(self, payload: temporalio.api.common.v1.Payload) -> None: + self.visited_payload_count += 1 + payload.metadata["visited"] = b"true" + + async def visit_payloads( + self, payloads: Sequence[temporalio.api.common.v1.Payload] + ) -> None: + for payload in payloads: + await self.visit_payload(payload) + + async def visit_system_nexus_envelope( + self, payload: temporalio.api.common.v1.Payload + ) -> None: + _ = payload + self.system_envelope_count += 1 + + +def _new_schedule_nexus_completion( + endpoint: str, payload: temporalio.api.common.v1.Payload +) -> WorkflowActivationCompletion: + completion = WorkflowActivationCompletion() + command = completion.successful.commands.add() + schedule = command.schedule_nexus_operation + schedule.seq = 1 + schedule.endpoint = endpoint + schedule.service = "not-a-registered-system-service" + schedule.operation = "NotARegisteredSystemOperation" + schedule.input.CopyFrom(payload) + return completion + + +def _new_system_nexus_request_payload() -> temporalio.api.common.v1.Payload: + nested_payload = temporalio.converter.PayloadConverter.default.to_payload( + "workflow-input" + ) + assert nested_payload is not None + request = workflowservice_pb2.SignalWithStartWorkflowExecutionRequest() + request.input.payloads.add().CopyFrom(nested_payload) + payload = nexus_system._get_payload_converter( + temporalio.converter.PayloadConverter.default + ).to_payload(request) + assert payload is not None + return payload + + +def _new_unmarked_system_nexus_request_payload() -> temporalio.api.common.v1.Payload: + payload = _new_system_nexus_request_payload() + del payload.metadata[SYSTEM_NEXUS_PAYLOAD_METADATA_KEY] + return payload + + +async def test_schedule_marked_system_nexus_payload_ignores_endpoint() -> None: + completion = _new_schedule_nexus_completion( + "not-the-system-endpoint", + _new_system_nexus_request_payload(), + ) + visitor = _MarkingPayloadVisitor() + + await PayloadVisitor().visit(visitor, completion) + + schedule = completion.successful.commands[0].schedule_nexus_operation + decoded = nexus_system._get_payload_converter( + temporalio.converter.PayloadConverter.default + ).from_payload(schedule.input) + assert isinstance( + decoded, workflowservice_pb2.SignalWithStartWorkflowExecutionRequest + ) + assert decoded.input.payloads[0].metadata["visited"] == b"true" + assert "visited" not in schedule.input.metadata + assert visitor.visited_payload_count == 1 + assert visitor.system_envelope_count == 1 + + +async def test_schedule_unmarked_system_nexus_payload_visits_input_as_regular_payload() -> ( + None +): + completion = _new_schedule_nexus_completion( + nexus_system.TEMPORAL_SYSTEM_ENDPOINT, + _new_unmarked_system_nexus_request_payload(), + ) + visitor = _MarkingPayloadVisitor() + + await PayloadVisitor().visit(visitor, completion) + + schedule = completion.successful.commands[0].schedule_nexus_operation + assert schedule.input.metadata["visited"] == b"true" + decoded = nexus_system._get_payload_converter( + temporalio.converter.default().payload_converter + ).from_payload(schedule.input) + assert isinstance( + decoded, workflowservice_pb2.SignalWithStartWorkflowExecutionRequest + ) + assert "visited" not in decoded.input.payloads[0].metadata + assert visitor.visited_payload_count == 1 + assert visitor.system_envelope_count == 0 + + +def _build_proto_sample(message_type: type[Message]) -> Message: + message = message_type() + _populate_proto_sample(message) + return message + + +def _populate_proto_sample(message: Message, *, path: str = "value") -> None: + seen_oneofs: set[str] = set() + for raw_field in message.DESCRIPTOR.fields: + field = cast(FieldDescriptor, raw_field) + if field.containing_oneof is not None: + if field.containing_oneof.name in seen_oneofs: + continue + seen_oneofs.add(field.containing_oneof.name) + if _field_is_repeated(field): + if ( + field.message_type is not None + and field.message_type.GetOptions().map_entry + ): + _populate_proto_map_entry(message, field, path=path) + elif field.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE: + _populate_proto_sample( + getattr(message, field.name).add(), + path=f"{path}.{field.name}[0]", + ) + else: + getattr(message, field.name).append( + _proto_scalar_sample(field, path=f"{path}.{field.name}[0]") + ) + elif field.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE: + _populate_proto_sample( + getattr(message, field.name), + path=f"{path}.{field.name}", + ) + else: + setattr( + message, + field.name, + _proto_scalar_sample(field, path=f"{path}.{field.name}"), + ) + + +def _populate_proto_map_entry( + message: Message, + field: FieldDescriptor, + *, + path: str, +) -> None: + message_type = field.message_type + assert message_type is not None + key_field = message_type.fields_by_name["key"] + value_field = message_type.fields_by_name["value"] + key = _proto_scalar_sample(key_field, path=f"{path}.{field.name}.key") + container = getattr(message, field.name) + if value_field.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE: + _populate_proto_sample( + container[key], + path=f"{path}.{field.name}[{key!r}]", + ) + else: + container[key] = _proto_scalar_sample( + value_field, + path=f"{path}.{field.name}[{key!r}]", + ) + + +def _proto_scalar_sample(field: FieldDescriptor, *, path: str) -> Any: + if field.type == FieldDescriptor.TYPE_BYTES: + return b"test" + if field.cpp_type == FieldDescriptor.CPPTYPE_STRING: + return f"{path}-value" + if field.cpp_type == FieldDescriptor.CPPTYPE_BOOL: + return True + if field.cpp_type in ( + FieldDescriptor.CPPTYPE_INT32, + FieldDescriptor.CPPTYPE_INT64, + FieldDescriptor.CPPTYPE_UINT32, + FieldDescriptor.CPPTYPE_UINT64, + ): + return 1 + if field.cpp_type in ( + FieldDescriptor.CPPTYPE_FLOAT, + FieldDescriptor.CPPTYPE_DOUBLE, + ): + return 1.5 + if field.cpp_type == FieldDescriptor.CPPTYPE_ENUM: + enum_type = field.enum_type + assert enum_type is not None + for enum_value in enum_type.values: + if enum_value.number != 0: + return enum_value.number + return enum_type.values[0].number + raise TypeError(f"Unhandled proto scalar sample at {path}: {field!r}") + + +def _field_is_repeated(field: FieldDescriptor) -> bool: + return bool( + getattr( + field, + "is_repeated", + getattr(field, "label") == FieldDescriptor.LABEL_REPEATED, + ) + ) + + +@pytest.mark.parametrize( + "message_type", + [ + workflowservice_pb2.SignalWithStartWorkflowExecutionRequest, + workflowservice_pb2.SignalWithStartWorkflowExecutionResponse, + ], +) +def test_system_nexus_proto_roundtrip(message_type: type[Message]) -> None: + payload_converter = nexus_system._get_payload_converter( + temporalio.converter.PayloadConverter.default + ) + proto_value = _build_proto_sample(message_type) + payload = payload_converter.to_payload(proto_value) + assert payload is not None + assert payload.metadata["encoding"] == b"binary/protobuf" + assert payload.metadata["messageType"] == message_type.DESCRIPTOR.full_name.encode() + assert payload.metadata[SYSTEM_NEXUS_PAYLOAD_METADATA_KEY] == b"true" + roundtripped = payload_converter.from_payload(payload, message_type) + assert isinstance(roundtripped, message_type) + assert roundtripped == proto_value + + +# Cloud namespaces created by CI do not have the System Nexus dynamic config. +@pytest.mark.requires_local_server +async def test_external_workflow_handle_signal_with_start_workflow_uses_system_nexus( + env: WorkflowEnvironment, +): + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work with the Java test server") + + codec = RejectOuterSystemNexusCodec() + interceptor_traces.clear() + driver = InMemoryTestDriver() + caller_config = env.client.config() + caller_config["data_converter"] = dataclasses.replace( + temporalio.converter.default(), + payload_codec=codec, + external_storage=ExternalStorage( + drivers=[driver], + payload_size_threshold=1, + ), + ) + caller_client = Client(**caller_config) + caller_task_queue = str(uuid.uuid4()) + handler_task_queue = str(uuid.uuid4()) + + caller_worker = Worker( + caller_client, + task_queue=caller_task_queue, + workflows=[ExternalHandleSignalWithStartWorkflowCaller], + workflow_runner=UnsandboxedWorkflowRunner(), + interceptors=[TracingWorkflowInterceptor()], + ) + + async with caller_worker: + result = await caller_client.execute_workflow( + ExternalHandleSignalWithStartWorkflowCaller.run, + args=[handler_task_queue], + id=str(uuid.uuid4()), + task_queue=caller_task_queue, + execution_timeout=timedelta(seconds=5), + ) + + assert result == "system-nexus-workflow-id" + assert codec.encode_count >= 5 + _assert_stored_payloads_include( + driver, + { + b'"workflow-input"', + b'"signal-input"', + b'"memo-value"', + b'"summary-value"', + b'"details-value"', + }, + ) + _assert_start_nexus_operation_interceptor_trace() diff --git a/tests/nexus/test_type_errors.py b/tests/nexus/test_type_errors.py deleted file mode 100644 index 1f5d3e2a7..000000000 --- a/tests/nexus/test_type_errors.py +++ /dev/null @@ -1,207 +0,0 @@ -""" -This file exists to test for type-checker false positives and false negatives. -It doesn't contain any test functions. -""" - -from dataclasses import dataclass - -import nexusrpc - -import temporalio.nexus -from temporalio import workflow - - -@dataclass -class MyInput: - pass - - -@dataclass -class MyOutput: - pass - - -@nexusrpc.service -class MyService: - my_sync_operation: nexusrpc.Operation[MyInput, MyOutput] - my_workflow_run_operation: nexusrpc.Operation[MyInput, MyOutput] - - -@nexusrpc.handler.service_handler(service=MyService) -class MyServiceHandler: - @nexusrpc.handler.sync_operation - async def my_sync_operation( - self, _ctx: nexusrpc.handler.StartOperationContext, _input: MyInput - ) -> MyOutput: - raise NotImplementedError - - @temporalio.nexus.workflow_run_operation - async def my_workflow_run_operation( - self, _ctx: temporalio.nexus.WorkflowRunOperationContext, _input: MyInput - ) -> temporalio.nexus.WorkflowHandle[MyOutput]: - raise NotImplementedError - - -@nexusrpc.handler.service_handler(service=MyService) -class MyServiceHandler2: - @nexusrpc.handler.sync_operation - async def my_sync_operation( - self, _ctx: nexusrpc.handler.StartOperationContext, _input: MyInput - ) -> MyOutput: - raise NotImplementedError - - @temporalio.nexus.workflow_run_operation - async def my_workflow_run_operation( - self, _ctx: temporalio.nexus.WorkflowRunOperationContext, _input: MyInput - ) -> temporalio.nexus.WorkflowHandle[MyOutput]: - raise NotImplementedError - - -@nexusrpc.handler.service_handler -class MyServiceHandlerWithoutServiceDefinition: - @nexusrpc.handler.sync_operation - async def my_sync_operation( - self, _ctx: nexusrpc.handler.StartOperationContext, _input: MyInput - ) -> MyOutput: - raise NotImplementedError - - @temporalio.nexus.workflow_run_operation - async def my_workflow_run_operation( - self, _ctx: temporalio.nexus.WorkflowRunOperationContext, _input: MyInput - ) -> temporalio.nexus.WorkflowHandle[MyOutput]: - raise NotImplementedError - - -@workflow.defn -class MyWorkflow1: - @workflow.run - async def test_invoke_by_operation_definition_happy_path(self) -> None: - """ - When a nexus client calls an operation by referencing an operation definition on - a service definition, the output type is inferred correctly. - """ - nexus_client = workflow.create_nexus_client( - service=MyService, - endpoint="fake-endpoint", - ) - input = MyInput() - - # sync operation - _output_1: MyOutput = await nexus_client.execute_operation( - MyService.my_sync_operation, input - ) - _handle_1: workflow.NexusOperationHandle[ - MyOutput - ] = await nexus_client.start_operation(MyService.my_sync_operation, input) - _output_1_1: MyOutput = await _handle_1 - - # workflow run operation - _output_2: MyOutput = await nexus_client.execute_operation( - MyService.my_workflow_run_operation, input - ) - _handle_2: workflow.NexusOperationHandle[ - MyOutput - ] = await nexus_client.start_operation( - MyService.my_workflow_run_operation, input - ) - _output_2_1: MyOutput = await _handle_2 - - -@workflow.defn -class MyWorkflow2: - @workflow.run - async def test_invoke_by_operation_handler_happy_path(self) -> None: - """ - When a nexus client calls an operation by referencing an operation handler on a - service handler, the output type is inferred correctly. - """ - nexus_client = workflow.create_nexus_client( - service=MyServiceHandler, # MyService would also work - endpoint="fake-endpoint", - ) - input = MyInput() - - # sync operation - _output_1: MyOutput = await nexus_client.execute_operation( - MyServiceHandler.my_sync_operation, input - ) - _handle_1: workflow.NexusOperationHandle[ - MyOutput - ] = await nexus_client.start_operation( - MyServiceHandler.my_sync_operation, input - ) - _output_1_1: MyOutput = await _handle_1 - - # workflow run operation - _output_2: MyOutput = await nexus_client.execute_operation( - MyServiceHandler.my_workflow_run_operation, input - ) - _handle_2: workflow.NexusOperationHandle[ - MyOutput - ] = await nexus_client.start_operation( - MyServiceHandler.my_workflow_run_operation, input - ) - _output_2_1: MyOutput = await _handle_2 - - -@workflow.defn -class MyWorkflow3: - @workflow.run - async def test_invoke_by_operation_definition_wrong_input_type(self) -> None: - """ - When a nexus client calls an operation by referencing an operation definition on - a service definition, there is a type error if the input type is wrong. - """ - nexus_client = workflow.create_nexus_client( - service=MyService, - endpoint="fake-endpoint", - ) - # assert-type-error-pyright: 'No overloads for "execute_operation" match' - await nexus_client.execute_operation( # type: ignore - MyService.my_sync_operation, - # assert-type-error-pyright: 'Argument of type .+ cannot be assigned to parameter "input"' - "wrong-input-type", # type: ignore - ) - - -@workflow.defn -class MyWorkflow4: - @workflow.run - async def test_invoke_by_operation_handler_wrong_input_type(self) -> None: - """ - When a nexus client calls an operation by referencing an operation handler on a - service handler, there is a type error if the input type is wrong. - """ - nexus_client = workflow.create_nexus_client( - service=MyServiceHandler, - endpoint="fake-endpoint", - ) - # assert-type-error-pyright: 'No overloads for "execute_operation" match' - await nexus_client.execute_operation( # type: ignore - MyServiceHandler.my_sync_operation, # type: ignore[arg-type] - # assert-type-error-pyright: 'Argument of type .+ cannot be assigned to parameter "input"' - "wrong-input-type", # type: ignore - ) - - -@workflow.defn -class MyWorkflow5: - @workflow.run - async def test_invoke_by_operation_handler_method_on_wrong_service(self) -> None: - """ - When a nexus client calls an operation by referencing an operation handler method - on a service handler, there is a type error if the method does not belong to the - service for which the client was created. - - (This form of type safety is not available when referencing an operation definition) - """ - nexus_client = workflow.create_nexus_client( - service=MyServiceHandler, - endpoint="fake-endpoint", - ) - # assert-type-error-pyright: 'No overloads for "execute_operation" match' - await nexus_client.execute_operation( # type: ignore - # assert-type-error-pyright: 'Argument of type .+ cannot be assigned to parameter "operation"' - MyServiceHandler2.my_sync_operation, # type: ignore - MyInput(), - ) diff --git a/tests/nexus/test_use_existing_conflict_policy.py b/tests/nexus/test_use_existing_conflict_policy.py index 03d2894e9..8ffa9f8f8 100644 --- a/tests/nexus/test_use_existing_conflict_policy.py +++ b/tests/nexus/test_use_existing_conflict_policy.py @@ -3,7 +3,6 @@ import asyncio import uuid from dataclasses import dataclass -from typing import Optional import pytest from nexusrpc.handler import service_handler @@ -13,7 +12,11 @@ from temporalio.common import WorkflowIDConflictPolicy from temporalio.testing import WorkflowEnvironment from temporalio.worker import Worker -from tests.helpers.nexus import create_nexus_endpoint, make_nexus_endpoint_name +from tests.helpers.nexus import make_nexus_endpoint_name + +# Cloud CI's namespace credentials cannot manage Nexus endpoints. +# See https://github.com/temporalio/sdk-python/issues/1704. +pytestmark = pytest.mark.requires_local_server @dataclass @@ -25,7 +28,7 @@ class OpInput: @workflow.defn class HandlerWorkflow: def __init__(self) -> None: - self.result: Optional[str] = None + self.result: str | None = None @workflow.run async def run(self) -> str: @@ -92,9 +95,6 @@ async def nexus_operations_have_started(self) -> None: async def test_multiple_operation_invocations_can_connect_to_same_handler_workflow( client: Client, env: WorkflowEnvironment ): - if env.supports_time_skipping: - pytest.skip("Nexus tests don't work with time-skipping server") - task_queue = str(uuid.uuid4()) workflow_id = str(uuid.uuid4()) @@ -104,7 +104,9 @@ async def test_multiple_operation_invocations_can_connect_to_same_handler_workfl workflows=[CallerWorkflow, HandlerWorkflow], task_queue=task_queue, ): - await create_nexus_endpoint(task_queue, client) + await env.create_nexus_endpoint( + make_nexus_endpoint_name(task_queue), task_queue + ) caller_handle = await client.start_workflow( CallerWorkflow.run, args=[ diff --git a/tests/nexus/test_workflow_caller.py b/tests/nexus/test_workflow_caller.py index 743f2b3e0..0c47f17c1 100644 --- a/tests/nexus/test_workflow_caller.py +++ b/tests/nexus/test_workflow_caller.py @@ -1,18 +1,22 @@ from __future__ import annotations import asyncio +import concurrent.futures +import dataclasses +import threading import uuid +from collections.abc import Awaitable, Callable from dataclasses import dataclass +from datetime import datetime, timezone from enum import IntEnum -from typing import Any, Awaitable, Callable, Union +from typing import Any +from urllib.request import urlopen import nexusrpc import nexusrpc.handler import pytest from nexusrpc.handler import ( CancelOperationContext, - FetchOperationInfoContext, - FetchOperationResultContext, OperationHandler, StartOperationContext, StartOperationResultAsync, @@ -23,9 +27,7 @@ from nexusrpc.handler._decorators import operation_handler import temporalio.api -import temporalio.api.common.v1 import temporalio.api.enums.v1 -import temporalio.api.history.v1 import temporalio.nexus._operation_handlers from temporalio import nexus, workflow from temporalio.client import ( @@ -36,17 +38,40 @@ WorkflowHandle, ) from temporalio.common import WorkflowIDConflictPolicy +from temporalio.converter import PayloadConverter from temporalio.exceptions import ( + ApplicationError, CancelledError, NexusOperationError, ) from temporalio.nexus import WorkflowRunOperationContext, workflow_run_operation +from temporalio.runtime import ( + BUFFERED_METRIC_KIND_COUNTER, + MetricBuffer, + PrometheusConfig, + Runtime, + TelemetryConfig, +) from temporalio.service import RPCError, RPCStatusCode from temporalio.testing import WorkflowEnvironment -from temporalio.worker import Worker -from tests.helpers.nexus import create_nexus_endpoint, make_nexus_endpoint_name +from temporalio.worker import ( + ExecuteNexusOperationCancelInput, + ExecuteNexusOperationStartInput, + Interceptor, + NexusOperationInboundInterceptor, + StartNexusOperationInput, + Worker, + WorkflowInboundInterceptor, + WorkflowInterceptorClassInput, + WorkflowOutboundInterceptor, +) +from tests.helpers import find_free_port, new_worker +from tests.helpers.metrics import PromMetricMatcher +from tests.helpers.nexus import ( + links_from_workflow_execution_started_event, + make_nexus_endpoint_name, +) -# TODO(nexus-prerelease): test availability of Temporal client etc in async context set by worker # TODO(nexus-preview): test worker shutdown, wait_all_completed, drain etc # ----------------------------------------------------------------------------- @@ -65,6 +90,11 @@ class OpDefinitionType(IntEnum): LONGHAND = 1 +# Cloud CI's namespace credentials cannot manage Nexus endpoints. +# See https://github.com/temporalio/sdk-python/issues/1704. +pytestmark = pytest.mark.requires_local_server + + @dataclass class SyncResponse: op_definition_type: OpDefinitionType @@ -83,7 +113,7 @@ class AsyncResponse: # The order of the two types in this union is critical since the data converter matches # eagerly, ignoring unknown fields, and so would identify an AsyncResponse as a # SyncResponse if SyncResponse came first. -ResponseType = Union[AsyncResponse, SyncResponse] +ResponseType = AsyncResponse | SyncResponse # ----------------------------------------------------------------------------- # Service interface @@ -102,6 +132,30 @@ class OpOutput: value: str +@dataclass +class HeaderTestOutput: + received_headers: dict[str, str] + + +@dataclass +class HeaderTestCallerWfInput: + headers: dict[str, str] + task_queue: str + + +@dataclass +class CancelHeaderTestCallerWfInput: + workflow_id: str + headers: dict[str, str] + task_queue: str + + +@dataclass +class WorkflowRunHeaderTestCallerWfInput: + headers: dict[str, str] + task_queue: str + + @dataclass class HandlerWfInput: op_input: OpInput @@ -119,6 +173,18 @@ class ServiceInterface: async_operation: nexusrpc.Operation[OpInput, HandlerWfOutput] +@nexusrpc.service +class HeaderTestService: + header_echo_operation: nexusrpc.Operation[None, HeaderTestOutput] + workflow_run_header_operation: nexusrpc.Operation[None, HeaderTestOutput] + cancellable_operation: nexusrpc.Operation[None, str] + + +@nexusrpc.service +class RequestDeadlineService: + cancellable_op: nexusrpc.Operation[None, str] + + # ----------------------------------------------------------------------------- # Service implementation # @@ -145,10 +211,7 @@ async def run( class SyncOrAsyncOperation(OperationHandler[OpInput, OpOutput]): async def start( # type: ignore[override] self, ctx: StartOperationContext, input: OpInput - ) -> Union[ - StartOperationResultSync[OpOutput], - StartOperationResultAsync, - ]: + ) -> StartOperationResultSync[OpOutput] | StartOperationResultAsync: if input.response_type.exception_in_operation_start: raise RPCError( "RPCError INVALID_ARGUMENT in Nexus operation", @@ -174,16 +237,6 @@ async def start( # type: ignore[override] async def cancel(self, ctx: CancelOperationContext, token: str) -> None: return await temporalio.nexus._operation_handlers._cancel_workflow(token) - async def fetch_info( - self, ctx: FetchOperationInfoContext, token: str - ) -> nexusrpc.OperationInfo: - raise NotImplementedError - - async def fetch_result( - self, ctx: FetchOperationResultContext, token: str - ) -> OpOutput: - raise NotImplementedError - @service_handler(service=ServiceInterface) class ServiceImpl: @@ -195,7 +248,7 @@ def sync_or_async_operation( @sync_operation async def sync_operation( - self, ctx: StartOperationContext, input: OpInput + self, _ctx: StartOperationContext, input: OpInput ) -> OpOutput: assert isinstance(input.response_type, SyncResponse) if input.response_type.exception_in_operation_start: @@ -224,6 +277,116 @@ async def async_operation( ) +@workflow.defn +class HeaderEchoWorkflow: + """A workflow that returns the headers it receives as input.""" + + @workflow.run + async def run(self, headers: dict[str, str]) -> HeaderTestOutput: + return HeaderTestOutput(received_headers=headers) + + +class CancellableOperationHandler(OperationHandler[None, str]): + """Operation handler that captures cancel headers.""" + + def __init__(self, cancel_headers_received: list[dict[str, str]]) -> None: + self._cancel_headers_received = cancel_headers_received + + async def start( + self, ctx: StartOperationContext, input: None + ) -> StartOperationResultAsync: + return StartOperationResultAsync("test-token") + + async def cancel(self, ctx: CancelOperationContext, token: str) -> None: + # Capture cancel headers for test verification + self._cancel_headers_received.append( + { + k: v + for k, v in ctx.headers.items() + if k.startswith("x-custom-") or k.startswith("x-interceptor-") + } + ) + + +@service_handler(service=HeaderTestService) +class HeaderTestServiceImpl: + def __init__(self) -> None: + self.cancel_headers_received: list[dict[str, str]] = [] + + @sync_operation + async def header_echo_operation( + self, ctx: StartOperationContext, _input: None + ) -> HeaderTestOutput: + # Return headers with "x-custom-" or "x-interceptor-" prefix for verification + return HeaderTestOutput( + received_headers={ + k: v + for k, v in ctx.headers.items() + if k.startswith("x-custom-") or k.startswith("x-interceptor-") + } + ) + + @workflow_run_operation + async def workflow_run_header_operation( + self, ctx: WorkflowRunOperationContext, _input: None + ) -> nexus.WorkflowHandle[HeaderTestOutput]: + # Filter headers and pass to backing workflow + filtered_headers = { + k: v + for k, v in ctx.headers.items() + if k.startswith("x-custom-") or k.startswith("x-interceptor-") + } + return await ctx.start_workflow( + HeaderEchoWorkflow.run, + filtered_headers, + id=str(uuid.uuid4()), + ) + + @operation_handler + def cancellable_operation(self) -> OperationHandler[None, str]: + return CancellableOperationHandler(self.cancel_headers_received) + + +class CancellableDeadlineOperationHandler(OperationHandler[None, str]): + """Operation handler that captures request_deadline from start and cancel contexts.""" + + def __init__( + self, + start_deadlines_received: list[datetime | None], + cancel_deadlines_received: list[datetime | None], + cancel_received: asyncio.Event, + ) -> None: + self._start_deadlines_received = start_deadlines_received + self._cancel_deadlines_received = cancel_deadlines_received + self._cancel_received = cancel_received + + async def start( + self, ctx: StartOperationContext, input: None + ) -> StartOperationResultAsync: + self._start_deadlines_received.append(ctx.request_deadline) + return StartOperationResultAsync("test-token") + + async def cancel(self, ctx: CancelOperationContext, token: str) -> None: + self._cancel_deadlines_received.append(ctx.request_deadline) + self._cancel_received.set() + + +@service_handler(service=RequestDeadlineService) +class RequestDeadlineServiceImpl: + def __init__(self) -> None: + self.start_deadlines_received: list[datetime | None] = [] + self.cancel_deadlines_received: list[datetime | None] = [] + self.cancel_received = asyncio.Event() + + @operation_handler + def cancellable_op(self) -> OperationHandler[None, str]: + return CancellableDeadlineOperationHandler( + self.start_deadlines_received, + self.cancel_deadlines_received, + self.cancel_received, + ) + + # ----------------------------------------------------------------------------- # Caller workflow # @@ -250,15 +413,17 @@ class CallerWorkflow: def __init__( self, input: CallerWfInput, - request_cancel: bool, + _request_cancel: bool, task_queue: str, ) -> None: - self.nexus_client = workflow.create_nexus_client( - service={ - CallerReference.IMPL_WITH_INTERFACE: ServiceImpl, - CallerReference.INTERFACE: ServiceInterface, - }[input.op_input.caller_reference], - endpoint=make_nexus_endpoint_name(task_queue), + self.nexus_client: workflow.NexusClient[ServiceInterface | ServiceImpl] = ( # type:ignore[reportAttributeAccessIssue] + workflow.create_nexus_client( + service={ + CallerReference.IMPL_WITH_INTERFACE: ServiceImpl, + CallerReference.INTERFACE: ServiceInterface, + }[input.op_input.caller_reference], + endpoint=make_nexus_endpoint_name(task_queue), + ) ) self._nexus_operation_start_resolved = False self._proceed = False @@ -268,7 +433,7 @@ async def run( self, input: CallerWfInput, request_cancel: bool, - task_queue: str, + _task_queue: str, ) -> CallerWfOutput: op_input = input.op_input try: @@ -281,13 +446,13 @@ async def run( self._nexus_operation_start_resolved = True if not input.op_input.response_type.exception_in_operation_start: if isinstance(input.op_input.response_type, SyncResponse): - assert ( - op_handle.operation_token is None - ), "operation_token should be absent after a sync response" + assert op_handle.operation_token is None, ( + "operation_token should be absent after a sync response" + ) else: - assert ( - op_handle.operation_token - ), "operation_token should be present after an async response" + assert op_handle.operation_token, ( + "operation_token should be present after an async response" + ) if request_cancel: # Even for SyncResponse, the op_handle future is not done at this point; that @@ -303,15 +468,14 @@ async def wait_nexus_operation_start_resolved(self) -> None: @staticmethod def _get_operation( op_input: OpInput, - ) -> Union[ - nexusrpc.Operation[OpInput, OpOutput], - Callable[..., Awaitable[OpOutput]], + ) -> ( + nexusrpc.Operation[OpInput, OpOutput] | Callable[..., Awaitable[OpOutput]] # We are not exposing operation factory methods to users as a way to write nexus # operations, and accordingly the types on NexusClient # start_operation/execute_operation to not permit it. We fake the type by # pretending that this function doesn't return such operations. # Callable[[Any], OperationHandler[OpInput, OpOutput]], - ]: + ): return { # type: ignore[return-value] ( SyncResponse, @@ -390,7 +554,7 @@ def __init__( @workflow.run async def run( - self, input: CallerWfInput, request_cancel: bool, task_queue: str + self, input: CallerWfInput, _request_cancel: bool, _task_queue: str ) -> CallerWfOutput: op_input = input.op_input if op_input.response_type.op_definition_type == OpDefinitionType.LONGHAND: @@ -422,14 +586,89 @@ async def run( return CallerWfOutput(op_output=OpOutput(value=op_output.value)) +@workflow.defn +class HeaderTestCallerWorkflow: + @workflow.run + async def run(self, input: HeaderTestCallerWfInput) -> HeaderTestOutput: + nexus_client = workflow.create_nexus_client( + service=HeaderTestService, + endpoint=make_nexus_endpoint_name(input.task_queue), + ) + return await nexus_client.execute_operation( + HeaderTestService.header_echo_operation, + None, + headers=input.headers, + ) + + +@workflow.defn +class CancelHeaderTestCallerWorkflow: + """Workflow that starts a cancellable operation and then cancels it.""" + + @workflow.run + async def run(self, input: CancelHeaderTestCallerWfInput) -> None: + nexus_client = workflow.create_nexus_client( + service=HeaderTestService, + endpoint=make_nexus_endpoint_name(input.task_queue), + ) + op_handle = await nexus_client.start_operation( + HeaderTestService.cancellable_operation, + None, + headers=input.headers, + ) + # Request cancellation - this sends cancel headers to the handler + op_handle.cancel() + # Wait briefly to allow cancel request to be processed + await asyncio.sleep(0.1) + + +@workflow.defn +class CancelDeadlineCallerWorkflow: + """Workflow that starts a cancellable operation and then cancels it, for deadline testing.""" + + @workflow.run + async def run(self, task_queue: str) -> None: + nexus_client = workflow.create_nexus_client( + service=RequestDeadlineService, + endpoint=make_nexus_endpoint_name(task_queue), + ) + op_handle = await nexus_client.start_operation( + RequestDeadlineService.cancellable_op, + None, + cancellation_type=workflow.NexusOperationCancellationType.WAIT_REQUESTED, + ) + # Request cancellation - this sends a cancel operation to the handler + op_handle.cancel() + + try: + await op_handle + except NexusOperationError: + pass + + +@workflow.defn +class WorkflowRunHeaderTestCallerWorkflow: + """Workflow that calls a workflow_run_operation and verifies headers.""" + + @workflow.run + async def run(self, input: WorkflowRunHeaderTestCallerWfInput) -> HeaderTestOutput: + nexus_client = workflow.create_nexus_client( + service=HeaderTestService, + endpoint=make_nexus_endpoint_name(input.task_queue), + ) + return await nexus_client.execute_operation( + HeaderTestService.workflow_run_header_operation, + None, + headers=input.headers, + ) + + # ----------------------------------------------------------------------------- # Tests # async def test_sync_operation_happy_path(client: Client, env: WorkflowEnvironment): - if env.supports_time_skipping: - pytest.skip("Nexus tests don't work with time-skipping server") task_queue = str(uuid.uuid4()) async with Worker( client, @@ -438,7 +677,8 @@ async def test_sync_operation_happy_path(client: Client, env: WorkflowEnvironmen task_queue=task_queue, workflow_failure_exception_types=[Exception], ): - await create_nexus_endpoint(task_queue, client) + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) wf_output = await client.execute_workflow( CallerWorkflow.run, args=[ @@ -462,11 +702,57 @@ async def test_sync_operation_happy_path(client: Client, env: WorkflowEnvironmen assert wf_output.op_output.value == "sync response" +@service_handler +class NexusInfoService: + @sync_operation + async def get_info( + self, _ctx: StartOperationContext, _input: None + ) -> dict[str, str]: + info = nexus.info() + return { + "endpoint": info.endpoint, + "namespace": info.namespace, + "task_queue": info.task_queue, + } + + +@workflow.defn +class NexusInfoCallerWorkflow: + @workflow.run + async def run(self, task_queue: str) -> dict[str, str]: + nexus_client = workflow.create_nexus_client( + service=NexusInfoService, + endpoint=make_nexus_endpoint_name(task_queue), + ) + return await nexus_client.execute_operation(NexusInfoService.get_info, None) + + +async def test_nexus_info_includes_namespace(client: Client, env: WorkflowEnvironment): + task_queue = str(uuid.uuid4()) + async with Worker( + client, + nexus_service_handlers=[NexusInfoService()], + workflows=[NexusInfoCallerWorkflow], + task_queue=task_queue, + ): + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) + result = await client.execute_workflow( + NexusInfoCallerWorkflow.run, + task_queue, + id=str(uuid.uuid4()), + task_queue=task_queue, + ) + if not env.supports_time_skipping: + # Time-skipping server doesn't send the endpoint yet. + assert result["endpoint"] == endpoint_name + assert result["namespace"] == client.namespace + assert result["task_queue"] == task_queue + + async def test_workflow_run_operation_happy_path( client: Client, env: WorkflowEnvironment ): - if env.supports_time_skipping: - pytest.skip("Nexus tests don't work with time-skipping server") task_queue = str(uuid.uuid4()) async with Worker( client, @@ -475,7 +761,8 @@ async def test_workflow_run_operation_happy_path( task_queue=task_queue, workflow_failure_exception_types=[Exception], ): - await create_nexus_endpoint(task_queue, client) + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) wf_output = await client.execute_workflow( CallerWorkflow.run, args=[ @@ -502,7 +789,242 @@ async def test_workflow_run_operation_happy_path( # TODO(nexus-preview): cross-namespace tests # TODO(nexus-preview): nexus endpoint pytest fixture? -# TODO(nexus-prerelease): test headers + + +# ----------------------------------------------------------------------------- +# Header tests +# + + +@dataclass +class HeaderModificationRecord: + original_headers: dict[str, str] + modified_headers: dict[str, str] + + +@dataclass +class CancelHeaderRecord: + original_headers: dict[str, str] + modified_headers: dict[str, str] + + +class HeaderModifyingNexusInterceptor(Interceptor): + def __init__(self) -> None: + self.header_records: list[HeaderModificationRecord] = [] + self.cancel_header_records: list[CancelHeaderRecord] = [] + + def intercept_nexus_operation( + self, next: NexusOperationInboundInterceptor + ) -> NexusOperationInboundInterceptor: + return _HeaderModifyingNexusInboundInterceptor(next, self) + + +class _HeaderModifyingNexusInboundInterceptor(NexusOperationInboundInterceptor): + def __init__( + self, + next: NexusOperationInboundInterceptor, + root: HeaderModifyingNexusInterceptor, + ): + super().__init__(next) + self._root = root + + async def execute_nexus_operation_start( + self, input: ExecuteNexusOperationStartInput + ) -> StartOperationResultSync[Any] | StartOperationResultAsync: + import dataclasses + + original_headers = dict(input.ctx.headers) + + # Modify headers: prefix values and add new header + modified_headers = { + k: f"interceptor-modified-{v}" if k.startswith("x-custom-") else v + for k, v in input.ctx.headers.items() + } + modified_headers["x-interceptor-added"] = "interceptor-value" + + self._root.header_records.append( + HeaderModificationRecord( + original_headers=original_headers, + modified_headers=modified_headers, + ) + ) + + input.ctx = dataclasses.replace(input.ctx, headers=modified_headers) + return await super().execute_nexus_operation_start(input) + + async def execute_nexus_operation_cancel( + self, input: ExecuteNexusOperationCancelInput + ) -> None: + import dataclasses + + original_headers = dict(input.ctx.headers) + + # Modify headers: prefix values and add new header + modified_headers = { + k: f"interceptor-modified-{v}" if k.startswith("x-custom-") else v + for k, v in input.ctx.headers.items() + } + modified_headers["x-interceptor-added"] = "cancel-interceptor-value" + + self._root.cancel_header_records.append( + CancelHeaderRecord( + original_headers=original_headers, + modified_headers=modified_headers, + ) + ) + + input.ctx = dataclasses.replace(input.ctx, headers=modified_headers) + return await super().execute_nexus_operation_cancel(input) + + +class HeaderAddingOutboundInterceptor(Interceptor): + """Outbound interceptor that adds a static header to Nexus operation requests.""" + + def workflow_interceptor_class( + self, input: WorkflowInterceptorClassInput + ) -> type[WorkflowInboundInterceptor] | None: + return _HeaderAddingWorkflowInboundInterceptor + + +class _HeaderAddingWorkflowInboundInterceptor(WorkflowInboundInterceptor): + def init(self, outbound: WorkflowOutboundInterceptor) -> None: + super().init(_HeaderAddingWorkflowOutboundInterceptor(outbound)) + + +class _HeaderAddingWorkflowOutboundInterceptor(WorkflowOutboundInterceptor): + async def start_nexus_operation( + self, input: StartNexusOperationInput + ) -> workflow.NexusOperationHandle: + existing_headers = dict(input.headers) if input.headers else {} + existing_headers["x-custom-outbound"] = "outbound-value" + input = dataclasses.replace(input, headers=existing_headers) + return await super().start_nexus_operation(input) + + +async def test_start_operation_headers( + client: Client, + env: WorkflowEnvironment, +): + """Test headers from workflow and interceptors are propagated to start operation handler.""" + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work with time-skipping server") + + task_queue = str(uuid.uuid4()) + inbound_interceptor = HeaderModifyingNexusInterceptor() + + async with Worker( + client, + nexus_service_handlers=[HeaderTestServiceImpl()], + workflows=[HeaderTestCallerWorkflow], + task_queue=task_queue, + interceptors=[HeaderAddingOutboundInterceptor(), inbound_interceptor], + ): + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) + + workflow_headers = {"x-custom-from-workflow": "workflow-value"} + result = await client.execute_workflow( + HeaderTestCallerWorkflow.run, + HeaderTestCallerWfInput(headers=workflow_headers, task_queue=task_queue), + id=str(uuid.uuid4()), + task_queue=task_queue, + ) + + # Verify inbound interceptor saw headers from workflow and outbound interceptor + assert len(inbound_interceptor.header_records) == 1 + record = inbound_interceptor.header_records[0] + assert record.original_headers.get("x-custom-from-workflow") == "workflow-value" + assert record.original_headers.get("x-custom-outbound") == "outbound-value" + + # Verify handler received headers modified by inbound interceptor + assert ( + result.received_headers.get("x-custom-from-workflow") + == "interceptor-modified-workflow-value" + ) + assert ( + result.received_headers.get("x-custom-outbound") + == "interceptor-modified-outbound-value" + ) + assert result.received_headers.get("x-interceptor-added") == "interceptor-value" + + +async def test_workflow_run_operation_headers( + client: Client, + env: WorkflowEnvironment, +): + """Test that headers are propagated to @workflow_run_operation handlers.""" + task_queue = str(uuid.uuid4()) + test_headers = {"x-custom-workflow-run": "workflow-run-value"} + + async with Worker( + client, + nexus_service_handlers=[HeaderTestServiceImpl()], + workflows=[WorkflowRunHeaderTestCallerWorkflow, HeaderEchoWorkflow], + task_queue=task_queue, + ): + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) + + result = await client.execute_workflow( + WorkflowRunHeaderTestCallerWorkflow.run, + WorkflowRunHeaderTestCallerWfInput( + headers=test_headers, task_queue=task_queue + ), + id=str(uuid.uuid4()), + task_queue=task_queue, + ) + assert ( + result.received_headers.get("x-custom-workflow-run") == "workflow-run-value" + ) + + +async def test_cancel_operation_headers( + client: Client, + env: WorkflowEnvironment, +): + """Test headers from workflow and interceptor are propagated to cancel operation handler.""" + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work with time-skipping server") + + task_queue = str(uuid.uuid4()) + workflow_id = str(uuid.uuid4()) + inbound_interceptor = HeaderModifyingNexusInterceptor() + service_handler = HeaderTestServiceImpl() + + async with Worker( + client, + nexus_service_handlers=[service_handler], + workflows=[CancelHeaderTestCallerWorkflow], + task_queue=task_queue, + interceptors=[inbound_interceptor], + ): + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) + + workflow_headers = {"x-custom-cancel": "cancel-value"} + await client.execute_workflow( + CancelHeaderTestCallerWorkflow.run, + CancelHeaderTestCallerWfInput( + workflow_id=workflow_id, + headers=workflow_headers, + task_queue=task_queue, + ), + id=str(uuid.uuid4()), + task_queue=task_queue, + ) + + # Verify inbound interceptor saw cancel headers from workflow + assert len(inbound_interceptor.cancel_header_records) == 1 + record = inbound_interceptor.cancel_header_records[0] + assert record.original_headers.get("x-custom-cancel") == "cancel-value" + + # Verify handler received headers modified by inbound interceptor + assert len(service_handler.cancel_headers_received) == 1 + received = service_handler.cancel_headers_received[0] + assert received.get("x-custom-cancel") == "interceptor-modified-cancel-value" + assert received.get("x-interceptor-added") == "cancel-interceptor-value" + + @pytest.mark.parametrize("exception_in_operation_start", [False, True]) @pytest.mark.parametrize("request_cancel", [False, True]) @pytest.mark.parametrize( @@ -531,7 +1053,8 @@ async def test_sync_response( task_queue=task_queue, workflow_failure_exception_types=[Exception], ): - await create_nexus_endpoint(task_queue, client) + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) caller_wf_handle = await client.start_workflow( CallerWorkflow.run, args=[ @@ -553,8 +1076,6 @@ async def test_sync_response( task_queue=task_queue, ) - # TODO(nexus-prerelease): check bidi links for sync operation - # The operation result is returned even when request_cancel=True, because the # response was synchronous and it could not be cancelled. See explanation below. if exception_in_operation_start: @@ -607,6 +1128,7 @@ async def test_async_response( workflow_failure_exception_types=[Exception], ): caller_wf_handle, handler_wf_handle = await _start_wf_and_nexus_op( + env, client, task_queue, exception_in_operation_start, @@ -632,12 +1154,14 @@ async def test_async_response( ) return - # TODO(nexus-prerelease): race here? How do we know it hasn't been canceled already? handler_wf_info = await handler_wf_handle.describe() - assert handler_wf_info.status in [ + expected_statuses = [ WorkflowExecutionStatus.RUNNING, WorkflowExecutionStatus.COMPLETED, ] + if request_cancel: + expected_statuses.append(WorkflowExecutionStatus.CANCELED) + assert handler_wf_info.status in expected_statuses await assert_handler_workflow_has_link_to_caller_workflow( caller_wf_handle, handler_wf_handle ) @@ -673,13 +1197,14 @@ async def test_async_response( handler_wf_info = await handler_wf_handle.describe() assert handler_wf_info.status == WorkflowExecutionStatus.CANCELED else: - handler_wf_info = await handler_wf_handle.describe() - assert handler_wf_info.status == WorkflowExecutionStatus.COMPLETED result = await caller_wf_handle.result() assert result.op_output.value == "workflow result" + handler_wf_info = await handler_wf_handle.describe() + assert handler_wf_info.status == WorkflowExecutionStatus.COMPLETED async def _start_wf_and_nexus_op( + env: WorkflowEnvironment, client: Client, task_queue: str, exception_in_operation_start: bool, @@ -693,7 +1218,8 @@ async def _start_wf_and_nexus_op( """ Start the caller workflow and wait until the Nexus operation has started. """ - await create_nexus_endpoint(task_queue, client) + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) operation_workflow_id = str(uuid.uuid4()) # Start the caller workflow and wait until it confirms the Nexus operation has started. @@ -765,7 +1291,7 @@ async def test_untyped_caller( task_queue=task_queue, workflow_failure_exception_types=[Exception], ): - if response_type == SyncResponse: + if type(response_type) is SyncResponse: response_type = SyncResponse( op_definition_type=op_definition_type, use_async_def=True, @@ -778,7 +1304,8 @@ async def test_untyped_caller( op_definition_type=op_definition_type, exception_in_operation_start=exception_in_operation_start, ) - await create_nexus_endpoint(task_queue, client) + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) caller_wf_handle = await client.start_workflow( UntypedCallerWorkflow.run, args=[ @@ -821,9 +1348,6 @@ class ServiceClassNameOutput: name: str -# TODO(nexus-prerelease): async and non-async cancel methods - - @nexusrpc.service class ServiceInterfaceWithoutNameOverride: op: nexusrpc.Operation[None, ServiceClassNameOutput] @@ -838,7 +1362,7 @@ class ServiceInterfaceWithNameOverride: class ServiceImplInterfaceWithNeitherInterfaceNorNameOverride: @sync_operation async def op( - self, ctx: StartOperationContext, input: None + self, _ctx: StartOperationContext, _input: None ) -> ServiceClassNameOutput: return ServiceClassNameOutput(self.__class__.__name__) @@ -847,7 +1371,7 @@ async def op( class ServiceImplInterfaceWithoutNameOverride: @sync_operation async def op( - self, ctx: StartOperationContext, input: None + self, _ctx: StartOperationContext, _input: None ) -> ServiceClassNameOutput: return ServiceClassNameOutput(self.__class__.__name__) @@ -856,7 +1380,7 @@ async def op( class ServiceImplInterfaceWithNameOverride: @sync_operation async def op( - self, ctx: StartOperationContext, input: None + self, _ctx: StartOperationContext, _input: None ) -> ServiceClassNameOutput: return ServiceClassNameOutput(self.__class__.__name__) @@ -865,7 +1389,7 @@ async def op( class ServiceImplWithNameOverride: @sync_operation async def op( - self, ctx: StartOperationContext, input: None + self, _ctx: StartOperationContext, _input: None ) -> ServiceClassNameOutput: return ServiceClassNameOutput(self.__class__.__name__) @@ -902,7 +1426,7 @@ async def run( f"Invalid combination of caller_reference ({caller_reference}) and name_override ({name_override})" ) - nexus_client = workflow.create_nexus_client( + nexus_client: workflow.NexusClient[Any] = workflow.create_nexus_client( service=service_cls, endpoint=make_nexus_endpoint_name(task_queue), ) @@ -910,15 +1434,9 @@ async def run( return await nexus_client.execute_operation(service_cls.op, None) # type: ignore -# TODO(nexus-prerelease): check missing decorator behavior - - async def test_service_interface_and_implementation_names( client: Client, env: WorkflowEnvironment ): - if env.supports_time_skipping: - pytest.skip("Nexus tests don't work with time-skipping server") - # Note that: # - The caller can specify the service & operation via a reference to either the # interface or implementation class. @@ -945,7 +1463,8 @@ async def test_service_interface_and_implementation_names( task_queue=task_queue, workflow_failure_exception_types=[Exception], ): - await create_nexus_endpoint(task_queue, client) + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) assert await client.execute_workflow( ServiceInterfaceAndImplCallerWorkflow.run, args=(CallerReference.INTERFACE, NameOverride.YES, task_queue), @@ -1009,7 +1528,7 @@ async def run(self, input: str) -> str: class ServiceImplWithOperationsThatExecuteWorkflowBeforeStartingBackingWorkflow: @workflow_run_operation async def my_workflow_run_operation( - self, ctx: WorkflowRunOperationContext, input: None + self, ctx: WorkflowRunOperationContext, _input: None ) -> nexus.WorkflowHandle[str]: result_1 = await nexus.client().execute_workflow( EchoWorkflow.run, @@ -1030,7 +1549,7 @@ async def my_workflow_run_operation( @workflow.defn class WorkflowCallingNexusOperationThatExecutesWorkflowBeforeStartingBackingWorkflow: @workflow.run - async def run(self, input: str, task_queue: str) -> str: + async def run(self, _input: str, task_queue: str) -> str: nexus_client = workflow.create_nexus_client( service=ServiceImplWithOperationsThatExecuteWorkflowBeforeStartingBackingWorkflow, endpoint=make_nexus_endpoint_name(task_queue), @@ -1061,7 +1580,8 @@ async def test_workflow_run_operation_can_execute_workflow_before_starting_backi ], task_queue=task_queue, ): - await create_nexus_endpoint(task_queue, client) + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) result = await client.execute_workflow( WorkflowCallingNexusOperationThatExecutesWorkflowBeforeStartingBackingWorkflow.run, args=("result-1", task_queue), @@ -1071,6 +1591,71 @@ async def test_workflow_run_operation_can_execute_workflow_before_starting_backi assert result == "result-1-result-2" +@service_handler +class SimpleSyncService: + @sync_operation + async def sync_op(self, _ctx: StartOperationContext, input: str) -> str: + return input + + +@workflow.defn +class ExecuteNexusOperationWithSummaryWorkflow: + @workflow.run + async def run(self, input: str, task_queue: str) -> str: + nexus_client = workflow.create_nexus_client( + service=SimpleSyncService, + endpoint=make_nexus_endpoint_name(task_queue), + ) + + op_result = await nexus_client.execute_operation( + SimpleSyncService.sync_op, input, summary="nexus operation summary" + ) + + if op_result != input: + raise ApplicationError("expected nexus operation to echo input") + + return op_result + + +async def test_nexus_operation_summary( + client: Client, + env: WorkflowEnvironment, +): + task_queue = f"task-queue-{uuid.uuid4()}" + async with Worker( + client, + workflows=[ExecuteNexusOperationWithSummaryWorkflow], + nexus_service_handlers=[ + SimpleSyncService(), + ], + task_queue=task_queue, + ): + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) + wf_id = f"wf-{uuid.uuid4()}" + handle = await client.start_workflow( + ExecuteNexusOperationWithSummaryWorkflow.run, + args=("success", task_queue), + id=wf_id, + task_queue=task_queue, + ) + result = await handle.result() + assert result == "success" + + history = await handle.fetch_history() + + nexus_events = [ + event + for event in history.events + if event.HasField("nexus_operation_scheduled_event_attributes") + ] + assert len(nexus_events) == 1 + summary_value = PayloadConverter.default.from_payload( + nexus_events[0].user_metadata.summary + ) + assert summary_value == "nexus operation summary" + + # TODO(nexus-prerelease): test invalid service interface implementations # TODO(nexus-prerelease): test caller passing output_type @@ -1117,7 +1702,7 @@ async def assert_handler_workflow_has_link_to_caller_workflow( == temporalio.api.enums.v1.EventType.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED ) ) - links = _get_links_from_workflow_execution_started_event(wf_started_event) + links = links_from_workflow_execution_started_event(wf_started_event) if not len(links) == 1: pytest.fail( f"Expected 1 link on WorkflowExecutionStarted event, got {len(links)}" @@ -1133,16 +1718,6 @@ async def assert_handler_workflow_has_link_to_caller_workflow( ) -def _get_links_from_workflow_execution_started_event( - event: temporalio.api.history.v1.HistoryEvent, -) -> list[temporalio.api.common.v1.Link]: - [callback] = event.workflow_execution_started_event_attributes.completion_callbacks - if links := callback.links: - return list(links) - else: - return list(event.links) - - # When request_cancel is True, the NexusOperationHandle in the workflow evolves # through the following states: # start_fut result_fut handle_task w/ fut_waiter (task._must_cancel) @@ -1328,9 +1903,6 @@ async def run(self, op: str, input: OverloadTestValue) -> OverloadTestValue: async def test_workflow_run_operation_overloads( client: Client, env: WorkflowEnvironment, op: str ): - if env.supports_time_skipping: - pytest.skip("Nexus tests don't work with time-skipping server") - task_queue = str(uuid.uuid4()) async with Worker( client, @@ -1342,7 +1914,8 @@ async def test_workflow_run_operation_overloads( ], nexus_service_handlers=[OverloadTestServiceHandler()], ): - await create_nexus_endpoint(task_queue, client) + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) res = await client.execute_workflow( OverloadTestCallerWorkflow.run, args=[op, OverloadTestValue(value=2)], @@ -1354,3 +1927,383 @@ async def test_workflow_run_operation_overloads( if op != "no_param" else OverloadTestValue(value=0) ) + + +@nexusrpc.handler.service_handler +class CustomMetricsService: + @nexusrpc.handler.sync_operation + async def custom_metric_op( + self, _ctx: nexusrpc.handler.StartOperationContext, _input: None + ) -> None: + counter = nexus.metric_meter().create_counter( + "my-operation-counter", "my-operation-description", "my-operation-unit" + ) + counter.add(12) + counter.add(30, {"my-operation-extra-attr": 12.34}) + + @nexusrpc.handler.sync_operation + def custom_metric_op_executor( + self, _ctx: nexusrpc.handler.StartOperationContext, _input: None + ) -> None: + counter = nexus.metric_meter().create_counter( + "my-executor-operation-counter", + "my-executor-operation-description", + "my-executor-operation-unit", + ) + counter.add(12) + counter.add(30, {"my-executor-operation-extra-attr": 12.34}) + + +@workflow.defn +class CustomMetricsWorkflow: + @workflow.run + async def run(self, task_queue: str) -> None: + nexus_client = workflow.create_nexus_client( + service=CustomMetricsService, endpoint=make_nexus_endpoint_name(task_queue) + ) + + await nexus_client.execute_operation( + CustomMetricsService.custom_metric_op, None + ) + await nexus_client.execute_operation( + CustomMetricsService.custom_metric_op_executor, None + ) + + +async def test_workflow_caller_custom_metrics(client: Client, env: WorkflowEnvironment): + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) + + # Create new runtime with Prom server + prom_addr = f"127.0.0.1:{find_free_port()}" + runtime = Runtime( + telemetry=TelemetryConfig( + metrics=PrometheusConfig(bind_address=prom_addr), metric_prefix="foo_" + ) + ) + + # New client with the runtime + client = await env.connect_client( + runtime=runtime, + ) + + async with new_worker( + client, + CustomMetricsWorkflow, + task_queue=task_queue, + nexus_service_handlers=[CustomMetricsService()], + nexus_task_executor=concurrent.futures.ThreadPoolExecutor(), + ) as worker: + # Run workflow + await client.execute_workflow( + CustomMetricsWorkflow.run, + worker.task_queue, + id=f"wf-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + # Get Prom dump + with urlopen(url=f"http://{prom_addr}/metrics") as f: + prom_str: str = f.read().decode("utf-8") + prom_lines = prom_str.splitlines() + + prom_matcher = PromMetricMatcher(prom_lines) + + prom_matcher.assert_description_exists( + "my_operation_counter", "my-operation-description" + ) + prom_matcher.assert_metric_exists("my_operation_counter", {}, 12) + prom_matcher.assert_metric_exists( + "my_operation_counter", + { + "my_operation_extra_attr": "12.34", + # Also confirm some nexus operation labels + "nexus_service": CustomMetricsService.__name__, + "nexus_operation": CustomMetricsService.custom_metric_op.__name__, + "task_queue": worker.task_queue, + }, + 30, + ) + prom_matcher.assert_description_exists( + "my_executor_operation_counter", "my-executor-operation-description" + ) + prom_matcher.assert_metric_exists("my_executor_operation_counter", {}, 12) + prom_matcher.assert_metric_exists( + "my_executor_operation_counter", + { + "my_executor_operation_extra_attr": "12.34", + # Also confirm some nexus operation labels + "nexus_service": CustomMetricsService.__name__, + "nexus_operation": CustomMetricsService.custom_metric_op_executor.__name__, + "task_queue": worker.task_queue, + }, + 30, + ) + + +async def test_workflow_caller_buffered_metrics( + client: Client, env: WorkflowEnvironment +): + # Create runtime with metric buffer + buffer = MetricBuffer(10000) + runtime = Runtime( + telemetry=TelemetryConfig(metrics=buffer, metric_prefix="some_prefix_") + ) + + # Confirm no updates yet + assert not buffer.retrieve_updates() + + # Create a new client on the runtime and execute the custom metric workflow + client = await env.connect_client( + runtime=runtime, + ) + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) + async with new_worker( + client, + CustomMetricsWorkflow, + task_queue=task_queue, + nexus_service_handlers=[CustomMetricsService()], + nexus_task_executor=concurrent.futures.ThreadPoolExecutor(), + ) as worker: + await client.execute_workflow( + CustomMetricsWorkflow.run, + worker.task_queue, + id=f"wf-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + # Drain updates and confirm updates exist as expected + updates = buffer.retrieve_updates() + # Check for Nexus metrics + assert any( + update.metric.name == "my-operation-counter" + and update.metric.kind == BUFFERED_METRIC_KIND_COUNTER + and update.metric.description == "my-operation-description" + and update.attributes["nexus_service"] == CustomMetricsService.__name__ + and update.attributes["nexus_operation"] + == CustomMetricsService.custom_metric_op.__name__ + and update.attributes["task_queue"] == worker.task_queue + and "my-operation-extra-attr" not in update.attributes + and update.value == 12 + for update in updates + ) + assert any( + update.metric.name == "my-operation-counter" + and update.attributes.get("my-operation-extra-attr") == 12.34 + and update.value == 30 + for update in updates + ) + assert any( + update.metric.name == "my-executor-operation-counter" + and update.metric.description == "my-executor-operation-description" + and update.metric.kind == BUFFERED_METRIC_KIND_COUNTER + and update.attributes["nexus_service"] == CustomMetricsService.__name__ + and update.attributes["nexus_operation"] + == CustomMetricsService.custom_metric_op_executor.__name__ + and update.attributes["task_queue"] == worker.task_queue + and "my-executor-operation-extra-attr" not in update.attributes + and update.value == 12 + for update in updates + ) + assert any( + update.metric.name == "my-executor-operation-counter" + and update.attributes.get("my-executor-operation-extra-attr") == 12.34 + and update.value == 30 + for update in updates + ) + + +@workflow.defn() +class CancelTestCallerWorkflow: + def __init__(self) -> None: + self.released = False + + @workflow.run + async def run(self, use_async_cancel: bool, task_queue: str) -> str: + nexus_client = workflow.create_nexus_client( + service=TestAsyncAndNonAsyncCancel.CancelTestService, + endpoint=make_nexus_endpoint_name(task_queue), + ) + + op = ( + TestAsyncAndNonAsyncCancel.CancelTestService.async_cancel_op + if use_async_cancel + else TestAsyncAndNonAsyncCancel.CancelTestService.non_async_cancel_op + ) + + # Start the operation and immediately request cancellation + # Use WAIT_REQUESTED since we just need to verify the cancel handler was called + handle = await nexus_client.start_operation( + op, + None, + cancellation_type=workflow.NexusOperationCancellationType.WAIT_REQUESTED, + ) + + # Cancel the handle to trigger the cancel method on the handler + handle.cancel() + + try: + await handle + except NexusOperationError: + # Wait for release signal before completing + await workflow.wait_condition(lambda: self.released) + return "cancelled_successfully" + + return "unexpected_completion" + + @workflow.signal + def release(self) -> None: + self.released = True + + +@pytest.fixture(scope="class") +def cancel_test_events(request: pytest.FixtureRequest): + if request.cls: + request.cls.called_async = asyncio.Event() + request.cls.called_non_async = threading.Event() + yield + + +@pytest.mark.usefixtures("cancel_test_events") +class TestAsyncAndNonAsyncCancel: + called_async: asyncio.Event # pyright: ignore[reportUninitializedInstanceVariable] + called_non_async: threading.Event # pyright: ignore[reportUninitializedInstanceVariable] + + class OpWithAsyncCancel(OperationHandler[None, str]): + def __init__(self, evt: asyncio.Event) -> None: + self.evt = evt + + async def start( + self, ctx: StartOperationContext, input: None + ) -> StartOperationResultAsync: + return StartOperationResultAsync("test-token") + + async def cancel(self, ctx: CancelOperationContext, token: str) -> None: + self.evt.set() + + class OpWithNonAsyncCancel(OperationHandler[None, str]): + def __init__(self, evt: threading.Event) -> None: + self.evt = evt + + def start( + self, ctx: StartOperationContext, input: None + ) -> StartOperationResultAsync: + return StartOperationResultAsync("test-token") + + def cancel(self, ctx: CancelOperationContext, token: str) -> None: + self.evt.set() + + @nexusrpc.service + class CancelTestService: + async_cancel_op: nexusrpc.Operation[None, str] + non_async_cancel_op: nexusrpc.Operation[None, str] + + @service_handler(service=CancelTestService) + class CancelTestServiceHandler: + def __init__( + self, async_evt: asyncio.Event, non_async_evt: threading.Event + ) -> None: + self.async_evt = async_evt + self.non_async_evt = non_async_evt + + @operation_handler + def async_cancel_op(self) -> OperationHandler[None, str]: + return TestAsyncAndNonAsyncCancel.OpWithAsyncCancel(self.async_evt) + + @operation_handler + def non_async_cancel_op(self) -> OperationHandler[None, str]: + return TestAsyncAndNonAsyncCancel.OpWithNonAsyncCancel(self.non_async_evt) + + @pytest.mark.parametrize("use_async_cancel", [True, False]) + async def test_task_executor_operation_cancel_method( + self, client: Client, env: WorkflowEnvironment, use_async_cancel: bool + ): + """Test that both async and non-async cancel methods work for TaskExecutor-based operations.""" + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work with time-skipping server") + + task_queue = str(uuid.uuid4()) + async with Worker( + client, + task_queue=task_queue, + workflows=[CancelTestCallerWorkflow], + nexus_service_handlers=[ + TestAsyncAndNonAsyncCancel.CancelTestServiceHandler( + self.called_async, self.called_non_async + ) + ], + nexus_task_executor=concurrent.futures.ThreadPoolExecutor(), + ): + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) + + caller_wf_handle = await client.start_workflow( + CancelTestCallerWorkflow.run, + args=[use_async_cancel, task_queue], + id=f"caller-wf-{uuid.uuid4()}", + task_queue=task_queue, + ) + + # Wait for the cancel method to be called + fut = ( + self.called_async.wait() + if use_async_cancel + else asyncio.get_running_loop().run_in_executor( + None, self.called_non_async.wait + ) + ) + await asyncio.wait_for(fut, timeout=30) + + # Release the workflow to complete + await caller_wf_handle.signal(CancelTestCallerWorkflow.release) + + # Verify the workflow completed successfully + result = await caller_wf_handle.result() + assert result == "cancelled_successfully" + + +async def test_request_deadline_is_accessible_in_operation( + client: Client, + env: WorkflowEnvironment, +): + """Test that request_deadline is accessible in StartOperationContext.""" + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work with time-skipping server") + + task_queue = str(uuid.uuid4()) + service_handler = RequestDeadlineServiceImpl() + + async with Worker( + client, + nexus_service_handlers=[service_handler], + workflows=[CancelDeadlineCallerWorkflow], + task_queue=task_queue, + ): + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) + + await client.execute_workflow( + CancelDeadlineCallerWorkflow.run, + task_queue, + id=str(uuid.uuid4()), + task_queue=task_queue, + ) + + assert len(service_handler.start_deadlines_received) == 1 + deadline = service_handler.start_deadlines_received[0] + assert deadline is not None, ( + "request_deadline should be set in StartOperationContext" + ) + assert deadline.tzinfo is timezone.utc, "request_deadline should be in utc" + + await asyncio.wait_for(service_handler.cancel_received.wait(), 1) + + assert len(service_handler.cancel_deadlines_received) == 1 + deadline = service_handler.cancel_deadlines_received[0] + assert deadline is not None, ( + "request_deadline should be set in CancelOperationContext" + ) + assert deadline.tzinfo is timezone.utc, "request_deadline should be in utc" diff --git a/tests/nexus/test_workflow_caller_cancellation_types.py b/tests/nexus/test_workflow_caller_cancellation_types.py index 0590bb2a6..eca269984 100644 --- a/tests/nexus/test_workflow_caller_cancellation_types.py +++ b/tests/nexus/test_workflow_caller_cancellation_types.py @@ -1,14 +1,16 @@ import asyncio +import logging import uuid from dataclasses import dataclass, field from datetime import datetime, timezone -from typing import Any, Optional +from typing import Any import nexusrpc import nexusrpc.handler._decorators import pytest import temporalio.nexus._operation_handlers +import temporalio.worker._workflow_instance from temporalio import exceptions, nexus, workflow from temporalio.api.enums.v1 import EventType from temporalio.client import ( @@ -20,7 +22,12 @@ from temporalio.common import WorkflowIDConflictPolicy from temporalio.testing import WorkflowEnvironment from temporalio.worker import Worker -from tests.helpers.nexus import create_nexus_endpoint, make_nexus_endpoint_name +from tests.helpers import LogCapturer, assert_event_subsequence, assert_eventually +from tests.helpers.nexus import make_nexus_endpoint_name + +# Cloud CI's namespace credentials cannot manage Nexus endpoints. +# See https://github.com/temporalio/sdk-python/issues/1704. +pytestmark = pytest.mark.requires_local_server @dataclass @@ -40,12 +47,15 @@ class TestContext: class HandlerWorkflow: def __init__(self): self.caller_op_future_resolved = asyncio.Event() + self.cancel_requested = False + self.release_cancellation = asyncio.Event() @workflow.run async def run(self) -> None: try: await asyncio.Future() except asyncio.CancelledError: + self.cancel_requested = True if test_context.cancellation_type in [ workflow.NexusOperationCancellationType.TRY_CANCEL, workflow.NexusOperationCancellationType.WAIT_REQUESTED, @@ -53,12 +63,25 @@ async def run(self) -> None: # We want to prove that the caller op future can be resolved before the operation # (i.e. its backing workflow) is cancelled. await self.caller_op_future_resolved.wait() + elif ( + test_context.cancellation_type + == workflow.NexusOperationCancellationType.WAIT_COMPLETED + ): + await self.release_cancellation.wait() raise @workflow.signal def set_caller_op_future_resolved(self) -> None: self.caller_op_future_resolved.set() + @workflow.signal + def set_release_cancellation(self) -> None: + self.release_cancellation.set() + + @workflow.query + def has_cancel_requested(self) -> bool: + return self.cancel_requested + @nexusrpc.service class Service: @@ -68,7 +91,7 @@ class Service: class WorkflowOpHandler( temporalio.nexus._operation_handlers.WorkflowRunOperationHandler ): - def __init__(self): + def __init__(self): # type:ignore[reportMissingSuperCall] pass async def start( @@ -116,7 +139,7 @@ def workflow_op(self) -> nexusrpc.handler.OperationHandler[None, None]: @dataclass class Input: endpoint: str - cancellation_type: Optional[workflow.NexusOperationCancellationType] + cancellation_type: workflow.NexusOperationCancellationType | None @dataclass @@ -134,7 +157,7 @@ def __init__(self, input: Input): endpoint=input.endpoint, ) self.released = False - self.operation_token: Optional[str] = None + self.operation_token: str | None = None self.caller_op_future_resolved: asyncio.Future[datetime] = asyncio.Future() @workflow.signal @@ -151,6 +174,10 @@ async def get_operation_token(self) -> str: async def wait_caller_op_future_resolved(self) -> None: await self.caller_op_future_resolved + @workflow.query + def has_caller_op_future_resolved(self) -> bool: + return self.caller_op_future_resolved.done() + @workflow.run async def run(self, input: Input) -> CancellationResult: op_handle = await ( @@ -238,9 +265,6 @@ async def test_cancellation_type( env: WorkflowEnvironment, cancellation_type_name: str, ): - if env.supports_time_skipping: - pytest.skip("Nexus tests don't work with time-skipping server") - cancellation_type = workflow.NexusOperationCancellationType[cancellation_type_name] global test_context test_context = TestContext( @@ -250,52 +274,72 @@ async def test_cancellation_type( client = env.client - async with Worker( - client, - task_queue=str(uuid.uuid4()), - workflows=[CallerWorkflow, HandlerWorkflow], - nexus_service_handlers=[ServiceHandler()], - ) as worker: - await create_nexus_endpoint(worker.task_queue, client) - - # Start the caller workflow, wait for the nexus op to have started and retrieve the nexus op - # token - with_start_workflow = WithStartWorkflowOperation( - CallerWorkflow.run, - Input( - endpoint=make_nexus_endpoint_name(worker.task_queue), - cancellation_type=cancellation_type, - ), - id=test_context.caller_workflow_id, - task_queue=worker.task_queue, - id_conflict_policy=WorkflowIDConflictPolicy.FAIL, - ) + log_capturer = LogCapturer() + with log_capturer.logs_captured( + temporalio.worker._workflow_instance.logger, level=logging.WARNING + ): + async with Worker( + client, + task_queue=str(uuid.uuid4()), + workflows=[CallerWorkflow, HandlerWorkflow], + nexus_service_handlers=[ServiceHandler()], + ) as worker: + await env.create_nexus_endpoint( + make_nexus_endpoint_name(worker.task_queue), worker.task_queue + ) - operation_token = await client.execute_update_with_start_workflow( - CallerWorkflow.get_operation_token, - start_workflow_operation=with_start_workflow, - ) - handler_wf = ( - nexus.WorkflowHandle[None] - .from_token(operation_token) - ._to_client_workflow_handle(client) - ) - caller_wf = await with_start_workflow.workflow_handle() - - if cancellation_type == workflow.NexusOperationCancellationType.ABANDON: - await check_behavior_for_abandon(caller_wf, handler_wf) - elif cancellation_type == workflow.NexusOperationCancellationType.TRY_CANCEL: - await check_behavior_for_try_cancel(caller_wf, handler_wf) - elif ( - cancellation_type == workflow.NexusOperationCancellationType.WAIT_REQUESTED - ): - await check_behavior_for_wait_cancellation_requested(caller_wf, handler_wf) - elif ( - cancellation_type == workflow.NexusOperationCancellationType.WAIT_COMPLETED - ): - await check_behavior_for_wait_cancellation_completed(caller_wf, handler_wf) - else: - pytest.fail(f"Invalid cancellation type: {cancellation_type}") + # Start the caller workflow, wait for the nexus op to have started and retrieve the nexus op + # token + with_start_workflow = WithStartWorkflowOperation( + CallerWorkflow.run, + Input( + endpoint=make_nexus_endpoint_name(worker.task_queue), + cancellation_type=cancellation_type, + ), + id=test_context.caller_workflow_id, + task_queue=worker.task_queue, + id_conflict_policy=WorkflowIDConflictPolicy.FAIL, + ) + + operation_token = await client.execute_update_with_start_workflow( + CallerWorkflow.get_operation_token, + start_workflow_operation=with_start_workflow, + ) + handler_wf = ( + nexus.WorkflowHandle[None] + .from_token(operation_token) + ._to_client_workflow_handle(client) + ) + caller_wf = await with_start_workflow.workflow_handle() + + if cancellation_type == workflow.NexusOperationCancellationType.ABANDON: + await check_behavior_for_abandon(caller_wf, handler_wf) + elif ( + cancellation_type == workflow.NexusOperationCancellationType.TRY_CANCEL + ): + await check_behavior_for_try_cancel(caller_wf, handler_wf) + elif ( + cancellation_type + == workflow.NexusOperationCancellationType.WAIT_REQUESTED + ): + await check_behavior_for_wait_cancellation_requested( + caller_wf, handler_wf + ) + elif ( + cancellation_type + == workflow.NexusOperationCancellationType.WAIT_COMPLETED + ): + await check_behavior_for_wait_cancellation_completed( + caller_wf, handler_wf + ) + else: + pytest.fail(f"Invalid cancellation type: {cancellation_type}") + + # Verify no spurious "exception in shielded future" error logs + shielded_err = log_capturer.find_log("exception in shielded future") + assert shielded_err is None, ( + f"Unexpected 'exception in shielded future' log: {shielded_err}" + ) async def check_behavior_for_abandon( @@ -409,6 +453,17 @@ async def check_behavior_for_wait_cancellation_completed( Check that a cancellation request is sent and the caller workflow nexus operation future is unblocked after the operation is canceled. """ + + async def assert_handler_cancel_requested() -> None: + assert await handler_wf.query(HandlerWorkflow.has_cancel_requested) + + await assert_eventually(assert_handler_cancel_requested) + + handler_status = (await handler_wf.describe()).status + assert handler_status == WorkflowExecutionStatus.RUNNING + assert not await caller_wf.query(CallerWorkflow.has_caller_op_future_resolved) + + await handler_wf.signal(HandlerWorkflow.set_release_cancellation) try: await handler_wf.result() except WorkflowFailureError as err: @@ -419,8 +474,13 @@ async def check_behavior_for_wait_cancellation_completed( handler_status = (await handler_wf.describe()).status assert handler_status == WorkflowExecutionStatus.CANCELED + async def assert_caller_op_future_resolved() -> None: + assert await caller_wf.query(CallerWorkflow.has_caller_op_future_resolved) + + await assert_eventually(assert_caller_op_future_resolved) + await caller_wf.signal(CallerWorkflow.release) - result = await caller_wf.result() + await caller_wf.result() await assert_event_subsequence( caller_wf, @@ -431,14 +491,6 @@ async def check_behavior_for_wait_cancellation_completed( EventType.EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED, ], ) - handler_wf_canceled_event = await get_event_time( - handler_wf, - EventType.EVENT_TYPE_WORKFLOW_EXECUTION_CANCELED, - ) - assert handler_wf_canceled_event <= result.caller_op_future_resolved, ( - "expected caller op future resolved after handler workflow canceled, but got " - f"{result.caller_op_future_resolved} before {handler_wf_canceled_event}" - ) async def has_event(wf_handle: WorkflowHandle, event_type: EventType.ValueType): @@ -457,38 +509,3 @@ async def get_event_time( return event.event_time.ToDatetime().replace(tzinfo=timezone.utc) event_type_name = EventType.Name(event_type).removeprefix("EVENT_TYPE_") assert False, f"Event {event_type_name} not found in {wf_handle.id}" - - -async def assert_event_subsequence( - wf_handle: WorkflowHandle, - expected_events: list[EventType.ValueType], -) -> None: - """ - Given a workflow handle and a sequence of event types, assert that the workflow's history - contains that subsequence of events in the order specified. - """ - all_events = [] - async for e in wf_handle.fetch_history_events(): - all_events.append(e) - - _all_events = iter(all_events) - _expected_events = iter(expected_events) - - previous_expected_event_type_name = None - for expected_event_type in _expected_events: - expected_event_type_name = EventType.Name(expected_event_type).removeprefix( - "EVENT_TYPE_" - ) - has_expected = next( - (e for e in _all_events if e.event_type == expected_event_type), - None, - ) - if not has_expected: - if previous_expected_event_type_name is not None: - prefix = f"After {previous_expected_event_type_name}, " - else: - prefix = "" - pytest.fail( - f"{prefix}expected {expected_event_type_name} in workflow {wf_handle.id}" - ) - previous_expected_event_type_name = expected_event_type_name diff --git a/tests/nexus/test_workflow_caller_cancellation_types_when_cancel_handler_fails.py b/tests/nexus/test_workflow_caller_cancellation_types_when_cancel_handler_fails.py index 12f99a714..44d91a7d1 100644 --- a/tests/nexus/test_workflow_caller_cancellation_types_when_cancel_handler_fails.py +++ b/tests/nexus/test_workflow_caller_cancellation_types_when_cancel_handler_fails.py @@ -6,7 +6,7 @@ import uuid from dataclasses import dataclass, field from datetime import datetime, timezone -from typing import Any, Optional +from typing import Any import nexusrpc import nexusrpc.handler._decorators @@ -23,13 +23,17 @@ from temporalio.common import WorkflowIDConflictPolicy from temporalio.testing import WorkflowEnvironment from temporalio.worker import Worker -from tests.helpers.nexus import create_nexus_endpoint, make_nexus_endpoint_name +from tests.helpers import assert_event_subsequence, assert_eventually +from tests.helpers.nexus import make_nexus_endpoint_name from tests.nexus.test_workflow_caller_cancellation_types import ( - assert_event_subsequence, get_event_time, has_event, ) +# Cloud CI's namespace credentials cannot manage Nexus endpoints. +# See https://github.com/temporalio/sdk-python/issues/1704. +pytestmark = pytest.mark.requires_local_server + @dataclass class TestContext: @@ -48,6 +52,7 @@ class HandlerWorkflow: def __init__(self): self.cancel_handler_released = asyncio.Event() self.caller_op_future_resolved = asyncio.Event() + self.release_completion = asyncio.Event() @workflow.run async def run(self) -> None: @@ -61,6 +66,11 @@ async def run(self) -> None: # For WAIT_REQUESTED, we want to prove that the future can be unblocked before the # handler workflow completes. await self.caller_op_future_resolved.wait() + elif ( + test_context.cancellation_type + == workflow.NexusOperationCancellationType.WAIT_COMPLETED + ): + await self.release_completion.wait() @workflow.signal def set_cancel_handler_released(self) -> None: @@ -70,6 +80,14 @@ def set_cancel_handler_released(self) -> None: def set_caller_op_future_resolved(self) -> None: self.caller_op_future_resolved.set() + @workflow.signal + def set_release_completion(self) -> None: + self.release_completion.set() + + @workflow.query + def has_cancel_handler_released(self) -> bool: + return self.cancel_handler_released.is_set() + @nexusrpc.service class Service: @@ -79,7 +97,7 @@ class Service: class WorkflowOpHandler( temporalio.nexus._operation_handlers.WorkflowRunOperationHandler ): - def __init__(self): + def __init__(self): # type:ignore[reportMissingSuperCall] pass async def start( @@ -120,15 +138,15 @@ def workflow_op(self) -> nexusrpc.handler.OperationHandler[None, None]: @dataclass class Input: endpoint: str - cancellation_type: Optional[workflow.NexusOperationCancellationType] + cancellation_type: workflow.NexusOperationCancellationType | None @dataclass class CancellationResult: operation_token: str caller_op_future_resolved: datetime - error_type: Optional[str] = None - error_cause_type: Optional[str] = None + error_type: str | None = None + error_cause_type: str | None = None @workflow.defn(sandboxed=False) @@ -140,7 +158,7 @@ def __init__(self, input: Input): endpoint=input.endpoint, ) self.released = False - self.operation_token: Optional[str] = None + self.operation_token: str | None = None self.caller_op_future_resolved: asyncio.Future[datetime] = asyncio.Future() @workflow.signal @@ -153,6 +171,10 @@ async def get_operation_token(self) -> str: assert self.operation_token return self.operation_token + @workflow.query + def has_caller_op_future_resolved(self) -> bool: + return self.caller_op_future_resolved.done() + @workflow.run async def run(self, input: Input) -> CancellationResult: op_handle = await ( @@ -222,7 +244,9 @@ async def test_cancellation_type( workflows=[CallerWorkflow, HandlerWorkflow], nexus_service_handlers=[ServiceHandler()], ) as worker: - await create_nexus_endpoint(worker.task_queue, client) + await env.create_nexus_endpoint( + make_nexus_endpoint_name(worker.task_queue), worker.task_queue + ) # Start the caller workflow, wait for the nexus op to have started and retrieve the nexus op # token @@ -296,6 +320,12 @@ async def check_behavior_for_try_cancel( handler_wf: WorkflowHandle[Any, None], ) -> None: await handler_wf.result() + + cancel_request_failed = EventType.EVENT_TYPE_NEXUS_OPERATION_CANCEL_REQUEST_FAILED + async for event in caller_wf.fetch_history_events(wait_new_event=True): + if event.event_type == cancel_request_failed: + break + await caller_wf.signal(CallerWorkflow.release) result = await caller_wf.result() assert result.error_type == "NexusOperationError" @@ -362,7 +392,23 @@ async def check_behavior_for_wait_cancellation_completed( caller_wf: WorkflowHandle[Any, CancellationResult], handler_wf: WorkflowHandle, ) -> None: + async def assert_cancel_handler_released() -> None: + assert await handler_wf.query(HandlerWorkflow.has_cancel_handler_released) + + await assert_eventually(assert_cancel_handler_released) + + handler_status = (await handler_wf.describe()).status + assert handler_status == WorkflowExecutionStatus.RUNNING + assert not await caller_wf.query(CallerWorkflow.has_caller_op_future_resolved) + + await handler_wf.signal(HandlerWorkflow.set_release_completion) await handler_wf.result() + + async def assert_caller_op_future_resolved() -> None: + assert await caller_wf.query(CallerWorkflow.has_caller_op_future_resolved) + + await assert_eventually(assert_caller_op_future_resolved) + await caller_wf.signal(CallerWorkflow.release) result = await caller_wf.result() assert not result.error_type @@ -378,8 +424,3 @@ async def check_behavior_for_wait_cancellation_completed( EventType.EVENT_TYPE_NEXUS_OPERATION_COMPLETED, ], ) - handler_wf_completed = await get_event_time( - handler_wf, - EventType.EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED, - ) - assert handler_wf_completed <= result.caller_op_future_resolved diff --git a/tests/nexus/test_workflow_caller_error_chains.py b/tests/nexus/test_workflow_caller_error_chains.py index 3e3c05b0b..1012d8a94 100644 --- a/tests/nexus/test_workflow_caller_error_chains.py +++ b/tests/nexus/test_workflow_caller_error_chains.py @@ -1,8 +1,8 @@ from __future__ import annotations import uuid -from dataclasses import dataclass -from typing import Any, Callable +from collections.abc import Iterator +from dataclasses import dataclass, field import nexusrpc import nexusrpc.handler @@ -18,26 +18,196 @@ ) from temporalio.exceptions import ( ApplicationError, + CancelledError, NexusOperationError, ) from temporalio.testing import WorkflowEnvironment from temporalio.worker import Worker -from tests.helpers.nexus import create_nexus_endpoint, make_nexus_endpoint_name +from tests.helpers.nexus import make_nexus_endpoint_name -error_conversion_test_cases: dict[str, type[ErrorConversionTestCase]] = {} +# Cloud CI's namespace credentials cannot manage Nexus endpoints. +# See https://github.com/temporalio/sdk-python/issues/1704. +pytestmark = pytest.mark.requires_local_server @dataclass -class ErrorConversionTestCase: - action_in_nexus_operation: Callable[..., Any] - expected_exception_chain_in_workflow: list[tuple[type[Exception], dict[str, Any]]] +class ExpectedError: + message: str + optional: bool = field(kw_only=True, default=False) - def __init_subclass__(cls, **kwargs): - super().__init_subclass__(**kwargs) - assert cls.__name__ not in error_conversion_test_cases - error_conversion_test_cases[cls.__name__] = cls +@dataclass +class ExpectedNexusOperationError(ExpectedError): + service: str + + +@dataclass +class ExpectedHandlerError(ExpectedError): + type: str # HandlerErrorType name (e.g., "INTERNAL", "NOT_FOUND") + retryable: bool + + +@dataclass +class ExpectedApplicationError(ExpectedError): + non_retryable: bool + type: str | None = None + + +@dataclass +class ExpectedCancelledError(ExpectedError): + pass + + +class ExpectedErrorChain: + def __init__(self, *errs: ExpectedError) -> None: + self._errs = errs + + def required_len(self) -> int: + return sum(not e.optional for e in self._errs) + + def __len__(self) -> int: + return len(self._errs) + + def __iter__(self) -> Iterator[ExpectedError]: + return iter(self._errs) + + def __getitem__(self, i: int) -> ExpectedError: + return self._errs[i] + + +ExpectedExceptionInfo = ( + ExpectedNexusOperationError + | ExpectedHandlerError + | ExpectedApplicationError + | ExpectedCancelledError +) + + +@dataclass +class ErrorTestCase: + name: str + operation_name: str + expected_exception_chain: ExpectedErrorChain + + +class CustomError(Exception): + pass + + +@dataclass +class ErrorTestInput: + task_queue: str + operation_name: str + + +# Handler service with one operation per test case + + +@nexusrpc.handler.service_handler +class ErrorTestService: + @sync_operation + async def raise_application_error_non_retryable( + self, _ctx: StartOperationContext, _input: ErrorTestInput + ) -> None: + raise ApplicationError( + "application-error-message", + type="application-error-type", + non_retryable=True, + ) + + @sync_operation + async def raise_application_error_non_retryable_from_custom_error( + self, _ctx: StartOperationContext, _input: ErrorTestInput + ) -> None: + try: + raise CustomError("custom-error-message") + except CustomError as err: + raise ApplicationError( + "application-error-message", + type="application-error-type", + non_retryable=True, + ) from err + + @sync_operation + async def raise_nexus_handler_error_not_found( + self, _ctx: StartOperationContext, _input: ErrorTestInput + ) -> None: + try: + raise RuntimeError("runtime-error-message") + except RuntimeError as err: + raise nexusrpc.HandlerError( + "handler-error-message", + type=nexusrpc.HandlerErrorType.NOT_FOUND, + ) from err + + @sync_operation + async def raise_nexus_handler_error_not_found_from_custom_error( + self, _ctx: StartOperationContext, _input: ErrorTestInput + ) -> None: + try: + raise CustomError("custom-error-message") + except CustomError as err: + raise nexusrpc.HandlerError( + "handler-error-message", + type=nexusrpc.HandlerErrorType.NOT_FOUND, + ) from err + @sync_operation + async def raise_nexus_handler_error_not_found_from_handler_error_unavailable( + self, _ctx: StartOperationContext, _input: ErrorTestInput + ) -> None: + try: + raise nexusrpc.HandlerError( + "handler-error-message-2", + type=nexusrpc.HandlerErrorType.UNAVAILABLE, + ) + except nexusrpc.HandlerError as err: + raise nexusrpc.HandlerError( + "handler-error-message", + type=nexusrpc.HandlerErrorType.NOT_FOUND, + ) from err + + @sync_operation + async def raise_nexus_operation_error_from_application_error_non_retryable_from_custom_error( + self, _ctx: StartOperationContext, _input: ErrorTestInput + ) -> None: + try: + try: + raise CustomError("custom-error-message") + except CustomError as err: + raise ApplicationError( + "application-error-message", + type="application-error-type", + non_retryable=True, + ) from err + except ApplicationError as err: + raise nexusrpc.OperationError( + "operation-error-message", + state=nexusrpc.OperationErrorState.FAILED, + ) from err + + @sync_operation + async def raise_nexus_operation_canceled_error_from_application_error_non_retryable_from_custom_error( + self, _ctx: StartOperationContext, _input: ErrorTestInput + ) -> None: + try: + try: + raise CustomError("custom-error-message") + except CustomError as err: + raise ApplicationError( + "application-error-message", + type="application-error-type", + non_retryable=True, + ) from err + except ApplicationError as err: + raise nexusrpc.OperationError( + "operation-error-message", + state=nexusrpc.OperationErrorState.CANCELED, + ) from err + + +# Test cases +# # If a nexus handler raises a non-retryable ApplicationError, the calling workflow # should see a non-retryable exception. # @@ -69,185 +239,149 @@ def __init_subclass__(cls, **kwargs): # ] # } # ) - - -class RaiseApplicationErrorNonRetryable(ErrorConversionTestCase): - @staticmethod - def action_in_nexus_operation(): - raise ApplicationError( - "application-error-message", +RaiseApplicationErrorNonRetryable = ErrorTestCase( + name="RaiseApplicationErrorNonRetryable", + operation_name=ErrorTestService.raise_application_error_non_retryable.__name__, + expected_exception_chain=ExpectedErrorChain( + ExpectedNexusOperationError( + message="nexus operation completed unsuccessfully", + service="ErrorTestService", + ), + ExpectedHandlerError( + message="Handler failed with non-retryable application error", + type="INTERNAL", + retryable=False, + ), + ExpectedHandlerError( + message="Handler failed with non-retryable application error", + type="INTERNAL", + retryable=False, + optional=True, + ), + ExpectedApplicationError( + message="application-error-message", type="application-error-type", non_retryable=True, - ) + ), + ), +) - expected_exception_chain_in_workflow = [ - ( - NexusOperationError, - { - "service": "ErrorTestService", - "message": "nexus operation completed unsuccessfully", - }, + +RaiseApplicationErrorNonRetryableFromCustomError = ErrorTestCase( + name="RaiseApplicationErrorNonRetryableFromCustomError", + operation_name=ErrorTestService.raise_application_error_non_retryable_from_custom_error.__name__, + expected_exception_chain=ExpectedErrorChain( + ExpectedNexusOperationError( + message="nexus operation completed unsuccessfully", + service="ErrorTestService", ), - ( - nexusrpc.HandlerError, - { - # In this test case the user code raised ApplicationError directly, and - # a wrapping HandlerError was synthesized with the same error message as - # that of the ApplicationError. The server prepends 'handler error - # (INTERNAL):' - "message": "handler error (INTERNAL): application-error-message", - "type": nexusrpc.HandlerErrorType.INTERNAL, - "retryable": False, - }, + ExpectedHandlerError( + message="Handler failed with non-retryable application error", + type="INTERNAL", + retryable=False, ), - ( - ApplicationError, - { - "message": "application-error-message", - "type": "application-error-type", - "non_retryable": True, - }, + ExpectedHandlerError( + message="Handler failed with non-retryable application error", + type="INTERNAL", + retryable=False, + optional=True, ), - ] - - -class RaiseApplicationErrorNonRetryableFromCustomError(ErrorConversionTestCase): - @staticmethod - def action_in_nexus_operation(): - try: - raise CustomError("custom-error-message") - except CustomError as err: - raise ApplicationError( - "application-error-message", - type="application-error-type", - non_retryable=True, - ) from err - - expected_exception_chain_in_workflow = ( - RaiseApplicationErrorNonRetryable.expected_exception_chain_in_workflow - + [ - ( - ApplicationError, - { - "message": "custom-error-message", - "type": "CustomError", - "non_retryable": False, - }, - ), - ] - ) + ExpectedApplicationError( + message="application-error-message", + type="application-error-type", + non_retryable=True, + ), + ExpectedApplicationError( + message="custom-error-message", + type="CustomError", + non_retryable=False, + ), + ), +) -class RaiseNexusHandlerErrorNotFound(ErrorConversionTestCase): - @staticmethod - def action_in_nexus_operation(): - try: - raise RuntimeError("runtime-error-message") - except RuntimeError as err: - raise nexusrpc.HandlerError( - "handler-error-message", - type=nexusrpc.HandlerErrorType.NOT_FOUND, - ) from err - - expected_exception_chain_in_workflow = [ - ( - NexusOperationError, - { - "service": "ErrorTestService", - "message": "nexus operation completed unsuccessfully", - }, +RaiseNexusHandlerErrorNotFound = ErrorTestCase( + name="RaiseNexusHandlerErrorNotFound", + operation_name=ErrorTestService.raise_nexus_handler_error_not_found.__name__, + expected_exception_chain=ExpectedErrorChain( + ExpectedNexusOperationError( + message="nexus operation completed unsuccessfully", + service="ErrorTestService", ), - ( - nexusrpc.HandlerError, - { - # In this test case the user code raised HandlerError directly, so there - # was no need to synthesize a wrapping HandlerError The server prepends - # 'handler error (INTERNAL):' - "message": "handler error (NOT_FOUND): handler-error-message", - "type": nexusrpc.HandlerErrorType.NOT_FOUND, - # The following HandlerError types should be considered non-retryable: - # BAD_REQUEST, UNAUTHENTICATED, UNAUTHORIZED, NOT_FOUND, and - # RESOURCE_EXHAUSTED. In this test case, the handler does not set the - # retryable flag in the HandlerError sent to the server. This value is - # computed by the retryable property on HandlerError. - "retryable": False, - }, + ExpectedHandlerError( + message="handler-error-message", + type="NOT_FOUND", + retryable=False, ), - ( - ApplicationError, - { - "message": "handler-error-message", - "non_retryable": True, - }, + ExpectedHandlerError( + message="handler-error-message", + type="NOT_FOUND", + retryable=False, + optional=True, ), - ( - ApplicationError, - { - "message": "runtime-error-message", - "type": "RuntimeError", - "non_retryable": False, - }, + ExpectedApplicationError( + message="runtime-error-message", + type="RuntimeError", + non_retryable=False, ), - ] - - -class RaiseNexusHandlerErrorNotFoundFromCustomError(ErrorConversionTestCase): - @staticmethod - def action_in_nexus_operation(): - try: - raise CustomError("custom-error-message") - except CustomError as err: - raise nexusrpc.HandlerError( - "handler-error-message", - type=nexusrpc.HandlerErrorType.NOT_FOUND, - ) from err + ), +) - expected_exception_chain_in_workflow = ( - RaiseNexusHandlerErrorNotFound.expected_exception_chain_in_workflow[:-1] - + [ - ( - ApplicationError, - { - # TODO(nexus-preview): empirically, this is "handler-error-message", - # but it should be "runtime-error-message" - # "message": "runtime-error-message", - "type": "CustomError", - "non_retryable": False, - }, - ) - ] - ) +RaiseNexusHandlerErrorNotFoundFromCustomError = ErrorTestCase( + name="RaiseNexusHandlerErrorNotFoundFromCustomError", + operation_name=ErrorTestService.raise_nexus_handler_error_not_found_from_custom_error.__name__, + expected_exception_chain=ExpectedErrorChain( + ExpectedNexusOperationError( + message="nexus operation completed unsuccessfully", + service="ErrorTestService", + ), + ExpectedHandlerError( + message="handler-error-message", + type="NOT_FOUND", + retryable=False, + ), + ExpectedHandlerError( + message="handler-error-message", + type="NOT_FOUND", + retryable=False, + optional=True, + ), + ExpectedApplicationError( + message="custom-error-message", + type="CustomError", + non_retryable=False, + ), + ), +) -class RaiseNexusHandlerErrorNotFoundFromHandlerErrorUnavailable( - ErrorConversionTestCase -): - @staticmethod - def action_in_nexus_operation(): - try: - raise nexusrpc.HandlerError( - "handler-error-message-2", - type=nexusrpc.HandlerErrorType.UNAVAILABLE, - ) - except nexusrpc.HandlerError as err: - raise nexusrpc.HandlerError( - "handler-error-message", - type=nexusrpc.HandlerErrorType.NOT_FOUND, - ) from err - expected_exception_chain_in_workflow = ( - RaiseNexusHandlerErrorNotFound.expected_exception_chain_in_workflow[:-1] - + [ - ( - nexusrpc.HandlerError, - { - "message": "handler-error-message-2", - "type": nexusrpc.HandlerErrorType.UNAVAILABLE, - "retryable": True, - }, - ) - ] - ) +RaiseNexusHandlerErrorNotFoundFromHandlerErrorUnavailable = ErrorTestCase( + name="RaiseNexusHandlerErrorNotFoundFromHandlerErrorUnavailable", + operation_name=ErrorTestService.raise_nexus_handler_error_not_found_from_handler_error_unavailable.__name__, + expected_exception_chain=ExpectedErrorChain( + ExpectedNexusOperationError( + message="nexus operation completed unsuccessfully", + service="ErrorTestService", + ), + ExpectedHandlerError( + message="handler-error-message", + type="NOT_FOUND", + retryable=False, + ), + ExpectedHandlerError( + message="handler-error-message", + type="NOT_FOUND", + retryable=False, + optional=True, + ), + ExpectedHandlerError( + message="handler-error-message-2", + type="UNAVAILABLE", + retryable=True, + ), + ), +) # If a nexus handler raises an OperationError, the calling workflow @@ -291,72 +425,168 @@ def action_in_nexus_operation(): # ] # } # ) -# -class RaiseNexusOperationErrorFromApplicationErrorNonRetryableFromCustomError( - ErrorConversionTestCase -): - @staticmethod - def action_in_nexus_operation(): - try: - try: - raise CustomError("custom-error-message") - except CustomError as err: - raise ApplicationError( - "application-error-message", - type="application-error-type", - non_retryable=True, - ) from err - except ApplicationError as err: - raise nexusrpc.OperationError( - "operation-error-message", - state=nexusrpc.OperationErrorState.FAILED, - ) from err - - expected_exception_chain_in_workflow = [ - ( - NexusOperationError, - { - "message": "nexus operation completed unsuccessfully", - "service": "ErrorTestService", - }, +RaiseNexusOperationErrorFromApplicationErrorNonRetryableFromCustomError = ErrorTestCase( + name="RaiseNexusOperationErrorFromApplicationErrorNonRetryableFromCustomError", + operation_name=ErrorTestService.raise_nexus_operation_error_from_application_error_non_retryable_from_custom_error.__name__, + expected_exception_chain=ExpectedErrorChain( + ExpectedNexusOperationError( + message="nexus operation completed unsuccessfully", + service="ErrorTestService", ), - ( - ApplicationError, - { - "message": "application-error-message", - "type": "application-error-type", - "non_retryable": True, - }, + ExpectedApplicationError( + message="operation-error-message", type="OperationError", non_retryable=True ), - ( - ApplicationError, - { - "message": "custom-error-message", - "type": "CustomError", - "non_retryable": False, - }, + ExpectedApplicationError( + message="application-error-message", + type="application-error-type", + non_retryable=True, ), - ] - - -class CustomError(Exception): - pass - - -@dataclass -class ErrorTestInput: - task_queue: str - name: str + ExpectedApplicationError( + message="custom-error-message", + type="CustomError", + non_retryable=False, + ), + ), +) +RaiseNexusOperationCanceledErrorFromApplicationErrorNonRetryableFromCustomError = ErrorTestCase( + name="RaiseNexusOperationCanceledErrorFromApplicationErrorNonRetryableFromCustomError", + operation_name=ErrorTestService.raise_nexus_operation_canceled_error_from_application_error_non_retryable_from_custom_error.__name__, + expected_exception_chain=ExpectedErrorChain( + ExpectedNexusOperationError( + message="nexus operation completed unsuccessfully", + service="ErrorTestService", + ), + ExpectedCancelledError( + message="operation-error-message", + ), + ExpectedApplicationError( + message="application-error-message", + type="application-error-type", + non_retryable=True, + ), + ExpectedApplicationError( + message="custom-error-message", + type="CustomError", + non_retryable=False, + ), + ), +) -@nexusrpc.handler.service_handler -class ErrorTestService: - @sync_operation - async def op(self, ctx: StartOperationContext, input: ErrorTestInput) -> None: - error_conversion_test_cases[input.name].action_in_nexus_operation() +_EXPECTED_CHAINS: dict[str, ExpectedErrorChain] = { + tc.operation_name: tc.expected_exception_chain + for tc in [ + RaiseApplicationErrorNonRetryable, + RaiseApplicationErrorNonRetryableFromCustomError, + RaiseNexusHandlerErrorNotFound, + RaiseNexusHandlerErrorNotFoundFromCustomError, + RaiseNexusHandlerErrorNotFoundFromHandlerErrorUnavailable, + RaiseNexusOperationErrorFromApplicationErrorNonRetryableFromCustomError, + RaiseNexusOperationCanceledErrorFromApplicationErrorNonRetryableFromCustomError, + ] +} + + +# Caller workflow + + +def _matches_expected(actual: BaseException, expected: ExpectedError) -> bool: + """Check if an actual exception matches the expected error specification.""" + if isinstance(expected, ExpectedNexusOperationError): + if not isinstance(actual, NexusOperationError): + return False + if actual.message != expected.message: + return False + if actual.service != expected.service: + return False + return True + + elif isinstance(expected, ExpectedHandlerError): + if not isinstance(actual, nexusrpc.HandlerError): + return False + if expected.message not in str(actual): + return False + if actual.type.name != expected.type: + return False + if actual.retryable != expected.retryable: + return False + return True + + elif isinstance(expected, ExpectedApplicationError): + if not isinstance(actual, ApplicationError): + return False + if actual.message != expected.message: + return False + if actual.non_retryable != expected.non_retryable: + return False + if expected.type is not None and actual.type != expected.type: + return False + return True + + elif isinstance(expected, ExpectedCancelledError): + if not isinstance(actual, CancelledError): + return False + if actual.message != expected.message: + return False + return True + + return False + + +def _format_mismatch(actual: BaseException, expected: ExpectedError) -> str: + """Format a detailed mismatch message for debugging.""" + lines = ["Mismatch between actual and expected error:"] + lines.append(f" Actual: {type(actual).__name__}: {actual}") + lines.append(f" Expected: {expected}") + return "\n".join(lines) + + +def _validate_exception_chain( + err: BaseException, + expected_chain: ExpectedErrorChain, +) -> None: + """Walk the exception chain and validate each exception against expected. + + Optional expected errors can be skipped if they don't match the current actual error. + """ + actual_chain: list[BaseException] = [] + current: BaseException | None = err + while current is not None: + actual_chain.append(current) + current = current.__cause__ + + actual_idx = 0 + expected_idx = 0 + + while actual_idx < len(actual_chain) and expected_idx < len(expected_chain): + actual = actual_chain[actual_idx] + expected = expected_chain[expected_idx] + + if _matches_expected(actual, expected): + # Match found, advance both + actual_idx += 1 + expected_idx += 1 + elif expected.optional: + # Optional expected error didn't match, skip it + print(f"Skipping optional expected error: {expected}") + expected_idx += 1 + else: + # Required expected error didn't match + assert False, _format_mismatch(actual, expected) + + # Check remaining expected errors are all optional + while expected_idx < len(expected_chain): + expected = expected_chain[expected_idx] + assert expected.optional, ( + f"Required expected error not found in chain: {expected}" + ) + expected_idx += 1 -# Caller + # Check no remaining actual errors + assert actual_idx == len(actual_chain), ( + f"Unexpected errors in chain: {actual_chain[actual_idx:]}" + ) @workflow.defn(sandboxed=False) @@ -369,38 +599,35 @@ def __init__(self, input: ErrorTestInput): ) @workflow.run - async def invoke_nexus_op_and_assert_error(self, input: ErrorTestInput) -> None: + async def run(self, input: ErrorTestInput) -> None: try: await self.nexus_client.execute_operation( - ErrorTestService.op, # type: ignore[arg-type] # mypy can't infer OutputT=None in Union type + input.operation_name, input, output_type=None, ) except BaseException as err: - errs = [err] - while err.__cause__: - errs.append(err.__cause__) - err = err.__cause__ - - test_case = error_conversion_test_cases[input.name] - assert len(errs) == len(test_case.expected_exception_chain_in_workflow) - for err, (expected_cls, expected_fields) in zip( - errs, test_case.expected_exception_chain_in_workflow - ): - assert isinstance(err, expected_cls) - for k, v in expected_fields.items(): - if k == "message" and isinstance(err, nexusrpc.HandlerError): - assert str(err) == v - else: - assert getattr(err, k) == v - - else: - assert False, "Unreachable" - - -@pytest.mark.parametrize("test_case", list(error_conversion_test_cases.values())) + _validate_exception_chain(err, _EXPECTED_CHAINS[input.operation_name]) + return + + raise AssertionError("Expected exception was not raised") + + +@pytest.mark.parametrize( + "test_case", + [ + RaiseApplicationErrorNonRetryable, + RaiseApplicationErrorNonRetryableFromCustomError, + RaiseNexusHandlerErrorNotFound, + RaiseNexusHandlerErrorNotFoundFromCustomError, + RaiseNexusHandlerErrorNotFoundFromHandlerErrorUnavailable, + RaiseNexusOperationErrorFromApplicationErrorNonRetryableFromCustomError, + RaiseNexusOperationCanceledErrorFromApplicationErrorNonRetryableFromCustomError, + ], + ids=lambda tc: tc.name, +) async def test_errors_raised_by_nexus_operation( - client: Client, env: WorkflowEnvironment, test_case: type[ErrorConversionTestCase] + client: Client, env: WorkflowEnvironment, test_case: ErrorTestCase ): if env.supports_time_skipping: pytest.skip("Nexus tests don't work with time-skipping server") @@ -412,12 +639,14 @@ async def test_errors_raised_by_nexus_operation( workflows=[ErrorTestCallerWorkflow], task_queue=task_queue, ): - await create_nexus_endpoint(task_queue, client) + await env.create_nexus_endpoint( + make_nexus_endpoint_name(task_queue), task_queue + ) await client.execute_workflow( - ErrorTestCallerWorkflow.invoke_nexus_op_and_assert_error, + ErrorTestCallerWorkflow.run, ErrorTestInput( task_queue=task_queue, - name=test_case.__name__, + operation_name=test_case.operation_name, ), id=str(uuid.uuid4()), task_queue=task_queue, diff --git a/tests/nexus/test_workflow_caller_errors.py b/tests/nexus/test_workflow_caller_errors.py index 2bff390da..0f1b6a789 100644 --- a/tests/nexus/test_workflow_caller_errors.py +++ b/tests/nexus/test_workflow_caller_errors.py @@ -4,40 +4,52 @@ import concurrent.futures import uuid from collections import Counter +from collections.abc import Sequence from dataclasses import dataclass from datetime import timedelta +from logging import getLogger +from typing import Any import nexusrpc import nexusrpc.handler import pytest from nexusrpc.handler import ( CancelOperationContext, - FetchOperationInfoContext, - FetchOperationResultContext, OperationHandler, StartOperationContext, StartOperationResultAsync, + operation_handler, service_handler, sync_operation, ) +import temporalio.api.common.v1 from temporalio import nexus, workflow from temporalio.client import ( Client, WorkflowFailureError, ) +from temporalio.converter import DataConverter, DefaultPayloadConverter, PayloadCodec from temporalio.exceptions import ( ApplicationError, NexusOperationError, TimeoutError, + TimeoutType, ) +from temporalio.service import RPCError, RPCStatusCode from temporalio.testing import WorkflowEnvironment from temporalio.worker import Worker -from tests.helpers import assert_eq_eventually -from tests.helpers.nexus import create_nexus_endpoint, make_nexus_endpoint_name +from tests.helpers import LogCapturer, assert_eq_eventually +from tests.helpers.nexus import make_nexus_endpoint_name + +# Cloud CI's namespace credentials cannot manage Nexus endpoints. +# See https://github.com/temporalio/sdk-python/issues/1704. +pytestmark = pytest.mark.requires_local_server operation_invocation_counts = Counter[str]() +logger = getLogger(__name__) + @dataclass class ErrorTestInput: @@ -47,6 +59,13 @@ class ErrorTestInput: id: str +@dataclass +class RPCErrorInput: + status_code_value: int # RPCStatusCode int value + task_queue: str + id: str + + @workflow.defn class NonTerminatingWorkflow: @workflow.run @@ -58,14 +77,14 @@ async def run(self) -> None: class ErrorTestService: @nexusrpc.handler.sync_operation def retried_due_to_exception( - self, ctx: nexusrpc.handler.StartOperationContext, input: ErrorTestInput + self, _ctx: nexusrpc.handler.StartOperationContext, input: ErrorTestInput ) -> None: operation_invocation_counts[input.id] += 1 raise Exception @nexusrpc.handler.sync_operation def retried_due_to_retryable_application_error( - self, ctx: nexusrpc.handler.StartOperationContext, input: ErrorTestInput + self, _ctx: nexusrpc.handler.StartOperationContext, input: ErrorTestInput ) -> None: operation_invocation_counts[input.id] += 1 raise ApplicationError( @@ -76,7 +95,7 @@ def retried_due_to_retryable_application_error( @nexusrpc.handler.sync_operation def retried_due_to_resource_exhausted_handler_error( - self, ctx: nexusrpc.handler.StartOperationContext, input: ErrorTestInput + self, _ctx: nexusrpc.handler.StartOperationContext, input: ErrorTestInput ) -> None: operation_invocation_counts[input.id] += 1 raise nexusrpc.HandlerError( @@ -86,7 +105,7 @@ def retried_due_to_resource_exhausted_handler_error( @nexusrpc.handler.sync_operation def retried_due_to_internal_handler_error( - self, ctx: nexusrpc.handler.StartOperationContext, input: ErrorTestInput + self, _ctx: nexusrpc.handler.StartOperationContext, input: ErrorTestInput ) -> None: operation_invocation_counts[input.id] += 1 raise nexusrpc.HandlerError( @@ -96,7 +115,7 @@ def retried_due_to_internal_handler_error( @nexusrpc.handler.sync_operation async def fails_due_to_workflow_already_started( - self, ctx: nexusrpc.handler.StartOperationContext, input: ErrorTestInput + self, _ctx: nexusrpc.handler.StartOperationContext, input: ErrorTestInput ) -> None: operation_invocation_counts[input.id] += 1 for _ in range(2): @@ -106,6 +125,18 @@ async def fails_due_to_workflow_already_started( task_queue=nexus.info().task_queue, ) + @nexusrpc.handler.sync_operation + def raise_rpc_error( + self, _ctx: nexusrpc.handler.StartOperationContext, input: RPCErrorInput + ) -> None: + operation_invocation_counts[input.id] += 1 + status_code = RPCStatusCode(input.status_code_value) + raise RPCError( + f"Test error for {status_code.name}", + status_code, + b"", + ) + @workflow.defn(sandboxed=False) class CallerWorkflow: @@ -118,6 +149,20 @@ async def run(self, input: ErrorTestInput) -> None: await nexus_client.execute_operation(input.operation_name, input) +@workflow.defn(sandboxed=False) +class RPCErrorCallerWorkflow: + @workflow.run + async def run(self, input: RPCErrorInput) -> None: + nexus_client = workflow.create_nexus_client( + service="ErrorTestService", + endpoint=make_nexus_endpoint_name(input.task_queue), + ) + await nexus_client.execute_operation( + ErrorTestService.raise_rpc_error, + input, + ) + + @pytest.mark.parametrize( "operation_name", [ @@ -146,7 +191,9 @@ async def test_nexus_operation_is_retried( workflows=[CallerWorkflow], task_queue=input.task_queue, ): - await create_nexus_endpoint(input.task_queue, client) + await env.create_nexus_endpoint( + make_nexus_endpoint_name(input.task_queue), input.task_queue + ) asyncio.create_task( client.execute_workflow( CallerWorkflow.run, @@ -209,7 +256,9 @@ async def test_nexus_operation_fails_without_retry_as_handler_error( workflows=[CallerWorkflow], task_queue=input.task_queue, ): - await create_nexus_endpoint(input.task_queue, client) + await env.create_nexus_endpoint( + make_nexus_endpoint_name(input.task_queue), input.task_queue + ) try: await client.execute_workflow( CallerWorkflow.run, @@ -233,28 +282,37 @@ async def test_nexus_operation_fails_without_retry_as_handler_error( @service_handler class StartTimeoutTestService: @sync_operation - async def op_handler_that_never_returns( - self, ctx: StartOperationContext, input: None + async def expect_timeout_cancellation_async( + self, ctx: StartOperationContext, _input: None + ) -> None: + try: + await asyncio.wait_for(ctx.task_cancellation.wait_until_cancelled(), 3) + except asyncio.TimeoutError: + raise ApplicationError("expected cancel", non_retryable=True) + + @sync_operation + def expect_timeout_cancellation_sync( + self, ctx: StartOperationContext, _input: None ) -> None: - await asyncio.Future() + ctx.task_cancellation.wait_until_cancelled_sync(5) @workflow.defn class StartTimeoutTestCallerWorkflow: @workflow.init - def __init__(self): + def __init__(self, operation: str): self.nexus_client = workflow.create_nexus_client( service=StartTimeoutTestService, endpoint=make_nexus_endpoint_name(workflow.info().task_queue), ) @workflow.run - async def run(self) -> None: + async def run(self, operation: str) -> None: await self.nexus_client.execute_operation( - StartTimeoutTestService.op_handler_that_never_returns, # type: ignore[arg-type] # mypy can't infer OutputT=None in Union type + operation, None, output_type=None, - schedule_to_close_timeout=timedelta(seconds=0.1), + schedule_to_close_timeout=timedelta(seconds=2), ) @@ -270,11 +328,15 @@ async def test_error_raised_by_timeout_of_nexus_start_operation( nexus_service_handlers=[StartTimeoutTestService()], workflows=[StartTimeoutTestCallerWorkflow], task_queue=task_queue, + nexus_task_executor=concurrent.futures.ThreadPoolExecutor(), ): - await create_nexus_endpoint(task_queue, client) + await env.create_nexus_endpoint( + make_nexus_endpoint_name(task_queue), task_queue + ) try: await client.execute_workflow( StartTimeoutTestCallerWorkflow.run, + "expect_timeout_cancellation_async", id=str(uuid.uuid4()), task_queue=task_queue, ) @@ -285,35 +347,192 @@ async def test_error_raised_by_timeout_of_nexus_start_operation( else: pytest.fail("Expected exception due to timeout of nexus start operation") + with LogCapturer().logs_captured(logger) as capturer: + try: + await client.execute_workflow( + StartTimeoutTestCallerWorkflow.run, + "expect_timeout_cancellation_sync", + id=str(uuid.uuid4()), + task_queue=task_queue, + ) + except Exception as err: + assert isinstance(err, WorkflowFailureError) + assert isinstance(err.__cause__, NexusOperationError) + assert isinstance(err.__cause__.__cause__, TimeoutError) + else: + pytest.fail( + "Expected exception due to timeout of nexus start operation" + ) + assert capturer.find_log("unexpected cancellation reason") is None + + +# Schedule to start timeout test +@service_handler +class ScheduleToStartTimeoutTestService: + @sync_operation + async def expect_schedule_to_start_timeout( + self, ctx: StartOperationContext, _input: None + ) -> None: + try: + await asyncio.wait_for(ctx.task_cancellation.wait_until_cancelled(), 1) + except asyncio.TimeoutError: + raise ApplicationError("expected cancel", non_retryable=True) + -# Cancellation timeout test +@workflow.defn +class ScheduleToStartTimeoutTestCallerWorkflow: + @workflow.init + def __init__(self): + self.nexus_client = workflow.create_nexus_client( + service=ScheduleToStartTimeoutTestService, + endpoint=make_nexus_endpoint_name(workflow.info().task_queue), + ) + + @workflow.run + async def run(self) -> None: + await self.nexus_client.execute_operation( + ScheduleToStartTimeoutTestService.expect_schedule_to_start_timeout, + None, + output_type=None, + schedule_to_start_timeout=timedelta(seconds=0.1), + ) -class OperationWithCancelMethodThatNeverReturns(OperationHandler[None, None]): +async def test_error_raised_by_schedule_to_start_timeout_of_nexus_operation( + client: Client, env: WorkflowEnvironment +): + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work with time-skipping server") + + task_queue = str(uuid.uuid4()) + async with Worker( + client, + nexus_service_handlers=[ScheduleToStartTimeoutTestService()], + workflows=[ScheduleToStartTimeoutTestCallerWorkflow], + task_queue=task_queue, + nexus_task_executor=concurrent.futures.ThreadPoolExecutor(), + ): + await env.create_nexus_endpoint( + make_nexus_endpoint_name(task_queue), task_queue + ) + try: + await client.execute_workflow( + ScheduleToStartTimeoutTestCallerWorkflow.run, + id=str(uuid.uuid4()), + task_queue=task_queue, + ) + except Exception as err: + assert isinstance(err, WorkflowFailureError) + assert isinstance(err.__cause__, NexusOperationError) + assert isinstance(err.__cause__.__cause__, TimeoutError) + timeout_err = err.__cause__.__cause__ + assert timeout_err.type == TimeoutType.SCHEDULE_TO_START + else: + pytest.fail( + "Expected exception due to schedule to start timeout of nexus operation" + ) + + +# Start to close timeout test + + +class OperationThatExpectsStartToCloseTimeoutAsync(OperationHandler[None, None]): async def start( self, ctx: StartOperationContext, input: None ) -> StartOperationResultAsync: return StartOperationResultAsync("fake-token") async def cancel(self, ctx: CancelOperationContext, token: str) -> None: - await asyncio.Future() + pass + + +@service_handler +class StartToCloseTimeoutTestService: + @operation_handler + def expect_start_to_close_timeout(self) -> OperationHandler[None, None]: + return OperationThatExpectsStartToCloseTimeoutAsync() + - async def fetch_info( - self, ctx: FetchOperationInfoContext, token: str - ) -> nexusrpc.OperationInfo: - raise NotImplementedError("Not implemented") +@workflow.defn +class StartToCloseTimeoutTestCallerWorkflow: + @workflow.init + def __init__( + self, + ): + self.nexus_client = workflow.create_nexus_client( + service=StartToCloseTimeoutTestService, + endpoint=make_nexus_endpoint_name(workflow.info().task_queue), + ) + + @workflow.run + async def run(self) -> None: + op_handle = await self.nexus_client.start_operation( + StartToCloseTimeoutTestService.expect_start_to_close_timeout, + None, + start_to_close_timeout=timedelta(seconds=0.1), + ) + await op_handle - async def fetch_result(self, ctx: FetchOperationResultContext, token: str) -> None: - raise NotImplementedError("Not implemented") + +async def test_error_raised_by_start_to_close_timeout_of_nexus_operation( + client: Client, env: WorkflowEnvironment +): + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work with time-skipping server") + + task_queue = str(uuid.uuid4()) + async with Worker( + client, + nexus_service_handlers=[StartToCloseTimeoutTestService()], + workflows=[StartToCloseTimeoutTestCallerWorkflow], + task_queue=task_queue, + nexus_task_executor=concurrent.futures.ThreadPoolExecutor(), + ): + await env.create_nexus_endpoint( + make_nexus_endpoint_name(task_queue), task_queue + ) + try: + await client.execute_workflow( + StartToCloseTimeoutTestCallerWorkflow.run, + id=str(uuid.uuid4()), + task_queue=task_queue, + ) + except Exception as err: + assert isinstance(err, WorkflowFailureError) + assert isinstance(err.__cause__, NexusOperationError) + timeout_err = err.__cause__.__cause__ + assert isinstance(timeout_err, TimeoutError) + assert timeout_err.type == TimeoutType.START_TO_CLOSE + else: + pytest.fail( + "Expected exception due to start to close timeout of nexus operation" + ) + + +# Cancellation timeout test + + +class OperationWithCancelMethodThatExpectsCancel(OperationHandler[None, None]): + async def start( + self, ctx: StartOperationContext, input: None + ) -> StartOperationResultAsync: + return StartOperationResultAsync("fake-token") + + async def cancel(self, ctx: CancelOperationContext, token: str) -> None: + try: + await asyncio.wait_for(ctx.task_cancellation.wait_until_cancelled(), 1) + except asyncio.TimeoutError: + logger.error("expected cancellation") + raise ApplicationError("expected cancellation", non_retryable=True) @service_handler class CancellationTimeoutTestService: - @nexusrpc.handler._decorators.operation_handler - def op_with_cancel_method_that_never_returns( + @operation_handler + def op_with_cancel_method_that_expects_cancel( self, ) -> OperationHandler[None, None]: - return OperationWithCancelMethodThatNeverReturns() + return OperationWithCancelMethodThatExpectsCancel() @workflow.defn @@ -327,13 +546,8 @@ def __init__(self): @workflow.run async def run(self) -> None: - # TODO(nexus-prerelease) op_handle = await self.nexus_client.start_operation( - # Although the tests are making use of it, we are not exposing operation - # factory methods to users as a way to write nexus operations, and so the - # types on NexusClient start_operation/execute_operation do not currently - # permit it. - CancellationTimeoutTestService.op_with_cancel_method_that_never_returns, # type: ignore + CancellationTimeoutTestService.op_with_cancel_method_that_expects_cancel, None, schedule_to_close_timeout=timedelta(seconds=0.1), ) @@ -347,7 +561,6 @@ async def test_error_raised_by_timeout_of_nexus_cancel_operation( if env.supports_time_skipping: pytest.skip("Nexus tests don't work with time-skipping server") - pytest.skip("TODO(nexus-prerelease): finish writing this test") task_queue = str(uuid.uuid4()) async with Worker( client, @@ -355,16 +568,276 @@ async def test_error_raised_by_timeout_of_nexus_cancel_operation( workflows=[CancellationTimeoutTestCallerWorkflow], task_queue=task_queue, ): - await create_nexus_endpoint(task_queue, client) + with LogCapturer().logs_captured(logger) as capturer: + await env.create_nexus_endpoint( + make_nexus_endpoint_name(task_queue), task_queue + ) + try: + await client.execute_workflow( + CancellationTimeoutTestCallerWorkflow.run, + id=str(uuid.uuid4()), + task_queue=task_queue, + ) + except Exception as err: + assert isinstance(err, WorkflowFailureError) + assert isinstance(err.__cause__, NexusOperationError) + assert isinstance(err.__cause__.__cause__, TimeoutError) + else: + pytest.fail( + "Expected exception due to timeout of nexus cancel operation" + ) + + assert capturer.find_log("expected cancellation") is None + + +# RPCError tests + + +@pytest.mark.parametrize( + ["status_code", "expected_handler_error_type"], + [ + (RPCStatusCode.INVALID_ARGUMENT, nexusrpc.HandlerErrorType.BAD_REQUEST), + (RPCStatusCode.ALREADY_EXISTS, nexusrpc.HandlerErrorType.INTERNAL), + (RPCStatusCode.FAILED_PRECONDITION, nexusrpc.HandlerErrorType.INTERNAL), + (RPCStatusCode.OUT_OF_RANGE, nexusrpc.HandlerErrorType.INTERNAL), + (RPCStatusCode.NOT_FOUND, nexusrpc.HandlerErrorType.NOT_FOUND), + (RPCStatusCode.UNIMPLEMENTED, nexusrpc.HandlerErrorType.NOT_IMPLEMENTED), + ], +) +async def test_rpc_error_fails_without_retry( + client: Client, + env: WorkflowEnvironment, + status_code: RPCStatusCode, + expected_handler_error_type: nexusrpc.HandlerErrorType, +): + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work with time-skipping server") + + input = RPCErrorInput( + status_code_value=status_code.value, + task_queue=str(uuid.uuid4()), + id=str(uuid.uuid4()), + ) + async with Worker( + client, + nexus_service_handlers=[ErrorTestService()], + nexus_task_executor=concurrent.futures.ThreadPoolExecutor(max_workers=1), + workflows=[RPCErrorCallerWorkflow], + task_queue=input.task_queue, + ): + await env.create_nexus_endpoint( + make_nexus_endpoint_name(input.task_queue), input.task_queue + ) try: await client.execute_workflow( - CancellationTimeoutTestCallerWorkflow.run, + RPCErrorCallerWorkflow.run, + input, + id=str(uuid.uuid4()), + task_queue=input.task_queue, + ) + except Exception as err: + assert isinstance(err, WorkflowFailureError) + assert isinstance(err.__cause__, NexusOperationError) + handler_error = err.__cause__.__cause__ + assert isinstance(handler_error, nexusrpc.HandlerError) + assert not handler_error.retryable + assert handler_error.type == expected_handler_error_type + # Verify no retry occurred + assert operation_invocation_counts[input.id] == 1 + else: + pytest.fail("Expected WorkflowFailureError") + + +@pytest.mark.parametrize( + "status_code", + [ + RPCStatusCode.ABORTED, + RPCStatusCode.UNAVAILABLE, + RPCStatusCode.CANCELLED, + RPCStatusCode.DATA_LOSS, + RPCStatusCode.INTERNAL, + RPCStatusCode.UNKNOWN, + RPCStatusCode.UNAUTHENTICATED, + RPCStatusCode.PERMISSION_DENIED, + RPCStatusCode.RESOURCE_EXHAUSTED, + RPCStatusCode.DEADLINE_EXCEEDED, + RPCStatusCode.OK, # fallback case + ], +) +async def test_rpc_error_is_retried( + client: Client, + env: WorkflowEnvironment, + status_code: RPCStatusCode, +): + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work with time-skipping server") + + input = RPCErrorInput( + status_code_value=status_code.value, + task_queue=str(uuid.uuid4()), + id=str(uuid.uuid4()), + ) + async with Worker( + client, + nexus_service_handlers=[ErrorTestService()], + nexus_task_executor=concurrent.futures.ThreadPoolExecutor(max_workers=1), + workflows=[RPCErrorCallerWorkflow], + task_queue=input.task_queue, + ): + await env.create_nexus_endpoint( + make_nexus_endpoint_name(input.task_queue), input.task_queue + ) + + handle = await client.start_workflow( + RPCErrorCallerWorkflow.run, + input, + id=str(uuid.uuid4()), + task_queue=input.task_queue, + ) + + async def times_called() -> int: + return operation_invocation_counts[input.id] + + await assert_eq_eventually(2, times_called) + + await handle.cancel() + + +# DataConverter codec/converter error tests + + +@nexusrpc.handler.service_handler +class DataConverterTestService: + @nexusrpc.handler.sync_operation + def succeed( + self, _ctx: nexusrpc.handler.StartOperationContext, _input: ErrorTestInput + ) -> None: + pass + + +class FailOnFirstDecodeCodec(PayloadCodec): + def __init__(self) -> None: + self.decode_count = 0 + + async def encode( + self, payloads: Sequence[temporalio.api.common.v1.Payload] + ) -> list[temporalio.api.common.v1.Payload]: + return list(payloads) + + async def decode( + self, payloads: Sequence[temporalio.api.common.v1.Payload] + ) -> list[temporalio.api.common.v1.Payload]: + self.decode_count += 1 + if self.decode_count == 1: + raise RuntimeError("Intentional codec decode failure") + return list(payloads) + + +class FailingFromPayloadsConverter(DefaultPayloadConverter): + def from_payloads( + self, + payloads: Sequence[temporalio.api.common.v1.Payload], + type_hints: list[type] | None = None, + ) -> list[Any]: + raise RuntimeError("Intentional payload converter failure") + + +async def test_nexus_operation_retried_on_codec_decode_failure( + client: Client, env: WorkflowEnvironment +): + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work with time-skipping server") + + task_queue = str(uuid.uuid4()) + codec = FailOnFirstDecodeCodec() + handler_client = Client( + client.service_client, + namespace=client.namespace, + data_converter=DataConverter(payload_codec=codec), + ) + input = ErrorTestInput( + service_name="DataConverterTestService", + operation_name="succeed", + task_queue=task_queue, + id=str(uuid.uuid4()), + ) + async with ( + Worker( + client, + workflows=[CallerWorkflow], + task_queue=task_queue, + ), + Worker( + handler_client, + nexus_service_handlers=[DataConverterTestService()], + nexus_task_executor=concurrent.futures.ThreadPoolExecutor(max_workers=1), + task_queue=task_queue, + ), + ): + await env.create_nexus_endpoint( + make_nexus_endpoint_name(input.task_queue), input.task_queue + ) + await client.execute_workflow( + CallerWorkflow.run, + input, + id=str(uuid.uuid4()), + task_queue=task_queue, + ) + assert codec.decode_count == 2 + + +async def test_nexus_operation_fails_without_retry_on_converter_failure( + client: Client, env: WorkflowEnvironment +): + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work with time-skipping server") + + task_queue = str(uuid.uuid4()) + handler_client = Client( + client.service_client, + namespace=client.namespace, + data_converter=DataConverter( + payload_converter_class=FailingFromPayloadsConverter + ), + ) + input = ErrorTestInput( + service_name="DataConverterTestService", + operation_name="succeed", + task_queue=task_queue, + id=str(uuid.uuid4()), + ) + async with ( + Worker( + client, + workflows=[CallerWorkflow], + task_queue=task_queue, + ), + Worker( + handler_client, + nexus_service_handlers=[DataConverterTestService()], + nexus_task_executor=concurrent.futures.ThreadPoolExecutor(max_workers=1), + task_queue=task_queue, + ), + ): + await env.create_nexus_endpoint( + make_nexus_endpoint_name(input.task_queue), input.task_queue + ) + try: + await client.execute_workflow( + CallerWorkflow.run, + input, id=str(uuid.uuid4()), task_queue=task_queue, ) except Exception as err: assert isinstance(err, WorkflowFailureError) assert isinstance(err.__cause__, NexusOperationError) - assert isinstance(err.__cause__.__cause__, TimeoutError) + handler_error = err.__cause__.__cause__ + assert isinstance(handler_error, nexusrpc.HandlerError) + assert handler_error.type == nexusrpc.HandlerErrorType.BAD_REQUEST + assert not handler_error.retryable + assert "Payload converter failed to decode Nexus operation input" in str( + handler_error + ) else: - pytest.fail("Expected exception due to timeout of nexus cancel operation") + pytest.fail("Expected WorkflowFailureError") diff --git a/tests/nexus/test_workflow_run_operation.py b/tests/nexus/test_workflow_run_operation.py index 01850379a..7135fde71 100644 --- a/tests/nexus/test_workflow_run_operation.py +++ b/tests/nexus/test_workflow_run_operation.py @@ -1,7 +1,7 @@ -import re import uuid from dataclasses import dataclass -from typing import Any, Type +from datetime import datetime, timezone +from typing import Any import nexusrpc import pytest @@ -14,17 +14,18 @@ ) from nexusrpc.handler._decorators import operation_handler -from temporalio import workflow -from temporalio.nexus import WorkflowRunOperationContext +from temporalio import nexus, workflow +from temporalio.client import Client +from temporalio.nexus import WorkflowRunOperationContext, workflow_run_operation from temporalio.nexus._operation_handlers import WorkflowRunOperationHandler +from temporalio.nexus._token import OperationToken, OperationTokenType from temporalio.testing import WorkflowEnvironment from temporalio.worker import Worker -from tests.helpers.nexus import ( - Failure, - ServiceClient, - create_nexus_endpoint, - dataclass_as_dict, -) +from tests.helpers.nexus import make_nexus_endpoint_name + +# Cloud CI's namespace credentials cannot manage Nexus endpoints. +# See https://github.com/temporalio/sdk-python/issues/1704. +pytestmark = pytest.mark.requires_local_server @dataclass @@ -52,7 +53,7 @@ async def start( handle = await tctx.start_workflow( EchoWorkflow.run, input.value, - id=str(uuid.uuid4()), + id=input.value, ) return StartOperationResultAsync(handle.to_token()) @@ -64,6 +65,42 @@ def op(self) -> OperationHandler[Input, str]: return MyOperation() +@service +class RequestDeadlineService: + op: Operation[Input, str] + + +@service_handler(service=RequestDeadlineService) +class RequestDeadlineHandler: + def __init__(self) -> None: + self.start_deadlines_received: list[datetime | None] = [] + + @workflow_run_operation + async def op( + self, ctx: WorkflowRunOperationContext, input: Input + ) -> nexus.WorkflowHandle[str]: + self.start_deadlines_received.append(ctx.request_deadline) + return await ctx.start_workflow( + EchoWorkflow.run, + input.value, + id=input.value, + ) + + +@workflow.defn +class RequestDeadlineWorkflow: + @workflow.run + async def run(self, input: Input, task_queue: str) -> str: + client = workflow.create_nexus_client( + service=RequestDeadlineService, + endpoint=make_nexus_endpoint_name(task_queue), + ) + return await client.execute_operation( + RequestDeadlineService.op, + input, + ) + + @service class Service: op: Operation[Input, str] @@ -79,6 +116,17 @@ def op(self) -> OperationHandler: return MyOperation() +@workflow.defn +class CallerWorkflow: + @workflow.run + async def run(self, input: Input, service_name: str, task_queue: str) -> str: + client = workflow.create_nexus_client( + service=service_name, + endpoint=make_nexus_endpoint_name(task_queue), + ) + return await client.execute_operation("op", input, output_type=str) + + @pytest.mark.parametrize( "service_handler_cls", [ @@ -87,33 +135,101 @@ def op(self) -> OperationHandler: ], ) async def test_workflow_run_operation( + client: Client, env: WorkflowEnvironment, - service_handler_cls: Type[Any], + service_handler_cls: type[Any], ): if env.supports_time_skipping: pytest.skip("Nexus tests don't work with time-skipping server") task_queue = str(uuid.uuid4()) - endpoint = (await create_nexus_endpoint(task_queue, env.client)).endpoint.id + await env.create_nexus_endpoint(make_nexus_endpoint_name(task_queue), task_queue) assert (service_defn := nexusrpc.get_service_definition(service_handler_cls)) - service_client = ServiceClient( - server_address=ServiceClient.default_server_address(env), - endpoint=endpoint, - service=service_defn.name, - ) async with Worker( - env.client, + client, task_queue=task_queue, nexus_service_handlers=[service_handler_cls()], + workflows=[CallerWorkflow, EchoWorkflow], ): - resp = await service_client.start_operation( - "op", - dataclass_as_dict(Input(value="test")), + input_value = str(uuid.uuid4()) + result = await client.execute_workflow( + CallerWorkflow.run, + args=[Input(value=input_value), service_defn.name, task_queue], + id=str(uuid.uuid4()), + task_queue=task_queue, ) - if hasattr(service_handler_cls, "__expected__error__"): - status_code, message = service_handler_cls.__expected__error__ - assert resp.status_code == status_code - failure = Failure(**resp.json()) - assert re.search(message, failure.message) - else: - assert resp.status_code == 201 + assert result == input_value + + +async def test_request_deadline_is_accessible_in_workflow_run_operation( + client: Client, + env: WorkflowEnvironment, +): + """Test that request_deadline is accessible in WorkflowRunOperationContext.""" + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work with time-skipping server") + + task_queue = str(uuid.uuid4()) + endpoint_name = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint_name, task_queue) + service_handler = RequestDeadlineHandler() + async with Worker( + env.client, + task_queue=task_queue, + nexus_service_handlers=[service_handler], + workflows=[RequestDeadlineWorkflow, EchoWorkflow], + ): + input_value = str(uuid.uuid4()) + await client.execute_workflow( + RequestDeadlineWorkflow.run, + args=[Input(value=input_value), task_queue], + task_queue=task_queue, + id=str(uuid.uuid4()), + ) + + assert len(service_handler.start_deadlines_received) == 1 + deadline = service_handler.start_deadlines_received[0] + assert deadline is not None, ( + "request_deadline should be set in WorkflowRunOperationContext" + ) + assert deadline.tzinfo is timezone.utc, "request_deadline should be in utc" + + +async def test_workflow_run_operation_includes_token_in_callback( + client: Client, + env: WorkflowEnvironment, +): + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work with time-skipping server") + + task_queue = str(uuid.uuid4()) + await env.create_nexus_endpoint(make_nexus_endpoint_name(task_queue), task_queue) + async with Worker( + client, + task_queue=task_queue, + nexus_service_handlers=[SubclassingHappyPath()], + workflows=[CallerWorkflow, EchoWorkflow], + ): + input_value = str(uuid.uuid4()) + result = await client.execute_workflow( + CallerWorkflow.run, + args=[Input(value=input_value), "SubclassingHappyPath", task_queue], + id=str(uuid.uuid4()), + task_queue=task_queue, + ) + assert result == input_value + + target_handle = client.get_workflow_handle(input_value) + + desc = await target_handle.describe() + token = desc.raw_description.callbacks[0].callback.nexus.header[ + "nexus-operation-token" + ] + + expected_token = OperationToken( + type=OperationTokenType.WORKFLOW, + namespace=client.namespace, + workflow_id=target_handle.id, + ).encode() + + assert token == expected_token diff --git a/tests/test_activity.py b/tests/test_activity.py new file mode 100644 index 000000000..6a6d14206 --- /dev/null +++ b/tests/test_activity.py @@ -0,0 +1,1349 @@ +import asyncio +import uuid +from dataclasses import dataclass +from datetime import timedelta + +import pytest + +from temporalio import activity, workflow +from temporalio.client import ( + ActivityExecutionCount, + ActivityExecutionCountAggregationGroup, + ActivityExecutionDescription, + ActivityExecutionStatus, + ActivityFailureError, + ActivityHandle, + CancelActivityInput, + Client, + CountActivitiesInput, + DescribeActivityInput, + Interceptor, + ListActivitiesInput, + OutboundInterceptor, + PendingActivityState, + StartActivityInput, + TerminateActivityInput, +) +from temporalio.exceptions import ApplicationError, CancelledError +from temporalio.service import RPCError, RPCStatusCode +from temporalio.testing import WorkflowEnvironment +from temporalio.worker import Worker +from tests.helpers import assert_eq_eventually, assert_eventually + + +@activity.defn +async def increment(input: int) -> int: + return input + 1 + + +# Activity classes for testing start_activity_class / execute_activity_class +@activity.defn +class IncrementClass: + """Async callable class activity with a parameter.""" + + async def __call__(self, x: int) -> int: + return x + 1 + + +@activity.defn +class NoParamClass: + """Async callable class activity with no parameters.""" + + async def __call__(self) -> str: + return "no-param-result" + + +@activity.defn +class SyncIncrementClass: + """Sync callable class activity with a parameter.""" + + def __call__(self, x: int) -> int: + return x + 1 + + +# Activity holder for testing start_activity_method / execute_activity_method +class ActivityHolder: + """Class holding activity methods.""" + + @activity.defn + async def async_increment(self, x: int) -> int: + return x + 1 + + @activity.defn + async def async_no_param(self) -> str: + return "async-method-result" + + @activity.defn + def sync_increment(self, x: int) -> int: + return x + 1 + + +class TestDescribe: + @pytest.fixture + async def activity_handle(self, client: Client, env: WorkflowEnvironment): + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + + id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + yield await client.start_activity( + increment, + args=(42,), + id=id, + task_queue=task_queue, + schedule_to_close_timeout=timedelta(hours=1), + ) + + async def test_describe(self, client: Client, activity_handle: ActivityHandle): + desc = await activity_handle.describe() + # From ActivityExecution (base class) + assert desc.activity_id == activity_handle.id + assert desc.activity_run_id == activity_handle.run_id + assert desc.activity_type == "increment" + assert desc.close_time is None # not closed yet + assert desc.execution_duration is None # not closed yet + assert desc.namespace == client.namespace + assert desc.raw_info is not None + assert desc.scheduled_time is not None + assert len(desc.typed_search_attributes) == 0 + assert desc.state_transition_count is not None + assert desc.status == ActivityExecutionStatus.RUNNING + assert desc.task_queue + # From ActivityExecutionDescription + assert desc.attempt == 1 + assert desc.canceled_reason is None + assert desc.current_retry_interval is None + assert desc.eager_execution_requested is False + assert desc.expiration_time is not None + assert len(desc.raw_heartbeat_details) == 0 + assert desc.run_state == PendingActivityState.SCHEDULED + assert desc.last_attempt_complete_time is None + assert desc.last_failure is None + assert desc.last_heartbeat_time is None + assert desc.last_started_time is None + assert desc.last_worker_identity == "" + assert desc.long_poll_token is not None + assert desc.next_attempt_schedule_time is None + assert desc.paused is False + assert desc.retry_policy is not None + + async def test_describe_long_poll(self, activity_handle: ActivityHandle): + desc1 = await activity_handle.describe() + assert desc1.long_poll_token + desc2_task = asyncio.create_task( + activity_handle.describe(long_poll_token=desc1.long_poll_token) + ) + # Worker poll causes a transition to Started which notifies the waiting long-poll. + async with Worker( + activity_handle._client, + task_queue=desc1.task_queue, + activities=[increment], + ): + desc2 = await desc2_task + assert desc2.state_transition_count and desc1.state_transition_count + assert desc2.state_transition_count > desc1.state_transition_count + + +class ActivityTracingInterceptor(Interceptor): + """Test interceptor that tracks all activity interceptor calls.""" + + def __init__(self) -> None: + super().__init__() + self.start_activity_calls: list[StartActivityInput] = [] + self.describe_activity_calls: list[DescribeActivityInput] = [] + self.cancel_activity_calls: list[CancelActivityInput] = [] + self.terminate_activity_calls: list[TerminateActivityInput] = [] + self.list_activities_calls: list[ListActivitiesInput] = [] + self.count_activities_calls: list[CountActivitiesInput] = [] + + def intercept_client(self, next: OutboundInterceptor) -> OutboundInterceptor: + return ActivityTracingOutboundInterceptor(self, next) + + +class ActivityTracingOutboundInterceptor(OutboundInterceptor): + def __init__( + self, + parent: ActivityTracingInterceptor, + next: OutboundInterceptor, + ) -> None: + super().__init__(next) + self._parent = parent + + async def start_activity(self, input: StartActivityInput): + assert isinstance(input, StartActivityInput) + self._parent.start_activity_calls.append(input) + return await super().start_activity(input) + + async def describe_activity(self, input: DescribeActivityInput): + assert isinstance(input, DescribeActivityInput) + self._parent.describe_activity_calls.append(input) + return await super().describe_activity(input) + + async def cancel_activity(self, input: CancelActivityInput): + assert isinstance(input, CancelActivityInput) + self._parent.cancel_activity_calls.append(input) + return await super().cancel_activity(input) + + async def terminate_activity(self, input: TerminateActivityInput): + assert isinstance(input, TerminateActivityInput) + self._parent.terminate_activity_calls.append(input) + return await super().terminate_activity(input) + + def list_activities(self, input: ListActivitiesInput): + assert isinstance(input, ListActivitiesInput) + self._parent.list_activities_calls.append(input) + return super().list_activities(input) + + async def count_activities(self, input: CountActivitiesInput): + assert isinstance(input, CountActivitiesInput) + self._parent.count_activities_calls.append(input) + return await super().count_activities(input) + + +# Cloud namespaces created by CI do not have the activity start-delay dynamic config. +@pytest.mark.requires_local_server +async def test_start_activity_calls_interceptor( + client: Client, env: WorkflowEnvironment +): + """Client.start_activity() should call the start_activity interceptor.""" + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + + interceptor = ActivityTracingInterceptor() + intercepted_client = Client( + service_client=client.service_client, + namespace=client.namespace, + interceptors=[interceptor], + ) + + activity_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + start_delay = timedelta(seconds=3) + + await intercepted_client.start_activity( + increment, + args=(1,), + id=activity_id, + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=5), + start_delay=start_delay, + ) + + assert len(interceptor.start_activity_calls) == 1 + call = interceptor.start_activity_calls[0] + assert call.id == activity_id + assert call.task_queue == task_queue + assert call.activity_type == "increment" + assert call.start_delay == start_delay + + +async def test_describe_activity_calls_interceptor( + client: Client, env: WorkflowEnvironment +): + """ActivityHandle.describe() should call the describe_activity interceptor.""" + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + + interceptor = ActivityTracingInterceptor() + intercepted_client = Client( + service_client=client.service_client, + namespace=client.namespace, + interceptors=[interceptor], + ) + + activity_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + + activity_handle = await intercepted_client.start_activity( + increment, + args=(1,), + id=activity_id, + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=5), + ) + + desc = await activity_handle.describe() + assert isinstance(desc, ActivityExecutionDescription) + + assert len(interceptor.describe_activity_calls) == 1 + call = interceptor.describe_activity_calls[0] + assert call.activity_id == activity_id + + +async def test_cancel_activity_calls_interceptor( + client: Client, env: WorkflowEnvironment +): + """ActivityHandle.cancel() should call the cancel_activity interceptor.""" + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + + interceptor = ActivityTracingInterceptor() + intercepted_client = Client( + service_client=client.service_client, + namespace=client.namespace, + interceptors=[interceptor], + ) + + activity_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + + activity_handle = await intercepted_client.start_activity( + increment, + args=(1,), + id=activity_id, + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=5), + ) + + await activity_handle.cancel(reason="test cancellation") + + assert len(interceptor.cancel_activity_calls) == 1 + call = interceptor.cancel_activity_calls[0] + assert call.activity_id == activity_id + assert call.reason == "test cancellation" + + +async def test_terminate_activity_calls_interceptor( + client: Client, env: WorkflowEnvironment +): + """ActivityHandle.terminate() should call the terminate_activity interceptor.""" + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + + interceptor = ActivityTracingInterceptor() + intercepted_client = Client( + service_client=client.service_client, + namespace=client.namespace, + interceptors=[interceptor], + ) + + activity_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + + activity_handle = await intercepted_client.start_activity( + increment, + args=(1,), + id=activity_id, + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=5), + ) + + await activity_handle.terminate(reason="test termination") + + assert len(interceptor.terminate_activity_calls) == 1 + call = interceptor.terminate_activity_calls[0] + assert call.activity_id == activity_id + assert call.reason == "test termination" + + +async def test_list_activities_calls_interceptor( + client: Client, env: WorkflowEnvironment +): + """Client.list_activities() should call the list_activities interceptor.""" + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + + interceptor = ActivityTracingInterceptor() + intercepted_client = Client( + service_client=client.service_client, + namespace=client.namespace, + interceptors=[interceptor], + ) + + activity_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + + await intercepted_client.start_activity( + increment, + args=(1,), + id=activity_id, + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=5), + ) + + query = f'ActivityId = "{activity_id}"' + async for _ in intercepted_client.list_activities(query): + pass + + assert len(interceptor.list_activities_calls) >= 1 + call = interceptor.list_activities_calls[0] + assert call.query == query + + +async def test_count_activities_calls_interceptor( + client: Client, env: WorkflowEnvironment +): + """Client.count_activities() should call the count_activities interceptor.""" + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + + interceptor = ActivityTracingInterceptor() + intercepted_client = Client( + service_client=client.service_client, + namespace=client.namespace, + interceptors=[interceptor], + ) + + activity_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + + await intercepted_client.start_activity( + increment, + args=(1,), + id=activity_id, + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=5), + ) + + query = f'ActivityId = "{activity_id}"' + count = await intercepted_client.count_activities(query) + assert isinstance(count, ActivityExecutionCount) + + assert len(interceptor.count_activities_calls) == 1 + call = interceptor.count_activities_calls[0] + assert call.query == query + + +async def test_start_activity_rejects_negative_start_delay(client: Client): + with pytest.raises(ValueError, match="start_delay must be non-negative"): + await client.start_activity( + increment, + args=(1,), + id=str(uuid.uuid4()), + task_queue=str(uuid.uuid4()), + start_to_close_timeout=timedelta(seconds=5), + start_delay=timedelta(seconds=-1), + ) + + +async def test_get_result(client: Client, env: WorkflowEnvironment): + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + + activity_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + + activity_handle = await client.start_activity( + increment, + args=(1,), + id=activity_id, + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=5), + ) + result_via_execute_activity = client.execute_activity( + increment, + args=(1,), + id=activity_id, + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=5), + ) + + async with Worker( + client, + task_queue=task_queue, + activities=[increment], + ): + assert await activity_handle.result() == 2 + assert await result_via_execute_activity == 2 + + +# Cloud namespaces created by CI do not have the activity start-delay dynamic config. +@pytest.mark.requires_local_server +async def test_start_activity_start_delay(client: Client, env: WorkflowEnvironment): + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + + activity_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + start_delay = timedelta(seconds=2) + + async with Worker( + client, + task_queue=task_queue, + activities=[increment], + ): + activity_handle = await client.start_activity( + increment, + args=(1,), + id=activity_id, + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=5), + start_delay=start_delay, + ) + + assert await activity_handle.result() == 2 + desc = await activity_handle.describe() + assert desc.last_started_time is not None + assert ( + desc.last_started_time - desc.scheduled_time + ).total_seconds() >= start_delay.total_seconds() - 0.5 + + +async def test_get_activity_handle(client: Client, env: WorkflowEnvironment): + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + + activity_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + + activity_handle = await client.start_activity( + increment, + 1, + id=activity_id, + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=5), + ) + + handle_by_id = client.get_activity_handle(activity_id) + assert handle_by_id.id == activity_id + assert handle_by_id.run_id is None + + handle_by_id_and_run_id = client.get_activity_handle( + activity_id, + run_id=activity_handle.run_id, + ) + assert handle_by_id_and_run_id.id == activity_id + assert handle_by_id_and_run_id.run_id == activity_handle.run_id + + handle_with_result_type = client.get_activity_handle( + activity_id, + run_id=activity_handle.run_id, + result_type=int, + ) + + async with Worker( + client, + task_queue=task_queue, + activities=[increment], + ): + assert await handle_by_id.result() == 2 + assert await handle_by_id_and_run_id.result() == 2 + assert await handle_with_result_type.result() == 2 + + +async def test_list_activities(client: Client, env: WorkflowEnvironment): + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + + activity_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + + await client.start_activity( + increment, + 1, + id=activity_id, + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=5), + ) + + async def check_executions(): + executions = [ + e async for e in client.list_activities(f'ActivityId = "{activity_id}"') + ] + assert len(executions) == 1 + execution = executions[0] + assert execution.activity_id == activity_id + assert execution.activity_type == "increment" + assert execution.task_queue == task_queue + assert execution.status == ActivityExecutionStatus.RUNNING + assert ( + execution.state_transition_count is None + ) # Not set until activity completes + + await assert_eventually(check_executions) + + +async def test_count_activities(client: Client, env: WorkflowEnvironment): + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + + activity_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + + await client.start_activity( + increment, + 1, + id=activity_id, + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=5), + ) + + async def fetch_count(): + return await client.count_activities(f'ActivityId = "{activity_id}"') + + await assert_eq_eventually( + ActivityExecutionCount(count=1, groups=[]), + fetch_count, + ) + + +async def test_count_activities_group_by(client: Client, env: WorkflowEnvironment): + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + + from temporalio.client import ActivityExecutionCount + + task_queue = str(uuid.uuid4()) + activity_ids = [] + + for _ in range(3): + activity_id = str(uuid.uuid4()) + activity_ids.append(activity_id) + await client.start_activity( + increment, + 1, + id=activity_id, + task_queue=task_queue, + schedule_to_close_timeout=timedelta(seconds=60), + ) + + ids_filter = " OR ".join([f'ActivityId = "{aid}"' for aid in activity_ids]) + + async def fetch_count() -> ActivityExecutionCount: + return await client.count_activities(f"({ids_filter}) GROUP BY ExecutionStatus") + + await assert_eq_eventually( + ActivityExecutionCount( + count=3, + groups=[ + ActivityExecutionCountAggregationGroup( + count=3, group_values=["Running"] + ), + ], + ), + fetch_count, + ) + + +@dataclass +class ActivityInput: + event_workflow_id: str + wait_for_activity_start_workflow_id: str | None = None + + +@activity.defn +async def async_activity(input: ActivityInput) -> int: + # Notify test that the activity has started and is ready to be completed manually + await ( + activity.client() + .get_workflow_handle(input.event_workflow_id) + .signal(EventWorkflow.set) + ) + activity.raise_complete_async() + + +async def test_manual_completion(client: Client, env: WorkflowEnvironment): + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + + activity_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + event_workflow_id = str(uuid.uuid4()) + + activity_handle = await client.start_activity( + async_activity, + ActivityInput(event_workflow_id=event_workflow_id), + id=activity_id, + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=5), + ) + + async with Worker( + client, + task_queue=task_queue, + activities=[async_activity], + workflows=[EventWorkflow], + ): + # Wait for activity to start + await client.execute_workflow( + EventWorkflow.wait, + id=event_workflow_id, + task_queue=task_queue, + ) + # Complete activity manually + async_activity_handle = client.get_async_activity_handle( + activity_id=activity_id, + run_id=activity_handle.run_id, + ) + await async_activity_handle.complete(7) + assert await activity_handle.result() == 7 + + desc = await activity_handle.describe() + assert desc.status == ActivityExecutionStatus.COMPLETED + + +async def test_manual_cancellation(client: Client, env: WorkflowEnvironment): + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + + activity_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + event_workflow_id = str(uuid.uuid4()) + + activity_handle = await client.start_activity( + async_activity, + ActivityInput(event_workflow_id=event_workflow_id), + id=activity_id, + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=5), + ) + + async with Worker( + client, + task_queue=task_queue, + activities=[async_activity], + workflows=[EventWorkflow], + ): + # Wait for activity to start + await client.execute_workflow( + EventWorkflow.wait, + id=event_workflow_id, + task_queue=task_queue, + ) + async_activity_handle = client.get_async_activity_handle( + activity_id=activity_id, + run_id=activity_handle.run_id, + ) + + # report_cancellation fails if activity is not in CANCELLATION_REQUESTED state + with pytest.raises(RPCError) as err: + await async_activity_handle.report_cancellation("Test cancellation") + assert err.value.status == RPCStatusCode.FAILED_PRECONDITION + assert "invalid transition from Started" in str(err.value) + + # Request cancellation to transition activity to CANCELLATION_REQUESTED state + await activity_handle.cancel() + + # Now report_cancellation succeeds + await async_activity_handle.report_cancellation("Test cancellation") + + with pytest.raises(ActivityFailureError) as exc_info: + await activity_handle.result() + assert isinstance(exc_info.value.cause, CancelledError) + assert list(exc_info.value.cause.details) == ["Test cancellation"] + + desc = await activity_handle.describe() + assert desc.status == ActivityExecutionStatus.CANCELED + + +async def test_manual_failure(client: Client, env: WorkflowEnvironment): + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + + activity_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + event_workflow_id = str(uuid.uuid4()) + + activity_handle = await client.start_activity( + async_activity, + ActivityInput(event_workflow_id=event_workflow_id), + id=activity_id, + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=5), + ) + async with Worker( + client, + task_queue=task_queue, + activities=[async_activity], + workflows=[EventWorkflow], + ): + await client.execute_workflow( + EventWorkflow.wait, + id=event_workflow_id, + task_queue=task_queue, + ) + async_activity_handle = client.get_async_activity_handle( + activity_id=activity_id, + run_id=activity_handle.run_id, + ) + await async_activity_handle.fail( + ApplicationError("Test failure", non_retryable=True) + ) + with pytest.raises(ActivityFailureError) as err: + await activity_handle.result() + assert isinstance(err.value.cause, ApplicationError) + assert str(err.value.cause) == "Test failure" + + desc = await activity_handle.describe() + assert desc.status == ActivityExecutionStatus.FAILED + + +@activity.defn +async def activity_for_testing_heartbeat(input: ActivityInput) -> str: + info = activity.info() + if info.attempt == 1: + # Signal that activity has started (only on first attempt) + if input.wait_for_activity_start_workflow_id: + await ( + activity.client() + .get_workflow_handle( + workflow_id=input.wait_for_activity_start_workflow_id, + ) + .signal(EventWorkflow.set) + ) + wait_for_heartbeat_wf_handle = await activity.client().start_workflow( + EventWorkflow.wait, + id=input.event_workflow_id, + task_queue=activity.info().task_queue, + ) + # Wait for test to notify that it has sent heartbeat + await wait_for_heartbeat_wf_handle.result() + raise Exception("Intentional error to force retry") + elif info.attempt == 2: + [heartbeat_data] = info.heartbeat_details + assert isinstance(heartbeat_data, str) + return heartbeat_data + else: + raise AssertionError(f"Unexpected attempt number: {info.attempt}") + + +async def test_manual_heartbeat(client: Client, env: WorkflowEnvironment): + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + + activity_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + event_workflow_id = str(uuid.uuid4()) + wait_for_activity_start_workflow_id = str(uuid.uuid4()) + + activity_handle = await client.start_activity( + activity_for_testing_heartbeat, + ActivityInput( + event_workflow_id=event_workflow_id, + wait_for_activity_start_workflow_id=wait_for_activity_start_workflow_id, + ), + id=activity_id, + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=5), + ) + wait_for_activity_start_wf_handle = await client.start_workflow( + EventWorkflow.wait, + id=wait_for_activity_start_workflow_id, + task_queue=task_queue, + ) + async with Worker( + client, + task_queue=task_queue, + activities=[activity_for_testing_heartbeat], + workflows=[EventWorkflow], + ): + async_activity_handle = client.get_async_activity_handle( + activity_id=activity_id, + run_id=activity_handle.run_id, + ) + await wait_for_activity_start_wf_handle.result() + await async_activity_handle.heartbeat("Test heartbeat details") + await client.get_workflow_handle( + workflow_id=event_workflow_id, + ).signal(EventWorkflow.set) + assert await activity_handle.result() == "Test heartbeat details" + + +async def test_id_conflict_policy_fail(client: Client, env: WorkflowEnvironment): + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + + activity_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + from temporalio.common import ActivityIDConflictPolicy + from temporalio.exceptions import ActivityAlreadyStartedError + + await client.start_activity( + increment, + 1, + id=activity_id, + task_queue=task_queue, + schedule_to_close_timeout=timedelta(seconds=60), + id_conflict_policy=ActivityIDConflictPolicy.FAIL, + ) + + with pytest.raises(ActivityAlreadyStartedError) as err: + await client.start_activity( + increment, + 1, + id=activity_id, + task_queue=task_queue, + schedule_to_close_timeout=timedelta(seconds=60), + id_conflict_policy=ActivityIDConflictPolicy.FAIL, + ) + assert err.value.activity_id == activity_id + assert "Activity" in str(err.value), ( + f"Expected 'Activity' in error message, got: {err.value}" + ) + + +async def test_id_conflict_policy_use_existing( + client: Client, env: WorkflowEnvironment +): + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + + activity_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + from temporalio.common import ActivityIDConflictPolicy + + handle1 = await client.start_activity( + increment, + 1, + id=activity_id, + task_queue=task_queue, + schedule_to_close_timeout=timedelta(seconds=60), + id_conflict_policy=ActivityIDConflictPolicy.USE_EXISTING, + ) + + handle2 = await client.start_activity( + increment, + 1, + id=activity_id, + task_queue=task_queue, + schedule_to_close_timeout=timedelta(seconds=60), + id_conflict_policy=ActivityIDConflictPolicy.USE_EXISTING, + ) + + assert handle1.id == handle2.id + assert handle1.run_id == handle2.run_id + + +async def test_id_reuse_policy_reject_duplicate( + client: Client, env: WorkflowEnvironment +): + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + + activity_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + from temporalio.common import ActivityIDReusePolicy + from temporalio.exceptions import ActivityAlreadyStartedError + + handle = await client.start_activity( + increment, + 1, + id=activity_id, + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=5), + id_reuse_policy=ActivityIDReusePolicy.REJECT_DUPLICATE, + ) + + async with Worker( + client, + task_queue=task_queue, + activities=[increment], + ): + await handle.result() + + with pytest.raises(ActivityAlreadyStartedError) as err: + await client.start_activity( + increment, + 1, + id=activity_id, + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=5), + id_reuse_policy=ActivityIDReusePolicy.REJECT_DUPLICATE, + ) + assert err.value.activity_id == activity_id + + +async def test_id_reuse_policy_allow_duplicate( + client: Client, env: WorkflowEnvironment +): + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + + activity_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + from temporalio.common import ActivityIDReusePolicy + + handle1 = await client.start_activity( + increment, + 1, + id=activity_id, + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=5), + id_reuse_policy=ActivityIDReusePolicy.ALLOW_DUPLICATE, + ) + + async with Worker( + client, + task_queue=task_queue, + activities=[increment], + ): + await handle1.result() + + handle2 = await client.start_activity( + increment, + 1, + id=activity_id, + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=5), + id_reuse_policy=ActivityIDReusePolicy.ALLOW_DUPLICATE, + ) + + assert handle1.id == handle2.id + assert handle1.run_id != handle2.run_id + + +async def test_search_attributes(client: Client, env: WorkflowEnvironment): + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + + from temporalio.common import ( + SearchAttributeKey, + SearchAttributePair, + TypedSearchAttributes, + ) + + activity_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + temporal_change_version_key = SearchAttributeKey.for_keyword_list( + "TemporalChangeVersion" + ) + + handle = await client.start_activity( + increment, + 1, + id=activity_id, + task_queue=task_queue, + schedule_to_close_timeout=timedelta(seconds=60), + search_attributes=TypedSearchAttributes( + [SearchAttributePair(temporal_change_version_key, ["test-1", "test-2"])] + ), + ) + + desc = await handle.describe() + assert desc.typed_search_attributes[temporal_change_version_key] == [ + "test-1", + "test-2", + ] + + +async def test_retry_policy(client: Client, env: WorkflowEnvironment): + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + + from temporalio.common import RetryPolicy + + activity_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + + handle = await client.start_activity( + increment, + 1, + id=activity_id, + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=5), + retry_policy=RetryPolicy( + initial_interval=timedelta(seconds=1), + maximum_interval=timedelta(seconds=10), + backoff_coefficient=2.0, + maximum_attempts=3, + ), + ) + + desc = await handle.describe() + assert desc.retry_policy is not None + assert desc.retry_policy.initial_interval == timedelta(seconds=1) + assert desc.retry_policy.maximum_interval == timedelta(seconds=10) + assert desc.retry_policy.backoff_coefficient == 2.0 + assert desc.retry_policy.maximum_attempts == 3 + + +async def test_terminate(client: Client, env: WorkflowEnvironment): + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + + activity_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + event_workflow_id = str(uuid.uuid4()) + + activity_handle = await client.start_activity( + async_activity, + args=(ActivityInput(event_workflow_id=event_workflow_id),), + id=activity_id, + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=5), + ) + + async with Worker( + client, + task_queue=task_queue, + activities=[async_activity], + workflows=[EventWorkflow], + ): + await client.execute_workflow( + EventWorkflow.wait, + id=event_workflow_id, + task_queue=task_queue, + ) + + await activity_handle.terminate(reason="Test termination") + + with pytest.raises(ActivityFailureError): + await activity_handle.result() + + desc = await activity_handle.describe() + assert desc.status == ActivityExecutionStatus.TERMINATED + + +# Tests for start_activity_class / execute_activity_class + + +async def test_start_activity_class_async(client: Client, env: WorkflowEnvironment): + """Test start_activity_class with an async callable class.""" + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + + activity_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + + handle = await client.start_activity_class( + IncrementClass, + 1, + id=activity_id, + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=5), + ) + + async with Worker( + client, + task_queue=task_queue, + activities=[IncrementClass()], + ): + result = await handle.result() + assert result == 2 + + +async def test_execute_activity_class_async(client: Client, env: WorkflowEnvironment): + """Test execute_activity_class with an async callable class.""" + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + + activity_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + + async with Worker( + client, + task_queue=task_queue, + activities=[IncrementClass()], + ): + result = await client.execute_activity_class( + IncrementClass, + 1, + id=activity_id, + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=5), + ) + assert result == 2 + + +async def test_start_activity_class_no_param(client: Client, env: WorkflowEnvironment): + """Test start_activity_class with a no-param callable class.""" + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + + activity_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + + handle = await client.start_activity_class( + NoParamClass, + id=activity_id, + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=5), + ) + + async with Worker( + client, + task_queue=task_queue, + activities=[NoParamClass()], + ): + result = await handle.result() + assert result == "no-param-result" + + +async def test_start_activity_class_sync(client: Client, env: WorkflowEnvironment): + """Test start_activity_class with a sync callable class.""" + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + + import concurrent.futures + + activity_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + + handle = await client.start_activity_class( + SyncIncrementClass, + 1, + id=activity_id, + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=5), + ) + + with concurrent.futures.ThreadPoolExecutor() as executor: + async with Worker( + client, + task_queue=task_queue, + activities=[SyncIncrementClass()], + activity_executor=executor, + ): + result = await handle.result() + assert result == 2 + + +# Tests for start_activity_method / execute_activity_method + + +async def test_start_activity_method_async(client: Client, env: WorkflowEnvironment): + """Test start_activity_method with an async method.""" + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + + activity_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + + holder = ActivityHolder() + handle = await client.start_activity_method( + ActivityHolder.async_increment, + 1, + id=activity_id, + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=5), + ) + + async with Worker( + client, + task_queue=task_queue, + activities=[holder.async_increment], + ): + result = await handle.result() + assert result == 2 + + +async def test_execute_activity_method_async(client: Client, env: WorkflowEnvironment): + """Test execute_activity_method with an async method.""" + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + + activity_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + + holder = ActivityHolder() + async with Worker( + client, + task_queue=task_queue, + activities=[holder.async_increment], + ): + result = await client.execute_activity_method( + ActivityHolder.async_increment, + 1, + id=activity_id, + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=5), + ) + assert result == 2 + + +async def test_start_activity_method_no_param(client: Client, env: WorkflowEnvironment): + """Test start_activity_method with a no-param method.""" + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + + activity_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + + holder = ActivityHolder() + handle = await client.start_activity_method( + ActivityHolder.async_no_param, + id=activity_id, + task_queue=task_queue, + start_to_close_timeout=timedelta(seconds=5), + ) + + async with Worker( + client, + task_queue=task_queue, + activities=[holder.async_no_param], + ): + result = await handle.result() + assert result == "async-method-result" + + +# Utilities + + +@workflow.defn +class EventWorkflow: + """ + A workflow version of asyncio.Event() + """ + + def __init__(self) -> None: + self.signal_received = asyncio.Event() + + @workflow.run + async def wait(self) -> None: + await self.signal_received.wait() + + @workflow.signal + def set(self) -> None: + self.signal_received.set() diff --git a/tests/test_activity_type_errors.py b/tests/test_activity_type_errors.py new file mode 100644 index 000000000..fadf7d14b --- /dev/null +++ b/tests/test_activity_type_errors.py @@ -0,0 +1,491 @@ +""" +This file exists to test for type-checker false positives and false negatives +for the activity client API. + +It doesn't contain any test functions - it uses the machinery in test_type_errors.py +to verify that pyright produces the expected errors. +""" + +from datetime import timedelta +from unittest.mock import Mock + +from temporalio import activity +from temporalio.client import ActivityHandle, Client +from temporalio.service import ServiceClient + + +@activity.defn +async def increment(x: int) -> int: + return x + 1 + + +@activity.defn +async def greet(name: str) -> str: + return f"Hello, {name}" + + +@activity.defn +async def no_return(_: int) -> None: + pass + + +@activity.defn +async def no_param_async() -> str: + return "done" + + +@activity.defn +def increment_sync(x: int) -> int: + return x + 1 + + +@activity.defn +def no_param_sync() -> str: + return "done" + + +@activity.defn +class IncrementClass: + """Async activity defined as a callable class.""" + + async def __call__(self, x: int) -> int: + return x + 1 + + +@activity.defn +class NoParamClass: + """Async activity class with no parameters.""" + + async def __call__(self) -> str: + return "done" + + +@activity.defn +class SyncIncrementClass: + """Sync activity defined as a callable class.""" + + def __call__(self, x: int) -> int: + return x + 1 + + +@activity.defn +class SyncNoParamClass: + """Sync activity class with no parameters.""" + + def __call__(self) -> str: + return "done" + + +class ActivityHolder: + """Class holding activity methods.""" + + @activity.defn + async def increment_method(self, x: int) -> int: + return x + 1 + + @activity.defn + async def no_param_method(self) -> str: + return "done" + + +async def _test_start_activity_typed_callable_happy_path() -> None: # type:ignore[reportUnusedFunction] + client = Client(service_client=Mock(spec=ServiceClient)) + + _handle: ActivityHandle[int] = await client.start_activity( + increment, + args=[1], + id="activity-id", + task_queue="tq", + start_to_close_timeout=timedelta(seconds=5), + ) + + _result: int = await _handle.result() + + +async def _test_execute_activity_typed_callable_happy_path() -> None: # type:ignore[reportUnusedFunction] + client = Client(service_client=Mock(spec=ServiceClient)) + + _result: int = await client.execute_activity( + increment, + args=[1], + id="activity-id", + task_queue="tq", + start_to_close_timeout=timedelta(seconds=5), + ) + + +async def _test_start_activity_positional_arg_happy_path() -> None: # type:ignore[reportUnusedFunction] + client = Client(service_client=Mock(spec=ServiceClient)) + + _handle: ActivityHandle[int] = await client.start_activity( + increment, + 1, + id="activity-id", + task_queue="tq", + start_to_close_timeout=timedelta(seconds=5), + ) + + +async def _test_execute_activity_positional_arg_happy_path() -> None: # type:ignore[reportUnusedFunction] + client = Client(service_client=Mock(spec=ServiceClient)) + + _result: int = await client.execute_activity( + increment, + 1, + id="activity-id", + task_queue="tq", + start_to_close_timeout=timedelta(seconds=5), + ) + + +async def _test_start_activity_string_name_with_result_type() -> None: # type:ignore[reportUnusedFunction] + client = Client(service_client=Mock(spec=ServiceClient)) + + _handle = await client.start_activity( + "increment", + args=[1], + id="activity-id", + task_queue="tq", + result_type=int, + start_to_close_timeout=timedelta(seconds=5), + ) + + +async def _test_start_activity_no_param_async_happy_path() -> None: # type:ignore[reportUnusedFunction] + client = Client(service_client=Mock(spec=ServiceClient)) + + _handle: ActivityHandle[str] = await client.start_activity( + no_param_async, + id="activity-id", + task_queue="tq", + start_to_close_timeout=timedelta(seconds=5), + ) + + +async def _test_execute_activity_no_param_async_happy_path() -> None: # type:ignore[reportUnusedFunction] + client = Client(service_client=Mock(spec=ServiceClient)) + + _result: str = await client.execute_activity( + no_param_async, + id="activity-id", + task_queue="tq", + start_to_close_timeout=timedelta(seconds=5), + ) + + +async def _test_start_activity_no_param_sync_happy_path() -> None: # type:ignore[reportUnusedFunction] + client = Client(service_client=Mock(spec=ServiceClient)) + + _handle: ActivityHandle[str] = await client.start_activity( + no_param_sync, + id="activity-id", + task_queue="tq", + start_to_close_timeout=timedelta(seconds=5), + ) + + +async def _test_execute_activity_no_param_sync_happy_path() -> None: # type:ignore[reportUnusedFunction] + client = Client(service_client=Mock(spec=ServiceClient)) + + _result: str = await client.execute_activity( + no_param_sync, + id="activity-id", + task_queue="tq", + start_to_close_timeout=timedelta(seconds=5), + ) + + +async def _test_start_activity_wrong_arg_type() -> None: # type:ignore[reportUnusedFunction] + client = Client(service_client=Mock(spec=ServiceClient)) + + _handle: ActivityHandle[int] = await client.start_activity( + increment, + # assert-type-error-pyright: 'cannot be assigned to parameter' + "wrong type", # type: ignore + id="activity-id", + task_queue="tq", + start_to_close_timeout=timedelta(seconds=5), + ) + + +async def _test_execute_activity_wrong_arg_type() -> None: # type:ignore[reportUnusedFunction] + client = Client(service_client=Mock(spec=ServiceClient)) + + _result: int = await client.execute_activity( + increment, + # assert-type-error-pyright: 'cannot be assigned to parameter' + "wrong type", # type: ignore + id="activity-id", + task_queue="tq", + start_to_close_timeout=timedelta(seconds=5), + ) + + +async def _test_start_activity_wrong_result_type_assignment() -> None: # type:ignore[reportUnusedFunction] + client = Client(service_client=Mock(spec=ServiceClient)) + + handle = await client.start_activity( + increment, + args=[1], + id="activity-id", + task_queue="tq", + start_to_close_timeout=timedelta(seconds=5), + ) + + # assert-type-error-pyright: 'Type "int" is not assignable to declared type "str"' + _wrong: str = await handle.result() # type: ignore + + +async def _test_execute_activity_wrong_result_type_assignment() -> None: # type:ignore[reportUnusedFunction] + client = Client(service_client=Mock(spec=ServiceClient)) + + # assert-type-error-pyright: 'Type "int" is not assignable to declared type "str"' + _wrong: str = await client.execute_activity( # type: ignore + increment, # type: ignore[arg-type] + args=[1], + id="activity-id", + task_queue="tq", + start_to_close_timeout=timedelta(seconds=5), + ) + + +async def _test_start_activity_missing_required_params() -> None: # type:ignore[reportUnusedFunction] + client = Client(service_client=Mock(spec=ServiceClient)) + + # assert-type-error-pyright: 'No overloads for "start_activity" match' + await client.start_activity( # type: ignore + increment, + args=[1], + task_queue="tq", + start_to_close_timeout=timedelta(seconds=5), + ) + + # assert-type-error-pyright: 'No overloads for "start_activity" match' + await client.start_activity( # type: ignore + increment, + args=[1], + id="activity-id", + start_to_close_timeout=timedelta(seconds=5), + ) + + +async def _test_activity_handle_typed_correctly() -> None: # type:ignore[reportUnusedFunction] + client = Client(service_client=Mock(spec=ServiceClient)) + + handle_int: ActivityHandle[int] = await client.start_activity( + increment, + args=[1], + id="activity-id", + task_queue="tq", + start_to_close_timeout=timedelta(seconds=5), + ) + _int_result: int = await handle_int.result() + + handle_str: ActivityHandle[str] = await client.start_activity( + greet, + args=["world"], + id="activity-id-2", + task_queue="tq", + start_to_close_timeout=timedelta(seconds=5), + ) + _str_result: str = await handle_str.result() + + handle_none: ActivityHandle[None] = await client.start_activity( + no_return, + args=[1], + id="activity-id-3", + task_queue="tq", + start_to_close_timeout=timedelta(seconds=5), + ) + _none_result: None = await handle_none.result() # type: ignore[func-returns-value] + + +async def _test_activity_handle_wrong_type_parameter() -> None: # type:ignore[reportUnusedFunction] + client = Client(service_client=Mock(spec=ServiceClient)) + + # assert-type-error-pyright: 'Type "ActivityHandle\[int\]" is not assignable to declared type "ActivityHandle\[str\]"' + _handle: ActivityHandle[str] = await client.start_activity( # type: ignore + increment, # type: ignore[arg-type] + args=[1], + id="activity-id", + task_queue="tq", + start_to_close_timeout=timedelta(seconds=5), + ) + + +async def _test_start_activity_sync_activity() -> None: # type:ignore[reportUnusedFunction] + client = Client(service_client=Mock(spec=ServiceClient)) + + _handle: ActivityHandle[int] = await client.start_activity( + increment_sync, + args=[1], + id="activity-id", + task_queue="tq", + start_to_close_timeout=timedelta(seconds=5), + ) + + +async def _test_execute_activity_sync_activity() -> None: # type:ignore[reportUnusedFunction] + client = Client(service_client=Mock(spec=ServiceClient)) + + _result: int = await client.execute_activity( + increment_sync, + args=[1], + id="activity-id", + task_queue="tq", + start_to_close_timeout=timedelta(seconds=5), + ) + + +async def _test_start_activity_sync_no_param() -> None: # type:ignore[reportUnusedFunction] + client = Client(service_client=Mock(spec=ServiceClient)) + + _handle: ActivityHandle[str] = await client.start_activity( + no_param_sync, + id="activity-id", + task_queue="tq", + start_to_close_timeout=timedelta(seconds=5), + ) + + +# Tests for start_activity_class and execute_activity_class +# Note: Type inference for callable classes is limited; use args= form + + +async def _test_start_activity_class_single_param() -> None: # type:ignore[reportUnusedFunction] + client = Client(service_client=Mock(spec=ServiceClient)) + + _handle: ActivityHandle[int] = await client.start_activity_class( + IncrementClass, + 1, + id="activity-id", + task_queue="tq", + start_to_close_timeout=timedelta(seconds=5), + ) + + +async def _test_execute_activity_class_single_param() -> None: # type:ignore[reportUnusedFunction] + client = Client(service_client=Mock(spec=ServiceClient)) + + _result: int = await client.execute_activity_class( + IncrementClass, + 1, + id="activity-id", + task_queue="tq", + start_to_close_timeout=timedelta(seconds=5), + ) + + +async def _test_start_activity_class_no_param() -> None: # type:ignore[reportUnusedFunction] + client = Client(service_client=Mock(spec=ServiceClient)) + + _handle: ActivityHandle[str] = await client.start_activity_class( + NoParamClass, + id="activity-id", + task_queue="tq", + start_to_close_timeout=timedelta(seconds=5), + ) + + +async def _test_execute_activity_class_no_param() -> None: # type:ignore[reportUnusedFunction] + client = Client(service_client=Mock(spec=ServiceClient)) + + _result: str = await client.execute_activity_class( + NoParamClass, + id="activity-id", + task_queue="tq", + start_to_close_timeout=timedelta(seconds=5), + ) + + +# Tests for sync callable classes + + +async def _test_start_activity_class_sync_single_param() -> None: # type:ignore[reportUnusedFunction] + client = Client(service_client=Mock(spec=ServiceClient)) + + _handle: ActivityHandle[int] = await client.start_activity_class( + SyncIncrementClass, + 1, + id="activity-id", + task_queue="tq", + start_to_close_timeout=timedelta(seconds=5), + ) + + +async def _test_execute_activity_class_sync_single_param() -> None: # type:ignore[reportUnusedFunction] + client = Client(service_client=Mock(spec=ServiceClient)) + + _result: int = await client.execute_activity_class( + SyncIncrementClass, + 1, + id="activity-id", + task_queue="tq", + start_to_close_timeout=timedelta(seconds=5), + ) + + +async def _test_start_activity_class_sync_no_param() -> None: # type:ignore[reportUnusedFunction] + client = Client(service_client=Mock(spec=ServiceClient)) + + _handle: ActivityHandle[str] = await client.start_activity_class( + SyncNoParamClass, + id="activity-id", + task_queue="tq", + start_to_close_timeout=timedelta(seconds=5), + ) + + +# Tests for start_activity_method and execute_activity_method +# Note: The _method variants work best with unbound methods (class references). +# For bound methods accessed via instance, use start_activity directly. + + +async def _test_start_activity_method_unbound() -> None: # type:ignore[reportUnusedFunction] + client = Client(service_client=Mock(spec=ServiceClient)) + + # Using unbound method reference + _handle: ActivityHandle[int] = await client.start_activity_method( + ActivityHolder.increment_method, + args=[1], + id="activity-id", + task_queue="tq", + start_to_close_timeout=timedelta(seconds=5), + ) + + +async def _test_execute_activity_method_unbound() -> None: # type:ignore[reportUnusedFunction] + client = Client(service_client=Mock(spec=ServiceClient)) + + # Using unbound method reference + _result: int = await client.execute_activity_method( + ActivityHolder.increment_method, + args=[1], + id="activity-id", + task_queue="tq", + start_to_close_timeout=timedelta(seconds=5), + ) + + +async def _test_start_activity_method_no_param_unbound() -> None: # type:ignore[reportUnusedFunction] + client = Client(service_client=Mock(spec=ServiceClient)) + + # Using unbound method reference + _handle: ActivityHandle[str] = await client.start_activity_method( + ActivityHolder.no_param_method, + id="activity-id", + task_queue="tq", + start_to_close_timeout=timedelta(seconds=5), + ) + + +async def _test_execute_activity_method_no_param_unbound() -> None: # type:ignore[reportUnusedFunction] + client = Client(service_client=Mock(spec=ServiceClient)) + + # Using unbound method reference + _result: str = await client.execute_activity_method( + ActivityHolder.no_param_method, + id="activity-id", + task_queue="tq", + start_to_close_timeout=timedelta(seconds=5), + ) diff --git a/tests/test_client.py b/tests/test_client.py index 5671bc118..15324cf78 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,24 +1,25 @@ import asyncio import dataclasses import json +import multiprocessing +import multiprocessing.context import os +import threading import uuid +from collections.abc import Mapping from datetime import datetime, timedelta, timezone -from typing import Any, List, Mapping, Optional, Tuple, cast +from typing import Any, Literal, cast from unittest import mock import google.protobuf.any_pb2 -import google.protobuf.message +import pytest from google.protobuf import json_format import temporalio.api.common.v1 -import temporalio.api.enums.v1 -import temporalio.api.errordetails.v1 import temporalio.api.workflowservice.v1 import temporalio.common import temporalio.exceptions from temporalio import workflow -from temporalio.api.cloud.cloudservice.v1 import GetNamespaceRequest from temporalio.api.enums.v1 import ( CancelExternalWorkflowExecutionFailedCause, ContinueAsNewInitiator, @@ -42,14 +43,9 @@ BuildIdOpPromoteSetByBuildId, CancelWorkflowInput, Client, - ClientConfig, - CloudOperationsClient, Interceptor, OutboundInterceptor, - Plugin, QueryWorkflowInput, - RPCError, - RPCStatusCode, Schedule, ScheduleActionExecutionStartWorkflow, ScheduleActionStartWorkflow, @@ -88,15 +84,18 @@ ) from temporalio.converter import DataConverter from temporalio.exceptions import WorkflowAlreadyStartedError -from temporalio.service import ServiceCall +from temporalio.service import ( + RPCError, + RPCStatusCode, +) from temporalio.testing import WorkflowEnvironment from tests.helpers import ( assert_eq_eventually, - assert_eventually, ensure_search_attributes_present, new_worker, worker_versioning_enabled, ) +from tests.helpers.fork import _ForkTestResult, _TestFork from tests.helpers.worker import ( ExternalWorker, KSAction, @@ -109,11 +108,6 @@ KSWorkflowParams, ) -# Passing through because Python 3.9 has an import bug at -# https://github.com/python/cpython/issues/91351 -with workflow.unsafe.imports_passed_through(): - import pytest - async def test_start_id_reuse( client: Client, worker: ExternalWorker, env: WorkflowEnvironment @@ -299,12 +293,7 @@ async def test_terminate(client: Client, worker: ExternalWorker): async def test_rpc_already_exists_error_is_raised(client: Client): - class start_workflow_execution( - ServiceCall[ - temporalio.api.workflowservice.v1.StartWorkflowExecutionRequest, - temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse, - ] - ): + class start_workflow_execution: already_exists_err = RPCError( "fake already exists error", RPCStatusCode.ALREADY_EXISTS, b"" ) @@ -324,8 +313,8 @@ async def __call__( req: temporalio.api.workflowservice.v1.StartWorkflowExecutionRequest, *, retry: bool = False, - metadata: Mapping[str, str] = {}, - timeout: Optional[timedelta] = None, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, ) -> temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse: raise self.already_exists_err @@ -391,7 +380,7 @@ async def test_query(client: Client, worker: ExternalWorker): await handle.result() assert "some query arg" == await handle.query("some query", "some query arg") # Try a query not on the workflow - with pytest.raises(WorkflowQueryFailedError) as err: + with pytest.raises(WorkflowQueryFailedError): await handle.query("does not exist") @@ -567,7 +556,7 @@ async def test_interceptor(client: Client, worker: ExternalWorker): assert interceptor.traces[4][1].id == handle.id -async def test_lazy_client(client: Client, env: WorkflowEnvironment): +async def test_lazy_client(env: WorkflowEnvironment): # TODO(cretz): Fix if env.supports_time_skipping: pytest.skip( @@ -575,16 +564,72 @@ async def test_lazy_client(client: Client, env: WorkflowEnvironment): ) # Create another client that is lazy. This test just makes sure the # functionality continues to work. - lazy_client = await Client.connect( - client.service_client.config.target_host, - namespace=client.namespace, - lazy=True, - ) + lazy_client = await env.connect_client(lazy=True) assert not lazy_client.service_client.worker_service_client._bridge_client await lazy_client.workflow_service.get_system_info(GetSystemInfoRequest()) assert lazy_client.service_client.worker_service_client._bridge_client +async def test_client_connected_skips_connect_lock(env: WorkflowEnvironment): + # Once connected, RPCs must not touch the lazy-connect lock. Acquiring it + # per-RPC put an event-loop-bound primitive on the hot path, pinning a + # connected client to the loop it connected on. + other = await env.connect_client() + svc = other.service_client.worker_service_client + assert svc._bridge_client + + class CountingLock(asyncio.Lock): + def __init__(self) -> None: + super().__init__() + self.acquire_count = 0 + + async def acquire(self) -> Literal[True]: + self.acquire_count += 1 + return await super().acquire() + + counting = CountingLock() + svc._bridge_client_connect_lock = counting + await other.workflow_service.get_system_info(GetSystemInfoRequest()) + assert counting.acquire_count == 0 + + +def test_client_reuse_across_event_loops(env: WorkflowEnvironment): + # A connected client must not be pinned to the loop (or thread) it + # connected on. This mirrors the long-lived-loop reuse pattern used by + # gevent/gunicorn and synchronous services. + connect_loop = asyncio.new_event_loop() + try: + reused_client = connect_loop.run_until_complete(env.connect_client()) + finally: + connect_loop.close() + + errors: list[BaseException] = [] + + async def hammer() -> None: + await asyncio.gather( + *( + reused_client.workflow_service.get_system_info(GetSystemInfoRequest()) + for _ in range(10) + ) + ) + + def reuse_on_new_loop() -> None: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + loop.run_until_complete(hammer()) + except BaseException as err: + errors.append(err) + finally: + asyncio.set_event_loop(None) + loop.close() + + thread = threading.Thread(target=reuse_on_new_loop) + thread.start() + thread.join() + assert not errors, f"cross-loop reuse failed: {errors[0]!r}" + + @workflow.defn class ListableWorkflow: @workflow.run @@ -612,21 +657,24 @@ async def test_list_workflows_and_fetch_history( ) expected_id_and_input.append((workflow_id, f'"user{i}"')) - # List them and get their history - actual_id_and_input = sorted( - [ - ( - hist.workflow_id, - hist.events[0] - .workflow_execution_started_event_attributes.input.payloads[0] - .data.decode(), - ) - async for hist in client.list_workflows( - f"WorkflowId = '{workflow_id}'" - ).map_histories() - ] - ) - assert actual_id_and_input == expected_id_and_input + # Visibility is eventually consistent, so wait for all runs before fetching + # their histories. + async def list_id_and_input() -> list[tuple[str, str]]: + return sorted( + [ + ( + hist.workflow_id, + hist.events[0] + .workflow_execution_started_event_attributes.input.payloads[0] + .data.decode(), + ) + async for hist in client.list_workflows( + f"WorkflowId = '{workflow_id}'" + ).map_histories() + ] + ) + + await assert_eq_eventually(expected_id_and_input, list_id_and_input) # Verify listing can limit results limited = [ @@ -675,7 +723,7 @@ async def fetch_count() -> WorkflowExecutionCount: resp = await client.count_workflows( f"TaskQueue = '{worker.task_queue}' GROUP BY ExecutionStatus" ) - cast(List[WorkflowExecutionCountAggregationGroup], resp.groups).sort( + cast(list[WorkflowExecutionCountAggregationGroup], resp.groups).sort( key=lambda g: g.count ) return resp @@ -781,6 +829,7 @@ def test_history_from_json(): ) +@pytest.mark.requires_local_server async def test_schedule_basics( client: Client, worker: ExternalWorker, env: WorkflowEnvironment ): @@ -788,8 +837,6 @@ async def test_schedule_basics( pytest.skip("Java test server doesn't support schedules") elif os.getenv("TEMPORAL_TEST_PROTO3"): pytest.skip("Older proto library cannot compare repeated fields") - await assert_no_schedules(client) - # Create a schedule with a lot of stuff schedule = Schedule( action=ScheduleActionStartWorkflow( @@ -912,7 +959,7 @@ def update_schedule_simple(input: ScheduleUpdateInput) -> ScheduleUpdate: # Update but error with pytest.raises(RuntimeError) as err: - def update_fail(input: ScheduleUpdateInput) -> ScheduleUpdate: + def update_fail(_input: ScheduleUpdateInput) -> ScheduleUpdate: raise RuntimeError("Oh no") await handle.update(update_fail) @@ -932,7 +979,7 @@ def update_fail(input: ScheduleUpdateInput) -> ScheduleUpdate: ) assert isinstance(new_schedule.action, ScheduleActionStartWorkflow) - async def update_schedule_basic(input: ScheduleUpdateInput) -> ScheduleUpdate: + async def update_schedule_basic(_input: ScheduleUpdateInput) -> ScheduleUpdate: return ScheduleUpdate(new_schedule) await handle.update(update_schedule_basic) @@ -1006,7 +1053,7 @@ async def update_desc_get_action_count() -> int: ) expected_ids.append(new_handle.id) - async def list_ids() -> List[str]: + async def list_ids() -> list[str]: return sorted( [ list_desc.id @@ -1027,10 +1074,9 @@ async def list_ids() -> List[str]: assert list_descs[0].id in [f"{handle.id}-3", f"{handle.id}-4"] assert list_descs[1].id in [f"{handle.id}-3", f"{handle.id}-4"] - # Delete all of the schedules - for id in await list_ids(): + # Delete the schedules created by this test. + for id in expected_ids: await client.get_schedule_handle(id).delete() - await assert_no_schedules(client) async def test_schedule_calendar_spec_defaults( @@ -1038,8 +1084,6 @@ async def test_schedule_calendar_spec_defaults( ): if env.supports_time_skipping: pytest.skip("Java test server doesn't support schedules") - await assert_no_schedules(client) - handle = await client.create_schedule( f"schedule-{uuid.uuid4()}", Schedule( @@ -1068,7 +1112,6 @@ async def test_schedule_calendar_spec_defaults( assert time == desc.info.next_action_times[i - 1] + timedelta(days=1) await handle.delete() - await assert_no_schedules(client) async def test_schedule_trigger_immediately( @@ -1076,8 +1119,6 @@ async def test_schedule_trigger_immediately( ): if env.supports_time_skipping: pytest.skip("Java test server doesn't support schedules") - await assert_no_schedules(client) - # Create paused schedule that triggers immediately handle = await client.create_schedule( f"schedule-{uuid.uuid4()}", @@ -1109,7 +1150,6 @@ async def test_schedule_trigger_immediately( ) await handle.delete() - await assert_no_schedules(client) async def test_schedule_backfill( @@ -1117,8 +1157,6 @@ async def test_schedule_backfill( ): if env.supports_time_skipping: pytest.skip("Java test server doesn't support schedules") - await assert_no_schedules(client) - begin = datetime(year=2020, month=1, day=20, hour=5) # Create paused schedule that runs every minute and has two backfills @@ -1149,34 +1187,30 @@ async def test_schedule_backfill( ) ], ) - # We accept both 1.24 and pre-1.24 action counts - assert (await handle.describe()).info.num_actions in [1, 2] - - # Add two more backfills and and -2m will be deduped - await handle.backfill( - # 3 actions on Server >= 1.24, 2 actions on Server < 1.24 - ScheduleBackfill( - start_at=begin - timedelta(minutes=4), - end_at=begin - timedelta(minutes=2), - overlap=ScheduleOverlapPolicy.ALLOW_ALL, - ), - # 3 actions on Server >= 1.24, 2 actions on Server < 1.24, except on - # Server >= 1.24, there is overlap with the prior backfill, so this is - # only net +2 actions, regardless of Server version. - ScheduleBackfill( - start_at=begin - timedelta(minutes=2), - end_at=begin, - overlap=ScheduleOverlapPolicy.ALLOW_ALL, - ), - ) - assert (await handle.describe()).info.num_actions in [5, 7] - - await handle.delete() - await assert_no_schedules(client) + try: + # Add two more backfills. Older servers treat the end time as + # exclusive, 1.24+ servers treat it as inclusive, and 1.31+ servers no + # longer dedupe the overlapping ALLOW_ALL backfills below. + await handle.backfill( + # 3 actions on Server >= 1.24, 2 actions on Server < 1.24 + ScheduleBackfill( + start_at=begin - timedelta(minutes=4), + end_at=begin - timedelta(minutes=2), + overlap=ScheduleOverlapPolicy.ALLOW_ALL, + ), + # 3 actions on Server >= 1.24, 2 actions on Server < 1.24. + ScheduleBackfill( + start_at=begin - timedelta(minutes=2), + end_at=begin, + overlap=ScheduleOverlapPolicy.ALLOW_ALL, + ), + ) + finally: + await handle.delete() async def test_schedule_create_limited_actions_validation( - client: Client, worker: ExternalWorker, env: WorkflowEnvironment + client: Client, worker: ExternalWorker ): sched = Schedule( action=ScheduleActionStartWorkflow( @@ -1198,13 +1232,12 @@ async def test_schedule_create_limited_actions_validation( assert "are remaining actions set" in str(err.value) +@pytest.mark.requires_local_server async def test_schedule_workflow_search_attribute_update( client: Client, env: WorkflowEnvironment ): if env.supports_time_skipping: pytest.skip("Java test server doesn't support schedules") - await assert_no_schedules(client) - # Put search attribute on server text_attr_key = SearchAttributeKey.for_text("python-test-schedule-text") untyped_keyword_key = SearchAttributeKey.for_keyword("python-test-schedule-keyword") @@ -1236,7 +1269,7 @@ async def test_schedule_workflow_search_attribute_update( # Do update of typed attrs def update_schedule_typed_attrs( input: ScheduleUpdateInput, - ) -> Optional[ScheduleUpdate]: + ) -> ScheduleUpdate | None: assert isinstance( input.description.schedule.action, ScheduleActionStartWorkflow ) @@ -1297,7 +1330,6 @@ def update_schedule_typed_attrs( assert desc.typed_search_attributes[text_attr_key] == "some-schedule-attr1" await handle.delete() - await assert_no_schedules(client) @pytest.mark.parametrize( @@ -1309,13 +1341,12 @@ def update_schedule_typed_attrs( "partial-new-values-overwrites-and-drops", ], ) +@pytest.mark.requires_local_server async def test_schedule_search_attribute_update( client: Client, env: WorkflowEnvironment, test_case: str ): if env.supports_time_skipping: pytest.skip("Java test server doesn't support schedules") - await assert_no_schedules(client) - # Put search attributes on server key_1 = SearchAttributeKey.for_text("python-test-schedule-sa-update-key-1") key_2 = SearchAttributeKey.for_keyword("python-test-schedule-sa-update-key-2") @@ -1343,7 +1374,7 @@ async def test_schedule_search_attribute_update( def update_search_attributes( input: ScheduleUpdateInput, - ) -> Optional[ScheduleUpdate]: + ) -> ScheduleUpdate | None: # Make sure the initial search attributes are present assert input.description.search_attributes[key_1.name] == [val_1] assert input.description.search_attributes[key_2.name] == [val_2] @@ -1433,15 +1464,6 @@ async def expectation() -> bool: raise ValueError(f"Invalid test case: {test_case}") await handle.delete() - await assert_no_schedules(client) - - -async def assert_no_schedules(client: Client) -> None: - # Listing appears eventually consistent - async def schedule_count() -> int: - return len([d async for d in await client.list_schedules()]) - - await assert_eq_eventually(0, schedule_count) async def test_build_id_interactions(client: Client, env: WorkflowEnvironment): @@ -1492,19 +1514,6 @@ async def test_build_id_interactions(client: Client, env: WorkflowEnvironment): assert reachability.build_id_reachability["1.1"].task_queue_reachability[tq] == [] -async def test_cloud_client_simple(): - if "TEMPORAL_CLIENT_CLOUD_API_KEY" not in os.environ: - pytest.skip("No cloud API key") - client = await CloudOperationsClient.connect( - api_key=os.environ["TEMPORAL_CLIENT_CLOUD_API_KEY"], - version=os.environ["TEMPORAL_CLIENT_CLOUD_API_VERSION"], - ) - result = await client.cloud_service.get_namespace( - GetNamespaceRequest(namespace=os.environ["TEMPORAL_CLIENT_CLOUD_NAMESPACE"]) - ) - assert os.environ["TEMPORAL_CLIENT_CLOUD_NAMESPACE"] == result.namespace.namespace - - @workflow.defn class LastCompletionResultWorkflow: @workflow.run @@ -1516,6 +1525,9 @@ async def run(self) -> str: return "My First Result" +# Cloud does not reliably provide a prior completion result to manually +# triggered schedule actions. +@pytest.mark.requires_local_server async def test_schedule_last_completion_result( client: Client, env: WorkflowEnvironment ): @@ -1536,7 +1548,7 @@ async def test_schedule_last_completion_result( ) await handle.trigger() - async def get_schedule_result() -> Tuple[int, Optional[str]]: + async def get_schedule_result() -> tuple[int, str | None]: desc = await handle.describe() length = len(desc.info.recent_actions) if length == 0: @@ -1550,11 +1562,64 @@ async def get_schedule_result() -> Tuple[int, Optional[str]]: result = await workflow_handle.result() return length, result - assert await get_schedule_result() == (1, "My First Result") + expected_first_result: tuple[int, str | None] = (1, "My First Result") + await assert_eq_eventually(expected_first_result, get_schedule_result) await handle.trigger() - assert await get_schedule_result() == ( + expected_second_result: tuple[int, str | None] = ( 2, "From last completion: My First Result", ) + await assert_eq_eventually(expected_second_result, get_schedule_result) await handle.delete() + + +class TestForkCreateClient(_TestFork): + async def coro(self): + await Client.connect( + self._env.client.config()["service_client"].config.target_host + ) + + def test_fork_create_client( + self, + env: WorkflowEnvironment, + mp_fork_ctx: multiprocessing.context.BaseContext | None, + ): + self._expected = _ForkTestResult.assertion_error( + "Cannot create client across forks" + ) + self._env = env # type:ignore[reportUninitializedInstanceVariable] + self.run(mp_fork_ctx) + + +def test_client_connect_config_matches_connect_params(): + """ClientConnectConfig TypedDict keys must match Client.connect kwargs.""" + import inspect + + from temporalio.client import Client, ClientConnectConfig + + connect_params = set(inspect.signature(Client.connect).parameters.keys()) - {"cls"} + config_keys = set(ClientConnectConfig.__annotations__.keys()) + assert config_keys == connect_params, ( + f"ClientConnectConfig is out of sync with Client.connect. " + f"Missing from config: {connect_params - config_keys}. " + f"Extra in config: {config_keys - connect_params}." + ) + + +class TestForkUseClient(_TestFork): + async def coro(self): + await self._client.start_workflow( + "some-workflow", + id=f"workflow-{uuid.uuid4()}", + task_queue=f"tq-{uuid.uuid4()}", + ) + + def test_fork_use_client( + self, client: Client, mp_fork_ctx: multiprocessing.context.BaseContext | None + ): + self._expected = _ForkTestResult.assertion_error( + "Cannot use client across forks" + ) + self._client = client # type:ignore[reportUninitializedInstanceVariable] + self.run(mp_fork_ctx) diff --git a/tests/test_client_exports.py b/tests/test_client_exports.py new file mode 100644 index 000000000..5317e53c4 --- /dev/null +++ b/tests/test_client_exports.py @@ -0,0 +1,217 @@ +import temporalio.client + +# Generated from temporalio.client on main +EXPECTED_CLIENT_EXPORTS = [ + "ActivityCancellationDetails", + "ActivityExecution", + "ActivityExecutionAsyncIterator", + "ActivityExecutionCount", + "ActivityExecutionCountAggregationGroup", + "ActivityExecutionDescription", + "ActivityExecutionStatus", + "ActivityFailureError", + "ActivityHandle", + "ActivitySerializationContext", + "AnyType", + "AsyncActivityCancelledError", + "AsyncActivityHandle", + "AsyncActivityIDReference", + "BackfillScheduleInput", + "BuildIdOp", + "BuildIdOpAddNewCompatible", + "BuildIdOpAddNewDefault", + "BuildIdOpMergeSets", + "BuildIdOpPromoteBuildIdWithinSet", + "BuildIdOpPromoteSetByBuildId", + "BuildIdReachability", + "BuildIdVersionSet", + "CallableAsyncNoParam", + "CallableAsyncSingleParam", + "CallableSyncNoParam", + "CallableSyncSingleParam", + "Callback", + "CancelActivityInput", + "CancelWorkflowInput", + "Client", + "ClientConfig", + "ClientConnectConfig", + "CloudOperationsClient", + "CompleteAsyncActivityInput", + "ConnectConfig", + "CountActivitiesInput", + "CountWorkflowsInput", + "CreateScheduleInput", + "DataConverter", + "DeleteScheduleInput", + "DescribeActivityInput", + "DescribeScheduleInput", + "DescribeWorkflowInput", + "DnsLoadBalancingConfig", + "FailAsyncActivityInput", + "FetchWorkflowHistoryEventsInput", + "GetWorkerBuildIdCompatibilityInput", + "GetWorkerTaskReachabilityInput", + "GrpcCompression", + "HeaderCodecBehavior", + "HeartbeatAsyncActivityInput", + "HttpConnectProxyConfig", + "Interceptor", + "KeepAliveConfig", + "ListActivitiesInput", + "ListSchedulesInput", + "ListWorkflowsInput", + "LocalReturnType", + "MethodAsyncNoParam", + "MethodAsyncSingleParam", + "MethodSyncOrAsyncNoParam", + "MethodSyncOrAsyncSingleParam", + "MultiParamSpec", + "OutboundInterceptor", + "ParamType", + "PauseScheduleInput", + "PendingActivityState", + "Plugin", + "QueryWorkflowInput", + "RPCError", + "RPCStatusCode", + "RPCTimeoutOrCancelledError", + "ReportCancellationAsyncActivityInput", + "RetryConfig", + "ReturnType", + "Schedule", + "ScheduleAction", + "ScheduleActionExecution", + "ScheduleActionExecutionStartWorkflow", + "ScheduleActionResult", + "ScheduleActionStartWorkflow", + "ScheduleAlreadyRunningError", + "ScheduleAsyncIterator", + "ScheduleBackfill", + "ScheduleCalendarSpec", + "ScheduleDescription", + "ScheduleHandle", + "ScheduleInfo", + "ScheduleIntervalSpec", + "ScheduleListAction", + "ScheduleListActionStartWorkflow", + "ScheduleListDescription", + "ScheduleListInfo", + "ScheduleListSchedule", + "ScheduleListState", + "ScheduleOverlapPolicy", + "SchedulePolicy", + "ScheduleRange", + "ScheduleSpec", + "ScheduleState", + "ScheduleUpdate", + "ScheduleUpdateInput", + "SelfType", + "SerializationContext", + "ServiceClient", + "SignalWorkflowInput", + "StartActivityInput", + "StartWorkflowInput", + "StartWorkflowUpdateInput", + "StartWorkflowUpdateWithStartInput", + "StorageDriverActivityInfo", + "StorageDriverStoreContext", + "StorageDriverWorkflowInfo", + "TLSConfig", + "TaskReachabilityType", + "TerminateActivityInput", + "TerminateWorkflowInput", + "TriggerScheduleInput", + "UnpauseScheduleInput", + "UpdateScheduleInput", + "UpdateWithStartStartWorkflowInput", + "UpdateWithStartUpdateWorkflowInput", + "UpdateWorkerBuildIdCompatibilityInput", + "WithSerializationContext", + "WithStartWorkflowOperation", + "WorkerBuildIdVersionSets", + "WorkerTaskReachability", + "WorkflowContinuedAsNewError", + "WorkflowExecution", + "WorkflowExecutionAsyncIterator", + "WorkflowExecutionCount", + "WorkflowExecutionCountAggregationGroup", + "WorkflowExecutionDescription", + "WorkflowExecutionStatus", + "WorkflowFailureError", + "WorkflowHandle", + "WorkflowHistory", + "WorkflowHistoryEventAsyncIterator", + "WorkflowHistoryEventFilterType", + "WorkflowQueryFailedError", + "WorkflowQueryRejectedError", + "WorkflowSerializationContext", + "WorkflowUpdateFailedError", + "WorkflowUpdateHandle", + "WorkflowUpdateRPCTimeoutOrCancelledError", + "WorkflowUpdateStage", + "_ClientImpl", + "_apply_headers", + "_decode_user_metadata", + "_encode_user_metadata", + "_fix_history_enum", + "_fix_history_failure", + "_history_from_json", + "_pascal_case_match", + "annotations", +] + + +EXPECTED_INTENTIONALLY_REMOVED_CLIENT_EXPORTS = [ + "ABC", + "Any", + "AsyncIterator", + "Awaitable", + "Callable", + "Concatenate", + "Enum", + "Future", + "Generic", + "IntEnum", + "Iterable", + "Mapping", + "MessageMap", + "Required", + "Self", + "Sequence", + "TypedDict", + "abc", + "abstractmethod", + "asyncio", + "cast", + "copy", + "dataclass", + "dataclasses", + "datetime", + "functools", + "google", + "inspect", + "json", + "overload", + "re", + "temporalio", + "timedelta", + "timezone", + "uuid", + "warnings", +] + + +def test_client_module_exports_match_main() -> None: + missing = [ + name for name in EXPECTED_CLIENT_EXPORTS if not hasattr(temporalio.client, name) + ] + assert not missing + + +def test_client_module_drops_intentionally_removed_import_exports() -> None: + exported = [ + name + for name in EXPECTED_INTENTIONALLY_REMOVED_CLIENT_EXPORTS + if hasattr(temporalio.client, name) + ] + assert not exported diff --git a/tests/test_client_type_errors.py b/tests/test_client_type_errors.py index d0f3b6de5..adf3fcd3c 100644 --- a/tests/test_client_type_errors.py +++ b/tests/test_client_type_errors.py @@ -79,7 +79,7 @@ async def run(self, _: WorkflowInput) -> WorkflowOutput: return WorkflowOutput() -async def _start_and_execute_workflow_code_for_type_checking_test(): +async def _start_and_execute_workflow_code_for_type_checking_test(): # type:ignore[reportUnusedFunction] client = Client(service_client=Mock(spec=ServiceClient)) # Good @@ -117,7 +117,7 @@ async def _start_and_execute_workflow_code_for_type_checking_test(): ) -async def _signal_workflow_code_for_type_checking_test(): +async def _signal_workflow_code_for_type_checking_test(): # type:ignore[reportUnusedFunction] client = Client(service_client=Mock(spec=ServiceClient)) handle: WorkflowHandle[TestWorkflow, WorkflowOutput] = await client.start_workflow( TestWorkflow.run, WorkflowInput(), id="wid", task_queue="tq" @@ -134,7 +134,7 @@ async def _signal_workflow_code_for_type_checking_test(): await handle.signal(TestWorkflow2.signal, SignalInput()) # type: ignore -async def _query_workflow_code_for_type_checking_test(): +async def _query_workflow_code_for_type_checking_test(): # type:ignore[reportUnusedFunction] client = Client(service_client=Mock(spec=ServiceClient)) handle: WorkflowHandle[TestWorkflow, WorkflowOutput] = await client.start_workflow( TestWorkflow.run, WorkflowInput(), id="wid", task_queue="tq" @@ -152,7 +152,7 @@ async def _query_workflow_code_for_type_checking_test(): await handle.query(TestWorkflow2.query, QueryInput()) # type: ignore -async def _update_workflow_code_for_type_checking_test(): +async def _update_workflow_code_for_type_checking_test(): # type:ignore[reportUnusedFunction] client = Client(service_client=Mock(spec=ServiceClient)) handle: WorkflowHandle[TestWorkflow, WorkflowOutput] = await client.start_workflow( TestWorkflow.run, WorkflowInput(), id="wid", task_queue="tq" @@ -186,7 +186,7 @@ async def _update_workflow_code_for_type_checking_test(): await handle.execute_update(TestWorkflow2.update, UpdateInput()) # type: ignore -async def _update_with_start_workflow_code_for_type_checking_test(): +async def _update_with_start_workflow_code_for_type_checking_test(): # type:ignore[reportUnusedFunction] client = Client(service_client=Mock(spec=ServiceClient)) # Good diff --git a/tests/test_cloud.py b/tests/test_cloud.py new file mode 100644 index 000000000..d7fefb4da --- /dev/null +++ b/tests/test_cloud.py @@ -0,0 +1,25 @@ +"""Tests that run against the Temporal Cloud Operations API.""" + +import os + +import pytest + +from temporalio.api.cloud.cloudservice.v1 import GetNamespaceRequest +from temporalio.client import CloudOperationsClient + +# Skip entire module unless explicitly enabled +pytestmark = pytest.mark.skipif( + "TEMPORAL_IS_CLOUD_TESTS" not in os.environ, + reason="Cloud tests not enabled", +) + + +async def test_cloud_client_simple(): + client = await CloudOperationsClient.connect( + api_key=os.environ["TEMPORAL_CLIENT_CLOUD_API_KEY"], + version=os.environ["TEMPORAL_CLIENT_CLOUD_API_VERSION"], + ) + result = await client.cloud_service.get_namespace( + GetNamespaceRequest(namespace=os.environ["TEMPORAL_CLIENT_CLOUD_NAMESPACE"]) + ) + assert os.environ["TEMPORAL_CLIENT_CLOUD_NAMESPACE"] == result.namespace.namespace diff --git a/tests/test_common.py b/tests/test_common.py index 0cbcd8bc7..59a5ebbdf 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -8,6 +8,7 @@ import pytest from temporalio.api.common.v1 import Payload +from temporalio.api.common.v1 import RetryPolicy as RetryPolicyProto from temporalio.common import ( Priority, RawValue, @@ -38,19 +39,19 @@ def test_retry_policy_validate(): RetryPolicy(maximum_attempts=-1)._validate() -def some_hinted_func(foo: str) -> DefinedLater: +def some_hinted_func(_foo: str) -> DefinedLater: return DefinedLater() -async def some_hinted_func_async(foo: str) -> DefinedLater: +async def some_hinted_func_async(_foo: str) -> DefinedLater: return DefinedLater() class MyCallableClass: - def __call__(self, foo: str) -> DefinedLater: + def __call__(self, _foo: str) -> DefinedLater: raise NotImplementedError - def some_method(self, foo: str) -> DefinedLater: + def some_method(self, _foo: str) -> DefinedLater: raise NotImplementedError @@ -99,8 +100,50 @@ def test_typed_search_attribute_duplicates(): ) +def test_typed_search_attributes_contains_with_falsy_value(): + int_key = SearchAttributeKey.for_int("my-int") + attrs = TypedSearchAttributes([SearchAttributePair(int_key, 0)]) + assert int_key in attrs + + +def test_typed_search_attributes_contains_with_truthy_value(): + int_key = SearchAttributeKey.for_int("my-int") + attrs = TypedSearchAttributes([SearchAttributePair(int_key, 42)]) + assert int_key in attrs + + +def test_typed_search_attributes_contains_missing_key(): + int_key = SearchAttributeKey.for_int("my-int") + missing_key = SearchAttributeKey.for_keyword("missing") + attrs = TypedSearchAttributes([SearchAttributePair(int_key, 42)]) + assert missing_key not in attrs + + def test_cant_construct_bad_priority(): with pytest.raises(TypeError): Priority(priority_key=1.1) # type: ignore with pytest.raises(ValueError): Priority(priority_key=-1) + + +def test_retry_policy_from_proto_pickle(): + """Test that RetryPolicy.from_proto() creates a picklable object when non_retryable_error_types is set.""" + # Create a protobuf with non_retryable_error_types + proto = RetryPolicyProto() + proto.initial_interval.seconds = 1 + proto.backoff_coefficient = 2.0 + proto.maximum_attempts = 3 + proto.non_retryable_error_types.extend(["SomeError", "AnotherError"]) + + # Convert from proto + retry_policy = RetryPolicy.from_proto(proto) + + # This should not raise a PickleError + pickled = pickle.dumps(retry_policy) + unpickled = pickle.loads(pickled) + + # Verify the data is intact + assert unpickled.initial_interval == timedelta(seconds=1) + assert unpickled.backoff_coefficient == 2.0 + assert unpickled.maximum_attempts == 3 + assert unpickled.non_retryable_error_types == ["SomeError", "AnotherError"] diff --git a/tests/test_converter.py b/tests/test_converter.py index e274b10d9..f1a056c5f 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -6,30 +6,26 @@ import logging import sys import traceback +import typing from collections import deque +from collections.abc import Iterable, Mapping, MutableMapping, Sequence from dataclasses import dataclass from datetime import datetime, timedelta, timezone from enum import Enum, IntEnum from typing import ( Any, - Deque, - Dict, - Iterable, - List, + Dict, # type:ignore[reportDeprecated] + Generic, Literal, - Mapping, - MutableMapping, NewType, - Optional, - Sequence, - Set, - Text, - Tuple, - Type, - Union, + TypeVar, + cast, + get_args, + get_type_hints, ) from uuid import UUID, uuid4 +import nexusrpc import pydantic import pytest import typing_extensions @@ -38,6 +34,7 @@ import temporalio.api.common.v1 import temporalio.common from temporalio.api.common.v1 import Payload, Payloads +from temporalio.api.enums.v1 import NexusHandlerErrorRetryBehavior from temporalio.api.failure.v1 import Failure from temporalio.common import RawValue from temporalio.converter import ( @@ -49,17 +46,26 @@ DefaultPayloadConverter, JSONPlainPayloadConverter, JSONTypeConverter, + JSONTypeConverterUnhandled, PayloadCodec, - _JSONTypeConverterUnhandled, + TransferTypeConverter, decode_search_attributes, encode_search_attribute_values, + transfer_type_convertible, value_to_type, ) -from temporalio.exceptions import ApplicationError, FailureError +from temporalio.converter._payload_converter import ( + _TemporalTransferTypePayloadConverter, +) +from temporalio.exceptions import ( + ApplicationError, + FailureError, + NexusOperationError, +) # StrEnum is available in 3.11+ if sys.version_info >= (3, 11): - from enum import StrEnum + from enum import StrEnum # type:ignore[reportUnreachable] class NonSerializableClass: @@ -76,7 +82,7 @@ class SerializableEnum(IntEnum): if sys.version_info >= (3, 11): - class SerializableStrEnum(StrEnum): + class SerializableStrEnum(StrEnum): # type:ignore[reportUnreachable] FOO = "foo" @@ -102,12 +108,12 @@ class NewTypeMessage: async def test_converter_default(): async def assert_payload( - input, - expected_encoding, - expected_data, + input, # type:ignore[reportMissingParameterType] + expected_encoding, # type:ignore[reportMissingParameterType] + expected_data, # type:ignore[reportMissingParameterType] *, - expected_decoded_input=None, - type_hint=None, + expected_decoded_input=None, # type:ignore[reportMissingParameterType] + type_hint=None, # type:ignore[reportMissingParameterType] ): payloads = await DataConverter().encode([input]) # Check encoding and data @@ -119,7 +125,7 @@ async def assert_payload( expected_data = expected_data.encode() assert payloads[0].data == expected_data # Decode and check - actual_inputs = await DataConverter().decode(payloads, [type_hint]) + actual_inputs = await DataConverter().decode(payloads, [type_hint]) # type: ignore[reportArgumentType] assert len(actual_inputs) == 1 if expected_decoded_input is None: expected_decoded_input = input @@ -257,11 +263,184 @@ def test_binary_proto(): assert decoded == proto +class TemporalTransferTypeValueConverter( + TransferTypeConverter[ + "TemporalTransferTypeValue", + temporalio.api.common.v1.WorkflowExecution, + ] +): + transfer_type = temporalio.api.common.v1.WorkflowExecution + + def to_transfer_type( + self, value: TemporalTransferTypeValue + ) -> temporalio.api.common.v1.WorkflowExecution: + return temporalio.api.common.v1.WorkflowExecution( + workflow_id=value.value, + run_id="run-id", + ) + + def from_transfer_type( + self, + value: temporalio.api.common.v1.WorkflowExecution, + type_hint: type[TemporalTransferTypeValue], + ) -> TemporalTransferTypeValue: + return TemporalTransferTypeValue(value=value.workflow_id) + + +@transfer_type_convertible(TemporalTransferTypeValueConverter) +@dataclass +class TemporalTransferTypeValue: + value: str + + +class TemporalTransferTypeValueWithoutHintConverter( + TransferTypeConverter[ + "TemporalTransferTypeValueWithoutHint", + temporalio.api.common.v1.WorkflowExecution, + ] +): + def to_transfer_type( + self, value: TemporalTransferTypeValueWithoutHint + ) -> temporalio.api.common.v1.WorkflowExecution: + return temporalio.api.common.v1.WorkflowExecution( + workflow_id=value.value, + run_id="run-id", + ) + + def from_transfer_type( + self, + value: temporalio.api.common.v1.WorkflowExecution, + type_hint: type[TemporalTransferTypeValueWithoutHint], + ) -> TemporalTransferTypeValueWithoutHint: + return TemporalTransferTypeValueWithoutHint(value=value.workflow_id) + + +@transfer_type_convertible(TemporalTransferTypeValueWithoutHintConverter) +@dataclass +class TemporalTransferTypeValueWithoutHint: + value: str + + +T = TypeVar("T") + + +@dataclass +class TemporalTransferTypeGenericValue(Generic[T]): + value: T + + +class TemporalTransferTypeGenericValueConverter( + TransferTypeConverter[ + TemporalTransferTypeGenericValue[T], + temporalio.api.common.v1.WorkflowExecution, + ] +): + transfer_type = temporalio.api.common.v1.WorkflowExecution + + def to_transfer_type( + self, value: TemporalTransferTypeGenericValue[T] + ) -> temporalio.api.common.v1.WorkflowExecution: + return temporalio.api.common.v1.WorkflowExecution( + workflow_id=str(value.value), + run_id="run-id", + ) + + def from_transfer_type( + self, + value: temporalio.api.common.v1.WorkflowExecution, + type_hint: type[TemporalTransferTypeGenericValue[T]], + ) -> TemporalTransferTypeGenericValue[T]: + converted_value: str | int = value.workflow_id + if typing.get_args(type_hint)[0] is int: + converted_value = int(converted_value) + return TemporalTransferTypeGenericValue(value=cast(T, converted_value)) + + +# Register after both classes are defined so the generic type can be resolved. +transfer_type_convertible(TemporalTransferTypeGenericValueConverter)( + TemporalTransferTypeGenericValue +) + + +class CustomDefaultPayloadConverter(DefaultPayloadConverter): + pass + + +def test_temporal_transfer_type_payload_converter_wraps_user_converter(): + data_converter = DataConverter( + payload_converter_class=CustomDefaultPayloadConverter + ) + converter = data_converter.payload_converter + assert isinstance(converter, _TemporalTransferTypePayloadConverter) + value = TemporalTransferTypeValue("workflow-id") + + payload = converter.to_payload(value) + + assert payload.metadata["encoding"] == b"json/protobuf" + assert ( + payload.metadata["messageType"] == b"temporal.api.common.v1.WorkflowExecution" + ) + assert all("temporal-wire" not in key for key in payload.metadata) + assert all(b"temporal-wire" not in value for value in payload.metadata.values()) + assert converter.from_payload(payload, TemporalTransferTypeValue) == value + + plain_proto_payload = converter.to_payload( + temporalio.api.common.v1.WorkflowExecution(workflow_id="id1", run_id="id2") + ) + assert plain_proto_payload.metadata["encoding"] == b"json/protobuf" + + +def test_temporal_transfer_type_payload_converter_without_transfer_type_hint(): + converter = DataConverter.default.payload_converter + value = TemporalTransferTypeValueWithoutHint("workflow-id") + + payload = converter.to_payload(value) + + assert payload.metadata["encoding"] == b"json/protobuf" + assert ( + payload.metadata["messageType"] == b"temporal.api.common.v1.WorkflowExecution" + ) + assert ( + converter.from_payload(payload, TemporalTransferTypeValueWithoutHint) == value + ) + + +@pytest.mark.parametrize( + ("value", "type_hint"), + [ + ( + TemporalTransferTypeGenericValue("workflow-id"), + TemporalTransferTypeGenericValue[str], + ), + ( + TemporalTransferTypeGenericValue(123), + TemporalTransferTypeGenericValue[int], + ), + ], +) +def test_temporal_transfer_type_payload_converter_with_generic_value( + value: TemporalTransferTypeGenericValue[T], + type_hint: type[TemporalTransferTypeGenericValue[T]], +): + converter = DataConverter.default.payload_converter + + payload = converter.to_payload(value) + + assert converter.from_payload(payload, type_hint) == value + + +def test_transfer_type_convertible_rejects_existing_converter(): + with pytest.raises(TypeError, match="already has a transfer type converter"): + transfer_type_convertible(TemporalTransferTypeValueConverter)( + TemporalTransferTypeValue + ) + + def test_encode_search_attribute_values(): with pytest.raises(TypeError, match="of type tuple not one of"): encode_search_attribute_values([("bad type",)]) # type: ignore[arg-type] with pytest.raises(ValueError, match="Timezone must be present"): - encode_search_attribute_values([datetime.utcnow()]) + encode_search_attribute_values([datetime.utcnow()]) # type: ignore[reportDeprecated] with pytest.raises(TypeError, match="must have the same type"): encode_search_attribute_values(["foo", 123]) # type: ignore[arg-type] @@ -269,7 +448,7 @@ def test_encode_search_attribute_values(): def test_decode_search_attributes(): """Tests decode from protobuf for python types""" - def payload(key, dtype, data, encoding=None): + def payload(key, dtype, data, encoding=None): # type:ignore[reportMissingParameterType] if encoding is None: encoding = {"encoding": b"json/plain"} check = temporalio.api.common.v1.Payload( @@ -308,9 +487,9 @@ def payload(key, dtype, data, encoding=None): @dataclass class NestedDataClass: foo: str - bar: List[NestedDataClass] = dataclasses.field(default_factory=list) - baz: Optional[NestedDataClass] = None - qux: Optional[UUID] = None + bar: list[NestedDataClass] = dataclasses.field(default_factory=list) + baz: NestedDataClass | None = None + qux: UUID | None = None class MyTypedDict(TypedDict): @@ -326,10 +505,10 @@ class MyTypedDictNotTotal(TypedDict, total=False): # TODO(cretz): Fix when https://github.com/pydantic/pydantic/pull/9612 tagged if sys.version_info <= (3, 12, 3): - class MyPydanticClass(pydantic.BaseModel): + class MyPydanticClass(pydantic.BaseModel): # type: ignore[reportUnreachable] foo: str - bar: List[MyPydanticClass] - baz: Optional[UUID] = None + bar: list[MyPydanticClass] + baz: UUID | None = None def test_json_type_hints(): @@ -359,7 +538,7 @@ def fail(hint: Any, value: Any) -> None: ok(float, 5.5) ok(bool, True) ok(str, "foo") - ok(Text, "foo") + ok(str, "foo") ok(bytes, b"foo") fail(int, "1") fail(float, "1") @@ -391,42 +570,41 @@ def fail(hint: Any, value: Any) -> None: ok(NestedDataClass, {"foo": "bar", "unknownfield": "baz"}, NestedDataClass("bar")) # Optional/Union - ok(Optional[int], 5) - ok(Optional[int], None) - ok(Optional[MyDataClass], MyDataClass("foo", 5, SerializableEnum.FOO)) - ok(Union[int, str], 5) - ok(Union[int, str], "foo") - ok(Union[MyDataClass, NestedDataClass], MyDataClass("foo", 5, SerializableEnum.FOO)) - ok(Union[MyDataClass, NestedDataClass], NestedDataClass("foo")) - if sys.version_info >= (3, 10): - ok(int | None, None) - ok(int | None, 5) - fail(int | None, "1") - ok(MyDataClass | NestedDataClass, MyDataClass("foo", 5, SerializableEnum.FOO)) - ok(MyDataClass | NestedDataClass, NestedDataClass("foo")) + ok(int | None, 5) + ok(int | None, None) + ok(MyDataClass | None, MyDataClass("foo", 5, SerializableEnum.FOO)) + ok(int | str, 5) + ok(int | str, "foo") + ok(MyDataClass | NestedDataClass, MyDataClass("foo", 5, SerializableEnum.FOO)) + ok(MyDataClass | NestedDataClass, NestedDataClass("foo")) + ok(int | None, None) + ok(int | None, 5) + fail(int | None, "1") + ok(MyDataClass | NestedDataClass, MyDataClass("foo", 5, SerializableEnum.FOO)) + ok(MyDataClass | NestedDataClass, NestedDataClass("foo")) # NewType ok(NewIntType, 5) # List-like - ok(List, [5]) - ok(List[int], [5]) - ok(List[MyDataClass], [MyDataClass("foo", 5, SerializableEnum.FOO)]) + ok(list, [5]) + ok(list[int], [5]) + ok(list[MyDataClass], [MyDataClass("foo", 5, SerializableEnum.FOO)]) ok(Iterable[int], [5, 6]) - ok(Tuple[int, str], (5, "6")) - ok(Tuple[int, ...], (5, 6, 7)) - ok(Set[int], set([5, 6])) - ok(Set, set([5, 6])) - ok(List, ["foo"]) - ok(Deque[int], deque([5, 6])) + ok(tuple[int, str], (5, "6")) + ok(tuple[int, ...], (5, 6, 7)) + ok(set[int], {5, 6}) + ok(set, {5, 6}) + ok(list, ["foo"]) + ok(deque[int], deque([5, 6])) ok(Sequence[int], [5, 6]) - fail(List[int], [1, 2, "3"]) + fail(list[int], [1, 2, "3"]) # Dict-like - ok(Dict[str, MyDataClass], {"foo": MyDataClass("foo", 5, SerializableEnum.FOO)}) - ok(Dict, {"foo": 123}) - ok(Dict[str, Any], {"foo": 123}) - ok(Dict[Any, int], {"foo": 123}) + ok(dict[str, MyDataClass], {"foo": MyDataClass("foo", 5, SerializableEnum.FOO)}) + ok(dict, {"foo": 123}) + ok(dict[str, Any], {"foo": 123}) + ok(dict[Any, int], {"foo": 123}) ok(Mapping, {"foo": 123}) ok(Mapping[str, int], {"foo": 123}) ok(MutableMapping[str, int], {"foo": 123}) @@ -441,52 +619,51 @@ def fail(hint: Any, value: Any) -> None: ok(MyTypedDict, {"foo": "bar", "blah": "meh"}) # Non-string dict keys are supported - ok(Dict[int, str], {1: "1"}) - ok(Dict[float, str], {1.0: "1"}) - ok(Dict[bool, str], {True: "1"}) - ok(Dict[None, str], {None: "1"}) + ok(dict[int, str], {1: "1"}) + ok(dict[float, str], {1.0: "1"}) + ok(dict[bool, str], {True: "1"}) + + # On a 3.10+ dict type, None isn't returned from a key. This is potentially a bug + ok(dict[None, str], {"null": "1"}) + + # Dict has a different value for None keys + ok(Dict[None, str], {None: "1"}) # type:ignore[reportDeprecated] # Alias ok(MyDataClassAlias, MyDataClass("foo", 5, SerializableEnum.FOO)) # IntEnum ok(SerializableEnum, SerializableEnum.FOO) - ok(List[SerializableEnum], [SerializableEnum.FOO, SerializableEnum.FOO]) + ok(list[SerializableEnum], [SerializableEnum.FOO, SerializableEnum.FOO]) # UUID ok(UUID, uuid4()) - ok(List[UUID], [uuid4(), uuid4()]) + ok(list[UUID], [uuid4(), uuid4()]) # StrEnum is available in 3.11+ if sys.version_info >= (3, 11): # StrEnum - ok(SerializableStrEnum, SerializableStrEnum.FOO) + ok(SerializableStrEnum, SerializableStrEnum.FOO) # type:ignore[reportUnreachable] ok( - List[SerializableStrEnum], + list[SerializableStrEnum], [SerializableStrEnum.FOO, SerializableStrEnum.FOO], ) - # 3.10+ checks - if sys.version_info >= (3, 10): - ok(list[int], [1, 2]) - ok(dict[str, int], {"1": 2}) - ok(tuple[int, str], (1, "2")) - # Pydantic # TODO(cretz): Fix when https://github.com/pydantic/pydantic/pull/9612 tagged if sys.version_info <= (3, 12, 3): - ok( + ok( # type: ignore[reportUnreachable] MyPydanticClass, MyPydanticClass( foo="foo", bar=[MyPydanticClass(foo="baz", bar=[])], baz=uuid4() ), ) - ok(List[MyPydanticClass], [MyPydanticClass(foo="foo", bar=[])]) - fail(List[MyPydanticClass], [MyPydanticClass(foo="foo", bar=[]), 5]) + ok(list[MyPydanticClass], [MyPydanticClass(foo="foo", bar=[])]) + fail(list[MyPydanticClass], [MyPydanticClass(foo="foo", bar=[]), 5]) # This is an example of appending the stack to every Temporal failure error -def append_temporal_stack(exc: Optional[BaseException]) -> None: +def append_temporal_stack(exc: BaseException | None) -> None: while exc: # Only append if it doesn't appear already there if ( @@ -541,7 +718,7 @@ async def test_exception_format(): # Just serializes in a "payloads" wrapper class SimpleCodec(PayloadCodec): - async def encode(self, payloads: Sequence[Payload]) -> List[Payload]: + async def encode(self, payloads: Sequence[Payload]) -> list[Payload]: wrapper = Payloads(payloads=payloads) return [ Payload( @@ -549,7 +726,7 @@ async def encode(self, payloads: Sequence[Payload]) -> List[Payload]: ) ] - async def decode(self, payloads: Sequence[Payload]) -> List[Payload]: + async def decode(self, payloads: Sequence[Payload]) -> list[Payload]: payloads = list(payloads) if len(payloads) != 1: raise RuntimeError("Expected only a single payload") @@ -609,6 +786,248 @@ async def test_failure_encoded_attributes(): assert failure == orig_failure +@pytest.mark.parametrize( + "handler_type,retryable_override,expected_retryable", + [ + (nexusrpc.HandlerErrorType.BAD_REQUEST, None, None), + (nexusrpc.HandlerErrorType.BAD_REQUEST, True, True), + (nexusrpc.HandlerErrorType.BAD_REQUEST, False, False), + (nexusrpc.HandlerErrorType.INTERNAL, None, None), + (nexusrpc.HandlerErrorType.INTERNAL, True, True), + (nexusrpc.HandlerErrorType.NOT_FOUND, False, False), + (nexusrpc.HandlerErrorType.RESOURCE_EXHAUSTED, None, None), + (nexusrpc.HandlerErrorType.UNAVAILABLE, True, True), + (nexusrpc.HandlerErrorType.UPSTREAM_TIMEOUT, None, None), + (nexusrpc.HandlerErrorType.UNAUTHENTICATED, None, None), + (nexusrpc.HandlerErrorType.UNAUTHORIZED, None, None), + ], +) +async def test_nexus_handler_error_round_trip( + handler_type: nexusrpc.HandlerErrorType, + retryable_override: bool | None, + expected_retryable: bool | None, +): + """Test round-trip conversion of nexusrpc.HandlerError through failure converter.""" + message = f"test message for {handler_type.name}" + original_error = nexusrpc.HandlerError( + message, + type=handler_type, + retryable_override=retryable_override, + ) + + # Convert to failure + failure = Failure() + await DataConverter.default.encode_failure(original_error, failure) + + # Verify failure structure + assert failure.HasField("nexus_handler_failure_info") + assert failure.nexus_handler_failure_info.type == handler_type.name + assert failure.message == message + + # Verify retryable behavior mapping + if retryable_override is True: + assert ( + failure.nexus_handler_failure_info.retry_behavior + == NexusHandlerErrorRetryBehavior.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_RETRYABLE + ) + elif retryable_override is False: + assert ( + failure.nexus_handler_failure_info.retry_behavior + == NexusHandlerErrorRetryBehavior.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_NON_RETRYABLE + ) + else: + assert ( + failure.nexus_handler_failure_info.retry_behavior + == NexusHandlerErrorRetryBehavior.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_UNSPECIFIED + ) + + # Convert back to error + result_error = await DataConverter.default.decode_failure(failure) + + # Verify result + assert isinstance(result_error, nexusrpc.HandlerError) + assert result_error.type == handler_type + assert result_error.retryable_override == expected_retryable + assert result_error.message == message + assert result_error.original_failure + assert result_error.original_failure.details + + # modify result_error.original_failure as a way of confirming that it is used + # when encoding the resulting failure + result_error.original_failure.details = { + "nexusHandlerFailureInfo": { + **result_error.original_failure.details["nexusHandlerFailureInfo"], + "type": "TEST TYPE", + } + } + + result_failure = Failure() + await DataConverter.default.encode_failure(result_error, result_failure) + assert result_failure.HasField("nexus_handler_failure_info") + assert result_failure.nexus_handler_failure_info.type == "TEST TYPE" + assert result_failure.message == message + + # Verify retryable behavior mapping + if retryable_override is True: + assert ( + result_failure.nexus_handler_failure_info.retry_behavior + == NexusHandlerErrorRetryBehavior.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_RETRYABLE + ) + elif retryable_override is False: + assert ( + result_failure.nexus_handler_failure_info.retry_behavior + == NexusHandlerErrorRetryBehavior.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_NON_RETRYABLE + ) + else: + assert ( + result_failure.nexus_handler_failure_info.retry_behavior + == NexusHandlerErrorRetryBehavior.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_UNSPECIFIED + ) + + +async def test_nexus_handler_error_with_cause(): + """Test HandlerError with a cause chain is properly converted.""" + # Create a cause chain + root_cause = ValueError("root cause") + middle_cause = RuntimeError("middle cause") + middle_cause.__cause__ = root_cause + + handler_error = nexusrpc.HandlerError( + "handler error message", + type=nexusrpc.HandlerErrorType.INTERNAL, + ) + handler_error.__cause__ = middle_cause + + # Convert to failure + failure = Failure() + await DataConverter.default.encode_failure(handler_error, failure) + + # Verify message and cause chain in failure + assert failure.message == "handler error message" + assert failure.HasField("cause") + assert failure.cause.message == "middle cause" + assert failure.cause.application_failure_info.type == "RuntimeError" + assert failure.cause.HasField("cause") + assert failure.cause.cause.message == "root cause" + assert failure.cause.cause.application_failure_info.type == "ValueError" + + # Convert back + result_error = await DataConverter.default.decode_failure(failure) + + # Verify cause chain with messages (ApplicationError prepends type to message in str()) + assert isinstance(result_error, nexusrpc.HandlerError) + assert str(result_error) == "handler error message" + assert result_error.__cause__ is not None + assert isinstance(result_error.__cause__, ApplicationError) + assert str(result_error.__cause__) == "RuntimeError: middle cause" + assert result_error.__cause__.__cause__ is not None + assert str(result_error.__cause__.__cause__) == "ValueError: root cause" + + +async def test_nexus_handler_error_unknown_type_fallback(): + """Test that unknown HandlerErrorType falls back to INTERNAL during from_failure.""" + # Create a failure with an unknown type + failure = Failure() + failure.message = "unknown type error" + failure.nexus_handler_failure_info.type = "UNKNOWN_TYPE_XYZ" + + # Convert to error + result_error = await DataConverter.default.decode_failure(failure) + + # Should fall back to INTERNAL with message preserved + assert isinstance(result_error, nexusrpc.HandlerError) + assert result_error.type == nexusrpc.HandlerErrorType.INTERNAL + assert str(result_error) == "unknown type error" + + +async def test_nexus_operation_error_round_trip(): + """Test round-trip conversion of NexusOperationError.""" + test_cases = [ + # (scheduled_event_id, endpoint, service, operation, operation_token) + (123, "my-endpoint", "MyService", "myOperation", "token-abc"), + (0, "", "", "", ""), # Empty values + (999, "endpoint-2", "ServiceB", "op2", ""), # Empty token + (1, "e", "s", "o", "very-long-token-" + "x" * 100), + ] + + for scheduled_event_id, endpoint, service, operation, operation_token in test_cases: + message = "nexus operation failed" + original_error = NexusOperationError( + message, + scheduled_event_id=scheduled_event_id, + endpoint=endpoint, + service=service, + operation=operation, + operation_token=operation_token, + ) + + # Convert to failure + failure = Failure() + await DataConverter.default.encode_failure(original_error, failure) + + # Verify failure structure and message + assert failure.message == message + assert failure.HasField("nexus_operation_execution_failure_info") + info = failure.nexus_operation_execution_failure_info + assert info.scheduled_event_id == scheduled_event_id + assert info.endpoint == endpoint + assert info.service == service + assert info.operation == operation + assert info.operation_token == operation_token + + # Convert back + result_error = await DataConverter.default.decode_failure(failure) + + # Verify result including message + assert isinstance(result_error, NexusOperationError) + assert result_error.message == message + assert result_error.scheduled_event_id == scheduled_event_id + assert result_error.endpoint == endpoint + assert result_error.service == service + assert result_error.operation == operation + assert result_error.operation_token == operation_token + + +async def test_nexus_operation_error_with_cause(): + """Test NexusOperationError with a HandlerError as cause.""" + # Create NexusOperationError with HandlerError as cause + cause_error = nexusrpc.HandlerError( + "handler failed", + type=nexusrpc.HandlerErrorType.NOT_FOUND, + ) + + original_error = NexusOperationError( + "nexus operation failed", + scheduled_event_id=42, + endpoint="test-endpoint", + service="TestService", + operation="testOp", + operation_token="token123", + ) + original_error.__cause__ = cause_error + + # Convert to failure + failure = Failure() + await DataConverter.default.encode_failure(original_error, failure) + + # Verify message and cause is present + assert failure.message == "nexus operation failed" + assert failure.HasField("cause") + assert failure.cause.HasField("nexus_handler_failure_info") + assert failure.cause.message == "handler failed" + + # Convert back + result_error = await DataConverter.default.decode_failure(failure) + + # Verify messages preserved + assert isinstance(result_error, NexusOperationError) + assert result_error.message == "nexus operation failed" + assert result_error.__cause__ is not None + assert isinstance(result_error.__cause__, nexusrpc.HandlerError) + assert result_error.__cause__.type == nexusrpc.HandlerErrorType.NOT_FOUND + assert str(result_error.__cause__) == "handler failed" + + class IPv4AddressPayloadConverter(CompositePayloadConverter): def __init__(self) -> None: # Replace default JSON plain with our own that has our type converter @@ -633,13 +1052,23 @@ def default(self, o: Any) -> Any: class IPv4AddressJSONTypeConverter(JSONTypeConverter): def to_typed_value( - self, hint: Type, value: Any - ) -> Union[Optional[Any], _JSONTypeConverterUnhandled]: + self, hint: type, value: Any + ) -> Any | None | JSONTypeConverterUnhandled: if inspect.isclass(hint) and issubclass(hint, ipaddress.IPv4Address): return ipaddress.IPv4Address(value) return JSONTypeConverter.Unhandled +def test_json_type_converter_unhandled_type_public(): + return_type = get_type_hints(JSONTypeConverter.to_typed_value)["return"] + + assert JSONTypeConverterUnhandled.__name__ == "JSONTypeConverterUnhandled" + assert JSONTypeConverterUnhandled in get_args(return_type) + assert JSONTypeConverterUnhandled(JSONTypeConverter.Unhandled) is ( + JSONTypeConverter.Unhandled + ) + + async def test_json_type_converter(): addr = ipaddress.IPv4Address("1.2.3.4") custom_conv = dataclasses.replace( @@ -663,11 +1092,37 @@ async def test_json_type_converter(): await DataConverter.default.decode([payload], [ipaddress.IPv4Address]) with pytest.raises(TypeError): await DataConverter.default.decode( - [list_payload], [List[ipaddress.IPv4Address]] + [list_payload], [list[ipaddress.IPv4Address]] ) # But decodes with custom assert addr == (await custom_conv.decode([payload], [ipaddress.IPv4Address]))[0] assert [addr, addr] == ( - await custom_conv.decode([list_payload], [List[ipaddress.IPv4Address]]) + await custom_conv.decode([list_payload], [list[ipaddress.IPv4Address]]) )[0] + + +def test_value_to_type_literal_key(): + # The type for the dictionary's *key*: + KeyHint = Literal[ + "Key1", + "Key2", + ] + + # The type for the dictionary's *value* (the inner dict): + InnerKeyHint = Literal[ + "Inner1", + "Inner2", + ] + InnerValueHint = str | int | float | None + ValueHint = dict[InnerKeyHint, InnerValueHint] + + # The full type hint for the mapping: + hint_with_bug = dict[KeyHint, ValueHint] + + # A value that uses one of the literal keys: + value_to_convert = {"Key1": {"Inner1": 123.45, "Inner2": 10}} + custom_converters: Sequence[JSONTypeConverter] = [] + + # Function executes without error + value_to_type(hint_with_bug, value_to_convert, custom_converters) diff --git a/tests/test_envconfig.py b/tests/test_envconfig.py index 775c59400..0b44db731 100644 --- a/tests/test_envconfig.py +++ b/tests/test_envconfig.py @@ -1,12 +1,20 @@ import os import textwrap from pathlib import Path +from types import SimpleNamespace +from unittest.mock import AsyncMock import pytest +import temporalio.runtime +import temporalio.service from temporalio.client import Client from temporalio.envconfig import ClientConfig, ClientConfigProfile, ClientConfigTLS from temporalio.service import TLSConfig +from temporalio.testing import WorkflowEnvironment +from tests import conftest + +pytestmark = pytest.mark.requires_local_server # A base TOML config with a default and a custom profile TOML_CONFIG_BASE = textwrap.dedent( @@ -147,6 +155,82 @@ def test_load_profile_from_data_env_overrides(): assert config.get("target_host") == "env-address" +@pytest.mark.parametrize( + ("env_type", "expected"), + [ + ("envconfig", True), + ("local", False), + ("time-skipping", False), + ], +) +def test_envconfig_server_selection( + env_type: str, + expected: bool, +): + assert conftest._uses_envconfig_server(env_type) is expected + + +async def test_envconfig_workflow_environment_uses_client_connect_config( + monkeypatch: pytest.MonkeyPatch, +): + monkeypatch.setenv("TEMPORAL_ADDRESS", "env-address") + monkeypatch.setenv("TEMPORAL_NAMESPACE", "env-namespace") + monkeypatch.setenv("TEMPORAL_API_KEY", "env-api-key") + monkeypatch.setenv("TEMPORAL_TLS", "false") + monkeypatch.setenv("TEMPORAL_GRPC_META_TEST_HEADER", "env-value") + client = object() + environment = object() + connect = AsyncMock(return_value=client) + monkeypatch.setattr(Client, "connect", connect) + monkeypatch.setattr( + WorkflowEnvironment, + "from_client", + lambda actual_client: environment, + ) + + assert await conftest._create_env_from_envconfig() is environment + connect.assert_awaited_once_with( + target_host="env-address", + namespace="env-namespace", + api_key="env-api-key", + tls=False, + rpc_metadata={"test-header": "env-value"}, + ) + + +async def test_workflow_environment_connect_client_inherits_connection_options( + monkeypatch: pytest.MonkeyPatch, +): + runtime = temporalio.runtime.Runtime.default() + source_client = SimpleNamespace( + namespace="env-namespace", + service_client=SimpleNamespace( + config=temporalio.service.ConnectConfig( + target_host="env-address", + api_key="env-api-key", + tls=False, + rpc_metadata={"test-header": "env-value"}, + runtime=runtime, + ) + ), + ) + environment = WorkflowEnvironment(source_client) # type: ignore[arg-type] + connect = AsyncMock(return_value=object()) + monkeypatch.setattr(Client, "connect", connect) + + await environment.connect_client(lazy=True) + + connect.assert_awaited_once_with( + "env-address", + namespace="env-namespace", + api_key="env-api-key", + tls=False, + rpc_metadata={"test-header": "env-value"}, + runtime=runtime, + lazy=True, + ) + + def test_load_profile_env_overrides(base_config_file: Path): """Test that environment variables correctly override file settings.""" env = { @@ -223,10 +307,13 @@ def test_load_profile_api_key_enables_tls(tmp_path: Path): config_file.write_text(config_toml) profile = ClientConfigProfile.load(config_source=config_file) assert profile.api_key == "my-key" - assert profile.tls is not None + # No TLS object should have been created + assert profile.tls is None config = profile.to_client_connect_config() - assert config.get("tls") + # Expect to_client_connect_config call to set TLS to True + # due to presence of api key. + assert config.get("tls") is True assert config.get("api_key") == "my-key" @@ -255,7 +342,7 @@ def test_load_profiles_from_data_all(): assert connect_config.get("target_host") == "custom-address" -def test_load_profiles_no_env_override(tmp_path: Path, monkeypatch): +def test_load_profiles_no_env_override(tmp_path: Path, monkeypatch): # type: ignore[reportMissingParameterType] """Confirm that load_profiles does not apply env overrides.""" config_file = tmp_path / "config.toml" config_file.write_text(TOML_CONFIG_BASE) @@ -285,13 +372,6 @@ def test_load_profiles_discovery(tmp_path: Path, monkeypatch): # type: ignore[r assert "default" in client_config.profiles -def test_load_profiles_disable_file(): - """Test load_profiles with file loading disabled.""" - # With no env vars, should be empty - client_config = ClientConfig.load(disable_file=True, override_env_vars={}) - assert not client_config.profiles - - def test_load_profiles_strict_mode_fail(tmp_path: Path): """Test that strict mode fails on unrecognized keys.""" config_file = tmp_path / "config.toml" @@ -519,7 +599,6 @@ def test_client_config_profile_to_from_dict(): namespace="some-namespace", api_key="some-api-key", tls=ClientConfigTLS( - disabled=False, server_name="some-server-name", server_root_ca_cert=b"ca-cert-data", client_cert=Path("/path/to/client.crt"), @@ -530,7 +609,7 @@ def test_client_config_profile_to_from_dict(): profile_dict = profile.to_dict() - # Check dict representation. Note that disabled=False is not in the dict. + # Check dict representation. Note that disabled=None is not in the dict. expected_dict = { "address": "some-address", "namespace": "some-namespace", @@ -557,7 +636,6 @@ def test_client_config_profile_to_from_dict(): namespace="some-namespace", api_key="some-api-key", tls=ClientConfigTLS( - disabled=False, server_name="some-server-name", server_root_ca_cert="ca-cert-data", # Was bytes, now str client_cert=Path("/path/to/client.crt"), @@ -614,3 +692,424 @@ def test_client_config_to_from_dict(): assert empty_config_dict == {} new_empty_config = ClientConfig.from_dict(empty_config_dict) assert empty_config == new_empty_config + + +def test_grpc_metadata_normalization_from_toml(): + """Test that gRPC metadata keys get normalized from TOML.""" + toml_config = textwrap.dedent( + """ + [profile.default] + address = "localhost:7233" + namespace = "default" + + [profile.default.grpc_meta] + "Custom-Header" = "custom-value" + "ANOTHER_HEADER_KEY" = "another-value" + "mixed_Case-header" = "mixed-value" + """ + ) + + profile = ClientConfigProfile.load(config_source=toml_config) + + # Keys should be normalized: uppercase -> lowercase, underscores -> hyphens + assert profile.grpc_meta["custom-header"] == "custom-value" + assert profile.grpc_meta["another-header-key"] == "another-value" + assert profile.grpc_meta["mixed-case-header"] == "mixed-value" + + # Original case variations should not exist + assert "Custom-Header" not in profile.grpc_meta + assert "ANOTHER_HEADER_KEY" not in profile.grpc_meta + assert "mixed_Case-header" not in profile.grpc_meta + + config = profile.to_client_connect_config() + rpc_metadata = config.get("rpc_metadata") + assert rpc_metadata is not None + assert rpc_metadata["custom-header"] == "custom-value" + assert rpc_metadata["another-header-key"] == "another-value" + + +def test_grpc_metadata_deletion_via_empty_env_value(base_config_file: Path): + """Test that empty environment variable values delete existing gRPC metadata.""" + env = { + # Empty value should remove the header + "TEMPORAL_GRPC_META_CUSTOM_HEADER": "", + # Non-empty value should set the header + "TEMPORAL_GRPC_META_NEW_HEADER": "new-value", + } + profile = ClientConfigProfile.load( + config_source=base_config_file, profile="custom", override_env_vars=env + ) + + # custom-header should be removed by empty env value + assert "custom-header" not in profile.grpc_meta + # new-header should be added + assert profile.grpc_meta["new-header"] == "new-value" + + config = profile.to_client_connect_config() + rpc_metadata = config.get("rpc_metadata") + if rpc_metadata: + assert "custom-header" not in rpc_metadata + assert rpc_metadata["new-header"] == "new-value" + + +def test_default_profile_not_found_returns_empty_profile(): + """Test that requesting missing 'default' profile returns empty profile instead of error.""" + toml_config = textwrap.dedent( + """ + [profile.existing] + address = "my-address" + """ + ) + profile = ClientConfigProfile.load(config_source=toml_config) + assert profile.address is None + assert profile.namespace is None + assert profile.api_key is None + assert not profile.grpc_meta + assert profile.tls is None + + +def test_tls_conflict_across_sources_path_in_toml_data_in_env(): + """Test error when cert path in TOML conflicts with cert data in env var.""" + toml_config = textwrap.dedent( + """ + [profile.default] + address = "localhost:7233" + [profile.default.tls] + client_cert_path = "/path/to/cert" + """ + ) + + env = {"TEMPORAL_TLS_CLIENT_CERT_DATA": "cert-data-from-env"} + + with pytest.raises( + RuntimeError, + match="Cannot specify cert data via TEMPORAL_TLS_CLIENT_CERT_DATA when cert path is already specified", + ): + ClientConfigProfile.load(config_source=toml_config, override_env_vars=env) + + +def test_tls_conflict_across_sources_data_in_toml_path_in_env(): + """Test error when cert data in TOML conflicts with cert path in env var.""" + toml_config = textwrap.dedent( + """ + [profile.default] + address = "localhost:7233" + [profile.default.tls] + client_cert_data = "cert-data-from-toml" + """ + ) + + env = {"TEMPORAL_TLS_CLIENT_CERT_PATH": "/path/from/env"} + + with pytest.raises( + RuntimeError, + match="Cannot specify cert path via TEMPORAL_TLS_CLIENT_CERT_PATH when cert data is already specified", + ): + ClientConfigProfile.load(config_source=toml_config, override_env_vars=env) + + +def test_load_client_connect_options_convenience_api(base_config_file: Path): + """Test the convenience API for loading client connect configuration.""" + # Test default profile with file + config = ClientConfig.load_client_connect_config(config_file=str(base_config_file)) + assert config.get("target_host") == "default-address" + assert config.get("namespace") == "default-namespace" + + # Test with environment overrides + env = {"TEMPORAL_NAMESPACE": "env-override-namespace"} + config_with_env = ClientConfig.load_client_connect_config( + config_file=str(base_config_file), override_env_vars=env + ) + assert config_with_env.get("target_host") == "default-address" + assert config_with_env.get("namespace") == "env-override-namespace" + + # Test with specific profile + config_custom = ClientConfig.load_client_connect_config( + profile="custom", config_file=str(base_config_file) + ) + assert config_custom.get("target_host") == "custom-address" + assert config_custom.get("namespace") == "custom-namespace" + assert config_custom.get("api_key") == "custom-api-key" + + +def test_load_client_connect_options_e2e_validation(): + """Test comprehensive end-to-end configuration loading with all features.""" + toml_content = textwrap.dedent( + """ + [profile.production] + address = "prod.temporal.com:443" + namespace = "production-ns" + api_key = "prod-api-key" + + [profile.production.tls] + server_name = "prod.temporal.com" + server_ca_cert_data = "prod-ca-cert" + + [profile.production.grpc_meta] + authorization = "Bearer prod-token" + "x-custom-header" = "prod-value" + """ + ) + + env_overrides = { + "TEMPORAL_GRPC_META_X_ENVIRONMENT": "production", + "TEMPORAL_TLS_SERVER_NAME": "override.temporal.com", + } + + config = ClientConfig.load_client_connect_config( + profile="production", + config_file=None, # Use config_source directly + override_env_vars=env_overrides, + disable_file=True, # Load from config_source instead + ) + + # First load the profile to get the raw config, then convert + profile = ClientConfigProfile.load( + profile="production", + config_source=toml_content, + override_env_vars=env_overrides, + ) + config = profile.to_client_connect_config() + + # Validate all configuration aspects + assert config.get("target_host") == "prod.temporal.com:443" + assert config.get("namespace") == "production-ns" + assert config.get("api_key") == "prod-api-key" + + # TLS configuration (API key should auto-enable TLS) + assert config.get("tls") is not None + tls_config = config.get("tls") + assert isinstance(tls_config, TLSConfig) + assert tls_config.domain == "override.temporal.com" # Env override + assert tls_config.server_root_ca_cert == b"prod-ca-cert" + + # gRPC metadata with normalization and env overrides + assert config.get("rpc_metadata") is not None + rpc_metadata = config.get("rpc_metadata") + assert rpc_metadata is not None + assert rpc_metadata["authorization"] == "Bearer prod-token" + assert rpc_metadata["x-custom-header"] == "prod-value" + assert rpc_metadata["x-environment"] == "production" # From env + + +async def test_e2e_basic_development_profile_client_connection(client: Client): + """Test basic development profile with actual client connection.""" + # Get connection details from the fixture client + target_host = client.service_client.config.target_host + namespace = client.namespace + + toml_content = textwrap.dedent( + f""" + [profile.development] + address = "{target_host}" + namespace = "{namespace}" + + [profile.development.grpc_meta] + "x-test-source" = "envconfig-python-dev" + """ + ) + + profile = ClientConfigProfile.load( + profile="development", config_source=toml_content + ) + + config = profile.to_client_connect_config() + + # Create actual Temporal client using envconfig + new_client = await Client.connect(**config) + + # Verify client configuration matches envconfig + assert new_client.service_client.config.target_host == target_host + assert new_client.namespace == namespace + if new_client.service_client.config.rpc_metadata: + assert ( + new_client.service_client.config.rpc_metadata["x-test-source"] + == "envconfig-python-dev" + ) + + +async def test_e2e_production_tls_api_key_client_connection(client: Client): + """Test production profile with TLS and API key with actual client connection.""" + # Get connection details from the fixture client + target_host = client.service_client.config.target_host + + toml_content = textwrap.dedent( + f""" + [profile.production] + address = "{target_host}" + namespace = "production-namespace" + api_key = "prod-api-key-123" + + [profile.production.tls] + disabled = true + + [profile.production.grpc_meta] + authorization = "Bearer prod-token" + "x-environment" = "production" + """ + ) + + profile = ClientConfigProfile.load(profile="production", config_source=toml_content) + + config = profile.to_client_connect_config() + + # Create TLS-enabled client with API key + new_client = await Client.connect(**config) + + # Verify production configuration + assert new_client.service_client.config.target_host == target_host + assert new_client.namespace == "production-namespace" + assert new_client.service_client.config.api_key == "prod-api-key-123" + if new_client.service_client.config.rpc_metadata: + assert ( + new_client.service_client.config.rpc_metadata["authorization"] + == "Bearer prod-token" + ) + assert ( + new_client.service_client.config.rpc_metadata["x-environment"] + == "production" + ) + + +async def test_e2e_environment_overrides_client_connection(client: Client): + """Test environment overrides with actual client connection.""" + # Get connection details from the fixture client + target_host = client.service_client.config.target_host + + toml_content = textwrap.dedent( + """ + [profile.staging] + address = "staging.temporal.com:443" + namespace = "staging-namespace" + + [profile.staging.grpc_meta] + "x-deployment" = "staging" + authorization = "Bearer staging-token" + """ + ) + + env_overrides = { + "TEMPORAL_ADDRESS": target_host, + "TEMPORAL_NAMESPACE": "override-namespace", + "TEMPORAL_GRPC_META_X_DEPLOYMENT": "canary", + "TEMPORAL_GRPC_META_AUTHORIZATION": "Bearer override-token", + } + + profile = ClientConfigProfile.load( + profile="staging", config_source=toml_content, override_env_vars=env_overrides + ) + + config = profile.to_client_connect_config() + + # Create client with environment overrides + new_client = await Client.connect(**config) + + # Verify environment overrides took effect + assert new_client.service_client.config.target_host == target_host + assert new_client.namespace == "override-namespace" + if new_client.service_client.config.rpc_metadata: + assert new_client.service_client.config.rpc_metadata["x-deployment"] == "canary" + assert ( + new_client.service_client.config.rpc_metadata["authorization"] + == "Bearer override-token" + ) + + +def test_tls_disabled_tri_state_behavior(): + """Test TLS disabled tri-state behavior: null (unset), false (enabled), true (disabled).""" + # Test 1: disabled=null (unset) with API key -> TLS enabled + toml_null = textwrap.dedent( + """ + [profile.default] + address = "my-address" + api_key = "my-api-key" + [profile.default.tls] + server_name = "my-server" + """ + ) + profile_null = ClientConfigProfile.load(config_source=toml_null) + assert profile_null.tls is not None + assert profile_null.tls.disabled is None # disabled is null (unset) + config_null = profile_null.to_client_connect_config() + assert config_null.get("tls") is not None # TLS enabled + + # Test 2: disabled=false (explicitly enabled) -> TLS enabled + toml_false = textwrap.dedent( + """ + [profile.default] + address = "my-address" + [profile.default.tls] + disabled = false + server_name = "my-server" + """ + ) + profile_false = ClientConfigProfile.load(config_source=toml_false) + assert profile_false.tls is not None + assert profile_false.tls.disabled is False # explicitly disabled=false + config_false = profile_false.to_client_connect_config() + assert config_false.get("tls") is not None # TLS enabled + + # Test 3: disabled=true (explicitly disabled) -> TLS disabled even with API key + toml_true = textwrap.dedent( + """ + [profile.default] + address = "my-address" + api_key = "my-api-key" + [profile.default.tls] + disabled = true + server_name = "should-be-ignored" + """ + ) + profile_true = ClientConfigProfile.load(config_source=toml_true) + assert profile_true.tls is not None + assert profile_true.tls.disabled is True # explicitly disabled=true + config_true = profile_true.to_client_connect_config() + assert config_true.get("tls") is False # TLS disabled even with API key + + +async def test_e2e_multi_profile_different_client_connections(client: Client): + """Test multiple profiles creating different client connections.""" + # Get connection details from the fixture client + target_host = client.service_client.config.target_host + + toml_content = textwrap.dedent( + f""" + [profile.development] + address = "{target_host}" + namespace = "dev" + + [profile.production] + address = "{target_host}" + namespace = "prod" + api_key = "prod-key" + + [profile.production.tls] + disabled = true + """ + ) + + # Load and create development client + dev_profile = ClientConfigProfile.load( + profile="development", config_source=toml_content + ) + + dev_config = dev_profile.to_client_connect_config() + dev_client = await Client.connect(**dev_config) + + # Load and create production client + prod_profile = ClientConfigProfile.load( + profile="production", config_source=toml_content + ) + + prod_config = prod_profile.to_client_connect_config() + prod_client = await Client.connect(**prod_config) + + # Verify different configurations for each client + assert dev_client.service_client.config.target_host == target_host + assert dev_client.namespace == "dev" + assert dev_client.service_client.config.api_key is None + assert dev_client.service_client.config.tls is None + + assert prod_client.service_client.config.target_host == target_host + assert prod_client.namespace == "prod" + assert prod_client.service_client.config.api_key == "prod-key" diff --git a/tests/test_extstore.py b/tests/test_extstore.py new file mode 100644 index 000000000..9a058c582 --- /dev/null +++ b/tests/test_extstore.py @@ -0,0 +1,805 @@ +"""Tests for external storage functionality.""" + +import asyncio +from collections.abc import Sequence + +import pytest + +from temporalio.api.common.v1 import Payload +from temporalio.api.sdk.v1.external_storage_pb2 import ExternalStorageReference +from temporalio.converter import ( + DataConverter, + ExternalStorage, + JSONPlainPayloadConverter, + PayloadCodec, + StorageDriver, + StorageDriverClaim, + StorageDriverRetrieveContext, + StorageDriverStoreContext, +) +from temporalio.converter._extstore import _REFERENCE_ENCODING, _StorageReference +from temporalio.converter._payload_converter import JSONProtoPayloadConverter +from temporalio.exceptions import ApplicationError + +_legacy_ref_converter = JSONPlainPayloadConverter(encoding=_REFERENCE_ENCODING.decode()) + + +def _make_legacy_payload( + driver_name: str, claim_data: dict[str, str], size_bytes: int +) -> Payload: + """Build a reference payload in the legacy ``json/external-storage-reference`` format.""" + ref = _StorageReference( + driver_name=driver_name, + driver_claim=StorageDriverClaim(claim_data=claim_data), + ) + payload = _legacy_ref_converter.to_payload(ref) + assert payload is not None + payload.external_payloads.add().size_bytes = size_bytes + return payload + + +class InMemoryTestDriver(StorageDriver): + """In-memory storage driver for testing.""" + + def __init__( + self, + driver_name: str = "test-driver", + ): + self._driver_name = driver_name + self._storage: dict[str, bytes] = {} + self._store_calls = 0 + self._retrieve_calls = 0 + + def name(self) -> str: + return self._driver_name + + async def store( + self, + context: StorageDriverStoreContext, + payloads: Sequence[Payload], + ) -> list[StorageDriverClaim]: + self._store_calls += 1 + start_index = len(self._storage) + + entries = [ + (f"payload-{start_index + i}", payload.SerializeToString()) + for i, payload in enumerate(payloads) + ] + self._storage.update(entries) + + # Small delay to ensure measurable duration even on low-resolution timers. + await asyncio.sleep(0.02) + + return [StorageDriverClaim(claim_data={"key": key}) for key, _ in entries] + + async def retrieve( + self, + context: StorageDriverRetrieveContext, + claims: Sequence[StorageDriverClaim], + ) -> list[Payload]: + self._retrieve_calls += 1 + + def parse_claim( + claim: StorageDriverClaim, + ) -> Payload: + key = claim.claim_data["key"] + if key not in self._storage: + raise ApplicationError( + f"Payload not found for key '{key}'", non_retryable=True + ) + payload = Payload() + payload.ParseFromString(self._storage[key]) + return payload + + # Small delay to ensure measurable duration even on low-resolution timers. + await asyncio.sleep(0.02) + + return [parse_claim(claim) for claim in claims] + + +class TestDataConverterExternalStorage: + """Tests for DataConverter with external storage.""" + + async def test_extstore_encode_decode(self): + """Test that large payloads are stored externally.""" + driver = InMemoryTestDriver() + + # Configure with 100-byte threshold + converter = DataConverter( + external_storage=ExternalStorage( + drivers=[driver], + payload_size_threshold=100, + ) + ) + + # Small value should not be externalized + small_value = "small" + encoded_small = await converter.encode([small_value]) + assert len(encoded_small) == 1 + assert not encoded_small[0].external_payloads # Not externalized + assert driver._store_calls == 0 + + # Large value should be externalized + large_value = "x" * 200 + encoded_large = await converter.encode([large_value]) + assert len(encoded_large) == 1 + assert len(encoded_large[0].external_payloads) > 0 # Externalized + assert driver._store_calls == 1 + + # Decode large value + decoded = await converter.decode(encoded_large, [str]) + assert len(decoded) == 1 + assert decoded[0] == large_value + assert driver._retrieve_calls == 1 + + async def test_extstore_reference_structure(self): + """Externalized payloads are written as ExternalStorageReference proto (json/protobuf encoding).""" + converter = DataConverter( + external_storage=ExternalStorage( + drivers=[InMemoryTestDriver("test-driver")], + payload_size_threshold=50, + ) + ) + + large_value = "x" * 100 + encoded = await converter.encode([large_value]) + + reference_payload = encoded[0] + assert len(reference_payload.external_payloads) > 0 + assert reference_payload.metadata.get("encoding") == b"json/protobuf" + + reference = JSONProtoPayloadConverter().from_payload( + reference_payload, ExternalStorageReference + ) + assert isinstance(reference, ExternalStorageReference) + assert reference.driver_name == "test-driver" + assert "key" in reference.claim_data + + async def test_extstore_composite_conditional(self): + """Test using multiple drivers based on size.""" + hot_driver = InMemoryTestDriver("hot-storage") + cold_driver = InMemoryTestDriver("cold-storage") + + options = ExternalStorage( + drivers=[hot_driver, cold_driver], + driver_selector=lambda context, payload: ( + hot_driver if payload.ByteSize() < 500 else cold_driver + ), + payload_size_threshold=100, + ) + converter = DataConverter(external_storage=options) + + # Small payload (not externalized) + small = "x" * 50 + encoded_small = await converter.encode([small]) + assert not encoded_small[0].external_payloads + assert hot_driver._store_calls == 0 + assert cold_driver._store_calls == 0 + + # Medium payload (hot storage) + medium = "x" * 200 + encoded_medium = await converter.encode([medium]) + assert len(encoded_medium[0].external_payloads) > 0 + assert hot_driver._store_calls == 1 + assert cold_driver._store_calls == 0 + + # Large payload (cold storage) + large = "x" * 2000 + encoded_large = await converter.encode([large]) + assert len(encoded_large[0].external_payloads) > 0 + assert hot_driver._store_calls == 1 # Unchanged + assert cold_driver._store_calls == 1 + + # Verify retrieval from correct drivers + decoded_medium = await converter.decode(encoded_medium, [str]) + assert decoded_medium[0] == medium + assert hot_driver._retrieve_calls == 1 + + decoded_large = await converter.decode(encoded_large, [str]) + assert decoded_large[0] == large + assert cold_driver._retrieve_calls == 1 + + +class TestDriverError: + """Tests for ValueError raised when a driver violates its contract.""" + + async def test_encode_wrong_claim_count_raises_runtime_error(self): + """store() returning fewer claims than payloads must raise ValueError.""" + + class _NoClaimsDriver(InMemoryTestDriver): + async def store( + self, context: StorageDriverStoreContext, payloads: Sequence[Payload] + ) -> list[StorageDriverClaim]: + return [] + + driver = _NoClaimsDriver() + converter = DataConverter( + external_storage=ExternalStorage( + drivers=[driver], + payload_size_threshold=10, + ) + ) + with pytest.raises( + ValueError, + match=f"Driver '{driver.name()}' returned 0 claims, expected 1", + ): + await converter.encode(["x" * 200]) + + async def test_decode_wrong_payload_count_raises_runtime_error(self): + """retrieve() returning fewer payloads than claims must raise ValueError.""" + good_converter = DataConverter( + external_storage=ExternalStorage( + drivers=[InMemoryTestDriver()], + payload_size_threshold=10, + ) + ) + encoded = await good_converter.encode(["x" * 200]) + + class _NoPayloadsDriver(InMemoryTestDriver): + async def retrieve( + self, + context: StorageDriverRetrieveContext, + claims: Sequence[StorageDriverClaim], + ) -> list[Payload]: + return [] + + driver = _NoPayloadsDriver() + bad_converter = DataConverter( + external_storage=ExternalStorage( + drivers=[driver], + payload_size_threshold=10, + ) + ) + with pytest.raises( + ValueError, + match=f"Driver '{driver.name()}' returned 0 payloads, expected 1", + ): + await bad_converter.decode(encoded, [str]) + + async def test_store_cancels_in_flight_driver_on_error(self): + """When one driver raises during concurrent store, other in-flight drivers are cancelled.""" + store_cancelled = asyncio.Event() + + class _SleepingStoreDriver(InMemoryTestDriver): + def __init__(self): + super().__init__("sleeping") + + async def store( + self, + context: StorageDriverStoreContext, + payloads: Sequence[Payload], + ) -> list[StorageDriverClaim]: + try: + await asyncio.sleep(float("inf")) + except asyncio.CancelledError: + store_cancelled.set() + raise + return [] # unreachable + + class _FailingStoreDriver(InMemoryTestDriver): + def __init__(self): + super().__init__("failing") + + async def store( + self, + context: StorageDriverStoreContext, + payloads: Sequence[Payload], + ) -> list[StorageDriverClaim]: + raise ValueError( + "failed to store payloads because remote service is unavailable" + ) + + drivers = [_SleepingStoreDriver(), _FailingStoreDriver()] + drivers_iter = iter(drivers) + converter = DataConverter( + external_storage=ExternalStorage( + drivers=drivers, + driver_selector=lambda ctx, p: next(drivers_iter), + payload_size_threshold=0, + ) + ) + + with pytest.raises( + ValueError, + match="^failed to store payloads because remote service is unavailable$", + ): + await converter.encode(["payload_a", "payload_b"]) + + assert store_cancelled.is_set() + + async def test_retrieve_cancels_in_flight_driver_on_error(self): + """When one driver raises during concurrent retrieve, other in-flight drivers are cancelled.""" + retrieve_cancelled = asyncio.Event() + + class _SleepingRetrieveDriver(InMemoryTestDriver): + def __init__(self): + super().__init__("sleeping") + + async def retrieve( + self, + context: StorageDriverRetrieveContext, + claims: Sequence[StorageDriverClaim], + ) -> list[Payload]: + try: + await asyncio.sleep(float("inf")) + except asyncio.CancelledError: + retrieve_cancelled.set() + raise + return [] # unreachable + + class _FailingRetrieveDriver(InMemoryTestDriver): + def __init__(self): + super().__init__("failing") + + async def retrieve( + self, + context: StorageDriverRetrieveContext, + claims: Sequence[StorageDriverClaim], + ) -> list[Payload]: + raise ValueError( + "failed to retrieve a payload because the object key does not exist" + ) + + drivers: list[StorageDriver] = [ + _SleepingRetrieveDriver(), + _FailingRetrieveDriver(), + ] + drivers_iter = iter(drivers) + converter = DataConverter( + external_storage=ExternalStorage( + drivers=drivers, + driver_selector=lambda ctx, p: next(drivers_iter), + payload_size_threshold=0, + ) + ) + encoded = await converter.encode(["payload_a", "payload_b"]) + + with pytest.raises( + ValueError, + match="^failed to retrieve a payload because the object key does not exist$", + ): + await converter.decode(encoded, [str, str]) + + assert retrieve_cancelled.is_set() + + +class RecordingPayloadCodec(PayloadCodec): + """Codec that wraps each payload under a recognisable ``encoding`` label. + + Encode sets ``metadata["encoding"]`` to ``encoding_label`` and stores the + serialised inner payload as ``data``. Decode reverses that. The call + counters let tests assert exactly how many payloads each codec processed. + """ + + def __init__(self, encoding_label: str) -> None: + self._encoding_label = encoding_label.encode() + self.encoded_count = 0 + self.decoded_count = 0 + + async def encode(self, payloads: Sequence[Payload]) -> list[Payload]: + self.encoded_count += len(payloads) + results = [] + for p in payloads: + wrapped = Payload() + wrapped.metadata["encoding"] = self._encoding_label + wrapped.data = p.SerializeToString() + results.append(wrapped) + return results + + async def decode(self, payloads: Sequence[Payload]) -> list[Payload]: + self.decoded_count += len(payloads) + results = [] + for p in payloads: + inner = Payload() + inner.ParseFromString(p.data) + results.append(inner) + return results + + +class TestPayloadCodecWithExternalStorage: + """Tests for interaction between DataConverter.payload_codec and external storage.""" + + async def test_dc_payload_codec_encodes_stored_bytes(self): + """DataConverter.payload_codec encodes the bytes handed to the driver + for storage. The reference payload written to workflow history is NOT + encoded by the DataConverter codec.""" + driver = InMemoryTestDriver() + dc_codec = RecordingPayloadCodec("binary/dc-encoded") + + converter = DataConverter( + payload_codec=dc_codec, + external_storage=ExternalStorage( + drivers=[driver], + payload_size_threshold=50, + ), + ) + + large_value = "x" * 200 + encoded = await converter.encode([large_value]) + assert len(encoded) == 1 + assert driver._store_calls == 1 + + # The reference payload written to history must NOT carry the dc_codec label. + assert dc_codec.encoded_count == 1 + assert encoded[0].metadata.get("encoding") != b"binary/dc-encoded" + + # The bytes given to the driver must carry the dc_codec label. + stored_payload = Payload() + stored_payload.ParseFromString(next(iter(driver._storage.values()))) + assert stored_payload.metadata.get("encoding") == b"binary/dc-encoded" + + # Round-trip must recover the original value. + decoded = await converter.decode(encoded, [str]) + assert decoded[0] == large_value + assert dc_codec.decoded_count == 1 + assert driver._retrieve_calls == 1 + + async def test_dc_payload_codec_does_not_encode_reference_payload(self): + """The reference payload stored in workflow history is NOT encoded by + DataConverter.payload_codec – encoding is applied to the stored bytes + instead.""" + driver = InMemoryTestDriver() + dc_codec = RecordingPayloadCodec("binary/dc-encoded") + + converter = DataConverter( + payload_codec=dc_codec, + external_storage=ExternalStorage( + drivers=[driver], + payload_size_threshold=50, + ), + ) + + large_value = "x" * 200 + encoded = await converter.encode([large_value]) + assert len(encoded) == 1 + assert driver._store_calls == 1 + + # Reference payload in history is NOT encoded by DataConverter.payload_codec. + assert dc_codec.encoded_count == 1 + assert encoded[0].metadata.get("encoding") != b"binary/dc-encoded" + + # Stored bytes ARE encoded by DataConverter.payload_codec. + stored_payload = Payload() + stored_payload.ParseFromString(next(iter(driver._storage.values()))) + assert stored_payload.metadata.get("encoding") == b"binary/dc-encoded" + + # Round-trip. + decoded = await converter.decode(encoded, [str]) + assert decoded[0] == large_value + assert dc_codec.decoded_count == 1 + assert driver._retrieve_calls == 1 + + +class TestMultiDriver: + """Tests for ExternalStorage with multiple drivers.""" + + async def test_selector_always_first_driver_handles_all_stores(self): + """A selector that always picks the first driver routes all store + operations there. The second driver is never called for store.""" + first = InMemoryTestDriver("driver-first") + second = InMemoryTestDriver("driver-second") + + converter = DataConverter( + external_storage=ExternalStorage( + drivers=[first, second], + driver_selector=lambda _ctx, _p: first, + payload_size_threshold=50, + ) + ) + + large = "x" * 200 + encoded = await converter.encode([large]) + + assert first._store_calls == 1 + assert second._store_calls == 0 + + # The reference in history names the first driver. + ref = JSONProtoPayloadConverter().from_payload( + encoded[0], ExternalStorageReference + ) + assert isinstance(ref, ExternalStorageReference) + assert ref.driver_name == "driver-first" + + # Retrieval also goes to the first driver. + decoded = await converter.decode(encoded, [str]) + assert decoded[0] == large + assert first._retrieve_calls == 1 + assert second._retrieve_calls == 0 + + async def test_no_selector_second_driver_is_retrieve_only(self): + """A driver that is second in the list acts as a retrieve-only driver. + References are resolved by name, not by position, so a payload stored + by driver-b is retrieved correctly even when driver-a is listed first.""" + driver_a = InMemoryTestDriver("driver-a") + driver_b = InMemoryTestDriver("driver-b") + + # Store with driver-b as the sole driver. + store_converter = DataConverter( + external_storage=ExternalStorage( + drivers=[driver_b], + payload_size_threshold=50, + ) + ) + large = "y" * 200 + encoded = await store_converter.encode([large]) + + # Retrieve with driver-a listed first, driver-b second. + # The "driver-b" name in the reference must route to driver-b. + retrieve_converter = DataConverter( + external_storage=ExternalStorage( + drivers=[driver_a, driver_b], + driver_selector=lambda _ctx, _p: driver_a, + payload_size_threshold=50, + ) + ) + decoded = await retrieve_converter.decode(encoded, [str]) + assert decoded[0] == large + assert driver_a._retrieve_calls == 0 # never consulted + assert driver_b._retrieve_calls == 1 + + async def test_selector_routes_payloads_to_different_drivers_in_single_batch(self): + """When a selector routes different payloads to different drivers, a + single encode([v1, v2, ...]) call batches payloads per driver so each + driver receives exactly one store() call regardless of how many + payloads are routed to it.""" + driver_a = InMemoryTestDriver("driver-a") + driver_b = InMemoryTestDriver("driver-b") + + # Route payloads that serialise to < 500 bytes to driver_a, larger ones + # to driver_b. + def selector(_ctx: object, payload: Payload) -> StorageDriver: + return driver_a if payload.ByteSize() < 500 else driver_b + + converter = DataConverter( + external_storage=ExternalStorage( + drivers=[driver_a, driver_b], + driver_selector=selector, + payload_size_threshold=50, + ) + ) + + small_ext = "a" * 100 # above threshold, serialises well below 500 B + large_ext = "b" * 1000 # serialises above 500 B + + # Encode both values in a single call — they should be batched per driver. + encoded = await converter.encode([small_ext, large_ext]) + assert driver_a._store_calls == 1 # one batched call, not two individual ones + assert driver_b._store_calls == 1 + + # Full round-trip. + decoded = await converter.decode(encoded, [str, str]) + assert decoded == [small_ext, large_ext] + assert driver_a._retrieve_calls == 1 + assert driver_b._retrieve_calls == 1 + + async def test_selector_returning_none_keeps_payload_inline(self): + """A selector that returns None for a payload leaves it stored inline + in workflow history rather than offloading it to any driver, even when + the payload exceeds the size threshold.""" + driver = InMemoryTestDriver("driver-a") + + converter = DataConverter( + external_storage=ExternalStorage( + drivers=[driver], + driver_selector=lambda _ctx, _payload: None, + payload_size_threshold=50, + ) + ) + + large = "x" * 200 + encoded = await converter.encode([large]) + + assert driver._store_calls == 0 + assert len(encoded[0].external_payloads) == 0 # payload is inline + + decoded = await converter.decode(encoded, [str]) + assert decoded[0] == large + assert driver._retrieve_calls == 0 + + async def test_selector_returns_unregistered_driver_raises(self): + """A selector that returns a driver instance not present in + ExternalStorage.drivers raises ValueError during encode.""" + registered = InMemoryTestDriver("registered") + unregistered = InMemoryTestDriver("unregistered") + + converter = DataConverter( + external_storage=ExternalStorage( + drivers=[registered], + driver_selector=lambda _ctx, _payload: unregistered, + payload_size_threshold=50, + ) + ) + + with pytest.raises(ValueError): + await converter.encode(["x" * 200]) + + async def test_selector_dispatches_drivers_concurrently(self): + started_a = asyncio.Event() + started_b = asyncio.Event() + + class BarrierDriver(InMemoryTestDriver): + def __init__( + self, name: str, my_event: asyncio.Event, their_event: asyncio.Event + ): + super().__init__(name) + self._my_event = my_event + self._their_event = their_event + + async def store( + self, + context: StorageDriverStoreContext, + payloads: Sequence[Payload], + ) -> list[StorageDriverClaim]: + self._my_event.set() + await asyncio.wait_for(self._their_event.wait(), timeout=2.0) + return await super().store(context, payloads) + + driver_a = BarrierDriver("driver-a", started_a, started_b) + driver_b = BarrierDriver("driver-b", started_b, started_a) + + def selector(_ctx: object, payload: Payload) -> StorageDriver: + return driver_a if payload.ByteSize() < 500 else driver_b + + converter = DataConverter( + external_storage=ExternalStorage( + drivers=[driver_a, driver_b], + driver_selector=selector, + payload_size_threshold=0, + ) + ) + + small_ext = "a" * 100 # routes to driver-a + large_ext = "b" * 1000 # routes to driver-b + + # This will deadlock (and timeout) if the two store() calls are not + # dispatched concurrently. + encoded = await asyncio.wait_for( + converter.encode([small_ext, large_ext]), timeout=5.0 + ) + + decoded = await converter.decode(encoded, [str, str]) + assert decoded == [small_ext, large_ext] + + def test_multiple_drivers_without_selector_raises(self): + """Registering more than one driver without a driver_selector raises + ValueError immediately when constructing ExternalStorage.""" + first = InMemoryTestDriver("driver-a") + second = InMemoryTestDriver("driver-b") + + with pytest.raises( + ValueError, + match=r"^ExternalStorage\.driver_selector must be specified if multiple drivers are registered\.$", + ): + ExternalStorage( + drivers=[first, second], + payload_size_threshold=50, + ) + + def test_duplicate_driver_names_raises(self): + """Registering two drivers with identical names raises ValueError immediately + when constructing ExternalStorage.""" + first = InMemoryTestDriver("dup-name") + duplicate = InMemoryTestDriver("dup-name") + + with pytest.raises( + ValueError, + match=r"^ExternalStorage\.drivers contains multiple drivers with name 'dup-name'\. Each driver must have a unique name\.$", + ): + ExternalStorage( + drivers=[first, duplicate], + driver_selector=lambda _ctx, _p: first, + payload_size_threshold=50, + ) + + @pytest.mark.parametrize("threshold", [-1, -1000]) + def test_negative_payload_size_threshold_raises(self, threshold: int): + """A negative payload_size_threshold raises ValueError immediately + when constructing ExternalStorage.""" + driver = InMemoryTestDriver() + + with pytest.raises( + ValueError, + match=r"^ExternalStorage\.payload_size_threshold must be greater than or equal to zero\.$", + ): + ExternalStorage( + drivers=[driver], + payload_size_threshold=threshold, + ) + + +class TestBackwardCompat: + """Tests that the retrieval path handles the legacy ``json/external-storage-reference`` + format for in-flight workflows written before the ExternalStorageReference proto.""" + + async def test_legacy_format_single_payload_decode(self): + """A single payload in the legacy reference format is retrieved correctly.""" + driver = InMemoryTestDriver() + + inner_payload = (await DataConverter().encode(["x" * 200]))[0] + stored_key = "payload-0" + driver._storage[stored_key] = inner_payload.SerializeToString() + + legacy_payload = _make_legacy_payload( + driver_name=driver.name(), + claim_data={"key": stored_key}, + size_bytes=inner_payload.ByteSize(), + ) + + converter = DataConverter( + external_storage=ExternalStorage( + drivers=[driver], + payload_size_threshold=100, + ) + ) + decoded = await converter.decode([legacy_payload], [str]) + assert decoded[0] == "x" * 200 + assert driver._retrieve_calls == 1 + + async def test_legacy_and_new_format_mixed_batch_decode(self): + """A batch containing legacy-format, new proto-format, and inline payloads + all decode correctly in a single call.""" + driver = InMemoryTestDriver() + converter = DataConverter( + external_storage=ExternalStorage( + drivers=[driver], + payload_size_threshold=50, + ) + ) + + new_value = "new-format-value" * 20 + inline_value = "small" + encoded = await converter.encode([new_value, inline_value]) + new_format_payload = encoded[0] + inline_payload = encoded[1] + assert driver._store_calls == 1 + + legacy_value = "legacy-format-value" * 20 + legacy_inner = (await DataConverter().encode([legacy_value]))[0] + stored_key = f"payload-{len(driver._storage)}" + driver._storage[stored_key] = legacy_inner.SerializeToString() + legacy_payload = _make_legacy_payload( + driver_name=driver.name(), + claim_data={"key": stored_key}, + size_bytes=legacy_inner.ByteSize(), + ) + + decoded = await converter.decode( + [legacy_payload, new_format_payload, inline_payload], [str, str, str] + ) + assert decoded[0] == legacy_value + assert decoded[1] == new_value + assert decoded[2] == inline_value + # Both external payloads share the same driver and are batched into one retrieve call. + assert driver._retrieve_calls == 1 + + async def test_new_format_encode_round_trips(self): + """Payloads written with the new ExternalStorageReference format round-trip + correctly and carry the expected proto encoding.""" + driver = InMemoryTestDriver() + converter = DataConverter( + external_storage=ExternalStorage( + drivers=[driver], + payload_size_threshold=50, + ) + ) + + value = "round-trip-value" * 20 + encoded = await converter.encode([value]) + ref_payload = encoded[0] + + assert ref_payload.metadata.get("encoding") == b"json/protobuf" + assert len(ref_payload.external_payloads) > 0 + + ref = JSONProtoPayloadConverter().from_payload( + ref_payload, ExternalStorageReference + ) + assert isinstance(ref, ExternalStorageReference) + assert ref.driver_name == driver.name() + assert "key" in ref.claim_data + + decoded = await converter.decode(encoded, [str]) + assert decoded[0] == value + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/tests/test_plugins.py b/tests/test_plugins.py index eb08bba2d..9414e8df0 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -1,16 +1,22 @@ import dataclasses import uuid import warnings +from collections.abc import AsyncIterator, Awaitable, Callable from contextlib import AbstractAsyncContextManager, asynccontextmanager -from typing import AsyncIterator, cast +from typing import cast import pytest +import temporalio.bridge.temporal_sdk_bridge import temporalio.client +import temporalio.converter import temporalio.worker from temporalio import workflow -from temporalio.client import Client, ClientConfig, OutboundInterceptor, Plugin +from temporalio.client import Client, ClientConfig, OutboundInterceptor, WorkflowHistory from temporalio.contrib.pydantic import pydantic_data_converter +from temporalio.converter import DataConverter +from temporalio.plugin import SimplePlugin +from temporalio.service import ConnectConfig, ServiceClient from temporalio.testing import WorkflowEnvironment from temporalio.worker import ( Replayer, @@ -37,23 +43,24 @@ class MyClientPlugin(temporalio.client.Plugin): def __init__(self): self.interceptor = TestClientInterceptor() - def init_client_plugin(self, next: Plugin) -> None: - self.next_client_plugin = next - def configure_client(self, config: ClientConfig) -> ClientConfig: config["namespace"] = "replaced_namespace" config["interceptors"] = list(config.get("interceptors") or []) + [ self.interceptor ] - return self.next_client_plugin.configure_client(config) + return config async def connect_service_client( - self, config: temporalio.service.ConnectConfig - ) -> temporalio.service.ServiceClient: + self, + config: ConnectConfig, + next: Callable[[ConnectConfig], Awaitable[ServiceClient]], + ) -> ServiceClient: config.api_key = "replaced key" - return await self.next_client_plugin.connect_service_client(config) + config.tls = False + return await next(config) +@pytest.mark.requires_local_server async def test_client_plugin(client: Client, env: WorkflowEnvironment): if env.supports_time_skipping: pytest.skip("Client connect is only designed for local") @@ -73,90 +80,100 @@ async def test_client_plugin(client: Client, env: WorkflowEnvironment): class MyCombinedPlugin(temporalio.client.Plugin, temporalio.worker.Plugin): - def init_worker_plugin(self, next: temporalio.worker.Plugin) -> None: - self.next_worker_plugin = next - - def init_client_plugin(self, next: temporalio.client.Plugin) -> None: - self.next_client_plugin = next - def configure_client(self, config: ClientConfig) -> ClientConfig: - return self.next_client_plugin.configure_client(config) + return config def configure_worker(self, config: WorkerConfig) -> WorkerConfig: - config["task_queue"] = "combined" - return self.next_worker_plugin.configure_worker(config) + config["task_queue"] = "combined" + str(uuid.uuid4()) + return config async def connect_service_client( - self, config: temporalio.service.ConnectConfig - ) -> temporalio.service.ServiceClient: - return await self.next_client_plugin.connect_service_client(config) + self, + config: ConnectConfig, + next: Callable[[ConnectConfig], Awaitable[ServiceClient]], + ) -> ServiceClient: + return await next(config) - async def run_worker(self, worker: Worker) -> None: - await self.next_worker_plugin.run_worker(worker) + async def run_worker( + self, worker: Worker, next: Callable[[Worker], Awaitable[None]] + ) -> None: + await next(worker) def configure_replayer(self, config: ReplayerConfig) -> ReplayerConfig: - return self.next_worker_plugin.configure_replayer(config) + return config def run_replayer( self, replayer: Replayer, histories: AsyncIterator[temporalio.client.WorkflowHistory], + next: Callable[ + [Replayer, AsyncIterator[WorkflowHistory]], + AbstractAsyncContextManager[AsyncIterator[WorkflowReplayResult]], + ], ) -> AbstractAsyncContextManager[AsyncIterator[WorkflowReplayResult]]: - return self.next_worker_plugin.run_replayer(replayer, histories) + return next(replayer, histories) class MyWorkerPlugin(temporalio.worker.Plugin): - def init_worker_plugin(self, next: temporalio.worker.Plugin) -> None: - self.next_worker_plugin = next - def configure_worker(self, config: WorkerConfig) -> WorkerConfig: - config["task_queue"] = "replaced_queue" + config["task_queue"] = "replaced_queue" + str(uuid.uuid4()) runner = config.get("workflow_runner") if isinstance(runner, SandboxedWorkflowRunner): config["workflow_runner"] = dataclasses.replace( runner, restrictions=runner.restrictions.with_passthrough_modules("my_module"), ) - return self.next_worker_plugin.configure_worker(config) + return config - async def run_worker(self, worker: Worker) -> None: - await self.next_worker_plugin.run_worker(worker) + async def run_worker( + self, worker: Worker, next: Callable[[Worker], Awaitable[None]] + ) -> None: + await next(worker) def configure_replayer(self, config: ReplayerConfig) -> ReplayerConfig: - return self.next_worker_plugin.configure_replayer(config) + return config def run_replayer( self, replayer: Replayer, histories: AsyncIterator[temporalio.client.WorkflowHistory], + next: Callable[ + [Replayer, AsyncIterator[WorkflowHistory]], + AbstractAsyncContextManager[AsyncIterator[WorkflowReplayResult]], + ], ) -> AbstractAsyncContextManager[AsyncIterator[WorkflowReplayResult]]: - return self.next_worker_plugin.run_replayer(replayer, histories) + return next(replayer, histories) async def test_worker_plugin_basic_config(client: Client) -> None: worker = Worker( client, - task_queue="queue", + task_queue="queue" + str(uuid.uuid4()), activities=[never_run_activity], plugins=[MyWorkerPlugin()], ) - assert worker.config().get("task_queue") == "replaced_queue" + task_queue = worker.config(active_config=True).get("task_queue") + assert task_queue is not None and task_queue.startswith("replaced_queue") # Test client plugin propagation to worker plugins new_config = client.config() new_config["plugins"] = [MyCombinedPlugin()] client = Client(**new_config) - worker = Worker(client, task_queue="queue", activities=[never_run_activity]) - assert worker.config().get("task_queue") == "combined" + worker = Worker( + client, task_queue="queue" + str(uuid.uuid4()), activities=[never_run_activity] + ) + task_queue = worker.config(active_config=True).get("task_queue") + assert task_queue is not None and task_queue.startswith("combined") # Test both. Client propagated plugins are called first, so the worker plugin overrides in this case worker = Worker( client, - task_queue="queue", + task_queue="queue" + str(uuid.uuid4()), activities=[never_run_activity], plugins=[MyWorkerPlugin()], ) - assert worker.config().get("task_queue") == "replaced_queue" + task_queue = worker.config(active_config=True).get("task_queue") + assert task_queue is not None and task_queue.startswith("replaced_queue") async def test_worker_duplicated_plugin(client: Client) -> None: @@ -165,9 +182,9 @@ async def test_worker_duplicated_plugin(client: Client) -> None: client = Client(**new_config) with warnings.catch_warnings(record=True) as warning_list: - worker = Worker( + Worker( client, - task_queue="queue", + task_queue="queue" + str(uuid.uuid4()), activities=[never_run_activity], plugins=[MyCombinedPlugin()], ) @@ -177,56 +194,59 @@ async def test_worker_duplicated_plugin(client: Client) -> None: async def test_worker_sandbox_restrictions(client: Client) -> None: - with warnings.catch_warnings(record=True) as warning_list: + with warnings.catch_warnings(record=True): worker = Worker( client, - task_queue="queue", + task_queue="queue" + str(uuid.uuid4()), activities=[never_run_activity], plugins=[MyWorkerPlugin()], ) assert ( "my_module" in cast( - SandboxedWorkflowRunner, worker.config().get("workflow_runner") + SandboxedWorkflowRunner, + worker.config(active_config=True).get("workflow_runner"), ).restrictions.passthrough_modules ) class ReplayCheckPlugin(temporalio.client.Plugin, temporalio.worker.Plugin): - def init_worker_plugin(self, next: temporalio.worker.Plugin) -> None: - self.next_worker_plugin = next - - def init_client_plugin(self, next: temporalio.client.Plugin) -> None: - self.next_client_plugin = next - def configure_client(self, config: ClientConfig) -> ClientConfig: config["data_converter"] = pydantic_data_converter - return self.next_client_plugin.configure_client(config) + return config def configure_worker(self, config: WorkerConfig) -> WorkerConfig: config["workflows"] = list(config.get("workflows") or []) + [HelloWorkflow] - return self.next_worker_plugin.configure_worker(config) + return config def configure_replayer(self, config: ReplayerConfig) -> ReplayerConfig: config["data_converter"] = pydantic_data_converter config["workflows"] = list(config.get("workflows") or []) + [HelloWorkflow] - return self.next_worker_plugin.configure_replayer(config) + return config - async def run_worker(self, worker: Worker) -> None: - await self.next_worker_plugin.run_worker(worker) + async def run_worker( + self, worker: Worker, next: Callable[[Worker], Awaitable[None]] + ) -> None: + await next(worker) async def connect_service_client( - self, config: temporalio.service.ConnectConfig + self, + config: temporalio.service.ConnectConfig, + next: Callable[[ConnectConfig], Awaitable[ServiceClient]], ) -> temporalio.service.ServiceClient: - return await self.next_client_plugin.connect_service_client(config) + return await next(config) @asynccontextmanager async def run_replayer( self, replayer: Replayer, histories: AsyncIterator[temporalio.client.WorkflowHistory], + next: Callable[ + [Replayer, AsyncIterator[WorkflowHistory]], + AbstractAsyncContextManager[AsyncIterator[WorkflowReplayResult]], + ], ) -> AsyncIterator[AsyncIterator[WorkflowReplayResult]]: - async with self.next_worker_plugin.run_replayer(replayer, histories) as result: + async with next(replayer, histories) as result: yield result @@ -237,6 +257,13 @@ async def run(self, name: str) -> str: return f"Hello, {name}!" +@workflow.defn +class HelloWorkflow2: + @workflow.run + async def run(self, name: str) -> str: + return f"Hello, {name}!" + + async def test_replay(client: Client) -> None: plugin = ReplayCheckPlugin() new_config = client.config() @@ -252,7 +279,340 @@ async def test_replay(client: Client) -> None: ) await handle.result() replayer = Replayer(workflows=[], plugins=[plugin]) - assert len(replayer.config().get("workflows") or []) == 1 - assert replayer.config().get("data_converter") == pydantic_data_converter + assert len(replayer.config(active_config=True).get("workflows") or []) == 1 + assert ( + replayer.config(active_config=True).get("data_converter") + == pydantic_data_converter + ) await replayer.replay_workflow(await handle.fetch_history()) + + +async def test_simple_plugins(client: Client) -> None: + plugin = SimplePlugin( + "MyPlugin", + data_converter=pydantic_data_converter, + workflows=[HelloWorkflow2], + ) + config = client.config() + config["plugins"] = [plugin] + new_client = Client(**config) + + assert new_client.data_converter == pydantic_data_converter + + # Test without plugin registered in client + worker = Worker( + client, + task_queue="queue" + str(uuid.uuid4()), + activities=[never_run_activity], + workflows=[HelloWorkflow], + plugins=[plugin], + ) + # On a sequence, a value is appended + assert worker.config(active_config=True).get("workflows") == [ + HelloWorkflow, + HelloWorkflow2, + ] + + # Test with plugin registered in client + worker = Worker( + new_client, + task_queue="queue" + str(uuid.uuid4()), + activities=[never_run_activity], + ) + assert worker.config(active_config=True).get("workflows") == [HelloWorkflow2] + + replayer = Replayer(workflows=[HelloWorkflow], plugins=[plugin]) + assert ( + replayer.config(active_config=True).get("data_converter") + == pydantic_data_converter + ) + assert replayer.config(active_config=True).get("workflows") == [ + HelloWorkflow, + HelloWorkflow2, + ] + + +async def test_simple_plugins_callables(client: Client) -> None: + def converter(old: DataConverter | None): + if old != temporalio.converter.default(): + raise ValueError("Can't override non-default converter") + return pydantic_data_converter + + plugin = SimplePlugin( + "MyPlugin", + data_converter=converter, + ) + config = client.config() + config["plugins"] = [plugin] + new_client = Client(**config) + + assert new_client.data_converter == pydantic_data_converter + + with pytest.raises(ValueError): + config["data_converter"] = pydantic_data_converter + Client(**config) + + # On a sequence, the lambda overrides the existing values + plugin = SimplePlugin( + "MyPlugin", + workflows=lambda workflows: [], + ) + worker = Worker( + client, + task_queue="queue" + str(uuid.uuid4()) + str(uuid.uuid4()), + workflows=[HelloWorkflow], + activities=[never_run_activity], + plugins=[plugin], + ) + assert worker.config(active_config=True).get("workflows") == [] + + +class MediumPlugin(SimplePlugin): + def __init__(self): + super().__init__("MediumPlugin", data_converter=pydantic_data_converter) + + def configure_worker(self, config: WorkerConfig) -> WorkerConfig: + config = super().configure_worker(config) + config["task_queue"] = "override" + str(uuid.uuid4()) + return config + + +async def test_medium_plugin(client: Client) -> None: + plugin = MediumPlugin() + worker = Worker( + client, + task_queue="queue" + str(uuid.uuid4()), + plugins=[plugin], + workflows=[HelloWorkflow], + ) + task_queue = worker.config(active_config=True).get("task_queue") + assert task_queue is not None and task_queue.startswith("override") + + +class CombinedClientWorkerInterceptor( + temporalio.client.Interceptor, temporalio.worker.Interceptor +): + """Test interceptor that can be used as both client and worker interceptor with execution counting.""" + + def __init__(self): + super().__init__() + self.client_intercepted = False + self.worker_intercepted = False + self.call_count = {"execute_workflow": 0} + + def intercept_client( + self, next: temporalio.client.OutboundInterceptor + ) -> temporalio.client.OutboundInterceptor: + self.client_intercepted = True + return super().intercept_client(next) + + def intercept_activity( + self, next: temporalio.worker.ActivityInboundInterceptor + ) -> temporalio.worker.ActivityInboundInterceptor: + self.worker_intercepted = True + return super().intercept_activity(next) + + def workflow_interceptor_class( + self, input: temporalio.worker.WorkflowInterceptorClassInput + ) -> type[temporalio.worker.WorkflowInboundInterceptor] | None: + # This method gets called when the worker is configured with workflows + # Mark that worker interceptor was used + self.worker_intercepted = True + + # Return counting interceptor class + call_count = self.call_count + + class CountingWorkflowInterceptor(temporalio.worker.WorkflowInboundInterceptor): + async def execute_workflow( + self, input: temporalio.worker.ExecuteWorkflowInput + ): + call_count["execute_workflow"] += 1 + return await super().execute_workflow(input) + + return CountingWorkflowInterceptor + + +async def test_simple_plugin_worker_interceptor_only_used_on_worker( + client: Client, +) -> None: + """Test that when a combined client/worker interceptor is provided by SimplePlugin + to interceptors, and the plugin is only used on a worker (not on the client + used to create that worker), the worker interceptor functionality is still provided.""" + + interceptor = CombinedClientWorkerInterceptor() + + # Create SimplePlugin that provides the combined interceptor + plugin = SimplePlugin( + "TestCombinedPlugin", + interceptors=[interceptor], + ) + + # Create worker with the plugin (but don't add plugin to client) + worker = Worker( + client, + task_queue="queue" + str(uuid.uuid4()), + activities=[never_run_activity], + workflows=[ + HelloWorkflow + ], # Add workflows to trigger workflow_interceptor_class + plugins=[plugin], + ) + + # Worker creation triggers plugin configuration + assert worker is not None + + # The interceptor should NOT have been used for client interception + # since the plugin was not added to the client + assert not interceptor.client_intercepted, ( + "Client interceptor should not have been used" + ) + + # The interceptor SHOULD have been used for worker interception + # even though it was specified in interceptors + assert interceptor.worker_intercepted, "Worker interceptor should have been used" + + +async def test_simple_plugin_interceptor_duplication_when_used_on_client_and_worker( + client: Client, +) -> None: + """Test that when a combined client/worker interceptor is provided by SimplePlugin + to interceptors, and the plugin is used on both client and worker, + the interceptor is not duplicated in the worker.""" + + interceptor = CombinedClientWorkerInterceptor() + + # Create SimplePlugin that provides the combined interceptor + plugin = SimplePlugin( + "TestCombinedPlugin", + interceptors=[interceptor], + ) + + # Add plugin to client first + config = client.config() + config["plugins"] = [plugin] + new_client = Client(**config) + + # Verify client interceptor was used + assert interceptor.client_intercepted, "Client interceptor should have been used" + + # Reset the worker intercepted flag to test worker behavior + interceptor.worker_intercepted = False + + # Create worker with the same plugin-enabled client + worker = Worker( + new_client, + task_queue="queue" + str(uuid.uuid4()), + activities=[never_run_activity], + workflows=[HelloWorkflow], + ) + + # The worker interceptor functionality should still work + # (regardless of whether it comes from client propagation or worker config) + assert interceptor.worker_intercepted, "Worker interceptor should have been used" + + # Test execution-level duplication by running a workflow + async with new_worker( + new_client, + HelloWorkflow, + max_cached_workflows=0, + ) as worker: + # Start and complete a workflow + handle = await new_client.start_workflow( + HelloWorkflow.run, + "test", + id=f"counting-workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + result = await handle.result() + assert result == "Hello, test!" + + # The workflow interceptor should only be called ONCE, not twice + assert interceptor.call_count["execute_workflow"] == 1, ( + f"Expected execute_workflow to be called once, but was called {interceptor.call_count['execute_workflow']} times. This indicates interceptor duplication in execution." + ) + + +async def test_simple_plugin_no_duplication_when_interceptor_in_both_client_and_worker_params( + client: Client, +) -> None: + """Test that when the same interceptor is provided to the unified interceptors + parameter in a SimplePlugin, it doesn't get duplicated.""" + + interceptor = CombinedClientWorkerInterceptor() + + # Create SimplePlugin that provides the interceptor once to the unified parameter + plugin = SimplePlugin( + "TestCombinedPlugin", + interceptors=[interceptor], # Single unified parameter + ) + + # Create worker with plugin (not on client) + worker = Worker( + client, + task_queue="queue" + str(uuid.uuid4()), + activities=[never_run_activity], + workflows=[HelloWorkflow], + plugins=[plugin], + ) + + # The worker interceptor functionality should work + assert interceptor.worker_intercepted, "Worker interceptor should have been used" + + # Test execution-level duplication by running a workflow + async with worker: + # Start and complete a workflow + handle = await client.start_workflow( + HelloWorkflow.run, + "test", + id=f"counting-workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + result = await handle.result() + assert result == "Hello, test!" + + # The workflow interceptor should only be called ONCE, not twice + assert interceptor.call_count["execute_workflow"] == 1, ( + f"Expected execute_workflow to be called once, but was called {interceptor.call_count['execute_workflow']} times. This indicates interceptor duplication in execution." + ) + + +async def test_simple_plugin_no_duplication_in_interceptor_chain( + client: Client, +) -> None: + """Test that interceptors don't get duplicated in the actual interceptor chain execution. + This catches the specific OpenTelemetry issue where the same interceptor method gets called twice.""" + + interceptor = CombinedClientWorkerInterceptor() + + # Create SimplePlugin that provides the combined interceptor + plugin = SimplePlugin( + "CountingPlugin", + interceptors=[interceptor], + ) + + # Add plugin to client (like OpenTelemetryPlugin does) + config = client.config() + config["plugins"] = [plugin] + new_client = Client(**config) + + # Create worker with the plugin-enabled client (plugin propagates from client) + async with new_worker( + new_client, + HelloWorkflow, + max_cached_workflows=0, + ) as worker: + # Start and complete a workflow + handle = await new_client.start_workflow( + HelloWorkflow.run, + "test", + id=f"counting-workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + result = await handle.result() + assert result == "Hello, test!" + + # The workflow interceptor should only be called ONCE, not twice + assert interceptor.call_count["execute_workflow"] == 1, ( + f"Expected execute_workflow to be called once, but was called {interceptor.call_count['execute_workflow']} times. This indicates interceptor duplication in the chain." + ) diff --git a/tests/test_prepare_release.py b/tests/test_prepare_release.py new file mode 100644 index 000000000..ab5d4bf7e --- /dev/null +++ b/tests/test_prepare_release.py @@ -0,0 +1,154 @@ +from __future__ import annotations + +import datetime +import importlib.util +import pathlib +import subprocess +import sys +from types import ModuleType + +import pytest + +from scripts.prepare_release import ( + create_release_branch, + create_release_pr, + ensure_clean_worktree, + ensure_only_release_changes, + finalize_changelog_release, + push_release_branch, + replace_project_version, + replace_service_version, +) + + +def _release_verify_module() -> ModuleType: + path = pathlib.Path(__file__).parents[1] / ".github/scripts/release_verify.py" + spec = importlib.util.spec_from_file_location("release_verify", path) + assert spec and spec.loader + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def test_sdk_core_changelog_entries_runs_core_script( + monkeypatch: pytest.MonkeyPatch, +) -> None: + release_verify = _release_verify_module() + calls: list[tuple[list[str], pathlib.Path]] = [] + + def check_output(args: list[str], *, cwd: pathlib.Path, **_kwargs: object) -> str: + calls.append((args, cwd)) + return "#### Added\n\n* Core feature.\n" + + monkeypatch.setattr(subprocess, "check_output", check_output) + core_path = pathlib.Path("sdk-core") + assert release_verify._sdk_core_changelog_entries("old", "new", core_path) == [ + "#### Added", + "", + "* Core feature.", + ] + assert calls == [ + ( + [ + "cargo", + "run", + "--quiet", + "-p", + "temporalio-sdk-core", + "--bin", + "changelog-release-notes", + "--", + "--from", + "old", + "--to", + "new", + ], + core_path, + ) + ] + + +def test_sdk_core_release_notes_embed_core_output( + monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path +) -> None: + release_verify = _release_verify_module() + (tmp_path / ".git").mkdir() + monkeypatch.setattr(release_verify, "_previous_release_tag", lambda _version: "old") + monkeypatch.setattr(release_verify, "_gitlink", lambda revision, _path: revision) + monkeypatch.setattr( + subprocess, + "check_output", + lambda *_args, **_kwargs: "#### Commits\n\n- Core commit\n", + ) + + assert release_verify._sdk_core_release_notes("1.30.0", str(tmp_path)) == [ + "### SDK Core", + "", + "#### Commits", + "", + "- Core commit", + ] + + +def test_finalize_changelog_release() -> None: + text = "## [Unreleased]\n\n### Added\n\n- A thing.\n" + assert "## [1.30.0] - 2026-06-18" in finalize_changelog_release( + text, version="1.30.0", release_date=datetime.date(2026, 6, 18) + ) + + +def test_replace_versions() -> None: + assert 'version = "1.30.0"' in replace_project_version( + 'version = "1.29.0"\n', "1.30.0" + ) + assert '__version__ = "1.30.0"' in replace_service_version( + '__version__ = "1.29.0"\n', "1.30.0" + ) + + +def test_create_release_branch(monkeypatch: pytest.MonkeyPatch) -> None: + calls: list[list[str]] = [] + monkeypatch.setattr( + subprocess, "run", lambda command, **_kwargs: calls.append(command) + ) + create_release_branch(pathlib.Path("/repo"), "1.30.0") + assert calls[1] == [ + "git", + "switch", + "--create", + "chore/release-1.30.0", + "origin/main", + ] + + +def test_clean_worktree_rejected(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr( + subprocess, + "run", + lambda *_args, **_kwargs: subprocess.CompletedProcess([], 0, " M file\n"), + ) + with pytest.raises(RuntimeError, match="clean worktree"): + ensure_clean_worktree(pathlib.Path("/repo")) + with pytest.raises(RuntimeError, match="unexpected files"): + ensure_only_release_changes(pathlib.Path("/repo")) + + +def test_create_release_pr(monkeypatch: pytest.MonkeyPatch) -> None: + calls: list[list[str]] = [] + monkeypatch.setattr( + subprocess, "run", lambda command, **_kwargs: calls.append(command) + ) + create_release_pr(pathlib.Path("/repo"), "1.30.0") + assert "chore/release-1.30.0" in calls[0] + + +def test_push_release_branch(monkeypatch: pytest.MonkeyPatch) -> None: + calls: list[list[str]] = [] + monkeypatch.setattr( + subprocess, "run", lambda command, **_kwargs: calls.append(command) + ) + push_release_branch(pathlib.Path("/repo"), "1.30.0") + assert calls == [ + ["git", "push", "--set-upstream", "origin", "chore/release-1.30.0"] + ] diff --git a/tests/test_runtime.py b/tests/test_runtime.py index 4505ebfcf..e003af768 100644 --- a/tests/test_runtime.py +++ b/tests/test_runtime.py @@ -4,21 +4,33 @@ import re import uuid from datetime import timedelta -from typing import List, cast +from typing import Any, cast from urllib.request import urlopen +import pytest + +import temporalio.bridge.metric +import temporalio.bridge.runtime from temporalio import workflow from temporalio.client import Client from temporalio.runtime import ( LogForwardingConfig, LoggingConfig, + OpenTelemetryConfig, PrometheusConfig, Runtime, TelemetryConfig, TelemetryFilter, + _RuntimeRef, ) +from temporalio.testing import WorkflowEnvironment from temporalio.worker import Worker -from tests.helpers import assert_eq_eventually, assert_eventually, find_free_port +from tests.helpers import ( + LogHandler, + assert_eq_eventually, + assert_eventually, + find_free_port, +) @workflow.defn @@ -28,22 +40,18 @@ async def run(self, name: str) -> str: return f"Hello, {name}!" -async def test_different_runtimes(client: Client): +async def test_different_runtimes(env: WorkflowEnvironment): # Create two workers in separate runtimes and run workflows on them. # Confirm they each have different Prometheus addresses. prom_addr1 = f"127.0.0.1:{find_free_port()}" - client1 = await Client.connect( - client.service_client.config.target_host, - namespace=client.namespace, + client1 = await env.connect_client( runtime=Runtime( telemetry=TelemetryConfig(metrics=PrometheusConfig(bind_address=prom_addr1)) ), ) prom_addr2 = f"127.0.0.1:{find_free_port()}" - client2 = await Client.connect( - client.service_client.config.target_host, - namespace=client.namespace, + client2 = await env.connect_client( runtime=Runtime( telemetry=TelemetryConfig(metrics=PrometheusConfig(bind_address=prom_addr2)) ), @@ -72,9 +80,9 @@ async def run_workflow(client: Client): async def test_runtime_log_forwarding(): # Create logger with record capture log_queue: queue.Queue[logging.LogRecord] = queue.Queue() - log_queue_list = cast(List[logging.LogRecord], log_queue.queue) + log_queue_list = cast(list[logging.LogRecord], log_queue.queue) + handler = logging.handlers.QueueHandler(log_queue) logger = logging.getLogger(f"log-{uuid.uuid4()}") - logger.addHandler(logging.handlers.QueueHandler(log_queue)) async def log_queue_len() -> int: return len(log_queue_list) @@ -89,49 +97,50 @@ async def log_queue_len() -> int: ) ) - # Set capture only info logs - logger.setLevel(logging.INFO) - # Write some logs - runtime._core_runtime.write_test_info_log("info1", "extra1") - runtime._core_runtime.write_test_debug_log("debug2", "extra2") - runtime._core_runtime.write_test_info_log("info3", "extra3") - - # Check the expected records - await assert_eq_eventually(2, log_queue_len) - assert log_queue_list[0].levelno == logging.INFO - assert log_queue_list[0].message.startswith( - "[sdk_core::temporal_sdk_bridge::runtime] info1" - ) - assert ( - log_queue_list[0].name - == f"{logger.name}-sdk_core::temporal_sdk_bridge::runtime" - ) - assert log_queue_list[0].created == log_queue_list[0].temporal_log.time # type: ignore - assert log_queue_list[0].temporal_log.fields == {"extra_data": "extra1"} # type: ignore - assert log_queue_list[1].levelno == logging.INFO - assert log_queue_list[1].message.startswith( - "[sdk_core::temporal_sdk_bridge::runtime] info3" - ) + with LogHandler.apply(logger, handler): + # Set capture only info logs + logger.setLevel(logging.INFO) + # Write some logs + runtime._core_runtime.write_test_info_log("info1", "extra1") + runtime._core_runtime.write_test_debug_log("debug2", "extra2") + runtime._core_runtime.write_test_info_log("info3", "extra3") + + # Check the expected records + await assert_eq_eventually(2, log_queue_len) + assert log_queue_list[0].levelno == logging.INFO + assert log_queue_list[0].message.startswith( + "[sdk_core::temporal_sdk_bridge::runtime] info1" + ) + assert ( + log_queue_list[0].name + == f"{logger.name}-sdk_core::temporal_sdk_bridge::runtime" + ) + assert log_queue_list[0].created == log_queue_list[0].temporal_log.time # type: ignore + assert log_queue_list[0].temporal_log.fields == {"extra_data": "extra1"} # type: ignore + assert log_queue_list[1].levelno == logging.INFO + assert log_queue_list[1].message.startswith( + "[sdk_core::temporal_sdk_bridge::runtime] info3" + ) - # Clear logs and enable debug and try again - log_queue_list.clear() - logger.setLevel(logging.DEBUG) - runtime._core_runtime.write_test_info_log("info4", "extra4") - runtime._core_runtime.write_test_debug_log("debug5", "extra5") - runtime._core_runtime.write_test_info_log("info6", "extra6") - await assert_eq_eventually(3, log_queue_len) - assert log_queue_list[0].levelno == logging.INFO - assert log_queue_list[0].message.startswith( - "[sdk_core::temporal_sdk_bridge::runtime] info4" - ) - assert log_queue_list[1].levelno == logging.DEBUG - assert log_queue_list[1].message.startswith( - "[sdk_core::temporal_sdk_bridge::runtime] debug5" - ) - assert log_queue_list[2].levelno == logging.INFO - assert log_queue_list[2].message.startswith( - "[sdk_core::temporal_sdk_bridge::runtime] info6" - ) + # Clear logs and enable debug and try again + log_queue_list.clear() + logger.setLevel(logging.DEBUG) + runtime._core_runtime.write_test_info_log("info4", "extra4") + runtime._core_runtime.write_test_debug_log("debug5", "extra5") + runtime._core_runtime.write_test_info_log("info6", "extra6") + await assert_eq_eventually(3, log_queue_len) + assert log_queue_list[0].levelno == logging.INFO + assert log_queue_list[0].message.startswith( + "[sdk_core::temporal_sdk_bridge::runtime] info4" + ) + assert log_queue_list[1].levelno == logging.DEBUG + assert log_queue_list[1].message.startswith( + "[sdk_core::temporal_sdk_bridge::runtime] debug5" + ) + assert log_queue_list[2].levelno == logging.INFO + assert log_queue_list[2].message.startswith( + "[sdk_core::temporal_sdk_bridge::runtime] info6" + ) @workflow.defn @@ -141,16 +150,14 @@ async def run(self) -> None: raise RuntimeError("Intentional error") -async def test_runtime_task_fail_log_forwarding(client: Client): +async def test_runtime_task_fail_log_forwarding(env: WorkflowEnvironment): # Client with lo capturing runtime log_queue: queue.Queue[logging.LogRecord] = queue.Queue() - log_queue_list = cast(List[logging.LogRecord], log_queue.queue) + log_queue_list = cast(list[logging.LogRecord], log_queue.queue) + handler = logging.handlers.QueueHandler(log_queue) logger = logging.getLogger(f"log-{uuid.uuid4()}") - logger.addHandler(logging.handlers.QueueHandler(log_queue)) logger.setLevel(logging.WARN) - client = await Client.connect( - client.service_client.config.target_host, - namespace=client.namespace, + client = await env.connect_client( runtime=Runtime( telemetry=TelemetryConfig( logging=LoggingConfig( @@ -161,31 +168,35 @@ async def test_runtime_task_fail_log_forwarding(client: Client): ), ) - # Start workflow - task_queue = f"task-queue-{uuid.uuid4()}" - async with Worker(client, task_queue=task_queue, workflows=[TaskFailWorkflow]): - handle = await client.start_workflow( - TaskFailWorkflow.run, - id=f"workflow-{uuid.uuid4()}", - task_queue=task_queue, - ) - - # Wait for log to appear - async def has_log() -> bool: - return any( - l for l in log_queue_list if "Failing workflow task" in l.message + with LogHandler.apply(logger, handler): + # Start workflow + task_queue = f"task-queue-{uuid.uuid4()}" + async with Worker(client, task_queue=task_queue, workflows=[TaskFailWorkflow]): + handle = await client.start_workflow( + TaskFailWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=task_queue, ) - await assert_eq_eventually(True, has_log) + # Wait for log to appear + async def has_log() -> bool: + return any( + l for l in log_queue_list if "Failing workflow task" in l.message + ) - # Check record - record = next((l for l in log_queue_list if "Failing workflow task" in l.message)) - assert record.levelno == logging.WARNING - assert record.name == f"{logger.name}-sdk_core::temporal_sdk_core::worker::workflow" - assert record.temporal_log.fields["run_id"] == handle.result_run_id # type: ignore + await assert_eq_eventually(True, has_log) + # Check record + record = next(l for l in log_queue_list if "Failing workflow task" in l.message) + assert record.levelno == logging.WARNING + assert ( + record.name + == f"{logger.name}-sdk_core::temporalio_sdk_core::worker::workflow" + ) + assert record.temporal_log.fields["run_id"] == handle.result_run_id # type: ignore -async def test_prometheus_histogram_bucket_overrides(client: Client): + +async def test_prometheus_histogram_bucket_overrides(env: WorkflowEnvironment): # Set up a Prometheus configuration with custom histogram bucket overrides prom_addr = f"127.0.0.1:{find_free_port()}" special_value = float(1234.5678) @@ -215,9 +226,7 @@ async def test_prometheus_histogram_bucket_overrides(client: Client): custom_histogram.record(600) # Create client with overrides - client_with_overrides = await Client.connect( - client.service_client.config.target_host, - namespace=client.namespace, + client_with_overrides = await env.connect_client( runtime=runtime, ) @@ -242,15 +251,196 @@ async def check_metrics() -> None: metrics_output = f.read().decode("utf-8") for key, buckets in histogram_overrides.items(): - assert ( - key in metrics_output - ), f"Missing {key} in full output: {metrics_output}" + assert key in metrics_output, ( + f"Missing {key} in full output: {metrics_output}" + ) for bucket in buckets: # expect to have {key}_bucket and le={bucket} in the same line with arbitrary strings between them regex = re.compile(f'{key}_bucket.*le="{bucket}"') - assert regex.search( - metrics_output - ), f"Missing bucket for {key} in full output: {metrics_output}" + assert regex.search(metrics_output), ( + f"Missing bucket for {key} in full output: {metrics_output}" + ) # Wait for metrics to appear and match the expected buckets await assert_eventually(check_metrics) + + +async def test_opentelemetry_histogram_bucket_overrides(env: WorkflowEnvironment): + # Set up an OpenTelemetry configuration with custom histogram bucket overrides + import threading + from http.server import BaseHTTPRequestHandler, HTTPServer + + from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( + ExportMetricsServiceRequest, + ExportMetricsServiceResponse, + ) + + special_value = float(1234.5678) + histogram_overrides = { + "temporal_long_request_latency": [special_value / 2, special_value], + "custom_histogram": [special_value / 2, special_value], + } + + captured: dict[str, list[float]] = {} + lock = threading.Lock() + + class Handler(BaseHTTPRequestHandler): + def log_message(self, format: str, *args: Any): + pass # silence default stderr logging + + def do_POST(self): + length = int(self.headers.get("Content-Length", "0")) + req = ExportMetricsServiceRequest() + req.ParseFromString(self.rfile.read(length)) + with lock: + for rm in req.resource_metrics: + for sm in rm.scope_metrics: + for m in sm.metrics: + if m.HasField("histogram"): + for dp in m.histogram.data_points: + captured[m.name] = list(dp.explicit_bounds) + body = ExportMetricsServiceResponse().SerializeToString() + self.send_response(200) + self.send_header("Content-Type", "application/x-protobuf") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + otel_port = find_free_port() + server = HTTPServer(("127.0.0.1", otel_port), Handler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + runtime = Runtime( + telemetry=TelemetryConfig( + metrics=OpenTelemetryConfig( + url=f"http://127.0.0.1:{otel_port}/v1/metrics", + http=True, + metric_periodicity=timedelta(milliseconds=100), + durations_as_seconds=False, + histogram_bucket_overrides=histogram_overrides, + ), + ), + ) + + # Create and record to a custom histogram + custom_histogram = runtime.metric_meter.create_histogram( + "custom_histogram", "Custom histogram", "ms" + ) + custom_histogram.record(600) + + # Run a workflow so built-in histograms (e.g. temporal_long_request_latency) + # are recorded and exported. + client_with_overrides = await env.connect_client( + runtime=runtime, + ) + task_queue = f"task-queue-{uuid.uuid4()}" + async with Worker( + client_with_overrides, + task_queue=task_queue, + workflows=[HelloWorkflow], + ): + assert "Hello, World!" == await client_with_overrides.execute_workflow( + HelloWorkflow.run, + "World", + id=f"workflow-{uuid.uuid4()}", + task_queue=task_queue, + ) + + async def check_metrics() -> None: + with lock: + snapshot = dict(captured) + for key, buckets in histogram_overrides.items(): + assert key in snapshot, ( + f"Missing {key} in captured metrics: {list(snapshot)}" + ) + assert snapshot[key] == pytest.approx(buckets), ( + f"Bucket mismatch for {key}: got {snapshot[key]} expected {buckets}" + ) + + await assert_eventually(check_metrics) + finally: + server.shutdown() + server.server_close() + + +def test_runtime_options_invalid_heartbeat() -> None: + with pytest.raises(ValueError): + Runtime( + telemetry=TelemetryConfig(), worker_heartbeat_interval=timedelta(seconds=-5) + ) + + +def test_runtime_environment_info_option(monkeypatch: pytest.MonkeyPatch) -> None: + captured_options = [] + + class MockRuntime: + def __init__(self, *, options: Any) -> None: + captured_options.append(options) + + monkeypatch.setattr(temporalio.bridge.runtime, "Runtime", MockRuntime) + monkeypatch.setattr( + temporalio.bridge.metric.MetricMeter, "create", staticmethod(lambda _: None) + ) + + Runtime(telemetry=TelemetryConfig()) + Runtime(telemetry=TelemetryConfig(), disable_environment_info=True) + + assert [option.disable_environment_info for option in captured_options] == [ + False, + True, + ] + + +def test_runtime_environment_info_can_be_enabled() -> None: + Runtime(telemetry=TelemetryConfig(), disable_environment_info=False) + + +def test_runtime_ref_creates_default(): + ref = _RuntimeRef() + assert not ref._default_runtime + ref.default() + assert ref._default_runtime + + +def test_runtime_ref_prevents_default(): + ref = _RuntimeRef() + ref.prevent_default() + with pytest.raises(RuntimeError) as exc_info: + ref.default() + assert exc_info.match( + "Cannot create default Runtime after Runtime.prevent_default has been called" + ) + + # explicitly setting a default runtime will allow future calls to `default()`` + explicit_runtime = Runtime(telemetry=TelemetryConfig()) + ref.set_default(explicit_runtime) + + assert ref.default() is explicit_runtime + + +def test_runtime_ref_prevent_default_errors_after_default(): + ref = _RuntimeRef() + ref.default() + with pytest.raises(RuntimeError) as exc_info: + ref.prevent_default() + + assert exc_info.match( + "Runtime.prevent_default called after default runtime has been created" + ) + + +def test_runtime_ref_set_default(): + ref = _RuntimeRef() + explicit_runtime = Runtime(telemetry=TelemetryConfig()) + ref.set_default(explicit_runtime) + assert ref.default() is explicit_runtime + + new_runtime = Runtime(telemetry=TelemetryConfig()) + + with pytest.raises(RuntimeError) as exc_info: + ref.set_default(new_runtime) + assert exc_info.match("Runtime default already set") + + ref.set_default(new_runtime, error_if_already_set=False) + assert ref.default() is new_runtime diff --git a/tests/test_serialization_context.py b/tests/test_serialization_context.py new file mode 100644 index 000000000..8d65d5f1f --- /dev/null +++ b/tests/test_serialization_context.py @@ -0,0 +1,1919 @@ +""" +Test context-aware serde/codec operations. + +Serialization context should be available on all serde/codec operations, but testing all of them is +infeasible; this test suite only covers a selection. +""" + +from __future__ import annotations + +import asyncio +import dataclasses +import json +import uuid +from collections import defaultdict +from collections.abc import Sequence +from dataclasses import dataclass, field +from datetime import timedelta +from typing import Any, Literal + +import nexusrpc +import pytest +from pydantic import BaseModel +from typing_extensions import Never + +import temporalio.api.common.v1 +import temporalio.api.failure.v1 +from temporalio import activity, workflow +from temporalio.client import ( + AsyncActivityHandle, + Client, + WorkflowFailureError, + WorkflowUpdateFailedError, +) +from temporalio.common import RetryPolicy +from temporalio.contrib.pydantic import PydanticJSONPlainPayloadConverter +from temporalio.converter import ( + ActivitySerializationContext, + CompositePayloadConverter, + DataConverter, + DefaultFailureConverter, + DefaultPayloadConverter, + EncodingPayloadConverter, + JSONPlainPayloadConverter, + PayloadCodec, + PayloadConverter, + SerializationContext, + WithSerializationContext, + WorkflowSerializationContext, +) +from temporalio.exceptions import ApplicationError +from temporalio.testing import WorkflowEnvironment +from temporalio.worker import Worker +from temporalio.worker._workflow_instance import UnsandboxedWorkflowRunner +from tests.helpers.nexus import make_nexus_endpoint_name + + +@dataclass +class TraceItem: + method: Literal[ + "to_payload", + "from_payload", + "to_failure", + "from_failure", + "encode", + "decode", + ] + context: dict[str, Any] + + +@dataclass +class TraceData: + items: list[TraceItem] = field(default_factory=list) + + +class SerializationContextPayloadConverter( + EncodingPayloadConverter, WithSerializationContext +): + def __init__(self): + self.context: SerializationContext | None = None + + @property + def encoding(self) -> str: + return "test-serialization-context" + + def with_context( + self, context: SerializationContext | None + ) -> SerializationContextPayloadConverter: + converter = SerializationContextPayloadConverter() + converter.context = context + return converter + + def to_payload(self, value: Any) -> temporalio.api.common.v1.Payload | None: + if not isinstance(value, TraceData): + return None + if isinstance(self.context, WorkflowSerializationContext): + value.items.append( + TraceItem( + method="to_payload", + context=dataclasses.asdict(self.context), + ) + ) + elif isinstance(self.context, ActivitySerializationContext): + value.items.append( + TraceItem( + method="to_payload", + context=dataclasses.asdict(self.context), + ) + ) + else: + raise Exception(f"Unexpected context type: {type(self.context)}") + payload = JSONPlainPayloadConverter().to_payload(value) + assert payload + payload.metadata["encoding"] = self.encoding.encode() + return payload + + def from_payload( + self, + payload: temporalio.api.common.v1.Payload, + type_hint: type | None = None, + ) -> Any: + value = JSONPlainPayloadConverter().from_payload(payload, TraceData) + assert isinstance(value, TraceData) + if isinstance(self.context, WorkflowSerializationContext): + value.items.append( + TraceItem( + method="from_payload", + context=dataclasses.asdict(self.context), + ) + ) + elif isinstance(self.context, ActivitySerializationContext): + value.items.append( + TraceItem( + method="from_payload", + context=dataclasses.asdict(self.context), + ) + ) + else: + raise Exception(f"Unexpected context type: {type(self.context)}") + return value + + +class SerializationContextCompositePayloadConverter( + CompositePayloadConverter, WithSerializationContext +): + def __init__(self): + super().__init__( + SerializationContextPayloadConverter(), + *DefaultPayloadConverter.default_encoding_payload_converters, + ) + + +# Payload conversion tests + +## Misc payload conversion + + +@activity.defn +async def passthrough_activity(input: TraceData) -> TraceData: + activity.payload_converter().to_payload(input) + activity.heartbeat(input) + # Wait for the heartbeat to be processed so that it modifies the data before the activity returns + await asyncio.sleep(0.2) + return input + + +@workflow.defn +class EchoWorkflow: + @workflow.run + async def run(self, data: TraceData) -> TraceData: + return data + + +@workflow.defn +class PayloadConversionWorkflow: + @workflow.run + async def run(self, data: TraceData) -> TraceData: + workflow.payload_converter().to_payload(data) + data = await workflow.execute_activity( + passthrough_activity, + data, + start_to_close_timeout=timedelta(seconds=10), + heartbeat_timeout=timedelta(seconds=2), + activity_id="activity-id", + ) + data = await workflow.execute_child_workflow( + EchoWorkflow.run, data, id=f"{workflow.info().workflow_id}_child" + ) + return data + + +async def test_payload_conversion_calls_follow_expected_sequence_and_contexts( + client: Client, +): + workflow_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + + config = client.config() + config["data_converter"] = dataclasses.replace( + DataConverter.default, + payload_converter_class=SerializationContextCompositePayloadConverter, + ) + client = Client(**config) + + async with Worker( + client, + task_queue=task_queue, + workflows=[PayloadConversionWorkflow, EchoWorkflow], + activities=[passthrough_activity], + workflow_runner=UnsandboxedWorkflowRunner(), # so that we can use isinstance + ): + result = await client.execute_workflow( + PayloadConversionWorkflow.run, + TraceData(), + id=workflow_id, + task_queue=task_queue, + ) + + workflow_context = dataclasses.asdict( + WorkflowSerializationContext( + namespace=client.namespace, + workflow_id=workflow_id, + ) + ) + child_workflow_context = dataclasses.asdict( + WorkflowSerializationContext( + namespace=client.namespace, + workflow_id=f"{workflow_id}_child", + ) + ) + activity_context = dataclasses.asdict( + ActivitySerializationContext( + namespace=client.namespace, + workflow_id=workflow_id, + workflow_type=PayloadConversionWorkflow.__name__, + activity_type=passthrough_activity.__name__, + activity_id="activity-id", + activity_task_queue=task_queue, + is_local=False, + ) + ) + assert result.items == [ + TraceItem( + method="to_payload", + context=workflow_context, # Outbound workflow input + ), + TraceItem( + method="from_payload", + context=workflow_context, # Inbound workflow input + ), + TraceItem( + method="to_payload", + context=workflow_context, # workflow payload converter + ), + TraceItem( + method="to_payload", + context=activity_context, # Outbound activity input + ), + TraceItem( + method="from_payload", + context=activity_context, # Inbound activity input + ), + TraceItem( + method="to_payload", + context=activity_context, # activity payload converter + ), + TraceItem( + method="to_payload", + context=activity_context, # Outbound heartbeat + ), + TraceItem( + method="to_payload", + context=activity_context, # Outbound activity result + ), + TraceItem( + method="from_payload", + context=activity_context, # Inbound activity result + ), + TraceItem( + method="to_payload", + context=child_workflow_context, # Outbound child workflow input + ), + TraceItem( + method="from_payload", + context=child_workflow_context, # Inbound child workflow input + ), + TraceItem( + method="to_payload", + context=child_workflow_context, # Outbound child workflow result + ), + TraceItem( + method="from_payload", + context=child_workflow_context, # Inbound child workflow result + ), + TraceItem( + method="to_payload", + context=workflow_context, # Outbound workflow result + ), + TraceItem( + method="from_payload", + context=workflow_context, # Inbound workflow result + ), + ] + + +## Activity heartbeat payload conversion + + +@activity.defn +async def activity_with_heartbeat_details() -> TraceData: + info = activity.info() + if info.attempt == 1: + data = TraceData() + activity.heartbeat(data) + raise Exception("Intentional error to force retry") + elif info.attempt == 2: + [heartbeat_data] = info.heartbeat_details + assert isinstance(heartbeat_data, TraceData) + return heartbeat_data + else: + raise AssertionError(f"Unexpected attempt number: {info.attempt}") + + +@workflow.defn +class HeartbeatDetailsSerializationContextTestWorkflow: + @workflow.run + async def run(self) -> TraceData: + return await workflow.execute_activity( + activity_with_heartbeat_details, + start_to_close_timeout=timedelta(seconds=10), + retry_policy=RetryPolicy( + initial_interval=timedelta(milliseconds=100), + maximum_attempts=2, + ), + activity_id="activity-id", + ) + + +async def test_heartbeat_details_payload_conversion(client: Client): + """Test that heartbeat details are decoded with activity context.""" + workflow_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + + config = client.config() + config["data_converter"] = dataclasses.replace( + DataConverter.default, + payload_converter_class=SerializationContextCompositePayloadConverter, + ) + + client = Client(**config) + + async with Worker( + client, + task_queue=task_queue, + workflows=[HeartbeatDetailsSerializationContextTestWorkflow], + activities=[activity_with_heartbeat_details], + workflow_runner=UnsandboxedWorkflowRunner(), # so that we can use isinstance + ): + result = await client.execute_workflow( + HeartbeatDetailsSerializationContextTestWorkflow.run, + id=workflow_id, + task_queue=task_queue, + ) + + workflow_context = dataclasses.asdict( + WorkflowSerializationContext( + namespace=client.namespace, + workflow_id=workflow_id, + ) + ) + + activity_context = dataclasses.asdict( + ActivitySerializationContext( + namespace=client.namespace, + workflow_id=workflow_id, + workflow_type=HeartbeatDetailsSerializationContextTestWorkflow.__name__, + activity_type=activity_with_heartbeat_details.__name__, + activity_id="activity-id", + activity_task_queue=task_queue, + is_local=False, + ) + ) + + assert result.items == [ + TraceItem( + method="to_payload", + context=activity_context, # Outbound heartbeat + ), + TraceItem( + method="from_payload", + context=activity_context, # Inbound heartbeart detail + ), + TraceItem( + method="to_payload", + context=activity_context, # Outbound activity result + ), + TraceItem( + method="from_payload", + context=activity_context, # Inbound activity result + ), + TraceItem( + method="to_payload", + context=workflow_context, # Outbound workflow result + ), + TraceItem( + method="from_payload", + context=workflow_context, # Inbound workflow result + ), + ] + + +## Local activity payload conversion + + +@activity.defn +async def local_activity(input: TraceData) -> TraceData: + return input + + +@workflow.defn +class LocalActivityWorkflow: + @workflow.run + async def run(self, data: TraceData) -> TraceData: + return await workflow.execute_local_activity( + local_activity, + data, + start_to_close_timeout=timedelta(seconds=10), + activity_id="activity-id", + ) + + +async def test_local_activity_payload_conversion(client: Client): + workflow_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + + config = client.config() + config["data_converter"] = dataclasses.replace( + DataConverter.default, + payload_converter_class=SerializationContextCompositePayloadConverter, + ) + client = Client(**config) + + async with Worker( + client, + task_queue=task_queue, + workflows=[LocalActivityWorkflow], + activities=[local_activity], + workflow_runner=UnsandboxedWorkflowRunner(), # so that we can use isinstance + ): + result = await client.execute_workflow( + LocalActivityWorkflow.run, + TraceData(), + id=workflow_id, + task_queue=task_queue, + ) + + workflow_context = dataclasses.asdict( + WorkflowSerializationContext( + namespace=client.namespace, + workflow_id=workflow_id, + ) + ) + local_activity_context = dataclasses.asdict( + ActivitySerializationContext( + namespace=client.namespace, + workflow_id=workflow_id, + workflow_type=LocalActivityWorkflow.__name__, + activity_type=local_activity.__name__, + activity_id="activity-id", + activity_task_queue=task_queue, + is_local=True, + ) + ) + + assert result.items == [ + TraceItem( + method="to_payload", + context=workflow_context, # Outbound workflow input + ), + TraceItem( + method="from_payload", + context=workflow_context, # Inbound workflow input + ), + TraceItem( + method="to_payload", + context=local_activity_context, # Outbound local activity input + ), + TraceItem( + method="from_payload", + context=local_activity_context, # Inbound local activity input + ), + TraceItem( + method="to_payload", + context=local_activity_context, # Outbound local activity result + ), + TraceItem( + method="from_payload", + context=local_activity_context, # Inbound local activity result + ), + TraceItem( + method="to_payload", + context=workflow_context, # Outbound workflow result + ), + TraceItem( + method="from_payload", + context=workflow_context, # Inbound workflow result + ), + ] + + +## Async activity completion payload conversion + + +@workflow.defn +class WaitForSignalWorkflow: + # Like a global asyncio.Event() + + def __init__(self) -> None: + self.signal_received = asyncio.Event() + + @workflow.run + async def run(self) -> None: + await self.signal_received.wait() + + @workflow.signal + def signal(self) -> None: + self.signal_received.set() + + +@activity.defn +async def async_activity() -> TraceData: + # Notify test that the activity has started and is ready to be completed manually + await ( + activity.client() + .get_workflow_handle("activity-started-wf-id") + .signal(WaitForSignalWorkflow.signal) + ) + activity.raise_complete_async() + + +@workflow.defn +class AsyncActivityCompletionSerializationContextTestWorkflow: + @workflow.run + async def run(self) -> TraceData: + return await workflow.execute_activity( + async_activity, + start_to_close_timeout=timedelta(seconds=10), + activity_id="async-activity-id", + ) + + +async def test_async_activity_completion_payload_conversion( + client: Client, +): + workflow_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + + config = client.config() + config["data_converter"] = dataclasses.replace( + DataConverter.default, + payload_converter_class=SerializationContextCompositePayloadConverter, + ) + client = Client(**config) + + async with Worker( + client, + task_queue=task_queue, + workflows=[ + AsyncActivityCompletionSerializationContextTestWorkflow, + WaitForSignalWorkflow, + ], + activities=[async_activity], + workflow_runner=UnsandboxedWorkflowRunner(), # so that we can use isinstance + ): + workflow_context = WorkflowSerializationContext( + namespace=client.namespace, + workflow_id=workflow_id, + ) + activity_context = ActivitySerializationContext( + namespace=client.namespace, + workflow_id=workflow_id, + workflow_type=AsyncActivityCompletionSerializationContextTestWorkflow.__name__, + activity_type=async_activity.__name__, + activity_id="async-activity-id", + activity_task_queue=task_queue, + is_local=False, + ) + + act_started_wf_handle = await client.start_workflow( + WaitForSignalWorkflow.run, + id="activity-started-wf-id", + task_queue=task_queue, + ) + wf_handle = await client.start_workflow( + AsyncActivityCompletionSerializationContextTestWorkflow.run, + id=workflow_id, + task_queue=task_queue, + ) + activity_handle = client.get_async_activity_handle( + workflow_id=workflow_id, + run_id=wf_handle.first_execution_run_id, + activity_id="async-activity-id", + ).with_context(activity_context) + + await act_started_wf_handle.result() + data = TraceData() + await activity_handle.heartbeat(data) + await activity_handle.complete(data) + result = await wf_handle.result() + + activity_context_dict = dataclasses.asdict(activity_context) + workflow_context_dict = dataclasses.asdict(workflow_context) + + assert result.items == [ + TraceItem( + method="to_payload", + context=activity_context_dict, # Outbound activity heartbeat + ), + TraceItem( + method="to_payload", + context=activity_context_dict, # Outbound activity completion + ), + TraceItem( + method="from_payload", + context=activity_context_dict, # Inbound activity result + ), + TraceItem( + method="to_payload", + context=workflow_context_dict, # Outbound workflow result + ), + TraceItem( + method="from_payload", + context=workflow_context_dict, # Inbound workflow result + ), + ] + + +class MyAsyncActivityHandle(AsyncActivityHandle): + def my_method(self) -> None: + pass + + +class MyAsyncActivityHandleWithOverriddenConstructor(AsyncActivityHandle): + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + def my_method(self) -> None: + pass + + +def test_subclassed_async_activity_handle(client: Client): + activity_context = ActivitySerializationContext( + namespace=client.namespace, + workflow_id="workflow-id", + workflow_type="workflow-type", + activity_type="activity-type", + activity_id="activity-id", + activity_task_queue="activity-task-queue", + is_local=False, + ) + handle = MyAsyncActivityHandle(client=client, id_or_token=b"task-token") + # This works because the data converter does not use context so AsyncActivityHandle.with_context + # returns self + assert isinstance(handle.with_context(activity_context), MyAsyncActivityHandle) + + # This time the data converter uses context so AsyncActivityHandle.with_context attempts to + # return a new instance of the user's subclass. It works, because they have not overridden the + # constructor. + client_config = client.config() + client_config["data_converter"] = dataclasses.replace( + DataConverter.default, + payload_converter_class=SerializationContextCompositePayloadConverter, + ) + client = Client(**client_config) + handle = MyAsyncActivityHandle(client=client, id_or_token=b"task-token") + assert isinstance(handle.with_context(activity_context), MyAsyncActivityHandle) + + # Finally, a user attempts the same but having overridden the constructor. This fails: + # AsyncActivityHandle.with_context refuses to attempt to create an instance of their subclass. + handle2 = MyAsyncActivityHandleWithOverriddenConstructor( + client=client, id_or_token=b"task-token" + ) + with pytest.raises( + TypeError, + match="you must override with_context to return an instance of your class", + ): + assert isinstance( + handle2.with_context(activity_context), + MyAsyncActivityHandleWithOverriddenConstructor, + ) + + +# Signal test + + +@workflow.defn(sandboxed=False) # so that we can use isinstance +class SignalSerializationContextTestWorkflow: + def __init__(self) -> None: + self.signal_received: TraceData | None = None + + @workflow.run + async def run(self) -> TraceData: + await workflow.wait_condition(lambda: self.signal_received is not None) + assert self.signal_received is not None + return self.signal_received + + @workflow.signal + async def my_signal(self, data: TraceData) -> None: + self.signal_received = data + + +async def test_signal_payload_conversion( + client: Client, +): + workflow_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + + config = client.config() + config["data_converter"] = dataclasses.replace( + DataConverter.default, + payload_converter_class=SerializationContextCompositePayloadConverter, + ) + + custom_client = Client(**config) + + async with Worker( + custom_client, + task_queue=task_queue, + workflows=[SignalSerializationContextTestWorkflow], + activities=[], + workflow_runner=UnsandboxedWorkflowRunner(), # so that we can use isinstance + ): + handle = await custom_client.start_workflow( + SignalSerializationContextTestWorkflow.run, + id=workflow_id, + task_queue=task_queue, + ) + await handle.signal( + SignalSerializationContextTestWorkflow.my_signal, + TraceData(), + ) + result = await handle.result() + + workflow_context = dataclasses.asdict( + WorkflowSerializationContext( + namespace=client.namespace, + workflow_id=workflow_id, + ) + ) + assert result.items == [ + TraceItem( + method="to_payload", + context=workflow_context, # Outbound signal input + ), + TraceItem( + method="from_payload", + context=workflow_context, # Inbound signal input + ), + TraceItem( + method="to_payload", + context=workflow_context, # Outbound workflow result + ), + TraceItem( + method="from_payload", + context=workflow_context, # Inbound workflow result + ), + ] + + +# Query test + + +@workflow.defn +class QuerySerializationContextTestWorkflow: + @workflow.run + async def run(self) -> None: + await asyncio.Event().wait() + + @workflow.query + def my_query(self, input: TraceData) -> TraceData: + return input + + +async def test_query_payload_conversion( + client: Client, +): + workflow_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + + config = client.config() + config["data_converter"] = dataclasses.replace( + DataConverter.default, + payload_converter_class=SerializationContextCompositePayloadConverter, + ) + custom_client = Client(**config) + + async with Worker( + custom_client, + task_queue=task_queue, + workflows=[QuerySerializationContextTestWorkflow], + activities=[], + workflow_runner=UnsandboxedWorkflowRunner(), # so that we can use isinstance + ): + handle = await custom_client.start_workflow( + QuerySerializationContextTestWorkflow.run, + id=workflow_id, + task_queue=task_queue, + ) + result = await handle.query( + QuerySerializationContextTestWorkflow.my_query, TraceData() + ) + + workflow_context = dataclasses.asdict( + WorkflowSerializationContext( + namespace=client.namespace, + workflow_id=workflow_id, + ) + ) + assert result.items == [ + TraceItem( + method="to_payload", + context=workflow_context, # Outbound query input + ), + TraceItem( + method="from_payload", + context=workflow_context, # Inbound query input + ), + TraceItem( + method="to_payload", + context=workflow_context, # Outbound query result + ), + TraceItem( + method="from_payload", + context=workflow_context, # Inbound query result + ), + ] + + +# Update test + + +@workflow.defn +class UpdateSerializationContextTestWorkflow: + @workflow.init + def __init__(self, pass_validation: bool) -> None: + self.pass_validation = pass_validation + self.input: TraceData | None = None + + @workflow.run + async def run(self, _pass_validation: bool) -> TraceData: + await workflow.wait_condition(lambda: self.input is not None) + assert self.input + return self.input + + @workflow.update + def my_update(self, input: TraceData) -> TraceData: + self.input = input + return input + + @my_update.validator + def my_update_validator(self, input: TraceData) -> None: + if not self.pass_validation: + raise ApplicationError("Rejected", input) + + +@pytest.mark.parametrize("pass_validation", [True, False]) +async def test_update_payload_conversion( + client: Client, + pass_validation: bool, +): + workflow_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + + config = client.config() + config["data_converter"] = dataclasses.replace( + DataConverter.default, + payload_converter_class=SerializationContextCompositePayloadConverter, + ) + custom_client = Client(**config) + + async with Worker( + custom_client, + task_queue=task_queue, + workflows=[UpdateSerializationContextTestWorkflow], + activities=[], + workflow_runner=UnsandboxedWorkflowRunner(), # so that we can use isinstance + ): + wf_handle = await custom_client.start_workflow( + UpdateSerializationContextTestWorkflow.run, + pass_validation, + id=workflow_id, + task_queue=task_queue, + ) + if pass_validation: + result = await wf_handle.execute_update( + UpdateSerializationContextTestWorkflow.my_update, TraceData() + ) + else: + try: + await wf_handle.execute_update( + UpdateSerializationContextTestWorkflow.my_update, TraceData() + ) + raise AssertionError("Expected WorkflowUpdateFailedError") + except WorkflowUpdateFailedError as e: + assert isinstance(e.cause, ApplicationError) + assert len(e.cause.details) == 1 + result = e.cause.details[0] + assert isinstance(result, TraceData) + await wf_handle.terminate() + + workflow_context = dataclasses.asdict( + WorkflowSerializationContext( + namespace=client.namespace, + workflow_id=workflow_id, + ) + ) + assert result.items == [ + TraceItem( + method="to_payload", + context=workflow_context, # Outbound update input + ), + TraceItem( + method="from_payload", + context=workflow_context, # Inbound update input + ), + TraceItem( + method="to_payload", + context=workflow_context, # Outbound update result or error detail + ), + TraceItem( + method="from_payload", + context=workflow_context, # Inbound update result or error detail + ), + ] + + +# External workflow test + + +@workflow.defn +class ExternalWorkflowTarget: + def __init__(self) -> None: + self.signal_received: TraceData | None = None + + @workflow.run + async def run(self) -> TraceData: + try: + await workflow.wait_condition(lambda: self.signal_received is not None) + return self.signal_received or TraceData() + except asyncio.CancelledError: + return TraceData() + + @workflow.signal + async def external_signal(self, data: TraceData) -> None: + self.signal_received = data + + +@workflow.defn +class ExternalWorkflowSignaler: + @workflow.run + async def run(self, target_id: str, data: TraceData) -> TraceData: + handle = workflow.get_external_workflow_handle(target_id) + await handle.signal(ExternalWorkflowTarget.external_signal, data) + return data + + +@workflow.defn +class ExternalWorkflowCanceller: + @workflow.run + async def run(self, target_id: str) -> TraceData: + handle = workflow.get_external_workflow_handle(target_id) + await handle.cancel() + return TraceData() + + +@pytest.mark.timeout(10) +async def test_external_workflow_signal_and_cancel_payload_conversion( + client: Client, +): + target_workflow_id = str(uuid.uuid4()) + signaler_workflow_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + + config = client.config() + config["data_converter"] = dataclasses.replace( + DataConverter.default, + payload_converter_class=SerializationContextCompositePayloadConverter, + ) + client = Client(**config) + + async with Worker( + client, + task_queue=task_queue, + workflows=[ + ExternalWorkflowTarget, + ExternalWorkflowSignaler, + ExternalWorkflowCanceller, + ], + activities=[], + workflow_runner=UnsandboxedWorkflowRunner(), # so that we can use isinstance + ): + target_handle = await client.start_workflow( + ExternalWorkflowTarget.run, + id=target_workflow_id, + task_queue=task_queue, + ) + + signaler_handle = await client.start_workflow( + ExternalWorkflowSignaler.run, + args=[target_workflow_id, TraceData()], + id=signaler_workflow_id, + task_queue=task_queue, + ) + + signaler_result = await signaler_handle.result() + await target_handle.result() + + signaler_context = dataclasses.asdict( + WorkflowSerializationContext( + namespace=client.namespace, + workflow_id=signaler_workflow_id, + ) + ) + target_context = dataclasses.asdict( + WorkflowSerializationContext( + namespace=client.namespace, + workflow_id=target_workflow_id, + ) + ) + + assert ( + signaler_result.items + == [ + TraceItem( + method="to_payload", + context=signaler_context, # Outbound signaler workflow input + ), + TraceItem( + method="from_payload", + context=signaler_context, # Inbound signaler workflow input + ), + TraceItem( + method="to_payload", + context=target_context, # Should use target workflow's context for external signal + ), + TraceItem( + method="to_payload", + context=signaler_context, # Outbound signaler workflow result + ), + TraceItem( + method="from_payload", + context=signaler_context, # Inbound signaler workflow result + ), + ] + ) + + +# Failure conversion + + +@activity.defn +async def failing_activity() -> Never: + raise ApplicationError("test error", dataclasses.asdict(TraceData())) + + +@workflow.defn +class FailureConverterTestWorkflow: + @workflow.run + async def run(self) -> Never: + await workflow.execute_activity( + failing_activity, + start_to_close_timeout=timedelta(seconds=10), + retry_policy=RetryPolicy(maximum_attempts=1), + activity_id="activity-id", + ) + raise Exception("Unreachable") + + +test_traces: dict[str | None, list[TraceItem]] = defaultdict(list) + + +class FailureConverterWithContext(DefaultFailureConverter, WithSerializationContext): + def __init__(self): + super().__init__(encode_common_attributes=False) + self.context: SerializationContext | None = None + + def with_context( + self, context: SerializationContext | None + ) -> FailureConverterWithContext: + converter = FailureConverterWithContext() + converter.context = context + return converter + + def to_failure( + self, + exception: BaseException, + payload_converter: PayloadConverter, + failure: temporalio.api.failure.v1.Failure, + ) -> None: + assert isinstance( + self.context, (WorkflowSerializationContext, ActivitySerializationContext) + ) + test_traces[self.context.workflow_id].append( + TraceItem( + method="to_failure", + context=dataclasses.asdict(self.context), + ) + ) + super().to_failure(exception, payload_converter, failure) + + def from_failure( + self, + failure: temporalio.api.failure.v1.Failure, + payload_converter: PayloadConverter, + ) -> BaseException: + assert isinstance( + self.context, (WorkflowSerializationContext, ActivitySerializationContext) + ) + test_traces[self.context.workflow_id].append( + TraceItem( + method="from_failure", + context=dataclasses.asdict(self.context), + ) + ) + return super().from_failure(failure, payload_converter) + + +async def test_failure_converter_with_context(client: Client): + workflow_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + + data_converter = dataclasses.replace( + DataConverter.default, + failure_converter_class=FailureConverterWithContext, + ) + config = client.config() + config["data_converter"] = data_converter + client = Client(**config) + + async with Worker( + client, + task_queue=task_queue, + workflows=[FailureConverterTestWorkflow], + activities=[failing_activity], + workflow_runner=UnsandboxedWorkflowRunner(), + ): + try: + await client.execute_workflow( + FailureConverterTestWorkflow.run, + id=workflow_id, + task_queue=task_queue, + ) + raise AssertionError("unreachable") + except WorkflowFailureError: + pass + + assert isinstance(data_converter.failure_converter, FailureConverterWithContext) + + workflow_context = dataclasses.asdict( + WorkflowSerializationContext( + namespace=client.namespace, + workflow_id=workflow_id, + ) + ) + activity_context = dataclasses.asdict( + ActivitySerializationContext( + namespace=client.namespace, + workflow_id=workflow_id, + workflow_type=FailureConverterTestWorkflow.__name__, + activity_type=failing_activity.__name__, + activity_id="activity-id", + activity_task_queue=task_queue, + is_local=False, + ) + ) + assert test_traces[workflow_id] == ( + [ + TraceItem( + context=activity_context, + method="to_failure", # outbound activity result + ) + ] + + ( + [ + TraceItem( + context=activity_context, + method="from_failure", # inbound activity result + ) + ] + * 2 # from_failure deserializes the error and error cause + ) + + [ + TraceItem( + context=workflow_context, + method="to_failure", # outbound workflow result + ) + ] + + ( + [ + TraceItem( + context=workflow_context, + method="from_failure", # inbound workflow result + ) + ] + * 2 # from_failure deserializes the error and error cause + ) + ) + del test_traces[workflow_id] + + +# Test payload codec + + +class PayloadCodecWithContext(PayloadCodec, WithSerializationContext): + def __init__(self): + self.context: SerializationContext | None = None + self.encode_called_with_context = False + self.decode_called_with_context = False + + def with_context( + self, context: SerializationContext | None + ) -> PayloadCodecWithContext: + codec = PayloadCodecWithContext() + codec.context = context + return codec + + async def encode( + self, payloads: Sequence[temporalio.api.common.v1.Payload] + ) -> list[temporalio.api.common.v1.Payload]: + assert self.context + if isinstance(self.context, ActivitySerializationContext): + test_traces[self.context.workflow_id].append( + TraceItem( + context=dataclasses.asdict(self.context), + method="encode", + ) + ) + else: + assert isinstance(self.context, WorkflowSerializationContext) + test_traces[self.context.workflow_id].append( + TraceItem( + context=dataclasses.asdict(self.context), + method="encode", + ) + ) + return list(payloads) + + async def decode( + self, payloads: Sequence[temporalio.api.common.v1.Payload] + ) -> list[temporalio.api.common.v1.Payload]: + assert self.context + if isinstance(self.context, ActivitySerializationContext): + test_traces[self.context.workflow_id].append( + TraceItem( + context=dataclasses.asdict(self.context), + method="decode", + ) + ) + else: + assert isinstance(self.context, WorkflowSerializationContext) + test_traces[self.context.workflow_id].append( + TraceItem( + context=dataclasses.asdict(self.context), + method="decode", + ) + ) + return list(payloads) + + +@workflow.defn +class CodecTestWorkflow: + @workflow.run + async def run(self, data: str) -> str: + return data + + +async def test_codec_with_context(client: Client): + workflow_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + + client_config = client.config() + client_config["data_converter"] = dataclasses.replace( + DataConverter.default, payload_codec=PayloadCodecWithContext() + ) + client = Client(**client_config) + async with Worker( + client, + task_queue=task_queue, + workflows=[CodecTestWorkflow], + ): + await client.execute_workflow( + CodecTestWorkflow.run, + "data", + id=workflow_id, + task_queue=task_queue, + ) + workflow_context = dataclasses.asdict( + WorkflowSerializationContext( + namespace=client.namespace, + workflow_id=workflow_id, + ) + ) + assert test_traces[workflow_id] == [ + TraceItem( + context=workflow_context, + method="encode", + ), + TraceItem( + context=workflow_context, + method="decode", + ), + TraceItem( + context=workflow_context, + method="encode", + ), + TraceItem( + context=workflow_context, + method="decode", + ), + ] + del test_traces[workflow_id] + + +# Local activity codec test + + +@activity.defn +async def codec_test_local_activity(data: str) -> str: + return data + + +@workflow.defn +class LocalActivityCodecTestWorkflow: + @workflow.run + async def run(self, data: str) -> str: + return await workflow.execute_local_activity( + codec_test_local_activity, + data, + start_to_close_timeout=timedelta(seconds=10), + activity_id="activity-id", + ) + + +async def test_local_activity_codec_with_context(client: Client): + """Test that codec gets correct context with is_local=True for local activities.""" + workflow_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + + client_config = client.config() + client_config["data_converter"] = dataclasses.replace( + DataConverter.default, payload_codec=PayloadCodecWithContext() + ) + client = Client(**client_config) + async with Worker( + client, + task_queue=task_queue, + workflows=[LocalActivityCodecTestWorkflow], + activities=[codec_test_local_activity], + ): + await client.execute_workflow( + LocalActivityCodecTestWorkflow.run, + "data", + id=workflow_id, + task_queue=task_queue, + ) + + workflow_context = dataclasses.asdict( + WorkflowSerializationContext( + namespace=client.namespace, + workflow_id=workflow_id, + ) + ) + local_activity_context = dataclasses.asdict( + ActivitySerializationContext( + namespace=client.namespace, + workflow_id=workflow_id, + workflow_type=LocalActivityCodecTestWorkflow.__name__, + activity_type=codec_test_local_activity.__name__, + activity_id="activity-id", + activity_task_queue=task_queue, + is_local=True, + ) + ) + + assert test_traces[workflow_id] == [ + TraceItem( + context=workflow_context, + method="encode", # outbound workflow input + ), + TraceItem( + context=workflow_context, + method="decode", # inbound workflow input + ), + TraceItem( + context=local_activity_context, + method="encode", # outbound local activity input + ), + TraceItem( + context=local_activity_context, + method="decode", # inbound local activity input + ), + TraceItem( + context=local_activity_context, + method="encode", # outbound local activity result + ), + TraceItem( + context=local_activity_context, + method="decode", # inbound local activity result + ), + TraceItem( + context=workflow_context, + method="encode", # outbound workflow result + ), + TraceItem( + context=workflow_context, + method="decode", # inbound workflow result + ), + ] + del test_traces[workflow_id] + + +# Child workflow codec test + + +@workflow.defn +class ChildWorkflowCodecTestWorkflow: + @workflow.run + async def run(self, data: TraceData) -> TraceData: + return await workflow.execute_child_workflow( + EchoWorkflow.run, + data, + id=f"{workflow.info().workflow_id}-child", + ) + + +async def test_child_workflow_codec_with_context(client: Client): + workflow_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + child_workflow_id = f"{workflow_id}-child" + + config = client.config() + config["data_converter"] = dataclasses.replace( + DataConverter.default, + payload_codec=PayloadCodecWithContext(), + ) + client = Client(**config) + + async with Worker( + client, + task_queue=task_queue, + workflows=[ChildWorkflowCodecTestWorkflow, EchoWorkflow], + workflow_runner=UnsandboxedWorkflowRunner(), + ): + await client.execute_workflow( + ChildWorkflowCodecTestWorkflow.run, + TraceData(), + id=workflow_id, + task_queue=task_queue, + ) + + parent_workflow_context = dataclasses.asdict( + WorkflowSerializationContext( + namespace=client.namespace, + workflow_id=workflow_id, + ) + ) + child_workflow_context = dataclasses.asdict( + WorkflowSerializationContext( + namespace=client.namespace, + workflow_id=child_workflow_id, + ) + ) + + assert test_traces[workflow_id] == [ + TraceItem( + context=parent_workflow_context, + method="encode", # outbound workflow input + ), + TraceItem( + context=parent_workflow_context, + method="decode", # inbound workflow input + ), + TraceItem( + context=parent_workflow_context, + method="encode", # outbound workflow result + ), + TraceItem( + context=parent_workflow_context, + method="decode", # inbound workflow result + ), + ] + assert test_traces[child_workflow_id] == [ + TraceItem( + context=child_workflow_context, + method="encode", # outbound child workflow input + ), + TraceItem( + context=child_workflow_context, + method="decode", # inbound child workflow input + ), + TraceItem( + context=child_workflow_context, + method="encode", # outbound child workflow result + ), + TraceItem( + context=child_workflow_context, + method="decode", # inbound child workflow result + ), + ] + del test_traces[workflow_id] + del test_traces[child_workflow_id] + + +# Payload codec: test decode context matches encode context + + +class PayloadEncryptionCodec(PayloadCodec, WithSerializationContext): + """ + The outbound data for encoding must always be the string "outbound". "Encrypt" it by replacing + it with a key that is derived from the context available during encoding. On decryption, assert + that the same key can be derived from the context available during decoding, and return the + string "inbound". + """ + + def __init__(self): + self.context: SerializationContext | None = None + + def with_context( + self, context: SerializationContext | None + ) -> PayloadEncryptionCodec: + codec = PayloadEncryptionCodec() + codec.context = context + return codec + + async def encode( + self, payloads: Sequence[temporalio.api.common.v1.Payload] + ) -> list[temporalio.api.common.v1.Payload]: + [payload] = payloads + return [ + temporalio.api.common.v1.Payload( + metadata=payload.metadata, + data=json.dumps(self._get_encryption_key()).encode(), + ) + ] + + async def decode( + self, payloads: Sequence[temporalio.api.common.v1.Payload] + ) -> list[temporalio.api.common.v1.Payload]: + [payload] = payloads + assert json.loads(payload.data.decode()) == self._get_encryption_key() + metadata = dict(payload.metadata) + return [temporalio.api.common.v1.Payload(metadata=metadata, data=b'"inbound"')] + + def _get_encryption_key(self) -> str: + context = ( + dataclasses.asdict(self.context) + if isinstance( + self.context, + (WorkflowSerializationContext, ActivitySerializationContext), + ) + else {} + ) + return json.dumps({k: v for k, v in sorted(context.items())}) + + +@activity.defn +async def payload_encryption_activity(data: str) -> str: + assert data == "inbound" + return "outbound" + + +@workflow.defn +class PayloadEncryptionChildWorkflow: + @workflow.run + async def run(self, data: str) -> str: + assert data == "inbound" + return "outbound" + + +@nexusrpc.service +class PayloadEncryptionService: + payload_encryption_operation: nexusrpc.Operation[str, str] + + +@nexusrpc.handler.service_handler +class PayloadEncryptionServiceHandler: + @nexusrpc.handler.sync_operation + async def payload_encryption_operation( + self, _: nexusrpc.handler.StartOperationContext, data: str + ) -> str: + assert data == "inbound" + return "outbound" + + +@workflow.defn +class PayloadEncryptionWorkflow: + def __init__(self): + self.received_signal = False + self.received_update = False + + @workflow.run + async def run(self, _data: str) -> str: + await workflow.wait_condition( + lambda: self.received_signal and self.received_update + ) + # Run them in parallel to check that data converter operations do not mix up contexts when + # there are multiple concurrent payload types. + coros = [ + workflow.execute_activity( + payload_encryption_activity, + "outbound", + start_to_close_timeout=timedelta(seconds=10), + activity_id="activity-id", + ), + workflow.execute_child_workflow( + PayloadEncryptionChildWorkflow.run, + "outbound", + id=f"{workflow.info().workflow_id}_child", + ), + ] + [act_result, cw_result], _ = await workflow.wait( + [asyncio.create_task(c) for c in coros] + ) + assert await act_result == "inbound" + assert await cw_result == "inbound" + return "outbound" + + @workflow.query + def query(self, data: str) -> str: + assert data == "inbound" + return "outbound" + + @workflow.signal + def signal(self, data: str) -> None: + assert data == "inbound" + self.received_signal = True + + @workflow.update + def update(self, data: str) -> str: + assert data == "inbound" + self.received_update = True + return "outbound" + + @update.validator + def update_validator(self, data: str) -> None: + assert data == "inbound" + + +async def test_decode_context_matches_encode_context( + client: Client, +): + """ + Encode outbound payloads with a key using all available context fields, in order to demonstrate + that the same context is available to decode inbound payloads. + """ + workflow_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + + config = client.config() + config["data_converter"] = dataclasses.replace( + DataConverter.default, + payload_codec=PayloadEncryptionCodec(), + ) + client = Client(**config) + + async with Worker( + client, + task_queue=task_queue, + workflows=[PayloadEncryptionWorkflow, PayloadEncryptionChildWorkflow], + activities=[payload_encryption_activity], + nexus_service_handlers=[PayloadEncryptionServiceHandler()], + ): + wf_handle = await client.start_workflow( + PayloadEncryptionWorkflow.run, + "outbound", + id=workflow_id, + task_queue=task_queue, + ) + assert "inbound" == await wf_handle.query( + PayloadEncryptionWorkflow.query, "outbound" + ) + await wf_handle.signal(PayloadEncryptionWorkflow.signal, "outbound") + assert "inbound" == await wf_handle.execute_update( + PayloadEncryptionWorkflow.update, "outbound" + ) + assert "inbound" == await wf_handle.result() + + +# Test nexus payload codec + + +class AssertNexusLacksContextPayloadCodec(PayloadCodec, WithSerializationContext): + def __init__(self): + self.context = None + + def with_context( + self, context: SerializationContext + ) -> AssertNexusLacksContextPayloadCodec: + codec = AssertNexusLacksContextPayloadCodec() + codec.context = context + return codec + + async def _assert_context_iff_not_nexus( + self, payloads: Sequence[temporalio.api.common.v1.Payload] + ) -> list[temporalio.api.common.v1.Payload]: + [payload] = payloads + assert bool(self.context) == (payload.data.decode() != '"nexus-data"') + return list(payloads) + + encode = decode = _assert_context_iff_not_nexus + + +@nexusrpc.handler.service_handler +class NexusOperationTestServiceHandler: + @nexusrpc.handler.sync_operation + async def operation( + self, _: nexusrpc.handler.StartOperationContext, data: str + ) -> str: + return data + + +@workflow.defn +class NexusOperationTestWorkflow: + @workflow.run + async def run(self, _data: str) -> None: + nexus_client = workflow.create_nexus_client( + service=NexusOperationTestServiceHandler, + endpoint=make_nexus_endpoint_name(workflow.info().task_queue), + ) + await nexus_client.start_operation( + NexusOperationTestServiceHandler.operation, input="nexus-data" + ) + + +@pytest.mark.requires_local_server +async def test_nexus_payload_codec_operations_lack_context( + env: WorkflowEnvironment, +): + """ + encode() and decode() on nexus payloads should not have any context set. + """ + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work with the Java test server") + + config = env.client.config() + config["data_converter"] = dataclasses.replace( + DataConverter.default, + payload_codec=AssertNexusLacksContextPayloadCodec(), + ) + client = Client(**config) + + async with Worker( + client, + task_queue=str(uuid.uuid4()), + workflows=[NexusOperationTestWorkflow], + nexus_service_handlers=[NexusOperationTestServiceHandler()], + ) as worker: + endpoint_name = make_nexus_endpoint_name(worker.task_queue) + await env.create_nexus_endpoint(endpoint_name, worker.task_queue) + await client.execute_workflow( + NexusOperationTestWorkflow.run, + "workflow-data", + id=str(uuid.uuid4()), + task_queue=worker.task_queue, + ) + + +# Test pydantic converter with context + + +class PydanticData(BaseModel): + value: str + trace: list[str] = [] + + +class PydanticJSONConverterWithContext( + PydanticJSONPlainPayloadConverter, WithSerializationContext +): + def __init__(self): + super().__init__() + self.context: SerializationContext | None = None + + def with_context( + self, context: SerializationContext | None + ) -> PydanticJSONConverterWithContext: + converter = PydanticJSONConverterWithContext() + converter.context = context + return converter + + def to_payload(self, value: Any) -> temporalio.api.common.v1.Payload | None: + if isinstance(value, PydanticData) and self.context: + if isinstance(self.context, WorkflowSerializationContext): + value.trace.append(f"wf_{self.context.workflow_id}") + return super().to_payload(value) + + +class PydanticConverterWithContext(CompositePayloadConverter, WithSerializationContext): + def __init__(self): + super().__init__( + *( + c + if not isinstance(c, JSONPlainPayloadConverter) + else PydanticJSONConverterWithContext() + for c in DefaultPayloadConverter.default_encoding_payload_converters + ) + ) + self.context: SerializationContext | None = None + + +@workflow.defn +class PydanticContextWorkflow: + @workflow.run + async def run(self, data: PydanticData) -> PydanticData: + return data + + +async def test_pydantic_converter_with_context(client: Client): + wf_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + + client_config = client.config() + client_config["data_converter"] = dataclasses.replace( + DataConverter.default, + payload_converter_class=PydanticConverterWithContext, + ) + client = Client(**client_config) + + async with Worker( + client, + task_queue=task_queue, + workflows=[PydanticContextWorkflow], + ): + result = await client.execute_workflow( + PydanticContextWorkflow.run, + PydanticData(value="test"), + id=wf_id, + task_queue=task_queue, + ) + assert f"wf_{wf_id}" in result.trace + + +# Test customized DefaultPayloadConverter + +# The SDK's CompositePayloadConverter comes with a with_context implementation that ensures that its +# component EncodingPayloadConverters will be replaced with the results of calling with_context() on +# them, if they support with_context (this happens when we call data_converter._with_context). In +# this test, the user has subclassed CompositePayloadConverter. The test confirms that the +# CompositePayloadConverter's with_context yields an instance of the user's subclass. + + +class UserMethodCalledError(Exception): + pass + + +class CustomEncodingPayloadConverter( + JSONPlainPayloadConverter, WithSerializationContext +): + @property + def encoding(self) -> str: + return "custom-encoding-that-does-not-clash-with-default-converters" + + def __init__(self): + super().__init__() + self.context: SerializationContext | None = None + + def with_context( + self, context: SerializationContext | None + ) -> CustomEncodingPayloadConverter: + converter = CustomEncodingPayloadConverter() + converter.context = context + return converter + + +class CustomPayloadConverter(CompositePayloadConverter): + def __init__(self): + # Add a context-aware EncodingPayloadConverter so that + # CompositePayloadConverter.with_context is forced to construct and return a new instance. + super().__init__( + CustomEncodingPayloadConverter(), + *DefaultPayloadConverter.default_encoding_payload_converters, + ) + + def to_payloads( + self, values: Sequence[Any] + ) -> list[temporalio.api.common.v1.Payload]: + raise UserMethodCalledError + + def from_payloads( + self, + payloads: Sequence[temporalio.api.common.v1.Payload], + type_hints: list[type] | None = None, + ) -> list[Any]: + raise NotImplementedError + + +async def test_user_customization_of_default_payload_converter( + client: Client, +): + wf_id = str(uuid.uuid4()) + task_queue = str(uuid.uuid4()) + + client_config = client.config() + client_config["data_converter"] = dataclasses.replace( + DataConverter.default, + payload_converter_class=CustomPayloadConverter, + ) + client = Client(**client_config) + + async with Worker( + client, + task_queue=task_queue, + workflows=[EchoWorkflow], + ): + with pytest.raises(UserMethodCalledError): + await client.execute_workflow( + EchoWorkflow.run, + TraceData(), + id=wf_id, + task_queue=task_queue, + ) diff --git a/tests/test_service.py b/tests/test_service.py index 374ae0869..954c87092 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -1,24 +1,37 @@ import inspect import os import re -from typing import Any, Callable, Dict, Mapping, Tuple, Type +from collections.abc import Callable, Mapping +from datetime import timedelta +from typing import Any import google.protobuf.empty_pb2 import google.protobuf.message +import google.protobuf.symbol_database import grpc import pytest +from google.protobuf.descriptor import FileDescriptor, MethodDescriptor import temporalio import temporalio.api.cloud.cloudservice.v1 +import temporalio.api.cloud.cloudservice.v1.service_pb2 import temporalio.api.errordetails.v1 import temporalio.api.operatorservice.v1 +import temporalio.api.operatorservice.v1.service_pb2 import temporalio.api.testservice.v1 +import temporalio.api.testservice.v1.service_pb2 import temporalio.api.workflowservice.v1 +import temporalio.api.workflowservice.v1.service_pb2 +import temporalio.bridge.proto.health.v1.health_pb2 import temporalio.service from temporalio.client import Client from temporalio.testing import WorkflowEnvironment +def _camel_to_snake(name: str) -> str: + return re.sub(r"(? None: # Collect service calls - service_calls: Dict[str, Tuple[Type, Type]] = {} - for _, call in inspect.getmembers(service): - if isinstance(call, temporalio.service.ServiceCall): - service_calls[call.name] = (call.req_type, call.resp_type) + service_calls = set() + for name, _call in inspect.getmembers(service): + # ignore private methods and non-rpc members "client" and "service" + if name[0] != "_" and name != "client" and name != "service": + service_calls.add(name) # Collect gRPC service calls with a fake channel - channel = CallCollectingChannel(package, custom_req_resp) + channel = CallCollectingChannel(package, custom_req_resp) # type: ignore new_stub(channel) # Confirm they are the same - missing = channel.calls.keys() - service_calls.keys() + missing = channel.calls.keys() - service_calls assert not missing - added = service_calls.keys() - channel.calls.keys() + added = service_calls - channel.calls.keys() assert not added assert_all_calls_present( @@ -91,18 +105,18 @@ def __init__( package: Any, custom_req_resp: Mapping[ str, - Tuple[ - Type[google.protobuf.message.Message], - Type[google.protobuf.message.Message], + tuple[ + type[google.protobuf.message.Message], + type[google.protobuf.message.Message], ], ], ) -> None: super().__init__() self.package = package self.custom_req_resp = custom_req_resp - self.calls: Dict[str, Tuple[Type, Type]] = {} + self.calls: dict[str, tuple[type, type]] = {} - def unary_unary(self, method, request_serializer, response_deserializer): + def unary_unary(self, method, request_serializer, response_deserializer): # type: ignore[reportIncompatibleMethodOverride] # Last part after slash name = method.rsplit("/", 1)[-1] req_resp = self.custom_req_resp.get(name, None) or ( @@ -110,18 +124,16 @@ def unary_unary(self, method, request_serializer, response_deserializer): getattr(self.package, name + "Response"), ) # Camel to snake case - name = re.sub(r"(? _Handshake: + """Attempt a connection and return the server's view of the handshake.""" + config = temporalio.service.ConnectConfig( + target_host=f"localhost:{server.port}", tls=tls + ) + # The connect always fails since this server speaks no gRPC; only whether + # the TLS handshake completed matters. + try: + await asyncio.wait_for(temporalio.service.ServiceClient.connect(config), 20) + except asyncio.TimeoutError: + raise + except Exception: + pass + else: + pytest.fail("connect unexpectedly succeeded") + return await server.next_handshake() + + +class _TlsServer: + """TLS server that records handshake outcomes and the SNI it receives.""" + + def __init__(self, certs: Path) -> None: + self.ca_pem = (certs / "ca.pem").read_bytes() + self._ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + self._ctx.load_cert_chain(certs / "srv.pem", certs / "srv.key") + self._ctx.set_alpn_protocols(["h2"]) + # Populated by the SNI callback for the connection currently + # handshaking; the accept loop is single-threaded. + self._sni: str | None = None + + def on_sni( + _sock: ssl.SSLObject, name: str | None, _ctx: ssl.SSLContext + ) -> None: + self._sni = name + + self._ctx.sni_callback = on_sni + self._handshakes: list[_Handshake] = [] + self._lock = threading.Lock() + self._stop = threading.Event() + self._sock = socket.create_server(("127.0.0.1", 0)) + self._sock.settimeout(0.1) + self.port: int = self._sock.getsockname()[1] + self._thread = threading.Thread(target=self._serve, daemon=True) + self._thread.start() + + def _serve(self) -> None: + while not self._stop.is_set(): + try: + conn, _ = self._sock.accept() + except TimeoutError: + continue + conn.settimeout(10) # A stalled handshake must not wedge this loop. + self._sni = None + try: + tls = self._ctx.wrap_socket(conn, server_side=True) + except (ssl.SSLError, OSError): + conn.close() + self._record(_Handshake(ok=False, sni=self._sni)) + continue + self._record(_Handshake(ok=True, sni=self._sni)) + tls.close() + self._sock.close() + + def _record(self, handshake: _Handshake) -> None: + with self._lock: + self._handshakes.append(handshake) + + async def next_handshake(self) -> _Handshake: + """Wait for and consume the next recorded handshake.""" + # The client can report its result before this thread has recorded + # the outcome, so wait for the observation to land. + for _ in range(200): + with self._lock: + if self._handshakes: + return self._handshakes.pop(0) + await asyncio.sleep(0.05) + raise AssertionError("server observed no TLS handshake") + + def close(self) -> None: + self._stop.set() + self._thread.join(timeout=2) + + +@pytest_asyncio.fixture # type: ignore[reportUntypedFunctionDecorator] +async def tls_server(tmp_path: Path) -> AsyncIterator[_TlsServer]: + _write_pinned_certs(tmp_path) + server = _TlsServer(tmp_path) + try: + yield server + finally: + server.close() + + +def _write_pinned_certs(path: Path) -> None: + """Write a CA and a server cert (chained to it) valid only for ``pinned.test``.""" + now = datetime.datetime.now(datetime.timezone.utc) + ca_key = ec.generate_private_key(ec.SECP256R1()) + ca_name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "test-ca")]) + ca_cert = ( + x509.CertificateBuilder() + .subject_name(ca_name) + .issuer_name(ca_name) + .public_key(ca_key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(now - datetime.timedelta(days=1)) + .not_valid_after(now + datetime.timedelta(days=7)) + .add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True) + .sign(ca_key, hashes.SHA256()) + ) + server_key = ec.generate_private_key(ec.SECP256R1()) + server_cert = ( + x509.CertificateBuilder() + .subject_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, PINNED_NAME)])) + .issuer_name(ca_name) + .public_key(server_key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(now - datetime.timedelta(days=1)) + .not_valid_after(now + datetime.timedelta(days=7)) + .add_extension( + x509.SubjectAlternativeName([x509.DNSName(PINNED_NAME)]), critical=False + ) + .sign(ca_key, hashes.SHA256()) + ) + (path / "ca.pem").write_bytes(ca_cert.public_bytes(serialization.Encoding.PEM)) + (path / "srv.pem").write_bytes(server_cert.public_bytes(serialization.Encoding.PEM)) + (path / "srv.key").write_bytes( + server_key.private_bytes( + serialization.Encoding.PEM, + serialization.PrivateFormat.PKCS8, + serialization.NoEncryption(), + ) + ) diff --git a/tests/test_type_errors.py b/tests/test_type_errors.py index d8e6e2afb..2b70d7f63 100644 --- a/tests/test_type_errors.py +++ b/tests/test_type_errors.py @@ -86,7 +86,7 @@ def _test_type_errors( def _has_type_error_assertions(test_file: Path) -> bool: """Check if a file contains any type error assertions.""" - with open(test_file) as f: + with open(test_file, encoding="utf-8") as f: return any(re.search(r"# assert-type-error-\w+:", line) for line in f) @@ -94,7 +94,7 @@ def _get_expected_errors(test_file: Path, type_checker: str) -> dict[int, str]: """Parse expected type errors from comments in a file for the specified type checker.""" expected_errors = {} - with open(test_file) as f: + with open(test_file, encoding="utf-8") as f: lines = zip(itertools.count(1), f) for line_num, line in lines: if match := re.search( diff --git a/tests/test_workflow.py b/tests/test_workflow.py index 00233cded..5618c34f5 100644 --- a/tests/test_workflow.py +++ b/tests/test_workflow.py @@ -1,19 +1,18 @@ import inspect import itertools -from typing import Any, Callable, Sequence, Set, Type, get_type_hints +import typing +from collections.abc import Callable, Sequence +from typing import Any, get_type_hints + +import pytest from temporalio import workflow from temporalio.common import RawValue, VersioningBehavior -# Passing through because Python 3.9 has an import bug at -# https://github.com/python/cpython/issues/91351 -with workflow.unsafe.imports_passed_through(): - import pytest - class GoodDefnBase: @workflow.run - async def run(self, name: str) -> str: + async def run(self, _name: str) -> str: raise NotImplementedError @workflow.signal @@ -32,7 +31,7 @@ def base_update(self): @workflow.defn(name="workflow-custom") class GoodDefn(GoodDefnBase): @workflow.run - async def run(self, name: str) -> str: + async def run(self, _name: str) -> str: raise NotImplementedError @workflow.signal @@ -44,7 +43,7 @@ def signal2(self): pass @workflow.signal(dynamic=True, description="boo") - def signal3(self, name: str, args: Sequence[RawValue]): + def signal3(self, _name: str, _args: Sequence[RawValue]): pass @workflow.query @@ -56,7 +55,7 @@ def query2(self): pass @workflow.query(dynamic=True, description="dqd") - def query3(self, name: str, args: Sequence[RawValue]): + def query3(self, _name: str, _args: Sequence[RawValue]): pass @workflow.update @@ -68,7 +67,28 @@ def update2(self): pass @workflow.update(dynamic=True, description="dud") - def update3(self, name: str, args: Sequence[RawValue]): + def update3(self, _name: str, _args: Sequence[RawValue]): + pass + + +@workflow.defn() +class GoodDefnDeprecatedTypes(GoodDefnBase): + # Just having the definition here is enough to confirm the signatures + # do not trigger a RuntimeError + @workflow.run + async def run(self, _name: str) -> str: + raise NotImplementedError + + @workflow.signal(dynamic=True) + def signal(self, _name: str, _args: typing.Sequence[RawValue]): # type: ignore[reportDeprecated] + pass + + @workflow.query(dynamic=True) + def query(self, _name: str, _args: typing.Sequence[RawValue]): # type: ignore[reportDeprecated] + pass + + @workflow.update(dynamic=True) + def update(self, _name: str, _args: typing.Sequence[RawValue]): # type: ignore[reportDeprecated] pass @@ -140,7 +160,7 @@ def test_workflow_defn_good(): @workflow.defn(versioning_behavior=VersioningBehavior.PINNED) class VersioningBehaviorDefn: @workflow.run - async def run(self, name: str) -> str: + async def run(self, _name: str) -> str: raise NotImplementedError @@ -185,11 +205,11 @@ def signal2(self): pass @workflow.signal(dynamic=True) - def signal3(self, name: str, args: Sequence[RawValue]): + def signal3(self, _name: str, _args: Sequence[RawValue]): pass @workflow.signal(dynamic=True) - def signal4(self, name: str, args: Sequence[RawValue]): + def signal4(self, _name: str, _args: Sequence[RawValue]): pass # Intentionally missing decorator @@ -205,11 +225,11 @@ def query2(self): pass @workflow.query(dynamic=True) - def query3(self, name: str, args: Sequence[RawValue]): + def query3(self, _name: str, _args: Sequence[RawValue]): pass @workflow.query(dynamic=True) - def query4(self, name: str, args: Sequence[RawValue]): + def query4(self, _name: str, _args: Sequence[RawValue]): pass # Intentionally missing decorator @@ -217,15 +237,15 @@ def base_query(self): pass @workflow.update - def update1(self, arg1: str): + def update1(self, _arg1: str): pass @workflow.update(name="update1") - def update2(self, arg1: str): + def update2(self, _arg1: str): pass # Intentionally missing decorator - def base_update(self): # type: ignore[reportIncompatibleVariableOverride] + def base_update(self): # type: ignore[override] pass @@ -273,7 +293,7 @@ def test_workflow_defn_local_class(): with pytest.raises(ValueError) as err: @workflow.defn - class LocalClass: + class LocalClass: # type:ignore[reportUnusedClass] @workflow.run async def run(self): pass @@ -389,31 +409,31 @@ def a1(self, a): # type: ignore[reportMissingParameterType] def a2(self, b): # type: ignore[reportMissingParameterType] pass - def b1(self, a: int): + def b1(self, _a: int): pass - def b2(self, b: int) -> str: + def b2(self, _b: int) -> str: return "" - def c1(self, a1: int, a2: str) -> str: + def c1(self, _a1: int, _a2: str) -> str: return "" - def c2(self, b1: int, b2: str) -> int: + def c2(self, _b1: int, _b2: str) -> int: return 0 - def d1(self, a1, a2: str) -> None: # type: ignore[reportMissingParameterType] + def d1(self, _a1, _a2: str) -> None: # type: ignore[reportMissingParameterType] pass - def d2(self, b1, b2: str) -> str: # type: ignore[reportMissingParameterType] + def d2(self, _b1, _b2: str) -> str: # type: ignore[reportMissingParameterType] return "" - def e1(self, a1, a2: str = "") -> None: # type: ignore[reportMissingParameterType] + def e1(self, _a1, _a2: str = "") -> None: # type: ignore[reportMissingParameterType] return None - def e2(self, b1, b2: str = "") -> str: # type: ignore[reportMissingParameterType] + def e2(self, _b1, _b2: str = "") -> str: # type: ignore[reportMissingParameterType] return "" - def f1(self, a1, a2: str = "a") -> None: # type: ignore[reportMissingParameterType] + def f1(self, _a1, _a2: str = "a") -> None: # type: ignore[reportMissingParameterType] return None @@ -426,9 +446,9 @@ def test_parameters_identical_up_to_naming(): for f1, f2 in itertools.combinations(fns, 2): name1, name2 = f1.__name__, f2.__name__ expect_equal = name1[0] == name2[0] - assert ( - workflow._parameters_identical_up_to_naming(f1, f2) == (expect_equal) - ), f"expected {name1} and {name2} parameters{' ' if expect_equal else ' not '}to compare equal" + assert workflow._parameters_identical_up_to_naming(f1, f2) == (expect_equal), ( + f"expected {name1} and {name2} parameters{' ' if expect_equal else ' not '}to compare equal" + ) @workflow.defn @@ -449,12 +469,12 @@ def test_workflow_init_not__init__(): class BadUpdateValidator: @workflow.update - def my_update(self, a: str): + def my_update(self, _a: str): pass # assert-type-error-pyright: "Argument of type .+ cannot be assigned to parameter" @my_update.validator # type: ignore - def my_validator(self, a: int): + def my_validator(self, _a: int): pass @workflow.run @@ -473,10 +493,9 @@ def test_workflow_update_validator_not_update(): def _assert_config_function_parity( function_obj: Callable[..., Any], - config_class: Type[Any], - excluded_params: Set[str], + config_class: type[Any], + excluded_params: set[str], ) -> None: - function_name = function_obj.__name__ config_name = config_class.__name__ # Get the signature and type hints @@ -484,14 +503,14 @@ def _assert_config_function_parity( config_hints = get_type_hints(config_class) # Get parameter names from function (excluding excluded ones and applying mappings) - expected_config_params = set( - [name for name in function_sig.parameters.keys() if name not in excluded_params] - ) + expected_config_params = { + name for name in function_sig.parameters.keys() if name not in excluded_params + } # Get parameter names from config - actual_config_params = set( - [name for name in config_hints.keys() if name not in excluded_params] - ) + actual_config_params = { + name for name in config_hints.keys() if name not in excluded_params + } # Check for missing and extra parameters missing_in_config = expected_config_params - actual_config_params diff --git a/tests/test_workflow_exports.py b/tests/test_workflow_exports.py new file mode 100644 index 000000000..8788addc5 --- /dev/null +++ b/tests/test_workflow_exports.py @@ -0,0 +1,217 @@ +import temporalio.workflow + +# Generated from temporalio.workflow on main +EXPECTED_WORKFLOW_EXPORTS = [ + "ActivityCancellationType", + "ActivityConfig", + "ActivityHandle", + "AnyType", + "CallableAsyncNoParam", + "CallableAsyncSingleParam", + "CallableAsyncType", + "CallableSyncNoParam", + "CallableSyncOrAsyncReturnNoneType", + "CallableSyncOrAsyncType", + "CallableSyncSingleParam", + "CallableType", + "ChildWorkflowCancellationType", + "ChildWorkflowConfig", + "ChildWorkflowHandle", + "ClassType", + "ContinueAsNewError", + "ContinueAsNewVersioningBehavior", + "DynamicWorkflowConfig", + "ExternalWorkflowHandle", + "HandlerUnfinishedPolicy", + "Info", + "LocalActivityConfig", + "LoggerAdapter", + "MethodAsyncNoParam", + "MethodAsyncSingleParam", + "MethodSyncNoParam", + "MethodSyncOrAsyncNoParam", + "MethodSyncOrAsyncSingleParam", + "MethodSyncSingleParam", + "MultiParamSpec", + "NexusClient", + "NexusOperationCancellationType", + "NexusOperationHandle", + "NondeterminismError", + "ParamType", + "ParentClosePolicy", + "ParentInfo", + "ProtocolReturnType", + "ReadOnlyContextError", + "ReturnType", + "RootInfo", + "SandboxImportNotificationPolicy", + "SelfType", + "UnfinishedSignalHandlersWarning", + "UnfinishedUpdateHandlersWarning", + "UpdateInfo", + "UpdateMethodMultiParam", + "VersioningIntent", + "_AsyncioTask", + "_Definition", + "_FT", + "_NexusClient", + "_NotInWorkflowEventLoopError", + "_QueryDefinition", + "_Runtime", + "_SignalDefinition", + "_UpdateDefinition", + "_assert_dynamic_handler_args", + "_bind_method", + "_build_log_context", + "_current_update_info", + "_imports_passed_through", + "_in_sandbox", + "_is_unbound_method_on_cls", + "_parameters_identical_up_to_naming", + "_release_waiter", + "_sandbox_import_notification_policy_override", + "_sandbox_unrestricted", + "_set_current_update_info", + "_update_validator", + "_wait", + "all_handlers_finished", + "annotations", + "as_completed", + "continue_as_new", + "create_nexus_client", + "current_update_info", + "defn", + "deprecate_patch", + "dynamic_config", + "execute_activity", + "execute_activity_class", + "execute_activity_method", + "execute_child_workflow", + "execute_local_activity", + "execute_local_activity_class", + "execute_local_activity_method", + "extern_functions", + "get_current_details", + "get_dynamic_query_handler", + "get_dynamic_signal_handler", + "get_dynamic_update_handler", + "get_external_workflow_handle", + "get_external_workflow_handle_for", + "get_last_completion_result", + "get_last_failure", + "get_query_handler", + "get_signal_handler", + "get_update_handler", + "has_last_completion_result", + "in_workflow", + "info", + "init", + "instance", + "is_failure_exception", + "logger", + "memo", + "memo_value", + "metric_meter", + "new_random", + "now", + "patched", + "payload_converter", + "query", + "random", + "random_seed", + "register_random_seed_callback", + "run", + "set_current_details", + "set_dynamic_query_handler", + "set_dynamic_signal_handler", + "set_dynamic_update_handler", + "set_query_handler", + "set_signal_handler", + "set_update_handler", + "signal", + "sleep", + "start_activity", + "start_activity_class", + "start_activity_method", + "start_child_workflow", + "start_local_activity", + "start_local_activity_class", + "start_local_activity_method", + "time", + "time_ns", + "unsafe", + "update", + "upsert_memo", + "upsert_search_attributes", + "uuid4", + "wait", + "wait_condition", +] + + +EXPECTED_INTENTIONALLY_REMOVED_WORKFLOW_EXPORTS = [ + "ABC", + "Any", + "Awaitable", + "Callable", + "Concatenate", + "Enum", + "Flag", + "Generator", + "Generic", + "InputT", + "IntEnum", + "Iterable", + "Iterator", + "Literal", + "Mapping", + "MutableMapping", + "NoReturn", + "OutputT", + "Protocol", + "Random", + "Sequence", + "TYPE_CHECKING", + "TypeVar", + "TypedDict", + "abstractmethod", + "asyncio", + "auto", + "cast", + "contextmanager", + "contextvars", + "dataclass", + "datetime", + "inspect", + "logging", + "nexusrpc", + "overload", + "partial", + "runtime_checkable", + "sys", + "temporalio", + "threading", + "timedelta", + "timezone", + "typing", + "uuid", + "warnings", +] + + +def test_workflow_module_exports_match_main() -> None: + missing = [ + name + for name in EXPECTED_WORKFLOW_EXPORTS + if not hasattr(temporalio.workflow, name) + ] + assert not missing + + +def test_workflow_module_drops_intentionally_removed_import_exports() -> None: + exported = [ + name + for name in EXPECTED_INTENTIONALLY_REMOVED_WORKFLOW_EXPORTS + if hasattr(temporalio.workflow, name) + ] + assert not exported diff --git a/tests/testing/test_activity.py b/tests/testing/test_activity.py index 2acf93639..71ba7f590 100644 --- a/tests/testing/test_activity.py +++ b/tests/testing/test_activity.py @@ -167,3 +167,47 @@ def my_activity() -> None: env = ActivityEnvironment(client=Mock(spec=Client)) env.run(my_activity) assert saw_error + + +async def test_activity_logger_does_not_mutate_caller_extra(): + """Regression test for https://github.com/temporalio/sdk-python/issues/503. + + The activity LoggerAdapter must not mutate the ``extra`` dict provided by + the caller; it should merge its temporal context into a fresh dict. + """ + import logging + import logging.handlers + import queue + + handler = logging.handlers.QueueHandler(queue.Queue()) + activity.logger.base_logger.addHandler(handler) + previous_level = activity.logger.base_logger.level + activity.logger.base_logger.setLevel(logging.INFO) + previous_full = activity.logger.full_activity_info_on_extra + activity.logger.full_activity_info_on_extra = True + + try: + + def log_with_extra() -> dict: + caller_extra = {"request_id": "req-1"} + activity.logger.info("hi", extra=caller_extra) + return caller_extra + + env = ActivityEnvironment() + caller_extra = env.run(log_with_extra) + finally: + activity.logger.base_logger.removeHandler(handler) + activity.logger.base_logger.setLevel(previous_level) + activity.logger.full_activity_info_on_extra = previous_full + + # The caller's dict must be untouched. + assert caller_extra == {"request_id": "req-1"} + + # But the emitted record should still carry the temporal-injected fields + # alongside the caller-provided one. + records: list[logging.LogRecord] = list(handler.queue.queue) # type: ignore[attr-defined] + assert records, "expected one log record" + record = records[-1] + assert record.__dict__["request_id"] == "req-1" + assert record.__dict__["temporal_activity"]["activity_type"] == "unknown" + assert "activity_info" in record.__dict__ diff --git a/tests/testing/test_workflow.py b/tests/testing/test_workflow.py index 7915da8f6..b47d0ac05 100644 --- a/tests/testing/test_workflow.py +++ b/tests/testing/test_workflow.py @@ -1,17 +1,17 @@ import asyncio import platform -import sys import uuid from datetime import datetime, timedelta, timezone from time import monotonic -from typing import Any, Optional, Union +from typing import Any + +import pytest from temporalio import activity, workflow from temporalio.client import ( Client, Interceptor, OutboundInterceptor, - RPCError, StartWorkflowInput, WorkflowFailureError, WorkflowHandle, @@ -28,15 +28,11 @@ TimeoutError, TimeoutType, ) +from temporalio.service import RPCError from temporalio.testing import WorkflowEnvironment from tests import DEV_SERVER_DOWNLOAD_VERSION from tests.helpers import new_worker -# Passing through because Python 3.9 has an import bug at -# https://github.com/python/cpython/issues/91351 -with workflow.unsafe.imports_passed_through(): - import pytest - @workflow.defn class ReallySlowWorkflow: @@ -223,14 +219,12 @@ async def test_workflow_env_assert(client: Client): client_config["interceptors"] = [interceptor] client = Client(**client_config) - def assert_proper_error(err: Optional[BaseException]) -> None: + def assert_proper_error(err: BaseException | None) -> None: assert isinstance(err, ApplicationError) # In unsandboxed workflows, this message has extra diff info appended # due to pytest's custom loader that does special assert tricks. But in # sandboxed workflows, this just has the first line. - # The plain asserter is used for 3.9 & below due to import issues - if sys.version_info[:2] > (3, 9): - assert err.message.startswith("assert 'foo' == 'bar'") + assert err.message.startswith("assert 'foo' == 'bar'") async with WorkflowEnvironment.from_client(client) as env: async with new_worker(env.client, AssertFailWorkflow) as worker: @@ -258,6 +252,7 @@ def assert_proper_error(err: Optional[BaseException]) -> None: assert_proper_error(err.value.cause) +@pytest.mark.requires_local_server async def test_search_attributes_on_dev_server( client: Client, env: WorkflowEnvironment ): @@ -323,8 +318,18 @@ async def test_search_attributes_on_dev_server( assert attrs == desc.typed_search_attributes +async def test_ui_port(): + """Test that ui_port parameter works correctly.""" + async with await WorkflowEnvironment.start_local( + ui=True, + ui_port=18080, + ) as env: + # Just verify it starts without error + assert env.client is not None + + def assert_timestamp_from_now( - ts: Union[datetime, float], expected_from_now: float, max_delta: float = 30 + ts: datetime | float, expected_from_now: float, max_delta: float = 30 ) -> None: if isinstance(ts, datetime): ts = ts.timestamp() diff --git a/tests/worker/test_activity.py b/tests/worker/test_activity.py index 7868fc281..64691a93f 100644 --- a/tests/worker/test_activity.py +++ b/tests/worker/test_activity.py @@ -2,22 +2,26 @@ import concurrent.futures import logging import logging.handlers -import multiprocessing import os import queue import signal import threading import time import uuid +from collections.abc import Callable, Sequence from concurrent.futures import ThreadPoolExecutor from concurrent.futures.process import BrokenProcessPool from contextvars import ContextVar from dataclasses import dataclass from datetime import datetime, timedelta, timezone from time import sleep -from typing import Any, Callable, List, NoReturn, Optional, Sequence, Type +from typing import Any, NoReturn +import pytest + +import temporalio.api.common.v1 import temporalio.api.workflowservice.v1 +import temporalio.exceptions from temporalio import activity, workflow from temporalio.client import ( AsyncActivityHandle, @@ -42,49 +46,63 @@ Worker, WorkerConfig, ) +from tests.helpers import LogHandler from tests.helpers.worker import ( ExternalWorker, KSAction, KSExecuteActivityAction, KSWorkflowParams, -) - -# Passing through because Python 3.9 has an import bug at -# https://github.com/python/cpython/issues/91351 -with workflow.unsafe.imports_passed_through(): - import pytest - - -_default_shared_state_manager = SharedStateManager.create_from_multiprocessing( - multiprocessing.Manager() + kitchen_sink_retry_policy, ) default_max_concurrent_activities = 50 -async def test_activity_hello(client: Client, worker: ExternalWorker): +async def test_activity_hello( + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, +): @activity.defn async def say_hello(name: str) -> str: return f"Hello, {name}!" result = await _execute_workflow_with_activity( - client, worker, say_hello, "Temporal" + client, + worker, + say_hello, + "Temporal", + shared_state_manager=shared_state_manager, ) assert result.result == "Hello, Temporal!" -async def test_activity_without_decorator(client: Client, worker: ExternalWorker): +async def test_activity_without_decorator( + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, +): async def say_hello(name: str) -> str: return f"Hello, {name}!" with pytest.raises(TypeError) as err: - await _execute_workflow_with_activity(client, worker, say_hello, "Temporal") + await _execute_workflow_with_activity( + client, + worker, + say_hello, + "Temporal", + shared_state_manager=shared_state_manager, + ) assert "Activity say_hello missing attributes" in str(err.value) -async def test_activity_custom_name(client: Client, worker: ExternalWorker): +async def test_activity_custom_name( + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, +): @activity.defn(name="my custom activity name!") - async def get_name(name: str) -> str: + async def get_name(_name: str) -> str: return f"Name: {activity.info().activity_type}" result = await _execute_workflow_with_activity( @@ -93,29 +111,39 @@ async def get_name(name: str) -> str: get_name, "Temporal", activity_name_override="my custom activity name!", + shared_state_manager=shared_state_manager, ) assert result.result == "Name: my custom activity name!" async def test_client_available_in_async_activities( - client: Client, worker: ExternalWorker + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, ): with pytest.raises(RuntimeError, match="Not in activity context"): activity.client() - captured_client: Optional[Client] = None + captured_client: Client | None = None @activity.defn async def capture_client() -> None: nonlocal captured_client captured_client = activity.client() - await _execute_workflow_with_activity(client, worker, capture_client) + await _execute_workflow_with_activity( + client, + worker, + capture_client, + shared_state_manager=shared_state_manager, + ) assert captured_client is client async def test_client_not_available_in_sync_activities( - client: Client, worker: ExternalWorker + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, ): saw_error = False @@ -136,12 +164,16 @@ def some_activity() -> None: "activity_executor": concurrent.futures.ThreadPoolExecutor(1), "max_concurrent_activities": 1, }, + shared_state_manager=shared_state_manager, ) assert saw_error async def test_activity_info( - client: Client, worker: ExternalWorker, env: WorkflowEnvironment + client: Client, + worker: ExternalWorker, + env: WorkflowEnvironment, + shared_state_manager: SharedStateManager, ): # TODO(cretz): Fix if env.supports_time_skipping: @@ -155,7 +187,7 @@ async def test_activity_info( assert str(err.value) == "Not in activity context" # Capture the info from the activity - info: Optional[activity.Info] = None + info: activity.Info | None = None @activity.defn async def capture_info() -> None: @@ -163,11 +195,15 @@ async def capture_info() -> None: info = activity.info() result = await _execute_workflow_with_activity( - client, worker, capture_info, start_to_close_timeout_ms=4000 + client, + worker, + capture_info, + start_to_close_timeout_ms=4000, + shared_state_manager=shared_state_manager, ) assert info - assert info.activity_id + assert info.activity_id # type:ignore[reportUnreachable] assert info.activity_type == "capture_info" assert info.attempt == 1 assert abs( @@ -186,9 +222,14 @@ async def capture_info() -> None: assert info.workflow_namespace == client.namespace assert info.workflow_run_id == result.handle.first_execution_run_id assert info.workflow_type == "kitchen_sink" + assert info.retry_policy == kitchen_sink_retry_policy() -async def test_sync_activity_thread(client: Client, worker: ExternalWorker): +async def test_sync_activity_thread( + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, +): @activity.defn def some_activity() -> str: return f"activity name: {activity.info().activity_type}" @@ -205,6 +246,7 @@ def some_activity() -> str: worker, some_activity, worker_config={"activity_executor": executor}, + shared_state_manager=shared_state_manager, ) assert result.result == "activity name: some_activity" @@ -214,7 +256,11 @@ def picklable_activity() -> str: return f"activity name: {activity.info().activity_type}" -async def test_sync_activity_process(client: Client, worker: ExternalWorker): +async def test_sync_activity_process( + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, +): # We intentionally leave max_workers by default in the process pool executor # to confirm that the warning is triggered with concurrent.futures.ProcessPoolExecutor() as executor: @@ -227,12 +273,15 @@ async def test_sync_activity_process(client: Client, worker: ExternalWorker): worker, picklable_activity, worker_config={"activity_executor": executor}, + shared_state_manager=shared_state_manager, ) assert result.result == "activity name: picklable_activity" async def test_sync_activity_process_non_picklable( - client: Client, worker: ExternalWorker + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, ): @activity.defn def some_activity() -> str: @@ -245,17 +294,27 @@ def some_activity() -> str: worker, some_activity, worker_config={"activity_executor": executor}, + shared_state_manager=shared_state_manager, ) assert "must be picklable when using a process executor" in str(err.value) -async def test_activity_failure(client: Client, worker: ExternalWorker): +async def test_activity_failure( + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, +): @activity.defn async def raise_error(): raise RuntimeError("oh no!") with pytest.raises(WorkflowFailureError) as err: - await _execute_workflow_with_activity(client, worker, raise_error) + await _execute_workflow_with_activity( + client, + worker, + raise_error, + shared_state_manager=shared_state_manager, + ) assert str(assert_activity_application_error(err.value)) == "RuntimeError: oh no!" @@ -264,7 +323,11 @@ def picklable_activity_failure(): raise RuntimeError("oh no!") -async def test_sync_activity_process_failure(client: Client, worker: ExternalWorker): +async def test_sync_activity_process_failure( + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, +): with pytest.raises(WorkflowFailureError) as err: with concurrent.futures.ProcessPoolExecutor() as executor: await _execute_workflow_with_activity( @@ -272,17 +335,27 @@ async def test_sync_activity_process_failure(client: Client, worker: ExternalWor worker, picklable_activity_failure, worker_config={"activity_executor": executor}, + shared_state_manager=shared_state_manager, ) assert str(assert_activity_application_error(err.value)) == "RuntimeError: oh no!" -async def test_activity_bad_params(client: Client, worker: ExternalWorker): +async def test_activity_bad_params( + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, +): @activity.defn async def say_hello(name: str) -> str: return f"Hello, {name}!" with pytest.raises(WorkflowFailureError) as err: - await _execute_workflow_with_activity(client, worker, say_hello) + await _execute_workflow_with_activity( + client, + worker, + say_hello, + shared_state_manager=shared_state_manager, + ) assert str(assert_activity_application_error(err.value)).endswith( "missing 1 required positional argument: 'name'" ) @@ -292,21 +365,30 @@ async def test_activity_kwonly_params(): with pytest.raises(TypeError) as err: @activity.defn - async def say_hello(*, name: str) -> str: + async def say_hello(*, name: str) -> str: # type:ignore[reportUnusedFunction] return f"Hello, {name}!" assert str(err.value).endswith("cannot have keyword-only arguments") -async def test_activity_cancel_catch(client: Client, worker: ExternalWorker): +async def test_activity_cancel_catch( + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, +): @activity.defn async def wait_cancel() -> str: try: while True: await asyncio.sleep(0.3) activity.heartbeat() - except asyncio.CancelledError: - return "Got cancelled error, cancelled? " + str(activity.is_cancelled()) + except asyncio.CancelledError as err: + return ( + "Got cancelled error, cancelled? " + + str(activity.is_cancelled()) + + ", reason: " + + str(err) + ) result = await _execute_workflow_with_activity( client, @@ -315,11 +397,19 @@ async def wait_cancel() -> str: cancel_after_ms=100, wait_for_cancellation=True, heartbeat_timeout_ms=2000, + shared_state_manager=shared_state_manager, + ) + assert ( + result.result + == "Got cancelled error, cancelled? True, reason: Activity cancelled" ) - assert result.result == "Got cancelled error, cancelled? True" -async def test_activity_cancel_throw(client: Client, worker: ExternalWorker): +async def test_activity_cancel_throw( + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, +): @activity.defn async def wait_cancel() -> str: while True: @@ -334,13 +424,16 @@ async def wait_cancel() -> str: cancel_after_ms=100, wait_for_cancellation=True, heartbeat_timeout_ms=1000, + shared_state_manager=shared_state_manager, ) assert isinstance(err.value.cause, ActivityError) assert isinstance(err.value.cause.cause, CancelledError) async def test_sync_activity_thread_cancel_caught( - client: Client, worker: ExternalWorker + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, ): @activity.defn def wait_cancel() -> str: @@ -363,12 +456,15 @@ def wait_cancel() -> str: wait_for_cancellation=True, heartbeat_timeout_ms=3000, worker_config={"activity_executor": executor}, + shared_state_manager=shared_state_manager, ) assert result.result == "Cancelled" async def test_sync_activity_thread_cancel_uncaught( - client: Client, worker: ExternalWorker + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, ): @activity.defn def wait_cancel() -> NoReturn: @@ -388,13 +484,16 @@ def wait_cancel() -> NoReturn: wait_for_cancellation=True, heartbeat_timeout_ms=3000, worker_config={"activity_executor": executor}, + shared_state_manager=shared_state_manager, ) assert isinstance(err.value.cause, ActivityError) assert isinstance(err.value.cause.cause, CancelledError) async def test_sync_activity_thread_cancel_exception_disabled( - client: Client, worker: ExternalWorker + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, ): @activity.defn(no_thread_cancel_exception=True) def wait_cancel() -> str: @@ -418,14 +517,17 @@ def wait_cancel() -> str: wait_for_cancellation=True, heartbeat_timeout_ms=3000, worker_config={"activity_executor": executor}, + shared_state_manager=shared_state_manager, ) assert result.result == "Cancelled" async def test_sync_activity_thread_cancel_exception_shielded( - client: Client, worker: ExternalWorker + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, ): - events: List[str] = [] + events: list[str] = [] @activity.defn def wait_cancel() -> None: @@ -453,6 +555,7 @@ def wait_cancel() -> None: wait_for_cancellation=True, heartbeat_timeout_ms=3000, worker_config={"activity_executor": executor}, + shared_state_manager=shared_state_manager, ) assert isinstance(err.value.cause, ActivityError) assert isinstance(err.value.cause.cause, CancelledError) @@ -532,7 +635,11 @@ def picklable_activity_wait_cancel() -> str: return "Cancelled" -async def test_sync_activity_process_cancel(client: Client, worker: ExternalWorker): +async def test_sync_activity_process_cancel( + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, +): with concurrent.futures.ProcessPoolExecutor() as executor: result = await _execute_workflow_with_activity( client, @@ -540,8 +647,9 @@ async def test_sync_activity_process_cancel(client: Client, worker: ExternalWork picklable_activity_wait_cancel, cancel_after_ms=100, wait_for_cancellation=True, - heartbeat_timeout_ms=3000, + heartbeat_timeout_ms=5000, worker_config={"activity_executor": executor}, + shared_state_manager=shared_state_manager, ) assert result.result == "Cancelled" @@ -555,7 +663,9 @@ def picklable_activity_raise_cancel() -> str: async def test_sync_activity_process_cancel_uncaught( - client: Client, worker: ExternalWorker + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, ): with pytest.raises(WorkflowFailureError) as err: with concurrent.futures.ProcessPoolExecutor() as executor: @@ -567,6 +677,7 @@ async def test_sync_activity_process_cancel_uncaught( wait_for_cancellation=True, heartbeat_timeout_ms=5000, worker_config={"activity_executor": executor}, + shared_state_manager=shared_state_manager, ) assert isinstance(err.value.cause, ActivityError) assert isinstance(err.value.cause.cause, CancelledError) @@ -597,8 +708,12 @@ async def say_hello(name: str) -> str: assert "is not registered" in str(assert_activity_application_error(err.value)) -async def test_max_concurrent_activities(client: Client, worker: ExternalWorker): - seen_indexes: List[int] = [] +async def test_max_concurrent_activities( + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, +): + seen_indexes: list[int] = [] complete_activities_event = asyncio.Event() @activity.defn @@ -621,6 +736,7 @@ async def some_activity(index: int) -> str: schedule_to_start_timeout_ms=1000, worker_config={"max_concurrent_activities": 42}, on_complete=complete_activities_event.set, + shared_state_manager=shared_state_manager, ) timeout = assert_activity_error(err.value) assert isinstance(timeout, TimeoutError) @@ -635,10 +751,14 @@ class SomeClass1: @dataclass class SomeClass2: foo: str - bar: Optional[SomeClass1] = None + bar: SomeClass1 | None = None -async def test_activity_type_hints(client: Client, worker: ExternalWorker): +async def test_activity_type_hints( + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, +): activity_param1: SomeClass2 @activity.defn @@ -653,6 +773,7 @@ async def some_activity(param1: SomeClass2, param2: str) -> str: some_activity, SomeClass2(foo="str1", bar=SomeClass1(foo=123)), "123", + shared_state_manager=shared_state_manager, ) assert ( result.result @@ -662,7 +783,10 @@ async def some_activity(param1: SomeClass2, param2: str) -> str: async def test_activity_heartbeat_details( - client: Client, worker: ExternalWorker, env: WorkflowEnvironment + client: Client, + worker: ExternalWorker, + env: WorkflowEnvironment, + shared_state_manager: SharedStateManager, ): if env.supports_time_skipping: pytest.skip("https://github.com/temporalio/sdk-java/issues/2459") @@ -683,6 +807,7 @@ async def some_activity() -> str: worker, some_activity, retry_max_attempts=4, + shared_state_manager=shared_state_manager, ) assert result.result == "final count: 36" @@ -692,7 +817,9 @@ class NotSerializableValue: async def test_activity_heartbeat_details_converter_fail( - client: Client, worker: ExternalWorker + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, ): @activity.defn async def some_activity() -> str: @@ -704,14 +831,22 @@ async def some_activity() -> str: return "Should not get here" with pytest.raises(WorkflowFailureError) as err: - await _execute_workflow_with_activity(client, worker, some_activity) + await _execute_workflow_with_activity( + client, + worker, + some_activity, + shared_state_manager=shared_state_manager, + ) assert str(assert_activity_application_error(err.value)).endswith( "is not JSON serializable" ) async def test_activity_heartbeat_details_timeout( - client: Client, worker: ExternalWorker, env: WorkflowEnvironment + client: Client, + worker: ExternalWorker, + env: WorkflowEnvironment, + shared_state_manager: SharedStateManager, ): # TODO(cretz): Fix if env.supports_time_skipping: @@ -729,7 +864,11 @@ async def some_activity() -> str: # then check the timeout's details with pytest.raises(WorkflowFailureError) as err: await _execute_workflow_with_activity( - client, worker, some_activity, heartbeat_timeout_ms=1000 + client, + worker, + some_activity, + heartbeat_timeout_ms=1000, + shared_state_manager=shared_state_manager, ) timeout = assert_activity_error(err.value) assert isinstance(timeout, TimeoutError) @@ -741,7 +880,7 @@ async def some_activity() -> str: @activity.defn def picklable_heartbeat_details_activity() -> str: info = activity.info() - some_list: List[str] = ( + some_list: list[str] = ( next(iter(info.heartbeat_details)) if info.heartbeat_details else [] ) some_list.append(f"attempt: {info.attempt}") @@ -753,7 +892,10 @@ def picklable_heartbeat_details_activity() -> str: async def test_sync_activity_thread_heartbeat_details( - client: Client, worker: ExternalWorker, env: WorkflowEnvironment + client: Client, + worker: ExternalWorker, + env: WorkflowEnvironment, + shared_state_manager: SharedStateManager, ): if env.supports_time_skipping: pytest.skip("https://github.com/temporalio/sdk-java/issues/2459") @@ -767,12 +909,16 @@ async def test_sync_activity_thread_heartbeat_details( picklable_heartbeat_details_activity, retry_max_attempts=2, worker_config={"activity_executor": executor}, + shared_state_manager=shared_state_manager, ) assert result.result == "attempt: 1, attempt: 2" async def test_sync_activity_process_heartbeat_details( - client: Client, worker: ExternalWorker, env: WorkflowEnvironment + client: Client, + worker: ExternalWorker, + env: WorkflowEnvironment, + shared_state_manager: SharedStateManager, ): if env.supports_time_skipping: pytest.skip("https://github.com/temporalio/sdk-java/issues/2459") @@ -784,6 +930,7 @@ async def test_sync_activity_process_heartbeat_details( picklable_heartbeat_details_activity, retry_max_attempts=2, worker_config={"activity_executor": executor}, + shared_state_manager=shared_state_manager, ) assert result.result == "attempt: 1, attempt: 2" @@ -795,7 +942,9 @@ def picklable_activity_non_pickable_heartbeat_details() -> str: async def test_sync_activity_process_non_picklable_heartbeat_details( - client: Client, worker: ExternalWorker + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, ): with pytest.raises(WorkflowFailureError) as err: with concurrent.futures.ProcessPoolExecutor() as executor: @@ -804,6 +953,7 @@ async def test_sync_activity_process_non_picklable_heartbeat_details( worker, picklable_activity_non_pickable_heartbeat_details, worker_config={"activity_executor": executor}, + shared_state_manager=shared_state_manager, ) msg = str(assert_activity_application_error(err.value)) # TODO: different messages can apparently be produced across runs/platforms @@ -815,7 +965,11 @@ async def test_sync_activity_process_non_picklable_heartbeat_details( ) -async def test_activity_error_non_retryable(client: Client, worker: ExternalWorker): +async def test_activity_error_non_retryable( + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, +): @activity.defn async def some_activity(): if activity.info().attempt < 2: @@ -829,6 +983,7 @@ async def some_activity(): worker, some_activity, retry_max_attempts=100, + shared_state_manager=shared_state_manager, ) app_err = assert_activity_application_error(err.value) assert str(app_err) == "Do not retry me" @@ -836,7 +991,9 @@ async def some_activity(): async def test_activity_error_non_retryable_type( - client: Client, worker: ExternalWorker + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, ): @activity.defn async def some_activity(): @@ -851,6 +1008,7 @@ async def some_activity(): some_activity, retry_max_attempts=100, non_retryable_error_types=["Cannot retry me"], + shared_state_manager=shared_state_manager, ) assert ( str(assert_activity_application_error(err.value)) @@ -858,7 +1016,11 @@ async def some_activity(): ) -async def test_activity_logging(client: Client, worker: ExternalWorker): +async def test_activity_logging( + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, +): @activity.defn async def say_hello(name: str) -> str: activity.logger.info(f"Called with arg: {name}") @@ -866,18 +1028,17 @@ async def say_hello(name: str) -> str: # Create a queue, add handler to logger, call normal activity, then check handler = logging.handlers.QueueHandler(queue.Queue()) - activity.logger.base_logger.addHandler(handler) - prev_level = activity.logger.base_logger.level - activity.logger.base_logger.setLevel(logging.INFO) - try: + with LogHandler.apply(activity.logger.base_logger, handler): + activity.logger.base_logger.setLevel(logging.INFO) result = await _execute_workflow_with_activity( - client, worker, say_hello, "Temporal" + client, + worker, + say_hello, + "Temporal", + shared_state_manager=shared_state_manager, ) - finally: - activity.logger.base_logger.removeHandler(handler) - activity.logger.base_logger.setLevel(prev_level) assert result.result == "Hello, Temporal!" - records: List[logging.LogRecord] = list(handler.queue.queue) # type: ignore + records: list[logging.LogRecord] = list(handler.queue.queue) # type: ignore assert len(records) > 0 assert records[-1].message.startswith( "Called with arg: Temporal ({'activity_id': '" @@ -885,7 +1046,10 @@ async def say_hello(name: str) -> str: assert records[-1].__dict__["temporal_activity"]["activity_type"] == "say_hello" -async def test_activity_worker_shutdown(client: Client, worker: ExternalWorker): +async def test_activity_worker_shutdown( + client: Client, + worker: ExternalWorker, +): activity_started = asyncio.Event() @activity.defn @@ -973,7 +1137,9 @@ def picklable_wait_on_event() -> str: async def test_sync_activity_process_worker_shutdown_graceful( - client: Client, worker: ExternalWorker + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, ): act_task_queue = str(uuid.uuid4()) with concurrent.futures.ProcessPoolExecutor() as executor: @@ -984,7 +1150,7 @@ async def test_sync_activity_process_worker_shutdown_graceful( activity_executor=executor, max_concurrent_activities=default_max_concurrent_activities, graceful_shutdown_timeout=timedelta(seconds=2), - shared_state_manager=_default_shared_state_manager, + shared_state_manager=shared_state_manager, ) asyncio.create_task(act_worker.run()) @@ -1030,7 +1196,9 @@ def kill_my_process() -> str: async def test_sync_activity_process_executor_crash( - client: Client, worker: ExternalWorker + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, ): act_task_queue = str(uuid.uuid4()) with concurrent.futures.ProcessPoolExecutor() as executor: @@ -1041,7 +1209,7 @@ async def test_sync_activity_process_executor_crash( activity_executor=executor, max_concurrent_activities=default_max_concurrent_activities, graceful_shutdown_timeout=timedelta(seconds=2), - shared_state_manager=_default_shared_state_manager, + shared_state_manager=shared_state_manager, ) act_worker_task = asyncio.create_task(act_worker.run()) @@ -1076,11 +1244,11 @@ async def test_sync_activity_process_executor_crash( class AsyncActivityWrapper: def __init__(self) -> None: - self._info: Optional[activity.Info] = None + self._info: activity.Info | None = None self._info_set = asyncio.Event() @activity.defn - async def run(self) -> Optional[str]: + async def run(self) -> str | None: self._info = activity.info() self._info_set.set() activity.raise_complete_async() @@ -1095,6 +1263,9 @@ def async_handle(self, client: Client, use_task_token: bool) -> AsyncActivityHan assert self._info if use_task_token: return client.get_async_activity_handle(task_token=self._info.task_token) + assert ( + self._info.workflow_id + ) # These tests are for workflow-triggered activities return client.get_async_activity_handle( workflow_id=self._info.workflow_id, run_id=self._info.workflow_run_id, @@ -1104,12 +1275,20 @@ def async_handle(self, client: Client, use_task_token: bool) -> AsyncActivityHan @pytest.mark.parametrize("use_task_token", [True, False]) async def test_activity_async_success( - client: Client, worker: ExternalWorker, use_task_token: bool + client: Client, + worker: ExternalWorker, + use_task_token: bool, + shared_state_manager: SharedStateManager, ): # Start task, wait for info, complete with value, wait on workflow wrapper = AsyncActivityWrapper() task = asyncio.create_task( - _execute_workflow_with_activity(client, worker, wrapper.run) + _execute_workflow_with_activity( + client, + worker, + wrapper.run, + shared_state_manager=shared_state_manager, + ) ) await wrapper.wait_info() await wrapper.async_handle(client, use_task_token).complete("some value") @@ -1118,7 +1297,12 @@ async def test_activity_async_success( # Do again with a None value wrapper = AsyncActivityWrapper() task = asyncio.create_task( - _execute_workflow_with_activity(client, worker, wrapper.run) + _execute_workflow_with_activity( + client, + worker, + wrapper.run, + shared_state_manager=shared_state_manager, + ) ) await wrapper.wait_info() await wrapper.async_handle(client, use_task_token).complete(None) @@ -1131,6 +1315,7 @@ async def test_activity_async_heartbeat_and_fail( worker: ExternalWorker, env: WorkflowEnvironment, use_task_token: bool, + shared_state_manager: SharedStateManager, ): if env.supports_time_skipping: pytest.skip("https://github.com/temporalio/sdk-java/issues/2459") @@ -1139,7 +1324,11 @@ async def test_activity_async_heartbeat_and_fail( # Start task w/ max attempts 2, wait for info, send heartbeat, fail task = asyncio.create_task( _execute_workflow_with_activity( - client, worker, wrapper.run, retry_max_attempts=2 + client, + worker, + wrapper.run, + retry_max_attempts=2, + shared_state_manager=shared_state_manager, ) ) info = await wrapper.wait_info() @@ -1167,13 +1356,21 @@ async def test_activity_async_heartbeat_and_fail( @pytest.mark.parametrize("use_task_token", [True, False]) async def test_activity_async_cancel( - client: Client, worker: ExternalWorker, use_task_token: bool + client: Client, + worker: ExternalWorker, + use_task_token: bool, + shared_state_manager: SharedStateManager, ): wrapper = AsyncActivityWrapper() # Start task, wait for info, cancel, wait on workflow task = asyncio.create_task( _execute_workflow_with_activity( - client, worker, wrapper.run, cancel_after_ms=50, wait_for_cancellation=True + client, + worker, + wrapper.run, + cancel_after_ms=50, + wait_for_cancellation=True, + shared_state_manager=shared_state_manager, ) ) await wrapper.wait_info() @@ -1205,7 +1402,11 @@ async def execute_activity(self, input: ExecuteActivityInput) -> Any: return await super().execute_activity(input) -async def test_sync_activity_contextvars(client: Client, worker: ExternalWorker): +async def test_sync_activity_contextvars( + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, +): @activity.defn def some_activity() -> str: return f"context var: {some_context_var.get()}" @@ -1221,10 +1422,159 @@ def some_activity() -> str: "activity_executor": executor, "interceptors": [ContextVarInterceptor()], }, + shared_state_manager=shared_state_manager, ) assert result.result == "context var: some value!" +@activity.defn +def post_return_cancel_activity() -> str: + return "done" + + +@activity.defn +def post_return_cancel_probe_activity() -> str: + return "probe" + + +class PostReturnCancelInterceptor(Interceptor): + def __init__( + self, after_return_started: asyncio.Event, allow_completion: asyncio.Event + ) -> None: + super().__init__() + self.after_return_started = after_return_started + self.allow_completion = allow_completion + + def intercept_activity( + self, next: ActivityInboundInterceptor + ) -> ActivityInboundInterceptor: + return PostReturnCancelActivityInbound( + next, self.after_return_started, self.allow_completion + ) + + +class PostReturnCancelActivityInbound(ActivityInboundInterceptor): + def __init__( + self, + next: ActivityInboundInterceptor, + after_return_started: asyncio.Event, + allow_completion: asyncio.Event, + ) -> None: + super().__init__(next) + self.after_return_started = after_return_started + self.allow_completion = allow_completion + + async def execute_activity(self, input: ExecuteActivityInput) -> Any: + result = await super().execute_activity(input) + if activity.info().activity_type == "post_return_cancel_activity": + self.after_return_started.set() + await self.allow_completion.wait() + return result + + +async def test_sync_activity_cancel_after_return_does_not_kill_thread_pool_worker( + client: Client, + worker: ExternalWorker, + env: WorkflowEnvironment, + shared_state_manager: SharedStateManager, +): + if env.supports_time_skipping: + pytest.skip("Test requires real worker-side timeout cancellation delivery") + + def alive_thread_count(executor: ThreadPoolExecutor) -> int: + return sum(1 for thread in list(executor._threads) if thread.is_alive()) + + after_return_started = asyncio.Event() + allow_completion = asyncio.Event() + act_task_queue = str(uuid.uuid4()) + + with ThreadPoolExecutor(max_workers=1) as executor: + with pytest.warns( + UserWarning, + match="Worker max_concurrent_activities is 2 but activity_executor's max_workers is only", + ): + act_worker = Worker( + client, + task_queue=act_task_queue, + activities=[ + post_return_cancel_activity, + post_return_cancel_probe_activity, + ], + activity_executor=executor, + interceptors=[ + PostReturnCancelInterceptor(after_return_started, allow_completion) + ], + max_concurrent_activities=2, + shared_state_manager=shared_state_manager, + ) + + async with act_worker: + try: + timed_out_workflow = asyncio.create_task( + client.execute_workflow( + "kitchen_sink", + KSWorkflowParams( + actions=[ + KSAction( + execute_activity=KSExecuteActivityAction( + name="post_return_cancel_activity", + task_queue=act_task_queue, + start_to_close_timeout_ms=200, + retry_max_attempts=1, + ) + ) + ] + ), + id=str(uuid.uuid4()), + task_queue=worker.task_queue, + ) + ) + await asyncio.wait_for(after_return_started.wait(), timeout=5) + + with pytest.raises(WorkflowFailureError) as err: + await timed_out_workflow + timeout = assert_activity_error(err.value) + assert isinstance(timeout, TimeoutError) + assert timeout.type == TimeoutType.START_TO_CLOSE + + activity_worker = act_worker._activity_worker + assert activity_worker + for _ in range(50): + if any( + running.cancelled_event and running.cancelled_event.is_set() + for running in activity_worker._running_activities.values() + ): + break + await asyncio.sleep(0.1) + else: + pytest.fail("Timed out waiting for activity cancellation") + + probe_result = await asyncio.wait_for( + client.execute_workflow( + "kitchen_sink", + KSWorkflowParams( + actions=[ + KSAction( + execute_activity=KSExecuteActivityAction( + name="post_return_cancel_probe_activity", + task_queue=act_task_queue, + start_to_close_timeout_ms=30000, + retry_max_attempts=1, + ) + ) + ] + ), + id=str(uuid.uuid4()), + task_queue=worker.task_queue, + ), + timeout=5, + ) + assert probe_result == "probe" + assert alive_thread_count(executor) == 1 + finally: + allow_completion.set() + + @activity.defn async def local_without_schedule_to_close_activity() -> str: return "some-activity" @@ -1275,7 +1625,11 @@ def sync_dyn_activity(args: Sequence[RawValue]) -> DynActivityValue: ) -async def test_activity_dynamic(client: Client, worker: ExternalWorker): +async def test_activity_dynamic( + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, +): @activity.defn(dynamic=True) async def async_dyn_activity(args: Sequence[RawValue]) -> DynActivityValue: return sync_dyn_activity(args) @@ -1288,11 +1642,16 @@ async def async_dyn_activity(args: Sequence[RawValue]) -> DynActivityValue: DynActivityValue("val2"), activity_name_override="some-activity-name", result_type_override=DynActivityValue, + shared_state_manager=shared_state_manager, ) assert result.result == DynActivityValue("some-activity-name - val1 - val2") -async def test_sync_activity_dynamic_thread(client: Client, worker: ExternalWorker): +async def test_sync_activity_dynamic_thread( + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, +): with concurrent.futures.ThreadPoolExecutor( max_workers=default_max_concurrent_activities ) as executor: @@ -1305,11 +1664,16 @@ async def test_sync_activity_dynamic_thread(client: Client, worker: ExternalWork worker_config={"activity_executor": executor}, activity_name_override="some-activity-name", result_type_override=DynActivityValue, + shared_state_manager=shared_state_manager, ) assert result.result == DynActivityValue("some-activity-name - val1 - val2") -async def test_sync_activity_dynamic_process(client: Client, worker: ExternalWorker): +async def test_sync_activity_dynamic_process( + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, +): with concurrent.futures.ProcessPoolExecutor() as executor: result = await _execute_workflow_with_activity( client, @@ -1320,22 +1684,31 @@ async def test_sync_activity_dynamic_process(client: Client, worker: ExternalWor worker_config={"activity_executor": executor}, activity_name_override="some-activity-name", result_type_override=DynActivityValue, + shared_state_manager=shared_state_manager, ) assert result.result == DynActivityValue("some-activity-name - val1 - val2") -async def test_activity_dynamic_duplicate(client: Client, worker: ExternalWorker): +async def test_activity_dynamic_duplicate( + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, +): @activity.defn(dynamic=True) - async def dyn_activity_1(args: Sequence[RawValue]) -> None: + async def dyn_activity_1(_args: Sequence[RawValue]) -> None: pass @activity.defn(dynamic=True) - async def dyn_activity_2(args: Sequence[RawValue]) -> None: + async def dyn_activity_2(_args: Sequence[RawValue]) -> None: pass with pytest.raises(TypeError) as err: await _execute_workflow_with_activity( - client, worker, dyn_activity_1, additional_activities=[dyn_activity_2] + client, + worker, + dyn_activity_1, + additional_activities=[dyn_activity_2], + shared_state_manager=shared_state_manager, ) assert "More than one dynamic activity" in str(err.value) @@ -1352,26 +1725,27 @@ async def _execute_workflow_with_activity( worker: ExternalWorker, fn: Callable, *args: Any, - count: Optional[int] = None, - index_as_arg: Optional[bool] = None, - schedule_to_close_timeout_ms: Optional[int] = None, - start_to_close_timeout_ms: Optional[int] = None, - schedule_to_start_timeout_ms: Optional[int] = None, - cancel_after_ms: Optional[int] = None, - wait_for_cancellation: Optional[bool] = None, - heartbeat_timeout_ms: Optional[int] = None, - retry_max_attempts: Optional[int] = None, - non_retryable_error_types: Optional[Sequence[str]] = None, + shared_state_manager: SharedStateManager, + count: int | None = None, + index_as_arg: bool | None = None, + schedule_to_close_timeout_ms: int | None = None, + start_to_close_timeout_ms: int | None = None, + schedule_to_start_timeout_ms: int | None = None, + cancel_after_ms: int | None = None, + wait_for_cancellation: bool | None = None, + heartbeat_timeout_ms: int | None = None, + retry_max_attempts: int | None = None, + non_retryable_error_types: Sequence[str] | None = None, worker_config: WorkerConfig = {}, - on_complete: Optional[Callable[[], None]] = None, - activity_name_override: Optional[str] = None, - result_type_override: Optional[Type] = None, - additional_activities: List[Callable] = [], + on_complete: Callable[[], None] | None = None, + activity_name_override: str | None = None, + result_type_override: type | None = None, + additional_activities: list[Callable] = [], ) -> _ActivityResult: worker_config["client"] = client worker_config["task_queue"] = str(uuid.uuid4()) worker_config["activities"] = [fn] + additional_activities - worker_config["shared_state_manager"] = _default_shared_state_manager + worker_config["shared_state_manager"] = shared_state_manager if not worker_config.get("max_concurrent_activities"): worker_config["max_concurrent_activities"] = default_max_concurrent_activities async with Worker(**worker_config): @@ -1443,28 +1817,35 @@ def emit(self, record: logging.LogRecord) -> None: async def test_activity_failure_trace_identifier( - client: Client, worker: ExternalWorker + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, ): @activity.defn async def raise_error(): raise RuntimeError("oh no!") handler = CustomLogHandler() - activity.logger.base_logger.addHandler(handler) - try: + with LogHandler.apply(activity.logger.base_logger, handler): with pytest.raises(WorkflowFailureError) as err: - await _execute_workflow_with_activity(client, worker, raise_error) + await _execute_workflow_with_activity( + client, + worker, + raise_error, + shared_state_manager=shared_state_manager, + ) assert ( str(assert_activity_application_error(err.value)) == "RuntimeError: oh no!" ) assert handler._trace_identifiers == 1 - finally: - activity.logger.base_logger.removeHandler(CustomLogHandler()) - -async def test_activity_heartbeat_context(client: Client, worker: ExternalWorker): +async def test_activity_heartbeat_context( + client: Client, + worker: ExternalWorker, + shared_state_manager: SharedStateManager, +): @activity.defn async def heartbeat(): if activity.info().attempt == 1: @@ -1486,13 +1867,20 @@ async def h(): return "details: " + activity.info().heartbeat_details[0] result = await _execute_workflow_with_activity( - client, worker, heartbeat, retry_max_attempts=2 + client, + worker, + heartbeat, + retry_max_attempts=2, + shared_state_manager=shared_state_manager, ) assert result.result == "details: Some detail" async def test_activity_reset_catch( - client: Client, worker: ExternalWorker, env: WorkflowEnvironment + client: Client, + worker: ExternalWorker, + env: WorkflowEnvironment, + shared_state_manager: SharedStateManager, ): if env.supports_time_skipping: pytest.skip("Time skipping server doesn't support activity reset") @@ -1502,8 +1890,8 @@ async def wait_cancel() -> str: req = temporalio.api.workflowservice.v1.ResetActivityRequest( namespace=client.namespace, execution=temporalio.api.common.v1.WorkflowExecution( - workflow_id=activity.info().workflow_id, - run_id=activity.info().workflow_run_id, + workflow_id=activity.info().workflow_id or "", + run_id=activity.info().workflow_run_id or "", ), id=activity.info().activity_id, ) @@ -1522,8 +1910,8 @@ def sync_wait_cancel() -> str: req = temporalio.api.workflowservice.v1.ResetActivityRequest( namespace=client.namespace, execution=temporalio.api.common.v1.WorkflowExecution( - workflow_id=activity.info().workflow_id, - run_id=activity.info().workflow_run_id, + workflow_id=activity.info().workflow_id or "", + run_id=activity.info().workflow_run_id or "", ), id=activity.info().activity_id, ) @@ -1543,6 +1931,7 @@ def sync_wait_cancel() -> str: client, worker, wait_cancel, + shared_state_manager=shared_state_manager, ) assert result.result == "Got cancelled error, reset? True" @@ -1554,12 +1943,16 @@ def sync_wait_cancel() -> str: worker, sync_wait_cancel, worker_config=config, + shared_state_manager=shared_state_manager, ) assert result.result == "Got cancelled error, reset? True" async def test_activity_reset_history( - client: Client, worker: ExternalWorker, env: WorkflowEnvironment + client: Client, + worker: ExternalWorker, + env: WorkflowEnvironment, + shared_state_manager: SharedStateManager, ): if env.supports_time_skipping: pytest.skip("Time skipping server doesn't support activity reset") @@ -1569,8 +1962,8 @@ async def wait_cancel() -> str: req = temporalio.api.workflowservice.v1.ResetActivityRequest( namespace=client.namespace, execution=temporalio.api.common.v1.WorkflowExecution( - workflow_id=activity.info().workflow_id, - run_id=activity.info().workflow_run_id, + workflow_id=activity.info().workflow_id or "", + run_id=activity.info().workflow_run_id or "", ), id=activity.info().activity_id, ) @@ -1580,10 +1973,11 @@ async def wait_cancel() -> str: activity.heartbeat() with pytest.raises(WorkflowFailureError) as e: - result = await _execute_workflow_with_activity( + await _execute_workflow_with_activity( client, worker, wait_cancel, + shared_state_manager=shared_state_manager, ) assert isinstance(e.value.cause, ActivityError) assert isinstance(e.value.cause.cause, ApplicationError) @@ -1591,3 +1985,40 @@ async def wait_cancel() -> str: e.value.cause.cause.message == "Unhandled activity cancel error produced by activity reset" ) + + +@activity.defn +async def local_activity_for_no_remote_test(name: str) -> str: + assert activity.info().is_local + return f"Hello from local activity, {name}!" + + +@workflow.defn(sandboxed=False) +class LocalActivityWithNoRemoteActivitiesWorkflow: + @workflow.run + async def run(self, name: str) -> str: + return await workflow.execute_local_activity( + local_activity_for_no_remote_test, + name, + schedule_to_close_timeout=timedelta(seconds=5), + ) + + +async def test_local_activities_with_no_remote_activities_option(client: Client): + task_queue = f"tq-{uuid.uuid4()}" + + async with Worker( + client, + task_queue=task_queue, + activities=[local_activity_for_no_remote_test], + workflows=[LocalActivityWithNoRemoteActivitiesWorkflow], + no_remote_activities=True, + ): + result = await client.execute_workflow( + LocalActivityWithNoRemoteActivitiesWorkflow.run, + "Temporal", + id=f"workflow-{uuid.uuid4()}", + task_queue=task_queue, + ) + + assert result == "Hello from local activity, Temporal!" diff --git a/tests/worker/test_breakpoint_hang.py b/tests/worker/test_breakpoint_hang.py new file mode 100644 index 000000000..29f3cb3f7 --- /dev/null +++ b/tests/worker/test_breakpoint_hang.py @@ -0,0 +1,223 @@ +from __future__ import annotations + +import pdb +import threading +import uuid +from types import FrameType +from typing import Any +from unittest.mock import patch + +import pytest + +from temporalio import workflow +from temporalio.client import Client, WorkflowFailureError +from temporalio.worker import Worker +from temporalio.worker.workflow_sandbox._restrictions import ( + RestrictedWorkflowAccessError, +) + + +@workflow.defn(sandboxed=False) +class ThreadCaptureWorkflow: + """Returns the name of the thread the workflow runs on. + + `sandboxed=False` so `threading.current_thread()` isn't intercepted — + these tests are about thread placement, not sandbox behavior. + """ + + @workflow.run + async def run(self) -> str: + return threading.current_thread().name + + +async def test_workflow_runs_on_pool_thread_without_debug_mode(client: Client): + """Production behavior unchanged: workflows run on `temporal_workflow_*`.""" + task_queue = f"tq-{uuid.uuid4()}" + async with Worker( + client, + task_queue=task_queue, + workflows=[ThreadCaptureWorkflow], + ): + thread_name = await client.execute_workflow( + ThreadCaptureWorkflow.run, + id=f"wf-{uuid.uuid4()}", + task_queue=task_queue, + ) + + main_name = threading.main_thread().name + assert thread_name != main_name, ( + f"workflow ran on the main thread ({main_name!r}) — production behavior changed" + ) + assert thread_name.startswith("temporal_workflow_"), ( + f"expected pool thread, got {thread_name!r}" + ) + + +async def test_workflow_runs_on_main_thread_in_debug_mode(client: Client): + """debug_mode=True moves workflow activation to the asyncio main thread + so pdb's input() reaches the controlling TTY.""" + task_queue = f"tq-{uuid.uuid4()}" + async with Worker( + client, + task_queue=task_queue, + workflows=[ThreadCaptureWorkflow], + debug_mode=True, + ): + thread_name = await client.execute_workflow( + ThreadCaptureWorkflow.run, + id=f"wf-{uuid.uuid4()}", + task_queue=task_queue, + ) + + main_name = threading.main_thread().name + assert thread_name == main_name, ( + f"expected workflow on main thread ({main_name!r}) in debug mode; " + f"got {thread_name!r}" + ) + + +@workflow.defn +class SandboxedBreakpointWorkflow: + """Sandboxed workflow that calls breakpoint() — verifies the fix works + without requiring users to switch to UnsandboxedWorkflowRunner.""" + + @workflow.run + async def run(self) -> str: + bird = "chicken" + breakpoint() + return f"bird was {bird}" + + +async def test_breakpoint_works_in_sandboxed_workflow_in_debug_mode(client: Client): + """breakpoint() inside a sandboxed workflow reaches the debugger when + debug_mode=True — no need to switch to UnsandboxedWorkflowRunner. + + Patches `pdb.Pdb.set_trace` with a stub so CI doesn't hang on an + interactive prompt. Reaching the stub on `MainThread` with the + workflow's `run` frame proves the full path (sandbox relaxation -> + our hook -> pdb) works through the sandbox. Also verifies workflow + locals (`bird`) are visible in the captured frame. + """ + captured: dict[str, object] = {} + + def stub_set_trace(_self: pdb.Pdb, frame: FrameType | None = None) -> None: + captured["thread"] = threading.current_thread().name + captured["frame_name"] = frame.f_code.co_name if frame else None + captured["bird"] = frame.f_locals.get("bird") if frame else None + captured["called"] = True + + task_queue = f"tq-{uuid.uuid4()}" + with patch.object(pdb.Pdb, "set_trace", stub_set_trace): + async with Worker( + client, + task_queue=task_queue, + workflows=[SandboxedBreakpointWorkflow], + debug_mode=True, + ): + result = await client.execute_workflow( + SandboxedBreakpointWorkflow.run, + id=f"wf-{uuid.uuid4()}", + task_queue=task_queue, + ) + + assert result == "bird was chicken", ( + f"workflow did not complete; breakpoint() likely raised inside the sandbox: " + f"result={result!r}" + ) + assert captured.get("called"), "pdb.Pdb.set_trace was never reached" + assert captured["thread"] == threading.main_thread().name, ( + f"breakpoint landed on {captured['thread']!r}, not the main thread" + ) + assert captured["frame_name"] == "run", ( + f"breakpoint stopped at frame {captured['frame_name']!r}, " + f"expected the workflow's `run` method" + ) + assert captured["bird"] == "chicken", ( + f"workflow local `bird` not visible in pdb frame: got {captured['bird']!r}" + ) + + +async def test_breakpoint_quit_continues_workflow_in_debug_mode(client: Client): + """Typing `q` (or hitting Ctrl-D) in a workflow pdb session should + continue the workflow rather than failing the workflow task with + BdbQuit. The hook overrides `do_quit`/`do_EOF` to call `do_continue` + instead, so a debug session ends cleanly. + + Drives pdb via `cmdqueue` so no real stdin is needed. The first + iteration of cmdloop sees `q`, which dispatches to our overridden + `do_quit` -> `do_continue`. The workflow then completes normally. + """ + captured: dict[str, object] = {} + + class _AutoQuitPdb(pdb.Pdb): + """Pdb subclass that pre-queues `q` and captures frame state on + entry to `interaction`.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.cmdqueue = ["q"] + + def interaction( # type: ignore[override] + self, frame: FrameType | None, traceback: Any + ) -> Any: + if frame is not None: + captured["frame_name"] = frame.f_code.co_name + captured["bird"] = frame.f_locals.get("bird") + return super().interaction(frame, traceback) + + task_queue = f"tq-{uuid.uuid4()}" + with patch("pdb.Pdb", _AutoQuitPdb): + async with Worker( + client, + task_queue=task_queue, + workflows=[SandboxedBreakpointWorkflow], + debug_mode=True, + ): + result = await client.execute_workflow( + SandboxedBreakpointWorkflow.run, + id=f"wf-{uuid.uuid4()}", + task_queue=task_queue, + ) + + assert result == "bird was chicken", ( + f"workflow did not complete after `q`; `BdbQuit` likely propagated: " + f"result={result!r}" + ) + assert captured.get("frame_name") == "run", ( + f"pdb didn't stop in workflow.run frame: got {captured.get('frame_name')!r}" + ) + assert captured.get("bird") == "chicken", ( + f"workflow local `bird` not visible at pdb breakpoint: " + f"got {captured.get('bird')!r}" + ) + + +async def test_sandboxed_breakpoint_points_at_debug_mode(client: Client): + """Without `debug_mode`, calling `breakpoint()` in a sandboxed workflow + should raise the sandbox's restricted-access error with a message that + directs the user at `debug_mode=True` (rather than the generic + pass-through advice that doesn't apply here). + + `workflow_failure_exception_types=[RestrictedWorkflowAccessError]` makes + the sandbox error terminal for the workflow execution instead of + triggering Temporal's normal task-retry loop, so the test gets the + failure surfaced promptly without waiting on a timeout. + """ + task_queue = f"tq-{uuid.uuid4()}" + with pytest.raises(WorkflowFailureError) as exc_info: + async with Worker( + client, + task_queue=task_queue, + workflows=[SandboxedBreakpointWorkflow], + workflow_failure_exception_types=[RestrictedWorkflowAccessError], + ): + await client.execute_workflow( + SandboxedBreakpointWorkflow.run, + id=f"wf-{uuid.uuid4()}", + task_queue=task_queue, + ) + + cause_msg = str(exc_info.value.cause) + assert "debug_mode=True" in cause_msg, ( + f"sandbox error didn't point at debug_mode: {cause_msg!r}" + ) diff --git a/tests/worker/test_command_aware_visitor.py b/tests/worker/test_command_aware_visitor.py new file mode 100644 index 000000000..6e7da7963 --- /dev/null +++ b/tests/worker/test_command_aware_visitor.py @@ -0,0 +1,88 @@ +"""Test that CommandAwarePayloadVisitor handles all commands with seq fields that have payloads.""" + +from collections.abc import Iterator +from typing import Any + +from temporalio.bridge._visitor import PayloadVisitor +from temporalio.bridge.proto.workflow_activation import workflow_activation_pb2 +from temporalio.bridge.proto.workflow_commands import workflow_commands_pb2 +from temporalio.worker._command_aware_visitor import CommandAwarePayloadVisitor + + +def test_command_aware_visitor_has_methods_for_all_seq_protos_with_payloads(): + """Verify CommandAwarePayloadVisitor has methods for all protos with seq fields that have payloads. + + We only override methods when the base class has a visitor method (i.e., there are payloads to visit). + Commands without payloads don't need overrides since there's nothing to visit. + """ + # Find all protos with seq + command_protos = list(_get_workflow_command_protos_with_seq()) + job_protos = list(_get_workflow_activation_job_protos_with_seq()) + assert command_protos, "Should find workflow commands with seq" + assert job_protos, "Should find workflow activation jobs with seq" + + # Check workflow commands - only ones with payloads need overrides + commands_missing = [] + commands_with_payloads = [] + for proto_class in command_protos: + method_name = f"_visit_coresdk_workflow_commands_{proto_class.__name__}" + # Only check if base class has this visitor (meaning there are payloads) + if hasattr(PayloadVisitor, method_name): + commands_with_payloads.append(proto_class.__name__) + # Check if CommandAwarePayloadVisitor has its own override (not just inherited) + if method_name not in CommandAwarePayloadVisitor.__dict__: + commands_missing.append(proto_class.__name__) + + # Check workflow activation jobs - only ones with payloads need overrides + jobs_missing = [] + jobs_with_payloads = [] + for proto_class in job_protos: + method_name = f"_visit_coresdk_workflow_activation_{proto_class.__name__}" + # Only check if base class has this visitor (meaning there are payloads) + if hasattr(PayloadVisitor, method_name): + jobs_with_payloads.append(proto_class.__name__) + # Check if CommandAwarePayloadVisitor has its own override (not just inherited) + if method_name not in CommandAwarePayloadVisitor.__dict__: + jobs_missing.append(proto_class.__name__) + + errors = [] + if commands_missing: + errors.append( + f"Missing visitor methods for commands with seq and payloads: {commands_missing}\n" + f"Add methods to CommandAwarePayloadVisitor for these commands." + ) + if jobs_missing: + errors.append( + f"Missing visitor methods for activation jobs with seq and payloads: {jobs_missing}\n" + f"Add methods to CommandAwarePayloadVisitor for these jobs." + ) + + assert not errors, "\n".join(errors) + + # Verify we found the expected commands/jobs with payloads + assert len(commands_with_payloads) > 0, "Should find commands with payloads" + assert len(jobs_with_payloads) > 0, "Should find activation jobs with payloads" + + # Sanity check: we should have fewer overrides than total protos with seq + # (because some don't have payloads) + assert len(commands_with_payloads) < len(command_protos), ( + "Should have some commands without payloads" + ) + # All activation jobs except FireTimer have payloads + assert len(jobs_with_payloads) == len(job_protos) - 1, ( + "Should have exactly one activation job without payloads (FireTimer)" + ) + + +def _get_workflow_command_protos_with_seq() -> Iterator[type[Any]]: + """Get concrete classes of all workflow command protos with a seq field.""" + for descriptor in workflow_commands_pb2.DESCRIPTOR.message_types_by_name.values(): + if "seq" in descriptor.fields_by_name: + yield getattr(descriptor, "_concrete_class") + + +def _get_workflow_activation_job_protos_with_seq() -> Iterator[type[Any]]: + """Get concrete classes of all workflow activation job protos with a seq field.""" + for descriptor in workflow_activation_pb2.DESCRIPTOR.message_types_by_name.values(): + if "seq" in descriptor.fields_by_name: + yield getattr(descriptor, "_concrete_class") diff --git a/tests/worker/test_extstore.py b/tests/worker/test_extstore.py new file mode 100644 index 000000000..2f8fde5fe --- /dev/null +++ b/tests/worker/test_extstore.py @@ -0,0 +1,1361 @@ +import dataclasses +import logging +import re +import uuid +from collections.abc import Sequence +from dataclasses import dataclass +from datetime import timedelta +from unittest import mock + +import pytest + +import temporalio +import temporalio.bridge.client +import temporalio.bridge.worker +import temporalio.client +import temporalio.converter +import temporalio.worker._workflow +from temporalio import activity, workflow +from temporalio.api.common.v1 import Payload +from temporalio.client import Client, WorkflowFailureError, WorkflowHandle +from temporalio.common import RetryPolicy +from temporalio.converter import ( + ExternalStorage, + StorageDriver, + StorageDriverActivityInfo, + StorageDriverClaim, + StorageDriverRetrieveContext, + StorageDriverStoreContext, + StorageDriverWorkflowInfo, +) +from temporalio.exceptions import ActivityError, ApplicationError +from temporalio.testing._workflow import WorkflowEnvironment +from temporalio.worker import Replayer +from tests.helpers import LogCapturer, assert_task_fail_eventually, new_worker +from tests.test_extstore import InMemoryTestDriver + + +@dataclass(frozen=True) +class ExtStoreActivityInput: + input_data: str + output_size: int + pass + + +# --------------------------------------------------------------------------- +# Chained-activity scenario +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class ProcessDataInput: + """Input for the first activity: generate a large result.""" + + size: int + + +@dataclass(frozen=True) +class SummarizeInput: + """Input for the second activity: receives the large result from the first.""" + + data: str + + +@activity.defn +async def process_data(input: ProcessDataInput) -> str: + """Produces a large string result that will be stored externally.""" + return "x" * input.size + + +@activity.defn +async def summarize(input: SummarizeInput) -> str: + """Receives the large result and returns a short summary.""" + return f"received {len(input.data)} bytes" + + +@workflow.defn +class ChainedExtStoreWorkflow: + """Workflow that passes a large activity result directly into a second activity. + + This mirrors a common customer pattern: activity A produces a large payload + (e.g. a fetched document or ML inference result) which is too big to store + inline in workflow history, and is then consumed by activity B. External + storage should transparently offload the payload between the two steps + without any special handling in the workflow code. + """ + + @workflow.run + async def run(self, payload_size: int) -> str: + large_result = await workflow.execute_activity( + process_data, + ProcessDataInput(size=payload_size), + schedule_to_close_timeout=timedelta(seconds=10), + ) + return await workflow.execute_activity( + summarize, + SummarizeInput(data=large_result), + schedule_to_close_timeout=timedelta(seconds=10), + ) + + +@activity.defn +async def ext_store_activity( + input: ExtStoreActivityInput, +) -> str: + return "ao" * int(input.output_size / 2) + + +@dataclass(frozen=True) +class ExtStoreWorkflowInput: + input_data: str + activity_input_size: int + activity_output_size: int + output_size: int + max_activity_attempts: int | None = None + + +@workflow.defn +class ExtStoreWorkflow: + @workflow.run + async def run(self, input: ExtStoreWorkflowInput) -> str: + retry_policy = ( + RetryPolicy(maximum_attempts=input.max_activity_attempts) + if input.max_activity_attempts is not None + else None + ) + await workflow.execute_activity( + ext_store_activity, + ExtStoreActivityInput( + input_data="ai" * int(input.activity_input_size / 2), + output_size=input.activity_output_size, + ), + schedule_to_close_timeout=timedelta(seconds=3), + retry_policy=retry_policy, + ) + return "wo" * int(input.output_size / 2) + + +class BadTestDriver(InMemoryTestDriver): + def __init__( + self, + driver_name: str = "bad-driver", + no_store: bool = False, + no_retrieve: bool = False, + raise_payload_not_found: bool = False, + ): + super().__init__(driver_name) + self._no_store = no_store + self._no_retrieve = no_retrieve + self._raise_payload_not_found = raise_payload_not_found + + async def store( + self, + context: StorageDriverStoreContext, + payloads: Sequence[Payload], + ) -> list[StorageDriverClaim]: + if self._no_store: + return [] + return await super().store(context, payloads) + + async def retrieve( + self, + context: StorageDriverRetrieveContext, + claims: Sequence[StorageDriverClaim], + ) -> list[Payload]: + if self._no_retrieve: + return [] + if self._raise_payload_not_found: + raise ApplicationError( + "Payload not found because the bucket does not exist.", + type="BucketNotFoundError", + non_retryable=True, + ) + return await super().retrieve(context, claims) + + +async def test_extstore_activity_input_no_retrieve( + env: WorkflowEnvironment, +): + """When the driver's retrieve returns no payloads for an externalized + activity input, the activity fails and the workflow terminates with a + WorkflowFailureError wrapping an ActivityError.""" + driver = BadTestDriver(no_retrieve=True) + + client = await env.connect_client( + data_converter=dataclasses.replace( + temporalio.converter.default(), + external_storage=ExternalStorage( + drivers=[driver], + payload_size_threshold=1024, + ), + ), + ) + + async with new_worker( + client, ExtStoreWorkflow, activities=[ext_store_activity] + ) as worker: + handle = await client.start_workflow( + ExtStoreWorkflow.run, + ExtStoreWorkflowInput( + input_data="workflow input", + activity_input_size=1000, + activity_output_size=10, + output_size=10, + max_activity_attempts=1, + ), + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + with pytest.raises(WorkflowFailureError) as err: + await handle.result() + + assert isinstance(err.value.cause, ActivityError) + assert isinstance(err.value.cause.cause, ApplicationError) + assert err.value.cause.cause.message == "Failed decoding arguments" + + +async def test_extstore_activity_result_no_store( + env: WorkflowEnvironment, +): + """When the driver's store returns no claims for an activity result that + exceeds the size threshold, the activity fails to complete and the workflow + terminates with a WorkflowFailureError wrapping an ActivityError.""" + driver = BadTestDriver(no_store=True) + + client = await env.connect_client( + data_converter=dataclasses.replace( + temporalio.converter.default(), + external_storage=ExternalStorage( + drivers=[driver], + payload_size_threshold=1024, + ), + ), + ) + + async with new_worker( + client, ExtStoreWorkflow, activities=[ext_store_activity] + ) as worker: + handle = await client.start_workflow( + ExtStoreWorkflow.run, + ExtStoreWorkflowInput( + input_data="workflow input", + activity_input_size=10, + activity_output_size=1000, + output_size=10, + max_activity_attempts=1, + ), + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + with pytest.raises(WorkflowFailureError) as err: + await handle.result() + + assert isinstance(err.value.cause, ActivityError) + assert isinstance(err.value.cause.cause, ApplicationError) + assert ( + err.value.cause.cause.message + == "Driver 'bad-driver' returned 0 claims, expected 1" + ) + assert err.value.cause.cause.type == "ValueError" + + +async def test_extstore_worker_missing_driver( + env: WorkflowEnvironment, +): + """Validate that when a worker is provided a workflow history with + external storage references and the worker is not configured for external + storage, it will cause a workflow task failure. + """ + driver = InMemoryTestDriver() + + far_client = await env.connect_client( + data_converter=dataclasses.replace( + temporalio.converter.default(), + external_storage=ExternalStorage( + drivers=[driver], + payload_size_threshold=1024, + ), + ), + ) + + worker_client = await env.connect_client() + + async with new_worker( + worker_client, ExtStoreWorkflow, activities=[ext_store_activity] + ) as worker: + handle = await far_client.start_workflow( + ExtStoreWorkflow.run, + ExtStoreWorkflowInput( + input_data="wi" * 1024, + activity_input_size=10, + activity_output_size=10, + output_size=10, + ), + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + await assert_task_fail_eventually(handle) + + +async def test_extstore_payload_not_found_fails_workflow( + env: WorkflowEnvironment, +): + """When a non-retryable ApplicationError is raised while retrieving workflow input, + the workflow must fail terminally (not retry as a task failure). + """ + client = await env.connect_client( + data_converter=dataclasses.replace( + temporalio.converter.default(), + external_storage=ExternalStorage( + drivers=[BadTestDriver(raise_payload_not_found=True)], + payload_size_threshold=1024, + ), + ), + ) + + async with new_worker( + client, ExtStoreWorkflow, activities=[ext_store_activity] + ) as worker: + handle = await client.start_workflow( + ExtStoreWorkflow.run, + ExtStoreWorkflowInput( + input_data="wi" * 512, # exceeds 1024-byte threshold + activity_input_size=10, + activity_output_size=10, + output_size=10, + ), + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + + with pytest.raises(WorkflowFailureError) as exc_info: + await handle.result() + + assert isinstance(exc_info.value.cause, ApplicationError) + assert ( + exc_info.value.cause.message + == "Payload not found because the bucket does not exist." + ) + assert exc_info.value.cause.type == "BucketNotFoundError" + assert exc_info.value.cause.non_retryable is True + + +async def _run_extstore_workflow_and_fetch_history( + env: WorkflowEnvironment, + driver: InMemoryTestDriver, + *, + input_data: str, + activity_output_size: int = 10, +) -> WorkflowHandle: + """Helper: run ExtStoreWorkflow with the given driver and return its history handle.""" + extstore_client = await env.connect_client( + data_converter=dataclasses.replace( + temporalio.converter.default(), + external_storage=ExternalStorage( + drivers=[driver], + payload_size_threshold=512, + ), + ), + ) + async with new_worker( + extstore_client, ExtStoreWorkflow, activities=[ext_store_activity] + ) as worker: + handle = await extstore_client.start_workflow( + ExtStoreWorkflow.run, + ExtStoreWorkflowInput( + input_data=input_data, + activity_input_size=10, + activity_output_size=activity_output_size, + output_size=10, + ), + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.result() + return handle + + +async def test_replay_extstore_history_fails_without_extstore( + env: WorkflowEnvironment, +) -> None: + """A history with externalized workflow input fails to replay when the + Replayer has no external storage configured.""" + driver = InMemoryTestDriver() + handle = await _run_extstore_workflow_and_fetch_history( + env, + driver, + input_data="wi" * 512, # exceeds 512-byte threshold + ) + history = await handle.fetch_history() + + # Replay without external storage: decode_activation raises when it + # encounters a reference payload with no driver configured, producing a + # task failure (not a NondeterminismError). + result = await Replayer(workflows=[ExtStoreWorkflow]).replay_workflow( + history, raise_on_replay_failure=False + ) + assert isinstance(result.replay_failure, RuntimeError) + assert not isinstance(result.replay_failure, workflow.NondeterminismError) + assert ( + "[TMPRL1105] Detected externally stored payload(s) but external storage is not configured." + in result.replay_failure.args[0] + ) + + +async def test_replay_extstore_history_succeeds_with_correct_extstore( + env: WorkflowEnvironment, +) -> None: + """A history with externalized workflow input replays successfully when the + Replayer is configured with the same storage driver that holds the data.""" + driver = InMemoryTestDriver() + handle = await _run_extstore_workflow_and_fetch_history( + env, driver, input_data="wi" * 512 + ) + history = await handle.fetch_history() + + # Replay with the same populated driver — must succeed. + await Replayer( + workflows=[ExtStoreWorkflow], + data_converter=dataclasses.replace( + temporalio.converter.default(), + external_storage=ExternalStorage( + drivers=[driver], + payload_size_threshold=512, + ), + ), + ).replay_workflow(history) + + +async def test_replay_extstore_history_fails_with_empty_driver( + env: WorkflowEnvironment, +) -> None: + """A history with external storage references fails to replay when the + Replayer has external storage configured but the driver holds no data + (simulates pointing at the wrong backend or a purged store).""" + driver = InMemoryTestDriver() + handle = await _run_extstore_workflow_and_fetch_history( + env, driver, input_data="wi" * 512 + ) + history = await handle.fetch_history() + + # Replay with a fresh empty driver — retrieval will fail. + result = await Replayer( + workflows=[ExtStoreWorkflow], + data_converter=dataclasses.replace( + temporalio.converter.default(), + external_storage=ExternalStorage( + drivers=[InMemoryTestDriver()], + payload_size_threshold=512, + ), + ), + ).replay_workflow(history, raise_on_replay_failure=False) + # InMemoryTestDriver raises ApplicationError for absent keys. + # ApplicationError is re-raised without wrapping, so it propagates + # through decode_activation (before the workflow task runs). The core SDK + # receives an activation failure, issues a FailWorkflow command, but the + # next history event is ActivityTaskScheduled — causing a NondeterminismError. + assert isinstance(result.replay_failure, workflow.NondeterminismError) + + +async def test_replay_extstore_activity_result_fails_without_extstore( + env: WorkflowEnvironment, +) -> None: + """A history where only the activity result was stored externally also fails + to replay without external storage, verifying that mid-workflow reference + payloads are caught regardless of whether the workflow uses the result.""" + driver = InMemoryTestDriver() + handle = await _run_extstore_workflow_and_fetch_history( + env, + driver, + input_data="small", # well under 512 bytes — stays inline + activity_output_size=2048, # 2 KB result — stored externally + ) + history = await handle.fetch_history() + + # Replay without external storage. The workflow input decodes fine, but + # decode_activation raises when the ActivityTaskCompleted reference payload + # is encountered, producing a task failure (not a NondeterminismError). + result = await Replayer(workflows=[ExtStoreWorkflow]).replay_workflow( + history, raise_on_replay_failure=False + ) + assert isinstance(result.replay_failure, RuntimeError) + assert not isinstance(result.replay_failure, workflow.NondeterminismError) + assert ( + "[TMPRL1105] Detected externally stored payload(s) but external storage is not configured." + in result.replay_failure.args[0] + ) + + +async def test_extstore_chained_activities( + env: WorkflowEnvironment, +) -> None: + """Large activity output is transparently offloaded and passed to a second activity. + + This is a representative customer scenario: activity A returns a payload that + exceeds the size threshold (e.g. a fetched document), external storage offloads + it so it never bloats workflow history, and activity B receives it as its input + without any special handling in the workflow code. + """ + driver = InMemoryTestDriver() + + client = await env.connect_client( + data_converter=dataclasses.replace( + temporalio.converter.default(), + external_storage=ExternalStorage( + drivers=[driver], + payload_size_threshold=1024, # 1 KB threshold + ), + ), + ) + + # process_data returns 10 KB — well above the 1 KB threshold. + payload_size = 10_000 + + async with new_worker( + client, + ChainedExtStoreWorkflow, + activities=[process_data, summarize], + ) as worker: + result = await client.execute_workflow( + ChainedExtStoreWorkflow.run, + payload_size, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=30), + ) + + # The second activity received the full payload and summarized it correctly. + assert result == f"received {payload_size} bytes" + + # External storage was actually used: the large activity result and its + # re-use as the second activity's input should have triggered at least two + # round-trips (one store on completion, one retrieve on the next WFT). + assert driver._store_calls == 2 + assert driver._retrieve_calls == 2 + + +async def test_worker_storage_drivers_populated_from_client( + env: WorkflowEnvironment, +): + """Worker._storage_drivers is populated from the client's ExternalStorage and + passed to the bridge config as a set of driver type strings.""" + + class DifferentTestDriver(InMemoryTestDriver): + def __init__(self, driver_name: str): + super().__init__(driver_name=driver_name) + + driver1 = InMemoryTestDriver(driver_name="driver1") + driver2 = InMemoryTestDriver(driver_name="driver2") + driver3 = DifferentTestDriver(driver_name="driver3") + + client = await env.connect_client( + data_converter=dataclasses.replace( + temporalio.converter.default(), + external_storage=ExternalStorage( + drivers=[driver1, driver2, driver3], + driver_selector=lambda _context, _payload: driver1, + payload_size_threshold=0, + ), + ), + ) + + captured_config: list[temporalio.bridge.worker.WorkerConfig] = [] + original_create = temporalio.bridge.worker.Worker.create + + def capture_config( + bridge_client: temporalio.bridge.client.Client, + config: temporalio.bridge.worker.WorkerConfig, + ): + captured_config.append(config) + return original_create(bridge_client, config) + + with mock.patch.object( + temporalio.bridge.worker.Worker, "create", side_effect=capture_config + ): + async with new_worker( + client, ExtStoreWorkflow, activities=[ext_store_activity] + ) as worker: + assert worker._storage_drivers == [driver1, driver2, driver3] + + assert len(captured_config) == 1 + assert captured_config[0].storage_drivers == {driver1.type(), driver3.type()} + + +async def test_worker_storage_drivers_empty_without_external_storage( + env: WorkflowEnvironment, +): + """Worker._storage_drivers is empty when the client has no ExternalStorage.""" + async with new_worker( + env.client, ExtStoreWorkflow, activities=[ext_store_activity] + ) as worker: + assert worker._storage_drivers == [] + + +# --------------------------------------------------------------------------- +# TMPRL1104 workflow task duration logging +# --------------------------------------------------------------------------- + +_workflow_logger = logging.getLogger(temporalio.worker._workflow.__name__) + + +def _tmprl1104_records(capturer: LogCapturer) -> list[logging.LogRecord]: + """Return all TMPRL1104 log records from the capturer.""" + return capturer.find_all(lambda r: r.getMessage().startswith("[TMPRL1104]")) + + +# Accept any duration-bucket wording: a loaded host can push a trivial task past 5s. +_TMPRL1104_DURATION_MESSAGE = re.compile( + r"\[TMPRL1104\] [^:]+:\d+:\d+ Workflow task " + r"(?:duration information|exceeded \d+ seconds) \(" +) + + +async def _expected_payload_size( + converter: temporalio.converter.DataConverter, value: object +) -> int: + """Encode a value and return the protobuf ByteSize of the resulting payload.""" + payloads = converter.payload_converter.to_payloads([value]) + return payloads[0].ByteSize() + + +@workflow.defn +class SimpleWorkflow: + """Minimal workflow for testing logging without external storage.""" + + @workflow.run + async def run(self) -> str: + return "done" + + +async def test_tmprl1104_no_extstore(env: WorkflowEnvironment) -> None: + """Without external storage, TMPRL1104 logs contain duration but no + download/upload metrics.""" + with LogCapturer().logs_captured(_workflow_logger, level=logging.DEBUG) as capturer: + async with new_worker(env.client, SimpleWorkflow) as worker: + await env.client.execute_workflow( + SimpleWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + records = _tmprl1104_records(capturer) + assert len(records) == 1 + record = records[0] + assert _TMPRL1104_DURATION_MESSAGE.match(record.getMessage()) + assert hasattr(record, "workflow_task_duration") + assert hasattr(record, "event_id") + # No external storage — download/upload fields must be absent + assert not hasattr(record, "payload_download_count") + assert not hasattr(record, "payload_download_size") + assert not hasattr(record, "payload_download_duration") + assert not hasattr(record, "payload_upload_count") + assert not hasattr(record, "payload_upload_size") + assert not hasattr(record, "payload_upload_duration") + + +async def test_tmprl1104_with_extstore_download(env: WorkflowEnvironment) -> None: + """When external storage decodes payloads, TMPRL1104 logs include download + metrics on the activation that retrieves them.""" + driver = InMemoryTestDriver() + data_converter = dataclasses.replace( + temporalio.converter.default(), + external_storage=ExternalStorage( + drivers=[driver], + payload_size_threshold=512, + ), + ) + client = await env.connect_client( + data_converter=data_converter, + ) + + wf_input = ExtStoreWorkflowInput( + input_data="wi" * 512, # exceeds 512-byte threshold → stored externally + activity_input_size=10, + activity_output_size=10, + output_size=10, + ) + expected_input_size = await _expected_payload_size(data_converter, wf_input) + + with LogCapturer().logs_captured(_workflow_logger, level=logging.DEBUG) as capturer: + async with new_worker( + client, ExtStoreWorkflow, activities=[ext_store_activity] + ) as worker: + await client.execute_workflow( + ExtStoreWorkflow.run, + wf_input, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + records = _tmprl1104_records(capturer) + assert len(records) == 2 + + # WFT 1: retrieves the externalized workflow input + assert _TMPRL1104_DURATION_MESSAGE.match(records[0].getMessage()) + assert getattr(records[0], "payload_download_count") == 1 + assert getattr(records[0], "payload_download_size") == expected_input_size + assert getattr(records[0], "payload_download_duration") > timedelta(0) + assert not hasattr(records[0], "payload_upload_count") + + # WFT 2: activity result is small — no external storage + assert _TMPRL1104_DURATION_MESSAGE.match(records[1].getMessage()) + assert not hasattr(records[1], "payload_download_count") + assert not hasattr(records[1], "payload_upload_count") + + +async def test_tmprl1104_with_extstore_upload(env: WorkflowEnvironment) -> None: + """When external storage encodes payloads, TMPRL1104 logs include upload + metrics on the WFT that produces them.""" + driver = InMemoryTestDriver() + data_converter = dataclasses.replace( + temporalio.converter.default(), + external_storage=ExternalStorage( + drivers=[driver], + payload_size_threshold=512, + ), + ) + client = await env.connect_client( + data_converter=data_converter, + ) + + wf_output = "wo" * 1024 # 2048 bytes → stored externally + expected_output_size = await _expected_payload_size(data_converter, wf_output) + + with LogCapturer().logs_captured(_workflow_logger, level=logging.DEBUG) as capturer: + async with new_worker( + client, ExtStoreWorkflow, activities=[ext_store_activity] + ) as worker: + await client.execute_workflow( + ExtStoreWorkflow.run, + ExtStoreWorkflowInput( + input_data="small", + activity_input_size=10, + activity_output_size=10, + output_size=2048, # large output → stored externally on completion + ), + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + records = _tmprl1104_records(capturer) + assert len(records) == 2 + + # WFT 1: small input — no external storage + assert _TMPRL1104_DURATION_MESSAGE.match(records[0].getMessage()) + assert not hasattr(records[0], "payload_download_count") + assert not hasattr(records[0], "payload_upload_count") + + # WFT 2: workflow returns large result → uploaded + assert _TMPRL1104_DURATION_MESSAGE.match(records[1].getMessage()) + assert not hasattr(records[1], "payload_download_count") + assert getattr(records[1], "payload_upload_count") == 1 + assert getattr(records[1], "payload_upload_size") == expected_output_size + assert getattr(records[1], "payload_upload_duration") > timedelta(0) + + +async def test_tmprl1104_with_extstore_download_and_upload( + env: WorkflowEnvironment, +) -> None: + """When both download and upload happen across WFTs, TMPRL1104 logs include + both sets of metrics.""" + driver = InMemoryTestDriver() + data_converter = dataclasses.replace( + temporalio.converter.default(), + external_storage=ExternalStorage( + drivers=[driver], + payload_size_threshold=512, + ), + ) + client = await env.connect_client( + data_converter=data_converter, + ) + + wf_input = ExtStoreWorkflowInput( + input_data="wi" * 512, # large input → download on first WFT + activity_input_size=10, + activity_output_size=10, + output_size=2048, # large output → upload on final WFT + ) + expected_input_size = await _expected_payload_size(data_converter, wf_input) + wf_output = "wo" * 1024 + expected_output_size = await _expected_payload_size(data_converter, wf_output) + + with LogCapturer().logs_captured(_workflow_logger, level=logging.DEBUG) as capturer: + async with new_worker( + client, ExtStoreWorkflow, activities=[ext_store_activity] + ) as worker: + await client.execute_workflow( + ExtStoreWorkflow.run, + wf_input, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + records = _tmprl1104_records(capturer) + assert len(records) == 2 + + # WFT 1: retrieves externalized workflow input + assert _TMPRL1104_DURATION_MESSAGE.match(records[0].getMessage()) + assert getattr(records[0], "payload_download_count") == 1 + assert getattr(records[0], "payload_download_size") == expected_input_size + assert getattr(records[0], "payload_download_duration") > timedelta(0) + assert not hasattr(records[0], "payload_upload_count") + + # WFT 2: uploads externalized workflow result + assert _TMPRL1104_DURATION_MESSAGE.match(records[1].getMessage()) + assert not hasattr(records[1], "payload_download_count") + assert getattr(records[1], "payload_upload_count") == 1 + assert getattr(records[1], "payload_upload_size") == expected_output_size + assert getattr(records[1], "payload_upload_duration") > timedelta(0) + + +# --------------------------------------------------------------------------- +# Store-metadata context tests +# --------------------------------------------------------------------------- + + +class ContextTrackingStorageDriver(StorageDriver): + """In-memory driver that records the store context on each store/retrieve.""" + + def __init__(self) -> None: + self._storage: dict[str, bytes] = {} + self.store_contexts: list[StorageDriverStoreContext] = [] + + def name(self) -> str: + return "context-tracking" + + async def store( + self, + context: StorageDriverStoreContext, + payloads: Sequence[Payload], + ) -> list[StorageDriverClaim]: + self.store_contexts.append(context) + claims: list[StorageDriverClaim] = [] + for payload in payloads: + key = f"payload-{len(self._storage)}" + self._storage[key] = payload.SerializeToString() + claims.append(StorageDriverClaim(claim_data={"key": key})) + return claims + + async def retrieve( + self, + context: StorageDriverRetrieveContext, + claims: Sequence[StorageDriverClaim], + ) -> list[Payload]: + results: list[Payload] = [] + for claim in claims: + payload = Payload() + payload.ParseFromString(self._storage[claim.claim_data["key"]]) + results.append(payload) + return results + + +@workflow.defn +class SignalWaitWorkflow: + def __init__(self) -> None: + self._signal_data: str | None = None + + @workflow.run + async def run(self, _arg: str) -> str: + await workflow.wait_condition(lambda: self._signal_data is not None) + return self._signal_data # type: ignore + + @workflow.signal + async def my_signal(self, data: str) -> None: + self._signal_data = data + + +@workflow.defn +class EchoWorkflow: + @workflow.run + async def run(self, data: str) -> str: + return data + + +@workflow.defn +class ChildWorkflowStoreMetadataTestWorkflow: + @workflow.run + async def run(self, data: str) -> str: + return await workflow.execute_child_workflow( + EchoWorkflow.run, + data, + id=f"{workflow.info().workflow_id}-child", + ) + + +async def _make_tracking_client( + env: WorkflowEnvironment, +) -> tuple[Client, ContextTrackingStorageDriver]: + driver = ContextTrackingStorageDriver() + client = await env.connect_client( + data_converter=dataclasses.replace( + temporalio.converter.default(), + external_storage=ExternalStorage( + drivers=[driver], + payload_size_threshold=0, + ), + ), + ) + return client, driver + + +async def test_store_metadata_start_workflow(env: WorkflowEnvironment) -> None: + """start_workflow should set workflow id and type on store context.""" + client, driver = await _make_tracking_client(env) + workflow_id = str(uuid.uuid4()) + + async with new_worker(client, EchoWorkflow) as worker: + await client.execute_workflow( + EchoWorkflow.run, + "hello", + id=workflow_id, + task_queue=worker.task_queue, + ) + + assert len(driver.store_contexts) == 2 + + # [0] Workflow input arg + client_ctx = driver.store_contexts[0] + assert isinstance(client_ctx.target, StorageDriverWorkflowInfo) + assert client_ctx.target.namespace == client.namespace + assert client_ctx.target.id == workflow_id + assert client_ctx.target.type == "EchoWorkflow" + assert client_ctx.target.run_id is None + + # [1] Workflow result + worker_ctx = driver.store_contexts[1] + assert isinstance(worker_ctx.target, StorageDriverWorkflowInfo) + assert worker_ctx.target.namespace == client.namespace + assert worker_ctx.target.id == workflow_id + assert worker_ctx.target.type == "EchoWorkflow" + assert worker_ctx.target.run_id is not None + + +async def test_store_metadata_signal_with_start(env: WorkflowEnvironment) -> None: + """signal_with_start should set workflow metadata for signal arg encoding.""" + client, driver = await _make_tracking_client(env) + workflow_id = str(uuid.uuid4()) + + async with new_worker(client, SignalWaitWorkflow) as worker: + handle = await client.start_workflow( + SignalWaitWorkflow.run, + "hello", + id=workflow_id, + task_queue=worker.task_queue, + start_signal="my_signal", + start_signal_args=["signal-data"], + ) + await handle.result() + + assert len(driver.store_contexts) == 3 + + # [0] Workflow input arg + input_ctx = driver.store_contexts[0] + assert isinstance(input_ctx.target, StorageDriverWorkflowInfo) + assert input_ctx.target.id == workflow_id + assert input_ctx.target.type == "SignalWaitWorkflow" + assert input_ctx.target.run_id is None + + # [1] Signal arg + signal_ctx = driver.store_contexts[1] + assert isinstance(signal_ctx.target, StorageDriverWorkflowInfo) + assert signal_ctx.target.id == workflow_id + assert signal_ctx.target.type == "SignalWaitWorkflow" + assert signal_ctx.target.run_id is None + + # [2] Workflow result + result_ctx = driver.store_contexts[2] + assert isinstance(result_ctx.target, StorageDriverWorkflowInfo) + assert result_ctx.target.id == workflow_id + assert result_ctx.target.type == "SignalWaitWorkflow" + assert result_ctx.target.run_id is not None + + +async def test_store_metadata_signal_workflow(env: WorkflowEnvironment) -> None: + """signal_workflow should set workflow id on store context.""" + client, driver = await _make_tracking_client(env) + workflow_id = str(uuid.uuid4()) + + async with new_worker(client, SignalWaitWorkflow) as worker: + handle = await client.start_workflow( + SignalWaitWorkflow.run, + "hello", + id=workflow_id, + task_queue=worker.task_queue, + ) + # Signal separately (not signal-with-start) + await handle.signal(SignalWaitWorkflow.my_signal, "signal-data") + await handle.result() + + assert len(driver.store_contexts) == 3 + + # [0] Client starts workflow + start_ctx = driver.store_contexts[0] + assert isinstance(start_ctx.target, StorageDriverWorkflowInfo) + assert start_ctx.target.id == workflow_id + assert start_ctx.target.type == "SignalWaitWorkflow" + assert start_ctx.target.run_id is None + + # [1] Client sends signal: type and run_id are unknown at signal time + signal_ctx = driver.store_contexts[1] + assert isinstance(signal_ctx.target, StorageDriverWorkflowInfo) + assert signal_ctx.target.id == workflow_id + assert signal_ctx.target.type is None + assert signal_ctx.target.run_id is None + + # [2] Workflow worker returns result + result_ctx = driver.store_contexts[2] + assert isinstance(result_ctx.target, StorageDriverWorkflowInfo) + assert result_ctx.target.id == workflow_id + assert result_ctx.target.type == "SignalWaitWorkflow" + assert result_ctx.target.run_id is not None + + +async def test_store_metadata_schedule_action(env: WorkflowEnvironment) -> None: + """Schedule action _to_proto should set workflow metadata.""" + if env.supports_time_skipping: + pytest.skip("Java test server doesn't support schedules") + client, driver = await _make_tracking_client(env) + task_queue = str(uuid.uuid4()) + schedule_id = f"sched-{uuid.uuid4()}" + + try: + await client.create_schedule( + schedule_id, + temporalio.client.Schedule( + action=temporalio.client.ScheduleActionStartWorkflow( + EchoWorkflow.run, + "hello", + id=f"wf-{schedule_id}", + task_queue=task_queue, + ), + spec=temporalio.client.ScheduleSpec(), + ), + ) + + assert len(driver.store_contexts) == 1 + + # [0] Client encodes workflow args when creating the schedule action + ctx = driver.store_contexts[0] + assert isinstance(ctx.target, StorageDriverWorkflowInfo) + assert ctx.target.namespace == client.namespace + assert ctx.target.id == f"wf-{schedule_id}" + assert ctx.target.type == "EchoWorkflow" + assert ctx.target.run_id is None + finally: + try: + handle = client.get_schedule_handle(schedule_id) + await handle.delete() + except Exception: + pass + + +async def test_store_metadata_child_workflow(env: WorkflowEnvironment) -> None: + """External storage should receive the child workflow as the target when scheduling.""" + client, driver = await _make_tracking_client(env) + workflow_id = f"workflow-{uuid.uuid4()}" + child_workflow_id = f"{workflow_id}-child" + + async with new_worker( + client, + ChildWorkflowStoreMetadataTestWorkflow, + EchoWorkflow, + ) as worker: + await client.execute_workflow( + ChildWorkflowStoreMetadataTestWorkflow.run, + "hello", + id=workflow_id, + task_queue=worker.task_queue, + ) + + assert len(driver.store_contexts) == 4 + + # [0] Client starts parent workflow + client_ctx = driver.store_contexts[0] + assert isinstance(client_ctx.target, StorageDriverWorkflowInfo) + assert client_ctx.target.id == workflow_id + assert client_ctx.target.type == "ChildWorkflowStoreMetadataTestWorkflow" + assert client_ctx.target.run_id is None + + # [1] Parent schedules child: target = child workflow + start_child_ctx = driver.store_contexts[1] + assert isinstance(start_child_ctx.target, StorageDriverWorkflowInfo) + assert start_child_ctx.target.id == child_workflow_id + assert start_child_ctx.target.type == "EchoWorkflow" + assert start_child_ctx.target.run_id is None + + # [2] Child returns result: target = parent workflow (child results are + # stored in the parent's key space so they remain accessible during replay) + child_result_ctx = driver.store_contexts[2] + assert isinstance(child_result_ctx.target, StorageDriverWorkflowInfo) + assert child_result_ctx.target.id == workflow_id + # ParentInfo does not carry workflow type + assert child_result_ctx.target.type is None + assert child_result_ctx.target.run_id is not None + + # [3] Parent returns result: target = parent (current execution) + parent_result_ctx = driver.store_contexts[3] + assert isinstance(parent_result_ctx.target, StorageDriverWorkflowInfo) + assert parent_result_ctx.target.id == workflow_id + assert parent_result_ctx.target.type == "ChildWorkflowStoreMetadataTestWorkflow" + assert parent_result_ctx.target.run_id is not None + + +# Workflow definitions for gap tests + + +@activity.defn +async def echo_activity(input: str) -> str: + """Simple activity that returns its input.""" + return input + + +@workflow.defn +class ActivityScheduleMetadataWorkflow: + """Workflow that schedules an activity to test activity metadata on the store context.""" + + @workflow.run + async def run(self, data: str) -> str: + return await workflow.execute_activity( + echo_activity, + data, + activity_id="my-activity-id", + schedule_to_close_timeout=timedelta(seconds=10), + ) + + +@workflow.defn +class SignalExternalMetadataWorkflow: + """Workflow that signals another workflow.""" + + @workflow.run + async def run(self, target_workflow_id: str) -> None: + await workflow.get_external_workflow_handle(target_workflow_id).signal( + SignalWaitWorkflow.my_signal, "signal-from-workflow" + ) + + +async def test_store_metadata_activity_scheduling(env: WorkflowEnvironment) -> None: + """When a workflow schedules an activity, context.activity should be populated.""" + client, driver = await _make_tracking_client(env) + workflow_id = f"workflow-{uuid.uuid4()}" + + async with new_worker( + client, + ActivityScheduleMetadataWorkflow, + activities=[echo_activity], + ) as worker: + await client.execute_workflow( + ActivityScheduleMetadataWorkflow.run, + "hello", + id=workflow_id, + task_queue=worker.task_queue, + ) + + assert len(driver.store_contexts) == 4 + + # [0] Client starts workflow + client_ctx = driver.store_contexts[0] + assert isinstance(client_ctx.target, StorageDriverWorkflowInfo) + assert client_ctx.target.id == workflow_id + assert client_ctx.target.type == "ActivityScheduleMetadataWorkflow" + assert client_ctx.target.run_id is None + + # [1] Workflow worker schedules activity + schedule_ctx = driver.store_contexts[1] + assert isinstance(schedule_ctx.target, StorageDriverWorkflowInfo) + assert schedule_ctx.target.namespace == client.namespace + assert schedule_ctx.target.id == workflow_id + assert schedule_ctx.target.type == "ActivityScheduleMetadataWorkflow" + assert schedule_ctx.target.run_id is not None + + # [2] Activity worker completes + execute_ctx = driver.store_contexts[2] + assert isinstance(execute_ctx.target, StorageDriverWorkflowInfo) + assert execute_ctx.target.namespace == client.namespace + assert execute_ctx.target.id == workflow_id + assert execute_ctx.target.type == "ActivityScheduleMetadataWorkflow" + assert execute_ctx.target.run_id is not None + + # [3] Workflow returns result + result_ctx = driver.store_contexts[3] + assert isinstance(result_ctx.target, StorageDriverWorkflowInfo) + assert result_ctx.target.id == workflow_id + assert result_ctx.target.type == "ActivityScheduleMetadataWorkflow" + assert result_ctx.target.run_id is not None + + +async def test_store_metadata_signal_external_workflow( + env: WorkflowEnvironment, +) -> None: + """Signaling an external workflow should set workflow.id to the target.""" + client, driver = await _make_tracking_client(env) + target_workflow_id = f"target-{uuid.uuid4()}" + sender_workflow_id = f"sender-{uuid.uuid4()}" + + async with new_worker( + client, + SignalExternalMetadataWorkflow, + SignalWaitWorkflow, + ) as worker: + # Start the target workflow first + target_handle = await client.start_workflow( + SignalWaitWorkflow.run, + "waiting", + id=target_workflow_id, + task_queue=worker.task_queue, + ) + # Start the sender which will signal the target + await client.execute_workflow( + SignalExternalMetadataWorkflow.run, + target_workflow_id, + id=sender_workflow_id, + task_queue=worker.task_queue, + ) + await target_handle.result() + + assert len(driver.store_contexts) == 5 + + # [0] Client starts target workflow (SignalWaitWorkflow) + target_start_ctx = driver.store_contexts[0] + assert isinstance(target_start_ctx.target, StorageDriverWorkflowInfo) + assert target_start_ctx.target.id == target_workflow_id + assert target_start_ctx.target.type == "SignalWaitWorkflow" + assert target_start_ctx.target.run_id is None + + # [1] Client starts sender workflow (SignalExternalMetadataWorkflow) + sender_start_ctx = driver.store_contexts[1] + assert isinstance(sender_start_ctx.target, StorageDriverWorkflowInfo) + assert sender_start_ctx.target.id == sender_workflow_id + assert sender_start_ctx.target.type == "SignalExternalMetadataWorkflow" + assert sender_start_ctx.target.run_id is None + + # [2] Sender signals target: target = the workflow being signaled + signal_ctx = driver.store_contexts[2] + assert isinstance(signal_ctx.target, StorageDriverWorkflowInfo) + assert signal_ctx.target.id == target_workflow_id + assert signal_ctx.target.type is None + assert signal_ctx.target.run_id is None + + # [3] and [4] are the sender and target workflow completions in some order. + # The sender's WFT 2 (after signal resolution) and the target's WFT (after + # receiving the signal) are both scheduled by the server at nearly the same + # time, so the order of their completions is non-deterministic. + completion_ctxs = { + ctx.target.id: ctx + for ctx in driver.store_contexts[3:5] + if isinstance(ctx.target, StorageDriverWorkflowInfo) and ctx.target.id + } + assert sender_workflow_id in completion_ctxs + assert target_workflow_id in completion_ctxs + + sender_result_ctx = completion_ctxs[sender_workflow_id] + assert isinstance(sender_result_ctx.target, StorageDriverWorkflowInfo) + assert sender_result_ctx.target.run_id is not None + + target_result_ctx = completion_ctxs[target_workflow_id] + assert isinstance(target_result_ctx.target, StorageDriverWorkflowInfo) + assert target_result_ctx.target.run_id is not None + + +async def test_store_metadata_standalone_activity(env: WorkflowEnvironment) -> None: + """Standalone activity worker should use StorageDriverActivityInfo as target.""" + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2741" + ) + client, driver = await _make_tracking_client(env) + activity_id = f"activity-{uuid.uuid4()}" + + async with new_worker(client, activities=[echo_activity]) as worker: + await client.execute_activity( + echo_activity, + "hello", + id=activity_id, + task_queue=worker.task_queue, + schedule_to_close_timeout=timedelta(seconds=30), + ) + + assert len(driver.store_contexts) == 2 + + client_ctx = driver.store_contexts[0] + # [0] Client schedules standalone activity + assert isinstance(client_ctx.target, StorageDriverActivityInfo) + assert client_ctx.target.namespace == client.namespace + assert client_ctx.target.id == activity_id + assert client_ctx.target.type == "echo_activity" + assert client_ctx.target.run_id is None + + # [1] Activity worker completes: target = activity (no parent workflow) + execute_ctx = driver.store_contexts[1] + assert isinstance(execute_ctx.target, StorageDriverActivityInfo) + assert execute_ctx.target.namespace == client.namespace + assert execute_ctx.target.id == activity_id + assert execute_ctx.target.type == "echo_activity" + assert execute_ctx.target.run_id is not None + + +@workflow.defn +class ContinueAsNewExtStoreWorkflow: + """Workflow that continues-as-new once with a large payload. + + Run 1: called with large_payload, waits for signal, calls continue_as_new with same payload. + Run 2: called with large_payload again (from CaN), returns immediately. + """ + + def __init__(self) -> None: + self._proceed = False + + @workflow.run + async def run(self, large_payload: str) -> str: + if workflow.info().continued_run_id is None: + await workflow.wait_condition(lambda: self._proceed) + workflow.continue_as_new(large_payload) + return "done" + + @workflow.signal + def proceed(self) -> None: + self._proceed = True + + +async def test_extstore_continue_as_new_result_stored_under_current_run( + env: WorkflowEnvironment, +) -> None: + """A CaN continuation's result payloads are stored under the continuation's + own run_id, not under the originating run's run_id. + """ + client, driver = await _make_tracking_client(env) + + async with new_worker(client, ContinueAsNewExtStoreWorkflow) as worker: + handle = await client.start_workflow( + ContinueAsNewExtStoreWorkflow.run, + "x" * 1024, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + # Capture the first run_id before signalling the workflow to proceed. + first_run_id = (await handle.describe()).run_id + await handle.signal(ContinueAsNewExtStoreWorkflow.proceed) + await handle.result() + last_run_id = (await handle.describe()).run_id + assert len(driver.store_contexts) == 3 + + # [0] Client starts workflow + client_ctx = driver.store_contexts[0] + assert isinstance(client_ctx.target, StorageDriverWorkflowInfo) + assert client_ctx.target.run_id is None + + # [1] Workflow 1 encodes CaN args + can_args_ctx = driver.store_contexts[1] + assert isinstance(can_args_ctx.target, StorageDriverWorkflowInfo) + assert can_args_ctx.target.run_id == first_run_id + + # [2] Workflow 2 encodes result in its own context + result_ctx = driver.store_contexts[2] + assert isinstance(result_ctx.target, StorageDriverWorkflowInfo) + assert result_ctx.target.run_id is not None + assert result_ctx.target.run_id == last_run_id diff --git a/tests/worker/test_interceptor.py b/tests/worker/test_interceptor.py index 0df57c606..4a1da399d 100644 --- a/tests/worker/test_interceptor.py +++ b/tests/worker/test_interceptor.py @@ -1,9 +1,13 @@ import asyncio import uuid +from collections.abc import Callable from datetime import timedelta -from typing import Any, Callable, List, NoReturn, Optional, Tuple, Type +from typing import Any, NoReturn -from temporalio import activity, workflow +import nexusrpc +import pytest + +from temporalio import activity, nexus, workflow from temporalio.client import Client, WorkflowUpdateFailedError from temporalio.exceptions import ApplicationError, NexusOperationError from temporalio.testing import WorkflowEnvironment @@ -12,30 +16,28 @@ ActivityOutboundInterceptor, ContinueAsNewInput, ExecuteActivityInput, + ExecuteNexusOperationCancelInput, + ExecuteNexusOperationStartInput, ExecuteWorkflowInput, HandleQueryInput, HandleSignalInput, HandleUpdateInput, Interceptor, + NexusOperationInboundInterceptor, SignalChildWorkflowInput, SignalExternalWorkflowInput, StartActivityInput, StartChildWorkflowInput, StartLocalActivityInput, + StartNexusOperationInput, Worker, WorkflowInboundInterceptor, WorkflowInterceptorClassInput, WorkflowOutboundInterceptor, ) -from temporalio.worker._interceptor import StartNexusOperationInput -from tests.helpers.nexus import create_nexus_endpoint, make_nexus_endpoint_name - -# Passing through because Python 3.9 has an import bug at -# https://github.com/python/cpython/issues/91351 -with workflow.unsafe.imports_passed_through(): - import pytest +from tests.helpers.nexus import make_nexus_endpoint_name -interceptor_traces: List[Tuple[str, Any]] = [] +interceptor_traces: list[tuple[str, Any]] = [] class TracingWorkerInterceptor(Interceptor): @@ -46,9 +48,14 @@ def intercept_activity( def workflow_interceptor_class( self, input: WorkflowInterceptorClassInput - ) -> Optional[Type[WorkflowInboundInterceptor]]: + ) -> type[WorkflowInboundInterceptor] | None: return TracingWorkflowInboundInterceptor + def intercept_nexus_operation( + self, next: NexusOperationInboundInterceptor + ) -> NexusOperationInboundInterceptor: + return TracingNexusInboundInterceptor(next) + class TracingActivityInboundInterceptor(ActivityInboundInterceptor): def init(self, outbound: ActivityOutboundInterceptor) -> None: @@ -136,6 +143,50 @@ async def start_nexus_operation( return await super().start_nexus_operation(input) +class TracingNexusInboundInterceptor(NexusOperationInboundInterceptor): + async def execute_nexus_operation_start( + self, input: ExecuteNexusOperationStartInput + ) -> ( + nexusrpc.handler.StartOperationResultSync[Any] + | nexusrpc.handler.StartOperationResultAsync + ): + interceptor_traces.append( + (f"nexus.start_operation.{input.ctx.service}.{input.ctx.operation}", input) + ) + return await super().execute_nexus_operation_start(input) + + async def execute_nexus_operation_cancel( + self, input: ExecuteNexusOperationCancelInput + ) -> None: + interceptor_traces.append( + (f"nexus.cancel_operation.{input.ctx.service}.{input.ctx.operation}", input) + ) + return await super().execute_nexus_operation_cancel(input) + + +@workflow.defn +class ExpectCancelNexusWorkflow: + @workflow.run + async def run(self, _input: str): + try: + await asyncio.wait_for(asyncio.Future(), 2) + except asyncio.TimeoutError: + raise ApplicationError("expected cancellation") + + +@nexusrpc.handler.service_handler +class InterceptedNexusService: + @nexus.workflow_run_operation + async def intercepted_operation( + self, ctx: nexus.WorkflowRunOperationContext, input: str + ) -> nexus.WorkflowHandle[None]: + return await ctx.start_workflow( + ExpectCancelNexusWorkflow.run, + input, + id=f"wf-{uuid.uuid4()}-{ctx.request_id}", + ) + + @activity.defn async def intercepted_activity(param: str) -> str: if not activity.info().is_local: @@ -179,20 +230,18 @@ async def run(self, style: str) -> None: nexus_client = workflow.create_nexus_client( endpoint=make_nexus_endpoint_name(workflow.info().task_queue), - service="non-existent-nexus-service", + service=InterceptedNexusService, + ) + + nexus_handle = await nexus_client.start_operation( + operation=InterceptedNexusService.intercepted_operation, + input="nexus-workflow", ) + nexus_handle.cancel() + try: - await nexus_client.start_operation( - operation="non-existent-nexus-operation", - input={"test": "data"}, - schedule_to_close_timeout=timedelta(microseconds=1), - ) - raise Exception("unreachable") + await nexus_handle except NexusOperationError: - # The test requires only that the workflow attempts to schedule the nexus operation. - # Instead of setting up a nexus service, we deliberately schedule a call to a - # non-existent nexus operation with an insufficiently long timeout, and expect this - # error. pass await self.finish.wait() @@ -203,7 +252,7 @@ def query(self, param: str) -> str: return f"query: {param}" @workflow.signal - def signal(self, param: str) -> None: + def signal(self, _param: str) -> None: self.finish.set() @workflow.update @@ -220,6 +269,7 @@ def update_validated_validator(self, param: str) -> None: raise ApplicationError("Invalid update") +@pytest.mark.requires_local_server async def test_worker_interceptor(client: Client, env: WorkflowEnvironment): # TODO(cretz): Fix if env.supports_time_skipping: @@ -227,14 +277,15 @@ async def test_worker_interceptor(client: Client, env: WorkflowEnvironment): "Java test server: https://github.com/temporalio/sdk-java/issues/1424" ) task_queue = f"task-queue-{uuid.uuid4()}" - await create_nexus_endpoint(task_queue, client) + await env.create_nexus_endpoint(make_nexus_endpoint_name(task_queue), task_queue) async with Worker( client, task_queue=task_queue, - workflows=[InterceptedWorkflow], + workflows=[InterceptedWorkflow, ExpectCancelNexusWorkflow], activities=[intercepted_activity], interceptors=[TracingWorkerInterceptor()], + nexus_service_handlers=[InterceptedNexusService()], ): # Run workflow handle = await client.start_workflow( @@ -257,7 +308,7 @@ async def test_worker_interceptor(client: Client, env: WorkflowEnvironment): await handle.result() # Check traces - def pop_trace(name: str, filter: Optional[Callable[[Any], bool]] = None) -> Any: + def pop_trace(name: str, filter: Callable[[Any], bool] | None = None) -> Any: index = next( ( i @@ -313,6 +364,14 @@ def pop_trace(name: str, filter: Optional[Callable[[Any], bool]] = None) -> Any: assert pop_trace( "workflow.update.validator", lambda v: v.args[0] == "reject-me" ) + assert pop_trace( + "nexus.start_operation.InterceptedNexusService.intercepted_operation", + lambda v: v.input == "nexus-workflow", + ) + assert pop_trace("workflow.execute", lambda v: v.args[0] == "nexus-workflow") + assert pop_trace( + "nexus.cancel_operation.InterceptedNexusService.intercepted_operation", + ) # Confirm no unexpected traces assert not interceptor_traces @@ -321,7 +380,7 @@ def pop_trace(name: str, filter: Optional[Callable[[Any], bool]] = None) -> Any: class WorkflowInstanceAccessInterceptor(Interceptor): def workflow_interceptor_class( self, input: WorkflowInterceptorClassInput - ) -> Optional[Type[WorkflowInboundInterceptor]]: + ) -> type[WorkflowInboundInterceptor] | None: return WorkflowInstanceAccessInboundInterceptor diff --git a/tests/worker/test_payload_size_limits.py b/tests/worker/test_payload_size_limits.py new file mode 100644 index 000000000..18cc7e75a --- /dev/null +++ b/tests/worker/test_payload_size_limits.py @@ -0,0 +1,250 @@ +import logging +import uuid +from dataclasses import dataclass +from datetime import timedelta + +import pytest + +import temporalio.api.enums.v1 +from temporalio import activity, workflow +from temporalio.client import PayloadLimitsConfig, WorkflowFailureError +from temporalio.exceptions import ( + TerminatedError, + TimeoutError, + TimeoutType, +) +from temporalio.runtime import ( + LogForwardingConfig, + LoggingConfig, + Runtime, + TelemetryConfig, + TelemetryFilter, +) +from temporalio.testing._workflow import WorkflowEnvironment +from tests import DEV_SERVER_DOWNLOAD_VERSION +from tests.helpers import LogCapturer, assert_eventually, new_worker + +# Payload/memo size-limit enforcement lives in sdk-rust. These tests only assert that the SDK's +# plumbing reaches core: oversized completions are failed proactively, the worker opt-out lets +# oversized payloads through to the server, and the connection's warn threshold produces a +# forwarded [TMPRL1103] warning. + + +@dataclass +class LargePayloadWorkflowInput: + activity_input_data_size: int + workflow_output_data_size: int + + +@dataclass +class LargePayloadActivityInput: + data: str + + +@activity.defn +async def large_payload_activity(_input: LargePayloadActivityInput) -> None: + return None + + +@workflow.defn +class LargePayloadWorkflow: + @workflow.run + async def run(self, input: LargePayloadWorkflowInput) -> str: + if input.activity_input_data_size > 0: + await workflow.execute_activity( + large_payload_activity, + LargePayloadActivityInput(data="i" * input.activity_input_data_size), + schedule_to_close_timeout=timedelta(seconds=5), + ) + return "o" * input.workflow_output_data_size + + +PAYLOAD_ERROR_LIMIT = 10 * 1024 +PAYLOAD_LIMITS_EXTRA_ARGS = [ + "--dynamic-config-value", + f"limit.blobSize.error={PAYLOAD_ERROR_LIMIT}", + # The server only enforces the error limit for payloads that also exceed the warn limit, so the + # warn limit must be below the error limit. + "--dynamic-config-value", + f"limit.blobSize.warn={2 * 1024}", +] + + +def _forwarding_runtime(logger: logging.Logger) -> Runtime: + return Runtime( + telemetry=TelemetryConfig( + logging=LoggingConfig( + filter=TelemetryFilter(core_level="WARN", other_level="ERROR"), + forwarding=LogForwardingConfig(logger=logger), + ) + ) + ) + + +async def test_oversized_payload_fails_task_with_error_log(env: WorkflowEnvironment): + """An oversized workflow completion is proactively failed by worker, which logs [TMPRL1103].""" + if env.supports_time_skipping: + pytest.skip("Time-skipping server does not report payload limits.") + + async with await WorkflowEnvironment.start_local( + dev_server_extra_args=PAYLOAD_LIMITS_EXTRA_ARGS, + dev_server_download_version=DEV_SERVER_DOWNLOAD_VERSION, + ) as env: + worker_logger = logging.getLogger(f"log-{uuid.uuid4()}") + worker_client = await env.connect_client( + runtime=_forwarding_runtime(worker_logger), + ) + + def predicate(record: logging.LogRecord) -> bool: + return ( + record.levelname == "ERROR" + and "[TMPRL1103] Attempted to upload payloads with size that exceeded the error limit." + in record.msg + ) + + with LogCapturer().logs_captured(worker_logger) as capturer: + async with new_worker( + worker_client, LargePayloadWorkflow, activities=[large_payload_activity] + ) as worker: + handle = await env.client.start_workflow( + LargePayloadWorkflow.run, + LargePayloadWorkflowInput( + activity_input_data_size=0, + workflow_output_data_size=PAYLOAD_ERROR_LIMIT + 1024, + ), + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=3), + ) + + with pytest.raises(WorkflowFailureError) as err: + await handle.result() + assert isinstance(err.value.cause, TimeoutError) + assert err.value.cause.type == TimeoutType.START_TO_CLOSE + + # Core forwards logs on a buffered interval; poll while the capturer is attached. + async def error_forwarded() -> None: + assert capturer.find(predicate) is not None + + await assert_eventually(error_forwarded) + + # Confirm worker reported a WorkflowTaskFailed with cause PAYLOADS_TOO_LARGE. + history = await handle.fetch_history() + assert any( + event.event_type + == temporalio.api.enums.v1.EventType.EVENT_TYPE_WORKFLOW_TASK_FAILED + and event.workflow_task_failed_event_attributes.cause + == temporalio.api.enums.v1.WorkflowTaskFailedCause.WORKFLOW_TASK_FAILED_CAUSE_PAYLOADS_TOO_LARGE + for event in history.events + ) + + +async def test_disable_payload_error_limit_sends_to_server(env: WorkflowEnvironment): + """With the opt-out, worker does not pre-fail; the oversized payload reaches (and is rejected by) + the server.""" + if env.supports_time_skipping: + pytest.skip("Time-skipping server does not report payload limits.") + + async with await WorkflowEnvironment.start_local( + dev_server_extra_args=PAYLOAD_LIMITS_EXTRA_ARGS, + dev_server_download_version=DEV_SERVER_DOWNLOAD_VERSION, + ) as env: + async with new_worker( + env.client, + LargePayloadWorkflow, + activities=[large_payload_activity], + disable_payload_error_limit=True, + ) as worker: + with pytest.raises(WorkflowFailureError) as err: + await env.client.execute_workflow( + LargePayloadWorkflow.run, + LargePayloadWorkflowInput( + activity_input_data_size=PAYLOAD_ERROR_LIMIT + 1024, + workflow_output_data_size=0, + ), + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=3), + ) + + assert isinstance(err.value.cause, TerminatedError) + assert ( + err.value.cause.message + == "BadScheduleActivityAttributes: ScheduleActivityTaskCommandAttributes.Input exceeds size limit." + ) + + +async def test_payload_size_warning_forwarded(env: WorkflowEnvironment): + """The connection's warn threshold produces a forwarded [TMPRL1103] warning for over-threshold payloads.""" + worker_logger = logging.getLogger(f"log-{uuid.uuid4()}") + worker_client = await env.connect_client( + runtime=_forwarding_runtime(worker_logger), + payload_limits=PayloadLimitsConfig(payloads_warn_size=1024), + ) + + def predicate(record: logging.LogRecord) -> bool: + return ( + record.levelname == "WARNING" + and "[TMPRL1103] Attempted to upload payloads with size that exceeded the warning limit." + in record.msg + ) + + with LogCapturer().logs_captured(worker_logger) as capturer: + async with new_worker( + worker_client, LargePayloadWorkflow, activities=[large_payload_activity] + ) as worker: + await worker_client.execute_workflow( + LargePayloadWorkflow.run, + LargePayloadWorkflowInput( + activity_input_data_size=0, + workflow_output_data_size=2 * 1024, + ), + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=5), + ) + + # Core forwards logs on a buffered interval; poll while the capturer is attached. + async def warning_forwarded() -> None: + assert capturer.find(predicate) is not None + + await assert_eventually(warning_forwarded) + + +async def test_memo_size_warning_forwarded(env: WorkflowEnvironment): + """The connection's memo warn threshold produces a forwarded [TMPRL1103] warning for an + over-threshold memo.""" + worker_logger = logging.getLogger(f"log-{uuid.uuid4()}") + worker_client = await env.connect_client( + runtime=_forwarding_runtime(worker_logger), + payload_limits=PayloadLimitsConfig(memo_warn_size=1024), + ) + + def predicate(record: logging.LogRecord) -> bool: + return ( + record.levelname == "WARNING" + and "[TMPRL1103] Attempted to upload memo with size that exceeded the warning limit." + in record.msg + ) + + with LogCapturer().logs_captured(worker_logger) as capturer: + async with new_worker( + worker_client, LargePayloadWorkflow, activities=[large_payload_activity] + ) as worker: + await worker_client.execute_workflow( + LargePayloadWorkflow.run, + LargePayloadWorkflowInput( + activity_input_data_size=0, + workflow_output_data_size=0, + ), + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=5), + memo={"key": "a" * 2048}, + ) + + # Core forwards logs on a buffered interval; poll while the capturer is attached. + async def warning_forwarded() -> None: + assert capturer.find(predicate) is not None + + await assert_eventually(warning_forwarded) diff --git a/tests/worker/test_replayer.py b/tests/worker/test_replayer.py index 146d0792b..52fed7416 100644 --- a/tests/worker/test_replayer.py +++ b/tests/worker/test_replayer.py @@ -6,8 +6,11 @@ from dataclasses import dataclass from datetime import timedelta from pathlib import Path -from typing import Any, Dict, Optional, Type +from typing import Any +import pytest + +import temporalio.worker._workflow_instance from temporalio import activity, workflow from temporalio.client import Client, WorkflowFailureError, WorkflowHistory from temporalio.exceptions import ApplicationError @@ -27,11 +30,6 @@ SignalsActivitiesTimersUpdatesTracingWorkflow, ) -# Passing through because Python 3.9 has an import bug at -# https://github.com/python/cpython/issues/91351 -with workflow.unsafe.imports_passed_through(): - import pytest - @activity.defn async def say_hello(name: str) -> str: @@ -84,6 +82,24 @@ def waiting(self) -> bool: return self._waiting +_WorkflowLogicFlag = temporalio.worker._workflow_instance._WorkflowLogicFlag +_SINGLE_BATCH_WORKFLOW_LOGIC_FLAG = ( + _WorkflowLogicFlag.PROCESS_WORKFLOW_ACTIVATION_JOBS_AS_SINGLE_BATCH +) + + +def _history_uses_workflow_logic_flag( + history: WorkflowHistory, + flag: _WorkflowLogicFlag, +) -> bool: + return any( + event.HasField("workflow_task_completed_event_attributes") + and int(flag) + in event.workflow_task_completed_event_attributes.sdk_metadata.lang_used_flags + for event in history.events + ) + + @pytest.mark.skipif(sys.version_info < (3, 12), reason="Skipping for < 3.12") async def test_replayer_workflow_complete(client: Client) -> None: # This test skips for versions < 3.12 because this is flaky due to CPython reimport issue: @@ -286,6 +302,7 @@ async def test_replayer_workflow_not_registered(client: Client) -> None: assert "SayHelloWorkflow is not registered" in str(err.value) +@pytest.mark.requires_local_server async def test_replayer_multiple_from_client( client: Client, env: WorkflowEnvironment ) -> None: @@ -296,7 +313,7 @@ async def test_replayer_multiple_from_client( # workflow ID so we can query it using standard visibility. workflow_id = f"workflow-{uuid.uuid4()}" async with new_say_hello_worker(client) as worker: - expected_runs_and_non_det: Dict[str, bool] = {} + expected_runs_and_non_det: dict[str, bool] = {} for i in range(5): should_cause_nondeterminism = i == 1 or i == 3 handle = await client.start_workflow( @@ -314,6 +331,14 @@ async def test_replayer_multiple_from_client( ) await handle.result() + async def visible_run_ids() -> set[str]: + return { + workflow.run_id + async for workflow in client.list_workflows(f"WorkflowId = '{workflow_id}'") + } + + await assert_eq_eventually(set(expected_runs_and_non_det), visible_run_ids) + # Run replayer with list iterator mapped to histories and collect results async with Replayer(workflows=[SayHelloWorkflow]).workflow_replay_iterator( client.list_workflows(f"WorkflowId = '{workflow_id}'").map_histories() @@ -418,7 +443,7 @@ async def test_replayer_command_reordering_backward_compatibility() -> None: class WorkerWorkflowResultInterceptor(Interceptor): def workflow_interceptor_class( self, input: WorkflowInterceptorClassInput - ) -> Optional[Type[WorkflowInboundInterceptor]]: + ) -> type[WorkflowInboundInterceptor] | None: return WorkflowResultInterceptor @@ -430,15 +455,12 @@ async def execute_workflow(self, input: ExecuteWorkflowInput) -> Any: return res -async def test_replayer_async_ordering() -> None: - """ - This test verifies that the order that asyncio tasks/coroutines are woken up matches the - order they were before changes to apply all jobs and then run the event loop, where previously - the event loop was ran after each "batch" of jobs. - """ - histories_and_expecteds = [ - ( +@pytest.mark.parametrize( + ("history_filename", "uses_single_batch", "expected"), + [ + pytest.param( "test_replayer_event_tracing.json", + False, [ "sig-before-sync", "sig-before-1", @@ -456,9 +478,33 @@ async def test_replayer_async_ordering() -> None: "timer-1", "timer-2", ], + id="legacy-event-tracing", ), - ( + pytest.param( + "test_replayer_event_tracing_single_batch.json", + True, + [ + "sig-before-sync", + "sig-before-1", + "timer-sync", + "act-sync", + "sig-before-2", + "act-1", + "act-2", + "sig-1-sync", + "sig-1-1", + "sig-1-2", + "update-1-sync", + "update-1-1", + "update-1-2", + "timer-1", + "timer-2", + ], + id="single-batch-event-tracing", + ), + pytest.param( "test_replayer_event_tracing_double_sig_at_start.json", + False, [ "sig-before-sync", "sig-before-1", @@ -476,29 +522,72 @@ async def test_replayer_async_ordering() -> None: "timer-1", "timer-2", ], + id="legacy-double-signal-at-start", ), - ] - for history, expected in histories_and_expecteds: - with Path(__file__).with_name(history).open() as f: - history = f.read() - await Replayer( - workflows=[SignalsActivitiesTimersUpdatesTracingWorkflow], - interceptors=[WorkerWorkflowResultInterceptor()], - ).replay_workflow(WorkflowHistory.from_json("fake", history)) - assert test_replayer_workflow_res == expected + pytest.param( + "test_replayer_event_tracing_double_sig_at_start_single_batch.json", + True, + [ + "sig-before-sync", + "sig-before-1", + "sig-1-sync", + "sig-1-1", + "timer-sync", + "act-sync", + "sig-before-2", + "sig-1-2", + "act-1", + "act-2", + "update-1-sync", + "update-1-1", + "update-1-2", + "timer-1", + "timer-2", + ], + id="single-batch-double-signal-at-start", + ), + ], +) +async def test_replayer_async_ordering( + history_filename: str, + uses_single_batch: bool, + expected: list[str], +) -> None: + """ + Verify legacy and single-batch histories replay with the asyncio scheduling order that their + original executions observed. + """ + with Path(__file__).with_name(history_filename).open() as f: + history = WorkflowHistory.from_json("fake", f.read()) + assert ( + _history_uses_workflow_logic_flag(history, _SINGLE_BATCH_WORKFLOW_LOGIC_FLAG) + is uses_single_batch + ) + await Replayer( + workflows=[SignalsActivitiesTimersUpdatesTracingWorkflow], + interceptors=[WorkerWorkflowResultInterceptor()], + ).replay_workflow(history) + assert test_replayer_workflow_res == expected -async def test_replayer_alternate_async_ordering() -> None: +async def test_replayer_unflagged_history_uses_legacy_async_ordering() -> None: with ( Path(__file__) .with_name("test_replayer_event_tracing_alternate.json") .open() as f ): - history = f.read() - await Replayer( + history = WorkflowHistory.from_json("fake", f.read()) + assert not _history_uses_workflow_logic_flag( + history, _SINGLE_BATCH_WORKFLOW_LOGIC_FLAG + ) + replayer = Replayer( workflows=[ActivityAndSignalsWhileWorkflowDown], interceptors=[WorkerWorkflowResultInterceptor()], - ).replay_workflow(WorkflowHistory.from_json("fake", history)) + ) + replayer._set_default_workflow_logic_flag( + _SINGLE_BATCH_WORKFLOW_LOGIC_FLAG, enabled=True + ) + await replayer.replay_workflow(history) assert test_replayer_workflow_res == [ "act-start", "sig-1", diff --git a/tests/worker/test_replayer_event_tracing_double_sig_at_start_single_batch.json b/tests/worker/test_replayer_event_tracing_double_sig_at_start_single_batch.json new file mode 100644 index 000000000..ed0eae143 --- /dev/null +++ b/tests/worker/test_replayer_event_tracing_double_sig_at_start_single_batch.json @@ -0,0 +1,433 @@ +{ + "events": [ + { + "eventId": "1", + "eventTime": "2026-07-22T17:46:09.820508351Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED", + "taskId": "1048655", + "workflowExecutionStartedEventAttributes": { + "workflowType": { + "name": "SignalsActivitiesTimersUpdatesTracingWorkflow" + }, + "taskQueue": { + "name": "tq-104f9b8b-4835-4705-8f36-3fb3f0b9bce7", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "workflowTaskTimeout": "10s", + "originalExecutionRunId": "019f8aef-5a1c-77c0-8c6f-4c42b8984e71", + "identity": "2468547@monolith", + "firstExecutionRunId": "019f8aef-5a1c-77c0-8c6f-4c42b8984e71", + "attempt": 1, + "firstWorkflowTaskBackoff": "0s", + "workflowId": "wf-50105b28-aad3-488b-b8b5-6cd3c168369c", + "priority": {} + } + }, + { + "eventId": "2", + "eventTime": "2026-07-22T17:46:09.820553958Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048656", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "tq-104f9b8b-4835-4705-8f36-3fb3f0b9bce7", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "3", + "eventTime": "2026-07-22T17:46:09.823263829Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED", + "taskId": "1048661", + "workflowExecutionSignaledEventAttributes": { + "signalName": "dosig", + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "ImJlZm9yZSI=" + } + ] + }, + "identity": "2468547@monolith", + "requestId": "0f1accfe-ffda-4af1-bc26-2a4b43007ef5" + } + }, + { + "eventId": "4", + "eventTime": "2026-07-22T17:46:09.826228197Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED", + "taskId": "1048663", + "workflowExecutionSignaledEventAttributes": { + "signalName": "dosig", + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IjEi" + } + ] + }, + "identity": "2468547@monolith", + "requestId": "7c76e2c6-c5c9-4bfa-9ef8-680e5fa0fdac" + } + }, + { + "eventId": "5", + "eventTime": "2026-07-22T17:46:09.911987108Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048665", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "2", + "identity": "2468547@monolith", + "requestId": "eb18fba3-0020-4bee-9824-68dee535759c", + "historySizeBytes": "600", + "workerVersion": { + "buildId": "d7dd3d4f7dae90ccfe98229c9f116c9a" + } + } + }, + { + "eventId": "6", + "eventTime": "2026-07-22T17:46:10.029696095Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048669", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "2", + "startedEventId": "5", + "identity": "2468547@monolith", + "workerVersion": { + "buildId": "d7dd3d4f7dae90ccfe98229c9f116c9a" + }, + "sdkMetadata": { + "coreUsedFlags": [ + 2, + 3, + 1 + ], + "langUsedFlags": [ + 2 + ], + "sdkName": "temporal-python", + "sdkVersion": "1.30.0" + }, + "meteringMetadata": {} + } + }, + { + "eventId": "7", + "eventTime": "2026-07-22T17:46:10.029723573Z", + "eventType": "EVENT_TYPE_TIMER_STARTED", + "taskId": "1048670", + "timerStartedEventAttributes": { + "timerId": "1", + "startToFireTimeout": "0.100s", + "workflowTaskCompletedEventId": "6" + } + }, + { + "eventId": "8", + "eventTime": "2026-07-22T17:46:10.029791694Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", + "taskId": "1048671", + "activityTaskScheduledEventAttributes": { + "activityId": "1", + "activityType": { + "name": "say_hello" + }, + "taskQueue": { + "name": "tq-104f9b8b-4835-4705-8f36-3fb3f0b9bce7", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "header": {}, + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IkVuY2hpIg==" + } + ] + }, + "scheduleToCloseTimeout": "30s", + "scheduleToStartTimeout": "30s", + "startToCloseTimeout": "30s", + "heartbeatTimeout": "0s", + "workflowTaskCompletedEventId": "6", + "retryPolicy": { + "initialInterval": "1s", + "backoffCoefficient": 2.0, + "maximumInterval": "100s" + }, + "useWorkflowBuildId": true, + "priority": {} + } + }, + { + "eventId": "9", + "eventTime": "2026-07-22T17:46:10.031520468Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", + "taskId": "1048679", + "activityTaskStartedEventAttributes": { + "scheduledEventId": "8", + "identity": "2468547@monolith", + "requestId": "58a7bab7-a808-49ba-b906-3c07a281f078", + "attempt": 1, + "workerVersion": { + "buildId": "d7dd3d4f7dae90ccfe98229c9f116c9a" + } + } + }, + { + "eventId": "10", + "eventTime": "2026-07-22T17:46:10.034867651Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", + "taskId": "1048680", + "activityTaskCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IkhlbGxvLCBFbmNoaSEi" + } + ] + }, + "scheduledEventId": "8", + "startedEventId": "9", + "identity": "2468547@monolith" + } + }, + { + "eventId": "11", + "eventTime": "2026-07-22T17:46:10.034876231Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048681", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "2468547@monolith-af9e32c231d44027a7fbb997838520bf", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "tq-104f9b8b-4835-4705-8f36-3fb3f0b9bce7" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "12", + "eventTime": "2026-07-22T17:46:10.035716599Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048685", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "11", + "identity": "2468547@monolith", + "requestId": "cf2bd6a2-3498-427d-920a-daf350415c70", + "historySizeBytes": "1389", + "workerVersion": { + "buildId": "d7dd3d4f7dae90ccfe98229c9f116c9a" + } + } + }, + { + "eventId": "13", + "eventTime": "2026-07-22T17:46:10.039414094Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048689", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "11", + "startedEventId": "12", + "identity": "2468547@monolith", + "workerVersion": { + "buildId": "d7dd3d4f7dae90ccfe98229c9f116c9a" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "14", + "eventTime": "2026-07-22T17:46:10.112263987Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048695", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "2468547@monolith-af9e32c231d44027a7fbb997838520bf", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "tq-104f9b8b-4835-4705-8f36-3fb3f0b9bce7" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "15", + "eventTime": "2026-07-22T17:46:10.112924510Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048696", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "14", + "identity": "2468547@monolith", + "requestId": "6e079d38-0b99-48eb-b1e9-1b8236232fb1", + "historySizeBytes": "1598", + "workerVersion": { + "buildId": "d7dd3d4f7dae90ccfe98229c9f116c9a" + } + } + }, + { + "eventId": "16", + "eventTime": "2026-07-22T17:46:10.118047317Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048697", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "14", + "startedEventId": "15", + "identity": "2468547@monolith", + "workerVersion": { + "buildId": "d7dd3d4f7dae90ccfe98229c9f116c9a" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "17", + "eventTime": "2026-07-22T17:46:10.118078114Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_ACCEPTED", + "taskId": "1048698", + "workflowExecutionUpdateAcceptedEventAttributes": { + "protocolInstanceId": "3078b22e-fa4a-4bb3-bf21-7502464f3f8e", + "acceptedRequestMessageId": "3078b22e-fa4a-4bb3-bf21-7502464f3f8e/request", + "acceptedRequestSequencingEventId": "14", + "acceptedRequest": { + "meta": { + "updateId": "3078b22e-fa4a-4bb3-bf21-7502464f3f8e", + "identity": "2468547@monolith" + }, + "input": { + "name": "doupdate", + "args": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IjEi" + } + ] + } + } + } + } + }, + { + "eventId": "18", + "eventTime": "2026-07-22T17:46:10.118094048Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_COMPLETED", + "taskId": "1048699", + "workflowExecutionUpdateCompletedEventAttributes": { + "meta": { + "updateId": "3078b22e-fa4a-4bb3-bf21-7502464f3f8e", + "identity": "2468547@monolith" + }, + "outcome": { + "success": { + "payloads": [ + { + "metadata": { + "encoding": "YmluYXJ5L251bGw=" + } + } + ] + } + }, + "acceptedEventId": "17" + } + }, + { + "eventId": "19", + "eventTime": "2026-07-22T17:46:10.822704848Z", + "eventType": "EVENT_TYPE_TIMER_FIRED", + "taskId": "1048702", + "timerFiredEventAttributes": { + "timerId": "1", + "startedEventId": "7" + } + }, + { + "eventId": "20", + "eventTime": "2026-07-22T17:46:10.822723284Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048703", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "2468547@monolith-af9e32c231d44027a7fbb997838520bf", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "tq-104f9b8b-4835-4705-8f36-3fb3f0b9bce7" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "21", + "eventTime": "2026-07-22T17:46:10.827860954Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048707", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "20", + "identity": "2468547@monolith", + "requestId": "facf828f-060f-4a8b-87cf-bdbb3a3297e9", + "historySizeBytes": "2430", + "workerVersion": { + "buildId": "d7dd3d4f7dae90ccfe98229c9f116c9a" + } + } + }, + { + "eventId": "22", + "eventTime": "2026-07-22T17:46:10.843601707Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048711", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "20", + "startedEventId": "21", + "identity": "2468547@monolith", + "workerVersion": { + "buildId": "d7dd3d4f7dae90ccfe98229c9f116c9a" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "23", + "eventTime": "2026-07-22T17:46:10.843661263Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED", + "taskId": "1048712", + "workflowExecutionCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "WyJzaWctYmVmb3JlLXN5bmMiLCJzaWctYmVmb3JlLTEiLCJzaWctMS1zeW5jIiwic2lnLTEtMSIsInRpbWVyLXN5bmMiLCJhY3Qtc3luYyIsInNpZy1iZWZvcmUtMiIsInNpZy0xLTIiLCJhY3QtMSIsImFjdC0yIiwidXBkYXRlLTEtc3luYyIsInVwZGF0ZS0xLTEiLCJ1cGRhdGUtMS0yIiwidGltZXItMSIsInRpbWVyLTIiXQ==" + } + ] + }, + "workflowTaskCompletedEventId": "22" + } + } + ] +} + diff --git a/tests/worker/test_replayer_event_tracing_single_batch.json b/tests/worker/test_replayer_event_tracing_single_batch.json new file mode 100644 index 000000000..42bab0b4f --- /dev/null +++ b/tests/worker/test_replayer_event_tracing_single_batch.json @@ -0,0 +1,479 @@ +{ + "events": [ + { + "eventId": "1", + "eventTime": "2026-07-22T17:46:08.798556582Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED", + "taskId": "1048587", + "workflowExecutionStartedEventAttributes": { + "workflowType": { + "name": "SignalsActivitiesTimersUpdatesTracingWorkflow" + }, + "taskQueue": { + "name": "tq-7049fd3e-356e-49d5-9047-2b97b454562e", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "workflowTaskTimeout": "10s", + "originalExecutionRunId": "019f8aef-561e-787b-8565-31e78b4a889a", + "identity": "2468547@monolith", + "firstExecutionRunId": "019f8aef-561e-787b-8565-31e78b4a889a", + "attempt": 1, + "firstWorkflowTaskBackoff": "0s", + "workflowId": "wf-2dd53446-a657-4f47-9e47-a3edeb302c5f", + "priority": {} + } + }, + { + "eventId": "2", + "eventTime": "2026-07-22T17:46:08.798633092Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048588", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "tq-7049fd3e-356e-49d5-9047-2b97b454562e", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "3", + "eventTime": "2026-07-22T17:46:08.801258084Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED", + "taskId": "1048593", + "workflowExecutionSignaledEventAttributes": { + "signalName": "dosig", + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "ImJlZm9yZSI=" + } + ] + }, + "identity": "2468547@monolith", + "requestId": "c78a6332-adb5-4ff1-af4b-f55e1180d4d8" + } + }, + { + "eventId": "4", + "eventTime": "2026-07-22T17:46:08.949831498Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048595", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "2", + "identity": "2468547@monolith", + "requestId": "1e30e279-6cc7-497a-89f5-3e032d169c49", + "historySizeBytes": "477", + "workerVersion": { + "buildId": "d7dd3d4f7dae90ccfe98229c9f116c9a" + } + } + }, + { + "eventId": "5", + "eventTime": "2026-07-22T17:46:09.032870384Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048599", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "2", + "startedEventId": "4", + "identity": "2468547@monolith", + "workerVersion": { + "buildId": "d7dd3d4f7dae90ccfe98229c9f116c9a" + }, + "sdkMetadata": { + "coreUsedFlags": [ + 3, + 1, + 2 + ], + "langUsedFlags": [ + 2 + ], + "sdkName": "temporal-python", + "sdkVersion": "1.30.0" + }, + "meteringMetadata": {} + } + }, + { + "eventId": "6", + "eventTime": "2026-07-22T17:46:09.032946093Z", + "eventType": "EVENT_TYPE_TIMER_STARTED", + "taskId": "1048600", + "timerStartedEventAttributes": { + "timerId": "1", + "startToFireTimeout": "0.100s", + "workflowTaskCompletedEventId": "5" + } + }, + { + "eventId": "7", + "eventTime": "2026-07-22T17:46:09.033008651Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", + "taskId": "1048601", + "activityTaskScheduledEventAttributes": { + "activityId": "1", + "activityType": { + "name": "say_hello" + }, + "taskQueue": { + "name": "tq-7049fd3e-356e-49d5-9047-2b97b454562e", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "header": {}, + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IkVuY2hpIg==" + } + ] + }, + "scheduleToCloseTimeout": "30s", + "scheduleToStartTimeout": "30s", + "startToCloseTimeout": "30s", + "heartbeatTimeout": "0s", + "workflowTaskCompletedEventId": "5", + "retryPolicy": { + "initialInterval": "1s", + "backoffCoefficient": 2.0, + "maximumInterval": "100s" + }, + "useWorkflowBuildId": true, + "priority": {} + } + }, + { + "eventId": "8", + "eventTime": "2026-07-22T17:46:09.035163378Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", + "taskId": "1048609", + "activityTaskStartedEventAttributes": { + "scheduledEventId": "7", + "identity": "2468547@monolith", + "requestId": "bd46ae91-1b79-4c8b-9366-e24395d0da8b", + "attempt": 1, + "workerVersion": { + "buildId": "d7dd3d4f7dae90ccfe98229c9f116c9a" + } + } + }, + { + "eventId": "9", + "eventTime": "2026-07-22T17:46:09.039131108Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", + "taskId": "1048610", + "activityTaskCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IkhlbGxvLCBFbmNoaSEi" + } + ] + }, + "scheduledEventId": "7", + "startedEventId": "8", + "identity": "2468547@monolith" + } + }, + { + "eventId": "10", + "eventTime": "2026-07-22T17:46:09.039143351Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048611", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "2468547@monolith-3a1c4b9f82644345a21e1663c393c0d0", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "tq-7049fd3e-356e-49d5-9047-2b97b454562e" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "11", + "eventTime": "2026-07-22T17:46:09.040109929Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048615", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "10", + "identity": "2468547@monolith", + "requestId": "d3d3c3c3-5c9f-4570-8d8b-cce1f5856a4f", + "historySizeBytes": "1266", + "workerVersion": { + "buildId": "d7dd3d4f7dae90ccfe98229c9f116c9a" + } + } + }, + { + "eventId": "12", + "eventTime": "2026-07-22T17:46:09.043328426Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048619", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "10", + "startedEventId": "11", + "identity": "2468547@monolith", + "workerVersion": { + "buildId": "d7dd3d4f7dae90ccfe98229c9f116c9a" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "13", + "eventTime": "2026-07-22T17:46:09.146751311Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED", + "taskId": "1048621", + "workflowExecutionSignaledEventAttributes": { + "signalName": "dosig", + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IjEi" + } + ] + }, + "identity": "2468547@monolith", + "requestId": "f18a65f6-8773-425d-b8f6-d1099056f758" + } + }, + { + "eventId": "14", + "eventTime": "2026-07-22T17:46:09.146757799Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048622", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "2468547@monolith-3a1c4b9f82644345a21e1663c393c0d0", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "tq-7049fd3e-356e-49d5-9047-2b97b454562e" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "15", + "eventTime": "2026-07-22T17:46:09.147668735Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048626", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "14", + "identity": "2468547@monolith", + "requestId": "7f9aa9f0-4bef-44a3-93f4-744ff1db8eb4", + "historySizeBytes": "1724", + "workerVersion": { + "buildId": "d7dd3d4f7dae90ccfe98229c9f116c9a" + } + } + }, + { + "eventId": "16", + "eventTime": "2026-07-22T17:46:09.151221140Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048630", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "14", + "startedEventId": "15", + "identity": "2468547@monolith", + "workerVersion": { + "buildId": "d7dd3d4f7dae90ccfe98229c9f116c9a" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "17", + "eventTime": "2026-07-22T17:46:09.151638714Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048633", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "2468547@monolith-3a1c4b9f82644345a21e1663c393c0d0", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "tq-7049fd3e-356e-49d5-9047-2b97b454562e" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "18", + "eventTime": "2026-07-22T17:46:09.151645790Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048634", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "17", + "identity": "2468547@monolith", + "requestId": "request-from-RespondWorkflowTaskCompleted", + "historySizeBytes": "1933", + "workerVersion": { + "buildId": "d7dd3d4f7dae90ccfe98229c9f116c9a" + } + } + }, + { + "eventId": "19", + "eventTime": "2026-07-22T17:46:09.155070433Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048635", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "17", + "startedEventId": "18", + "identity": "2468547@monolith", + "workerVersion": { + "buildId": "d7dd3d4f7dae90ccfe98229c9f116c9a" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "20", + "eventTime": "2026-07-22T17:46:09.155126964Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_ACCEPTED", + "taskId": "1048636", + "workflowExecutionUpdateAcceptedEventAttributes": { + "protocolInstanceId": "dd163639-396a-440a-b950-00ac2f2ee490", + "acceptedRequestMessageId": "dd163639-396a-440a-b950-00ac2f2ee490/request", + "acceptedRequestSequencingEventId": "17", + "acceptedRequest": { + "meta": { + "updateId": "dd163639-396a-440a-b950-00ac2f2ee490", + "identity": "2468547@monolith" + }, + "input": { + "name": "doupdate", + "args": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IjEi" + } + ] + } + } + } + } + }, + { + "eventId": "21", + "eventTime": "2026-07-22T17:46:09.155192377Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_COMPLETED", + "taskId": "1048637", + "workflowExecutionUpdateCompletedEventAttributes": { + "meta": { + "updateId": "dd163639-396a-440a-b950-00ac2f2ee490", + "identity": "2468547@monolith" + }, + "outcome": { + "success": { + "payloads": [ + { + "metadata": { + "encoding": "YmluYXJ5L251bGw=" + } + } + ] + } + }, + "acceptedEventId": "20" + } + }, + { + "eventId": "22", + "eventTime": "2026-07-22T17:46:09.801318803Z", + "eventType": "EVENT_TYPE_TIMER_FIRED", + "taskId": "1048640", + "timerFiredEventAttributes": { + "timerId": "1", + "startedEventId": "6" + } + }, + { + "eventId": "23", + "eventTime": "2026-07-22T17:46:09.801325729Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048641", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "2468547@monolith-3a1c4b9f82644345a21e1663c393c0d0", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "tq-7049fd3e-356e-49d5-9047-2b97b454562e" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "24", + "eventTime": "2026-07-22T17:46:09.802775220Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048645", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "23", + "identity": "2468547@monolith", + "requestId": "3d1c5327-09f4-4434-8880-f181a287c207", + "historySizeBytes": "2770", + "workerVersion": { + "buildId": "d7dd3d4f7dae90ccfe98229c9f116c9a" + } + } + }, + { + "eventId": "25", + "eventTime": "2026-07-22T17:46:09.808121470Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048649", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "23", + "startedEventId": "24", + "identity": "2468547@monolith", + "workerVersion": { + "buildId": "d7dd3d4f7dae90ccfe98229c9f116c9a" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "26", + "eventTime": "2026-07-22T17:46:09.808174063Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED", + "taskId": "1048650", + "workflowExecutionCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "WyJzaWctYmVmb3JlLXN5bmMiLCJzaWctYmVmb3JlLTEiLCJ0aW1lci1zeW5jIiwiYWN0LXN5bmMiLCJzaWctYmVmb3JlLTIiLCJhY3QtMSIsImFjdC0yIiwic2lnLTEtc3luYyIsInNpZy0xLTEiLCJzaWctMS0yIiwidXBkYXRlLTEtc3luYyIsInVwZGF0ZS0xLTEiLCJ1cGRhdGUtMS0yIiwidGltZXItMSIsInRpbWVyLTIiXQ==" + } + ] + }, + "workflowTaskCompletedEventId": "25" + } + } + ] +} + diff --git a/tests/worker/test_update_with_start.py b/tests/worker/test_update_with_start.py index c6a11d852..0b3368725 100644 --- a/tests/worker/test_update_with_start.py +++ b/tests/worker/test_update_with_start.py @@ -2,12 +2,15 @@ import asyncio import uuid +from collections.abc import Mapping from dataclasses import dataclass from datetime import timedelta from enum import Enum, IntEnum -from typing import Any, Mapping, Optional +from typing import Any from unittest.mock import patch +import pytest + import temporalio.api.common.v1 import temporalio.api.workflowservice.v1 from temporalio import activity, workflow @@ -27,17 +30,12 @@ WorkflowIDReusePolicy, ) from temporalio.exceptions import ApplicationError, WorkflowAlreadyStartedError -from temporalio.service import RPCError, RPCStatusCode, ServiceCall +from temporalio.service import RPCError, RPCStatusCode from temporalio.testing import WorkflowEnvironment from tests.helpers import ( new_worker, ) -# Passing through because Python 3.9 has an import bug at -# https://github.com/python/cpython/issues/91351 -with workflow.unsafe.imports_passed_through(): - import pytest - @activity.defn async def activity_called_by_update() -> None: @@ -194,6 +192,7 @@ async def _do_test( id_conflict_policy: WorkflowIDConflictPolicy, expect_error_when_workflow_exists: ExpectErrorWhenWorkflowExists, ): + workflow_id = f"{workflow_id}-{uuid.uuid4()}" await self._do_execute_update_test( client, workflow_id + "-execute-update", @@ -338,6 +337,7 @@ async def test_update_with_start_sets_first_execution_run_id( WorkflowForUpdateWithStartTest, activities=[activity_called_by_update], ) as worker: + workflow_id_prefix = f"wid-{uuid.uuid4()}" def make_start_op(workflow_id: str): return WithStartWorkflowOperation( @@ -350,7 +350,7 @@ def make_start_op(workflow_id: str): # conflict policy is FAIL # First UWS succeeds and sets the first execution run ID - start_op_1 = make_start_op("wid-1") + start_op_1 = make_start_op(f"{workflow_id_prefix}-1") update_handle_1 = await client.start_update_with_start_workflow( WorkflowForUpdateWithStartTest.my_non_blocking_update, "1", @@ -362,7 +362,7 @@ def make_start_op(workflow_id: str): # Second UWS start fails because the workflow already exists # first execution run ID is not set on the second UWS handle - start_op_2 = make_start_op("wid-1") + start_op_2 = make_start_op(f"{workflow_id_prefix}-1") for aw in [ client.start_update_with_start_workflow( @@ -377,7 +377,7 @@ def make_start_op(workflow_id: str): await aw # Third UWS start succeeds, but the update fails after acceptance - start_op_3 = make_start_op("wid-2") + start_op_3 = make_start_op(f"{workflow_id_prefix}-2") update_handle_3 = await client.start_update_with_start_workflow( WorkflowForUpdateWithStartTest.my_non_blocking_update, "fail-after-acceptance", @@ -396,7 +396,7 @@ def make_start_op(workflow_id: str): assert await wf_handle_3.result() == "workflow-result-0" # Fourth UWS is same as third, but we use execute_update instead of start_update. - start_op_4 = make_start_op("wid-3") + start_op_4 = make_start_op(f"{workflow_id_prefix}-3") with pytest.raises(WorkflowUpdateFailedError): await client.execute_update_with_start_workflow( WorkflowForUpdateWithStartTest.my_non_blocking_update, @@ -804,12 +804,7 @@ async def test_update_with_start_two_param(client: Client): # Verify correcting issue #791 async def test_start_update_with_start_empty_details(client: Client): - class execute_multi_operation( - ServiceCall[ - temporalio.api.workflowservice.v1.ExecuteMultiOperationRequest, - temporalio.api.workflowservice.v1.ExecuteMultiOperationResponse, - ] - ): + class execute_multi_operation: empty_details_err = RPCError("empty details", RPCStatusCode.INTERNAL, b"") # Set grpc_status with empty details empty_details_err._grpc_status = temporalio.api.common.v1.GrpcStatus(details=[]) @@ -822,8 +817,8 @@ async def __call__( req: temporalio.api.workflowservice.v1.ExecuteMultiOperationRequest, *, retry: bool = False, - metadata: Mapping[str, str] = {}, - timeout: Optional[timedelta] = None, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, ) -> temporalio.api.workflowservice.v1.ExecuteMultiOperationResponse: raise self.empty_details_err @@ -1111,3 +1106,84 @@ async def _do_update() -> Any: elif id_reuse_policy == WorkflowIDReusePolicy.REJECT_DUPLICATE: with pytest.raises(WorkflowAlreadyStartedError): await _do_update() + + +class MetadataCapturingInterceptor(Interceptor): + """Interceptor that sets rpc_metadata on update-with-start calls.""" + + def intercept_client(self, next: OutboundInterceptor) -> OutboundInterceptor: + return MetadataCapturingOutboundInterceptor(super().intercept_client(next)) + + +class MetadataCapturingOutboundInterceptor(OutboundInterceptor): + def __init__(self, next: OutboundInterceptor) -> None: + super().__init__(next) + + async def start_update_with_start_workflow( + self, input: StartWorkflowUpdateWithStartInput + ) -> WorkflowUpdateHandle[Any]: + input.rpc_metadata = { + **input.rpc_metadata, + "test-header-key": "test-header-value", + } + return await super().start_update_with_start_workflow(input) + + +# Verify fix for https://github.com/temporalio/sdk-python/issues/1582 +async def test_update_with_start_rpc_metadata_and_timeout_forwarded(client: Client): + """Test that rpc_metadata and rpc_timeout on StartWorkflowUpdateWithStartInput + are forwarded to the execute_multi_operation gRPC call.""" + captured_metadata: dict[str, str | bytes] = {} + captured_timeout: list[timedelta | None] = [] + + class execute_multi_operation: + err = RPCError("intentional", RPCStatusCode.INTERNAL, b"") + err._grpc_status = temporalio.api.common.v1.GrpcStatus(details=[]) + + def __init__(self) -> None: # type: ignore[reportMissingSuperCall] + pass + + async def __call__( + self, + req: temporalio.api.workflowservice.v1.ExecuteMultiOperationRequest, + *, + retry: bool = False, + metadata: Mapping[str, str | bytes] = {}, + timeout: timedelta | None = None, + ) -> temporalio.api.workflowservice.v1.ExecuteMultiOperationResponse: + captured_metadata.update(metadata) + captured_timeout.append(timeout) + raise self.err + + interceptor = MetadataCapturingInterceptor() + intercepted_client = Client( + **{**client.config(), "interceptors": [interceptor]} # type: ignore + ) + + with patch.object( + intercepted_client.workflow_service, + "execute_multi_operation", + execute_multi_operation(), + ): + start_workflow_operation = WithStartWorkflowOperation( + UpdateWithStartInterceptorWorkflow.run, + "wf-arg", + id=f"wf-{uuid.uuid4()}", + task_queue="tq", + id_conflict_policy=WorkflowIDConflictPolicy.FAIL, + ) + with pytest.raises(RPCError): + await intercepted_client.start_update_with_start_workflow( + UpdateWithStartInterceptorWorkflow.my_update, + "update-arg", + start_workflow_operation=start_workflow_operation, + wait_for_stage=WorkflowUpdateStage.ACCEPTED, + rpc_metadata={"original-key": "original-value"}, + rpc_timeout=timedelta(seconds=42), + ) + + # The interceptor should have added its metadata on top of the caller's + assert captured_metadata.get("test-header-key") == "test-header-value" + assert captured_metadata.get("original-key") == "original-value" + # The caller's timeout should have been forwarded + assert captured_timeout == [timedelta(seconds=42)] diff --git a/tests/worker/test_visitor.py b/tests/worker/test_visitor.py new file mode 100644 index 000000000..d9606624e --- /dev/null +++ b/tests/worker/test_visitor.py @@ -0,0 +1,490 @@ +import asyncio +import dataclasses +import time +from collections.abc import MutableSequence + +import pytest +from google.protobuf.duration_pb2 import Duration + +import temporalio.api.workflowservice.v1.request_response_pb2 as workflowservice_pb2 +import temporalio.bridge.worker +import temporalio.converter +import temporalio.nexus.system as nexus_system +from temporalio.api.common.v1.message_pb2 import ( + Payload, + Payloads, + Priority, + SearchAttributes, +) +from temporalio.api.sdk.v1.user_metadata_pb2 import UserMetadata +from temporalio.bridge._visitor import PayloadVisitor +from temporalio.bridge._visitor_functions import VisitorFunctions +from temporalio.bridge.proto.workflow_activation.workflow_activation_pb2 import ( + InitializeWorkflow, + WorkflowActivation, + WorkflowActivationJob, +) +from temporalio.bridge.proto.workflow_commands.workflow_commands_pb2 import ( + ContinueAsNewWorkflowExecution, + ScheduleActivity, + ScheduleLocalActivity, + ScheduleNexusOperation, + SignalExternalWorkflowExecution, + StartChildWorkflowExecution, + UpdateResponse, + WorkflowCommand, +) +from temporalio.bridge.proto.workflow_completion.workflow_completion_pb2 import ( + Success, + WorkflowActivationCompletion, +) +from tests.worker.test_workflow import SimpleCodec + + +class Visitor(VisitorFunctions): + async def visit_payload(self, payload: Payload) -> None: + payload.metadata["visited"] = b"True" + + async def visit_payloads(self, payloads: MutableSequence[Payload]) -> None: + for payload in payloads: + payload.metadata["visited"] = b"True" + + +async def test_workflow_activation_completion(): + comp = WorkflowActivationCompletion( + run_id="1", + successful=Success( + commands=[ + WorkflowCommand( + schedule_activity=ScheduleActivity( + seq=1, + activity_id="1", + activity_type="", + task_queue="", + headers={"foo": Payload(data=b"bar")}, + arguments=[Payload(data=b"baz")], + schedule_to_close_timeout=Duration(seconds=5), + priority=Priority(), + ), + user_metadata=UserMetadata(summary=Payload(data=b"Summary")), + ) + ], + ), + ) + + await PayloadVisitor().visit(Visitor(), comp) + + cmd = comp.successful.commands[0] + sa = cmd.schedule_activity + assert sa.headers["foo"].metadata["visited"] + assert len(sa.arguments) == 1 and sa.arguments[0].metadata["visited"] + + assert cmd.user_metadata.summary.metadata["visited"] + + +async def test_workflow_activation(): + original = WorkflowActivation( + jobs=[ + WorkflowActivationJob( + initialize_workflow=InitializeWorkflow( + arguments=[ + Payload(data=b"repeated1"), + Payload(data=b"repeated2"), + ], + headers={"header": Payload(data=b"map")}, + last_completion_result=Payloads( + payloads=[ + Payload(data=b"obj1"), + Payload(data=b"obj2"), + ] + ), + search_attributes=SearchAttributes( + indexed_fields={ + "sakey": Payload(data=b"saobj"), + } + ), + ), + ) + ] + ) + + act = original.__deepcopy__() + await PayloadVisitor().visit(Visitor(), act) + assert act.jobs[0].initialize_workflow.arguments[0].metadata["visited"] + assert act.jobs[0].initialize_workflow.arguments[1].metadata["visited"] + assert act.jobs[0].initialize_workflow.headers["header"].metadata["visited"] + assert ( + act.jobs[0] + .initialize_workflow.last_completion_result.payloads[0] + .metadata["visited"] + ) + assert ( + act.jobs[0] + .initialize_workflow.last_completion_result.payloads[1] + .metadata["visited"] + ) + assert ( + act.jobs[0] + .initialize_workflow.search_attributes.indexed_fields["sakey"] + .metadata["visited"] + ) + + act = original.__deepcopy__() + await PayloadVisitor(skip_search_attributes=True).visit(Visitor(), act) + assert ( + not act.jobs[0] + .initialize_workflow.search_attributes.indexed_fields["sakey"] + .metadata["visited"] + ) + + act = original.__deepcopy__() + await PayloadVisitor(skip_headers=True).visit(Visitor(), act) + assert not act.jobs[0].initialize_workflow.headers["header"].metadata["visited"] + + +async def test_visit_payloads_on_other_commands(): + comp = WorkflowActivationCompletion( + run_id="2", + successful=Success( + commands=[ + # Continue as new + WorkflowCommand( + continue_as_new_workflow_execution=ContinueAsNewWorkflowExecution( + arguments=[Payload(data=b"a1")], + headers={"h1": Payload(data=b"a2")}, + memo={"m1": Payload(data=b"a3")}, + ) + ), + # Start child + WorkflowCommand( + start_child_workflow_execution=StartChildWorkflowExecution( + input=[Payload(data=b"b1")], + headers={"h2": Payload(data=b"b2")}, + memo={"m2": Payload(data=b"b3")}, + ) + ), + # Signal external + WorkflowCommand( + signal_external_workflow_execution=SignalExternalWorkflowExecution( + args=[Payload(data=b"c1")], + headers={"h3": Payload(data=b"c2")}, + ) + ), + # Schedule local activity + WorkflowCommand( + schedule_local_activity=ScheduleLocalActivity( + arguments=[Payload(data=b"d1")], + headers={"h4": Payload(data=b"d2")}, + ) + ), + # Update response completed + WorkflowCommand( + update_response=UpdateResponse( + completed=Payload(data=b"e1"), + ) + ), + ] + ), + ) + + await PayloadVisitor().visit(Visitor(), comp) + + cmds = comp.successful.commands + can = cmds[0].continue_as_new_workflow_execution + assert can.arguments[0].metadata["visited"] + assert can.headers["h1"].metadata["visited"] + assert can.memo["m1"].metadata["visited"] + + sc = cmds[1].start_child_workflow_execution + assert sc.input[0].metadata["visited"] + assert sc.headers["h2"].metadata["visited"] + assert sc.memo["m2"].metadata["visited"] + + se = cmds[2].signal_external_workflow_execution + assert se.args[0].metadata["visited"] + assert se.headers["h3"].metadata["visited"] + + sla = cmds[3].schedule_local_activity + assert sla.arguments[0].metadata["visited"] + assert sla.headers["h4"].metadata["visited"] + + ur = cmds[4].update_response + assert ur.completed.metadata["visited"] + + +async def test_system_nexus_envelope_is_detected_in_generic_payload_field(): + class SystemNexusVisitor(Visitor): + def __init__(self) -> None: + self.visited_payload_count = 0 + self.system_envelope_count = 0 + + async def visit_payload(self, payload: Payload) -> None: + self.visited_payload_count += 1 + await super().visit_payload(payload) + + async def visit_payloads(self, payloads: MutableSequence[Payload]) -> None: + self.visited_payload_count += len(payloads) + await super().visit_payloads(payloads) + + async def visit_system_nexus_envelope(self, payload: Payload) -> None: + _ = payload + self.system_envelope_count += 1 + + system_request = workflowservice_pb2.SignalWithStartWorkflowExecutionRequest( + input=Payloads(payloads=[Payload(data=b"workflow-input")]), + ) + payload_converter = nexus_system._get_payload_converter( + temporalio.converter.default().payload_converter + ) + system_payload = payload_converter.to_payload(system_request) + assert system_payload is not None + comp = WorkflowActivationCompletion( + run_id="3", + successful=Success( + commands=[ + WorkflowCommand( + update_response=UpdateResponse(completed=system_payload), + ) + ] + ), + ) + visitor = SystemNexusVisitor() + + await PayloadVisitor(concurrency_limit=3).visit(visitor, comp) + + completed = comp.successful.commands[0].update_response.completed + assert completed.metadata["__temporal_system_payload"] == b"true" + assert "visited" not in completed.metadata + decoded = payload_converter.from_payload(completed) + assert isinstance( + decoded, workflowservice_pb2.SignalWithStartWorkflowExecutionRequest + ) + assert decoded.input.payloads[0].metadata["visited"] == b"True" + assert visitor.visited_payload_count == 1 + assert visitor.system_envelope_count == 1 + + +async def test_concurrent_throughput(): + """Demonstrate that concurrent visitation is faster than serialized for I/O-bound codecs.""" + N_CMDS = 10 + N_ARGS = 5 + SLEEP = 0.02 + + class SlowVisitor(VisitorFunctions): + def __init__(self, *, blocking: bool = False): + self.visit_count = 0 + self._active = 0 + self.max_concurrent = 0 + self._blocking = blocking + + async def visit_payload(self, payload: Payload) -> None: + return await self._visit(1) + + async def visit_payloads(self, payloads: MutableSequence[Payload]) -> None: + return await self._visit(len(payloads)) + + async def _visit(self, count: int) -> None: + self._active += 1 + self.max_concurrent = max(self.max_concurrent, self._active) + try: + if self._blocking: + time.sleep(SLEEP * count) + else: + await asyncio.sleep(SLEEP * count) + self.visit_count += count + finally: + self._active -= 1 + + completion = WorkflowActivationCompletion( + run_id="1", + successful=Success( + commands=[ + WorkflowCommand( + schedule_activity=ScheduleActivity( + seq=i, + activity_id=str(i), + activity_type="", + task_queue="", + arguments=[ + Payload(data=f"cmd_{i}_arg_{j}".encode()) + for j in range(N_ARGS) + ], + priority=Priority(), + ) + ) + for i in range(N_CMDS) + ] + ), + ) + + visitor_default = SlowVisitor() + await PayloadVisitor().visit(visitor_default, completion) + + assert visitor_default.visit_count == N_CMDS * N_ARGS + assert visitor_default.max_concurrent == 1 + + visitor_concurrent = SlowVisitor() + await PayloadVisitor(concurrency_limit=5).visit(visitor_concurrent, completion) + + assert visitor_concurrent.visit_count == N_CMDS * N_ARGS + assert visitor_concurrent.max_concurrent == 5 + + +async def test_cancel_drains_background_tasks(): + """Cancelling visit() cancels in-flight tasks and awaits their cleanup.""" + tasks_started = 0 + tasks_cleaned_up = 0 + background_running = asyncio.Event() + + class SlowVisitor(VisitorFunctions): + async def visit_payload(self, payload: Payload) -> None: + pass + + async def visit_payloads(self, payloads: MutableSequence[Payload]) -> None: + nonlocal tasks_started, tasks_cleaned_up + tasks_started += 1 + background_running.set() + try: + await asyncio.sleep(10) + finally: + tasks_cleaned_up += 1 + + completion = WorkflowActivationCompletion( + run_id="1", + successful=Success( + commands=[ + WorkflowCommand( + schedule_activity=ScheduleActivity( + seq=i, + activity_id=str(i), + activity_type="", + task_queue="", + arguments=[Payload(data=f"arg_{i}".encode())], + priority=Priority(), + ) + ) + for i in range(5) + ] + ), + ) + + task = asyncio.create_task( + PayloadVisitor(concurrency_limit=5).visit(SlowVisitor(), completion) + ) + await background_running.wait() + task.cancel() + + with pytest.raises(asyncio.CancelledError): + await task + + # All started tasks ran their finally blocks before drain() returned. + assert tasks_started > 0 + assert tasks_cleaned_up == tasks_started + + +async def test_system_nexus_envelope_visit_is_bounded(): + active_visits = 0 + max_active_visits = 0 + two_visits_started = asyncio.Event() + release_visits = asyncio.Event() + + class SlowVisitor(VisitorFunctions): + async def visit_payload(self, payload: Payload) -> None: + await self._visit() + + async def visit_payloads(self, payloads: MutableSequence[Payload]) -> None: + await self._visit() + + async def visit_system_nexus_envelope(self, payload: Payload) -> None: + await self._visit() + + async def _visit(self) -> None: + nonlocal active_visits, max_active_visits + active_visits += 1 + max_active_visits = max(max_active_visits, active_visits) + if active_visits == 2: + two_visits_started.set() + try: + await release_visits.wait() + finally: + active_visits -= 1 + + payload_converter = nexus_system._get_payload_converter( + temporalio.converter.PayloadConverter.default + ) + system_request = workflowservice_pb2.SignalWithStartWorkflowExecutionRequest( + input=Payloads(payloads=[Payload(data=b"workflow-input")]), + signal_input=Payloads(payloads=[Payload(data=b"signal-input")]), + ) + payload = payload_converter.to_payload(system_request) + assert payload is not None + completion = WorkflowActivationCompletion( + run_id="1", + successful=Success( + commands=[ + WorkflowCommand( + schedule_nexus_operation=ScheduleNexusOperation( + seq=1, + endpoint=nexus_system.TEMPORAL_SYSTEM_ENDPOINT, + service="temporal.api.workflowservice.v1.WorkflowService", + operation="SignalWithStartWorkflowExecution", + input=payload, + ), + ) + ], + ), + ) + + task = asyncio.create_task( + PayloadVisitor(concurrency_limit=2).visit(SlowVisitor(), completion) + ) + await two_visits_started.wait() + await asyncio.sleep(0) + assert max_active_visits == 2 + + release_visits.set() + await task + assert max_active_visits == 2 + + +async def test_bridge_encoding(): + comp = WorkflowActivationCompletion( + run_id="1", + successful=Success( + commands=[ + WorkflowCommand( + schedule_activity=ScheduleActivity( + seq=1, + activity_id="1", + activity_type="", + task_queue="", + headers={"foo": Payload(data=b"bar")}, + arguments=[ + Payload(data=b"repeated1"), + Payload(data=b"repeated2"), + ], + schedule_to_close_timeout=Duration(seconds=5), + priority=Priority(), + ), + user_metadata=UserMetadata(summary=Payload(data=b"Summary")), + ) + ], + ), + ) + + data_converter = dataclasses.replace( + temporalio.converter.default(), + payload_codec=SimpleCodec(), + ) + + await temporalio.bridge.worker.encode_completion( + comp, data_converter, True, storage_concurrency_limit=1 + ) + + cmd = comp.successful.commands[0] + sa = cmd.schedule_activity + assert sa.headers["foo"].metadata["simple-codec"] + assert len(sa.arguments) == 1 + assert sa.arguments[0].metadata["simple-codec"] + + assert cmd.user_metadata.summary.metadata["simple-codec"] diff --git a/tests/worker/test_worker.py b/tests/worker/test_worker.py index 3e1a1c8f7..57614c21e 100644 --- a/tests/worker/test_worker.py +++ b/tests/worker/test_worker.py @@ -2,13 +2,21 @@ import asyncio import concurrent.futures +import multiprocessing +import multiprocessing.context +import os import uuid +from collections.abc import Awaitable, Callable, Sequence +from contextlib import contextmanager from datetime import timedelta -from typing import Any, Awaitable, Callable, Optional, Sequence +from typing import Any from urllib.request import urlopen +import nexusrpc +import pytest + import temporalio.api.enums.v1 -import temporalio.client +import temporalio.nexus import temporalio.worker._worker from temporalio import activity, workflow from temporalio.api.workflowservice.v1 import ( @@ -20,12 +28,15 @@ SetWorkerDeploymentRampingVersionResponse, ) from temporalio.client import ( - BuildIdOpAddNewDefault, Client, - TaskReachabilityType, + WorkflowHandle, ) from temporalio.common import PinnedVersioningOverride, RawValue, VersioningBehavior -from temporalio.runtime import PrometheusConfig, Runtime, TelemetryConfig +from temporalio.runtime import ( + PrometheusConfig, + Runtime, + TelemetryConfig, +) from temporalio.service import RPCError from temporalio.testing import WorkflowEnvironment from temporalio.worker import ( @@ -33,6 +44,7 @@ CustomSlotSupplier, FixedSizeSlotSupplier, LocalActivitySlotInfo, + NexusSlotInfo, PollerBehaviorAutoscaling, ResourceBasedSlotConfig, ResourceBasedSlotSupplier, @@ -42,24 +54,20 @@ SlotReleaseContext, SlotReserveContext, Worker, - WorkerConfig, WorkerDeploymentConfig, WorkerDeploymentVersion, WorkerTuner, WorkflowSlotInfo, ) +from temporalio.worker.workflow_sandbox import SandboxedWorkflowRunner from temporalio.workflow import DynamicWorkflowConfig, VersioningIntent from tests.helpers import ( assert_eventually, find_free_port, new_worker, - worker_versioning_enabled, ) - -# Passing through because Python 3.9 has an import bug at -# https://github.com/python/cpython/issues/91351 -with workflow.unsafe.imports_passed_through(): - import pytest +from tests.helpers.fork import _ForkTestResult, _TestFork +from tests.helpers.nexus import make_nexus_endpoint_name def test_load_default_worker_binary_id(): @@ -81,6 +89,31 @@ async def run(self) -> None: raise NotImplementedError +@workflow.defn +class TestClientUpdateWorkflow: + @workflow.run + async def run(self) -> None: + await workflow.execute_activity( + "capture_client_activity", + start_to_close_timeout=timedelta(seconds=10), + ) + + +@nexusrpc.handler.service_handler +class NeverRunService: + @nexusrpc.handler.sync_operation + async def never_run_operation( + self, _ctx: nexusrpc.handler.StartOperationContext, _input: None + ) -> None: + raise NotImplementedError + + @temporalio.nexus.workflow_run_operation + async def never_run_workflow_run_operation( + self, _ctx: temporalio.nexus.WorkflowRunOperationContext, _input: None + ) -> temporalio.nexus.WorkflowHandle[None]: + raise NotImplementedError + + async def test_worker_fatal_error_run(client: Client): # Run worker with injected workflow poll error worker = create_worker(client) @@ -148,7 +181,7 @@ async def test_worker_fatal_error_with(client: Client): async def test_worker_fatal_error_callback(client: Client): - callback_err: Optional[BaseException] = None + callback_err: BaseException | None = None async def on_fatal_error(exc: BaseException) -> None: nonlocal callback_err @@ -202,64 +235,6 @@ def my_signal(self, value: str) -> None: workflow.logger.info(f"Signal: {value}") -async def test_worker_versioning(client: Client, env: WorkflowEnvironment): - if env.supports_time_skipping: - pytest.skip("Java test server does not support worker versioning") - if not await worker_versioning_enabled(client): - pytest.skip("This server does not have worker versioning enabled") - - task_queue = f"worker-versioning-{uuid.uuid4()}" - await client.update_worker_build_id_compatibility( - task_queue, BuildIdOpAddNewDefault("1.0") - ) - - async with new_worker( - client, - WaitOnSignalWorkflow, - activities=[say_hello], - task_queue=task_queue, - build_id="1.0", - use_worker_versioning=True, - ): - wf1 = await client.start_workflow( - WaitOnSignalWorkflow.run, - id=f"worker-versioning-1-{uuid.uuid4()}", - task_queue=task_queue, - ) - # Sleep for a beat, otherwise it's possible for new workflow to start on 2.0 - await asyncio.sleep(0.1) - await client.update_worker_build_id_compatibility( - task_queue, BuildIdOpAddNewDefault("2.0") - ) - wf2 = await client.start_workflow( - WaitOnSignalWorkflow.run, - id=f"worker-versioning-2-{uuid.uuid4()}", - task_queue=task_queue, - ) - async with new_worker( - client, - WaitOnSignalWorkflow, - activities=[say_hello], - task_queue=task_queue, - build_id="2.0", - use_worker_versioning=True, - ): - # Confirm reachability type parameter is respected. If it wasn't, list would have - # `OPEN_WORKFLOWS` in it. - reachability = await client.get_worker_task_reachability( - build_ids=["2.0"], - reachability_type=TaskReachabilityType.CLOSED_WORKFLOWS, - ) - assert reachability.build_id_reachability["2.0"].task_queue_reachability[ - task_queue - ] == [TaskReachabilityType.NEW_WORKFLOWS] - - await wf1.signal(WaitOnSignalWorkflow.my_signal, "finish") - await wf2.signal(WaitOnSignalWorkflow.my_signal, "finish") - await wf1.result() - await wf2.result() - - async def test_worker_validate_fail(client: Client, env: WorkflowEnvironment): if env.supports_time_skipping: pytest.skip("Java test server does not appear to fail on invalid namespace") @@ -274,7 +249,7 @@ async def test_worker_validate_fail(client: Client, env: WorkflowEnvironment): assert str(err.value).startswith("Worker validation failed") -async def test_can_run_resource_based_worker(client: Client, env: WorkflowEnvironment): +async def test_can_run_resource_based_worker(client: Client): tuner = WorkerTuner.create_resource_based( target_memory_usage=0.5, target_cpu_usage=0.5, @@ -297,7 +272,7 @@ async def test_can_run_resource_based_worker(client: Client, env: WorkflowEnviro await wf1.result() -async def test_can_run_composite_tuner_worker(client: Client, env: WorkflowEnvironment): +async def test_can_run_composite_tuner_worker(client: Client): resource_based_options = ResourceBasedTunerConfig(0.5, 0.5) tuner = WorkerTuner.create_composite( workflow_supplier=FixedSizeSlotSupplier(5), @@ -317,6 +292,7 @@ async def test_can_run_composite_tuner_worker(client: Client, env: WorkflowEnvir ), resource_based_options, ), + nexus_supplier=FixedSizeSlotSupplier(10), ) async with new_worker( client, @@ -333,9 +309,7 @@ async def test_can_run_composite_tuner_worker(client: Client, env: WorkflowEnvir await wf1.result() -async def test_cant_specify_max_concurrent_and_tuner( - client: Client, env: WorkflowEnvironment -): +async def test_cant_specify_max_concurrent_and_tuner(client: Client): tuner = WorkerTuner.create_resource_based( target_memory_usage=0.5, target_cpu_usage=0.5, @@ -354,7 +328,7 @@ async def test_cant_specify_max_concurrent_and_tuner( assert "when also specifying tuner" in str(err.value) -async def test_warns_when_workers_too_lot(client: Client, env: WorkflowEnvironment): +async def test_warns_when_workers_too_low(client: Client): tuner = WorkerTuner.create_resource_based( target_memory_usage=0.5, target_cpu_usage=0.5, @@ -372,9 +346,64 @@ async def test_warns_when_workers_too_lot(client: Client, env: WorkflowEnvironme activity_executor=executor, ): pass + with concurrent.futures.ThreadPoolExecutor() as executor: + with pytest.warns( + UserWarning, + match="Worker max_concurrent_nexus_tasks is 500 but nexus_task_executor's max_workers is only", + ): + async with new_worker( + client, + WaitOnSignalWorkflow, + nexus_service_handlers=[NeverRunService()], + tuner=tuner, + nexus_task_executor=executor, + ): + pass + + +@nexusrpc.handler.service_handler +class SayHelloService: + @nexusrpc.handler.sync_operation + async def say_hello( + self, _ctx: nexusrpc.handler.StartOperationContext, name: str + ) -> str: + return f"Hello, {name}!" + + +@workflow.defn +class CustomSlotSupplierWorkflow: + def __init__(self) -> None: + self._last_signal = "" + + @workflow.run + async def run(self) -> None: + await workflow.wait_condition(lambda: self._last_signal == "finish") + await workflow.execute_activity( + say_hello, + "hi", + versioning_intent=VersioningIntent.DEFAULT, + start_to_close_timeout=timedelta(seconds=5), + ) + nexus_client = workflow.create_nexus_client( + endpoint=make_nexus_endpoint_name(workflow.info().task_queue), + service=SayHelloService, + ) + await nexus_client.execute_operation( + SayHelloService.say_hello, + "hi", + ) + + @workflow.signal + def my_signal(self, value: str) -> None: + self._last_signal = value + workflow.logger.info(f"Signal: {value}") +@pytest.mark.requires_local_server async def test_custom_slot_supplier(client: Client, env: WorkflowEnvironment): + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work under Java test server") + class MyPermit(SlotPermit): def __init__(self, pnum: int): super().__init__() @@ -398,7 +427,7 @@ async def reserve_slot(self, ctx: SlotReserveContext) -> SlotPermit: self.reserves += 1 return MyPermit(self.reserves) - def try_reserve_slot(self, ctx: SlotReserveContext) -> Optional[SlotPermit]: + def try_reserve_slot(self, ctx: SlotReserveContext) -> SlotPermit | None: self.reserve_asserts(ctx) return None @@ -413,6 +442,8 @@ def mark_slot_used(self, ctx: SlotMarkUsedContext) -> None: self.seen_used_slot_kinds.add("a") elif isinstance(ctx.slot_info, LocalActivitySlotInfo): self.seen_used_slot_kinds.add("la") + elif isinstance(ctx.slot_info, NexusSlotInfo): + self.seen_used_slot_kinds.add("nx") self.used += 1 def release_slot(self, ctx: SlotReleaseContext) -> None: @@ -439,33 +470,38 @@ def reserve_asserts(self, ctx: SlotReserveContext) -> None: ss = MySlotSupplier() tuner = WorkerTuner.create_composite( - workflow_supplier=ss, activity_supplier=ss, local_activity_supplier=ss + workflow_supplier=ss, + activity_supplier=ss, + local_activity_supplier=ss, + nexus_supplier=ss, ) async with new_worker( client, - WaitOnSignalWorkflow, + CustomSlotSupplierWorkflow, activities=[say_hello], + nexus_service_handlers=[SayHelloService()], tuner=tuner, identity="myworker", ) as w: + endpoint_name = make_nexus_endpoint_name(w.task_queue) + await env.create_nexus_endpoint(endpoint_name, w.task_queue) wf1 = await client.start_workflow( - WaitOnSignalWorkflow.run, + CustomSlotSupplierWorkflow.run, id=f"custom-slot-supplier-{uuid.uuid4()}", task_queue=w.task_queue, ) - await wf1.signal(WaitOnSignalWorkflow.my_signal, "finish") + await wf1.signal(CustomSlotSupplierWorkflow.my_signal, "finish") await wf1.result() # We can't use reserve number directly because there is a technically possible race # where the python reserve function appears to complete, but Rust doesn't see that. # This isn't solvable without redoing a chunk of pyo3-asyncio. So we only check # that the permits passed to release line up. - assert ss.highest_seen_reserve_on_release == ss.releases - # Two workflow tasks, one activity - assert ss.used == 3 + assert ss.highest_seen_reserve_on_release >= ss.releases + assert ss.used == 5 assert ss.seen_sticky_kinds == {True, False} - assert ss.seen_slot_kinds == {"workflow", "activity", "local-activity"} - assert ss.seen_used_slot_kinds == {"wf", "a"} + assert ss.seen_slot_kinds == {"workflow", "activity", "local-activity", "nexus"} + assert ss.seen_used_slot_kinds == {"wf", "a", "nx"} assert ss.seen_release_info_empty assert ss.seen_release_info_nonempty @@ -477,7 +513,7 @@ async def run(self) -> str: return "hi" -async def test_throwing_slot_supplier(client: Client, env: WorkflowEnvironment): +async def test_throwing_slot_supplier(client: Client): """Ensures a (mostly) broken slot supplier doesn't hose everything up""" class ThrowingSlotSupplier(CustomSlotSupplier): @@ -489,7 +525,7 @@ async def reserve_slot(self, ctx: SlotReserveContext) -> SlotPermit: return SlotPermit() raise ValueError("I always throw") - def try_reserve_slot(self, ctx: SlotReserveContext) -> Optional[SlotPermit]: + def try_reserve_slot(self, ctx: SlotReserveContext) -> SlotPermit | None: raise ValueError("I always throw") def mark_slot_used(self, ctx: SlotMarkUsedContext) -> None: @@ -501,7 +537,10 @@ def release_slot(self, ctx: SlotReleaseContext) -> None: ss = ThrowingSlotSupplier() tuner = WorkerTuner.create_composite( - workflow_supplier=ss, activity_supplier=ss, local_activity_supplier=ss + workflow_supplier=ss, + activity_supplier=ss, + local_activity_supplier=ss, + nexus_supplier=ss, ) async with new_worker( client, @@ -517,7 +556,7 @@ def release_slot(self, ctx: SlotReleaseContext) -> None: await wf1.result() -async def test_blocking_slot_supplier(client: Client, env: WorkflowEnvironment): +async def test_blocking_slot_supplier(client: Client): class BlockingSlotSupplier(CustomSlotSupplier): marked_used = False @@ -525,7 +564,7 @@ async def reserve_slot(self, ctx: SlotReserveContext) -> SlotPermit: await asyncio.get_event_loop().create_future() raise ValueError("Should be unreachable") - def try_reserve_slot(self, ctx: SlotReserveContext) -> Optional[SlotPermit]: + def try_reserve_slot(self, ctx: SlotReserveContext) -> SlotPermit | None: return None def mark_slot_used(self, ctx: SlotMarkUsedContext) -> None: @@ -537,7 +576,10 @@ def release_slot(self, ctx: SlotReleaseContext) -> None: ss = BlockingSlotSupplier() tuner = WorkerTuner.create_composite( - workflow_supplier=ss, activity_supplier=ss, local_activity_supplier=ss + workflow_supplier=ss, + activity_supplier=ss, + local_activity_supplier=ss, + nexus_supplier=ss, ) async with new_worker( client, @@ -548,6 +590,95 @@ def release_slot(self, ctx: SlotReleaseContext) -> None: await asyncio.sleep(1) +@activity.defn +async def heartbeating_activity(beats: int) -> None: + for i in range(beats): + activity.heartbeat(i) + await asyncio.sleep(0) + + +@workflow.defn +class HeartbeatingFanOutWorkflow: + @workflow.run + async def run(self, total: int, beats: int, window: int) -> None: + in_flight = asyncio.Semaphore(window) + + async def run_one() -> None: + async with in_flight: + await workflow.execute_activity( + heartbeating_activity, + beats, + start_to_close_timeout=timedelta(minutes=1), + heartbeat_timeout=timedelta(seconds=30), + ) + + await asyncio.gather(*(run_one() for _ in range(total))) + + +async def test_custom_slot_supplier_with_heartbeating_activities(client: Client): + """Regression test for the GIL <-> core-mutex deadlock (#1642). + + Any custom slot supplier plus heartbeating activities used to wedge the + worker permanently: the heartbeat FFI held the GIL while blocking on + core's outstanding-activity lock, while an activity task start invoked + mark_slot_used (which acquires the GIL) under that same lock. The + supplier below is deliberately trivial - the deadlock was in the calling + convention, not in what the callbacks do. + + NOTE: on regression this test HANGS rather than fails - the deadlock + freezes the event loop, so no in-process timeout (including the + wait_for below) can fire, and only a CI-level job timeout kills it. + """ + + class CappedSlotSupplier(CustomSlotSupplier): + def __init__(self, max_slots: int) -> None: + self.max_slots = max_slots + self.used = 0 + + async def reserve_slot(self, ctx: SlotReserveContext) -> SlotPermit: + while True: + permit = self.try_reserve_slot(ctx) + if permit is not None: + return permit + await asyncio.sleep(0.005) + + def try_reserve_slot(self, ctx: SlotReserveContext) -> SlotPermit | None: + # Called with the GIL held, so no extra lock is needed + if self.used >= self.max_slots: + return None + self.used += 1 + return SlotPermit() + + def mark_slot_used(self, ctx: SlotMarkUsedContext) -> None: + return None + + def release_slot(self, ctx: SlotReleaseContext) -> None: + self.used = max(0, self.used - 1) + + fixed = FixedSizeSlotSupplier(100) + tuner = WorkerTuner.create_composite( + workflow_supplier=fixed, + # A small cap keeps activity starts (and thus mark_slot_used calls) + # churning against the heartbeat FFI + activity_supplier=CappedSlotSupplier(8), + local_activity_supplier=fixed, + nexus_supplier=fixed, + ) + async with new_worker( + client, + HeartbeatingFanOutWorkflow, + activities=[heartbeating_activity], + tuner=tuner, + ) as w: + wf1 = await client.start_workflow( + HeartbeatingFanOutWorkflow.run, + args=[600, 50, 150], + id=f"heartbeating-slot-supplier-{uuid.uuid4()}", + task_queue=w.task_queue, + ) + await asyncio.wait_for(wf1.result(), timeout=120) + + @workflow.defn( name="DeploymentVersioningWorkflow", versioning_behavior=VersioningBehavior.AUTO_UPGRADE, @@ -625,6 +756,7 @@ async def test_worker_with_worker_deployment_config( pytest.skip("Test Server doesn't support worker deployments") deployment_name = f"deployment-{uuid.uuid4()}" + workflow_id_prefix = f"basic-versioning-{uuid.uuid4()}" worker_v1 = WorkerDeploymentVersion(deployment_name=deployment_name, build_id="1.0") worker_v2 = WorkerDeploymentVersion(deployment_name=deployment_name, build_id="2.0") worker_v3 = WorkerDeploymentVersion(deployment_name=deployment_name, build_id="3.0") @@ -667,7 +799,7 @@ async def test_worker_with_worker_deployment_config( # Start workflow 1 which will use the 1.0 worker on auto-upgrade wf1 = await client.start_workflow( DeploymentVersioningWorkflowV1AutoUpgrade.run, - id="basic-versioning-v1", + id=f"{workflow_id_prefix}-v1", task_queue=w1.task_queue, ) assert "v1" == await wf1.query("state") @@ -679,7 +811,7 @@ async def test_worker_with_worker_deployment_config( wf2 = await client.start_workflow( DeploymentVersioningWorkflowV2Pinned.run, - id="basic-versioning-v2", + id=f"{workflow_id_prefix}-v2", task_queue=w1.task_queue, ) assert "v2" == await wf2.query("state") @@ -691,7 +823,7 @@ async def test_worker_with_worker_deployment_config( wf3 = await client.start_workflow( DeploymentVersioningWorkflowV3AutoUpgrade.run, - id="basic-versioning-v3", + id=f"{workflow_id_prefix}-v3", task_queue=w1.task_queue, ) assert "v3" == await wf3.query("state") @@ -743,9 +875,15 @@ async def test_worker_deployment_ramp(client: Client, env: WorkflowEnvironment): client, describe_resp.conflict_token, v1 ) ).conflict_token + await wait_for_worker_deployment_routing_config_propagation( + client, deployment_name, v1.build_id + ) conflict_token = ( await set_ramping_version(client, conflict_token, v2, 100) ).conflict_token + await wait_for_worker_deployment_routing_config_propagation( + client, deployment_name, v1.build_id, v2.build_id, 100 + ) # Run workflows and verify they run on v2 for i in range(3): @@ -762,6 +900,9 @@ async def test_worker_deployment_ramp(client: Client, env: WorkflowEnvironment): conflict_token = ( await set_ramping_version(client, conflict_token, v2, 0) ).conflict_token + await wait_for_worker_deployment_routing_config_propagation( + client, deployment_name, v1.build_id, v2.build_id, 0 + ) for i in range(3): wfa = await client.start_workflow( DeploymentVersioningWorkflowV1AutoUpgrade.run, @@ -774,6 +915,9 @@ async def test_worker_deployment_ramp(client: Client, env: WorkflowEnvironment): # Set ramp to 50 and eventually verify workflows run on both versions await set_ramping_version(client, conflict_token, v2, 50) + await wait_for_worker_deployment_routing_config_propagation( + client, deployment_name, v1.build_id, v2.build_id, 50 + ) seen_results = set() async def run_and_record(): @@ -796,7 +940,7 @@ async def check_results(): @workflow.defn(dynamic=True, versioning_behavior=VersioningBehavior.PINNED) class DynamicWorkflowVersioningOnDefn: @workflow.run - async def run(self, args: Sequence[RawValue]) -> str: + async def run(self, _args: Sequence[RawValue]) -> str: return "dynamic" @@ -809,7 +953,7 @@ def dynamic_config(self) -> DynamicWorkflowConfig: ) @workflow.run - async def run(self, args: Sequence[RawValue]) -> str: + async def run(self, _args: Sequence[RawValue]) -> str: return "dynamic" @@ -890,12 +1034,12 @@ async def run(self) -> str: @workflow.defn(dynamic=True) class NoVersioningAnnotationDynamicWorkflow: @workflow.run - async def run(self, args: Sequence[RawValue]) -> str: + async def run(self, _args: Sequence[RawValue]) -> str: return "whee" async def test_workflows_must_have_versioning_behavior_when_feature_turned_on( - client: Client, env: WorkflowEnvironment + client: Client, ): with pytest.raises(ValueError) as exc_info: Worker( @@ -970,6 +1114,63 @@ async def test_workflows_can_use_default_versioning_behavior( ) +async def test_worker_deployment_config_without_versioning( + client: Client, env: WorkflowEnvironment +): + if env.supports_time_skipping: + pytest.skip("Test Server doesn't support worker deployments") + + build_id = "my-custom-build-id-1.0" + deployment_name = f"deployment-no-versioning-{uuid.uuid4()}" + + async with new_worker( + client, + NoVersioningAnnotationWorkflow, + deployment_config=WorkerDeploymentConfig( + version=WorkerDeploymentVersion( + deployment_name=deployment_name, build_id=build_id + ), + use_worker_versioning=False, + ), + ) as worker: + handle = await client.start_workflow( + NoVersioningAnnotationWorkflow.run, + id=f"no-versioning-build-id-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + result = await handle.result() + assert result == "whee" + + history = await handle.fetch_history() + assert any( + event.workflow_task_completed_event_attributes + and event.workflow_task_completed_event_attributes.worker_version + and event.workflow_task_completed_event_attributes.worker_version.build_id + == build_id + for event in history.events + ), "Expected build ID to appear in workflow history" + + +async def test_deployment_config_rejects_versioning_behavior_without_versioning( + client: Client, +): + with pytest.raises( + ValueError, match="default_versioning_behavior must be UNSPECIFIED" + ): + Worker( + client, + task_queue=f"task-queue-{uuid.uuid4()}", + workflows=[NoVersioningAnnotationWorkflow], + deployment_config=WorkerDeploymentConfig( + version=WorkerDeploymentVersion( + deployment_name="whatever", build_id="1.0" + ), + use_worker_versioning=False, + default_versioning_behavior=VersioningBehavior.AUTO_UPGRADE, + ), + ) + + async def test_workflows_can_use_versioning_override( client: Client, env: WorkflowEnvironment ): @@ -1029,9 +1230,7 @@ async def test_can_run_autoscaling_polling_worker( metrics=PrometheusConfig(bind_address=prom_addr), ) ) - client = await Client.connect( - client.service_client.config.target_host, - namespace=client.namespace, + client = await env.connect_client( runtime=runtime, ) @@ -1052,7 +1251,9 @@ async def test_can_run_autoscaling_polling_worker( activity_pollers = [l for l in matches if "activity_task" in l] assert len(activity_pollers) == 1 assert activity_pollers[0].endswith("2") - workflow_pollers = [l for l in matches if "workflow_task" in l] + workflow_pollers = [ + l for l in matches if "workflow_task" in l and w.task_queue in l + ] assert len(workflow_pollers) == 2 # There's sticky & non-sticky pollers, and they may have a count of 1 or 2 depending on # initialization timing. @@ -1125,15 +1326,70 @@ async def set_ramping_version( return response +async def wait_for_worker_deployment_routing_config_propagation( + client: Client, + deployment_name: str, + expected_current_build_id: str, + expected_ramping_build_id: str = "", + expected_ramping_percentage: float | None = None, +) -> None: + """Wait for routing config to be propagated to all task queues.""" + import temporalio.api.enums.v1 + + async def check() -> bool: + resp = await client.workflow_service.describe_worker_deployment( + DescribeWorkerDeploymentRequest( + namespace=client.namespace, + deployment_name=deployment_name, + ) + ) + routing_config = resp.worker_deployment_info.routing_config + if ( + routing_config.current_deployment_version.build_id + != expected_current_build_id + ): + return False + if ( + routing_config.ramping_deployment_version.build_id + != expected_ramping_build_id + ): + return False + if ( + expected_ramping_percentage is not None + and routing_config.ramping_version_percentage != expected_ramping_percentage + ): + return False + state = resp.worker_deployment_info.routing_config_update_state + if ( + state + == temporalio.api.enums.v1.RoutingConfigUpdateState.ROUTING_CONFIG_UPDATE_STATE_COMPLETED + ): + return True + if ( + state + == temporalio.api.enums.v1.RoutingConfigUpdateState.ROUTING_CONFIG_UPDATE_STATE_UNSPECIFIED + ): + return True # unimplemented + if ( + state + == temporalio.api.enums.v1.RoutingConfigUpdateState.ROUTING_CONFIG_UPDATE_STATE_IN_PROGRESS + ): + return False + return False + + await assert_eventually(check) + + def create_worker( client: Client, - on_fatal_error: Optional[Callable[[BaseException], Awaitable[None]]] = None, + on_fatal_error: Callable[[BaseException], Awaitable[None]] | None = None, ) -> Worker: return Worker( client, task_queue=f"task-queue-{uuid.uuid4()}", activities=[never_run_activity], workflows=[NeverRunWorkflow], + nexus_service_handlers=[NeverRunService()], on_fatal_error=on_fatal_error, ) @@ -1158,8 +1414,8 @@ def __init__(self, worker: Worker, attr: str) -> None: self.poll_fail_queue: asyncio.Queue[Exception] = asyncio.Queue() self.orig_poll_call = getattr(worker._bridge_worker, attr) setattr(worker._bridge_worker, attr, self.patched_poll_call) - self.next_poll_task: Optional[asyncio.Task] = None - self.next_exception_task: Optional[asyncio.Task] = None + self.next_poll_task: asyncio.Task | None = None + self.next_exception_task: asyncio.Task | None = None async def patched_poll_call(self) -> Any: if not self.next_poll_task: @@ -1189,3 +1445,404 @@ def shutdown(self) -> None: if self.next_exception_task: self.next_exception_task.cancel() setattr(self.worker._bridge_worker, self.attr, self.orig_poll_call) + + +class TestForkCreateWorker(_TestFork): + async def coro(self): + self._worker = Worker( # type:ignore[reportUninitializedInstanceVariable] + self._client, + task_queue=f"task-queue-{uuid.uuid4()}", + activities=[never_run_activity], + workflows=[], + nexus_service_handlers=[], + ) + + def test_fork_create_worker( + self, client: Client, mp_fork_ctx: multiprocessing.context.BaseContext | None + ): + self._expected = _ForkTestResult.assertion_error( + "Cannot create worker across forks" + ) + self._client = client # type:ignore[reportUninitializedInstanceVariable] + self.run(mp_fork_ctx) + + +class TestForkUseWorker(_TestFork): + async def coro(self): + await self._pre_fork_worker.run() + + def test_fork_use_worker( + self, client: Client, mp_fork_ctx: multiprocessing.context.BaseContext | None + ): + self._expected = _ForkTestResult.assertion_error( + "Cannot use worker across forks" + ) + self._pre_fork_worker = Worker( # type:ignore[reportUninitializedInstanceVariable] + client, + task_queue=f"task-queue-{uuid.uuid4()}", + activities=[never_run_activity], + workflows=[], + nexus_service_handlers=[], + ) + self.run(mp_fork_ctx) + + +async def test_activity_client_updates_when_worker_client_changes( + client: Client, env: WorkflowEnvironment +): + """Test that activities get the updated client when worker.client is changed.""" + # Create a second client (simulating a new client after cert rotation) + # Must use the same runtime + client2 = await env.connect_client( + data_converter=client.data_converter, + ) + + captured_clients: list[Client] = [] + + @activity.defn + async def capture_client_activity() -> None: + captured_clients.append(activity.client()) + + # Create worker with activities + worker = Worker( + client, + task_queue=f"task-queue-{uuid.uuid4()}", + activities=[capture_client_activity], + workflows=[TestClientUpdateWorkflow], + ) + + async with worker: + # Execute activity with original client + await client.execute_workflow( + TestClientUpdateWorkflow.run, + id=f"wf-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + # Update worker's client + worker.client = client2 + + # Execute activity again - should get the new client + await client2.execute_workflow( + TestClientUpdateWorkflow.run, + id=f"wf-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + # Should have captured both clients + assert len(captured_clients) == 2 + assert captured_clients[0] is client + assert captured_clients[1] is client2 # This will fail before the fix + + +@workflow.defn( + name="ContinueAsNewWithVersionUpgrade", + versioning_behavior=VersioningBehavior.PINNED, +) +class ContinueAsNewWithVersionUpgradeV1: + @workflow.run + async def run(self, attempt: int) -> str: + if attempt > 0: + return "v1.0" + + # Loop waiting for CAN suggestion with version changed + while True: + # Trigger a WFT when timer expires, thereby refreshing the continue-as-new-suggested flag + await asyncio.sleep(0.01) + info = workflow.info() + if info.is_target_worker_deployment_version_changed(): + workflow.continue_as_new( + arg=attempt + 1, + initial_versioning_behavior=workflow.ContinueAsNewVersioningBehavior.AUTO_UPGRADE, + ) + + +@workflow.defn( + name="ContinueAsNewWithVersionUpgrade", + versioning_behavior=VersioningBehavior.PINNED, +) +class ContinueAsNewWithVersionUpgradeV2: + @workflow.run + async def run(self, attempt: int) -> str: # type:ignore[reportUnusedParameter] + return "v2.0" + + +@workflow.defn( + name="ContinueAsNewWithRampingVersion", + versioning_behavior=VersioningBehavior.PINNED, +) +class ContinueAsNewWithRampingVersionV1: + def __init__(self) -> None: + self._should_continue_as_new = False + + @workflow.run + async def run(self, attempt: int) -> str: + if attempt > 0: + return "v1.0" + + await workflow.wait_condition(lambda: self._should_continue_as_new) + workflow.continue_as_new( + arg=attempt + 1, + initial_versioning_behavior=workflow.ContinueAsNewVersioningBehavior.USE_RAMPING_VERSION, + ) + + @workflow.signal + def do_continue_as_new(self) -> None: + self._should_continue_as_new = True + + +@workflow.defn( + name="ContinueAsNewWithRampingVersion", + versioning_behavior=VersioningBehavior.PINNED, +) +class ContinueAsNewWithRampingVersionV2: + @workflow.run + async def run(self, attempt: int) -> str: # type:ignore[reportUnusedParameter] + return "v2.0" + + +async def wait_for_workflow_running_on_version( + handle: WorkflowHandle[Any, Any], expected_build_id: str +) -> None: + """Wait until workflow is RUNNING with expected build ID.""" + + async def check() -> bool: + desc = await handle.describe() + if ( + desc.status + != temporalio.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_RUNNING + ): + return False + versioning_info = desc.raw_description.workflow_execution_info.versioning_info + if not versioning_info.HasField("deployment_version"): + return False + return versioning_info.deployment_version.build_id == expected_build_id + + await assert_eventually(check) + + +async def test_continue_as_new_with_version_upgrade( + client: Client, env: WorkflowEnvironment +): + if env.supports_time_skipping: + pytest.skip("Test Server doesn't support worker deployments") + + deployment_name = f"deployment-can-upgrade-{uuid.uuid4()}" + v1 = WorkerDeploymentVersion(deployment_name=deployment_name, build_id="1.0") + v2 = WorkerDeploymentVersion(deployment_name=deployment_name, build_id="2.0") + + async with ( + new_worker( + client, + ContinueAsNewWithVersionUpgradeV1, + deployment_config=WorkerDeploymentConfig( + version=v1, + use_worker_versioning=True, + ), + ) as w1, + new_worker( + client, + ContinueAsNewWithVersionUpgradeV2, + deployment_config=WorkerDeploymentConfig( + version=v2, + use_worker_versioning=True, + ), + task_queue=w1.task_queue, + ), + ): + # Wait for the deployment to be ready + describe_resp = await wait_until_worker_deployment_visible(client, v1) + + # Set version 1.0 as current + resp2 = await set_current_deployment_version( + client, describe_resp.conflict_token, v1 + ) + + # Wait for v1.0-as-Current routing config to be propagated + await wait_for_worker_deployment_routing_config_propagation( + client, deployment_name, v1.build_id + ) + + # Start workflow with v1 as current + handle = await client.start_workflow( + "ContinueAsNewWithVersionUpgrade", + 0, + id=f"test-can-version-upgrade-{uuid.uuid4()}", + task_queue=w1.task_queue, + ) + + # Wait for workflow to complete one WFT on v1.0 + await wait_for_workflow_running_on_version(handle, v1.build_id) + + # Wait for version 2.0 to be ready + await wait_until_worker_deployment_visible(client, v2) + + # Set version 2.0 as current + await set_current_deployment_version(client, resp2.conflict_token, v2) + + # Wait for v2.0-as-Current routing config to be propagated + await wait_for_worker_deployment_routing_config_propagation( + client, deployment_name, v2.build_id + ) + + # Expect workflow to return "v2.0", indicating that it continued-as-new and completed on v2 + result = await handle.result() + assert result == "v2.0" + + +async def test_continue_as_new_with_ramping_version( + client: Client, env: WorkflowEnvironment +): + if env.supports_time_skipping: + pytest.skip("Test Server doesn't support worker deployments") + + deployment_name = f"deployment-can-ramping-{uuid.uuid4()}" + v1 = WorkerDeploymentVersion(deployment_name=deployment_name, build_id="1.0") + v2 = WorkerDeploymentVersion(deployment_name=deployment_name, build_id="2.0") + + async with ( + new_worker( + client, + ContinueAsNewWithRampingVersionV1, + deployment_config=WorkerDeploymentConfig( + version=v1, + use_worker_versioning=True, + ), + ) as w1, + new_worker( + client, + ContinueAsNewWithRampingVersionV2, + deployment_config=WorkerDeploymentConfig( + version=v2, + use_worker_versioning=True, + ), + task_queue=w1.task_queue, + ), + ): + describe_resp = await wait_until_worker_deployment_visible(client, v1) + + resp2 = await set_current_deployment_version( + client, describe_resp.conflict_token, v1 + ) + await wait_for_worker_deployment_routing_config_propagation( + client, deployment_name, v1.build_id + ) + + handle = await client.start_workflow( + "ContinueAsNewWithRampingVersion", + 0, + id=f"test-can-ramping-version-{uuid.uuid4()}", + task_queue=w1.task_queue, + ) + await wait_for_workflow_running_on_version(handle, v1.build_id) + + await wait_until_worker_deployment_visible(client, v2) + await set_ramping_version(client, resp2.conflict_token, v2, 0) + await wait_for_worker_deployment_routing_config_propagation( + client, deployment_name, v1.build_id, v2.build_id + ) + + await handle.signal(ContinueAsNewWithRampingVersionV1.do_continue_as_new) + + result = await handle.result() + assert result == "v2.0" + + +def test_worker_config_matches_init_params(): + """WorkerConfig TypedDict keys must match Worker.__init__ kwargs.""" + import inspect + + from temporalio.worker import Worker, WorkerConfig + + init_params = set(inspect.signature(Worker.__init__).parameters.keys()) - {"self"} + config_keys = set(WorkerConfig.__annotations__.keys()) + assert config_keys == init_params, ( + f"WorkerConfig is out of sync with Worker.__init__. " + f"Missing from config: {init_params - config_keys}. " + f"Extra in config: {config_keys - init_params}." + ) + + +async def test_worker_max_eager_activity_reservations_per_workflow_task_config( + client: Client, +): + worker = Worker( + client, + workflows=[SimpleWorkflow], + task_queue=f"task-queue-{uuid.uuid4()}", + max_eager_activity_reservations_per_workflow_task=7, + ) + assert worker.config().get("max_eager_activity_reservations_per_workflow_task") == 7 + + +@pytest.mark.parametrize("value", [0, -1]) +async def test_worker_rejects_non_positive_max_eager_activity_reservations( + client: Client, value: int +): + with pytest.raises( + ValueError, + match=( + "max_eager_activity_reservations_per_workflow_task must be positive; " + "use disable_eager_activity_execution=True to disable eager activity execution" + ), + ): + Worker( + client, + workflows=[SimpleWorkflow], + task_queue=f"task-queue-{uuid.uuid4()}", + max_eager_activity_reservations_per_workflow_task=value, + ) + + +async def test_worker_debug_mode(client: Client): + worker = Worker( + client, + workflows=[SimpleWorkflow], + task_queue=f"task-queue-{uuid.uuid4()}", + ) + assert worker._workflow_worker + assert worker._workflow_worker._deadlock_timeout_seconds == 2 + assert isinstance(worker._workflow_worker._workflow_runner, SandboxedWorkflowRunner) + assert ( + "_pydevd_bundle" + not in worker._workflow_worker._workflow_runner.restrictions.passthrough_modules + ) + + worker = Worker( + client, + workflows=[SimpleWorkflow], + task_queue=f"task-queue-{uuid.uuid4()}", + debug_mode=True, + ) + assert worker._workflow_worker + assert worker._workflow_worker._deadlock_timeout_seconds is None + assert isinstance(worker._workflow_worker._workflow_runner, SandboxedWorkflowRunner) + assert ( + "_pydevd_bundle" + in worker._workflow_worker._workflow_runner.restrictions.passthrough_modules + ) + + @contextmanager + def debug_envvar(): + os.environ["TEMPORAL_DEBUG"] = "true" + try: + yield + finally: + os.environ.pop("TEMPORAL_DEBUG") + + with debug_envvar(): + worker = Worker( + client, + workflows=[SimpleWorkflow], + task_queue=f"task-queue-{uuid.uuid4()}", + ) + assert worker._workflow_worker + assert worker._workflow_worker._deadlock_timeout_seconds is None + assert isinstance( + worker._workflow_worker._workflow_runner, + SandboxedWorkflowRunner, + ) + assert ( + "_pydevd_bundle" + in worker._workflow_worker._workflow_runner.restrictions.passthrough_modules + ) diff --git a/tests/worker/test_workflow.py b/tests/worker/test_workflow.py index d13debf12..cca6e779d 100644 --- a/tests/worker/test_workflow.py +++ b/tests/worker/test_workflow.py @@ -1,3 +1,4 @@ +# pyright: reportUnreachable=false from __future__ import annotations import asyncio @@ -15,36 +16,32 @@ import typing import uuid from abc import ABC, abstractmethod -from contextlib import contextmanager +from collections.abc import Awaitable, Mapping, Sequence from dataclasses import dataclass from datetime import datetime, timedelta, timezone from enum import IntEnum from functools import partial from typing import ( Any, - Awaitable, - Callable, - Dict, - List, - Mapping, + Literal, NoReturn, - Optional, - Sequence, - Tuple, - Type, - Union, cast, ) from urllib.request import urlopen import pydantic +import pytest from google.protobuf.timestamp_pb2 import Timestamp -from typing_extensions import Literal, Protocol, runtime_checkable +from typing_extensions import Protocol, runtime_checkable import temporalio.activity import temporalio.api.sdk.v1 import temporalio.client +import temporalio.converter +import temporalio.converter._extstore import temporalio.worker +import temporalio.worker._command_aware_visitor +import temporalio.worker._workflow_instance import temporalio.workflow from temporalio import activity, workflow from temporalio.api.common.v1 import Payload, Payloads, WorkflowExecution @@ -62,8 +59,6 @@ AsyncActivityCancelledError, Client, CreateScheduleInput, - RPCError, - RPCStatusCode, ScheduleActionStartWorkflow, ScheduleHandle, SignalWorkflowInput, @@ -115,11 +110,12 @@ Runtime, TelemetryConfig, ) -from temporalio.service import __version__ +from temporalio.service import RPCError, RPCStatusCode, __version__ from temporalio.testing import WorkflowEnvironment from temporalio.worker import ( ExecuteWorkflowInput, HandleSignalInput, + Replayer, UnsandboxedWorkflowRunner, Worker, WorkflowInstance, @@ -128,29 +124,34 @@ ) from tests import DEV_SERVER_DOWNLOAD_VERSION from tests.helpers import ( + LogCapturer, + LogHandler, admitted_update_task, assert_eq_eventually, assert_eventually, assert_pending_activity_exists_eventually, assert_task_fail_eventually, assert_workflow_exists_eventually, + async_wait_for_pause_event, ensure_search_attributes_present, find_free_port, get_pending_activity_info, new_worker, pause_and_assert, unpause_and_assert, + wait_for_pause_event, workflow_update_exists, ) +from tests.helpers.cache_eviction import ( + CacheEvictionTearDownWorkflow, + WaitForeverWorkflow, + wait_forever_activity, +) from tests.helpers.external_stack_trace import ( ExternalStackTraceWorkflow, external_wait_cancel, ) - -# Passing through because Python 3.9 has an import bug at -# https://github.com/python/cpython/issues/91351 -with workflow.unsafe.imports_passed_through(): - import pytest +from tests.helpers.metrics import PromMetricMatcher @workflow.defn @@ -220,7 +221,7 @@ async def test_workflow_multi_param(client: Client): @workflow.defn class InfoWorkflow: @workflow.run - async def run(self) -> Dict: + async def run(self) -> dict: # Convert to JSON and back so it'll stringify un-JSON-able pieces ret = dataclasses.asdict(workflow.info()) return json.loads(json.dumps(ret, default=str)) @@ -307,6 +308,7 @@ def get_history_info(self) -> HistoryInfo: ) +@pytest.mark.requires_local_server async def test_workflow_history_info( client: Client, env: WorkflowEnvironment, continue_as_new_suggest_history_count: int ): @@ -330,20 +332,42 @@ async def test_workflow_history_info( await handle.signal( HistoryInfoWorkflow.bunch_of_events, continue_as_new_suggest_history_count ) + + # Wait for the first signal's timers to be committed so the next + # signal creates a post-timer workflow task with updated workflow.info(). + # This avoids a race where both signals are accepted before the worker + # processes the first one and both make it into the same activation. + # If that occurs, the query will have the stale history that the + # final signal is intended to avoid. + async def timer_events_recorded() -> None: + timer_started_count = 0 + async for event in handle.fetch_history_events(): + if event.HasField("timer_started_event_attributes"): + timer_started_count += 1 + if timer_started_count >= continue_as_new_suggest_history_count: + return + assert timer_started_count >= continue_as_new_suggest_history_count + + await assert_eventually(timer_events_recorded) + # Send one more event to trigger the WFT update. We have to do this # because just a query will have a stale representation of history # counts, but signal forces a new WFT. await handle.signal(HistoryInfoWorkflow.bunch_of_events, 1) - new_info = await handle.query(HistoryInfoWorkflow.get_history_info) - assert new_info.history_length > continue_as_new_suggest_history_count - assert new_info.history_size > orig_info.history_size - assert new_info.continue_as_new_suggested + + async def history_info_updated() -> None: + new_info = await handle.query(HistoryInfoWorkflow.get_history_info) + assert new_info.history_length > continue_as_new_suggest_history_count + assert new_info.history_size > orig_info.history_size + assert new_info.continue_as_new_suggested + + await assert_eventually(history_info_updated) @workflow.defn class SignalAndQueryWorkflow: def __init__(self) -> None: - self._last_event: Optional[str] = None + self._last_event: str | None = None @workflow.run async def run(self) -> None: @@ -420,7 +444,7 @@ async def test_workflow_signal_and_query(client: Client): @workflow.defn class SignalAndQueryHandlersWorkflow: def __init__(self) -> None: - self._last_event: Optional[str] = None + self._last_event: str | None = None @workflow.run async def run(self) -> None: @@ -561,7 +585,7 @@ async def test_workflow_signal_and_query_errors(client: Client): @workflow.defn class SignalAndQueryOldDynamicStyleWorkflow: def __init__(self) -> None: - self._last_event: Optional[str] = None + self._last_event: str | None = None @workflow.run async def run(self) -> None: @@ -602,7 +626,7 @@ async def test_workflow_signal_and_query_old_dynamic_style(client: Client): @workflow.defn class SignalAndQueryHandlersOldDynamicStyleWorkflow: def __init__(self) -> None: - self._last_event: Optional[str] = None + self._last_event: str | None = None @workflow.run async def run(self) -> None: @@ -668,10 +692,10 @@ class BadSignalParam: @workflow.defn class BadSignalParamWorkflow: def __init__(self) -> None: - self._signals: List[BadSignalParam] = [] + self._signals: list[BadSignalParam] = [] @workflow.run - async def run(self) -> List[BadSignalParam]: + async def run(self) -> list[BadSignalParam]: await workflow.wait_condition( lambda: bool(self._signals) and self._signals[-1].some_str == "finish" ) @@ -683,21 +707,36 @@ async def some_signal(self, param: BadSignalParam) -> None: async def test_workflow_bad_signal_param(client: Client): - async with new_worker(client, BadSignalParamWorkflow) as worker: - handle = await client.start_workflow( - BadSignalParamWorkflow.run, - id=f"workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, + with LogCapturer().logs_captured( + temporalio.worker._workflow_instance.logger + ) as capturer: + async with new_worker(client, BadSignalParamWorkflow) as worker: + handle = await client.start_workflow( + BadSignalParamWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + # Send 4 signals, first and third are bad + await handle.signal("some_signal", "bad") + await handle.signal("some_signal", BadSignalParam(some_str="good")) + await handle.signal("some_signal", 123) + await handle.signal("some_signal", BadSignalParam(some_str="finish")) + assert [ + BadSignalParam(some_str="good"), + BadSignalParam(some_str="finish"), + ] == await handle.result() + + # Check that the log message includes workflow context + record = capturer.find_log("Failed deserializing signal input") + assert record is not None + assert "some_signal" in record.message + assert "BadSignalParamWorkflow" in record.message + assert handle.id in record.message + assert hasattr(record, "temporal_workflow") + assert ( + getattr(record, "temporal_workflow")["workflow_type"] + == "BadSignalParamWorkflow" ) - # Send 4 signals, first and third are bad - await handle.signal("some_signal", "bad") - await handle.signal("some_signal", BadSignalParam(some_str="good")) - await handle.signal("some_signal", 123) - await handle.signal("some_signal", BadSignalParam(some_str="finish")) - assert [ - BadSignalParam(some_str="good"), - BadSignalParam(some_str="finish"), - ] == await handle.result() @workflow.defn @@ -708,7 +747,7 @@ def __init__(self) -> None: self._received_event2 = False @workflow.run - async def run(self) -> Dict: + async def run(self) -> dict: # Record start times ret = { # "now" timestamp and current event loop monotonic time @@ -778,8 +817,8 @@ async def status() -> str: execution=WorkflowExecution(workflow_id=handle.id), ) ) - first_timestamp: Optional[Timestamp] = None - last_timestamp: Optional[Timestamp] = None + first_timestamp: Timestamp | None = None + last_timestamp: Timestamp | None = None for event in resp.history.events: # Get timestamp from first workflow task started if event.event_type is EventType.EVENT_TYPE_WORKFLOW_TASK_STARTED: @@ -927,9 +966,6 @@ async def run(self, params: CancelActivityWorkflowParams) -> None: self._activity_result = await handle except ActivityError as err: self._activity_result = f"Error: {err.cause.__class__.__name__}" - # TODO(cretz): Remove when https://github.com/temporalio/sdk-core/issues/323 is fixed - except CancelledError as err: - self._activity_result = f"Error: {err.__class__.__name__}" # Wait forever await asyncio.Future() @@ -1060,6 +1096,148 @@ async def started() -> bool: assert (await handle.describe()).status == WorkflowExecutionStatus.CANCELED +@workflow.defn +class CancelReasonWorkflow: + def __init__(self) -> None: + self._started = False + # Reason observed when the inner task was cancelled (no external + # workflow cancel has happened yet at that point). + self._reason_inner: str | None = "unset" + # Reason observed in the outer CancelledError handler after the + # external workflow cancel has been delivered. + self._reason_outer: str | None = "unset" + + @workflow.run + async def run(self) -> NoReturn: + self._started = True + task = asyncio.create_task(asyncio.sleep(1000)) + try: + task.cancel() + await task + except asyncio.CancelledError: + self._reason_inner = workflow.cancellation_reason() + try: + await asyncio.sleep(1000) + except asyncio.CancelledError: + self._reason_outer = workflow.cancellation_reason() + raise + raise RuntimeError("unreachable") + + @workflow.query + def started(self) -> bool: + return self._started + + @workflow.query + def reason_inner(self) -> str | None: + return self._reason_inner + + @workflow.query + def reason_outer(self) -> str | None: + return self._reason_outer + + +@pytest.mark.parametrize("reason", ["user-supplied reason", ""]) +async def test_workflow_cancellation_reason(client: Client, reason: str): + async with new_worker(client, CancelReasonWorkflow) as worker: + handle = await client.start_workflow( + CancelReasonWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + async def started() -> bool: + return await handle.query(CancelReasonWorkflow.started) + + await assert_eq_eventually(True, started) + # Before any external cancel, reason is None even though an inner task + # cancel has already been observed. + assert await handle.query(CancelReasonWorkflow.reason_inner) is None + + # When reason is "", cancel without providing the kwarg at all to + # exercise the default path. + if reason: + await handle.cancel(reason=reason) + else: + await handle.cancel() + with pytest.raises(WorkflowFailureError) as err: + await handle.result() + assert isinstance(err.value.cause, CancelledError) + + outer = await handle.query(CancelReasonWorkflow.reason_outer) + # Load-bearing: a cancel with no reason still produces an empty string, + # not None — None means "no external cancel happened". + assert outer is not None + assert outer == reason + + +@workflow.defn +class CancelReasonReporter: + """Workflow that swallows a cancel and returns the observed reason.""" + + @workflow.run + async def run(self) -> str: + try: + await asyncio.sleep(1000) + except asyncio.CancelledError: + return workflow.cancellation_reason() or "" + raise RuntimeError("unreachable") + + +@workflow.defn +class ChildCancelReasonWorkflow: + @workflow.run + async def run(self, msg: str) -> str: + child = await workflow.start_child_workflow( + CancelReasonReporter.run, + id=f"{workflow.info().workflow_id}_child", + ) + child.cancel(msg) + return await child + + +async def test_workflow_child_cancel_reason(client: Client): + async with new_worker( + client, ChildCancelReasonWorkflow, CancelReasonReporter + ) as worker: + result = await client.execute_workflow( + ChildCancelReasonWorkflow.run, + "from-parent", + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + assert result == "from-parent" + + +@workflow.defn +class ExternalCancelReasonWorkflow: + @workflow.run + async def run(self, target_id: str) -> None: + await workflow.get_external_workflow_handle(target_id).cancel( + reason="from-external-caller" + ) + + +async def test_workflow_external_cancel_reason(client: Client): + async with new_worker( + client, ExternalCancelReasonWorkflow, CancelReasonReporter + ) as worker: + target_id = f"workflow-{uuid.uuid4()}" + target = await client.start_workflow( + CancelReasonReporter.run, + id=target_id, + task_queue=worker.task_queue, + ) + await client.execute_workflow( + ExternalCancelReasonWorkflow.run, + target_id, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + # Server wraps the user-supplied reason with metadata about the caller + # when one workflow cancels another, so check for substring. + assert "from-external-caller" in await target.result() + + @workflow.defn class TrapCancelWorkflow: @workflow.run @@ -1071,7 +1249,103 @@ async def run(self) -> str: return "cancelled" -async def test_workflow_cancel_before_run(client: Client): +_WorkflowLogicFlag = temporalio.worker._workflow_instance._WorkflowLogicFlag +_SINGLE_BATCH_WORKFLOW_LOGIC_FLAG = ( + _WorkflowLogicFlag.PROCESS_WORKFLOW_ACTIVATION_JOBS_AS_SINGLE_BATCH +) + + +def test_single_batch_workflow_activation_jobs_default_disabled() -> None: + assert ( + _SINGLE_BATCH_WORKFLOW_LOGIC_FLAG + not in temporalio.worker._workflow_instance._DEFAULT_ENABLED_WORKFLOW_LOGIC_FLAGS + ) + + +@workflow.defn +class EnableWorkflowLogicFlagAfterReplayWorkflow: + def __init__(self) -> None: + self._ready = False + self._finish = False + + @workflow.run + async def run(self) -> str: + self._ready = True + await workflow.wait_condition(lambda: self._finish) + return "done" + + @workflow.signal + def finish(self) -> None: + self._finish = True + + @workflow.query + def ready(self) -> bool: + return self._ready + + +async def test_workflow_logic_flag_enabled_after_replay(client: Client) -> None: + task_queue = str(uuid.uuid4()) + handle = await client.start_workflow( + EnableWorkflowLogicFlagAfterReplayWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=task_queue, + ) + + async with new_worker( + client, + EnableWorkflowLogicFlagAfterReplayWorkflow, + task_queue=task_queue, + # The Java test server does not reliably reschedule an abandoned sticky + # task after its timeout, so keep events sent after this worker stops on + # the normal queue. + max_cached_workflows=0, + ): + + async def ready() -> bool: + return await handle.query(EnableWorkflowLogicFlagAfterReplayWorkflow.ready) + + await assert_eq_eventually(True, ready) + + await handle.signal(EnableWorkflowLogicFlagAfterReplayWorkflow.finish) + + runner = CustomWorkflowRunner() + worker = new_worker( + client, + EnableWorkflowLogicFlagAfterReplayWorkflow, + task_queue=task_queue, + workflow_runner=runner, + ) + worker._set_default_workflow_logic_flag( + _SINGLE_BATCH_WORKFLOW_LOGIC_FLAG, enabled=True + ) + async with worker: + assert await handle.result() == "done" + + assert any(activation.is_replaying for activation, _ in runner._pairs) + assert any( + not activation.is_replaying + and _SINGLE_BATCH_WORKFLOW_LOGIC_FLAG + in completion.successful.used_internal_flags + for activation, completion in runner._pairs + ) + + history = await handle.fetch_history() + workflow_task_flags = [ + event.workflow_task_completed_event_attributes.sdk_metadata.lang_used_flags + for event in history.events + if event.HasField("workflow_task_completed_event_attributes") + ] + assert _SINGLE_BATCH_WORKFLOW_LOGIC_FLAG not in workflow_task_flags[0] + assert any( + _SINGLE_BATCH_WORKFLOW_LOGIC_FLAG in flags for flags in workflow_task_flags[1:] + ) + await Replayer( + workflows=[EnableWorkflowLogicFlagAfterReplayWorkflow] + ).replay_workflow(history) + + +@pytest.mark.parametrize("single_batch", [False, True]) +async def test_workflow_cancel_before_run(client: Client, single_batch: bool): # Start the workflow _and_ send cancel before even starting the workflow task_queue = str(uuid.uuid4()) handle = await client.start_workflow( @@ -1081,10 +1355,248 @@ async def test_workflow_cancel_before_run(client: Client): ) await handle.cancel() # Start worker and wait for result - async with new_worker(client, TrapCancelWorkflow, task_queue=task_queue): + worker = new_worker(client, TrapCancelWorkflow, task_queue=task_queue) + if single_batch: + worker._set_default_workflow_logic_flag( + _SINGLE_BATCH_WORKFLOW_LOGIC_FLAG, enabled=True + ) + async with worker: assert "cancelled" == await handle.result() +@workflow.defn +class CancelAtWaitConditionWorkflow: + def __init__(self) -> None: + self._ready = False + self._proceed = False + self._waiting_after_cancel = False + self._finish_after_cancel = False + + @workflow.run + async def run(self, timeout: bool) -> str: + self._ready = True + try: + await workflow.wait_condition( + lambda: self._proceed, timeout=1000 if timeout else None + ) + except asyncio.CancelledError: + # A caught cancellation must not be raised again merely because a + # later wait sees the already-recorded workflow cancellation. + self._waiting_after_cancel = True + await workflow.wait_condition(lambda: self._finish_after_cancel) + return "cancelled" + return "condition" + + @workflow.signal + def proceed(self) -> None: + self._proceed = True + + @workflow.signal + def finish_after_cancel(self) -> None: + self._finish_after_cancel = True + + @workflow.query + def ready(self) -> bool: + return self._ready + + @workflow.query + def waiting_after_cancel(self) -> bool: + return self._waiting_after_cancel + + +@pytest.mark.parametrize("timeout", [False, True]) +async def test_workflow_cancel_and_condition_ready_in_same_activation( + client: Client, timeout: bool +): + task_queue = str(uuid.uuid4()) + runner = CustomWorkflowRunner() + handle = await client.start_workflow( + CancelAtWaitConditionWorkflow.run, + timeout, + id=f"workflow-{uuid.uuid4()}", + task_queue=task_queue, + ) + + worker = new_worker( + client, + CancelAtWaitConditionWorkflow, + task_queue=task_queue, + workflow_runner=runner, + # The Java test server does not reliably reschedule an abandoned sticky + # task after its timeout, so keep events sent after this worker stops on + # the normal queue. + max_cached_workflows=0, + ) + worker._set_default_workflow_logic_flag( + _SINGLE_BATCH_WORKFLOW_LOGIC_FLAG, enabled=True + ) + async with worker: + + async def ready() -> bool: + return await handle.query(CancelAtWaitConditionWorkflow.ready) + + await assert_eq_eventually(True, ready) + + # Keep the worker offline so the signal and cancellation are delivered in + # one activation when polling resumes. + await handle.signal(CancelAtWaitConditionWorkflow.proceed) + await handle.cancel() + + worker = new_worker( + client, + CancelAtWaitConditionWorkflow, + task_queue=task_queue, + workflow_runner=runner, + ) + worker._set_default_workflow_logic_flag( + _SINGLE_BATCH_WORKFLOW_LOGIC_FLAG, enabled=True + ) + async with worker: + + async def waiting_after_cancel() -> bool: + return await handle.query( + CancelAtWaitConditionWorkflow.waiting_after_cancel + ) + + await assert_eq_eventually(True, waiting_after_cancel) + await handle.signal(CancelAtWaitConditionWorkflow.finish_after_cancel) + assert await handle.result() == "cancelled" + + assert any( + _SINGLE_BATCH_WORKFLOW_LOGIC_FLAG in completion.successful.used_internal_flags + for _, completion in runner._pairs + ) + assert any( + {"signal_workflow", "cancel_workflow"}.issubset( + {job.WhichOneof("variant") for job in activation.jobs} + ) + for activation, _ in runner._pairs + ) + await Replayer(workflows=[CancelAtWaitConditionWorkflow]).replay_workflow( + await handle.fetch_history() + ) + + +@workflow.defn +class CompleteChildOnSignalWorkflow: + def __init__(self) -> None: + self._finish = False + + @workflow.run + async def run(self) -> str: + await workflow.wait_condition(lambda: self._finish) + return "child complete" + + @workflow.signal + def finish(self) -> None: + self._finish = True + + +@workflow.defn +class CancelAtChildCompletionWorkflow: + def __init__(self) -> None: + self._child_started = False + + @workflow.run + async def run(self, child_task_queue: str) -> str: + child = await workflow.start_child_workflow( + CompleteChildOnSignalWorkflow.run, + id=f"{workflow.info().workflow_id}-child", + task_queue=child_task_queue, + ) + self._child_started = True + return await child + + @workflow.query + def child_started(self) -> bool: + return self._child_started + + +async def test_workflow_cancel_and_child_completion_in_same_activation( + client: Client, +): + parent_task_queue = str(uuid.uuid4()) + child_task_queue = str(uuid.uuid4()) + workflow_id = f"workflow-{uuid.uuid4()}" + child_id = f"{workflow_id}-child" + runner = CustomWorkflowRunner() + + child_worker = new_worker( + client, + CompleteChildOnSignalWorkflow, + task_queue=child_task_queue, + ) + child_worker._set_default_workflow_logic_flag( + _SINGLE_BATCH_WORKFLOW_LOGIC_FLAG, enabled=True + ) + async with child_worker: + handle = await client.start_workflow( + CancelAtChildCompletionWorkflow.run, + child_task_queue, + id=workflow_id, + task_queue=parent_task_queue, + ) + parent_worker = new_worker( + client, + CancelAtChildCompletionWorkflow, + task_queue=parent_task_queue, + workflow_runner=runner, + # The Java test server does not reliably reschedule an abandoned + # sticky task after its timeout, so keep events sent after this + # worker stops on the normal queue. + max_cached_workflows=0, + ) + parent_worker._set_default_workflow_logic_flag( + _SINGLE_BATCH_WORKFLOW_LOGIC_FLAG, enabled=True + ) + async with parent_worker: + + async def child_started() -> bool: + return await handle.query(CancelAtChildCompletionWorkflow.child_started) + + await assert_eq_eventually(True, child_started) + + child_handle = client.get_workflow_handle(child_id) + await child_handle.signal(CompleteChildOnSignalWorkflow.finish) + assert await child_handle.result() == "child complete" + + async def child_completion_recorded() -> None: + async for event in handle.fetch_history_events(): + if ( + event.event_type + == EventType.EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_COMPLETED + ): + return + raise AssertionError("Child completion is not in parent history") + + await assert_eventually(child_completion_recorded) + await handle.cancel() + + parent_worker = new_worker( + client, + CancelAtChildCompletionWorkflow, + task_queue=parent_task_queue, + workflow_runner=runner, + ) + parent_worker._set_default_workflow_logic_flag( + _SINGLE_BATCH_WORKFLOW_LOGIC_FLAG, enabled=True + ) + async with parent_worker: + with pytest.raises(WorkflowFailureError) as err: + await handle.result() + assert isinstance(err.value.cause, CancelledError) + + assert any( + {"resolve_child_workflow_execution", "cancel_workflow"}.issubset( + {job.WhichOneof("variant") for job in activation.jobs} + ) + for activation, _ in runner._pairs + ) + await Replayer(workflows=[CancelAtChildCompletionWorkflow]).replay_workflow( + await handle.fetch_history() + ) + + @activity.defn async def wait_forever() -> NoReturn: await asyncio.Future() @@ -1144,7 +1656,7 @@ async def started() -> bool: class CancelChildWorkflow: def __init__(self) -> None: self._ready = False - self._task: Optional[asyncio.Task[Any]] = None + self._task: asyncio.Task[Any] | None = None @workflow.run async def run(self, use_execute: bool) -> None: @@ -1193,15 +1705,64 @@ async def test_workflow_cancel_child_started(client: Client, use_execute: bool): assert isinstance(err.value.cause.cause, CancelledError) -@pytest.mark.skip(reason="unable to easily prevent child start currently") -async def test_workflow_cancel_child_unstarted(client: Client): - raise NotImplementedError +@workflow.defn +class CancelDuringChildStartWorkflow: + def __init__(self) -> None: + self._proceed = False + + @workflow.signal + def proceed(self) -> None: + self._proceed = True + + @workflow.run + async def run(self) -> None: + await workflow.wait_condition(lambda: self._proceed) + # Start a child on a task queue with no worker. The child's first WFT + # never starts, so _start_fut remains unresolved and the start loop + # blocks forever. + await workflow.start_child_workflow( + LongSleepWorkflow.run, + id=f"{workflow.info().workflow_id}_child", + task_queue="nonexistent-task-queue-no-worker-abc123", + ) + await workflow.sleep(1000) + + +async def test_workflow_cancel_child_unstarted(client: Client): + # Regression test for https://github.com/temporalio/sdk-python/issues/1445 + # + # When cancellation arrived while the parent was waiting for a child + # workflow to start, the CancelledError was caught in the start loop + # to send a cancel command to the child — but was not re-raised. + # Because _start_fut never resolves (child on a queue with no worker), + # the loop would keep waiting forever, hanging the parent workflow. + # + # The fix: re-raise only when self._cancel_requested is True, which + # distinguishes Temporal workflow cancellation from other CancelledError + # sources such as asyncio.wait_for timeouts. + async with new_worker( + client, + CancelDuringChildStartWorkflow, + # Deliberately not registering LongSleepWorkflow and not starting + # a worker on the child's task queue. + ) as worker: + handle = await client.start_workflow( + CancelDuringChildStartWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=30), + ) + await handle.signal(CancelDuringChildStartWorkflow.proceed) + await handle.cancel() + with pytest.raises(WorkflowFailureError) as err: + await handle.result() + assert isinstance(err.value.cause, CancelledError) @workflow.defn class ReturnSignalWorkflow: def __init__(self) -> None: - self._signal: Optional[str] = None + self._signal: str | None = None @workflow.run async def run(self) -> str: @@ -1304,8 +1865,8 @@ async def test_workflow_signal_external(client: Client): @workflow.defn class MultiCancelWorkflow: @workflow.run - async def run(self) -> List[str]: - events: List[str] = [] + async def run(self) -> list[str]: + events: list[str] = [] async def timer(): nonlocal events @@ -1510,7 +2071,7 @@ async def test_workflow_activity_timeout(client: Client): # Just serializes in a "payloads" wrapper class SimpleCodec(PayloadCodec): - async def encode(self, payloads: Sequence[Payload]) -> List[Payload]: + async def encode(self, payloads: Sequence[Payload]) -> list[Payload]: wrapper = Payloads(payloads=payloads) return [ Payload( @@ -1518,7 +2079,7 @@ async def encode(self, payloads: Sequence[Payload]) -> List[Payload]: ) ] - async def decode(self, payloads: Sequence[Payload]) -> List[Payload]: + async def decode(self, payloads: Sequence[Payload]) -> list[Payload]: payloads = list(payloads) if len(payloads) != 1: raise RuntimeError("Expected only a single payload") @@ -1529,7 +2090,7 @@ async def decode(self, payloads: Sequence[Payload]) -> List[Payload]: return list(wrapper.payloads) -async def test_workflow_with_codec(client: Client, env: WorkflowEnvironment): +async def test_workflow_with_codec(client: Client): # Make client with this codec and run a couple of existing tests config = client.config() config["data_converter"] = DataConverter(payload_codec=SimpleCodec()) @@ -1541,10 +2102,10 @@ async def test_workflow_with_codec(client: Client, env: WorkflowEnvironment): class PassThroughCodec(PayloadCodec): - async def encode(self, payloads: Sequence[Payload]) -> List[Payload]: + async def encode(self, payloads: Sequence[Payload]) -> list[Payload]: return list(payloads) - async def decode(self, payloads: Sequence[Payload]) -> List[Payload]: + async def decode(self, payloads: Sequence[Payload]) -> list[Payload]: return list(payloads) @@ -1587,7 +2148,7 @@ class CustomWorkflowRunner(WorkflowRunner): def __init__(self) -> None: super().__init__() self._unsandboxed = UnsandboxedWorkflowRunner() - self._pairs: List[Tuple[WorkflowActivation, WorkflowActivationCompletion]] = [] + self._pairs: list[tuple[WorkflowActivation, WorkflowActivationCompletion]] = [] def prepare_workflow(self, defn: workflow._Definition) -> None: pass @@ -1609,6 +2170,21 @@ def activate(self, act: WorkflowActivation) -> WorkflowActivationCompletion: self._runner._pairs.append((act, comp)) return comp + def get_serialization_context( + self, + command_info: temporalio.worker._command_aware_visitor.CommandInfo | None, + ) -> temporalio.converter.SerializationContext | None: + return self._unsandboxed.get_serialization_context(command_info) + + def get_external_store_context( + self, + command_info: temporalio.worker._command_aware_visitor.CommandInfo | None, + ) -> temporalio.converter._extstore.StorageDriverStoreContext: + return self._unsandboxed.get_external_store_context(command_info) + + def get_info(self) -> temporalio.workflow.Info: + return self._unsandboxed.get_info() + async def test_workflow_with_custom_runner(client: Client): runner = CustomWorkflowRunner() @@ -1635,7 +2211,7 @@ async def test_workflow_with_custom_runner(client: Client): @workflow.defn class ContinueAsNewWorkflow: @workflow.run - async def run(self, past_run_ids: List[str]) -> List[str]: + async def run(self, past_run_ids: list[str]) -> list[str]: # Check memo and retry policy assert workflow.memo_value("past_run_id_count") == len(past_run_ids) retry_policy = workflow.info().retry_policy @@ -1654,6 +2230,7 @@ async def run(self, past_run_ids: List[str]) -> List[str]: # Add memo and retry policy to check memo={"past_run_id_count": len(past_run_ids)}, retry_policy=RetryPolicy(maximum_attempts=1000 + len(past_run_ids)), + backoff_start_interval=timedelta(milliseconds=1), ) @@ -1666,7 +2243,7 @@ async def test_workflow_continue_as_new(client: Client, env: WorkflowEnvironment async with new_worker(client, ContinueAsNewWorkflow) as worker: handle = await client.start_workflow( ContinueAsNewWorkflow.run, - cast(List[str], []), + cast(list[str], []), id=f"workflow-{uuid.uuid4()}", task_queue=worker.task_queue, memo={"past_run_id_count": 0}, @@ -1675,13 +2252,26 @@ async def test_workflow_continue_as_new(client: Client, env: WorkflowEnvironment result = await handle.result() assert len(result) == 5 assert result[0] == handle.first_execution_run_id + first_run_handle = client.get_workflow_handle( + handle.id, run_id=handle.first_execution_run_id + ) + history = await first_run_handle.fetch_history() + continued = [ + event.workflow_execution_continued_as_new_event_attributes + for event in history.events + if event.HasField("workflow_execution_continued_as_new_event_attributes") + ] + assert continued + assert continued[0].backoff_start_interval.ToTimedelta() == timedelta( + milliseconds=1 + ) sa_prefix = "python_test_" def search_attributes_to_serializable( - attrs: Union[SearchAttributes, TypedSearchAttributes], + attrs: SearchAttributes | TypedSearchAttributes, ) -> Mapping[str, Any]: if isinstance(attrs, TypedSearchAttributes): return { @@ -1724,7 +2314,7 @@ def get_search_attributes_typed(self) -> Mapping[str, Any]: @workflow.signal def do_search_attribute_update_untyped(self) -> None: - empty_float_list: List[float] = [] + empty_float_list: list[float] = [] workflow.upsert_search_attributes( { SearchAttributeWorkflow.text_attribute.name: ["text2"], @@ -1763,6 +2353,7 @@ def do_search_attribute_update_typed(self) -> None: ) +@pytest.mark.requires_local_server async def test_workflow_search_attributes(client: Client, env_type: str): if env_type != "local": pytest.skip("Only testing search attributes on local which disables cache") @@ -1808,7 +2399,7 @@ async def test_workflow_search_attributes(client: Client, env_type: str): ), ] ) - updated_attrs_untyped: Dict[str, SearchAttributeValues] = { + updated_attrs_untyped: dict[str, SearchAttributeValues] = { SearchAttributeWorkflow.text_attribute.name: ["text2"], SearchAttributeWorkflow.keyword_attribute.name: ["keyword1"], SearchAttributeWorkflow.keyword_list_attribute.name: [ @@ -1816,13 +2407,13 @@ async def test_workflow_search_attributes(client: Client, env_type: str): "keywordlist4", ], SearchAttributeWorkflow.int_attribute.name: [456], - SearchAttributeWorkflow.float_attribute.name: cast(List[float], []), + SearchAttributeWorkflow.float_attribute.name: cast(list[float], []), SearchAttributeWorkflow.bool_attribute.name: [False], SearchAttributeWorkflow.datetime_attribute.name: [ datetime(2003, 4, 5, 6, 7, 8, tzinfo=timezone(timedelta(hours=9))) ], } - updated_attrs_untyped_from_server: Dict[str, SearchAttributeValues] = { + updated_attrs_untyped_from_server: dict[str, SearchAttributeValues] = { SearchAttributeWorkflow.text_attribute.name: ["text2"], SearchAttributeWorkflow.keyword_attribute.name: ["keyword1"], SearchAttributeWorkflow.keyword_list_attribute.name: [ @@ -1948,6 +2539,7 @@ async def run(self) -> None: # All we need to do is complete +@pytest.mark.requires_local_server async def test_workflow_no_initial_search_attributes(client: Client, env_type: str): if env_type != "local": pytest.skip("Only testing search attributes on local which disables cache") @@ -1985,42 +2577,31 @@ def my_update(self, value: str) -> None: @workflow.query def last_signal(self) -> str: + workflow.logger.info("Query called") return self._last_signal -class LogCapturer: - def __init__(self) -> None: - self.log_queue: queue.Queue[logging.LogRecord] = queue.Queue() - - @contextmanager - def logs_captured(self, *loggers: logging.Logger): - handler = logging.handlers.QueueHandler(self.log_queue) - - prev_levels = [l.level for l in loggers] - for l in loggers: - l.setLevel(logging.INFO) - l.addHandler(handler) - try: - yield self - finally: - for i, l in enumerate(loggers): - l.removeHandler(handler) - l.setLevel(prev_levels[i]) - - def find_log(self, starts_with: str) -> Optional[logging.LogRecord]: - return self.find(lambda l: l.message.startswith(starts_with)) +@pytest.mark.parametrize( + "with_workflow_info", + [True, False], +) +async def test_workflow_logging(client: Client, with_workflow_info: bool): + orig_on_message = workflow.logger.workflow_info_on_message + orig_on_extra = workflow.logger.workflow_info_on_extra + orig_full_on_extra = workflow.logger.full_workflow_info_on_extra - def find( - self, pred: Callable[[logging.LogRecord], bool] - ) -> Optional[logging.LogRecord]: - for record in cast(List[logging.LogRecord], self.log_queue.queue): - if pred(record): - return record - return None + try: + workflow.logger.workflow_info_on_message = with_workflow_info + workflow.logger.workflow_info_on_extra = with_workflow_info + workflow.logger.full_workflow_info_on_extra = with_workflow_info + await _do_workflow_logging_test(client, with_workflow_info) + finally: + workflow.logger.workflow_info_on_message = orig_on_message + workflow.logger.workflow_info_on_extra = orig_on_extra + workflow.logger.full_workflow_info_on_extra = orig_full_on_extra -async def test_workflow_logging(client: Client, env: WorkflowEnvironment): - workflow.logger.full_workflow_info_on_extra = True +async def _do_workflow_logging_test(client: Client, with_workflow_info: bool): with LogCapturer().logs_captured( workflow.logger.base_logger, activity.logger.base_logger ) as capturer: @@ -2047,30 +2628,43 @@ async def test_workflow_logging(client: Client, env: WorkflowEnvironment): assert "signal 2" == await handle.query(LoggingWorkflow.last_signal) # Confirm logs were produced - assert capturer.find_log("Signal: signal 1 ({'attempt':") + assert capturer.find_log("Signal: signal 1") assert capturer.find_log("Signal: signal 2") assert capturer.find_log("Update: update 1") assert capturer.find_log("Update: update 2") + assert capturer.find_log("Query called") assert not capturer.find_log("Signal: signal 3") - # Also make sure it has some workflow info and correct funcName - record = capturer.find_log("Signal: signal 1") - assert ( - record - and record.__dict__["temporal_workflow"]["workflow_type"] - == "LoggingWorkflow" - and record.funcName == "my_signal" - ) - # Since we enabled full info, make sure it's there - assert isinstance(record.__dict__["workflow_info"], workflow.Info) - # Check the log emitted by the update execution. - record = capturer.find_log("Update: update 1") - assert ( - record - and record.__dict__["temporal_workflow"]["update_id"] == "update-1" - and record.__dict__["temporal_workflow"]["update_name"] == "my_update" - and "'update_id': 'update-1'" in record.message - and "'update_name': 'my_update'" in record.message - ) + + if with_workflow_info: + record = capturer.find_log("Signal: signal 1 ({'attempt':") + assert ( + record + and record.__dict__["temporal_workflow"]["workflow_type"] + == "LoggingWorkflow" + and record.funcName == "my_signal" + ) + # Since we enabled full info, make sure it's there + assert isinstance(record.__dict__["workflow_info"], workflow.Info) + + # Check the log emitted by the update execution. + record = capturer.find_log("Update: update 1") + assert ( + record + and record.__dict__["temporal_workflow"]["update_id"] == "update-1" + and record.__dict__["temporal_workflow"]["update_name"] == "my_update" + and "'update_id': 'update-1'" in record.message + and "'update_name': 'my_update'" in record.message + ) + else: + record = capturer.find_log("Signal: signal 1") + assert record and "temporal_workflow" not in record.__dict__ + assert record and "workflow_info" not in record.__dict__ + + record = capturer.find_log("Update: update 1") + assert record and "temporal_workflow" not in record.__dict__ + assert record and "workflow_info" not in record.__dict__ + assert "'update_id': 'update-1'" not in record.message + assert "'update_name': 'my_update'" not in record.message # Clear queue and start a new one with more signals capturer.log_queue.queue.clear() @@ -2428,7 +3022,7 @@ async def test_workflow_dataclass_typed(client: Client, env: WorkflowEnvironment # TODO(cretz): Fix if env.supports_time_skipping: pytest.skip( - "Java test server: https://github.com/temporalio/sdk-core/issues/390" + "Java test server: https://github.com/temporalio/sdk-rust/issues/390" ) async with new_worker( client, DataClassTypedWorkflow, activities=[data_class_typed_activity] @@ -2877,8 +3471,8 @@ def __init__(self, *, should_patch: bool = False) -> None: self._waiting_signal = True @workflow.run - async def run(self) -> List[str]: - results: List[str] = [] + async def run(self) -> list[str]: + results: list[str] = [] if self.should_patch and workflow.patched("some-patch"): results.append("pre-patch") self._waiting_signal = True @@ -2903,7 +3497,7 @@ def __init__(self) -> None: super().__init__(should_patch=True) @workflow.run - async def run(self) -> List[str]: + async def run(self) -> list[str]: return await super().run() @@ -2946,12 +3540,21 @@ async def waiting_signal() -> bool: task_queue=task_queue, ) + # Need to wait until it has gotten halfway through, otherwise the post_patch workflow may never complete + async def waiting_signal() -> bool: + return await post_patch_handle.query( + PatchMemoizedWorkflowPatched.waiting_signal + ) + + await assert_eq_eventually(True, waiting_signal) + # Send signal to both and check results await pre_patch_handle.signal(PatchMemoizedWorkflowUnpatched.signal) await post_patch_handle.signal(PatchMemoizedWorkflowPatched.signal) # Confirm expected values assert ["some-value"] == await pre_patch_handle.result() + assert [ "pre-patch", "some-value", @@ -2960,1148 +3563,1528 @@ async def waiting_signal() -> bool: @workflow.defn -class UUIDWorkflow: +class PatchActivationWorkflow: + @workflow.run + async def run(self, patch_id: str, sleep_after_first: bool = False) -> list[bool]: + first = workflow.patched(patch_id) + if sleep_after_first: + await workflow.sleep(0.001) + return [first, workflow.patched(patch_id)] + + +@workflow.defn +class PatchActivationDeprecateWorkflow: + @workflow.run + async def run(self, patch_id: str) -> bool: + workflow.deprecate_patch(patch_id) + return workflow.patched(patch_id) + + +@workflow.defn(name="PatchActivationRolloutWorkflow") +class PatchActivationRolloutWorkflow: def __init__(self) -> None: - self._result = "" + self._ready = False + self._released = False @workflow.run - async def run(self) -> None: - self._result = str(workflow.uuid4()) + async def run(self) -> str: + workflow.patched("rollout-patch") + self._ready = True + await workflow.wait_condition(lambda: self._released) + return "new" if workflow.patched("rollout-patch") else "old" @workflow.query - def result(self) -> str: - return self._result + def ready(self) -> bool: + return self._ready + @workflow.signal + def release(self) -> None: + self._released = True -async def test_workflow_uuid(client: Client): - task_queue = str(uuid.uuid4()) - async with new_worker( - client, UUIDWorkflow, task_queue=task_queue, max_cached_workflows=0 - ): - # Get two handle UUID results. Need to disable workflow cache since we - # restart the worker and don't want to pay the sticky queue penalty. - handle1 = await client.start_workflow( - UUIDWorkflow.run, id=f"workflow-{uuid.uuid4()}", task_queue=task_queue - ) - await handle1.result() - handle1_query_result = await handle1.query(UUIDWorkflow.result) - handle2 = await client.start_workflow( - UUIDWorkflow.run, - id=f"workflow-{uuid.uuid4()}", - task_queue=task_queue, - ) - await handle2.result() - handle2_query_result = await handle2.query(UUIDWorkflow.result) +@workflow.defn(name="PatchActivationRolloutWorkflow") +class PatchActivationOldRolloutWorkflow: + def __init__(self) -> None: + self._ready = False + self._released = False - # Confirm they aren't equal to each other but they are equal to retries - # of the same query - assert handle1_query_result != handle2_query_result - assert handle1_query_result == await handle1.query(UUIDWorkflow.result) - assert handle2_query_result == await handle2.query(UUIDWorkflow.result) + @workflow.run + async def run(self) -> str: + self._ready = True + await workflow.wait_condition(lambda: self._released) + return "old" - # Now confirm those results are the same even on a new worker - async with new_worker( - client, UUIDWorkflow, task_queue=task_queue, max_cached_workflows=0 - ): - assert handle1_query_result == await handle1.query(UUIDWorkflow.result) - assert handle2_query_result == await handle2.query(UUIDWorkflow.result) + @workflow.query + def ready(self) -> bool: + return self._ready + @workflow.signal + def release(self) -> None: + self._released = True -@activity.defn(name="custom-name") -class CallableClassActivity: - def __init__(self, orig_field1: str) -> None: - self.orig_field1 = orig_field1 - async def __call__(self, to_add: MyDataClass) -> MyDataClass: - return MyDataClass(field1=self.orig_field1 + to_add.field1) +async def patch_marker_count(handle: WorkflowHandle) -> int: + count = 0 + async for event in handle.fetch_history_events(): + if event.event_type is EventType.EVENT_TYPE_MARKER_RECORDED: + count += 1 + return count -@workflow.defn -class ActivityCallableClassWorkflow: - @workflow.run - async def run(self, to_add: MyDataClass) -> MyDataClass: - result = await workflow.execute_activity_class( - CallableClassActivity, to_add, start_to_close_timeout=timedelta(seconds=30) - ) - assert isinstance(result, MyDataClass) - return result +async def has_completed_workflow_task(handle: WorkflowHandle) -> bool: + async for event in handle.fetch_history_events(): + if event.event_type is EventType.EVENT_TYPE_WORKFLOW_TASK_COMPLETED: + return True + return False -async def test_workflow_activity_callable_class(client: Client): - activity_instance = CallableClassActivity("in worker") +def recording_patch_activation_callback( + calls: list[temporalio.worker.PatchActivationInput], decision: bool +) -> typing.Callable[[temporalio.worker.PatchActivationInput], bool]: + def callback(input: temporalio.worker.PatchActivationInput) -> bool: + calls.append(input) + return decision + + return callback + + +async def test_workflow_patch_activation_callback(client: Client): + workflow_id = f"workflow-{uuid.uuid4()}" + calls: list[temporalio.worker.PatchActivationInput] = [] async with new_worker( - client, ActivityCallableClassWorkflow, activities=[activity_instance] + client, + PatchActivationWorkflow, + patch_activation_callback=recording_patch_activation_callback(calls, True), ) as worker: result = await client.execute_workflow( - ActivityCallableClassWorkflow.run, - MyDataClass(field1=", workflow param"), - id=f"workflow-{uuid.uuid4()}", + PatchActivationWorkflow.run, + args=["my-patch", False], + id=workflow_id, task_queue=worker.task_queue, ) - assert result == MyDataClass(field1="in worker, workflow param") - - -async def test_workflow_activity_callable_class_bad_register(client: Client): - # Try to register the class instead of the instance - with pytest.raises(TypeError) as err: - new_worker( - client, ActivityCallableClassWorkflow, activities=[CallableClassActivity] - ) - assert "is a class instead of an instance" in str(err.value) + assert result == [True, True] + assert len(calls) == 1 + assert calls[0].workflow_info.workflow_id == workflow_id + assert calls[0].patch_id == "my-patch" -class MethodActivity: - def __init__(self, orig_field1: str) -> None: - self.orig_field1 = orig_field1 - @activity.defn(name="custom-name") - async def add(self, to_add: MyDataClass) -> MyDataClass: - return MyDataClass(field1=self.orig_field1 + to_add.field1) +async def test_workflow_patch_activation_callback_can_decline(client: Client): + calls: list[temporalio.worker.PatchActivationInput] = [] + async with new_worker( + client, + PatchActivationWorkflow, + patch_activation_callback=recording_patch_activation_callback(calls, False), + ) as worker: + handle = await client.start_workflow( + PatchActivationWorkflow.run, + args=["my-patch", False], + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + assert await handle.result() == [False, False] - @activity.defn - async def add_multi(self, source: MyDataClass, to_add: str) -> MyDataClass: - return MyDataClass(field1=source.field1 + to_add) + assert len(calls) == 1 + assert await patch_marker_count(handle) == 0 -@workflow.defn -class ActivityMethodWorkflow: - @workflow.run - async def run(self, to_add: MyDataClass) -> MyDataClass: - ret = await workflow.execute_activity_method( - MethodActivity.add, to_add, start_to_close_timeout=timedelta(seconds=30) - ) - return await workflow.execute_activity_method( - MethodActivity.add_multi, - args=[ret, ", in workflow"], - start_to_close_timeout=timedelta(seconds=30), +async def test_workflow_patch_activation_default_activates(client: Client): + async with new_worker(client, PatchActivationWorkflow) as worker: + handle = await client.start_workflow( + PatchActivationWorkflow.run, + args=["my-patch", False], + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, ) + assert await handle.result() == [True, True] + assert await patch_marker_count(handle) == 1 -async def test_workflow_activity_method(client: Client): - activity_instance = MethodActivity("in worker") + +async def test_workflow_patch_activation_callback_not_recalled_on_replay( + client: Client, +): + calls: list[temporalio.worker.PatchActivationInput] = [] async with new_worker( client, - ActivityMethodWorkflow, - activities=[activity_instance.add, activity_instance.add_multi], + PatchActivationWorkflow, + max_cached_workflows=0, + patch_activation_callback=recording_patch_activation_callback(calls, False), ) as worker: result = await client.execute_workflow( - ActivityMethodWorkflow.run, - MyDataClass(field1=", workflow param"), + PatchActivationWorkflow.run, + args=["my-patch", True], id=f"workflow-{uuid.uuid4()}", task_queue=worker.task_queue, ) - assert result == MyDataClass(field1="in worker, workflow param, in workflow") - - -@workflow.defn -class WaitConditionTimeoutWorkflow: - def __init__(self) -> None: - self._done = False - self._waiting = False - - @workflow.run - async def run(self) -> None: - # Force timeout, ignore, wait again - try: - await workflow.wait_condition( - lambda: self._done, timeout=0.01, timeout_summary="hi!" - ) - raise RuntimeError("Expected timeout") - except asyncio.TimeoutError: - pass - self._waiting = True - await workflow.wait_condition(lambda: self._done) + assert result == [False, False] - @workflow.signal - def done(self) -> None: - self._done = True + assert len(calls) == 1 - @workflow.query - def waiting(self) -> bool: - return self._waiting +async def test_workflow_patch_activation_callback_bypassed_for_deprecate( + client: Client, +): + def unexpected_callback(_: temporalio.worker.PatchActivationInput) -> bool: + raise AssertionError("Patch activation callback should not be called") -async def test_workflow_wait_condition_timeout(client: Client): async with new_worker( client, - WaitConditionTimeoutWorkflow, + PatchActivationDeprecateWorkflow, + patch_activation_callback=unexpected_callback, ) as worker: - handle = await client.start_workflow( - WaitConditionTimeoutWorkflow.run, + result = await client.execute_workflow( + PatchActivationDeprecateWorkflow.run, + "my-patch", id=f"workflow-{uuid.uuid4()}", task_queue=worker.task_queue, ) - # Wait until it's waiting, then send the signal - async def waiting() -> bool: - return await handle.query(WaitConditionTimeoutWorkflow.waiting) - - await assert_eq_eventually(True, waiting) - await handle.signal(WaitConditionTimeoutWorkflow.done) - # Wait for result which should succeed - await handle.result() - - -@workflow.defn -class HelloWorkflowWithQuery: - @workflow.run - async def run(self, name: str) -> str: - return f"Hello, {name}!" + assert result is True - @workflow.query - def some_query(self) -> str: - return "some value" +async def test_workflow_patch_activation_callback_must_return_bool(client: Client): + def invalid_callback(_: temporalio.worker.PatchActivationInput) -> bool: + return "not a bool" # type: ignore[return-value] # pyright: ignore[reportReturnType] -async def test_workflow_query_rpc_timeout(client: Client): - # Run workflow under worker and confirm query works async with new_worker( client, - HelloWorkflowWithQuery, + PatchActivationWorkflow, + patch_activation_callback=invalid_callback, ) as worker: handle = await client.start_workflow( - HelloWorkflowWithQuery.run, - "Temporal", + PatchActivationWorkflow.run, + args=["my-patch", False], id=f"workflow-{uuid.uuid4()}", task_queue=worker.task_queue, ) - assert "Hello, Temporal!" == await handle.result() - assert "some value" == await handle.query(HelloWorkflowWithQuery.some_query) + await assert_task_fail_eventually( + handle, + message_contains="Patch activation callback must return true or false", + ) - # Now with the worker stopped, issue a query with a one second timeout - with pytest.raises(RPCError) as err: - await handle.query( - HelloWorkflowWithQuery.some_query, rpc_timeout=timedelta(seconds=1) - ) - assert ( - err.value.status == RPCStatusCode.CANCELLED - and "timeout" in str(err.value).lower() - ) or err.value.status == RPCStatusCode.DEADLINE_EXCEEDED +async def test_workflow_patch_activation_callback_is_read_only(client: Client): + def make_command(_: temporalio.worker.PatchActivationInput) -> bool: + workflow.upsert_memo({"foo": "bar"}) + return True -@dataclass -class TypedHandleResponse: - field1: str + def schedule_task(_: temporalio.worker.PatchActivationInput) -> bool: + asyncio.get_running_loop().call_soon(lambda: None) + return True + def wait_condition(_: temporalio.worker.PatchActivationInput) -> bool: + coroutine = workflow.wait_condition(lambda: True) + try: + coroutine.send(None) + finally: + coroutine.close() + return True -@workflow.defn -class TypedHandleWorkflow: - @workflow.run - async def run(self) -> TypedHandleResponse: - return TypedHandleResponse(field1="foo") + def use_random(_: temporalio.worker.PatchActivationInput) -> bool: + workflow.random().random() + return True + def register_random_seed_callback( + _: temporalio.worker.PatchActivationInput, + ) -> bool: + workflow.register_random_seed_callback(lambda _seed: None) + return True -async def test_workflow_typed_handle(client: Client): - async with new_worker(client, TypedHandleWorkflow) as worker: - # Run the workflow then get a typed handle for it and confirm response - # type is as expected - id = f"workflow-{uuid.uuid4()}" - await client.execute_workflow( - TypedHandleWorkflow.run, id=id, task_queue=worker.task_queue - ) - handle_result: TypedHandleResponse = await client.get_workflow_handle_for( - TypedHandleWorkflow.run, # type: ignore[arg-type] - id, - ).result() - assert isinstance(handle_result, TypedHandleResponse) + def create_new_random(_: temporalio.worker.PatchActivationInput) -> bool: + workflow.new_random() + return True + callbacks = [ + (make_command, "action attempted: add command"), + (schedule_task, "action attempted: schedule task"), + (wait_condition, "action attempted: wait condition"), + (use_random, "action attempted: random"), + ( + register_random_seed_callback, + "action attempted: register random seed callback", + ), + (create_new_random, "action attempted: register random seed callback"), + ] + for callback, message in callbacks: + async with new_worker( + client, + PatchActivationWorkflow, + patch_activation_callback=callback, + ) as worker: + handle = await client.start_workflow( + PatchActivationWorkflow.run, + args=["my-patch", False], + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await assert_task_fail_eventually(handle, message_contains=message) -@dataclass -class MemoValue: - field1: str +async def test_workflow_declined_patch_rolls_out_to_old_worker(client: Client): + task_queue = f"tq-{uuid.uuid4()}" + calls: list[temporalio.worker.PatchActivationInput] = [] + async with new_worker( + client, + PatchActivationRolloutWorkflow, + task_queue=task_queue, + max_cached_workflows=0, + patch_activation_callback=recording_patch_activation_callback(calls, False), + ): + handle = await client.start_workflow( + PatchActivationRolloutWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=task_queue, + ) + await assert_eq_eventually(True, lambda: has_completed_workflow_task(handle)) -@workflow.defn -class MemoWorkflow: - @workflow.run - async def run(self, run_child: bool) -> None: - expected_memo = { - "dict_memo": {"field1": "dict"}, - "dataclass_memo": {"field1": "data"}, - "changed_memo": {"field1": "old value"}, - "removed_memo": {"field1": "removed"}, - } + assert len(calls) == 1 + async with new_worker( + client, + PatchActivationOldRolloutWorkflow, + task_queue=task_queue, + max_cached_workflows=0, + ): + await handle.signal("release") + assert await handle.result() == "old" - # Test getting all memos (child) - # Alternating order of operations between parent and child workflow for more coverage - if run_child: - assert workflow.memo() == expected_memo - # Test getting single memo with and without type hint - assert workflow.memo_value("dict_memo", type_hint=MemoValue) == MemoValue( - field1="dict" - ) - assert workflow.memo_value("dict_memo") == {"field1": "dict"} - assert workflow.memo_value("dataclass_memo", type_hint=MemoValue) == MemoValue( - field1="data" +async def test_workflow_activated_patch_ignores_declining_worker(client: Client): + task_queue = f"tq-{uuid.uuid4()}" + activated_calls: list[temporalio.worker.PatchActivationInput] = [] + declining_calls: list[temporalio.worker.PatchActivationInput] = [] + async with new_worker( + client, + PatchActivationRolloutWorkflow, + task_queue=task_queue, + max_cached_workflows=0, + patch_activation_callback=recording_patch_activation_callback( + activated_calls, True + ), + ): + handle = await client.start_workflow( + PatchActivationRolloutWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=task_queue, ) - assert workflow.memo_value("dataclass_memo") == {"field1": "data"} - - # Test getting all memos (parent) - if not run_child: - assert workflow.memo() == expected_memo + await assert_eq_eventually(True, lambda: has_completed_workflow_task(handle)) - # Test missing value handling - with pytest.raises(KeyError): - workflow.memo_value("absent_memo", type_hint=MemoValue) - with pytest.raises(KeyError): - workflow.memo_value("absent_memo") + assert len(activated_calls) == 1 + async with new_worker( + client, + PatchActivationRolloutWorkflow, + task_queue=task_queue, + max_cached_workflows=0, + patch_activation_callback=recording_patch_activation_callback( + declining_calls, False + ), + ): + await handle.signal(PatchActivationRolloutWorkflow.release) + assert await handle.result() == "new" - # Test default value handling - assert ( - workflow.memo_value("absent_memo", "default value", type_hint=MemoValue) - == "default value" - ) - assert workflow.memo_value("absent_memo", "default value") == "default value" - assert workflow.memo_value( - "dict_memo", "default value", type_hint=MemoValue - ) == MemoValue(field1="dict") - assert workflow.memo_value("dict_memo", "default value") == {"field1": "dict"} + assert not declining_calls - # Saving original memo to pass to child workflow - old_memo = dict(workflow.memo()) - # Test upsert - assert workflow.memo_value("changed_memo", type_hint=MemoValue) == MemoValue( - field1="old value" - ) - assert workflow.memo_value("removed_memo", type_hint=MemoValue) == MemoValue( - field1="removed" - ) - with pytest.raises(KeyError): - workflow.memo_value("added_memo", type_hint=MemoValue) +@workflow.defn +class UUIDWorkflow: + def __init__(self) -> None: + self._result = "" - workflow.upsert_memo( - { - "changed_memo": MemoValue(field1="new value"), - "added_memo": MemoValue(field1="added"), - "removed_memo": None, - } - ) + @workflow.run + async def run(self) -> None: + self._result = str(workflow.uuid4()) - assert workflow.memo_value("changed_memo", type_hint=MemoValue) == MemoValue( - field1="new value" - ) - assert workflow.memo_value("added_memo", type_hint=MemoValue) == MemoValue( - field1="added" - ) - with pytest.raises(KeyError): - workflow.memo_value("removed_memo", type_hint=MemoValue) + @workflow.query + def result(self) -> str: + return self._result - # Run second time as child workflow - if run_child: - await workflow.execute_child_workflow( - MemoWorkflow.run, False, memo=old_memo - ) +async def test_workflow_uuid(client: Client): + task_queue = str(uuid.uuid4()) + async with new_worker( + client, UUIDWorkflow, task_queue=task_queue, max_cached_workflows=0 + ): + # Get two handle UUID results. Need to disable workflow cache since we + # restart the worker and don't want to pay the sticky queue penalty. + handle1 = await client.start_workflow( + UUIDWorkflow.run, id=f"workflow-{uuid.uuid4()}", task_queue=task_queue + ) + await handle1.result() + handle1_query_result = await handle1.query(UUIDWorkflow.result) -async def test_workflow_memo(client: Client): - async with new_worker(client, MemoWorkflow) as worker: - # Run workflow - handle = await client.start_workflow( - MemoWorkflow.run, - True, + handle2 = await client.start_workflow( + UUIDWorkflow.run, id=f"workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - memo={ - "dict_memo": {"field1": "dict"}, - "dataclass_memo": MemoValue(field1="data"), - "changed_memo": MemoValue(field1="old value"), - "removed_memo": MemoValue(field1="removed"), - }, + task_queue=task_queue, ) - await handle.result() - desc = await handle.describe() - # Check untyped memo - assert (await desc.memo()) == { - "dict_memo": {"field1": "dict"}, - "dataclass_memo": {"field1": "data"}, - "changed_memo": {"field1": "new value"}, - "added_memo": {"field1": "added"}, - } - # Check typed memo - assert ( - await desc.memo_value("dataclass_memo", type_hint=MemoValue) - ) == MemoValue(field1="data") - # Check default - assert ( - await desc.memo_value("absent_memo", "default value") - ) == "default value" - # Check key error - with pytest.raises(KeyError): - await desc.memo_value("absent_memo") + await handle2.result() + handle2_query_result = await handle2.query(UUIDWorkflow.result) + + # Confirm they aren't equal to each other but they are equal to retries + # of the same query + assert handle1_query_result != handle2_query_result + assert handle1_query_result == await handle1.query(UUIDWorkflow.result) + assert handle2_query_result == await handle2.query(UUIDWorkflow.result) + + # Now confirm those results are the same even on a new worker + async with new_worker( + client, UUIDWorkflow, task_queue=task_queue, max_cached_workflows=0 + ): + assert handle1_query_result == await handle1.query(UUIDWorkflow.result) + assert handle2_query_result == await handle2.query(UUIDWorkflow.result) @workflow.defn -class QueryAffectConditionWorkflow: +class UUID7Workflow: def __init__(self) -> None: - self.seen_query = False + self._result = "" + self._time_ms = -1 @workflow.run async def run(self) -> None: - def condition_never_after_query(): - assert not self.seen_query - return False + self._time_ms = workflow.time_ns() // 1_000_000 + self._result = str(workflow.uuid7()) - while True: - await workflow.wait_condition(condition_never_after_query) + @workflow.query + def result(self) -> str: + return self._result @workflow.query - def check_condition(self) -> bool: - # This is a bad thing, to mutate a workflow during a query, this is just - # for this test - self.seen_query = True - return True + def time_ms(self) -> int: + return self._time_ms -async def test_workflow_query_does_not_run_condition(client: Client): - async with new_worker(client, QueryAffectConditionWorkflow) as worker: - handle = await client.start_workflow( - QueryAffectConditionWorkflow.run, - id=f"workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, +async def test_workflow_uuid7(client: Client): + task_queue = str(uuid.uuid4()) + async with new_worker( + client, UUID7Workflow, task_queue=task_queue, max_cached_workflows=0 + ): + # Get two handle UUID results. Need to disable workflow cache since we + # restart the worker and don't want to pay the sticky queue penalty. + handle1 = await client.start_workflow( + UUID7Workflow.run, id=f"workflow-{uuid.uuid4()}", task_queue=task_queue ) - assert await handle.query(QueryAffectConditionWorkflow.check_condition) - + await handle1.result() + handle1_query_result = await handle1.query(UUID7Workflow.result) -@workflow.defn -class CancelSignalAndTimerFiredInSameTaskWorkflow: - timer_task: asyncio.Task[None] # type: ignore[reportUninitializedInstanceVariable] + handle2 = await client.start_workflow( + UUID7Workflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=task_queue, + ) + await handle2.result() + handle2_query_result = await handle2.query(UUID7Workflow.result) + + # Confirm they aren't equal to each other but they are equal to retries + # of the same query + assert handle1_query_result != handle2_query_result + assert handle1_query_result == await handle1.query(UUID7Workflow.result) + assert handle2_query_result == await handle2.query(UUID7Workflow.result) + + # Confirm RFC 9562 shape: version 7, RFC variant, and the leading 48 + # bits are the workflow time in milliseconds at generation + for handle, query_result in ( + (handle1, handle1_query_result), + (handle2, handle2_query_result), + ): + result_uuid = uuid.UUID(query_result) + assert result_uuid.version == 7 + assert result_uuid.variant == uuid.RFC_4122 + workflow_time_ms = await handle.query(UUID7Workflow.time_ms) + assert int(result_uuid) >> 80 == workflow_time_ms + + # Now confirm those results are the same even on a new worker + async with new_worker( + client, UUID7Workflow, task_queue=task_queue, max_cached_workflows=0 + ): + assert handle1_query_result == await handle1.query(UUID7Workflow.result) + assert handle2_query_result == await handle2.query(UUID7Workflow.result) + +@activity.defn(name="custom-name") +class CallableClassActivity: + def __init__(self, orig_field1: str) -> None: + self.orig_field1 = orig_field1 + + async def __call__(self, to_add: MyDataClass) -> MyDataClass: + return MyDataClass(field1=self.orig_field1 + to_add.field1) + + +@workflow.defn +class ActivityCallableClassWorkflow: @workflow.run - async def run(self) -> None: - # Start a 1 hour timer - self.timer_task = asyncio.create_task(asyncio.sleep(60 * 60)) - # Wait on it - try: - await self.timer_task - assert False - except asyncio.CancelledError: - pass + async def run(self, to_add: MyDataClass) -> MyDataClass: + result = await workflow.execute_activity_class( + CallableClassActivity, to_add, start_to_close_timeout=timedelta(seconds=30) + ) + assert isinstance(result, MyDataClass) + return result - @workflow.signal - def cancel_timer(self) -> None: - self.timer_task.cancel() +async def test_workflow_activity_callable_class(client: Client): + activity_instance = CallableClassActivity("in worker") + async with new_worker( + client, ActivityCallableClassWorkflow, activities=[activity_instance] + ) as worker: + result = await client.execute_workflow( + ActivityCallableClassWorkflow.run, + MyDataClass(field1=", workflow param"), + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + assert result == MyDataClass(field1="in worker, workflow param") -async def test_workflow_cancel_signal_and_timer_fired_in_same_task( - client: Client, env: WorkflowEnvironment -): - # This test only works when we support time skipping - if not env.supports_time_skipping: - pytest.skip("Need to skip time to validate this test") - # TODO(cretz): There is a bug in the Java test server, probably - # https://github.com/temporalio/sdk-java/issues/1138 where the first - # unlock-and-sleep hangs when running this test after - # test_workflow_cancel_activity. So we create a new test environment here. - async with await WorkflowEnvironment.start_time_skipping() as env: - # Start worker for 30 mins. Need to disable workflow cache since we - # restart the worker and don't want to pay the sticky queue penalty. - async with new_worker( - client, CancelSignalAndTimerFiredInSameTaskWorkflow, max_cached_workflows=0 - ) as worker: - task_queue = worker.task_queue - handle = await client.start_workflow( - CancelSignalAndTimerFiredInSameTaskWorkflow.run, - id=f"workflow-{uuid.uuid4()}", - task_queue=task_queue, - ) - # Wait 30 mins so the worker is waiting on timer - await env.sleep(30 * 60) +async def test_workflow_activity_callable_class_bad_register(client: Client): + # Try to register the class instead of the instance + with pytest.raises(TypeError) as err: + new_worker( + client, ActivityCallableClassWorkflow, activities=[CallableClassActivity] + ) + assert "is a class instead of an instance" in str(err.value) - # Listen to handler result in background so the auto-skipping works - result_task = asyncio.create_task(handle.result()) - # Now that worker is stopped, send a signal and wait another hour to pass - # the timer - await handle.signal(CancelSignalAndTimerFiredInSameTaskWorkflow.cancel_timer) - await env.sleep(60 * 60) +class MethodActivity: + def __init__(self, orig_field1: str) -> None: + self.orig_field1 = orig_field1 - # Start worker again and wait for workflow completion - async with new_worker( - client, - CancelSignalAndTimerFiredInSameTaskWorkflow, - task_queue=task_queue, - max_cached_workflows=0, - ): - # This used to not complete because a signal cancelling the timer was - # not respected by the timer fire - await result_task + @activity.defn(name="custom-name") + async def add(self, to_add: MyDataClass) -> MyDataClass: + return MyDataClass(field1=self.orig_field1 + to_add.field1) + + @activity.defn + async def add_multi(self, source: MyDataClass, to_add: str) -> MyDataClass: + return MyDataClass(field1=source.field1 + to_add) -class MyCustomError(ApplicationError): - def __init__(self, message: str) -> None: - super().__init__(message, type="MyCustomError", non_retryable=True) +@workflow.defn +class ActivityMethodWorkflow: + @workflow.run + async def run(self, to_add: MyDataClass) -> MyDataClass: + ret = await workflow.execute_activity_method( + MethodActivity.add, to_add, start_to_close_timeout=timedelta(seconds=30) + ) + return await workflow.execute_activity_method( + MethodActivity.add_multi, + args=[ret, ", in workflow"], + start_to_close_timeout=timedelta(seconds=30), + ) -@activity.defn -async def custom_error_activity() -> NoReturn: - raise MyCustomError("activity error!") +async def test_workflow_activity_method(client: Client): + activity_instance = MethodActivity("in worker") + async with new_worker( + client, + ActivityMethodWorkflow, + activities=[activity_instance.add, activity_instance.add_multi], + ) as worker: + result = await client.execute_workflow( + ActivityMethodWorkflow.run, + MyDataClass(field1=", workflow param"), + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + assert result == MyDataClass(field1="in worker, workflow param, in workflow") @workflow.defn -class CustomErrorWorkflow: +class WaitConditionTimeoutWorkflow: + def __init__(self) -> None: + self._done = False + self._waiting = False + @workflow.run - async def run(self) -> NoReturn: + async def run(self) -> None: + # Force timeout, ignore, wait again try: - await workflow.execute_activity( - custom_error_activity, schedule_to_close_timeout=timedelta(seconds=30) + await workflow.wait_condition( + lambda: self._done, timeout=0.01, timeout_summary="hi!" ) - except ActivityError: - raise MyCustomError("workflow error!") - raise RuntimeError("Unreachable") - + raise RuntimeError("Expected timeout") + except asyncio.TimeoutError: + pass + self._waiting = True + await workflow.wait_condition(lambda: self._done) -class CustomFailureConverter(DefaultFailureConverterWithEncodedAttributes): - # We'll override from failure to convert back to our type - def from_failure( - self, failure: Failure, payload_converter: PayloadConverter - ) -> BaseException: - err = super().from_failure(failure, payload_converter) - if isinstance(err, ApplicationError) and err.type == "MyCustomError": - my_err = MyCustomError(err.message) - my_err.__cause__ = err.__cause__ - err = my_err - return err + @workflow.signal + def done(self) -> None: + self._done = True + @workflow.query + def waiting(self) -> bool: + return self._waiting -async def test_workflow_custom_failure_converter(client: Client): - # Clone the client but change the data converter to use our failure - # converter - config = client.config() - config["data_converter"] = dataclasses.replace( - config["data_converter"], - failure_converter_class=CustomFailureConverter, - ) - client = Client(**config) - # Run workflow and confirm error +async def test_workflow_wait_condition_timeout(client: Client): async with new_worker( - client, CustomErrorWorkflow, activities=[custom_error_activity] + client, + WaitConditionTimeoutWorkflow, ) as worker: handle = await client.start_workflow( - CustomErrorWorkflow.run, + WaitConditionTimeoutWorkflow.run, id=f"workflow-{uuid.uuid4()}", task_queue=worker.task_queue, ) - with pytest.raises(WorkflowFailureError) as err: - await handle.result() - - # Check error is as expected - assert isinstance(err.value.cause, MyCustomError) - assert err.value.cause.message == "workflow error!" - assert isinstance(err.value.cause.cause, ActivityError) - assert isinstance(err.value.cause.cause.cause, MyCustomError) - assert err.value.cause.cause.cause.message == "activity error!" - assert err.value.cause.cause.cause.cause is None - - # Check in history it is encoded - failure = ( - (await handle.fetch_history()) - .events[-1] - .workflow_execution_failed_event_attributes.failure - ) - assert failure.application_failure_info.type == "MyCustomError" - while True: - assert failure.message == "Encoded failure" - assert failure.stack_trace == "" - attrs: Dict[str, Any] = PayloadConverter.default.from_payloads( - [failure.encoded_attributes] - )[0] - assert "message" in attrs - assert "stack_trace" in attrs - if not failure.HasField("cause"): - break - failure = failure.cause + # Wait until it's waiting, then send the signal + async def waiting() -> bool: + return await handle.query(WaitConditionTimeoutWorkflow.waiting) -@dataclass -class OptionalParam: - some_string: str + await assert_eq_eventually(True, waiting) + await handle.signal(WaitConditionTimeoutWorkflow.done) + # Wait for result which should succeed + await handle.result() @workflow.defn -class OptionalParamWorkflow: +class HelloWorkflowWithQuery: @workflow.run - async def run( - self, some_param: Optional[OptionalParam] = OptionalParam(some_string="default") - ) -> Optional[OptionalParam]: - assert some_param is None or ( - isinstance(some_param, OptionalParam) - and some_param.some_string in ["default", "foo"] - ) - return some_param + async def run(self, name: str) -> str: + return f"Hello, {name}!" + @workflow.query + def some_query(self) -> str: + return "some value" -async def test_workflow_optional_param(client: Client): - async with new_worker(client, OptionalParamWorkflow) as worker: - # Don't send a parameter and confirm it is defaulted - result1 = await client.execute_workflow( - "OptionalParamWorkflow", + +async def test_workflow_query_rpc_timeout(client: Client): + # Run workflow under worker and confirm query works + async with new_worker( + client, + HelloWorkflowWithQuery, + ) as worker: + handle = await client.start_workflow( + HelloWorkflowWithQuery.run, + "Temporal", id=f"workflow-{uuid.uuid4()}", task_queue=worker.task_queue, - result_type=OptionalParam, ) - assert result1 == OptionalParam(some_string="default") - # Send None explicitly - result2 = await client.execute_workflow( - OptionalParamWorkflow.run, - None, - id=f"workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, + assert "Hello, Temporal!" == await handle.result() + assert "some value" == await handle.query(HelloWorkflowWithQuery.some_query) + + # Now with the worker stopped, issue a query with a one second timeout + with pytest.raises(RPCError) as err: + await handle.query( + HelloWorkflowWithQuery.some_query, rpc_timeout=timedelta(seconds=1) ) - assert result2 is None - # Send param explicitly - result3 = await client.execute_workflow( - OptionalParamWorkflow.run, - OptionalParam(some_string="foo"), - id=f"workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, + assert ( + err.value.status == RPCStatusCode.CANCELLED + and ( + "timeout" in str(err.value).lower() + or "http2 error" in str(err.value).lower() ) - assert result3 == OptionalParam(some_string="foo") - - -class ExceptionRaisingPayloadConverter(DefaultPayloadConverter): - bad_outbound_str = "bad-outbound-payload-str" - bad_inbound_str = "bad-inbound-payload-str" + ) or err.value.status == RPCStatusCode.DEADLINE_EXCEEDED - def to_payloads(self, values: Sequence[Any]) -> List[Payload]: - if any( - value == ExceptionRaisingPayloadConverter.bad_outbound_str - for value in values - ): - raise ApplicationError("Intentional outbound converter failure") - return super().to_payloads(values) - def from_payloads( - self, payloads: Sequence[Payload], type_hints: Optional[List] = None - ) -> List[Any]: - # Check if any payloads contain the bad data - for payload in payloads: - if ( - ExceptionRaisingPayloadConverter.bad_inbound_str.encode() - in payload.data - ): - raise ApplicationError("Intentional inbound converter failure") - return super().from_payloads(payloads, type_hints) +@dataclass +class TypedHandleResponse: + field1: str @workflow.defn -class ExceptionRaisingConverterWorkflow: +class TypedHandleWorkflow: @workflow.run - async def run(self, some_param: str) -> str: - return some_param + async def run(self) -> TypedHandleResponse: + return TypedHandleResponse(field1="foo") -async def test_exception_raising_converter_param(client: Client): - # Clone the client but change the data converter to use our converter - config = client.config() - config["data_converter"] = dataclasses.replace( - config["data_converter"], - payload_converter_class=ExceptionRaisingPayloadConverter, - ) - client = Client(**config) +async def test_workflow_typed_handle(client: Client): + async with new_worker(client, TypedHandleWorkflow) as worker: + # Run the workflow then get a typed handle for it and confirm response + # type is as expected + id = f"workflow-{uuid.uuid4()}" + await client.execute_workflow( + TypedHandleWorkflow.run, id=id, task_queue=worker.task_queue + ) + handle_result: TypedHandleResponse = await client.get_workflow_handle_for( + TypedHandleWorkflow.run, # type: ignore[arg-type] + id, + ).result() + assert isinstance(handle_result, TypedHandleResponse) - # Run workflow and confirm error - async with new_worker(client, ExceptionRaisingConverterWorkflow) as worker: - with pytest.raises(WorkflowFailureError) as err: - await client.execute_workflow( - ExceptionRaisingConverterWorkflow.run, - ExceptionRaisingPayloadConverter.bad_inbound_str, - id=f"workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) - assert isinstance(err.value.cause, ApplicationError) - assert "Intentional inbound converter failure" in str(err.value.cause) + +@dataclass +class MemoValue: + field1: str @workflow.defn -class ActivityOutboundConversionFailureWorkflow: +class MemoWorkflow: @workflow.run - async def run(self) -> None: - await workflow.execute_activity( - "some-activity", - ExceptionRaisingPayloadConverter.bad_outbound_str, - start_to_close_timeout=timedelta(seconds=10), - ) + async def run(self, run_child: bool) -> None: + expected_memo = { + "dict_memo": {"field1": "dict"}, + "dataclass_memo": {"field1": "data"}, + "changed_memo": {"field1": "old value"}, + "removed_memo": {"field1": "removed"}, + } + # Test getting all memos (child) + # Alternating order of operations between parent and child workflow for more coverage + if run_child: + assert workflow.memo() == expected_memo -async def test_workflow_activity_outbound_conversion_failure(client: Client): - # This test used to fail because we created commands _before_ we attempted - # to convert the arguments thereby causing half-built commands to get sent - # to the server. + # Test getting single memo with and without type hint + assert workflow.memo_value("dict_memo", type_hint=MemoValue) == MemoValue( + field1="dict" + ) + assert workflow.memo_value("dict_memo") == {"field1": "dict"} + assert workflow.memo_value("dataclass_memo", type_hint=MemoValue) == MemoValue( + field1="data" + ) + assert workflow.memo_value("dataclass_memo") == {"field1": "data"} - # Clone the client but change the data converter to use our converter - config = client.config() - config["data_converter"] = dataclasses.replace( - config["data_converter"], - payload_converter_class=ExceptionRaisingPayloadConverter, - ) - client = Client(**config) - async with new_worker(client, ActivityOutboundConversionFailureWorkflow) as worker: - with pytest.raises(WorkflowFailureError) as err: - await client.execute_workflow( - ActivityOutboundConversionFailureWorkflow.run, - id=f"wf-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) - assert isinstance(err.value.cause, ApplicationError) - assert "Intentional outbound converter failure" in str(err.value.cause) + # Test getting all memos (parent) + if not run_child: + assert workflow.memo() == expected_memo + # Test missing value handling + with pytest.raises(KeyError): + workflow.memo_value("absent_memo", type_hint=MemoValue) + with pytest.raises(KeyError): + workflow.memo_value("absent_memo") -@dataclass -class ManualResultType: - some_string: str + # Test default value handling + assert ( + workflow.memo_value("absent_memo", "default value", type_hint=MemoValue) + == "default value" + ) + assert workflow.memo_value("absent_memo", "default value") == "default value" + assert workflow.memo_value( + "dict_memo", "default value", type_hint=MemoValue + ) == MemoValue(field1="dict") + assert workflow.memo_value("dict_memo", "default value") == {"field1": "dict"} + # Saving original memo to pass to child workflow + old_memo = dict(workflow.memo()) -@activity.defn -async def manual_result_type_activity() -> ManualResultType: - return ManualResultType(some_string="from-activity") + # Test upsert + assert workflow.memo_value("changed_memo", type_hint=MemoValue) == MemoValue( + field1="old value" + ) + assert workflow.memo_value("removed_memo", type_hint=MemoValue) == MemoValue( + field1="removed" + ) + with pytest.raises(KeyError): + workflow.memo_value("added_memo", type_hint=MemoValue) + + workflow.upsert_memo( + { + "changed_memo": MemoValue(field1="new value"), + "added_memo": MemoValue(field1="added"), + "removed_memo": None, + } + ) + assert workflow.memo_value("changed_memo", type_hint=MemoValue) == MemoValue( + field1="new value" + ) + assert workflow.memo_value("added_memo", type_hint=MemoValue) == MemoValue( + field1="added" + ) + with pytest.raises(KeyError): + workflow.memo_value("removed_memo", type_hint=MemoValue) -@workflow.defn -class ManualResultTypeWorkflow: - @workflow.run - async def run(self) -> ManualResultType: - # Only check activity and child if not a child ourselves - if not workflow.info().parent: - # Activity without result type and with - res1 = await workflow.execute_activity( - "manual_result_type_activity", - schedule_to_close_timeout=timedelta(minutes=2), - ) - assert res1 == {"some_string": "from-activity"} - res2 = await workflow.execute_activity( - "manual_result_type_activity", - result_type=ManualResultType, - schedule_to_close_timeout=timedelta(minutes=2), - ) - assert res2 == ManualResultType(some_string="from-activity") - # Child without result type and with - res3 = await workflow.execute_child_workflow( - "ManualResultTypeWorkflow", - ) - assert res3 == {"some_string": "from-workflow"} - res4 = await workflow.execute_child_workflow( - "ManualResultTypeWorkflow", - result_type=ManualResultType, + # Run second time as child workflow + if run_child: + await workflow.execute_child_workflow( + MemoWorkflow.run, False, memo=old_memo ) - assert res4 == ManualResultType(some_string="from-workflow") - return ManualResultType(some_string="from-workflow") - - @workflow.query - def some_query(self) -> ManualResultType: - return ManualResultType(some_string="from-query") -async def test_manual_result_type(client: Client): - async with new_worker( - client, ManualResultTypeWorkflow, activities=[manual_result_type_activity] - ) as worker: - # Workflow without result type and with - res1 = await client.execute_workflow( - "ManualResultTypeWorkflow", - id=f"workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) - assert res1 == {"some_string": "from-workflow"} +async def test_workflow_memo(client: Client): + async with new_worker(client, MemoWorkflow) as worker: + # Run workflow handle = await client.start_workflow( - "ManualResultTypeWorkflow", + MemoWorkflow.run, + True, id=f"workflow-{uuid.uuid4()}", task_queue=worker.task_queue, - result_type=ManualResultType, - ) - res2 = await handle.result() - assert res2 == ManualResultType(some_string="from-workflow") - # Query without result type and with - res3 = await handle.query("some_query") - assert res3 == {"some_string": "from-query"} - res4 = await handle.query("some_query", result_type=ManualResultType) - assert res4 == ManualResultType(some_string="from-query") - - -@activity.defn -async def wait_forever_activity() -> None: - await asyncio.Future() - - -@workflow.defn -class WaitForeverWorkflow: - @workflow.run - async def run(self) -> None: - await asyncio.Future() + memo={ + "dict_memo": {"field1": "dict"}, + "dataclass_memo": MemoValue(field1="data"), + "changed_memo": MemoValue(field1="old value"), + "removed_memo": MemoValue(field1="removed"), + }, + ) + await handle.result() + desc = await handle.describe() + # Check untyped memo + assert (await desc.memo()) == { + "dict_memo": {"field1": "dict"}, + "dataclass_memo": {"field1": "data"}, + "changed_memo": {"field1": "new value"}, + "added_memo": {"field1": "added"}, + } + # Check typed memo + assert ( + await desc.memo_value("dataclass_memo", type_hint=MemoValue) + ) == MemoValue(field1="data") + # Check default + assert ( + await desc.memo_value("absent_memo", "default value") + ) == "default value" + # Check key error + with pytest.raises(KeyError): + await desc.memo_value("absent_memo") @workflow.defn -class CacheEvictionTearDownWorkflow: +class QueryAffectConditionWorkflow: def __init__(self) -> None: - self._signal_count = 0 + self.seen_query = False @workflow.run async def run(self) -> None: - # Start several things in background. This is just to show that eviction - # can work even with these things running. - tasks = [ - asyncio.create_task( - workflow.execute_activity( - wait_forever_activity, start_to_close_timeout=timedelta(hours=1) - ) - ), - asyncio.create_task( - workflow.execute_child_workflow(WaitForeverWorkflow.run) - ), - asyncio.create_task(asyncio.sleep(1000)), - asyncio.shield( - workflow.execute_activity( - wait_forever_activity, start_to_close_timeout=timedelta(hours=1) - ) - ), - asyncio.create_task(workflow.wait_condition(lambda: False)), - ] - gather_fut = asyncio.gather(*tasks, return_exceptions=True) - # Let's also start something in the background that we never wait on - asyncio.create_task(asyncio.sleep(1000)) - try: - # Wait for signal count to reach 2 - await asyncio.sleep(0.01) - await workflow.wait_condition(lambda: self._signal_count > 1) - finally: - # This finally, on eviction, is actually called but the command - # should be ignored - await asyncio.sleep(0.01) - await workflow.wait_condition(lambda: self._signal_count > 2) - # Cancel gather tasks and wait on them, but ignore the errors - for task in tasks: - task.cancel() - await gather_fut + def condition_never_after_query(): + assert not self.seen_query + return False - @workflow.signal - async def signal(self) -> None: - self._signal_count += 1 + while True: + await workflow.wait_condition(condition_never_after_query) @workflow.query - def signal_count(self) -> int: - return self._signal_count + def check_condition(self) -> bool: + # This is a bad thing, to mutate a workflow during a query, this is just + # for this test + self.seen_query = True + return True -async def test_cache_eviction_tear_down(client: Client): - # This test simulates forcing eviction. This used to raise GeneratorExit on - # GC which triggered the finally which could run on any thread Python - # chooses, but now we expect eviction to properly tear down tasks and - # therefore we cancel them - async with new_worker( - client, - CacheEvictionTearDownWorkflow, - WaitForeverWorkflow, - activities=[wait_forever_activity], - max_cached_workflows=0, - ) as worker: - # Put a hook to catch unraisable exceptions - old_hook = sys.unraisablehook - hook_calls: List[Any] = [] - sys.unraisablehook = hook_calls.append - try: - handle = await client.start_workflow( - CacheEvictionTearDownWorkflow.run, - id=f"wf-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) +@pytest.mark.parametrize("single_batch", [False, True]) +async def test_workflow_query_does_not_run_condition( + client: Client, single_batch: bool +): + worker = new_worker(client, QueryAffectConditionWorkflow) + if single_batch: + worker._set_default_workflow_logic_flag( + _SINGLE_BATCH_WORKFLOW_LOGIC_FLAG, enabled=True + ) + async with worker: + handle = await client.start_workflow( + QueryAffectConditionWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + assert await handle.query(QueryAffectConditionWorkflow.check_condition) - async def signal_count() -> int: - return await handle.query(CacheEvictionTearDownWorkflow.signal_count) - # Confirm signal count as 0 - await assert_eq_eventually(0, signal_count) +@workflow.defn +class CancelSignalAndTimerFiredInSameTaskWorkflow: + timer_task: asyncio.Task[None] # type: ignore[reportUninitializedInstanceVariable] - # Send signal and confirm it's at 1 - await handle.signal(CacheEvictionTearDownWorkflow.signal) - await assert_eq_eventually(1, signal_count) + @workflow.run + async def run(self) -> None: + # Start a 1 hour timer + self.timer_task = asyncio.create_task(asyncio.sleep(60 * 60)) + # Wait on it + try: + await self.timer_task + assert False + except asyncio.CancelledError: + pass - await handle.signal(CacheEvictionTearDownWorkflow.signal) - await assert_eq_eventually(2, signal_count) + @workflow.signal + def cancel_timer(self) -> None: + self.timer_task.cancel() - await handle.signal(CacheEvictionTearDownWorkflow.signal) - await assert_eq_eventually(3, signal_count) - await handle.result() - finally: - sys.unraisablehook = old_hook +async def test_workflow_cancel_signal_and_timer_fired_in_same_task( + env: WorkflowEnvironment, +): + # This test only works when we support time skipping + if not env.supports_time_skipping: + pytest.skip("Need to skip time to validate this test") - # Confirm no unraisable exceptions - assert not hook_calls + # TODO(cretz): There is a bug in the Java test server, probably + # https://github.com/temporalio/sdk-java/issues/1138 where the first + # unlock-and-sleep hangs when running this test after + # test_workflow_cancel_activity. So we create a new test environment here. + async with await WorkflowEnvironment.start_time_skipping() as env: + # Start worker for 30 mins. Need to disable workflow cache since we + # restart the worker and don't want to pay the sticky queue penalty. + async with new_worker( + env.client, + CancelSignalAndTimerFiredInSameTaskWorkflow, + max_cached_workflows=0, + ) as worker: + task_queue = worker.task_queue + handle = await env.client.start_workflow( + CancelSignalAndTimerFiredInSameTaskWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=task_queue, + ) + # Wait 30 mins so the worker is waiting on timer + await env.sleep(30 * 60) + # Listen to handler result in background so the auto-skipping works + result_task = asyncio.create_task(handle.result()) -@dataclass -class CapturedEvictionException: - is_replaying: bool - exception: BaseException + # Now that worker is stopped, send a signal and wait another hour to pass + # the timer + await handle.signal(CancelSignalAndTimerFiredInSameTaskWorkflow.cancel_timer) + await env.sleep(60 * 60) + + # Start worker again and wait for workflow completion + async with new_worker( + env.client, + CancelSignalAndTimerFiredInSameTaskWorkflow, + task_queue=task_queue, + max_cached_workflows=0, + ): + # This used to not complete because a signal cancelling the timer was + # not respected by the timer fire + await result_task -captured_eviction_exceptions: List[CapturedEvictionException] = [] +@workflow.defn +class CancelWorkflowSleepTaskWorkflow: + """Like CancelSignalAndTimerFiredInSameTaskWorkflow but uses workflow.sleep.""" + _ready = False + timer_task: asyncio.Task[None] # type: ignore[reportUninitializedInstanceVariable] -@workflow.defn(sandboxed=False) -class EvictionCaptureExceptionWorkflow: @workflow.run - async def run(self) -> None: - # Going to sleep so we can force eviction + async def run(self) -> str: + self.timer_task = asyncio.create_task(workflow.sleep(60 * 60)) + self._ready = True try: - await asyncio.sleep(0.01) - except BaseException as err: - captured_eviction_exceptions.append( - CapturedEvictionException( - is_replaying=workflow.unsafe.is_replaying(), exception=err - ) - ) + await self.timer_task + return "timer_completed" + except asyncio.CancelledError: + return "timer_cancelled" + @workflow.query + def ready(self) -> bool: + return self._ready -async def test_workflow_eviction_exception(client: Client): - assert not captured_eviction_exceptions + @workflow.signal + def cancel_timer(self) -> None: + self.timer_task.cancel() - # Run workflow with no cache (forces eviction every step) + +async def test_workflow_sleep_task_cancellation( + client: Client, +): async with new_worker( - client, EvictionCaptureExceptionWorkflow, max_cached_workflows=0 + client, + CancelWorkflowSleepTaskWorkflow, ) as worker: - await client.execute_workflow( - EvictionCaptureExceptionWorkflow.run, + handle = await client.start_workflow( + CancelWorkflowSleepTaskWorkflow.run, id=f"workflow-{uuid.uuid4()}", task_queue=worker.task_queue, ) - # Confirm expected eviction replaying state and exception type - assert len(captured_eviction_exceptions) == 1 - assert captured_eviction_exceptions[0].is_replaying - assert ( - type(captured_eviction_exceptions[0].exception).__name__ - == "_WorkflowBeingEvictedError" + async def ready() -> bool: + return await handle.query(CancelWorkflowSleepTaskWorkflow.ready) + + await assert_eq_eventually(True, ready) + await handle.signal(CancelWorkflowSleepTaskWorkflow.cancel_timer) + result = await handle.result() + + assert result == "timer_cancelled" + # Verify the Temporal timer was actually cancelled on the server + resp = await client.workflow_service.get_workflow_execution_history( + GetWorkflowExecutionHistoryRequest( + namespace=client.namespace, + execution=WorkflowExecution(workflow_id=handle.id), + ) ) + timer_canceled = any( + e.event_type == EventType.EVENT_TYPE_TIMER_CANCELED for e in resp.history.events + ) + assert timer_canceled, "Expected TimerCanceled event in history" -@dataclass -class DynamicWorkflowValue: - some_string: str +class MyCustomError(ApplicationError): + def __init__(self, message: str) -> None: + super().__init__(message, type="MyCustomError", non_retryable=True) -@workflow.defn(dynamic=True) -class DynamicWorkflow: - @workflow.run - async def run(self, args: Sequence[RawValue]) -> DynamicWorkflowValue: - assert len(args) == 2 - arg1 = workflow.payload_converter().from_payload( - args[0].payload, DynamicWorkflowValue - ) - assert isinstance(arg1, DynamicWorkflowValue) - arg2 = workflow.payload_converter().from_payload( - args[1].payload, DynamicWorkflowValue - ) - assert isinstance(arg1, DynamicWorkflowValue) - return DynamicWorkflowValue( - f"{workflow.info().workflow_type} - {arg1.some_string} - {arg2.some_string}" - ) - - -async def test_workflow_dynamic(client: Client): - async with new_worker(client, DynamicWorkflow) as worker: - result = await client.execute_workflow( - "some-workflow", - args=[DynamicWorkflowValue("val1"), DynamicWorkflowValue("val2")], - id=f"wf-{uuid.uuid4()}", - task_queue=worker.task_queue, - result_type=DynamicWorkflowValue, - ) - assert isinstance(result, DynamicWorkflowValue) - assert result == DynamicWorkflowValue("some-workflow - val1 - val2") +@activity.defn +async def custom_error_activity() -> NoReturn: + raise MyCustomError("activity error!") @workflow.defn -class QueriesDoingBadThingsWorkflow: +class CustomErrorWorkflow: @workflow.run - async def run(self) -> None: - await workflow.wait_condition(lambda: False) - - @workflow.query - async def bad_query(self, bad_thing: str) -> str: - if bad_thing == "wait_condition": - await workflow.wait_condition(lambda: True) - elif bad_thing == "continue_as_new": - workflow.continue_as_new() - elif bad_thing == "upsert_search_attribute": - workflow.upsert_search_attributes({"foo": ["bar"]}) - elif bad_thing == "start_activity": - workflow.start_activity( - "some-activity", start_to_close_timeout=timedelta(minutes=10) + async def run(self) -> NoReturn: + try: + await workflow.execute_activity( + custom_error_activity, schedule_to_close_timeout=timedelta(seconds=30) ) - elif bad_thing == "start_child_workflow": - await workflow.start_child_workflow("some-workflow") - elif bad_thing == "random": - workflow.random().random() - elif bad_thing == "set_query_handler": - workflow.set_query_handler("some-handler", lambda: "whatever") - elif bad_thing == "patch": - workflow.patched("some-patch") - elif bad_thing == "signal_external_handle": - await workflow.get_external_workflow_handle("some-id").signal("some-signal") - return "should never get here" - - -async def test_workflow_queries_doing_bad_things(client: Client): - async with new_worker(client, QueriesDoingBadThingsWorkflow) as worker: - handle = await client.start_workflow( - QueriesDoingBadThingsWorkflow.run, - id=f"wf-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) + except ActivityError: + raise MyCustomError("workflow error!") + raise RuntimeError("Unreachable") - async def assert_bad_query(bad_thing: str) -> None: - with pytest.raises(WorkflowQueryFailedError) as err: - _ = await handle.query( - QueriesDoingBadThingsWorkflow.bad_query, bad_thing - ) - assert "While in read-only function, action attempted" in str(err) - await assert_bad_query("wait_condition") - await assert_bad_query("continue_as_new") - await assert_bad_query("upsert_search_attribute") - await assert_bad_query("start_activity") - await assert_bad_query("start_child_workflow") - await assert_bad_query("random") - await assert_bad_query("set_query_handler") - await assert_bad_query("patch") - await assert_bad_query("signal_external_handle") +class CustomFailureConverter(DefaultFailureConverterWithEncodedAttributes): + # We'll override from failure to convert back to our type + def from_failure( + self, failure: Failure, payload_converter: PayloadConverter + ) -> BaseException: + err = super().from_failure(failure, payload_converter) + if isinstance(err, ApplicationError) and err.type == "MyCustomError": + my_err = MyCustomError(err.message) + my_err.__cause__ = err.__cause__ + err = my_err + return err -# typing.Self only in 3.11+ -if sys.version_info >= (3, 11): +async def test_workflow_custom_failure_converter(client: Client): + # Clone the client but change the data converter to use our failure + # converter + config = client.config() + config["data_converter"] = dataclasses.replace( + config["data_converter"], + failure_converter_class=CustomFailureConverter, + ) + client = Client(**config) - @dataclass - class AnnotatedWithSelfParam: - some_str: str + # Run workflow and confirm error + async with new_worker( + client, CustomErrorWorkflow, activities=[custom_error_activity] + ) as worker: + handle = await client.start_workflow( + CustomErrorWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + with pytest.raises(WorkflowFailureError) as err: + await handle.result() - @workflow.defn - class WorkflowAnnotatedWithSelf: - @workflow.run - async def run(self: typing.Self, some_arg: AnnotatedWithSelfParam) -> str: - assert isinstance(some_arg, AnnotatedWithSelfParam) - return some_arg.some_str + # Check error is as expected + assert isinstance(err.value.cause, MyCustomError) + assert err.value.cause.message == "workflow error!" + assert isinstance(err.value.cause.cause, ActivityError) + assert isinstance(err.value.cause.cause.cause, MyCustomError) + assert err.value.cause.cause.cause.message == "activity error!" + assert err.value.cause.cause.cause.cause is None - async def test_workflow_annotated_with_self(client: Client): - async with new_worker(client, WorkflowAnnotatedWithSelf) as worker: - assert "foo" == await client.execute_workflow( - WorkflowAnnotatedWithSelf.run, - AnnotatedWithSelfParam(some_str="foo"), - id=f"wf-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) + # Check in history it is encoded + failure = ( + (await handle.fetch_history()) + .events[-1] + .workflow_execution_failed_event_attributes.failure + ) + assert failure.application_failure_info.type == "MyCustomError" + while True: + assert failure.message == "Encoded failure" + assert failure.stack_trace == "" + attrs: dict[str, Any] = PayloadConverter.default.from_payloads( + [failure.encoded_attributes] + )[0] + assert "message" in attrs + assert "stack_trace" in attrs + if not failure.HasField("cause"): + break + failure = failure.cause -@activity.defn -async def custom_metrics_activity() -> None: - counter = activity.metric_meter().create_counter( - "my-activity-counter", "my-activity-description", "my-activity-unit" - ) - counter.add(12) - counter.add(34, {"my-activity-extra-attr": 12.34}) +@dataclass +class OptionalParam: + some_string: str @workflow.defn -class CustomMetricsWorkflow: +class OptionalParamWorkflow: @workflow.run - async def run(self) -> None: - await workflow.execute_activity( - custom_metrics_activity, schedule_to_close_timeout=timedelta(seconds=30) - ) - - histogram = workflow.metric_meter().create_histogram( - "my-workflow-histogram", "my-workflow-description", "my-workflow-unit" - ) - histogram.record(56) - histogram.with_additional_attributes({"my-workflow-extra-attr": 1234}).record( - 78 + async def run( + self, some_param: OptionalParam | None = OptionalParam(some_string="default") + ) -> OptionalParam | None: + assert some_param is None or ( + isinstance(some_param, OptionalParam) + and some_param.some_string in ["default", "foo"] ) + return some_param -async def test_workflow_custom_metrics(client: Client): - # Run worker with default runtime which is noop meter just to confirm it - # doesn't fail - async with new_worker( - client, CustomMetricsWorkflow, activities=[custom_metrics_activity] - ) as worker: - await client.execute_workflow( - CustomMetricsWorkflow.run, - id=f"wf-{uuid.uuid4()}", +async def test_workflow_optional_param(client: Client): + async with new_worker(client, OptionalParamWorkflow) as worker: + # Don't send a parameter and confirm it is defaulted + result1 = await client.execute_workflow( + "OptionalParamWorkflow", + id=f"workflow-{uuid.uuid4()}", task_queue=worker.task_queue, + result_type=OptionalParam, ) - - # Create new runtime with Prom server - prom_addr = f"127.0.0.1:{find_free_port()}" - runtime = Runtime( - telemetry=TelemetryConfig( - metrics=PrometheusConfig(bind_address=prom_addr), metric_prefix="foo_" + assert result1 == OptionalParam(some_string="default") + # Send None explicitly + result2 = await client.execute_workflow( + OptionalParamWorkflow.run, + None, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, ) - ) + assert result2 is None + # Send param explicitly + result3 = await client.execute_workflow( + OptionalParamWorkflow.run, + OptionalParam(some_string="foo"), + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + assert result3 == OptionalParam(some_string="foo") - # Confirm meter fails with bad attribute type - with pytest.raises(TypeError) as err: - runtime.metric_meter.with_additional_attributes({"some_attr": None}) # type: ignore - assert str(err.value).startswith("Invalid value type for key") - # New client with the runtime - client = await Client.connect( - client.service_client.config.target_host, - namespace=client.namespace, - runtime=runtime, - ) +class ExceptionRaisingPayloadConverter(DefaultPayloadConverter): + bad_outbound_str = "bad-outbound-payload-str" + bad_inbound_str = "bad-inbound-payload-str" - async with new_worker( - client, CustomMetricsWorkflow, activities=[custom_metrics_activity] - ) as worker: - # Record a gauge at runtime level - gauge = runtime.metric_meter.with_additional_attributes( - {"my-runtime-extra-attr1": "val1", "my-runtime-extra-attr2": True} - ).create_gauge("my-runtime-gauge", "my-runtime-description") - gauge.set(90) + def to_payloads(self, values: Sequence[Any]) -> list[Payload]: + if any( + value == ExceptionRaisingPayloadConverter.bad_outbound_str + for value in values + ): + raise ApplicationError("Intentional outbound converter failure") + return super().to_payloads(values) - # Run workflow - await client.execute_workflow( - CustomMetricsWorkflow.run, - id=f"wf-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) + def from_payloads( + self, payloads: Sequence[Payload], type_hints: list | None = None + ) -> list[Any]: + # Check if any payloads contain the bad data + for payload in payloads: + if ( + ExceptionRaisingPayloadConverter.bad_inbound_str.encode() + in payload.data + ): + raise ApplicationError("Intentional inbound converter failure") + return super().from_payloads(payloads, type_hints) - # Get Prom dump - with urlopen(url=f"http://{prom_addr}/metrics") as f: - prom_str: str = f.read().decode("utf-8") - prom_lines = prom_str.splitlines() - # Intentionally naive metric checker - def matches_metric_line( - line: str, name: str, at_least_labels: Mapping[str, str], value: int - ) -> bool: - # Must have metric name - if not line.startswith(name + "{"): - return False - # Must have labels (don't escape for this test) - for k, v in at_least_labels.items(): - if f'{k}="{v}"' not in line: - return False - return line.endswith(f" {value}") - - def assert_metric_exists( - name: str, at_least_labels: Mapping[str, str], value: int - ) -> None: - assert any( - matches_metric_line(line, name, at_least_labels, value) - for line in prom_lines - ) +@workflow.defn +class ExceptionRaisingConverterWorkflow: + @workflow.run + async def run(self, some_param: str) -> str: + return some_param - def assert_description_exists(name: str, description: str) -> None: - assert f"# HELP {name} {description}" in prom_lines - # Check some metrics are as we expect - assert_description_exists("my_runtime_gauge", "my-runtime-description") - assert_metric_exists( +async def test_exception_raising_converter_param(client: Client): + # Clone the client but change the data converter to use our converter + config = client.config() + config["data_converter"] = dataclasses.replace( + config["data_converter"], + payload_converter_class=ExceptionRaisingPayloadConverter, + ) + client = Client(**config) + + # Run workflow and confirm error + async with new_worker(client, ExceptionRaisingConverterWorkflow) as worker: + with pytest.raises(WorkflowFailureError) as err: + await client.execute_workflow( + ExceptionRaisingConverterWorkflow.run, + ExceptionRaisingPayloadConverter.bad_inbound_str, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + assert isinstance(err.value.cause, ApplicationError) + assert "Intentional inbound converter failure" in str(err.value.cause) + + +@workflow.defn +class ActivityOutboundConversionFailureWorkflow: + @workflow.run + async def run(self) -> None: + await workflow.execute_activity( + "some-activity", + ExceptionRaisingPayloadConverter.bad_outbound_str, + start_to_close_timeout=timedelta(seconds=10), + ) + + +async def test_workflow_activity_outbound_conversion_failure(client: Client): + # This test used to fail because we created commands _before_ we attempted + # to convert the arguments thereby causing half-built commands to get sent + # to the server. + + # Clone the client but change the data converter to use our converter + config = client.config() + config["data_converter"] = dataclasses.replace( + config["data_converter"], + payload_converter_class=ExceptionRaisingPayloadConverter, + ) + client = Client(**config) + async with new_worker(client, ActivityOutboundConversionFailureWorkflow) as worker: + with pytest.raises(WorkflowFailureError) as err: + await client.execute_workflow( + ActivityOutboundConversionFailureWorkflow.run, + id=f"wf-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + assert isinstance(err.value.cause, ApplicationError) + assert "Intentional outbound converter failure" in str(err.value.cause) + + +@dataclass +class ManualResultType: + some_string: str + + +@activity.defn +async def manual_result_type_activity() -> ManualResultType: + return ManualResultType(some_string="from-activity") + + +@workflow.defn +class ManualResultTypeWorkflow: + @workflow.run + async def run(self) -> ManualResultType: + # Only check activity and child if not a child ourselves + if not workflow.info().parent: + # Activity without result type and with + res1 = await workflow.execute_activity( + "manual_result_type_activity", + schedule_to_close_timeout=timedelta(minutes=2), + ) + assert res1 == {"some_string": "from-activity"} + res2 = await workflow.execute_activity( + "manual_result_type_activity", + result_type=ManualResultType, + schedule_to_close_timeout=timedelta(minutes=2), + ) + assert res2 == ManualResultType(some_string="from-activity") + # Child without result type and with + res3 = await workflow.execute_child_workflow( + "ManualResultTypeWorkflow", + ) + assert res3 == {"some_string": "from-workflow"} + res4 = await workflow.execute_child_workflow( + "ManualResultTypeWorkflow", + result_type=ManualResultType, + ) + assert res4 == ManualResultType(some_string="from-workflow") + return ManualResultType(some_string="from-workflow") + + @workflow.query + def some_query(self) -> ManualResultType: + return ManualResultType(some_string="from-query") + + +async def test_manual_result_type(client: Client): + async with new_worker( + client, ManualResultTypeWorkflow, activities=[manual_result_type_activity] + ) as worker: + # Workflow without result type and with + res1 = await client.execute_workflow( + "ManualResultTypeWorkflow", + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + assert res1 == {"some_string": "from-workflow"} + handle = await client.start_workflow( + "ManualResultTypeWorkflow", + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + result_type=ManualResultType, + ) + res2 = await handle.result() + assert res2 == ManualResultType(some_string="from-workflow") + # Query without result type and with + res3 = await handle.query("some_query") + assert res3 == {"some_string": "from-query"} + res4 = await handle.query("some_query", result_type=ManualResultType) + assert res4 == ManualResultType(some_string="from-query") + + +async def test_cache_eviction_tear_down(client: Client): + # This test simulates forcing eviction. This used to raise GeneratorExit on + # GC which triggered the finally which could run on any thread Python + # chooses, but now we expect eviction to properly tear down tasks and + # therefore we cancel them + async with new_worker( + client, + CacheEvictionTearDownWorkflow, + WaitForeverWorkflow, + activities=[wait_forever_activity], + max_cached_workflows=0, + ) as worker: + # Put a hook to catch unraisable exceptions + old_hook = sys.unraisablehook + hook_calls: list[Any] = [] + sys.unraisablehook = hook_calls.append + try: + handle = await client.start_workflow( + CacheEvictionTearDownWorkflow.run, + id=f"wf-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + async def signal_count() -> int: + return await handle.query(CacheEvictionTearDownWorkflow.signal_count) + + # Confirm signal count as 0 + await assert_eq_eventually(0, signal_count) + + # Send signal and confirm it's at 1 + await handle.signal(CacheEvictionTearDownWorkflow.signal) + await assert_eq_eventually(1, signal_count) + + await handle.signal(CacheEvictionTearDownWorkflow.signal) + await assert_eq_eventually(2, signal_count) + + await handle.signal(CacheEvictionTearDownWorkflow.signal) + await assert_eq_eventually(3, signal_count) + + await handle.result() + finally: + sys.unraisablehook = old_hook + + # Confirm no unraisable exceptions + assert not hook_calls + + +@dataclass +class CapturedEvictionException: + is_replaying: bool + exception: BaseException + + +captured_eviction_exceptions: list[CapturedEvictionException] = [] + + +@workflow.defn(sandboxed=False) +class EvictionCaptureExceptionWorkflow: + @workflow.run + async def run(self) -> None: + # Going to sleep so we can force eviction + try: + await asyncio.sleep(0.01) + except BaseException as err: + captured_eviction_exceptions.append( + CapturedEvictionException( + is_replaying=workflow.unsafe.is_replaying(), exception=err + ) + ) + + +async def test_workflow_eviction_exception(client: Client): + assert not captured_eviction_exceptions + + # Run workflow with no cache (forces eviction every step) + async with new_worker( + client, EvictionCaptureExceptionWorkflow, max_cached_workflows=0 + ) as worker: + await client.execute_workflow( + EvictionCaptureExceptionWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + # Confirm expected eviction replaying state and exception type + assert len(captured_eviction_exceptions) == 1 + assert captured_eviction_exceptions[0].is_replaying + assert ( + type(captured_eviction_exceptions[0].exception).__name__ + == "_WorkflowBeingEvictedError" + ) + + +@dataclass +class DynamicWorkflowValue: + some_string: str + + +@workflow.defn(dynamic=True) +class DynamicWorkflow: + @workflow.run + async def run(self, args: Sequence[RawValue]) -> DynamicWorkflowValue: + assert len(args) == 2 + arg1 = workflow.payload_converter().from_payload( + args[0].payload, DynamicWorkflowValue + ) + assert isinstance(arg1, DynamicWorkflowValue) + arg2 = workflow.payload_converter().from_payload( + args[1].payload, DynamicWorkflowValue + ) + assert isinstance(arg1, DynamicWorkflowValue) + return DynamicWorkflowValue( + f"{workflow.info().workflow_type} - {arg1.some_string} - {arg2.some_string}" + ) + + +async def test_workflow_dynamic(client: Client): + async with new_worker(client, DynamicWorkflow) as worker: + result = await client.execute_workflow( + "some-workflow", + args=[DynamicWorkflowValue("val1"), DynamicWorkflowValue("val2")], + id=f"wf-{uuid.uuid4()}", + task_queue=worker.task_queue, + result_type=DynamicWorkflowValue, + ) + assert isinstance(result, DynamicWorkflowValue) + assert result == DynamicWorkflowValue("some-workflow - val1 - val2") + + +@workflow.defn +class QueriesDoingBadThingsWorkflow: + @workflow.run + async def run(self) -> None: + await workflow.wait_condition(lambda: False) + + @workflow.query + async def bad_query(self, bad_thing: str) -> str: + if bad_thing == "wait_condition": + await workflow.wait_condition(lambda: True) + elif bad_thing == "continue_as_new": + workflow.continue_as_new() + elif bad_thing == "upsert_search_attribute": + workflow.upsert_search_attributes({"foo": ["bar"]}) + elif bad_thing == "start_activity": + workflow.start_activity( + "some-activity", start_to_close_timeout=timedelta(minutes=10) + ) + elif bad_thing == "start_child_workflow": + await workflow.start_child_workflow("some-workflow") + elif bad_thing == "random": + workflow.random().random() + elif bad_thing == "set_query_handler": + workflow.set_query_handler("some-handler", lambda: "whatever") + elif bad_thing == "patch": + workflow.patched("some-patch") + elif bad_thing == "register_random_seed_callback": + workflow.register_random_seed_callback(lambda _seed: None) + elif bad_thing == "signal_external_handle": + await workflow.get_external_workflow_handle("some-id").signal("some-signal") + return "should never get here" + + +async def test_workflow_queries_doing_bad_things(client: Client): + async with new_worker(client, QueriesDoingBadThingsWorkflow) as worker: + handle = await client.start_workflow( + QueriesDoingBadThingsWorkflow.run, + id=f"wf-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + async def assert_bad_query(bad_thing: str) -> None: + with pytest.raises(WorkflowQueryFailedError) as err: + _ = await handle.query( + QueriesDoingBadThingsWorkflow.bad_query, bad_thing + ) + assert "While in read-only function, action attempted" in str(err) + + await assert_bad_query("wait_condition") + await assert_bad_query("continue_as_new") + await assert_bad_query("upsert_search_attribute") + await assert_bad_query("start_activity") + await assert_bad_query("start_child_workflow") + await assert_bad_query("random") + await assert_bad_query("set_query_handler") + await assert_bad_query("patch") + await assert_bad_query("register_random_seed_callback") + await assert_bad_query("signal_external_handle") + + +# typing.Self only in 3.11+ +if sys.version_info >= (3, 11): + + @dataclass + class AnnotatedWithSelfParam: + some_str: str + + @workflow.defn + class WorkflowAnnotatedWithSelf: + @workflow.run + async def run(self: typing.Self, some_arg: AnnotatedWithSelfParam) -> str: + assert isinstance(some_arg, AnnotatedWithSelfParam) + return some_arg.some_str + + async def test_workflow_annotated_with_self(client: Client): + async with new_worker(client, WorkflowAnnotatedWithSelf) as worker: + assert "foo" == await client.execute_workflow( + WorkflowAnnotatedWithSelf.run, + AnnotatedWithSelfParam(some_str="foo"), + id=f"wf-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + +@activity.defn +async def custom_metrics_activity() -> None: + counter = activity.metric_meter().create_counter( + "my-activity-counter", "my-activity-description", "my-activity-unit" + ) + counter.add(12) + counter.add(34, {"my-activity-extra-attr": 12.34}) + + +@workflow.defn +class CustomMetricsWorkflow: + @workflow.run + async def run(self) -> None: + await workflow.execute_activity( + custom_metrics_activity, schedule_to_close_timeout=timedelta(seconds=30) + ) + + histogram = workflow.metric_meter().create_histogram( + "my-workflow-histogram", "my-workflow-description", "my-workflow-unit" + ) + histogram.record(56) + histogram.with_additional_attributes({"my-workflow-extra-attr": 1234}).record( + 78 + ) + + +async def test_workflow_custom_metrics(client: Client, env: WorkflowEnvironment): + # Run worker with default runtime which is noop meter just to confirm it + # doesn't fail + async with new_worker( + client, CustomMetricsWorkflow, activities=[custom_metrics_activity] + ) as worker: + await client.execute_workflow( + CustomMetricsWorkflow.run, + id=f"wf-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + # Create new runtime with Prom server + prom_addr = f"127.0.0.1:{find_free_port()}" + runtime = Runtime( + telemetry=TelemetryConfig( + metrics=PrometheusConfig(bind_address=prom_addr), metric_prefix="foo_" + ) + ) + + # Confirm meter fails with bad attribute type + with pytest.raises(TypeError) as err: + runtime.metric_meter.with_additional_attributes({"some_attr": None}) # type: ignore + assert str(err.value).startswith("Invalid value type for key") + + # New client with the runtime + client = await env.connect_client( + runtime=runtime, + ) + + async with new_worker( + client, CustomMetricsWorkflow, activities=[custom_metrics_activity] + ) as worker: + # Record a gauge at runtime level + gauge = runtime.metric_meter.with_additional_attributes( + {"my-runtime-extra-attr1": "val1", "my-runtime-extra-attr2": True} + ).create_gauge("my-runtime-gauge", "my-runtime-description") + gauge.set(90) + + # Run workflow + await client.execute_workflow( + CustomMetricsWorkflow.run, + id=f"wf-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + # Get Prom dump + with urlopen(url=f"http://{prom_addr}/metrics") as f: + prom_str: str = f.read().decode("utf-8") + prom_lines = prom_str.splitlines() + + prom_matcher = PromMetricMatcher(prom_lines) + + # Check some metrics are as we expect + prom_matcher.assert_description_exists( + "my_runtime_gauge", "my-runtime-description" + ) + prom_matcher.assert_metric_exists( "my_runtime_gauge", { "my_runtime_extra_attr1": "val1", @@ -4111,9 +5094,11 @@ def assert_description_exists(name: str, description: str) -> None: }, 90, ) - assert_description_exists("my_workflow_histogram", "my-workflow-description") - assert_metric_exists("my_workflow_histogram_sum", {}, 56) - assert_metric_exists( + prom_matcher.assert_description_exists( + "my_workflow_histogram", "my-workflow-description" + ) + prom_matcher.assert_metric_exists("my_workflow_histogram_sum", {}, 56) + prom_matcher.assert_metric_exists( "my_workflow_histogram_sum", { "my_workflow_extra_attr": "1234", @@ -4124,9 +5109,11 @@ def assert_description_exists(name: str, description: str) -> None: }, 78, ) - assert_description_exists("my_activity_counter", "my-activity-description") - assert_metric_exists("my_activity_counter", {}, 12) - assert_metric_exists( + prom_matcher.assert_description_exists( + "my_activity_counter", "my-activity-description" + ) + prom_matcher.assert_metric_exists("my_activity_counter", {}, 12) + prom_matcher.assert_metric_exists( "my_activity_counter", { "my_activity_extra_attr": "12.34", @@ -4138,12 +5125,12 @@ def assert_description_exists(name: str, description: str) -> None: 34, ) # Also check Temporal metric got its prefix - assert_metric_exists( + prom_matcher.assert_metric_exists( "foo_workflow_completed", {"workflow_type": "CustomMetricsWorkflow"}, 1 ) -async def test_workflow_buffered_metrics(client: Client): +async def test_workflow_buffered_metrics(client: Client, env: WorkflowEnvironment): # Create runtime with metric buffer buffer = MetricBuffer(10000) runtime = Runtime( @@ -4204,9 +5191,7 @@ async def test_workflow_buffered_metrics(client: Client): assert runtime_updates2[1].value == 400 # Create a new client on the runtime and execute the custom metric workflow - client = await Client.connect( - client.service_client.config.target_host, - namespace=client.namespace, + client = await env.connect_client( runtime=runtime, ) async with new_worker( @@ -4267,12 +5252,10 @@ async def test_workflow_buffered_metrics(client: Client): ) -async def test_workflow_metrics_other_types(client: Client): +async def test_workflow_metrics_other_types(env: WorkflowEnvironment): async def do_stuff(buffer: MetricBuffer) -> None: runtime = Runtime(telemetry=TelemetryConfig(metrics=buffer)) - new_client = await Client.connect( - client.service_client.config.target_host, - namespace=client.namespace, + new_client = await env.connect_client( runtime=runtime, ) async with new_worker(new_client, HelloWorkflow) as worker: @@ -4348,7 +5331,7 @@ async def do_stuff(buffer: MetricBuffer) -> None: @workflow.defn class UpdateHandlersWorkflow: def __init__(self) -> None: - self._last_event: Optional[str] = None + self._last_event: str | None = None @workflow.run async def run(self) -> None: @@ -4828,7 +5811,7 @@ async def patched_call( try: await called.wait() finally: - client.workflow_service.poll_workflow_execution_update = unpatched_call + client.workflow_service.poll_workflow_execution_update = unpatched_call # type: ignore result_task.cancel() with pytest.raises(WorkflowUpdateRPCTimeoutOrCancelledError): await result_task @@ -4914,11 +5897,12 @@ async def test_workflow_timeout_support(client: Client, approach: str): @workflow.defn class BuildIDInfoWorkflow: + do_continue = False do_finish = False @workflow.run async def run(self): - await asyncio.sleep(1) + await workflow.wait_condition(lambda: self.do_continue) if workflow.info().get_current_build_id() == "1.0": await workflow.execute_activity( say_hello, "yo", schedule_to_close_timeout=timedelta(seconds=5) @@ -4929,6 +5913,10 @@ async def run(self): def get_build_id(self) -> str: return workflow.info().get_current_build_id() + @workflow.signal + async def continue_run(self): + self.do_continue = True + @workflow.signal async def finish(self): self.do_finish = True @@ -4973,9 +5961,11 @@ async def test_workflow_current_build_id_appropriately_set( ) as worker: bid = await handle.query(BuildIDInfoWorkflow.get_build_id) assert bid == "1.0" + await handle.signal(BuildIDInfoWorkflow.continue_run) + await assert_eq_eventually( + "1.1", lambda: handle.query(BuildIDInfoWorkflow.get_build_id) + ) await handle.signal(BuildIDInfoWorkflow.finish) - bid = await handle.query(BuildIDInfoWorkflow.get_build_id) - assert bid == "1.1" await handle.result() bid = await handle.query(BuildIDInfoWorkflow.get_build_id) assert bid == "1.1" @@ -5055,14 +6045,14 @@ async def run(self, scenario: FailureTypesScenario) -> None: async def test_workflow_failure_types_configured(client: Client): # Asserter for a single scenario async def assert_scenario( - workflow: Type[FailureTypesWorkflowBase], + workflow: type[FailureTypesWorkflowBase], *, expect_task_fail: bool, fail_message_contains: str, - worker_level_failure_exception_type: Optional[Type[Exception]] = None, - workflow_scenario: Optional[FailureTypesScenario] = None, - signal_scenario: Optional[FailureTypesScenario] = None, - update_scenario: Optional[FailureTypesScenario] = None, + worker_level_failure_exception_type: type[Exception] | None = None, + workflow_scenario: FailureTypesScenario | None = None, + signal_scenario: FailureTypesScenario | None = None, + update_scenario: FailureTypesScenario | None = None, ) -> None: logging.debug( "Asserting scenario %s", @@ -5093,7 +6083,7 @@ async def assert_scenario( ) if signal_scenario: await handle.signal(workflow.signal, signal_scenario) - update_handle: Optional[WorkflowUpdateHandle[Any]] = None + update_handle: WorkflowUpdateHandle[Any] | None = None if update_scenario: update_handle = await handle.start_update( workflow.update, @@ -5135,11 +6125,11 @@ async def has_expected_task_fail() -> bool: # Run a scenario async def run_scenario( - workflow: Type[FailureTypesWorkflowBase], + workflow: type[FailureTypesWorkflowBase], scenario: FailureTypesScenario, *, expect_task_fail: bool = False, - worker_level_failure_exception_type: Optional[Type[Exception]] = None, + worker_level_failure_exception_type: type[Exception] | None = None, ) -> None: # Run for workflow, signal, and update fail_message_contains = ( @@ -5239,7 +6229,7 @@ class Foo(pydantic.BaseModel): @workflow.defn(failure_exception_types=[pydantic.ValidationError]) class FailOnBadPydanticInputWorkflow: @workflow.run - async def run(self, params: Foo) -> None: + async def run(self, _params: Foo) -> None: pass @@ -5259,7 +6249,7 @@ async def test_workflow_fail_on_bad_pydantic_input(client: Client): @workflow.defn(failure_exception_types=[Exception]) class FailOnBadInputWorkflow: @workflow.run - async def run(self, param: str) -> None: + async def run(self, _param: str) -> None: pass @@ -5285,6 +6275,7 @@ async def run(self) -> None: await asyncio.sleep(0.1) +@pytest.mark.requires_local_server async def test_workflow_replace_worker_client(client: Client, env: WorkflowEnvironment): if env.supports_time_skipping: pytest.skip("Only testing against two real servers") @@ -5296,7 +6287,8 @@ async def test_workflow_replace_worker_client(client: Client, env: WorkflowEnvir # poller to force a quick re-poll to recognize our client change quickly (as # opposed to just waiting the minute for poll timeout). async with await WorkflowEnvironment.start_local( - dev_server_download_version=DEV_SERVER_DOWNLOAD_VERSION + runtime=client.service_client.config.runtime, + dev_server_download_version=DEV_SERVER_DOWNLOAD_VERSION, ) as other_env: # Start both workflows on different servers task_queue = f"tq-{uuid.uuid4()}" @@ -5325,28 +6317,44 @@ async def any_task_completed(handle: WorkflowHandle) -> bool: # the second await assert_eq_eventually(True, lambda: any_task_completed(handle1)) assert not await any_task_completed(handle2) - # Now replace the client, which should be used fairly quickly # because we should have timer-done poll completions every 100ms worker.client = other_env.client - # Now confirm the other workflow has started - await assert_eq_eventually(True, lambda: any_task_completed(handle2)) - + await assert_eq_eventually( + True, + lambda: any_task_completed(handle2), + timeout=timedelta(seconds=30), + ) # Terminate both await handle1.terminate() await handle2.terminate() +async def test_workflow_replace_worker_client_diff_runtimes_fail( + client: Client, env: WorkflowEnvironment +): + other_runtime = Runtime(telemetry=TelemetryConfig()) + other_client = await env.connect_client( + runtime=other_runtime, + ) + async with new_worker(client, HelloWorkflow) as worker: + with pytest.raises( + ValueError, + match="New client is not on the same runtime as the existing client", + ): + worker.client = other_client + + @activity.defn(dynamic=True) -async def return_name_activity(args: Sequence[RawValue]) -> str: +async def return_name_activity(_args: Sequence[RawValue]) -> str: return activity.info().activity_type @workflow.defn class AsCompletedWorkflow: @workflow.run - async def run(self) -> List[str]: + async def run(self) -> list[str]: # Lazily start 10 different activities and wait for each completed tasks = [ workflow.execute_activity( @@ -5383,7 +6391,7 @@ async def test_workflow_as_completed_utility(client: Client): @workflow.defn class WaitWorkflow: @workflow.run - async def run(self) -> List[str]: + async def run(self) -> list[str]: # Create 10 tasks that return activity names, wait on them, then execute # the activities async def new_activity_name(index: int) -> str: @@ -5422,10 +6430,10 @@ async def test_workflow_wait_utility(client: Client): @workflow.defn class CurrentUpdateWorkflow: def __init__(self) -> None: - self._pending_get_update_id_tasks: List[asyncio.Task[str]] = [] + self._pending_get_update_id_tasks: list[asyncio.Task[str]] = [] @workflow.run - async def run(self) -> List[str]: + async def run(self) -> list[str]: # Confirm no update info assert not workflow.current_update_info() @@ -5490,12 +6498,6 @@ async def test_workflow_current_update(client: Client): ) -def skip_unfinished_handler_tests_in_older_python(): - # These tests reliably fail or timeout in 3.9 - if sys.version_info < (3, 10): - pytest.skip("Skipping unfinished handler tests in Python < 3.10") - - @workflow.defn class UnfinishedHandlersWarningsWorkflow: def __init__(self): @@ -5553,7 +6555,6 @@ async def test_unfinished_update_handler(client: Client): async def test_unfinished_signal_handler(client: Client): - skip_unfinished_handler_tests_in_older_python() async with new_worker(client, UnfinishedHandlersWarningsWorkflow) as worker: test = _UnfinishedHandlersWarningsTest(client, worker, "signal") await test.test_wait_all_handlers_finished_and_unfinished_handlers_warning() @@ -5623,8 +6624,8 @@ async def _workflow_task_failed(self, workflow_id: str) -> bool: async def _get_workflow_result_and_warning( self, wait_all_handlers_finished: bool, - unfinished_policy: Optional[workflow.HandlerUnfinishedPolicy] = None, - ) -> Tuple[bool, bool]: + unfinished_policy: workflow.HandlerUnfinishedPolicy | None = None, + ) -> tuple[bool, bool]: with pytest.WarningsRecorder() as warnings: wf_result = await self._get_workflow_result( wait_all_handlers_finished, unfinished_policy @@ -5638,8 +6639,8 @@ async def _get_workflow_result_and_warning( async def _get_workflow_result( self, wait_all_handlers_finished: bool, - unfinished_policy: Optional[workflow.HandlerUnfinishedPolicy] = None, - handle_future: Optional[asyncio.Future[WorkflowHandle]] = None, + unfinished_policy: workflow.HandlerUnfinishedPolicy | None = None, + handle_future: asyncio.Future[WorkflowHandle] | None = None, ) -> bool: handle = await self.client.start_workflow( UnfinishedHandlersWarningsWorkflow.run, @@ -5667,7 +6668,7 @@ async def _get_workflow_result( return await handle.result() @property - def _unfinished_handler_warning_cls(self) -> Type: + def _unfinished_handler_warning_cls(self) -> type: return { "update": workflow.UnfinishedUpdateHandlersWarning, "signal": workflow.UnfinishedSignalHandlersWarning, @@ -5698,13 +6699,13 @@ async def run( if handler_dynamism == "-dynamic-": async def my_late_registered_dynamic_update( - name: str, args: Sequence[RawValue] + _name: str, _args: Sequence[RawValue] ) -> str: await workflow.wait_condition(lambda: self.handlers_may_finish) return "my-late-registered-dynamic-update-result" async def my_late_registered_dynamic_signal( - name: str, args: Sequence[RawValue] + _name: str, _args: Sequence[RawValue] ) -> None: await workflow.wait_condition(lambda: self.handlers_may_finish) @@ -5759,15 +6760,18 @@ async def my_signal(self) -> None: await workflow.wait_condition(lambda: self.handlers_may_finish) @workflow.update(dynamic=True) - async def my_dynamic_update(self, name: str, args: Sequence[RawValue]) -> str: + async def my_dynamic_update(self, _name: str, _args: Sequence[RawValue]) -> str: await workflow.wait_condition(lambda: self.handlers_may_finish) return "my-dynamic-update-result" @workflow.signal(dynamic=True) - async def my_dynamic_signal(self, name: str, args: Sequence[RawValue]) -> None: + async def my_dynamic_signal(self, _name: str, _args: Sequence[RawValue]) -> None: await workflow.wait_condition(lambda: self.handlers_may_finish) +# Cloud cancels in-flight dynamic handlers before SDK teardown can emit the +# unfinished-handler warning this test asserts. +@pytest.mark.requires_local_server @pytest.mark.parametrize("handler_type", ["-signal-", "-update-"]) @pytest.mark.parametrize( "handler_registration", ["-late-registered-", "-not-late-registered-"] @@ -5782,7 +6786,6 @@ async def my_dynamic_signal(self, name: str, args: Sequence[RawValue]) -> None: ) async def test_unfinished_handler_on_workflow_termination( client: Client, - env: WorkflowEnvironment, handler_type: Literal["-signal-", "-update-"], handler_registration: Literal["-late-registered-", "-not-late-registered-"], handler_dynamism: Literal["-dynamic-", "-not-dynamic-"], @@ -5793,11 +6796,6 @@ async def test_unfinished_handler_on_workflow_termination( "-cancellation-", "-failure-", "-continue-as-new-" ], ): - if env.supports_time_skipping: - pytest.skip( - "Issues with update: https://github.com/temporalio/sdk-python/issues/826" - ) - skip_unfinished_handler_tests_in_older_python() await _UnfinishedHandlersOnWorkflowTerminationTest( client, handler_type, @@ -5889,13 +6887,24 @@ async def _run_workflow_and_get_warning(self) -> bool: if self.handler_waiting == "-wait-all-handlers-finish-": await update_task else: - with pytest.raises(WorkflowUpdateFailedError) as err_info: + with pytest.raises( + (WorkflowUpdateFailedError, RPCError) + ) as err_info: await update_task update_err = err_info.value - assert isinstance(update_err.cause, ApplicationError) - assert ( - update_err.cause.type == "AcceptedUpdateCompletedWorkflow" - ) + if isinstance(update_err, WorkflowUpdateFailedError): + assert isinstance(update_err.cause, ApplicationError) + assert ( + update_err.cause.type + == "AcceptedUpdateCompletedWorkflow" + ) + else: + assert isinstance(update_err, RPCError) + assert update_err.status == RPCStatusCode.NOT_FOUND + assert ( + str(update_err) + == "workflow execution already completed" + ) with pytest.raises(WorkflowFailureError) as err: await handle.result() @@ -5920,7 +6929,7 @@ async def _run_workflow_and_get_warning(self) -> bool: return unfinished_handler_warning_emitted @property - def _unfinished_handler_warning_cls(self) -> Type: + def _unfinished_handler_warning_cls(self) -> type: return { "-update-": workflow.UnfinishedUpdateHandlersWarning, "-signal-": workflow.UnfinishedSignalHandlersWarning, @@ -5928,2442 +6937,3062 @@ def _unfinished_handler_warning_cls(self) -> Type: @workflow.defn -class IDConflictWorkflow: - # Just run forever +class IDConflictWorkflow: + # Just run forever + @workflow.run + async def run(self) -> None: + await workflow.wait_condition(lambda: False) + + +async def test_workflow_id_conflict(client: Client): + async with new_worker(client, IDConflictWorkflow) as worker: + # Start a workflow + handle = await client.start_workflow( + IDConflictWorkflow.run, + id=f"wf-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + handle = client.get_workflow_handle_for( + IDConflictWorkflow.run, handle.id, run_id=handle.result_run_id + ) + + # Confirm another fails by default + with pytest.raises(WorkflowAlreadyStartedError): + await client.start_workflow( + IDConflictWorkflow.run, + id=handle.id, + task_queue=worker.task_queue, + ) + + # Confirm fails if explicitly given that option + with pytest.raises(WorkflowAlreadyStartedError): + await client.start_workflow( + IDConflictWorkflow.run, + id=handle.id, + task_queue=worker.task_queue, + id_conflict_policy=WorkflowIDConflictPolicy.FAIL, + ) + + # Confirm gives back same handle if requested + new_handle = await client.start_workflow( + IDConflictWorkflow.run, + id=handle.id, + task_queue=worker.task_queue, + id_conflict_policy=WorkflowIDConflictPolicy.USE_EXISTING, + ) + new_handle = client.get_workflow_handle_for( + IDConflictWorkflow.run, new_handle.id, run_id=new_handle.result_run_id + ) + assert new_handle.run_id == handle.run_id + assert (await handle.describe()).status == WorkflowExecutionStatus.RUNNING + assert (await new_handle.describe()).status == WorkflowExecutionStatus.RUNNING + + # Confirm terminates and starts new if requested + new_handle = await client.start_workflow( + IDConflictWorkflow.run, + id=handle.id, + task_queue=worker.task_queue, + id_conflict_policy=WorkflowIDConflictPolicy.TERMINATE_EXISTING, + ) + new_handle = client.get_workflow_handle_for( + IDConflictWorkflow.run, new_handle.id, run_id=new_handle.result_run_id + ) + assert new_handle.run_id != handle.run_id + assert (await handle.describe()).status == WorkflowExecutionStatus.TERMINATED + assert (await new_handle.describe()).status == WorkflowExecutionStatus.RUNNING + + +@workflow.defn +class UpdateCompletionIsHonoredWhenAfterWorkflowReturn1Workflow: + def __init__(self) -> None: + self.workflow_returned = False + + @workflow.run + async def run(self) -> str: + self.workflow_returned = True + return "workflow-result" + + @workflow.update + async def my_update(self) -> str: + await workflow.wait_condition(lambda: self.workflow_returned) + return "update-result" + + +async def test_update_completion_is_honored_when_after_workflow_return_1( + client: Client, +): + update_id = "my-update" + task_queue = "tq" + wf_handle = await client.start_workflow( + UpdateCompletionIsHonoredWhenAfterWorkflowReturn1Workflow.run, + id=f"wf-{uuid.uuid4()}", + task_queue=task_queue, + ) + update_result_task = asyncio.create_task( + wf_handle.execute_update( + UpdateCompletionIsHonoredWhenAfterWorkflowReturn1Workflow.my_update, + id=update_id, + ) + ) + await workflow_update_exists(client, wf_handle.id, update_id) + + async with Worker( + client, + task_queue=task_queue, + workflows=[UpdateCompletionIsHonoredWhenAfterWorkflowReturn1Workflow], + ): + assert await wf_handle.result() == "workflow-result" + assert await update_result_task == "update-result" + + +@workflow.defn +class UpdateCompletionIsHonoredWhenAfterWorkflowReturnWorkflow2: + def __init__(self): + self.received_update = False + self.update_result: asyncio.Future[str] = asyncio.Future() + + @workflow.run + async def run(self) -> str: + await workflow.wait_condition(lambda: self.received_update) + self.update_result.set_result("update-result") + # Prior to https://github.com/temporalio/features/issues/481, the client + # waiting on the update got a "Workflow execution already completed" + # error instead of the update result, because the main workflow + # coroutine completion command is emitted before the update completion + # command, and we were truncating commands at the first completion + # command. + return "workflow-result" + + @workflow.update + async def my_update(self) -> str: + self.received_update = True + return await self.update_result + + +async def test_update_completion_is_honored_when_after_workflow_return_2( + client: Client, +): + async with Worker( + client, + task_queue="tq", + workflows=[UpdateCompletionIsHonoredWhenAfterWorkflowReturnWorkflow2], + ) as worker: + handle = await client.start_workflow( + UpdateCompletionIsHonoredWhenAfterWorkflowReturnWorkflow2.run, + id=f"wf-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + update_result = await handle.execute_update( + UpdateCompletionIsHonoredWhenAfterWorkflowReturnWorkflow2.my_update + ) + assert update_result == "update-result" + assert await handle.result() == "workflow-result" + + +@workflow.defn +class FirstCompletionCommandIsHonoredWorkflow: + def __init__( + self, main_workflow_returns_before_signal_completions: bool = False + ) -> None: + self.seen_first_signal = False + self.seen_second_signal = False + self.main_workflow_returns_before_signal_completions = ( + main_workflow_returns_before_signal_completions + ) + self.run_finished = False + + @workflow.run + async def run(self) -> str: + await workflow.wait_condition( + lambda: self.seen_first_signal and self.seen_second_signal + ) + self.run_finished = True + return "workflow-result" + + @workflow.signal + async def this_signal_executes_first(self): + self.seen_first_signal = True + if self.main_workflow_returns_before_signal_completions: + await workflow.wait_condition(lambda: self.run_finished) + raise ApplicationError( + "Client should see this error unless doing ping-pong " + "(in which case main coroutine returns first)" + ) + + @workflow.signal + async def this_signal_executes_second(self): + await workflow.wait_condition(lambda: self.seen_first_signal) + self.seen_second_signal = True + if self.main_workflow_returns_before_signal_completions: + await workflow.wait_condition(lambda: self.run_finished) + raise ApplicationError("Client should never see this error!") + + +@workflow.defn +class FirstCompletionCommandIsHonoredSignalWaitWorkflow( + FirstCompletionCommandIsHonoredWorkflow +): + def __init__(self) -> None: + super().__init__(main_workflow_returns_before_signal_completions=True) + + @workflow.run + async def run(self) -> str: + return await super().run() + + +async def test_first_of_two_signal_completion_commands_is_honored(client: Client): + await _do_first_completion_command_is_honored_test( + client, main_workflow_returns_before_signal_completions=False + ) + + +async def test_workflow_return_is_honored_when_it_precedes_signal_completion_command( + client: Client, +): + await _do_first_completion_command_is_honored_test( + client, main_workflow_returns_before_signal_completions=True + ) + + +async def _do_first_completion_command_is_honored_test( + client: Client, main_workflow_returns_before_signal_completions: bool +): + workflow_cls: ( + type[FirstCompletionCommandIsHonoredSignalWaitWorkflow] + | type[FirstCompletionCommandIsHonoredWorkflow] + ) = ( + FirstCompletionCommandIsHonoredSignalWaitWorkflow + if main_workflow_returns_before_signal_completions + else FirstCompletionCommandIsHonoredWorkflow + ) + async with Worker( + client, + task_queue="tq", + workflows=[workflow_cls], + ) as worker: + handle = await client.start_workflow( + workflow_cls.run, + id=f"wf-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.signal(workflow_cls.this_signal_executes_second) + await handle.signal(workflow_cls.this_signal_executes_first) + try: + result = await handle.result() + except WorkflowFailureError as err: + if main_workflow_returns_before_signal_completions: + raise RuntimeError( + "Expected no error due to main workflow coroutine returning first" + ) + else: + assert str(err.cause).startswith("Client should see this error") + else: + assert ( + main_workflow_returns_before_signal_completions + and result == "workflow-result" + ) + + +@workflow.defn +class TimerStartedAfterWorkflowCompletionWorkflow: + def __init__(self) -> None: + self.received_signal = False + self.main_workflow_coroutine_finished = False + @workflow.run - async def run(self) -> None: - await workflow.wait_condition(lambda: False) + async def run(self) -> str: + await workflow.wait_condition(lambda: self.received_signal) + self.main_workflow_coroutine_finished = True + return "workflow-result" + + @workflow.signal(unfinished_policy=workflow.HandlerUnfinishedPolicy.ABANDON) + async def my_signal(self): + self.received_signal = True + await workflow.wait_condition(lambda: self.main_workflow_coroutine_finished) + await asyncio.sleep(7777777) -async def test_workflow_id_conflict(client: Client): - async with new_worker(client, IDConflictWorkflow) as worker: - # Start a workflow +async def test_timer_started_after_workflow_completion(client: Client): + async with new_worker( + client, TimerStartedAfterWorkflowCompletionWorkflow + ) as worker: handle = await client.start_workflow( - IDConflictWorkflow.run, + TimerStartedAfterWorkflowCompletionWorkflow.run, id=f"wf-{uuid.uuid4()}", task_queue=worker.task_queue, ) - handle = client.get_workflow_handle_for( - IDConflictWorkflow.run, handle.id, run_id=handle.result_run_id + await handle.signal(TimerStartedAfterWorkflowCompletionWorkflow.my_signal) + assert await handle.result() == "workflow-result" + + +@activity.defn +async def activity_with_retry_delay(): + raise ApplicationError( + ActivitiesWithRetryDelayWorkflow.error_message, + next_retry_delay=ActivitiesWithRetryDelayWorkflow.next_retry_delay, + ) + + +@workflow.defn +class ActivitiesWithRetryDelayWorkflow: + error_message = "Deliberately failing with next_retry_delay set" + next_retry_delay = timedelta(milliseconds=5) + + @workflow.run + async def run(self) -> None: + await workflow.execute_activity( + activity_with_retry_delay, + retry_policy=RetryPolicy(maximum_attempts=2), + schedule_to_close_timeout=timedelta(minutes=5), ) - # Confirm another fails by default - with pytest.raises(WorkflowAlreadyStartedError): - await client.start_workflow( - IDConflictWorkflow.run, - id=handle.id, - task_queue=worker.task_queue, - ) - # Confirm fails if explicitly given that option - with pytest.raises(WorkflowAlreadyStartedError): - await client.start_workflow( - IDConflictWorkflow.run, - id=handle.id, +async def test_activity_retry_delay(client: Client): + async with new_worker( + client, ActivitiesWithRetryDelayWorkflow, activities=[activity_with_retry_delay] + ) as worker: + try: + await client.execute_workflow( + ActivitiesWithRetryDelayWorkflow.run, + id=str(uuid.uuid4()), task_queue=worker.task_queue, - id_conflict_policy=WorkflowIDConflictPolicy.FAIL, + ) + except WorkflowFailureError as err: + assert isinstance(err.cause, ActivityError) + assert isinstance(err.cause.cause, ApplicationError) + assert ( + str(err.cause.cause) == ActivitiesWithRetryDelayWorkflow.error_message + ) + assert ( + err.cause.cause.next_retry_delay + == ActivitiesWithRetryDelayWorkflow.next_retry_delay ) - # Confirm gives back same handle if requested - new_handle = await client.start_workflow( - IDConflictWorkflow.run, - id=handle.id, - task_queue=worker.task_queue, - id_conflict_policy=WorkflowIDConflictPolicy.USE_EXISTING, - ) - new_handle = client.get_workflow_handle_for( - IDConflictWorkflow.run, new_handle.id, run_id=new_handle.result_run_id - ) - assert new_handle.run_id == handle.run_id - assert (await handle.describe()).status == WorkflowExecutionStatus.RUNNING - assert (await new_handle.describe()).status == WorkflowExecutionStatus.RUNNING - # Confirm terminates and starts new if requested - new_handle = await client.start_workflow( - IDConflictWorkflow.run, - id=handle.id, - task_queue=worker.task_queue, - id_conflict_policy=WorkflowIDConflictPolicy.TERMINATE_EXISTING, - ) - new_handle = client.get_workflow_handle_for( - IDConflictWorkflow.run, new_handle.id, run_id=new_handle.result_run_id - ) - assert new_handle.run_id != handle.run_id - assert (await handle.describe()).status == WorkflowExecutionStatus.TERMINATED - assert (await new_handle.describe()).status == WorkflowExecutionStatus.RUNNING +@workflow.defn +class WorkflowWithoutInit: + value = "from class attribute" + _expected_update_result = "from class attribute" + + @workflow.update + async def my_update(self) -> str: + return self.value + + @workflow.run + async def run(self, _: str) -> str: + self.value = "set in run method" + return self.value @workflow.defn -class UpdateCompletionIsHonoredWhenAfterWorkflowReturn1Workflow: - def __init__(self) -> None: - self.workflow_returned = False +class WorkflowWithWorkflowInit: + _expected_update_result = "workflow input value" + + @workflow.init + def __init__(self, arg: str) -> None: + self.value = arg + + @workflow.update + async def my_update(self) -> str: + return self.value @workflow.run - async def run(self) -> str: - self.workflow_returned = True - return "workflow-result" + async def run(self, _: str) -> str: + self.value = "set in run method" + return self.value + + +@workflow.defn +class WorkflowWithNonWorkflowInitInit: + _expected_update_result = "from parameter default" + + def __init__(self, arg: str = "from parameter default") -> None: + self.value = arg @workflow.update async def my_update(self) -> str: - await workflow.wait_condition(lambda: self.workflow_returned) - return "update-result" + return self.value + @workflow.run + async def run(self, _: str) -> str: + self.value = "set in run method" + return self.value -async def test_update_completion_is_honored_when_after_workflow_return_1( - client: Client, + +@pytest.mark.parametrize( + ["client_cls", "worker_cls"], + [ + (WorkflowWithoutInit, WorkflowWithoutInit), + (WorkflowWithNonWorkflowInitInit, WorkflowWithNonWorkflowInitInit), + (WorkflowWithWorkflowInit, WorkflowWithWorkflowInit), + ], +) +async def test_update_in_first_wft_sees_workflow_init( + client: Client, client_cls: type, worker_cls: type ): - update_id = "my-update" - task_queue = "tq" + """ + Test how @workflow.init affects what an update in the first WFT sees. + + Such an update is guaranteed to start executing before the main workflow + coroutine. The update should see the side effects of the __init__ method if + and only if @workflow.init is in effect. + """ + # This test must ensure that the update is in the first WFT. To do so, + # before running the worker, we start the workflow, send the update, and + # wait until the update is admitted. + task_queue = "task-queue" + update_id = "update-id" wf_handle = await client.start_workflow( - UpdateCompletionIsHonoredWhenAfterWorkflowReturn1Workflow.run, - id=f"wf-{uuid.uuid4()}", + getattr(client_cls, "run"), + "workflow input value", + id=str(uuid.uuid4()), task_queue=task_queue, ) - update_result_task = asyncio.create_task( - wf_handle.execute_update( - UpdateCompletionIsHonoredWhenAfterWorkflowReturn1Workflow.my_update, - id=update_id, - ) + update_task = asyncio.create_task( + wf_handle.execute_update(getattr(client_cls, "my_update"), id=update_id) ) - await workflow_update_exists(client, wf_handle.id, update_id) + await assert_eq_eventually( + True, lambda: workflow_update_exists(client, wf_handle.id, update_id) + ) + # When the worker starts polling it will receive a first WFT containing the + # update, in addition to the start_workflow job. + async with new_worker(client, worker_cls, task_queue=task_queue): + assert await update_task == getattr(worker_cls, "_expected_update_result") + assert await wf_handle.result() == "set in run method" - async with Worker( - client, - task_queue=task_queue, - workflows=[UpdateCompletionIsHonoredWhenAfterWorkflowReturn1Workflow], - ): - assert await wf_handle.result() == "workflow-result" - assert await update_result_task == "update-result" + +@workflow.defn +class WorkflowRunSeesWorkflowInitWorkflow: + @workflow.init + def __init__(self, arg: str) -> None: + self.value = arg + + @workflow.run + async def run(self, _: str): + return f"hello, {self.value}" + + +async def test_workflow_run_sees_workflow_init(client: Client): + async with new_worker(client, WorkflowRunSeesWorkflowInitWorkflow) as worker: + workflow_result = await client.execute_workflow( + WorkflowRunSeesWorkflowInitWorkflow.run, + "world", + id=str(uuid.uuid4()), + task_queue=worker.task_queue, + ) + assert workflow_result == "hello, world" @workflow.defn -class UpdateCompletionIsHonoredWhenAfterWorkflowReturnWorkflow2: - def __init__(self): - self.received_update = False - self.update_result: asyncio.Future[str] = asyncio.Future() +class UserMetadataWorkflow: + def __init__(self) -> None: + self._done = False + self._waiting = False + + @workflow.run + async def run(self) -> None: + await workflow.execute_activity( + say_hello, + "Enchi", + start_to_close_timeout=timedelta(seconds=5), + summary="meow", + ) + # Force timeout, ignore, wait again + try: + await workflow.wait_condition( + lambda: self._done, timeout=0.01, timeout_summary="hi!" + ) + raise RuntimeError("Expected timeout") + except asyncio.TimeoutError: + pass + await workflow.sleep(0.01, summary="timer2") + self._waiting = True + workflow.set_current_details("such detail") + await workflow.wait_condition(lambda: self._done) - @workflow.run - async def run(self) -> str: - await workflow.wait_condition(lambda: self.received_update) - self.update_result.set_result("update-result") - # Prior to https://github.com/temporalio/features/issues/481, the client - # waiting on the update got a "Workflow execution already completed" - # error instead of the update result, because the main workflow - # coroutine completion command is emitted before the update completion - # command, and we were truncating commands at the first completion - # command. - return "workflow-result" + @workflow.signal(description="sdesc") + def done(self) -> None: + self._done = True - @workflow.update - async def my_update(self) -> str: - self.received_update = True - return await self.update_result + @workflow.query(description="qdesc") + def waiting(self) -> bool: + return self._waiting + @workflow.update(description="udesc") + def some_update(self): + pass -async def test_update_completion_is_honored_when_after_workflow_return_2( - client: Client, - env: WorkflowEnvironment, -): - async with Worker( - client, - task_queue="tq", - workflows=[UpdateCompletionIsHonoredWhenAfterWorkflowReturnWorkflow2], + +async def test_user_metadata_is_set(client: Client, env: WorkflowEnvironment): + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2219" + ) + async with new_worker( + client, UserMetadataWorkflow, activities=[say_hello] ) as worker: handle = await client.start_workflow( - UpdateCompletionIsHonoredWhenAfterWorkflowReturnWorkflow2.run, - id=f"wf-{uuid.uuid4()}", + UserMetadataWorkflow.run, + id=f"workflow-{uuid.uuid4()}", task_queue=worker.task_queue, + static_summary="cool workflow bro", + static_details="xtremely detailed", ) - update_result = await handle.execute_update( - UpdateCompletionIsHonoredWhenAfterWorkflowReturnWorkflow2.my_update - ) - assert update_result == "update-result" - assert await handle.result() == "workflow-result" + # Wait until it's waiting, then send the signal + async def waiting() -> bool: + return await handle.query(UserMetadataWorkflow.waiting) -@workflow.defn -class FirstCompletionCommandIsHonoredWorkflow: - def __init__( - self, main_workflow_returns_before_signal_completions: bool = False - ) -> None: - self.seen_first_signal = False - self.seen_second_signal = False - self.main_workflow_returns_before_signal_completions = ( - main_workflow_returns_before_signal_completions - ) - self.ping_pong_val = 1 - self.ping_pong_counter = 0 - self.ping_pong_max_count = 4 + await assert_eq_eventually(True, waiting) - @workflow.run - async def run(self) -> str: - await workflow.wait_condition( - lambda: self.seen_first_signal and self.seen_second_signal + md_query: temporalio.api.sdk.v1.WorkflowMetadata = await handle.query( + "__temporal_workflow_metadata", + result_type=temporalio.api.sdk.v1.WorkflowMetadata, ) - return "workflow-result" + matched_q = [ + q for q in md_query.definition.query_definitions if q.name == "waiting" + ] + assert len(matched_q) == 1 + assert matched_q[0].description == "qdesc" - @workflow.signal - async def this_signal_executes_first(self): - self.seen_first_signal = True - if self.main_workflow_returns_before_signal_completions: - await self.ping_pong(lambda: self.ping_pong_val > 0) - raise ApplicationError( - "Client should see this error unless doing ping-pong " - "(in which case main coroutine returns first)" - ) + matched_u = [ + u for u in md_query.definition.update_definitions if u.name == "some_update" + ] + assert len(matched_u) == 1 + assert matched_u[0].description == "udesc" - @workflow.signal - async def this_signal_executes_second(self): - await workflow.wait_condition(lambda: self.seen_first_signal) - self.seen_second_signal = True - if self.main_workflow_returns_before_signal_completions: - await self.ping_pong(lambda: self.ping_pong_val < 0) - raise ApplicationError("Client should never see this error!") + matched_s = [ + s for s in md_query.definition.signal_definitions if s.name == "done" + ] + assert len(matched_s) == 1 + assert matched_s[0].description == "sdesc" - async def ping_pong(self, cond: Callable[[], bool]): - while self.ping_pong_counter < self.ping_pong_max_count: - await workflow.wait_condition(cond) - self.ping_pong_val = -self.ping_pong_val - self.ping_pong_counter += 1 + assert md_query.current_details == "such detail" + await handle.signal(UserMetadataWorkflow.done) + await handle.result() -@workflow.defn -class FirstCompletionCommandIsHonoredPingPongWorkflow( - FirstCompletionCommandIsHonoredWorkflow -): - def __init__(self) -> None: - super().__init__(main_workflow_returns_before_signal_completions=True) + # Ensure metadatas are present in history + resp = await client.workflow_service.get_workflow_execution_history( + GetWorkflowExecutionHistoryRequest( + namespace=client.namespace, + execution=WorkflowExecution(workflow_id=handle.id), + ) + ) + timer_summs = set() + for event in resp.history.events: + if event.event_type == EventType.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED: + assert "cool workflow bro" in PayloadConverter.default.from_payload( + event.user_metadata.summary + ) + assert "xtremely detailed" in PayloadConverter.default.from_payload( + event.user_metadata.details + ) + elif event.event_type == EventType.EVENT_TYPE_ACTIVITY_TASK_SCHEDULED: + assert "meow" in PayloadConverter.default.from_payload( + event.user_metadata.summary + ) + elif event.event_type == EventType.EVENT_TYPE_TIMER_STARTED: + timer_summs.add( + PayloadConverter.default.from_payload(event.user_metadata.summary) + ) + assert timer_summs == {"hi!", "timer2"} + + describe_r = await handle.describe() + assert await describe_r.static_summary() == "cool workflow bro" + assert await describe_r.static_details() == "xtremely detailed" + +@workflow.defn +class WorkflowSleepWorkflow: @workflow.run - async def run(self) -> str: - return await super().run() + async def run(self) -> float: + start_time = workflow.time() + await workflow.sleep(1) + return workflow.time() - start_time -async def test_first_of_two_signal_completion_commands_is_honored(client: Client): - await _do_first_completion_command_is_honored_test( - client, main_workflow_returns_before_signal_completions=False - ) +async def test_workflow_sleep(client: Client, env: WorkflowEnvironment): + async with new_worker(client, WorkflowSleepWorkflow) as worker: + start_time = datetime.now() + workflow_elapsed = await client.execute_workflow( + WorkflowSleepWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + assert workflow_elapsed >= 1 + if not env.supports_time_skipping: + assert (datetime.now() - start_time) >= timedelta(seconds=1) -async def test_workflow_return_is_honored_when_it_precedes_signal_completion_command( - client: Client, -): - await _do_first_completion_command_is_honored_test( - client, main_workflow_returns_before_signal_completions=True - ) +@workflow.defn +class ConcurrentSleepsWorkflow: + @workflow.run + async def run(self) -> None: + sleeps_a = [workflow.sleep(0.1, summary=f"t{i}") for i in range(5)] + zero_a = workflow.sleep(0, summary="zero_timer") + wait_some = workflow.wait_condition( + lambda: False, timeout=0.1, timeout_summary="wait_some" + ) + zero_b = workflow.wait_condition( + lambda: False, timeout=0, timeout_summary="zero_wait" + ) + no_summ = workflow.sleep(0.1) + sleeps_b = [workflow.sleep(0.1, summary=f"t{i}") for i in range(5, 10)] + try: + await asyncio.gather( + *sleeps_a, + zero_a, + wait_some, + zero_b, + no_summ, + *sleeps_b, + return_exceptions=True, + ) + except asyncio.TimeoutError: + pass + + task_1 = asyncio.create_task(self.make_timers(100, 105)) + task_2 = asyncio.create_task(self.make_timers(105, 110)) + await asyncio.gather(task_1, task_2) + async def make_timers(self, start: int, end: int): + await asyncio.gather( + *[workflow.sleep(0.1, summary=f"m_t{i}") for i in range(start, end)] + ) -async def _do_first_completion_command_is_honored_test( - client: Client, main_workflow_returns_before_signal_completions: bool + +async def test_concurrent_sleeps_use_proper_options( + client: Client, env: WorkflowEnvironment ): - workflow_cls: Union[ - Type[FirstCompletionCommandIsHonoredPingPongWorkflow], - Type[FirstCompletionCommandIsHonoredWorkflow], - ] = ( - FirstCompletionCommandIsHonoredPingPongWorkflow - if main_workflow_returns_before_signal_completions - else FirstCompletionCommandIsHonoredWorkflow - ) - async with Worker( - client, - task_queue="tq", - workflows=[workflow_cls], - ) as worker: + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/2219" + ) + async with new_worker(client, ConcurrentSleepsWorkflow) as worker: handle = await client.start_workflow( - workflow_cls.run, - id=f"wf-{uuid.uuid4()}", + ConcurrentSleepsWorkflow.run, + id=f"workflow-{uuid.uuid4()}", task_queue=worker.task_queue, ) - await handle.signal(workflow_cls.this_signal_executes_second) - await handle.signal(workflow_cls.this_signal_executes_first) - try: - result = await handle.result() - except WorkflowFailureError as err: - if main_workflow_returns_before_signal_completions: - raise RuntimeError( - "Expected no error due to main workflow coroutine returning first" - ) - else: - assert str(err.cause).startswith("Client should see this error") - else: - assert ( - main_workflow_returns_before_signal_completions - and result == "workflow-result" + await handle.result() + resp = await client.workflow_service.get_workflow_execution_history( + GetWorkflowExecutionHistoryRequest( + namespace=client.namespace, + execution=WorkflowExecution(workflow_id=handle.id), ) + ) + timer_summaries = [ + PayloadConverter.default.from_payload(e.user_metadata.summary) + if e.user_metadata.HasField("summary") + else "" + for e in resp.history.events + if e.event_type == EventType.EVENT_TYPE_TIMER_STARTED + ] + assert timer_summaries == [ + *[f"t{i}" for i in range(5)], + "zero_timer", + "wait_some", + "", + *[f"t{i}" for i in range(5, 10)], + *[f"m_t{i}" for i in range(100, 110)], + ] + # Force replay with a query to ensure determinism + await handle.query("__temporal_workflow_metadata") -@workflow.defn -class TimerStartedAfterWorkflowCompletionWorkflow: - def __init__(self) -> None: - self.received_signal = False - self.main_workflow_coroutine_finished = False - - @workflow.run - async def run(self) -> str: - await workflow.wait_condition(lambda: self.received_signal) - self.main_workflow_coroutine_finished = True - return "workflow-result" - @workflow.signal(unfinished_policy=workflow.HandlerUnfinishedPolicy.ABANDON) - async def my_signal(self): - self.received_signal = True - await workflow.wait_condition(lambda: self.main_workflow_coroutine_finished) - await asyncio.sleep(7777777) +class BadFailureConverterError(Exception): + pass -async def test_timer_started_after_workflow_completion(client: Client): - async with new_worker( - client, TimerStartedAfterWorkflowCompletionWorkflow - ) as worker: - handle = await client.start_workflow( - TimerStartedAfterWorkflowCompletionWorkflow.run, - id=f"wf-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) - await handle.signal(TimerStartedAfterWorkflowCompletionWorkflow.my_signal) - assert await handle.result() == "workflow-result" +class BadFailureConverter(DefaultFailureConverter): + def to_failure( + self, + exception: BaseException, + payload_converter: PayloadConverter, + failure: Failure, + ) -> None: + if isinstance(exception, BadFailureConverterError): + raise RuntimeError("Intentional failure conversion error") + super().to_failure(exception, payload_converter, failure) @activity.defn -async def activity_with_retry_delay(): - raise ApplicationError( - ActivitiesWithRetryDelayWorkflow.error_message, - next_retry_delay=ActivitiesWithRetryDelayWorkflow.next_retry_delay, - ) - +async def bad_failure_converter_activity() -> None: + raise BadFailureConverterError -@workflow.defn -class ActivitiesWithRetryDelayWorkflow: - error_message = "Deliberately failing with next_retry_delay set" - next_retry_delay = timedelta(milliseconds=5) +@workflow.defn(sandboxed=False) +class BadFailureConverterWorkflow: @workflow.run - async def run(self) -> None: - await workflow.execute_activity( - activity_with_retry_delay, - retry_policy=RetryPolicy(maximum_attempts=2), - schedule_to_close_timeout=timedelta(minutes=5), - ) + async def run(self, fail_workflow_task: bool) -> None: + if fail_workflow_task: + raise BadFailureConverterError + else: + await workflow.execute_activity( + bad_failure_converter_activity, + schedule_to_close_timeout=timedelta(seconds=30), + retry_policy=RetryPolicy(maximum_attempts=1), + ) -async def test_activity_retry_delay(client: Client): +async def test_bad_failure_converter(client: Client): + config = client.config() + config["data_converter"] = dataclasses.replace( + config["data_converter"], + failure_converter_class=BadFailureConverter, + ) + client = Client(**config) async with new_worker( - client, ActivitiesWithRetryDelayWorkflow, activities=[activity_with_retry_delay] + client, BadFailureConverterWorkflow, activities=[bad_failure_converter_activity] ) as worker: - try: + # Check activity + with pytest.raises(WorkflowFailureError) as err: await client.execute_workflow( - ActivitiesWithRetryDelayWorkflow.run, - id=str(uuid.uuid4()), + BadFailureConverterWorkflow.run, + False, + id=f"workflow-{uuid.uuid4()}", task_queue=worker.task_queue, ) - except WorkflowFailureError as err: - assert isinstance(err.cause, ActivityError) - assert isinstance(err.cause.cause, ApplicationError) - assert ( - str(err.cause.cause) == ActivitiesWithRetryDelayWorkflow.error_message - ) - assert ( - err.cause.cause.next_retry_delay - == ActivitiesWithRetryDelayWorkflow.next_retry_delay - ) - + assert isinstance(err.value.cause, ActivityError) + assert isinstance(err.value.cause.cause, ApplicationError) + assert ( + err.value.cause.cause.message + == "Failed building exception result: Intentional failure conversion error" + ) -@workflow.defn -class WorkflowWithoutInit: - value = "from class attribute" - _expected_update_result = "from class attribute" + # Check workflow + handle = await client.start_workflow( + BadFailureConverterWorkflow.run, + True, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) - @workflow.update - async def my_update(self) -> str: - return self.value + async def task_failed_message() -> str | None: + async for e in handle.fetch_history_events(): + if e.HasField("workflow_task_failed_event_attributes"): + return e.workflow_task_failed_event_attributes.failure.message + return None - @workflow.run - async def run(self, _: str) -> str: - self.value = "set in run method" - return self.value + await assert_eq_eventually( + "Failed converting activation exception: Intentional failure conversion error", + task_failed_message, # type: ignore + ) @workflow.defn -class WorkflowWithWorkflowInit: - _expected_update_result = "workflow input value" - - @workflow.init - def __init__(self, arg: str) -> None: - self.value = arg +class SignalsActivitiesTimersUpdatesTracingWorkflow: + """ + These handlers all do different things that will cause the event loop to yield, sometimes + until the next workflow task (ex: timer) sometimes within the workflow task (ex: future resolve + or wait condition). + """ - @workflow.update - async def my_update(self) -> str: - return self.value + def __init__(self) -> None: + self.events: list[str] = [] @workflow.run - async def run(self, _: str) -> str: - self.value = "set in run method" - return self.value - - -@workflow.defn -class WorkflowWithNonWorkflowInitInit: - _expected_update_result = "from parameter default" + async def run(self) -> list[str]: + tt = asyncio.create_task(self.run_timer()) + at = asyncio.create_task(self.run_act()) + await asyncio.gather(tt, at) + return self.events - def __init__(self, arg: str = "from parameter default") -> None: - self.value = arg + @workflow.signal + async def dosig(self, name: str): + self.events.append(f"sig-{name}-sync") + fut: asyncio.Future[bool] = asyncio.Future() + fut.set_result(True) + await fut + self.events.append(f"sig-{name}-1") + await workflow.wait_condition(lambda: True) + self.events.append(f"sig-{name}-2") @workflow.update - async def my_update(self) -> str: - return self.value - - @workflow.run - async def run(self, _: str) -> str: - self.value = "set in run method" - return self.value - - -@pytest.mark.parametrize( - ["client_cls", "worker_cls"], - [ - (WorkflowWithoutInit, WorkflowWithoutInit), - (WorkflowWithNonWorkflowInitInit, WorkflowWithNonWorkflowInitInit), - (WorkflowWithWorkflowInit, WorkflowWithWorkflowInit), - ], -) -async def test_update_in_first_wft_sees_workflow_init( - client: Client, client_cls: Type, worker_cls: Type -): - """ - Test how @workflow.init affects what an update in the first WFT sees. + async def doupdate(self, name: str): + self.events.append(f"update-{name}-sync") + fut: asyncio.Future[bool] = asyncio.Future() + fut.set_result(True) + await fut + self.events.append(f"update-{name}-1") + await workflow.wait_condition(lambda: True) + self.events.append(f"update-{name}-2") - Such an update is guaranteed to start executing before the main workflow - coroutine. The update should see the side effects of the __init__ method if - and only if @workflow.init is in effect. - """ - # This test must ensure that the update is in the first WFT. To do so, - # before running the worker, we start the workflow, send the update, and - # wait until the update is admitted. - task_queue = "task-queue" - update_id = "update-id" - wf_handle = await client.start_workflow( - client_cls.run, - "workflow input value", - id=str(uuid.uuid4()), - task_queue=task_queue, - ) - update_task = asyncio.create_task( - wf_handle.execute_update(client_cls.my_update, id=update_id) - ) - await assert_eq_eventually( - True, lambda: workflow_update_exists(client, wf_handle.id, update_id) - ) - # When the worker starts polling it will receive a first WFT containing the - # update, in addition to the start_workflow job. - async with new_worker(client, worker_cls, task_queue=task_queue): - assert await update_task == worker_cls._expected_update_result - assert await wf_handle.result() == "set in run method" + async def run_timer(self): + self.events.append("timer-sync") + await workflow.sleep(0.1) + fut: asyncio.Future[bool] = asyncio.Future() + fut.set_result(True) + await fut + self.events.append("timer-1") + await workflow.wait_condition(lambda: True) + self.events.append("timer-2") + async def run_act(self): + self.events.append("act-sync") + await workflow.execute_activity( + say_hello, "Enchi", schedule_to_close_timeout=timedelta(seconds=30) + ) + fut: asyncio.Future[bool] = asyncio.Future() + fut.set_result(True) + await fut + self.events.append("act-1") + await workflow.wait_condition(lambda: True) + self.events.append("act-2") -@workflow.defn -class WorkflowRunSeesWorkflowInitWorkflow: - @workflow.init - def __init__(self, arg: str) -> None: - self.value = arg - @workflow.run - async def run(self, _: str): - return f"hello, {self.value}" +async def test_async_loop_ordering(client: Client, env: WorkflowEnvironment): + """This test mostly exists to generate PROCESS_WORKFLOW_ACTIVATION_JOBS_AS_SINGLE_BATCH + histories for test_replayer_async_ordering. See that test for more.""" + if env.supports_time_skipping: + pytest.skip("This test doesn't work right with time skipping for some reason") + task_queue = f"tq-{uuid.uuid4()}" + handle = await client.start_workflow( + SignalsActivitiesTimersUpdatesTracingWorkflow.run, + id=f"wf-{uuid.uuid4()}", + task_queue=task_queue, + ) + await handle.signal(SignalsActivitiesTimersUpdatesTracingWorkflow.dosig, "before") -async def test_workflow_run_sees_workflow_init(client: Client): - async with new_worker(client, WorkflowRunSeesWorkflowInitWorkflow) as worker: - workflow_result = await client.execute_workflow( - WorkflowRunSeesWorkflowInitWorkflow.run, - "world", - id=str(uuid.uuid4()), - task_queue=worker.task_queue, + worker = new_worker( + client, + SignalsActivitiesTimersUpdatesTracingWorkflow, + activities=[say_hello], + task_queue=task_queue, + ) + worker._set_default_workflow_logic_flag( + _SINGLE_BATCH_WORKFLOW_LOGIC_FLAG, enabled=True + ) + async with worker: + await asyncio.sleep(0.2) + await handle.signal(SignalsActivitiesTimersUpdatesTracingWorkflow.dosig, "1") + await handle.execute_update( + SignalsActivitiesTimersUpdatesTracingWorkflow.doupdate, "1" ) - assert workflow_result == "hello, world" + await handle.result() @workflow.defn -class UserMetadataWorkflow: +class ActivityAndSignalsWhileWorkflowDown: def __init__(self) -> None: - self._done = False - self._waiting = False + self.events: list[str] = [] + self.counter = 0 @workflow.run - async def run(self) -> None: + async def run(self, activity_tq: str) -> list[str]: + act_task = asyncio.create_task(self.run_act(activity_tq)) + await workflow.wait_condition(lambda: self.counter >= 2) + self.events.append(f"counter-{self.counter}") + await act_task + return self.events + + @workflow.signal + async def dosig(self, name: str): + self.events.append(f"sig-{name}") + self.counter += 1 + + async def run_act(self, activity_tq: str): + self.events.append("act-start") await workflow.execute_activity( say_hello, "Enchi", - start_to_close_timeout=timedelta(seconds=5), - summary="meow", + schedule_to_close_timeout=timedelta(seconds=30), + task_queue=activity_tq, ) - # Force timeout, ignore, wait again - try: - await workflow.wait_condition( - lambda: self._done, timeout=0.01, timeout_summary="hi!" - ) - raise RuntimeError("Expected timeout") - except asyncio.TimeoutError: - pass - await workflow.sleep(0.01, summary="timer2") - self._waiting = True - workflow.set_current_details("such detail") - await workflow.wait_condition(lambda: self._done) + self.counter += 1 + self.events.append("act-done") - @workflow.signal(description="sdesc") - def done(self) -> None: - self._done = True - @workflow.query(description="qdesc") - def waiting(self) -> bool: - return self._waiting +async def test_alternate_async_loop_ordering(client: Client, env: WorkflowEnvironment): + """This test mostly exists to generate histories for test_replayer_alternate_async_ordering. + See that test for more.""" - @workflow.update(description="udesc") - def some_update(self): - pass + if env.supports_time_skipping: + pytest.skip("This test doesn't work right with time skipping for some reason") + task_queue = f"tq-{uuid.uuid4()}" + activity_tq = f"tq-{uuid.uuid4()}" + handle = await client.start_workflow( + ActivityAndSignalsWhileWorkflowDown.run, + activity_tq, + id=f"wf-{uuid.uuid4()}", + task_queue=task_queue, + ) + async with new_worker( + client, + ActivityAndSignalsWhileWorkflowDown, + activities=[say_hello], + task_queue=task_queue, + ): + # This sleep exists to make sure the first WFT is processed + await asyncio.sleep(0.2) -async def test_user_metadata_is_set(client: Client, env: WorkflowEnvironment): - if env.supports_time_skipping: - pytest.skip( - "Java test server: https://github.com/temporalio/sdk-java/issues/2219" - ) async with new_worker( - client, UserMetadataWorkflow, activities=[say_hello] - ) as worker: - handle = await client.start_workflow( - UserMetadataWorkflow.run, - id=f"workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - static_summary="cool workflow bro", - static_details="xtremely detailed", - ) + client, + activities=[say_hello], + task_queue=activity_tq, + ): + # Make sure the activity starts being processed before sending signals + await asyncio.sleep(1) + await handle.signal(ActivityAndSignalsWhileWorkflowDown.dosig, "1") + await handle.signal(ActivityAndSignalsWhileWorkflowDown.dosig, "2") - # Wait until it's waiting, then send the signal - async def waiting() -> bool: - return await handle.query(UserMetadataWorkflow.waiting) + async with new_worker( + client, + ActivityAndSignalsWhileWorkflowDown, + activities=[say_hello], + task_queue=task_queue, + ): + await handle.result() - await assert_eq_eventually(True, waiting) - md_query: temporalio.api.sdk.v1.WorkflowMetadata = await handle.query( - "__temporal_workflow_metadata", - result_type=temporalio.api.sdk.v1.WorkflowMetadata, - ) - matched_q = [ - q for q in md_query.definition.query_definitions if q.name == "waiting" - ] - assert len(matched_q) == 1 - assert matched_q[0].description == "qdesc" +# The following Lock and Semaphore tests test that asyncio concurrency primitives work as expected +# in workflow code. There is nothing Temporal-specific about the way that asyncio.Lock and +# asyncio.Semaphore are used here. - matched_u = [ - u for u in md_query.definition.update_definitions if u.name == "some_update" - ] - assert len(matched_u) == 1 - assert matched_u[0].description == "udesc" - matched_s = [ - s for s in md_query.definition.signal_definitions if s.name == "done" - ] - assert len(matched_s) == 1 - assert matched_s[0].description == "sdesc" +@activity.defn +async def noop_activity_for_lock_or_semaphore_tests() -> None: + return None - assert md_query.current_details == "such detail" - await handle.signal(UserMetadataWorkflow.done) - await handle.result() +@dataclass +class LockOrSemaphoreWorkflowConcurrencySummary: + ever_in_critical_section: int + peak_in_critical_section: int - # Ensure metadatas are present in history - resp = await client.workflow_service.get_workflow_execution_history( - GetWorkflowExecutionHistoryRequest( - namespace=client.namespace, - execution=WorkflowExecution(workflow_id=handle.id), - ) - ) - timer_summs = set() - for event in resp.history.events: - if event.event_type == EventType.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED: - assert "cool workflow bro" in PayloadConverter.default.from_payload( - event.user_metadata.summary - ) - assert "xtremely detailed" in PayloadConverter.default.from_payload( - event.user_metadata.details - ) - elif event.event_type == EventType.EVENT_TYPE_ACTIVITY_TASK_SCHEDULED: - assert "meow" in PayloadConverter.default.from_payload( - event.user_metadata.summary - ) - elif event.event_type == EventType.EVENT_TYPE_TIMER_STARTED: - timer_summs.add( - PayloadConverter.default.from_payload(event.user_metadata.summary) - ) - assert timer_summs == {"hi!", "timer2"} - describe_r = await handle.describe() - assert await describe_r.static_summary() == "cool workflow bro" - assert await describe_r.static_details() == "xtremely detailed" +@dataclass +class UseLockOrSemaphoreWorkflowParameters: + n_coroutines: int = 0 + semaphore_initial_value: int | None = None + sleep: float | None = None + timeout: float | None = None @workflow.defn -class WorkflowSleepWorkflow: +class CoroutinesUseLockOrSemaphoreWorkflow: + def __init__(self) -> None: + self.params: UseLockOrSemaphoreWorkflowParameters + self.lock_or_semaphore: asyncio.Lock | asyncio.Semaphore + self._currently_in_critical_section: set[str] = set() + self._ever_in_critical_section: set[str] = set() + self._peak_in_critical_section = 0 + + def init(self, params: UseLockOrSemaphoreWorkflowParameters): + self.params = params + if self.params.semaphore_initial_value is not None: + self.lock_or_semaphore = asyncio.Semaphore( + self.params.semaphore_initial_value + ) + else: + self.lock_or_semaphore = asyncio.Lock() + @workflow.run - async def run(self) -> None: - await workflow.sleep(1) + async def run( + self, + params: UseLockOrSemaphoreWorkflowParameters | None, + ) -> LockOrSemaphoreWorkflowConcurrencySummary: + # TODO: Use workflow init method when it exists. + assert params + self.init(params) + await asyncio.gather( + *(self.coroutine(f"{i}") for i in range(self.params.n_coroutines)) + ) + assert not any(self._currently_in_critical_section) + return LockOrSemaphoreWorkflowConcurrencySummary( + len(self._ever_in_critical_section), + self._peak_in_critical_section, + ) + async def coroutine(self, id: str): + if self.params.timeout: + try: + await asyncio.wait_for( + self.lock_or_semaphore.acquire(), self.params.timeout + ) + except asyncio.TimeoutError: + return + else: + await self.lock_or_semaphore.acquire() + self._enters_critical_section(id) + try: + if self.params.sleep: + await asyncio.sleep(self.params.sleep) + else: + await workflow.execute_activity( + noop_activity_for_lock_or_semaphore_tests, + schedule_to_close_timeout=timedelta(seconds=30), + ) + finally: + self.lock_or_semaphore.release() + self._exits_critical_section(id) -async def test_workflow_sleep(client: Client): - async with new_worker(client, WorkflowSleepWorkflow) as worker: - start_time = datetime.now() - await client.execute_workflow( - WorkflowSleepWorkflow.run, - id=f"workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, + def _enters_critical_section(self, id: str) -> None: + self._currently_in_critical_section.add(id) + self._ever_in_critical_section.add(id) + self._peak_in_critical_section = max( + self._peak_in_critical_section, + len(self._currently_in_critical_section), ) - assert (datetime.now() - start_time) >= timedelta(seconds=1) + + def _exits_critical_section(self, id: str) -> None: + self._currently_in_critical_section.remove(id) @workflow.defn -class ConcurrentSleepsWorkflow: +class HandlerCoroutinesUseLockOrSemaphoreWorkflow(CoroutinesUseLockOrSemaphoreWorkflow): + def __init__(self) -> None: + super().__init__() + self.workflow_may_exit = False + @workflow.run - async def run(self) -> None: - sleeps_a = [workflow.sleep(0.1, summary=f"t{i}") for i in range(5)] - zero_a = workflow.sleep(0, summary="zero_timer") - wait_some = workflow.wait_condition( - lambda: False, timeout=0.1, timeout_summary="wait_some" - ) - zero_b = workflow.wait_condition( - lambda: False, timeout=0, timeout_summary="zero_wait" + async def run( + self, + params: UseLockOrSemaphoreWorkflowParameters | None = None, + ) -> LockOrSemaphoreWorkflowConcurrencySummary: + await workflow.wait_condition(lambda: self.workflow_may_exit) + return LockOrSemaphoreWorkflowConcurrencySummary( + len(self._ever_in_critical_section), + self._peak_in_critical_section, ) - no_summ = workflow.sleep(0.1) - sleeps_b = [workflow.sleep(0.1, summary=f"t{i}") for i in range(5, 10)] - try: - await asyncio.gather( - *sleeps_a, - zero_a, - wait_some, - zero_b, - no_summ, - *sleeps_b, - return_exceptions=True, - ) - except asyncio.TimeoutError: - pass - task_1 = asyncio.create_task(self.make_timers(100, 105)) - task_2 = asyncio.create_task(self.make_timers(105, 110)) - await asyncio.gather(task_1, task_2) + @workflow.update + async def my_update(self, params: UseLockOrSemaphoreWorkflowParameters): + # TODO: Use workflow init method when it exists. + if not hasattr(self, "params"): + self.init(params) + assert (update_info := workflow.current_update_info()) + await self.coroutine(update_info.id) - async def make_timers(self, start: int, end: int): - await asyncio.gather( - *[workflow.sleep(0.1, summary=f"m_t{i}") for i in range(start, end)] - ) + @workflow.signal + async def finish(self): + self.workflow_may_exit = True -async def test_concurrent_sleeps_use_proper_options( - client: Client, env: WorkflowEnvironment +async def _do_workflow_coroutines_lock_or_semaphore_test( + client: Client, + params: UseLockOrSemaphoreWorkflowParameters, + expectation: LockOrSemaphoreWorkflowConcurrencySummary, ): - if env.supports_time_skipping: - pytest.skip( - "Java test server: https://github.com/temporalio/sdk-java/issues/2219" - ) - async with new_worker(client, ConcurrentSleepsWorkflow) as worker: - handle = await client.start_workflow( - ConcurrentSleepsWorkflow.run, - id=f"workflow-{uuid.uuid4()}", + async with new_worker( + client, + CoroutinesUseLockOrSemaphoreWorkflow, + activities=[noop_activity_for_lock_or_semaphore_tests], + ) as worker: + summary = await client.execute_workflow( + CoroutinesUseLockOrSemaphoreWorkflow.run, + arg=params, + id=str(uuid.uuid4()), task_queue=worker.task_queue, ) - await handle.result() - resp = await client.workflow_service.get_workflow_execution_history( - GetWorkflowExecutionHistoryRequest( - namespace=client.namespace, - execution=WorkflowExecution(workflow_id=handle.id), - ) - ) - timer_summaries = [ - PayloadConverter.default.from_payload(e.user_metadata.summary) - if e.user_metadata.HasField("summary") - else "" - for e in resp.history.events - if e.event_type == EventType.EVENT_TYPE_TIMER_STARTED - ] - assert timer_summaries == [ - *[f"t{i}" for i in range(5)], - "zero_timer", - "wait_some", - "", - *[f"t{i}" for i in range(5, 10)], - *[f"m_t{i}" for i in range(100, 110)], - ] + assert summary == expectation - # Force replay with a query to ensure determinism - await handle.query("__temporal_workflow_metadata") +async def _do_update_handler_lock_or_semaphore_test( + client: Client, + env: WorkflowEnvironment, + params: UseLockOrSemaphoreWorkflowParameters, + n_updates: int, + expectation: LockOrSemaphoreWorkflowConcurrencySummary, +): + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/1903" + ) -class BadFailureConverterError(Exception): - pass + task_queue = "tq" + handle = await client.start_workflow( + HandlerCoroutinesUseLockOrSemaphoreWorkflow.run, + id=f"wf-{str(uuid.uuid4())}", + task_queue=task_queue, + ) + # Create updates in Admitted state, before the worker starts polling. + admitted_updates = [ + await admitted_update_task( + client, + handle, + HandlerCoroutinesUseLockOrSemaphoreWorkflow.my_update, + arg=params, + id=f"update-{i}", + ) + for i in range(n_updates) + ] + async with new_worker( + client, + HandlerCoroutinesUseLockOrSemaphoreWorkflow, + activities=[noop_activity_for_lock_or_semaphore_tests], + task_queue=task_queue, + ): + for update_task in admitted_updates: + await update_task + await handle.signal(HandlerCoroutinesUseLockOrSemaphoreWorkflow.finish) + summary = await handle.result() + assert summary == expectation -class BadFailureConverter(DefaultFailureConverter): - def to_failure( - self, - exception: BaseException, - payload_converter: PayloadConverter, - failure: Failure, - ) -> None: - if isinstance(exception, BadFailureConverterError): - raise RuntimeError("Intentional failure conversion error") - super().to_failure(exception, payload_converter, failure) +async def test_workflow_coroutines_can_use_lock(client: Client): + await _do_workflow_coroutines_lock_or_semaphore_test( + client, + UseLockOrSemaphoreWorkflowParameters(n_coroutines=5), + # The lock limits concurrency to 1 + expectation=LockOrSemaphoreWorkflowConcurrencySummary( + ever_in_critical_section=5, peak_in_critical_section=1 + ), + ) -@activity.defn -async def bad_failure_converter_activity() -> None: - raise BadFailureConverterError +async def test_update_handler_can_use_lock_to_serialize_handler_executions( + client: Client, env: WorkflowEnvironment +): + await _do_update_handler_lock_or_semaphore_test( + client, + env, + UseLockOrSemaphoreWorkflowParameters(), + n_updates=5, + # The lock limits concurrency to 1 + expectation=LockOrSemaphoreWorkflowConcurrencySummary( + ever_in_critical_section=5, peak_in_critical_section=1 + ), + ) -@workflow.defn(sandboxed=False) -class BadFailureConverterWorkflow: - @workflow.run - async def run(self, fail_workflow_task: bool) -> None: - if fail_workflow_task: - raise BadFailureConverterError - else: - await workflow.execute_activity( - bad_failure_converter_activity, - schedule_to_close_timeout=timedelta(seconds=30), - retry_policy=RetryPolicy(maximum_attempts=1), - ) +async def test_workflow_coroutines_lock_acquisition_respects_timeout(client: Client): + await _do_workflow_coroutines_lock_or_semaphore_test( + client, + UseLockOrSemaphoreWorkflowParameters(n_coroutines=5, sleep=0.5, timeout=0.1), + # Second and subsequent coroutines fail to acquire the lock due to the timeout. + expectation=LockOrSemaphoreWorkflowConcurrencySummary( + ever_in_critical_section=1, peak_in_critical_section=1 + ), + ) -async def test_bad_failure_converter(client: Client): - config = client.config() - config["data_converter"] = dataclasses.replace( - config["data_converter"], - failure_converter_class=BadFailureConverter, +async def test_update_handler_lock_acquisition_respects_timeout( + client: Client, env: WorkflowEnvironment +): + await _do_update_handler_lock_or_semaphore_test( + client, + env, + # Second and subsequent handler executions fail to acquire the lock due to the timeout. + UseLockOrSemaphoreWorkflowParameters(sleep=0.5, timeout=0.1), + n_updates=5, + expectation=LockOrSemaphoreWorkflowConcurrencySummary( + ever_in_critical_section=1, peak_in_critical_section=1 + ), ) - client = Client(**config) - async with new_worker( - client, BadFailureConverterWorkflow, activities=[bad_failure_converter_activity] - ) as worker: - # Check activity - with pytest.raises(WorkflowFailureError) as err: - await client.execute_workflow( - BadFailureConverterWorkflow.run, - False, - id=f"workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) - assert isinstance(err.value.cause, ActivityError) - assert isinstance(err.value.cause.cause, ApplicationError) - assert ( - err.value.cause.cause.message - == "Failed building exception result: Intentional failure conversion error" - ) - - # Check workflow - handle = await client.start_workflow( - BadFailureConverterWorkflow.run, - True, - id=f"workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) - async def task_failed_message() -> Optional[str]: - async for e in handle.fetch_history_events(): - if e.HasField("workflow_task_failed_event_attributes"): - return e.workflow_task_failed_event_attributes.failure.message - return None - await assert_eq_eventually( - "Failed converting activation exception: Intentional failure conversion error", - task_failed_message, # type: ignore - ) +async def test_workflow_coroutines_can_use_semaphore(client: Client): + await _do_workflow_coroutines_lock_or_semaphore_test( + client, + UseLockOrSemaphoreWorkflowParameters(n_coroutines=5, semaphore_initial_value=3), + # The semaphore limits concurrency to 3 + expectation=LockOrSemaphoreWorkflowConcurrencySummary( + ever_in_critical_section=5, peak_in_critical_section=3 + ), + ) -@workflow.defn -class SignalsActivitiesTimersUpdatesTracingWorkflow: - """ - These handlers all do different things that will cause the event loop to yield, sometimes - until the next workflow task (ex: timer) sometimes within the workflow task (ex: future resolve - or wait condition). - """ +async def test_update_handler_can_use_semaphore_to_control_handler_execution_concurrency( + client: Client, env: WorkflowEnvironment +): + await _do_update_handler_lock_or_semaphore_test( + client, + env, + # The semaphore limits concurrency to 3 + UseLockOrSemaphoreWorkflowParameters(semaphore_initial_value=3), + n_updates=5, + expectation=LockOrSemaphoreWorkflowConcurrencySummary( + ever_in_critical_section=5, peak_in_critical_section=3 + ), + ) - def __init__(self) -> None: - self.events: List[str] = [] - @workflow.run - async def run(self) -> List[str]: - tt = asyncio.create_task(self.run_timer()) - at = asyncio.create_task(self.run_act()) - await asyncio.gather(tt, at) - return self.events +async def test_workflow_coroutine_semaphore_acquisition_respects_timeout( + client: Client, +): + await _do_workflow_coroutines_lock_or_semaphore_test( + client, + UseLockOrSemaphoreWorkflowParameters( + n_coroutines=5, semaphore_initial_value=3, sleep=0.5, timeout=0.1 + ), + # Initial entry to the semaphore succeeds, but all subsequent attempts to acquire a semaphore + # slot fail. + expectation=LockOrSemaphoreWorkflowConcurrencySummary( + ever_in_critical_section=3, peak_in_critical_section=3 + ), + ) - @workflow.signal - async def dosig(self, name: str): - self.events.append(f"sig-{name}-sync") - fut: asyncio.Future[bool] = asyncio.Future() - fut.set_result(True) - await fut - self.events.append(f"sig-{name}-1") - await workflow.wait_condition(lambda: True) - self.events.append(f"sig-{name}-2") - @workflow.update - async def doupdate(self, name: str): - self.events.append(f"update-{name}-sync") - fut: asyncio.Future[bool] = asyncio.Future() - fut.set_result(True) - await fut - self.events.append(f"update-{name}-1") - await workflow.wait_condition(lambda: True) - self.events.append(f"update-{name}-2") +async def test_update_handler_semaphore_acquisition_respects_timeout( + client: Client, env: WorkflowEnvironment +): + await _do_update_handler_lock_or_semaphore_test( + client, + env, + # Initial entry to the semaphore succeeds, but all subsequent attempts to acquire a semaphore + # slot fail. + UseLockOrSemaphoreWorkflowParameters( + semaphore_initial_value=3, + sleep=0.5, + timeout=0.1, + ), + n_updates=5, + expectation=LockOrSemaphoreWorkflowConcurrencySummary( + ever_in_critical_section=3, peak_in_critical_section=3 + ), + ) - async def run_timer(self): - self.events.append("timer-sync") - await workflow.sleep(0.1) - fut: asyncio.Future[bool] = asyncio.Future() - fut.set_result(True) - await fut - self.events.append("timer-1") - await workflow.wait_condition(lambda: True) - self.events.append("timer-2") - async def run_act(self): - self.events.append("act-sync") - await workflow.execute_activity( - say_hello, "Enchi", schedule_to_close_timeout=timedelta(seconds=30) - ) - fut: asyncio.Future[bool] = asyncio.Future() - fut.set_result(True) - await fut - self.events.append("act-1") - await workflow.wait_condition(lambda: True) - self.events.append("act-2") +@workflow.defn +class TimeoutErrorWorkflow: + @workflow.run + async def run(self, scenario: str) -> None: + if scenario == "workflow.wait_condition": + await workflow.wait_condition(lambda: False, timeout=0.01) + elif scenario == "asyncio.wait_for": + await asyncio.wait_for(asyncio.sleep(1000), timeout=0.01) + elif scenario == "asyncio.timeout": + if sys.version_info >= (3, 11): + async with asyncio.timeout(0.1): + await asyncio.sleep(1000) + else: + raise RuntimeError("Unrecognized scenario") -async def test_async_loop_ordering(client: Client, env: WorkflowEnvironment): - """This test mostly exists to generate histories for test_replayer_async_ordering. - See that test for more.""" +async def test_workflow_timeout_error(client: Client): + async with new_worker(client, TimeoutErrorWorkflow) as worker: + scenarios = ["workflow.wait_condition", "asyncio.wait_for"] + if sys.version_info >= (3, 11): + scenarios.append("asyncio.timeout") - if env.supports_time_skipping: - pytest.skip("This test doesn't work right with time skipping for some reason") - task_queue = f"tq-{uuid.uuid4()}" - handle = await client.start_workflow( - SignalsActivitiesTimersUpdatesTracingWorkflow.run, - id=f"wf-{uuid.uuid4()}", - task_queue=task_queue, - ) - await handle.signal(SignalsActivitiesTimersUpdatesTracingWorkflow.dosig, "before") + for scenario in scenarios: + with pytest.raises(WorkflowFailureError) as err: + await client.execute_workflow( + TimeoutErrorWorkflow.run, + scenario, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + assert isinstance(err.value.cause, ApplicationError) + assert err.value.cause.type == "TimeoutError" - async with new_worker( - client, - SignalsActivitiesTimersUpdatesTracingWorkflow, - activities=[say_hello], - task_queue=task_queue, - ): - await asyncio.sleep(0.2) - await handle.signal(SignalsActivitiesTimersUpdatesTracingWorkflow.dosig, "1") - await handle.execute_update( - SignalsActivitiesTimersUpdatesTracingWorkflow.doupdate, "1" - ) - await handle.result() +def check_in_workflow() -> str: + return "in workflow" if workflow.in_workflow() else "not in workflow" -@workflow.defn -class ActivityAndSignalsWhileWorkflowDown: - def __init__(self) -> None: - self.events: List[str] = [] - self.counter = 0 +@workflow.defn +class InWorkflowUtilWorkflow: @workflow.run - async def run(self, activity_tq: str) -> List[str]: - act_task = asyncio.create_task(self.run_act(activity_tq)) - await workflow.wait_condition(lambda: self.counter >= 2) - self.events.append(f"counter-{self.counter}") - await act_task - return self.events + async def run(self) -> str: + return check_in_workflow() - @workflow.signal - async def dosig(self, name: str): - self.events.append(f"sig-{name}") - self.counter += 1 - async def run_act(self, activity_tq: str): - self.events.append("act-start") - await workflow.execute_activity( - say_hello, - "Enchi", - schedule_to_close_timeout=timedelta(seconds=30), - task_queue=activity_tq, +async def test_in_workflow_util(client: Client): + assert check_in_workflow() == "not in workflow" + async with new_worker(client, InWorkflowUtilWorkflow) as worker: + assert "in workflow" == await client.execute_workflow( + InWorkflowUtilWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, ) - self.counter += 1 - self.events.append("act-done") -async def test_alternate_async_loop_ordering(client: Client, env: WorkflowEnvironment): - """This test mostly exists to generate histories for test_replayer_alternate_async_ordering. - See that test for more.""" - - if env.supports_time_skipping: - pytest.skip("This test doesn't work right with time skipping for some reason") - task_queue = f"tq-{uuid.uuid4()}" - activity_tq = f"tq-{uuid.uuid4()}" - handle = await client.start_workflow( - ActivityAndSignalsWhileWorkflowDown.run, - activity_tq, - id=f"wf-{uuid.uuid4()}", - task_queue=task_queue, - ) +@workflow.defn +class LoopIsRunningWorkflow: + @workflow.run + async def run(self) -> bool: + return asyncio.get_running_loop().is_running() - async with new_worker( - client, - ActivityAndSignalsWhileWorkflowDown, - activities=[say_hello], - task_queue=task_queue, - ): - # This sleep exists to make sure the first WFT is processed - await asyncio.sleep(0.2) - async with new_worker( - client, - activities=[say_hello], - task_queue=activity_tq, - ): - # Make sure the activity starts being processed before sending signals - await asyncio.sleep(1) - await handle.signal(ActivityAndSignalsWhileWorkflowDown.dosig, "1") - await handle.signal(ActivityAndSignalsWhileWorkflowDown.dosig, "2") +async def test_workflow_loop_is_running(client: Client): + async with new_worker(client, LoopIsRunningWorkflow) as worker: + assert await client.execute_workflow( + LoopIsRunningWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) - async with new_worker( - client, - ActivityAndSignalsWhileWorkflowDown, - activities=[say_hello], - task_queue=task_queue, - ): - await handle.result() +deadlock_interruptible_completed = 0 -# The following Lock and Semaphore tests test that asyncio concurrency primitives work as expected -# in workflow code. There is nothing Temporal-specific about the way that asyncio.Lock and -# asyncio.Semaphore are used here. +@workflow.defn(sandboxed=False) +class DeadlockInterruptibleWorkflow: + @workflow.run + async def run(self) -> None: + # Infinite loop, which is interruptible via PyThreadState_SetAsyncExc + try: + while True: + pass + finally: + global deadlock_interruptible_completed + deadlock_interruptible_completed += 1 -@activity.defn -async def noop_activity_for_lock_or_semaphore_tests() -> None: - return None +async def test_workflow_deadlock_interruptible(client: Client): + # TODO(cretz): Improve this test and other deadlock/eviction tests by + # checking slot counts with Core. There are a couple of bugs where used slot + # counts are off by one and slots are released before eviction (see + # https://github.com/temporalio/sdk-rust/issues/894). -@dataclass -class LockOrSemaphoreWorkflowConcurrencySummary: - ever_in_critical_section: int - peak_in_critical_section: int + # This worker used to not be able to shutdown because we hung evictions on + # deadlock + async with new_worker(client, DeadlockInterruptibleWorkflow) as worker: + # Start the workflow + assert deadlock_interruptible_completed == 0 + handle = await client.start_workflow( + DeadlockInterruptibleWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + # Wait for task fail + await assert_task_fail_eventually(handle, message_contains="deadlock") + # Confirm workflow was interrupted + async def check_completed(): + assert deadlock_interruptible_completed >= 1 -@dataclass -class UseLockOrSemaphoreWorkflowParameters: - n_coroutines: int = 0 - semaphore_initial_value: Optional[int] = None - sleep: Optional[float] = None - timeout: Optional[float] = None + await assert_eventually(check_completed) + completed_sec = time.monotonic() + # Confirm worker shutdown didn't hang + assert time.monotonic() - completed_sec < 20 -@workflow.defn -class CoroutinesUseLockOrSemaphoreWorkflow: - def __init__(self) -> None: - self.params: UseLockOrSemaphoreWorkflowParameters - self.lock_or_semaphore: Union[asyncio.Lock, asyncio.Semaphore] - self._currently_in_critical_section: set[str] = set() - self._ever_in_critical_section: set[str] = set() - self._peak_in_critical_section = 0 +deadlock_uninterruptible_event = threading.Event() +deadlock_uninterruptible_completed = 0 - def init(self, params: UseLockOrSemaphoreWorkflowParameters): - self.params = params - if self.params.semaphore_initial_value is not None: - self.lock_or_semaphore = asyncio.Semaphore( - self.params.semaphore_initial_value - ) - else: - self.lock_or_semaphore = asyncio.Lock() +@workflow.defn(sandboxed=False) +class DeadlockUninterruptibleWorkflow: @workflow.run - async def run( - self, - params: Optional[UseLockOrSemaphoreWorkflowParameters], - ) -> LockOrSemaphoreWorkflowConcurrencySummary: - # TODO: Use workflow init method when it exists. - assert params - self.init(params) - await asyncio.gather( - *(self.coroutine(f"{i}") for i in range(self.params.n_coroutines)) - ) - assert not any(self._currently_in_critical_section) - return LockOrSemaphoreWorkflowConcurrencySummary( - len(self._ever_in_critical_section), - self._peak_in_critical_section, - ) - - async def coroutine(self, id: str): - if self.params.timeout: - try: - await asyncio.wait_for( - self.lock_or_semaphore.acquire(), self.params.timeout - ) - except asyncio.TimeoutError: - return - else: - await self.lock_or_semaphore.acquire() - self._enters_critical_section(id) + async def run(self) -> None: + # Wait on event, which is not interruptible via PyThreadState_SetAsyncExc try: - if self.params.sleep: - await asyncio.sleep(self.params.sleep) - else: - await workflow.execute_activity( - noop_activity_for_lock_or_semaphore_tests, - schedule_to_close_timeout=timedelta(seconds=30), - ) + deadlock_uninterruptible_event.wait() finally: - self.lock_or_semaphore.release() - self._exits_critical_section(id) + global deadlock_uninterruptible_completed + deadlock_uninterruptible_completed += 1 - def _enters_critical_section(self, id: str) -> None: - self._currently_in_critical_section.add(id) - self._ever_in_critical_section.add(id) - self._peak_in_critical_section = max( - self._peak_in_critical_section, - len(self._currently_in_critical_section), + +async def test_workflow_deadlock_uninterruptible(client: Client): + # This worker used to not be able to shutdown because we hung evictions on + # deadlock + async with new_worker(client, DeadlockUninterruptibleWorkflow) as worker: + # Start the workflow + assert deadlock_uninterruptible_completed == 0 + handle = await client.start_workflow( + DeadlockUninterruptibleWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, ) + # Wait for task fail + await assert_task_fail_eventually(handle, message_contains="deadlock") + # Confirm could not be interrupted + assert deadlock_uninterruptible_completed == 0 - def _exits_critical_section(self, id: str) -> None: - self._currently_in_critical_section.remove(id) + # Now complete the event and confirm the workflow does complete + deadlock_uninterruptible_event.set() + async def check_completed(): + assert deadlock_uninterruptible_completed >= 1 -@workflow.defn -class HandlerCoroutinesUseLockOrSemaphoreWorkflow(CoroutinesUseLockOrSemaphoreWorkflow): - def __init__(self) -> None: - super().__init__() - self.workflow_may_exit = False + await assert_eventually(check_completed) + completed_sec = time.monotonic() + # Confirm worker shutdown didn't hang + assert time.monotonic() - completed_sec < 20 + + +deadlock_fill_up_block_event = threading.Event() +deadlock_fill_up_block_completed = 0 + +@workflow.defn(sandboxed=False) +class DeadlockFillUpBlockWorkflow: @workflow.run - async def run( - self, - params: Optional[UseLockOrSemaphoreWorkflowParameters] = None, - ) -> LockOrSemaphoreWorkflowConcurrencySummary: - await workflow.wait_condition(lambda: self.workflow_may_exit) - return LockOrSemaphoreWorkflowConcurrencySummary( - len(self._ever_in_critical_section), - self._peak_in_critical_section, - ) + async def run(self) -> None: + try: + deadlock_fill_up_block_event.wait() + finally: + global deadlock_fill_up_block_completed + deadlock_fill_up_block_completed += 1 - @workflow.update - async def my_update(self, params: UseLockOrSemaphoreWorkflowParameters): - # TODO: Use workflow init method when it exists. - if not hasattr(self, "params"): - self.init(params) - assert (update_info := workflow.current_update_info()) - await self.coroutine(update_info.id) - @workflow.signal - async def finish(self): - self.workflow_may_exit = True +@workflow.defn(sandboxed=False) +class DeadlockFillUpSimpleWorkflow: + @workflow.run + async def run(self) -> str: + return "done" -async def _do_workflow_coroutines_lock_or_semaphore_test( - client: Client, - params: UseLockOrSemaphoreWorkflowParameters, - expectation: LockOrSemaphoreWorkflowConcurrencySummary, -): +async def test_workflow_deadlock_fill_up_slots(client: Client): + cpu_count = os.cpu_count() + assert cpu_count + # This worker used to not be able to shutdown because we hung evictions on + # deadlock. async with new_worker( client, - CoroutinesUseLockOrSemaphoreWorkflow, - activities=[noop_activity_for_lock_or_semaphore_tests], + DeadlockFillUpBlockWorkflow, + DeadlockFillUpSimpleWorkflow, + # Start the worker with CPU count + 11 task slots + max_concurrent_workflow_tasks=cpu_count + 11, ) as worker: - summary = await client.execute_workflow( - CoroutinesUseLockOrSemaphoreWorkflow.run, - arg=params, - id=str(uuid.uuid4()), - task_queue=worker.task_queue, - ) - assert summary == expectation + # For this test we're going to start cpu_count + 5 workflows that + # deadlock. In previous SDK versions we defaulted to CPU count + # number of workflow threads, so deadlocking that many would prevent + # other code from executing. Now that we default to more workers, we + # can handle more work while some are deadlocked. + # Start the workflows that deadlock + assert deadlock_fill_up_block_completed == 0 + handles = await asyncio.gather( + *[ + client.start_workflow( + DeadlockFillUpBlockWorkflow.run, + id=f"workflow-deadlock-{i}-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + for i in range(cpu_count + 5) + ] + ) -async def _do_update_handler_lock_or_semaphore_test( - client: Client, - env: WorkflowEnvironment, - params: UseLockOrSemaphoreWorkflowParameters, - n_updates: int, - expectation: LockOrSemaphoreWorkflowConcurrencySummary, -): - if env.supports_time_skipping: - pytest.skip( - "Java test server: https://github.com/temporalio/sdk-java/issues/1903" + # Wait for them all to deadlock + await asyncio.gather( + *[ + assert_task_fail_eventually(h, message_contains="deadlock") + for h in handles + ] ) - task_queue = "tq" - handle = await client.start_workflow( - HandlerCoroutinesUseLockOrSemaphoreWorkflow.run, - id=f"wf-{str(uuid.uuid4())}", - task_queue=task_queue, - ) - # Create updates in Admitted state, before the worker starts polling. - admitted_updates = [ - await admitted_update_task( - client, - handle, - HandlerCoroutinesUseLockOrSemaphoreWorkflow.my_update, - arg=params, - id=f"update-{i}", + # Now try to run a regular non-deadlocked workflow. Before recent + # changes, this would also cause a deadlock because it would submit + # to the thread pool but the thread pool didn't have enough room. + assert "done" == await asyncio.wait_for( + client.execute_workflow( + DeadlockFillUpSimpleWorkflow.run, + id=f"workflow-simple-{uuid.uuid4()}", + task_queue=worker.task_queue, + ), + 10, ) - for i in range(n_updates) - ] - async with new_worker( - client, - HandlerCoroutinesUseLockOrSemaphoreWorkflow, - activities=[noop_activity_for_lock_or_semaphore_tests], - task_queue=task_queue, - ): - for update_task in admitted_updates: - await update_task - await handle.signal(HandlerCoroutinesUseLockOrSemaphoreWorkflow.finish) - summary = await handle.result() - assert summary == expectation + # Let the deadlocked ones complete too + deadlock_fill_up_block_event.set() -async def test_workflow_coroutines_can_use_lock(client: Client): - await _do_workflow_coroutines_lock_or_semaphore_test( - client, - UseLockOrSemaphoreWorkflowParameters(n_coroutines=5), - # The lock limits concurrency to 1 - expectation=LockOrSemaphoreWorkflowConcurrencySummary( - ever_in_critical_section=5, peak_in_critical_section=1 - ), - ) + async def check_completed(): + assert deadlock_fill_up_block_completed >= len(handles) + await assert_eventually(check_completed) + completed_sec = time.monotonic() + # Confirm worker shutdown didn't hang + assert time.monotonic() - completed_sec < 20 -async def test_update_handler_can_use_lock_to_serialize_handler_executions( - client: Client, env: WorkflowEnvironment -): - await _do_update_handler_lock_or_semaphore_test( - client, - env, - UseLockOrSemaphoreWorkflowParameters(), - n_updates=5, - # The lock limits concurrency to 1 - expectation=LockOrSemaphoreWorkflowConcurrencySummary( - ever_in_critical_section=5, peak_in_critical_section=1 - ), - ) +eviction_swallow_keep_looping = True -async def test_workflow_coroutines_lock_acquisition_respects_timeout(client: Client): - await _do_workflow_coroutines_lock_or_semaphore_test( - client, - UseLockOrSemaphoreWorkflowParameters(n_coroutines=5, sleep=0.5, timeout=0.1), - # Second and subsequent coroutines fail to acquire the lock due to the timeout. - expectation=LockOrSemaphoreWorkflowConcurrencySummary( - ever_in_critical_section=1, peak_in_critical_section=1 - ), - ) +@workflow.defn(sandboxed=False) +class EvictionSwallowWorkflow: + @workflow.run + async def run(self) -> str: + # Start a task in the background that will prevent eviction because + # eviction requires all tasks complete + async def eviction_swallower(): + global eviction_swallow_keep_looping + while eviction_swallow_keep_looping: + try: + await workflow.wait_condition(lambda: False) + except BaseException: + # Swallow base exception intentionally which prevents + # eviction + pass -async def test_update_handler_lock_acquisition_respects_timeout( - client: Client, env: WorkflowEnvironment -): - await _do_update_handler_lock_or_semaphore_test( - client, - env, - # Second and subsequent handler executions fail to acquire the lock due to the timeout. - UseLockOrSemaphoreWorkflowParameters(sleep=0.5, timeout=0.1), - n_updates=5, - expectation=LockOrSemaphoreWorkflowConcurrencySummary( - ever_in_critical_section=1, peak_in_critical_section=1 - ), - ) + asyncio.create_task(eviction_swallower()) + return "done" -async def test_workflow_coroutines_can_use_semaphore(client: Client): - await _do_workflow_coroutines_lock_or_semaphore_test( - client, - UseLockOrSemaphoreWorkflowParameters(n_coroutines=5, semaphore_initial_value=3), - # The semaphore limits concurrency to 3 - expectation=LockOrSemaphoreWorkflowConcurrencySummary( - ever_in_critical_section=5, peak_in_critical_section=3 - ), - ) +async def test_workflow_eviction_swallow(client: Client): + # Add a queue handler to all logging, and remove later + log_queue: queue.Queue[logging.LogRecord] = queue.Queue() + log_handler = logging.handlers.QueueHandler(log_queue) + logging.getLogger().addHandler(log_handler) + try: + async with new_worker(client, EvictionSwallowWorkflow) as worker: + global eviction_swallow_keep_looping + assert eviction_swallow_keep_looping + # Run workflow that completes but cannot evict + handle = await client.start_workflow( + EvictionSwallowWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + assert "done" == await handle.result() -async def test_update_handler_can_use_semaphore_to_control_handler_execution_concurrency( - client: Client, env: WorkflowEnvironment -): - await _do_update_handler_lock_or_semaphore_test( - client, - env, - # The semaphore limits concurrency to 3 - UseLockOrSemaphoreWorkflowParameters(semaphore_initial_value=3), - n_updates=5, - expectation=LockOrSemaphoreWorkflowConcurrencySummary( - ever_in_critical_section=5, peak_in_critical_section=3 - ), - ) + # Make sure we get the log we expect + async def check_logs(): + try: + while True: + log_record = log_queue.get(block=False) + if log_record.message.startswith( + f"Timed out running eviction job for run ID {handle.result_run_id}" + ): + return + except queue.Empty: + pass + assert False, "log record not found" + await assert_eventually(check_logs) -async def test_workflow_coroutine_semaphore_acquisition_respects_timeout( - client: Client, -): - await _do_workflow_coroutines_lock_or_semaphore_test( - client, - UseLockOrSemaphoreWorkflowParameters( - n_coroutines=5, semaphore_initial_value=3, sleep=0.5, timeout=0.1 - ), - # Initial entry to the semaphore succeeds, but all subsequent attempts to acquire a semaphore - # slot fail. - expectation=LockOrSemaphoreWorkflowConcurrencySummary( - ever_in_critical_section=3, peak_in_critical_section=3 - ), - ) + # Let it finish now + eviction_swallow_keep_looping = False + completed_sec = time.monotonic() + # Confirm worker shutdown didn't hang + assert time.monotonic() - completed_sec < 20 + finally: + logging.getLogger().removeHandler(log_handler) -async def test_update_handler_semaphore_acquisition_respects_timeout( - client: Client, env: WorkflowEnvironment -): - await _do_update_handler_lock_or_semaphore_test( - client, - env, - # Initial entry to the semaphore succeeds, but all subsequent attempts to acquire a semaphore - # slot fail. - UseLockOrSemaphoreWorkflowParameters( - semaphore_initial_value=3, - sleep=0.5, - timeout=0.1, - ), - n_updates=5, - expectation=LockOrSemaphoreWorkflowConcurrencySummary( - ever_in_critical_section=3, peak_in_critical_section=3 - ), - ) +@activity.defn +async def check_priority_activity(should_have_priorty: int) -> str: + assert activity.info().priority.priority_key == should_have_priorty + return "Done!" @workflow.defn -class TimeoutErrorWorkflow: +class WorkflowUsingPriorities: @workflow.run - async def run(self, scenario: str) -> None: - if scenario == "workflow.wait_condition": - await workflow.wait_condition(lambda: False, timeout=0.01) - elif scenario == "asyncio.wait_for": - await asyncio.wait_for(asyncio.sleep(1000), timeout=0.01) - elif scenario == "asyncio.timeout": - if sys.version_info >= (3, 11): - async with asyncio.timeout(0.1): - await asyncio.sleep(1000) - else: - raise RuntimeError("Unrecognized scenario") + async def run(self, expected_priority: int | None, stop_after_check: bool) -> str: + assert workflow.info().priority.priority_key == expected_priority + if stop_after_check: + return "Done!" + await workflow.execute_child_workflow( + WorkflowUsingPriorities.run, + args=[4, True], + priority=Priority( + priority_key=4, fairness_key="tenant2", fairness_weight=1.0 + ), + ) + handle = await workflow.start_child_workflow( + WorkflowUsingPriorities.run, + args=[2, True], + priority=Priority( + priority_key=2, fairness_key="tenant3", fairness_weight=0.5 + ), + ) + await handle + await workflow.execute_activity( + say_hello, + "hi", + priority=Priority( + priority_key=5, fairness_key="tenant4", fairness_weight=3.0 + ), + start_to_close_timeout=timedelta(seconds=5), + ) + return "Done!" -async def test_workflow_timeout_error(client: Client): - async with new_worker(client, TimeoutErrorWorkflow) as worker: - scenarios = ["workflow.wait_condition", "asyncio.wait_for"] - if sys.version_info >= (3, 11): - scenarios.append("asyncio.timeout") +async def test_workflow_priorities(client: Client, env: WorkflowEnvironment): + if env.supports_time_skipping: + pytest.skip( + "Java test server needs release with: https://github.com/temporalio/sdk-java/pull/2453" + ) - for scenario in scenarios: - with pytest.raises(WorkflowFailureError) as err: - await client.execute_workflow( - TimeoutErrorWorkflow.run, - scenario, - id=f"workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, + async with new_worker( + client, WorkflowUsingPriorities, HelloWorkflow, activities=[say_hello] + ) as worker: + handle = await client.start_workflow( + WorkflowUsingPriorities.run, + args=[1, False], + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + priority=Priority( + priority_key=1, fairness_key="tenant1", fairness_weight=2.5 + ), + ) + await handle.result() + + first_child = True + async for e in handle.fetch_history_events(): + if e.HasField("workflow_execution_started_event_attributes"): + priority = e.workflow_execution_started_event_attributes.priority + assert priority.priority_key == 1 + assert priority.fairness_key == "tenant1" + assert priority.fairness_weight == 2.5 + elif e.HasField( + "start_child_workflow_execution_initiated_event_attributes" + ): + priority = ( + e.start_child_workflow_execution_initiated_event_attributes.priority ) - assert isinstance(err.value.cause, ApplicationError) - assert err.value.cause.type == "TimeoutError" + if first_child: + assert priority.priority_key == 4 + assert priority.fairness_key == "tenant2" + assert priority.fairness_weight == 1.0 + first_child = False + else: + assert priority.priority_key == 2 + assert priority.fairness_key == "tenant3" + assert priority.fairness_weight == 0.5 + elif e.HasField("activity_task_scheduled_event_attributes"): + priority = e.activity_task_scheduled_event_attributes.priority + assert priority.priority_key == 5 + assert priority.fairness_key == "tenant4" + assert priority.fairness_weight == 3.0 + + # Verify a workflow started without priorities sees None for the key + handle = await client.start_workflow( + WorkflowUsingPriorities.run, + args=[None, True], + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.result() -def check_in_workflow() -> str: - return "in workflow" if workflow.in_workflow() else "not in workflow" +@workflow.defn +class ExposeRootChildWorkflow: + def __init__(self) -> None: + self.blocked = True + + @workflow.signal + def unblock(self) -> None: + self.blocked = False + + @workflow.run + async def run(self) -> temporalio.workflow.RootInfo | None: + await workflow.wait_condition(lambda: not self.blocked) + return workflow.info().root @workflow.defn -class InWorkflowUtilWorkflow: +class ExposeRootWorkflow: @workflow.run - async def run(self) -> str: - return check_in_workflow() + async def run(self, child_wf_id: str) -> temporalio.workflow.RootInfo | None: + return await workflow.execute_child_workflow( + ExposeRootChildWorkflow.run, id=child_wf_id + ) -async def test_in_workflow_util(client: Client): - assert check_in_workflow() == "not in workflow" - async with new_worker(client, InWorkflowUtilWorkflow) as worker: - assert "in workflow" == await client.execute_workflow( - InWorkflowUtilWorkflow.run, - id=f"workflow-{uuid.uuid4()}", +async def test_expose_root_execution(client: Client, env: WorkflowEnvironment): + if env.supports_time_skipping: + pytest.skip( + "Java test server needs release with: https://github.com/temporalio/sdk-java/pull/2441" + ) + async with new_worker( + client, ExposeRootWorkflow, ExposeRootChildWorkflow + ) as worker: + parent_wf_id = f"workflow-{uuid.uuid4()}" + child_wf_id = parent_wf_id + "_child" + handle = await client.start_workflow( + ExposeRootWorkflow.run, + child_wf_id, + id=parent_wf_id, task_queue=worker.task_queue, ) + await assert_workflow_exists_eventually( + client, ExposeRootChildWorkflow, child_wf_id + ) + child_handle: WorkflowHandle = client.get_workflow_handle_for( + ExposeRootChildWorkflow.run, child_wf_id + ) + child_desc = await child_handle.describe() + parent_desc = await handle.describe() + # Assert child root execution is the same as it's parent execution + assert child_desc.root_id == parent_desc.id + assert child_desc.root_run_id == parent_desc.run_id + # Unblock child + await child_handle.signal(ExposeRootChildWorkflow.unblock) + # Get the result (child info) + child_wf_info_root = await handle.result() + # Assert root execution in child info is same as it's parent execution + assert child_wf_info_root is not None + assert child_wf_info_root.workflow_id == parent_desc.id + assert child_wf_info_root.run_id == parent_desc.run_id -deadlock_interruptible_completed = 0 +@workflow.defn(dynamic=True) +class WorkflowDynamicConfigFnFailure: + @workflow.dynamic_config + def dynamic_config(self) -> temporalio.workflow.DynamicWorkflowConfig: + raise Exception("Dynamic config failure") -@workflow.defn(sandboxed=False) -class DeadlockInterruptibleWorkflow: @workflow.run - async def run(self) -> None: - # Infinite loop, which is interruptible via PyThreadState_SetAsyncExc - try: - while True: - pass - finally: - global deadlock_interruptible_completed - deadlock_interruptible_completed += 1 - + async def run(self, _args: Sequence[RawValue]) -> None: + raise RuntimeError("Should never actually run") -async def test_workflow_deadlock_interruptible(client: Client): - # TODO(cretz): Improve this test and other deadlock/eviction tests by - # checking slot counts with Core. There are a couple of bugs where used slot - # counts are off by one and slots are released before eviction (see - # https://github.com/temporalio/sdk-core/issues/894). - # This worker used to not be able to shutdown because we hung evictions on - # deadlock - async with new_worker(client, DeadlockInterruptibleWorkflow) as worker: - # Start the workflow - assert deadlock_interruptible_completed == 0 +async def test_workflow_dynamic_config_failure(client: Client): + async with new_worker(client, WorkflowDynamicConfigFnFailure) as worker: handle = await client.start_workflow( - DeadlockInterruptibleWorkflow.run, - id=f"workflow-{uuid.uuid4()}", + "verycooldynamicworkflow", + id=f"dynamic-config-failure-{uuid.uuid4()}", task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=5), ) - # Wait for task fail - await assert_task_fail_eventually(handle, message_contains="deadlock") - - # Confirm workflow was interrupted - async def check_completed(): - assert deadlock_interruptible_completed >= 1 - await assert_eventually(check_completed) - completed_sec = time.monotonic() - # Confirm worker shutdown didn't hang - assert time.monotonic() - completed_sec < 20 + # Assert workflow task fails with our expected error message + await assert_task_fail_eventually( + handle, message_contains="Dynamic config failure" + ) -deadlock_uninterruptible_event = threading.Event() -deadlock_uninterruptible_completed = 0 +@activity.defn +async def raise_application_error(use_benign: bool) -> typing.NoReturn: + if use_benign: + raise ApplicationError( + "This is a benign error", category=ApplicationErrorCategory.BENIGN + ) + else: + raise ApplicationError( + "This is a regular error", category=ApplicationErrorCategory.UNSPECIFIED + ) -@workflow.defn(sandboxed=False) -class DeadlockUninterruptibleWorkflow: +@workflow.defn +class RaiseErrorWorkflow: @workflow.run - async def run(self) -> None: - # Wait on event, which is not interruptible via PyThreadState_SetAsyncExc - try: - deadlock_uninterruptible_event.wait() - finally: - global deadlock_uninterruptible_completed - deadlock_uninterruptible_completed += 1 + async def run(self, use_benign: bool) -> None: + # Execute activity that will raise an error + await workflow.execute_activity( + raise_application_error, + use_benign, + start_to_close_timeout=timedelta(seconds=5), + retry_policy=RetryPolicy(maximum_attempts=1), + ) -async def test_workflow_deadlock_uninterruptible(client: Client): - # This worker used to not be able to shutdown because we hung evictions on - # deadlock - async with new_worker(client, DeadlockUninterruptibleWorkflow) as worker: - # Start the workflow - assert deadlock_uninterruptible_completed == 0 +async def test_activity_benign_error_not_logged(client: Client): + if sys.version_info < (3, 12): + pytest.skip("This test currently fails frequently on 3.10 due to import bug") + with LogCapturer().logs_captured(activity.logger.base_logger) as capturer: + async with new_worker( + client, RaiseErrorWorkflow, activities=[raise_application_error] + ) as worker: + # Run with benign error + with pytest.raises(WorkflowFailureError) as err: + await client.execute_workflow( + RaiseErrorWorkflow.run, + True, + id=str(uuid.uuid4()), + task_queue=worker.task_queue, + ) + # Check that the cause is an ApplicationError + assert isinstance(err.value.cause, ActivityError) + assert isinstance(err.value.cause.cause, ApplicationError) + # Assert the expected category + assert err.value.cause.cause.category == ApplicationErrorCategory.BENIGN + assert capturer.find_log("Completing activity as failed") == None + + # Run with non-benign error + with pytest.raises(WorkflowFailureError) as err: + await client.execute_workflow( + RaiseErrorWorkflow.run, + False, + id=str(uuid.uuid4()), + task_queue=worker.task_queue, + ) + + # Check that the cause is an ApplicationError + assert isinstance(err.value.cause, ActivityError) + assert isinstance(err.value.cause.cause, ApplicationError) + # Assert the expected category + assert ( + err.value.cause.cause.category == ApplicationErrorCategory.UNSPECIFIED + ) + assert capturer.find_log("Completing activity as failed") != None + + +async def test_workflow_missing_local_activity(client: Client): + async with new_worker( + client, SimpleLocalActivityWorkflow, activities=[custom_error_activity] + ) as worker: handle = await client.start_workflow( - DeadlockUninterruptibleWorkflow.run, + SimpleLocalActivityWorkflow.run, + "Temporal", id=f"workflow-{uuid.uuid4()}", task_queue=worker.task_queue, ) - # Wait for task fail - await assert_task_fail_eventually(handle, message_contains="deadlock") - # Confirm could not be interrupted - assert deadlock_uninterruptible_completed == 0 - # Now complete the event and confirm the workflow does complete - deadlock_uninterruptible_event.set() + await assert_task_fail_eventually( + handle, + message_contains="Activity function say_hello is not registered on this worker, available activities: custom_error_activity", + ) - async def check_completed(): - assert deadlock_uninterruptible_completed >= 1 - await assert_eventually(check_completed) - completed_sec = time.monotonic() - # Confirm worker shutdown didn't hang - assert time.monotonic() - completed_sec < 20 +async def test_workflow_missing_local_activity_but_dynamic(client: Client): + async with new_worker( + client, + SimpleLocalActivityWorkflow, + activities=[custom_error_activity, return_name_activity], + ) as worker: + res = await client.execute_workflow( + SimpleLocalActivityWorkflow.run, + "Temporal", + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + assert res == "say_hello" -deadlock_fill_up_block_event = threading.Event() -deadlock_fill_up_block_completed = 0 + +async def test_workflow_missing_local_activity_no_activities(client: Client): + async with new_worker( + client, + SimpleLocalActivityWorkflow, + activities=[], + ) as worker: + handle = await client.start_workflow( + SimpleLocalActivityWorkflow.run, + "Temporal", + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + await assert_task_fail_eventually( + handle, + message_contains="Activity function say_hello is not registered on this worker, no available activities", + ) + + +@activity.defn +async def heartbeat_activity( + catch_err: bool = True, +) -> temporalio.activity.ActivityCancellationDetails | None: + try: + while True: + activity.heartbeat() + # If we have heartbeat details, we are on the second attempt, we have retried due to pause/unpause. + if activity.info().heartbeat_details: + return activity.cancellation_details() + await asyncio.sleep(0.1) + except (CancelledError, asyncio.CancelledError) as err: + if not catch_err: + raise err + await async_wait_for_pause_event(activity.info().activity_id) + return activity.cancellation_details() + finally: + activity.heartbeat("finally-complete") -@workflow.defn(sandboxed=False) -class DeadlockFillUpBlockWorkflow: - @workflow.run - async def run(self) -> None: - try: - deadlock_fill_up_block_event.wait() - finally: - global deadlock_fill_up_block_completed - deadlock_fill_up_block_completed += 1 +@activity.defn +def sync_heartbeat_activity( + catch_err: bool = True, +) -> temporalio.activity.ActivityCancellationDetails | None: + try: + while True: + activity.heartbeat() + # If we have heartbeat details, we are on the second attempt, we have retried due to pause/unpause. + if activity.info().heartbeat_details: + return activity.cancellation_details() + time.sleep(0.1) + except (CancelledError, asyncio.CancelledError) as err: + if not catch_err: + raise err + wait_for_pause_event(activity.info().activity_id) + return activity.cancellation_details() + finally: + activity.heartbeat("finally-complete") -@workflow.defn(sandboxed=False) -class DeadlockFillUpSimpleWorkflow: +@workflow.defn +class ActivityHeartbeatWorkflow: @workflow.run - async def run(self) -> str: - return "done" - - -async def test_workflow_deadlock_fill_up_slots(client: Client): - cpu_count = os.cpu_count() - assert cpu_count - # This worker used to not be able to shutdown because we hung evictions on - # deadlock. - async with new_worker( - client, - DeadlockFillUpBlockWorkflow, - DeadlockFillUpSimpleWorkflow, - # Start the worker with CPU count + 11 task slots - max_concurrent_workflow_tasks=cpu_count + 11, - ) as worker: - # For this test we're going to start cpu_count + 5 workflows that - # deadlock. In previous SDK versions we defaulted to CPU count - # number of workflow threads, so deadlocking that many would prevent - # other code from executing. Now that we default to more workers, we - # can handle more work while some are deadlocked. - - # Start the workflows that deadlock - assert deadlock_fill_up_block_completed == 0 - handles = await asyncio.gather( - *[ - client.start_workflow( - DeadlockFillUpBlockWorkflow.run, - id=f"workflow-deadlock-{i}-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) - for i in range(cpu_count + 5) - ] + async def run( + self, activity_id: str + ) -> list[temporalio.activity.ActivityCancellationDetails | None]: + result = [] + result.append( + await workflow.execute_activity( + sync_heartbeat_activity, + True, + activity_id=activity_id, + start_to_close_timeout=timedelta(seconds=10), + heartbeat_timeout=timedelta(seconds=2), + retry_policy=RetryPolicy(maximum_attempts=1), + ) ) - - # Wait for them all to deadlock - await asyncio.gather( - *[ - assert_task_fail_eventually(h, message_contains="deadlock") - for h in handles - ] + result.append( + await workflow.execute_activity( + heartbeat_activity, + True, + activity_id=f"{activity_id}-2", + start_to_close_timeout=timedelta(seconds=10), + heartbeat_timeout=timedelta(seconds=2), + retry_policy=RetryPolicy(maximum_attempts=1), + ) ) + return result - # Now try to run a regular non-deadlocked workflow. Before recent - # changes, this would also cause a deadlock because it would submit - # to the thread pool but the thread pool didn't have enough room. - assert "done" == await asyncio.wait_for( - client.execute_workflow( - DeadlockFillUpSimpleWorkflow.run, - id=f"workflow-simple-{uuid.uuid4()}", - task_queue=worker.task_queue, - ), - 10, - ) - # Let the deadlocked ones complete too - deadlock_fill_up_block_event.set() +async def test_activity_pause_cancellation_details( + client: Client, env: WorkflowEnvironment +): + if env.supports_time_skipping: + pytest.skip("Time-skipping server does not support pause API yet") + with concurrent.futures.ThreadPoolExecutor() as executor: + async with Worker( + client, + task_queue=str(uuid.uuid4()), + workflows=[ActivityHeartbeatWorkflow], + activities=[heartbeat_activity, sync_heartbeat_activity], + activity_executor=executor, + ) as worker: + test_activity_id = f"heartbeat-activity-{uuid.uuid4()}" - async def check_completed(): - assert deadlock_fill_up_block_completed >= len(handles) + handle = await client.start_workflow( + ActivityHeartbeatWorkflow.run, + test_activity_id, + id=f"test-activity-pause-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) - await assert_eventually(check_completed) - completed_sec = time.monotonic() - # Confirm worker shutdown didn't hang - assert time.monotonic() - completed_sec < 20 + # Wait for sync activity + activity_info_1 = await assert_pending_activity_exists_eventually( + handle, test_activity_id + ) + # Assert not paused + assert not activity_info_1.paused + # Pause activity then assert it is paused + await pause_and_assert(client, handle, activity_info_1.activity_id) + # Wait for async activity + activity_info_2 = await assert_pending_activity_exists_eventually( + handle, f"{test_activity_id}-2" + ) + # Assert not paused + assert not activity_info_2.paused + # Pause activity then assert it is paused + await pause_and_assert(client, handle, activity_info_2.activity_id) -eviction_swallow_keep_looping = True + # Assert workflow return value for paused activities that caught the + # cancel error + result = await handle.result() + assert result[0] == temporalio.activity.ActivityCancellationDetails( + paused=True + ) + assert result[1] == temporalio.activity.ActivityCancellationDetails( + paused=True + ) -@workflow.defn(sandboxed=False) -class EvictionSwallowWorkflow: +@workflow.defn +class ActivityHeartbeatPauseUnpauseWorkflow: @workflow.run - async def run(self) -> str: - # Start a task in the background that will prevent eviction because - # eviction requires all tasks complete - async def eviction_swallower(): - global eviction_swallow_keep_looping - while eviction_swallow_keep_looping: - try: - await workflow.wait_condition(lambda: False) - except BaseException: - # Swallow base exception intentionally which prevents - # eviction - pass + async def run( + self, activity_id: str + ) -> list[temporalio.activity.ActivityCancellationDetails | None]: + results = [] + results.append( + await workflow.execute_activity( + sync_heartbeat_activity, + False, + activity_id=activity_id, + start_to_close_timeout=timedelta(seconds=10), + heartbeat_timeout=timedelta(seconds=1), + retry_policy=RetryPolicy(maximum_attempts=2), + ) + ) + results.append( + await workflow.execute_activity( + heartbeat_activity, + False, + activity_id=f"{activity_id}-2", + start_to_close_timeout=timedelta(seconds=10), + heartbeat_timeout=timedelta(seconds=1), + retry_policy=RetryPolicy(maximum_attempts=2), + ) + ) + return results - asyncio.create_task(eviction_swallower()) - return "done" +async def test_activity_pause_unpause(client: Client, env: WorkflowEnvironment): + if env.supports_time_skipping: + pytest.skip("Time-skipping server does not support pause API yet") -async def test_workflow_eviction_swallow(client: Client): - # Add a queue handler to all logging, and remove later - log_queue: queue.Queue[logging.LogRecord] = queue.Queue() - log_handler = logging.handlers.QueueHandler(log_queue) - logging.getLogger().addHandler(log_handler) - try: - async with new_worker(client, EvictionSwallowWorkflow) as worker: - global eviction_swallow_keep_looping - assert eviction_swallow_keep_looping + async def check_heartbeat_details_exist( + handle: WorkflowHandle, + activity_id: str, + ) -> None: + act_info = await get_pending_activity_info(handle, activity_id) + if act_info is None: + raise AssertionError(f"Activity with ID {activity_id} not found.") + if len(act_info.heartbeat_details.payloads) == 0: + raise AssertionError( + f"Activity with ID {activity_id} has no heartbeat details" + ) + + with concurrent.futures.ThreadPoolExecutor() as executor: + async with Worker( + client, + task_queue=str(uuid.uuid4()), + workflows=[ActivityHeartbeatPauseUnpauseWorkflow], + activities=[heartbeat_activity, sync_heartbeat_activity], + activity_executor=executor, + max_heartbeat_throttle_interval=timedelta(milliseconds=300), + default_heartbeat_throttle_interval=timedelta(milliseconds=300), + ) as worker: + test_activity_id = f"heartbeat-activity-{uuid.uuid4()}" - # Run workflow that completes but cannot evict handle = await client.start_workflow( - EvictionSwallowWorkflow.run, - id=f"workflow-{uuid.uuid4()}", + ActivityHeartbeatPauseUnpauseWorkflow.run, + test_activity_id, + id=f"test-activity-pause-unpause-{uuid.uuid4()}", task_queue=worker.task_queue, ) - assert "done" == await handle.result() - # Make sure we get the log we expect - async def check_logs(): - try: - while True: - log_record = log_queue.get(block=False) - if log_record.message.startswith( - f"Timed out running eviction job for run ID {handle.result_run_id}" - ): - return - except queue.Empty: - pass - assert False, "log record not found" + # Wait for sync activity + activity_info_1 = await assert_pending_activity_exists_eventually( + handle, test_activity_id + ) + # Assert not paused + assert not activity_info_1.paused + # Pause activity then assert it is paused + await pause_and_assert(client, handle, activity_info_1.activity_id) + + # Wait for heartbeat details to exist. At this point, the activity has finished executing + # due to cancellation from the pause. + await assert_eventually( + lambda: check_heartbeat_details_exist( + handle, activity_info_1.activity_id + ) + ) - await assert_eventually(check_logs) + # Unpause activity + await unpause_and_assert(client, handle, activity_info_1.activity_id) + # Expect second activity to have started now + activity_info_2 = await assert_pending_activity_exists_eventually( + handle, f"{test_activity_id}-2" + ) + # Assert not paused + assert not activity_info_2.paused + # Pause activity then assert it is paused + await pause_and_assert(client, handle, activity_info_2.activity_id) + # Wait for heartbeat details to exist. At this point, the activity has finished executing + # due to cancellation from the pause. + await assert_eventually( + lambda: check_heartbeat_details_exist( + handle, activity_info_2.activity_id + ) + ) + # Unpause activity + await unpause_and_assert(client, handle, activity_info_2.activity_id) - # Let it finish now - eviction_swallow_keep_looping = False - completed_sec = time.monotonic() - # Confirm worker shutdown didn't hang - assert time.monotonic() - completed_sec < 20 - finally: - logging.getLogger().removeHandler(log_handler) + # Check workflow complete + result = await handle.result() + assert result[0] == None + assert result[1] == None @activity.defn -async def check_priority_activity(should_have_priorty: int) -> str: - assert activity.info().priority.priority_key == should_have_priorty - return "Done!" +async def external_activity_heartbeat() -> None: + activity.raise_complete_async() @workflow.defn -class WorkflowUsingPriorities: +class ExternalActivityWorkflow: @workflow.run - async def run( - self, expected_priority: Optional[int], stop_after_check: bool - ) -> str: - assert workflow.info().priority.priority_key == expected_priority - if stop_after_check: - return "Done!" - await workflow.execute_child_workflow( - WorkflowUsingPriorities.run, - args=[4, True], - priority=Priority( - priority_key=4, fairness_key="tenant2", fairness_weight=1.0 - ), - ) - handle = await workflow.start_child_workflow( - WorkflowUsingPriorities.run, - args=[2, True], - priority=Priority( - priority_key=2, fairness_key="tenant3", fairness_weight=0.5 - ), - ) - await handle + async def run(self, activity_id: str) -> None: await workflow.execute_activity( - say_hello, - "hi", - priority=Priority( - priority_key=5, fairness_key="tenant4", fairness_weight=3.0 - ), - start_to_close_timeout=timedelta(seconds=5), + external_activity_heartbeat, + activity_id=activity_id, + start_to_close_timeout=timedelta(seconds=10), + heartbeat_timeout=timedelta(seconds=1), + retry_policy=RetryPolicy(maximum_attempts=2), ) - return "Done!" -async def test_workflow_priorities(client: Client, env: WorkflowEnvironment): +async def test_external_activity_cancellation_details( + client: Client, env: WorkflowEnvironment +): if env.supports_time_skipping: - pytest.skip( - "Java test server needs release with: https://github.com/temporalio/sdk-java/pull/2453" - ) - - async with new_worker( - client, WorkflowUsingPriorities, HelloWorkflow, activities=[say_hello] + pytest.skip("Time-skipping server does not support pause API yet") + async with Worker( + client, + task_queue=str(uuid.uuid4()), + workflows=[ExternalActivityWorkflow], + activities=[external_activity_heartbeat], ) as worker: - handle = await client.start_workflow( - WorkflowUsingPriorities.run, - args=[1, False], - id=f"workflow-{uuid.uuid4()}", + test_activity_id = f"heartbeat-activity-{uuid.uuid4()}" + + wf_handle = await client.start_workflow( + ExternalActivityWorkflow.run, + test_activity_id, + id=f"test-external-activity-pause-{uuid.uuid4()}", task_queue=worker.task_queue, - priority=Priority( - priority_key=1, fairness_key="tenant1", fairness_weight=2.5 - ), ) - await handle.result() + wf_desc = await wf_handle.describe() - first_child = True - async for e in handle.fetch_history_events(): - if e.HasField("workflow_execution_started_event_attributes"): - priority = e.workflow_execution_started_event_attributes.priority - assert priority.priority_key == 1 - assert priority.fairness_key == "tenant1" - assert priority.fairness_weight == 2.5 - elif e.HasField( - "start_child_workflow_execution_initiated_event_attributes" - ): - priority = ( - e.start_child_workflow_execution_initiated_event_attributes.priority - ) - if first_child: - assert priority.priority_key == 4 - assert priority.fairness_key == "tenant2" - assert priority.fairness_weight == 1.0 - first_child = False - else: - assert priority.priority_key == 2 - assert priority.fairness_key == "tenant3" - assert priority.fairness_weight == 0.5 - elif e.HasField("activity_task_scheduled_event_attributes"): - priority = e.activity_task_scheduled_event_attributes.priority - assert priority.priority_key == 5 - assert priority.fairness_key == "tenant4" - assert priority.fairness_weight == 3.0 + # Wait for external activity + activity_info = await assert_pending_activity_exists_eventually( + wf_handle, test_activity_id + ) + # Assert not paused + assert not activity_info.paused - # Verify a workflow started without priorities sees None for the key - handle = await client.start_workflow( - WorkflowUsingPriorities.run, - args=[None, True], - id=f"workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, + external_activity_handle = client.get_async_activity_handle( + workflow_id=wf_desc.id, run_id=wf_desc.run_id, activity_id=test_activity_id ) - await handle.result() + # Pause activity then assert it is paused + await pause_and_assert(client, wf_handle, activity_info.activity_id) -@workflow.defn -class ExposeRootChildWorkflow: - def __init__(self) -> None: - self.blocked = True + try: + await external_activity_handle.heartbeat() + except AsyncActivityCancelledError as err: + assert err.details == temporalio.activity.ActivityCancellationDetails( + paused=True + ) - @workflow.signal - def unblock(self) -> None: - self.blocked = False - @workflow.run - async def run(self) -> Optional[temporalio.workflow.RootInfo]: - await workflow.wait_condition(lambda: not self.blocked) - return workflow.info().root +@activity.defn +async def short_activity_async(): + delay = random.uniform(0.05, 0.15) # 50~150ms delay + await asyncio.sleep(delay) + return 1 @workflow.defn -class ExposeRootWorkflow: +class QuickActivityWorkflow: @workflow.run - async def run(self, child_wf_id: str) -> Optional[temporalio.workflow.RootInfo]: - return await workflow.execute_child_workflow( - ExposeRootChildWorkflow.run, id=child_wf_id - ) + async def run(self, total_seconds: float = 10.0): + workflow.logger.info("Duration: %f", total_seconds) + end = workflow.now() + timedelta(seconds=total_seconds) + while True: + workflow.logger.info("Stage 1") + res = await workflow.execute_activity( + short_activity_async, schedule_to_close_timeout=timedelta(seconds=10) + ) + workflow.logger.info("Stage 2, %s", res) + if workflow.now() > end: + break -async def test_expose_root_execution(client: Client, env: WorkflowEnvironment): - if env.supports_time_skipping: - pytest.skip( - "Java test server needs release with: https://github.com/temporalio/sdk-java/pull/2441" - ) + +async def test_quick_activity_swallows_cancellation(client: Client): async with new_worker( - client, ExposeRootWorkflow, ExposeRootChildWorkflow + client, + QuickActivityWorkflow, + activities=[short_activity_async], + activity_executor=concurrent.futures.ThreadPoolExecutor(max_workers=1), ) as worker: - parent_wf_id = f"workflow-{uuid.uuid4()}" - child_wf_id = parent_wf_id + "_child" - handle = await client.start_workflow( - ExposeRootWorkflow.run, - child_wf_id, - id=parent_wf_id, - task_queue=worker.task_queue, - ) - - await assert_workflow_exists_eventually( - client, ExposeRootChildWorkflow, child_wf_id - ) - child_handle: WorkflowHandle = client.get_workflow_handle_for( - ExposeRootChildWorkflow.run, child_wf_id - ) - child_desc = await child_handle.describe() - parent_desc = await handle.describe() - # Assert child root execution is the same as it's parent execution - assert child_desc.root_id == parent_desc.id - assert child_desc.root_run_id == parent_desc.run_id - # Unblock child - await child_handle.signal(ExposeRootChildWorkflow.unblock) - # Get the result (child info) - child_wf_info_root = await handle.result() - # Assert root execution in child info is same as it's parent execution - assert child_wf_info_root is not None - assert child_wf_info_root.workflow_id == parent_desc.id - assert child_wf_info_root.run_id == parent_desc.run_id - - -@workflow.defn(dynamic=True) -class WorkflowDynamicConfigFnFailure: - @workflow.dynamic_config - def dynamic_config(self) -> temporalio.workflow.DynamicWorkflowConfig: - raise Exception("Dynamic config failure") - - @workflow.run - async def run(self, args: Sequence[RawValue]) -> None: - raise RuntimeError("Should never actually run") - - -async def test_workflow_dynamic_config_failure(client: Client): - async with new_worker(client, WorkflowDynamicConfigFnFailure) as worker: - handle = await client.start_workflow( - "verycooldynamicworkflow", - id=f"dynamic-config-failure-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=5), - ) - - # Assert workflow task fails with our expected error message - await assert_task_fail_eventually( - handle, message_contains="Dynamic config failure" - ) - + # Keep this deterministic and bounded. The original randomized 10-iteration + # version could exceed the per-test timeout on slower CI hosts if + # cancellation was delayed a few times in a row. + for i, wf_duration in enumerate((5.0, 7.5, 10.0)): + wf_handle = await client.start_workflow( + QuickActivityWorkflow.run, + id=f"short_activity_wf_id-{i}", + args=[wf_duration], + task_queue=worker.task_queue, + execution_timeout=timedelta(minutes=1), + ) -@activity.defn -async def raise_application_error(use_benign: bool) -> typing.NoReturn: - if use_benign: - raise ApplicationError( - "This is a benign error", category=ApplicationErrorCategory.BENIGN - ) - else: - raise ApplicationError( - "This is a regular error", category=ApplicationErrorCategory.UNSPECIFIED - ) + # Cancel wf + await asyncio.sleep(1.0) + await wf_handle.cancel() + with pytest.raises(WorkflowFailureError) as err_info: + await wf_handle.result() # failed + cause = err_info.value.cause -@workflow.defn -class RaiseErrorWorkflow: - @workflow.run - async def run(self, use_benign: bool) -> None: - # Execute activity that will raise an error - await workflow.execute_activity( - raise_application_error, - use_benign, - start_to_close_timeout=timedelta(seconds=5), - retry_policy=RetryPolicy(maximum_attempts=1), - ) + assert isinstance(cause, CancelledError) + assert cause.message == "Workflow cancelled" -async def test_activity_benign_error_not_logged(client: Client): - with LogCapturer().logs_captured(activity.logger.base_logger) as capturer: +async def test_workflow_logging_trace_identifier(client: Client): + with LogCapturer().logs_captured( + temporalio.worker._workflow_instance.logger + ) as capturer: async with new_worker( - client, RaiseErrorWorkflow, activities=[raise_application_error] + client, + TaskFailOnceWorkflow, + activities=[task_fail_once_activity], ) as worker: - # Run with benign error - with pytest.raises(WorkflowFailureError) as err: - await client.execute_workflow( - RaiseErrorWorkflow.run, - True, - id=str(uuid.uuid4()), - task_queue=worker.task_queue, - ) - # Check that the cause is an ApplicationError - assert isinstance(err.value.cause, ActivityError) - assert isinstance(err.value.cause.cause, ApplicationError) - # Assert the expected category - assert err.value.cause.cause.category == ApplicationErrorCategory.BENIGN - assert capturer.find_log("Completing activity as failed") == None + await client.execute_workflow( + TaskFailOnceWorkflow.run, + id="workflow_failure_trace_identifier", + task_queue=worker.task_queue, + ) - # Run with non-benign error - with pytest.raises(WorkflowFailureError) as err: - await client.execute_workflow( - RaiseErrorWorkflow.run, - False, - id=str(uuid.uuid4()), - task_queue=worker.task_queue, - ) + def workflow_failure(l: logging.LogRecord): + if ( + hasattr(l, "__temporal_error_identifier") + and getattr(l, "__temporal_error_identifier") == "WorkflowTaskFailure" + ): + assert l.msg.startswith("Failed activation on workflow") + return True + return False - # Check that the cause is an ApplicationError - assert isinstance(err.value.cause, ActivityError) - assert isinstance(err.value.cause.cause, ApplicationError) - # Assert the expected category - assert ( - err.value.cause.cause.category == ApplicationErrorCategory.UNSPECIFIED - ) - assert capturer.find_log("Completing activity as failed") != None + assert capturer.find(workflow_failure) is not None -async def test_workflow_missing_local_activity(client: Client): - async with new_worker( - client, SimpleLocalActivityWorkflow, activities=[custom_error_activity] - ) as worker: - handle = await client.start_workflow( - SimpleLocalActivityWorkflow.run, - "Temporal", - id=f"workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) +@activity.defn +def use_in_workflow() -> bool: + return workflow.in_workflow() - await assert_task_fail_eventually( - handle, - message_contains="Activity function say_hello is not registered on this worker, available activities: custom_error_activity", + +@workflow.defn +class UseInWorkflow: + @workflow.run + async def run(self): + res = await workflow.execute_activity( + use_in_workflow, schedule_to_close_timeout=timedelta(seconds=10) ) + return res -async def test_workflow_missing_local_activity_but_dynamic(client: Client): +async def test_in_workflow_sync(client: Client): async with new_worker( client, - SimpleLocalActivityWorkflow, - activities=[custom_error_activity, return_name_activity], + UseInWorkflow, + activities=[use_in_workflow], + activity_executor=concurrent.futures.ThreadPoolExecutor(max_workers=1), ) as worker: res = await client.execute_workflow( - SimpleLocalActivityWorkflow.run, - "Temporal", - id=f"workflow-{uuid.uuid4()}", + UseInWorkflow.run, + id="test_in_workflow_sync", task_queue=worker.task_queue, + execution_timeout=timedelta(minutes=1), ) + assert not res - assert res == "say_hello" +class SignalInterceptor(temporalio.worker.Interceptor): + def workflow_interceptor_class( + self, input: temporalio.worker.WorkflowInterceptorClassInput + ) -> type[SignalInboundInterceptor]: + return SignalInboundInterceptor -async def test_workflow_missing_local_activity_no_activities(client: Client): + +class SignalInboundInterceptor(temporalio.worker.WorkflowInboundInterceptor): + def init(self, outbound: temporalio.worker.WorkflowOutboundInterceptor) -> None: + def unblock() -> None: + return None + + workflow.set_signal_handler("my_random_signal", unblock) + super().init(outbound) + + +async def test_signal_handler_in_interceptor(client: Client): async with new_worker( client, - SimpleLocalActivityWorkflow, - activities=[], + HelloWorkflow, + interceptors=[SignalInterceptor()], ) as worker: - handle = await client.start_workflow( - SimpleLocalActivityWorkflow.run, + await client.execute_workflow( + HelloWorkflow.run, "Temporal", id=f"workflow-{uuid.uuid4()}", task_queue=worker.task_queue, ) - await assert_task_fail_eventually( - handle, - message_contains="Activity function say_hello is not registered on this worker, no available activities", - ) +class HeaderWorkerInterceptor(temporalio.worker.Interceptor): + def intercept_activity( + self, next: temporalio.worker.ActivityInboundInterceptor + ) -> temporalio.worker.ActivityInboundInterceptor: + return HeaderActivityInboundInterceptor(super().intercept_activity(next)) -@activity.defn -async def heartbeat_activity( - catch_err: bool = True, -) -> Optional[temporalio.activity.ActivityCancellationDetails]: - while True: - try: - activity.heartbeat() - # If we have heartbeat details, we are on the second attempt, we have retried due to pause/unpause. - if activity.info().heartbeat_details: - return activity.cancellation_details() - await asyncio.sleep(0.1) - except (CancelledError, asyncio.CancelledError) as err: - if not catch_err: - raise err - return activity.cancellation_details() - finally: - activity.heartbeat("finally-complete") + def workflow_interceptor_class( + self, input: temporalio.worker.WorkflowInterceptorClassInput + ) -> type[temporalio.worker.WorkflowInboundInterceptor] | None: + return HeaderWorkflowInboundInterceptor -@activity.defn -def sync_heartbeat_activity( - catch_err: bool = True, -) -> Optional[temporalio.activity.ActivityCancellationDetails]: - while True: - try: - activity.heartbeat() - # If we have heartbeat details, we are on the second attempt, we have retried due to pause/unpause. - if activity.info().heartbeat_details: - return activity.cancellation_details() - time.sleep(0.1) - except (CancelledError, asyncio.CancelledError) as err: - if not catch_err: - raise err - return activity.cancellation_details() - finally: - activity.heartbeat("finally-complete") +global_header_codec_behavior: HeaderCodecBehavior -@workflow.defn -class ActivityHeartbeatWorkflow: - @workflow.run - async def run( - self, activity_id: str - ) -> list[Optional[temporalio.activity.ActivityCancellationDetails]]: - result = [] - result.append( - await workflow.execute_activity( - sync_heartbeat_activity, - activity_id=activity_id, - start_to_close_timeout=timedelta(seconds=10), - heartbeat_timeout=timedelta(seconds=2), - retry_policy=RetryPolicy(maximum_attempts=1), - ) - ) - result.append( - await workflow.execute_activity( - heartbeat_activity, - activity_id=f"{activity_id}-2", - start_to_close_timeout=timedelta(seconds=10), - heartbeat_timeout=timedelta(seconds=2), - retry_policy=RetryPolicy(maximum_attempts=1), - ) +class HeaderActivityInboundInterceptor(temporalio.worker.ActivityInboundInterceptor): + async def execute_activity( + self, input: temporalio.worker.ExecuteActivityInput + ) -> Any: + if global_header_codec_behavior == HeaderCodecBehavior.WORKFLOW_ONLY_CODEC: + assert input.headers["foo"].data == b"\n\x05\x12\x03bar" + else: + assert input.headers["foo"].data == b"bar" + + return await super().execute_activity(input) + + +class HeaderWorkflowInboundInterceptor(temporalio.worker.WorkflowInboundInterceptor): + def init(self, outbound: temporalio.worker.WorkflowOutboundInterceptor) -> None: + super().init(HeaderWorkflowOutboundInterceptor(outbound)) + + async def handle_signal(self, input: HandleSignalInput) -> None: + assert input.headers["foo"].data == b"bar" + await super().handle_signal(input) + + async def execute_workflow(self, input: ExecuteWorkflowInput) -> Any: + assert input.headers["foo"].data == b"bar" + return await super().execute_workflow(input) + + +class HeaderWorkflowOutboundInterceptor(temporalio.worker.WorkflowOutboundInterceptor): + def start_activity( + self, input: temporalio.worker.StartActivityInput + ) -> workflow.ActivityHandle: + # Add a header to the outbound activity call + input.headers = {"foo": Payload(data=b"bar")} + return super().start_activity(input) + + +class HeaderClientInterceptor(temporalio.client.Interceptor): + def __init__(self, header: Payload): + self.header = header + super().__init__() + + def intercept_client( + self, next: temporalio.client.OutboundInterceptor + ) -> temporalio.client.OutboundInterceptor: + return HeaderClientOutboundInterceptor( + super().intercept_client(next), self.header ) - return result -async def test_activity_pause_cancellation_details( - client: Client, env: WorkflowEnvironment +class HeaderClientOutboundInterceptor(temporalio.client.OutboundInterceptor): + def __init__( + self, next: temporalio.client.OutboundInterceptor, header: Payload + ) -> None: + self.header = header + super().__init__(next) + + async def start_workflow( + self, input: temporalio.client.StartWorkflowInput + ) -> WorkflowHandle[Any, Any]: + input.headers = {"foo": self.header.__deepcopy__()} + return await super().start_workflow(input) + + async def signal_workflow(self, input: SignalWorkflowInput) -> None: + input.headers = {"foo": self.header.__deepcopy__()} + return await super().signal_workflow(input) + + async def create_schedule(self, input: CreateScheduleInput) -> ScheduleHandle: + cast(ScheduleActionStartWorkflow, input.schedule.action).headers = { + "foo": self.header.__deepcopy__() + } + return await super().create_schedule(input) + + +@pytest.mark.parametrize( + "header_codec_behavior", + [ + HeaderCodecBehavior.NO_CODEC, + HeaderCodecBehavior.CODEC, + HeaderCodecBehavior.WORKFLOW_ONLY_CODEC, + ], +) +async def test_workflow_headers_with_codec( + client: Client, env: WorkflowEnvironment, header_codec_behavior: HeaderCodecBehavior ): if env.supports_time_skipping: - pytest.skip("Time-skipping server does not support pause API yet") - with concurrent.futures.ThreadPoolExecutor() as executor: - async with Worker( - client, - task_queue=str(uuid.uuid4()), - workflows=[ActivityHeartbeatWorkflow], - activities=[heartbeat_activity, sync_heartbeat_activity], - activity_executor=executor, - ) as worker: - test_activity_id = f"heartbeat-activity-{uuid.uuid4()}" + pytest.skip("Time skipping server doesn't persist headers.") - handle = await client.start_workflow( - ActivityHeartbeatWorkflow.run, - test_activity_id, - id=f"test-activity-pause-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) + header_payload = Payload(data=b"bar") + if header_codec_behavior == HeaderCodecBehavior.WORKFLOW_ONLY_CODEC: + header_payload = (await SimpleCodec().encode([header_payload]))[0] - # Wait for sync activity - activity_info_1 = await assert_pending_activity_exists_eventually( - handle, test_activity_id - ) - # Assert not paused - assert not activity_info_1.paused - # Pause activity then assert it is paused - await pause_and_assert(client, handle, activity_info_1.activity_id) + # Make client with this codec and run a couple of existing tests + config = client.config() + config["data_converter"] = DataConverter(payload_codec=SimpleCodec()) + config["interceptors"] = [HeaderClientInterceptor(header_payload)] + config["header_codec_behavior"] = header_codec_behavior + client = Client(**config) - # Wait for async activity - activity_info_2 = await assert_pending_activity_exists_eventually( - handle, f"{test_activity_id}-2" - ) - # Assert not paused - assert not activity_info_2.paused - # Pause activity then assert it is paused - await pause_and_assert(client, handle, activity_info_2.activity_id) + global global_header_codec_behavior + global_header_codec_behavior = header_codec_behavior - # Assert workflow return value for paused activities that caught the - # cancel error - result = await handle.result() - assert result[0] == temporalio.activity.ActivityCancellationDetails( - paused=True - ) - assert result[1] == temporalio.activity.ActivityCancellationDetails( - paused=True - ) + async with new_worker( + client, + SimpleActivityWorkflow, + SignalAndQueryWorkflow, + activities=[say_hello], + interceptors=[HeaderWorkerInterceptor()], + ) as worker: + workflow_handle = await client.start_workflow( + SimpleActivityWorkflow.run, + "Temporal", + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + assert await workflow_handle.result() == "Hello, Temporal!" + async for e in workflow_handle.fetch_history_events(): + if e.HasField("activity_task_scheduled_event_attributes"): + header = e.activity_task_scheduled_event_attributes.header.fields["foo"] + if header_codec_behavior == HeaderCodecBehavior.CODEC: + assert "simple-codec" in header.metadata -@workflow.defn -class ActivityHeartbeatPauseUnpauseWorkflow: - @workflow.run - async def run( - self, activity_id: str - ) -> list[Optional[temporalio.activity.ActivityCancellationDetails]]: - results = [] - results.append( - await workflow.execute_activity( - sync_heartbeat_activity, - False, - activity_id=activity_id, - start_to_close_timeout=timedelta(seconds=10), - heartbeat_timeout=timedelta(seconds=1), - retry_policy=RetryPolicy(maximum_attempts=2), - ) + handle = await client.start_workflow( + SignalAndQueryWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, ) - results.append( - await workflow.execute_activity( - heartbeat_activity, - False, - activity_id=f"{activity_id}-2", - start_to_close_timeout=timedelta(seconds=10), - heartbeat_timeout=timedelta(seconds=1), - retry_policy=RetryPolicy(maximum_attempts=2), - ) + + # Simple signals and queries + await handle.signal(SignalAndQueryWorkflow.signal1, "some arg") + assert "signal1: some arg" == await handle.query( + SignalAndQueryWorkflow.last_event ) - return results + async for e in handle.fetch_history_events(): + if e.HasField("workflow_execution_signaled_event_attributes"): + header = e.workflow_execution_signaled_event_attributes.header.fields[ + "foo" + ] + if header_codec_behavior == HeaderCodecBehavior.CODEC: + assert "simple-codec" in header.metadata -async def test_activity_pause_unpause(client: Client, env: WorkflowEnvironment): - if env.supports_time_skipping: - pytest.skip("Time-skipping server does not support pause API yet") + schedule_handle = await client.create_schedule( + f"schedule-{uuid.uuid4()}", + temporalio.client.Schedule( + action=temporalio.client.ScheduleActionStartWorkflow( + "SimpleActivityWorkflow", + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ), + spec=temporalio.client.ScheduleSpec( + calendars=[temporalio.client.ScheduleCalendarSpec()] + ), + state=temporalio.client.ScheduleState(paused=True), + ), + ) + description = await schedule_handle.describe() - async def check_heartbeat_details_exist( - handle: WorkflowHandle, - activity_id: str, - ) -> None: - act_info = await get_pending_activity_info(handle, activity_id) - if act_info is None: - raise AssertionError(f"Activity with ID {activity_id} not found.") - if len(act_info.heartbeat_details.payloads) == 0: - raise AssertionError( - f"Activity with ID {activity_id} has no heartbeat details" - ) + # Header payload is still encoded due to limitations + headers = cast(ScheduleActionStartWorkflow, description.schedule.action).headers + assert headers is not None + if header_codec_behavior == HeaderCodecBehavior.NO_CODEC: + assert headers["foo"].data == b"bar" + else: + assert headers["foo"].data != b"bar" - with concurrent.futures.ThreadPoolExecutor() as executor: - async with Worker( - client, - task_queue=str(uuid.uuid4()), - workflows=[ActivityHeartbeatPauseUnpauseWorkflow], - activities=[heartbeat_activity, sync_heartbeat_activity], - activity_executor=executor, - max_heartbeat_throttle_interval=timedelta(milliseconds=300), - default_heartbeat_throttle_interval=timedelta(milliseconds=300), - ) as worker: - test_activity_id = f"heartbeat-activity-{uuid.uuid4()}" - handle = await client.start_workflow( - ActivityHeartbeatPauseUnpauseWorkflow.run, - test_activity_id, - id=f"test-activity-pause-unpause-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) +@workflow.defn +class PreviousRunFailureWorkflow: + @workflow.run + async def run(self) -> str: + if workflow.info().attempt != 1: + previous_failure = workflow.get_last_failure() + assert isinstance(previous_failure, ApplicationError) + assert previous_failure.message == "Intentional Failure" + return "Done" + raise ApplicationError("Intentional Failure") - # Wait for sync activity - activity_info_1 = await assert_pending_activity_exists_eventually( - handle, test_activity_id - ) - # Assert not paused - assert not activity_info_1.paused - # Pause activity then assert it is paused - await pause_and_assert(client, handle, activity_info_1.activity_id) - # Wait for heartbeat details to exist. At this point, the activity has finished executing - # due to cancellation from the pause. - await assert_eventually( - lambda: check_heartbeat_details_exist( - handle, activity_info_1.activity_id - ) - ) +async def test_previous_run_failure(client: Client): + async with new_worker(client, PreviousRunFailureWorkflow) as worker: + handle = await client.start_workflow( + PreviousRunFailureWorkflow.run, + id=f"previous-run-failure-workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + retry_policy=RetryPolicy( + initial_interval=timedelta(milliseconds=10), + maximum_attempts=2, + ), + ) + result = await handle.result() + assert result == "Done" - # Unpause activity - await unpause_and_assert(client, handle, activity_info_1.activity_id) - # Expect second activity to have started now - activity_info_2 = await assert_pending_activity_exists_eventually( - handle, f"{test_activity_id}-2" - ) - # Assert not paused - assert not activity_info_2.paused - # Pause activity then assert it is paused - await pause_and_assert(client, handle, activity_info_2.activity_id) - # Wait for heartbeat details to exist. At this point, the activity has finished executing - # due to cancellation from the pause. - await assert_eventually( - lambda: check_heartbeat_details_exist( - handle, activity_info_2.activity_id - ) + +class FakeEncryptionCodec(PayloadCodec): + def __init__( + self, + key_id: str = "test-key-id", + key: bytes = b"test-key-test-key-test-key-test!", + ) -> None: + super().__init__() + self.key_id = key_id + + async def encode(self, payloads: Sequence[Payload]) -> list[Payload]: + # We blindly encode all payloads with the key and set the metadata + # saying which key we used + return [ + Payload( + metadata={ + "encoding": b"binary/encrypted", + "encryption-key-id": self.key_id.encode(), + }, + data=self.encrypt(p.SerializeToString()), ) - # Unpause activity - await unpause_and_assert(client, handle, activity_info_2.activity_id) + for p in payloads + ] - # Check workflow complete - result = await handle.result() - assert result[0] == None - assert result[1] == None + async def decode(self, payloads: Sequence[Payload]) -> list[Payload]: + ret: list[Payload] = [] + for p in payloads: + # Ignore ones w/out our expected encoding + if p.metadata.get("encoding", b"").decode() != "binary/encrypted": + ret.append(p) + continue + # Confirm our key ID is the same + key_id = p.metadata.get("encryption-key-id", b"").decode() + if key_id != self.key_id: + raise ValueError( + f"Unrecognized key ID {key_id}. Current key ID is {self.key_id}." + ) + # Decrypt and append + ret.append(Payload.FromString(self.decrypt(p.data))) + return ret + def encrypt(self, data: bytes) -> bytes: + return data -@activity.defn -async def external_activity_heartbeat() -> None: - activity.raise_complete_async() + def decrypt(self, data: bytes) -> bytes: + return data @workflow.defn -class ExternalActivityWorkflow: +class SearchAttributeCodecParentWorkflow: @workflow.run - async def run(self, activity_id: str) -> None: - await workflow.execute_activity( - external_activity_heartbeat, - activity_id=activity_id, - start_to_close_timeout=timedelta(seconds=10), - heartbeat_timeout=timedelta(seconds=1), - retry_policy=RetryPolicy(maximum_attempts=2), + async def run(self, name: str) -> str: + print( + await workflow.execute_child_workflow( + workflow=SearchAttributeCodecChildWorkflow.run, + arg=name, + id=f"child-{name}", + search_attributes=workflow.info().typed_search_attributes, + ) ) + return f"Hello, {name}" -async def test_external_activity_cancellation_details( - client: Client, env: WorkflowEnvironment -): - if env.supports_time_skipping: - pytest.skip("Time-skipping server does not support pause API yet") - async with Worker( - client, - task_queue=str(uuid.uuid4()), - workflows=[ExternalActivityWorkflow], - activities=[external_activity_heartbeat], - ) as worker: - test_activity_id = f"heartbeat-activity-{uuid.uuid4()}" - - wf_handle = await client.start_workflow( - ExternalActivityWorkflow.run, - test_activity_id, - id=f"test-external-activity-pause-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) - wf_desc = await wf_handle.describe() +@workflow.defn +class SearchAttributeCodecChildWorkflow: + @workflow.run + async def run(self, name: str) -> str: + return f"Hello from child, {name}" - # Wait for external activity - activity_info = await assert_pending_activity_exists_eventually( - wf_handle, test_activity_id - ) - # Assert not paused - assert not activity_info.paused - external_activity_handle = client.get_async_activity_handle( - workflow_id=wf_desc.id, run_id=wf_desc.run_id, activity_id=test_activity_id - ) +@pytest.mark.requires_local_server +async def test_search_attribute_codec(client: Client, env_type: str): + if env_type != "local": + pytest.skip("Only testing search attributes on local which disables cache") + await ensure_search_attributes_present( + client, + SearchAttributeWorkflow.text_attribute, + ) - # Pause activity then assert it is paused - await pause_and_assert(client, wf_handle, activity_info.activity_id) + config = client.config() + config["data_converter"] = dataclasses.replace( + temporalio.converter.default(), payload_codec=FakeEncryptionCodec() + ) + client = Client(**config) - try: - await external_activity_handle.heartbeat() - except AsyncActivityCancelledError as err: - assert err.details == temporalio.activity.ActivityCancellationDetails( - paused=True - ) + # Run a worker for the workflow + async with new_worker( + client, + SearchAttributeCodecParentWorkflow, + SearchAttributeCodecChildWorkflow, + ) as worker: + # Run workflow + await client.execute_workflow( + SearchAttributeCodecParentWorkflow.run, + "Temporal", + id="encryption-workflow-id", + task_queue=worker.task_queue, + search_attributes=TypedSearchAttributes( + [ + SearchAttributePair( + SearchAttributeWorkflow.text_attribute, "test_text" + ) + ] + ), + ) @activity.defn -async def short_activity_async(): - delay = random.uniform(0.05, 0.15) # 50~150ms delay - await asyncio.sleep(delay) - return 1 +async def activity_that_fails_with_details() -> str: + """Activity that raises an ApplicationError with custom details.""" + raise ApplicationError( + "Activity failed intentionally", + "detail1", + {"error_code": "NOT_FOUND", "id": "test-123"}, + non_retryable=True, + ) @workflow.defn -class QuickActivityWorkflow: +class WorkflowWithFailingActivityAndCodec: @workflow.run - async def run(self, total_seconds: float = 10.0): - workflow.logger.info("Duration: %f", total_seconds) - end = workflow.now() + timedelta(seconds=total_seconds) - while True: - workflow.logger.info("Stage 1") - res = await workflow.execute_activity( - short_activity_async, schedule_to_close_timeout=timedelta(seconds=10) + async def run(self) -> str: + try: + return await workflow.execute_activity( + activity_that_fails_with_details, + schedule_to_close_timeout=timedelta(seconds=3), + retry_policy=RetryPolicy(maximum_attempts=1), ) - workflow.logger.info("Stage 2, %s", res) + except ActivityError as err: + assert isinstance(err.cause, ApplicationError) + assert err.cause.message == "Activity failed intentionally" + assert len(err.cause.details) == 2 + assert err.cause.details[0] == "detail1" + assert err.cause.details[1] == {"error_code": "NOT_FOUND", "id": "test-123"} + return "Handled encrypted failure successfully" - if workflow.now() > end: - break +async def test_activity_failure_with_encoded_payload_is_decoded_in_workflow( + client: Client, +): + config = client.config() + config["data_converter"] = dataclasses.replace( + temporalio.converter.default(), payload_codec=FakeEncryptionCodec() + ) + client = Client(**config) -async def test_quick_activity_swallows_cancellation(client: Client): async with new_worker( client, - QuickActivityWorkflow, - activities=[short_activity_async], - activity_executor=concurrent.futures.ThreadPoolExecutor(max_workers=1), + WorkflowWithFailingActivityAndCodec, + activities=[activity_that_fails_with_details], ) as worker: - for i in range(10): - wf_duration = random.uniform(5.0, 15.0) - wf_handle = await client.start_workflow( - QuickActivityWorkflow.run, - id=f"short_activity_wf_id-{i}", - args=[wf_duration], - task_queue=worker.task_queue, - execution_timeout=timedelta(minutes=1), - ) + result = await client.execute_workflow( + WorkflowWithFailingActivityAndCodec.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + run_timeout=timedelta(seconds=5), + ) + assert result == "Handled encrypted failure successfully" - # Cancel wf - await asyncio.sleep(1.0) - await wf_handle.cancel() - with pytest.raises(WorkflowFailureError) as err_info: - await wf_handle.result() # failed - cause = err_info.value.cause +@workflow.defn +class DisableLoggerSandbox: + @workflow.run + async def run(self): + workflow.logger.info("Running workflow") - assert isinstance(cause, CancelledError) - assert cause.message == "Workflow cancelled" +class CustomLogHandler(logging.Handler): + def emit(self, record: logging.LogRecord) -> None: + import httpx # type: ignore[reportUnusedImport] # noqa -async def test_workflow_logging_trace_identifier(client: Client): - with LogCapturer().logs_captured( - temporalio.worker._workflow_instance.logger - ) as capturer: + +async def test_disable_logger_sandbox( + client: Client, +): + async def execute_with_new_worker(*, disable_sandbox: bool) -> None: + workflow.logger.unsafe_disable_sandbox(disable_sandbox) async with new_worker( client, - TaskFailOnceWorkflow, - activities=[task_fail_once_activity], + DisableLoggerSandbox, + activities=[], ) as worker: await client.execute_workflow( - TaskFailOnceWorkflow.run, - id="workflow_failure_trace_identifier", + DisableLoggerSandbox.run, + id=f"workflow-{uuid.uuid4()}", task_queue=worker.task_queue, + run_timeout=timedelta(seconds=5), + retry_policy=RetryPolicy(maximum_attempts=1), ) - def workflow_failure(l: logging.LogRecord): - if ( - hasattr(l, "__temporal_error_identifier") - and getattr(l, "__temporal_error_identifier") == "WorkflowTaskFailure" - ): - assert l.msg.startswith("Failed activation on workflow") - return True - return False + logger = workflow.logger.logger + handler = CustomLogHandler() + with LogHandler.apply(logger, handler): + with pytest.raises(WorkflowFailureError): + await execute_with_new_worker(disable_sandbox=False) + await execute_with_new_worker(disable_sandbox=True) + with pytest.raises(WorkflowFailureError): + await execute_with_new_worker(disable_sandbox=False) - assert capturer.find(workflow_failure) is not None +@workflow.defn +class RandomSeedTestWorkflow: + def __init__(self) -> None: + self.seed_changes: list[int] = [] + self.continue_signal_received = False + self._ready = False -@activity.defn -def use_in_workflow() -> bool: - return workflow.in_workflow() + @workflow.run + async def run(self) -> dict[str, Any]: + # Get the initial seed + initial_seed = workflow.random_seed() + # Register callback to track seed changes + workflow.register_random_seed_callback(self._on_seed_change) -@workflow.defn -class UseInWorkflow: - @workflow.run - async def run(self): - res = await workflow.execute_activity( - use_in_workflow, schedule_to_close_timeout=timedelta(seconds=10) - ) - return res + # Create a new random instance that auto-reseeds + auto_random = workflow.new_random() + # Generate random values before waiting + auto_value1 = auto_random.randint(1, 1000000) -async def test_in_workflow_sync(client: Client): - async with new_worker( - client, - UseInWorkflow, - activities=[use_in_workflow], - activity_executor=concurrent.futures.ThreadPoolExecutor(max_workers=1), - ) as worker: - res = await client.execute_workflow( - UseInWorkflow.run, - id="test_in_workflow_sync", - task_queue=worker.task_queue, - execution_timeout=timedelta(minutes=1), + # Do an activity to give a reset point + await workflow.execute_activity( + say_hello, + "Hi", + schedule_to_close_timeout=timedelta(seconds=5), ) - assert not res + self._ready = True -class SignalInterceptor(temporalio.worker.Interceptor): - def workflow_interceptor_class( - self, input: temporalio.worker.WorkflowInterceptorClassInput - ) -> Type[SignalInboundInterceptor]: - return SignalInboundInterceptor + # Wait for signal to continue - this allows for workflow reset + await workflow.wait_condition(lambda: self.continue_signal_received) + # Generate more random values after reset might have occurred + auto_value2 = auto_random.randint(1, 1000000) -class SignalInboundInterceptor(temporalio.worker.WorkflowInboundInterceptor): - def init(self, outbound: temporalio.worker.WorkflowOutboundInterceptor) -> None: - def unblock() -> None: - return None + # Get final seed + final_seed = workflow.random_seed() - workflow.set_signal_handler("my_random_signal", unblock) - super().init(outbound) + return { + "initial_seed": initial_seed, + "final_seed": final_seed, + "seed_changes": self.seed_changes.copy(), + "auto_values": [auto_value1, auto_value2], + } + def _on_seed_change(self, new_seed: int) -> None: + self.seed_changes.append(new_seed) -async def test_signal_handler_in_interceptor(client: Client): + @workflow.signal + def continue_workflow(self) -> None: + self.continue_signal_received = True + + @workflow.query + def ready(self) -> bool: + return self._ready + + +async def test_random_seed_functionality( + client: Client, worker: Worker, env: WorkflowEnvironment +): + if env.supports_time_skipping: + pytest.skip("Java test server doesn't support reset") async with new_worker( - client, - HelloWorkflow, - interceptors=[SignalInterceptor()], + client, RandomSeedTestWorkflow, activities=[say_hello], max_cached_workflows=0 ) as worker: - await client.execute_workflow( - HelloWorkflow.run, - "Temporal", - id=f"workflow-{uuid.uuid4()}", + workflow_id = f"test-random-seed-{uuid.uuid4()}" + handle = await client.start_workflow( + RandomSeedTestWorkflow.run, + id=workflow_id, task_queue=worker.task_queue, ) + # Let workflow generate some random values + # Wait for workflow to be ready + async def ready() -> bool: + return await handle.query(RandomSeedTestWorkflow.ready) -class HeaderWorkerInterceptor(temporalio.worker.Interceptor): - def intercept_activity( - self, next: temporalio.worker.ActivityInboundInterceptor - ) -> temporalio.worker.ActivityInboundInterceptor: - return HeaderActivityInboundInterceptor(super().intercept_activity(next)) + await assert_eq_eventually(True, ready) + + # Reset workflow using raw gRPC call to trigger seed change + from temporalio.api.common.v1.message_pb2 import WorkflowExecution + from temporalio.api.enums.v1.reset_pb2 import ResetReapplyType + from temporalio.api.workflowservice.v1 import ResetWorkflowExecutionRequest + + await client.workflow_service.reset_workflow_execution( + ResetWorkflowExecutionRequest( + namespace=client.namespace, + workflow_execution=WorkflowExecution( + workflow_id=handle.id, + run_id="", + ), + reason="Test seed change", + reset_reapply_type=ResetReapplyType.RESET_REAPPLY_TYPE_UNSPECIFIED, + request_id=str(uuid.uuid4()), + workflow_task_finish_event_id=9, # Reset to after activity completion + ) + ) + + # Get handle to the reset workflow using the new run ID + reset_handle = client.get_workflow_handle( + workflow_id, + ) + + # Continue the workflow + await reset_handle.signal(RandomSeedTestWorkflow.continue_workflow) + + result = await reset_handle.result() + + # Verify basic functionality + assert isinstance(result["initial_seed"], int) + assert isinstance(result["final_seed"], int) + assert isinstance(result["seed_changes"], list) + assert len(result["auto_values"]) == 2 + assert len(result["seed_changes"]) == 1 - def workflow_interceptor_class( - self, input: temporalio.worker.WorkflowInterceptorClassInput - ) -> Optional[Type[temporalio.worker.WorkflowInboundInterceptor]]: - return HeaderWorkflowInboundInterceptor +# Tests for task.uncancel() fix in shield loops (Python 3.11+) +# See https://github.com/temporalio/sdk-python/pull/1523 + + +@workflow.defn +class UncancelShieldActivityWorkflow: + """Workflow that cancels a shielded activity and checks for duplicate commands.""" + + def __init__(self) -> None: + self._activity_result = "" + self._cancel_count = 0 + + @workflow.run + async def run(self) -> str: + handle = workflow.start_activity( + wait_cancel, + schedule_to_close_timeout=timedelta(seconds=30), + heartbeat_timeout=timedelta(seconds=2), + cancellation_type=workflow.ActivityCancellationType.WAIT_CANCELLATION_COMPLETED, + ) + # Let activity start + await asyncio.sleep(0.01) + # Cancel the activity task + handle.cancel() + try: + self._activity_result = await handle + except ActivityError as err: + self._activity_result = f"Error: {err.cause.__class__.__name__}" + except CancelledError: + self._activity_result = "CancelledError" + return self._activity_result -global_header_codec_behavior: HeaderCodecBehavior + @workflow.query + def activity_result(self) -> str: + return self._activity_result -class HeaderActivityInboundInterceptor(temporalio.worker.ActivityInboundInterceptor): - async def execute_activity( - self, input: temporalio.worker.ExecuteActivityInput - ) -> Any: - if global_header_codec_behavior == HeaderCodecBehavior.WORKFLOW_ONLY_CODEC: - assert input.headers["foo"].data == b"\n\x05\x12\x03bar" - else: - assert input.headers["foo"].data == b"bar" +async def test_workflow_uncancel_shield_activity(client: Client): + """Verify that cancelling a shielded activity does not produce duplicate + cancel commands or spurious error logs due to elevated cancellation counter. + """ + log_capturer = LogCapturer() + with log_capturer.logs_captured( + temporalio.worker._workflow_instance.logger, level=logging.WARNING + ): + async with new_worker( + client, + UncancelShieldActivityWorkflow, + activities=[wait_cancel], + ) as worker: + result = await client.execute_workflow( + UncancelShieldActivityWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + # The activity should have been cancelled successfully + assert result == "Got cancelled error, cancelled? True" - return await super().execute_activity(input) + # Verify no spurious "exception in shielded future" error logs + shielded_err = log_capturer.find_log("exception in shielded future") + assert shielded_err is None, ( + f"Unexpected 'exception in shielded future' log: {shielded_err}" + ) -class HeaderWorkflowInboundInterceptor(temporalio.worker.WorkflowInboundInterceptor): - def init(self, outbound: temporalio.worker.WorkflowOutboundInterceptor) -> None: - super().init(HeaderWorkflowOutboundInterceptor(outbound)) +@workflow.defn +class UncancelShieldChildWorkflow: + """Workflow that starts a child workflow, cancels it via task cancel, + and returns information about the cancellation.""" - async def handle_signal(self, input: HandleSignalInput) -> None: - assert input.headers["foo"].data == b"bar" - await super().handle_signal(input) + def __init__(self) -> None: + self._ready = False - async def execute_workflow(self, input: ExecuteWorkflowInput) -> Any: - assert input.headers["foo"].data == b"bar" - return await super().execute_workflow(input) + @workflow.run + async def run(self) -> str: + # Start a child workflow via execute (which internally uses shield loops) + child_task = asyncio.create_task( + workflow.execute_child_workflow( + LongSleepWorkflow.run, + id=f"{workflow.info().workflow_id}_child", + ) + ) + self._ready = True + # Let the child start + await asyncio.sleep(0.01) + # Cancel the child task — this triggers the shield loop's CancelledError + child_task.cancel() + try: + await child_task + return "completed" + except ChildWorkflowError as err: + if isinstance(err.cause, CancelledError): + return "child_cancelled" + return f"child_error: {err.cause}" + except CancelledError: + return "task_cancelled" + @workflow.query + def ready(self) -> bool: + return self._ready -class HeaderWorkflowOutboundInterceptor(temporalio.worker.WorkflowOutboundInterceptor): - def start_activity( - self, input: temporalio.worker.StartActivityInput - ) -> workflow.ActivityHandle: - # Add a header to the outbound activity call - input.headers = {"foo": Payload(data=b"bar")} - return super().start_activity(input) +async def test_workflow_uncancel_shield_child_workflow(client: Client): + """Verify that cancelling a shielded child workflow task does not produce + duplicate RequestCancelExternalWorkflowExecution commands in history. + This was the primary symptom of the bug fixed by task.uncancel(). + """ + log_capturer = LogCapturer() + with log_capturer.logs_captured( + temporalio.worker._workflow_instance.logger, level=logging.WARNING + ): + async with new_worker( + client, + UncancelShieldChildWorkflow, + LongSleepWorkflow, + ) as worker: + handle = await client.start_workflow( + UncancelShieldChildWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + result = await handle.result() -class HeaderClientInterceptor(temporalio.client.Interceptor): - def __init__(self, header: Payload): - self.header = header - super().__init__() + assert result == "child_cancelled" - def intercept_client( - self, next: temporalio.client.OutboundInterceptor - ) -> temporalio.client.OutboundInterceptor: - return HeaderClientOutboundInterceptor( - super().intercept_client(next), self.header + # Check history for duplicate cancel commands + resp = await client.workflow_service.get_workflow_execution_history( + GetWorkflowExecutionHistoryRequest( + namespace=client.namespace, + execution=WorkflowExecution(workflow_id=handle.id), ) + ) + cancel_events = [ + e + for e in resp.history.events + if e.event_type + == EventType.EVENT_TYPE_REQUEST_CANCEL_EXTERNAL_WORKFLOW_EXECUTION_INITIATED + ] + # There should be exactly one cancel request, not duplicates + assert len(cancel_events) == 1, ( + f"Expected exactly 1 RequestCancelExternalWorkflowExecution event, " + f"got {len(cancel_events)}" + ) + # Verify no spurious "exception in shielded future" error logs + shielded_err = log_capturer.find_log("exception in shielded future") + assert shielded_err is None, ( + f"Unexpected 'exception in shielded future' log: {shielded_err}" + ) -class HeaderClientOutboundInterceptor(temporalio.client.OutboundInterceptor): - def __init__( - self, next: temporalio.client.OutboundInterceptor, header: Payload - ) -> None: - self.header = header - super().__init__(next) - async def start_workflow( - self, input: temporalio.client.StartWorkflowInput - ) -> WorkflowHandle[Any, Any]: - input.headers = {"foo": self.header.__deepcopy__()} - return await super().start_workflow(input) +@workflow.defn +class UncancelShieldSignalExternalWorkflow: + """Workflow that signals an external workflow from a task that gets + cancelled, exercising the shield loop in _signal_external_workflow.""" - async def signal_workflow(self, input: SignalWorkflowInput) -> None: - input.headers = {"foo": self.header.__deepcopy__()} - return await super().signal_workflow(input) + def __init__(self) -> None: + self._ready = False - async def create_schedule(self, input: CreateScheduleInput) -> ScheduleHandle: - cast(ScheduleActionStartWorkflow, input.schedule.action).headers = { - "foo": self.header.__deepcopy__() - } - return await super().create_schedule(input) + @workflow.run + async def run(self, target_workflow_id: str) -> str: + # Start a signal task + signal_task = asyncio.create_task( + workflow.get_external_workflow_handle(target_workflow_id).signal( + ReturnSignalWorkflow.my_signal, "test_value" + ) + ) + self._ready = True + # The signal should complete quickly, but we test the path where + # the workflow itself gets cancelled while the signal is pending + try: + await signal_task + return "signal_sent" + except CancelledError: + return "signal_cancelled" + @workflow.query + def ready(self) -> bool: + return self._ready -@pytest.mark.parametrize( - "header_codec_behavior", - [ - HeaderCodecBehavior.NO_CODEC, - HeaderCodecBehavior.CODEC, - HeaderCodecBehavior.WORKFLOW_ONLY_CODEC, - ], -) -async def test_workflow_headers_with_codec( - client: Client, env: WorkflowEnvironment, header_codec_behavior: HeaderCodecBehavior -): - if env.supports_time_skipping: - pytest.skip("Time skipping server doesn't persist headers.") - header_payload = Payload(data=b"bar") - if header_codec_behavior == HeaderCodecBehavior.WORKFLOW_ONLY_CODEC: - header_payload = (await SimpleCodec().encode([header_payload]))[0] +async def test_workflow_uncancel_shield_signal_external(client: Client): + """Verify that signal external workflow completes without spurious errors + when the shield loop properly resets the cancellation counter. + """ + log_capturer = LogCapturer() + with log_capturer.logs_captured( + temporalio.worker._workflow_instance.logger, level=logging.WARNING + ): + async with new_worker( + client, + UncancelShieldSignalExternalWorkflow, + ReturnSignalWorkflow, + ) as worker: + # Start the target workflow that waits for a signal + target_handle = await client.start_workflow( + ReturnSignalWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + # Start the signaler workflow + result = await client.execute_workflow( + UncancelShieldSignalExternalWorkflow.run, + target_handle.id, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + assert result == "signal_sent" + # Confirm the target received the signal + target_result = await target_handle.result() + assert target_result == "test_value" + + # Verify no spurious error logs + shielded_err = log_capturer.find_log("exception in shielded future") + assert shielded_err is None, ( + f"Unexpected 'exception in shielded future' log: {shielded_err}" + ) - # Make client with this codec and run a couple of existing tests - config = client.config() - config["data_converter"] = DataConverter(payload_codec=SimpleCodec()) - config["interceptors"] = [HeaderClientInterceptor(header_payload)] - config["header_codec_behavior"] = header_codec_behavior - client = Client(**config) - global global_header_codec_behavior - global_header_codec_behavior = header_codec_behavior +class _SlowActivity: + def __init__(self) -> None: + self.started = asyncio.Event() - async with new_worker( - client, - SimpleActivityWorkflow, - SignalAndQueryWorkflow, - activities=[say_hello], - interceptors=[HeaderWorkerInterceptor()], - ) as worker: - workflow_handle = await client.start_workflow( - SimpleActivityWorkflow.run, - "Temporal", - id=f"workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) - assert await workflow_handle.result() == "Hello, Temporal!" + @activity.defn(name="slow_activity") + async def slow_activity(self) -> None: + self.started.set() + await asyncio.sleep(60) - async for e in workflow_handle.fetch_history_events(): - if e.HasField("activity_task_scheduled_event_attributes"): - header = e.activity_task_scheduled_event_attributes.header.fields["foo"] - if header_codec_behavior == HeaderCodecBehavior.CODEC: - assert "simple-codec" in header.metadata - handle = await client.start_workflow( - SignalAndQueryWorkflow.run, - id=f"workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, +@workflow.defn +class _CancelInFlightActivityWorkflow: + @workflow.run + async def run(self) -> None: + await asyncio.gather( + *( + workflow.execute_activity( + "slow_activity", + start_to_close_timeout=timedelta(minutes=2), + ) + for _ in range(4) + ) ) - # Simple signals and queries - await handle.signal(SignalAndQueryWorkflow.signal1, "some arg") - assert "signal1: some arg" == await handle.query( - SignalAndQueryWorkflow.last_event - ) - async for e in handle.fetch_history_events(): - if e.HasField("workflow_execution_signaled_event_attributes"): - header = e.workflow_execution_signaled_event_attributes.header.fields[ - "foo" - ] - if header_codec_behavior == HeaderCodecBehavior.CODEC: - assert "simple-codec" in header.metadata +@pytest.mark.asyncio +async def test_workflow_cancel_no_shielded_future_log( + client: Client, caplog: pytest.LogCaptureFixture +): + activity_inst = _SlowActivity() - schedule_handle = await client.create_schedule( - f"schedule-{uuid.uuid4()}", - temporalio.client.Schedule( - action=temporalio.client.ScheduleActionStartWorkflow( - "SimpleActivityWorkflow", - id=f"workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - ), - spec=temporalio.client.ScheduleSpec( - calendars=[temporalio.client.ScheduleCalendarSpec()] - ), - state=temporalio.client.ScheduleState(paused=True), - ), - ) - description = await schedule_handle.describe() + with caplog.at_level(logging.ERROR): + async with new_worker( + client, + _CancelInFlightActivityWorkflow, + activities=[activity_inst.slow_activity], + ) as worker: + handle = await client.start_workflow( + _CancelInFlightActivityWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(minutes=5), + ) - # Header payload is still encoded due to limitations - headers = cast(ScheduleActionStartWorkflow, description.schedule.action).headers - assert headers is not None - if header_codec_behavior == HeaderCodecBehavior.NO_CODEC: - assert headers["foo"].data == b"bar" - else: - assert headers["foo"].data != b"bar" + # Wait for activities to start + await asyncio.wait_for(activity_inst.started.wait(), timeout=10) + # Ignore worker startup logs + caplog.clear() -@workflow.defn -class PreviousRunFailureWorkflow: - @workflow.run - async def run(self) -> str: - if workflow.info().attempt != 1: - previous_failure = workflow.get_last_failure() - assert isinstance(previous_failure, ApplicationError) - assert previous_failure.message == "Intentional Failure" - return "Done" - raise ApplicationError("Intentional Failure") + await handle.cancel() + try: + await handle.result() + except WorkflowFailureError as err: + assert isinstance(err.cause, CancelledError) -async def test_previous_run_failure(client: Client): - async with new_worker(client, PreviousRunFailureWorkflow) as worker: - handle = await client.start_workflow( - PreviousRunFailureWorkflow.run, - id=f"previous-run-failure-workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - retry_policy=RetryPolicy( - initial_interval=timedelta(milliseconds=10), - ), - ) - result = await handle.result() - assert result == "Done" + assert not any( + "exception in shielded future" in record.message for record in caplog.records + ) diff --git a/tests/worker/workflow_sandbox/test_importer.py b/tests/worker/workflow_sandbox/test_importer.py index 33d7e2d9f..0ed478c03 100644 --- a/tests/worker/workflow_sandbox/test_importer.py +++ b/tests/worker/workflow_sandbox/test_importer.py @@ -20,7 +20,7 @@ def test_workflow_sandbox_importer_invalid_module(): with pytest.raises(RestrictedWorkflowAccessError) as err: with Importer(restrictions, RestrictionContext()).applied(): - import tests.worker.workflow_sandbox.testmodules.invalid_module + import tests.worker.workflow_sandbox.testmodules.invalid_module # type:ignore[reportUnusedImport] assert ( err.value.qualified_name == "tests.worker.workflow_sandbox.testmodules.invalid_module" @@ -116,8 +116,8 @@ def test_workflow_sandbox_importer_invalid_module_members(): def test_workflow_sandbox_importer_sys_module(): # Import outside to make sure this is in sys.modules - import tests.worker.workflow_sandbox.testmodules.passthrough_module - import tests.worker.workflow_sandbox.testmodules.stateful_module + import tests.worker.workflow_sandbox.testmodules.passthrough_module # type:ignore[reportUnusedImport] + import tests.worker.workflow_sandbox.testmodules.stateful_module # type:ignore[reportUnusedImport] with Importer(restrictions, RestrictionContext()).applied(): # Passthrough should be there but not non-passthrough @@ -142,8 +142,6 @@ def test_workflow_sandbox_importer_sys_module(): def test_thread_local_sys_module_attrs(): - if sys.version_info < (3, 9): - pytest.skip("Dict or methods only in >= 3.9") # Python chose not to put everything in MutableMapping they do in dict, see # https://bugs.python.org/issue22101. Therefore we manually confirm that # every attribute of sys modules is also in thread local sys modules to @@ -154,7 +152,7 @@ def test_thread_local_sys_module_attrs(): # Let's also test "or" and "copy" norm = {"foo": 123} thread_local = _ThreadLocalSysModules({"foo": 123}) # type: ignore[dict-item] - assert (norm | {"bar": 456}) == (thread_local | {"bar": 456}) + assert (norm | {"bar": 456}) == (thread_local | {"bar": 456}) # type: ignore norm |= {"baz": 789} thread_local |= {"baz": 789} # type: ignore assert norm.copy() == thread_local.copy() diff --git a/tests/worker/workflow_sandbox/test_restrictions.py b/tests/worker/workflow_sandbox/test_restrictions.py index cf96d28d6..ec001ba19 100644 --- a/tests/worker/workflow_sandbox/test_restrictions.py +++ b/tests/worker/workflow_sandbox/test_restrictions.py @@ -1,9 +1,8 @@ from __future__ import annotations import pathlib -import sys from dataclasses import dataclass -from typing import ClassVar, Dict, Optional +from typing import ClassVar import pytest @@ -13,29 +12,9 @@ SandboxMatcher, SandboxRestrictions, _RestrictedProxy, - _stdlib_module_names, ) -def test_workflow_sandbox_stdlib_module_names(): - if sys.version_info[1] != 11: - pytest.skip("Test only runs on 3.11") - actual_names = ",".join(sorted(sys.stdlib_module_names)) - # Uncomment to print code for generating these - code_lines = [""] - for mod_name in sorted(sys.stdlib_module_names): - if code_lines[-1]: - code_lines[-1] += "," - if len(code_lines[-1]) > 80: - code_lines.append("") - code_lines[-1] += mod_name - code = '_stdlib_module_names = (\n "' + '"\n "'.join(code_lines) + '"\n)' - # TODO(cretz): Point releases may add modules :-( - assert ( - actual_names == _stdlib_module_names - ), f"Expecting names as {actual_names}. In code as:\n{code}" - - def test_workflow_sandbox_restrictions_add_passthrough_modules(): updated = SandboxRestrictions.default.with_passthrough_modules("module1", "module2") assert ( @@ -46,12 +25,12 @@ def test_workflow_sandbox_restrictions_add_passthrough_modules(): @dataclass class RestrictableObject: - foo: Optional[RestrictableObject] = None + foo: RestrictableObject | None = None bar: int = 42 baz: ClassVar[int] = 57 qux: ClassVar[RestrictableObject] - some_dict: Optional[Dict] = None + some_dict: dict | None = None RestrictableObject.qux = RestrictableObject(foo=RestrictableObject(bar=70), bar=80) @@ -96,6 +75,21 @@ def test_restricted_proxy_dunder_methods(): assert f"{restricted_path_obj}" == expected_path +def test_restricted_proxy_in_place_operations_preserve_proxy(): + restricted_list = _RestrictedProxy( + "list", + list, + RestrictionContext(), + SandboxMatcher(), + ) + values = restricted_list([1]) + + values += [2] + + assert type(values) is _RestrictedProxy + assert RestrictionContext.unwrap_if_proxied(values) == [1, 2] + + def test_workflow_sandbox_restricted_proxy(): obj_class = _RestrictedProxy( "RestrictableObject", diff --git a/tests/worker/workflow_sandbox/test_runner.py b/tests/worker/workflow_sandbox/test_runner.py index 6d622dfa7..024ab7273 100644 --- a/tests/worker/workflow_sandbox/test_runner.py +++ b/tests/worker/workflow_sandbox/test_runner.py @@ -5,40 +5,46 @@ import functools import inspect import os +import sys import time import uuid +import warnings +from collections.abc import Callable, Sequence from dataclasses import dataclass from datetime import date, datetime, timedelta from enum import IntEnum -from typing import Callable, Dict, List, Optional, Sequence, Set, Type +from typing import Any + +import pytest from temporalio import activity, workflow from temporalio.client import Client, WorkflowFailureError, WorkflowHandle from temporalio.exceptions import ApplicationError -from temporalio.worker import Worker +from temporalio.worker import Worker, WorkflowInboundInterceptor +from temporalio.worker._interceptor import ( + ExecuteWorkflowInput, + Interceptor, + WorkflowInterceptorClassInput, +) from temporalio.worker.workflow_sandbox import ( RestrictedWorkflowAccessError, SandboxedWorkflowRunner, SandboxMatcher, SandboxRestrictions, + UnintentionalPassthroughError, ) +from temporalio.workflow import SandboxImportNotificationPolicy from tests.helpers import assert_eq_eventually from tests.worker.workflow_sandbox.testmodules import stateful_module from tests.worker.workflow_sandbox.testmodules.proto import SomeMessage -# Passing through because Python 3.9 has an import bug at -# https://github.com/python/cpython/issues/91351 -with workflow.unsafe.imports_passed_through(): - import pytest - - global_state = ["global orig"] # We just access os.name in here to show we _can_. It's access-restricted at # runtime only _ = os.name # This used to fail because our __init__ couldn't handle metaclass init -import zipfile +import zipfile # noqa: E402 class MyZipFile(zipfile.ZipFile): @@ -64,7 +70,7 @@ def __init__(self) -> None: self.append("inited") @workflow.run - async def run(self, params: GlobalStateWorkflowParams) -> Dict[str, List[str]]: + async def run(self, params: GlobalStateWorkflowParams) -> dict[str, list[str]]: self.append("started") if params.fail_on_first_attempt: raise ApplicationError("Failing first attempt") @@ -78,7 +84,7 @@ def append(self, str: str) -> None: stateful_module.module_state.append(str) @workflow.query - def state(self) -> Dict[str, List[str]]: + def state(self) -> dict[str, list[str]]: return {"global": global_state, "module": stateful_module.module_state} @@ -93,7 +99,7 @@ def state(self) -> Dict[str, List[str]]: ) async def test_workflow_sandbox_global_state( client: Client, - sandboxed_passthrough_modules: Set[str], + sandboxed_passthrough_modules: set[str], ): global global_state async with new_worker( @@ -103,7 +109,7 @@ async def test_workflow_sandbox_global_state( ) as worker: # Start several workflows in the sandbox and make sure none of it # clashes - handles: List[WorkflowHandle] = [] + handles: list[WorkflowHandle] = [] for _ in range(10): handles.append( await client.start_workflow( @@ -176,7 +182,6 @@ async def test_workflow_sandbox_restrictions(client: Client): "import datetime\ndatetime.datetime.now()", "import os\ngetattr(os.environ, 'foo')", "import os\nos.getenv('foo')", - "import os.path\nos.path.abspath('foo')", "import random\nrandom.choice(['foo', 'bar'])", "import secrets\nsecrets.choice(['foo', 'bar'])", "import threading\nthreading.current_thread()", @@ -186,6 +191,16 @@ async def test_workflow_sandbox_restrictions(client: Client): "import http.client\nhttp.client.HTTPConnection('example.com')", "import uuid\nuuid.uuid4()", ] + + # We can only validate this restriction prior to 3.14 because we had to exempt it due to + # https://github.com/python/cpython/issues/140228 + if sys.version_info < (3, 14): + invalid_code_to_check.append("import os.path\nos.path.abspath('foo')") # type: ignore[reportUnreachable] + + # uuid7 was only added to the stdlib in 3.14 + if sys.version_info >= (3, 14): + invalid_code_to_check.append("import uuid\nuuid.uuid7()") # type: ignore[reportUnreachable] + for code in invalid_code_to_check: with pytest.raises(WorkflowFailureError) as err: await client.execute_workflow( @@ -420,7 +435,7 @@ async def test_workflow_sandbox_known_issues(client: Client): @workflow.defn class BadAsyncioWorkflow: @workflow.run - async def run(self) -> List[str]: + async def run(self) -> list[str]: # Two known bad asyncio task calls, as_completed and wait async def return_value(value: str) -> str: return value @@ -459,11 +474,11 @@ async def test_workflow_sandbox_bad_asyncio(client: Client): def new_worker( client: Client, - *workflows: Type, + *workflows: type, activities: Sequence[Callable] = [], - task_queue: Optional[str] = None, - sandboxed_passthrough_modules: Set[str] = set(), - sandboxed_invalid_module_members: Optional[SandboxMatcher] = None, + task_queue: str | None = None, + sandboxed_passthrough_modules: set[str] = set(), + sandboxed_invalid_module_members: SandboxMatcher | None = None, ) -> Worker: restrictions = SandboxRestrictions.default if sandboxed_passthrough_modules: @@ -481,3 +496,172 @@ def new_worker( activities=activities, workflow_runner=SandboxedWorkflowRunner(restrictions=restrictions), ) + + +class _TestWorkflowInboundInterceptor(WorkflowInboundInterceptor): + async def execute_workflow(self, input: ExecuteWorkflowInput) -> Any: + # import in the interceptor to show it will be captured + # applying this policy should squelch the "after initial workload" warning + with workflow.unsafe.sandbox_import_notification_policy( + workflow.SandboxImportNotificationPolicy.WARN_ON_UNINTENTIONAL_PASSTHROUGH + ): + import tests.worker.workflow_sandbox.testmodules.lazy_module_interceptor # type:ignore[reportUnusedImport] # noqa: F401 + + return await super().execute_workflow(input) + + +class _TestInterceptor(Interceptor): + def workflow_interceptor_class( + self, input: WorkflowInterceptorClassInput + ) -> type[_TestWorkflowInboundInterceptor]: + return _TestWorkflowInboundInterceptor + + +@workflow.defn +class LazyImportWorkflow: + @workflow.run + async def run(self) -> None: + try: + import tests.worker.workflow_sandbox.testmodules.lazy_module # type:ignore[reportUnusedImport] # noqa: F401 + except UnintentionalPassthroughError as err: + raise ApplicationError( + str(err), type="UnintentionalPassthroughError" + ) from err + + +async def test_workflow_sandbox_import_default_warnings(client: Client): + restrictions = dataclasses.replace( + SandboxRestrictions.default, + # passthrough this test module to avoid a ton of noisy warnings + passthrough_modules=SandboxRestrictions.passthrough_modules_default + | {"tests.worker.workflow_sandbox.test_runner"}, + ) + + async with Worker( + client, + task_queue=str(uuid.uuid4()), + workflows=[LazyImportWorkflow], + workflow_runner=SandboxedWorkflowRunner(restrictions), + ) as worker: + with pytest.warns() as recorder: + await client.execute_workflow( + LazyImportWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + _assert_expected_warnings( + recorder, + { + "Module tests.worker.workflow_sandbox.testmodules.lazy_module was imported after initial workflow load.", + }, + ) + + +async def test_workflow_sandbox_import_all_warnings(client: Client): + restrictions = dataclasses.replace( + SandboxRestrictions.default, + import_notification_policy=SandboxImportNotificationPolicy.WARN_ON_DYNAMIC_IMPORT + | SandboxImportNotificationPolicy.WARN_ON_UNINTENTIONAL_PASSTHROUGH, + # passthrough this test module to avoid a ton of noisy warnings + passthrough_modules=SandboxRestrictions.passthrough_modules_default + | {"tests.worker.workflow_sandbox.test_runner"}, + ) + + async with Worker( + client, + task_queue=str(uuid.uuid4()), + workflows=[LazyImportWorkflow], + interceptors=[_TestInterceptor()], + workflow_runner=SandboxedWorkflowRunner(restrictions), + ) as worker: + with pytest.warns() as recorder: + await client.execute_workflow( + LazyImportWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + _assert_expected_warnings( + recorder, + { + "Module tests.worker.workflow_sandbox.testmodules.lazy_module_interceptor was not intentionally passed through to the sandbox.", + "Module tests.worker.workflow_sandbox.testmodules.lazy_module was imported after initial workflow load.", + "Module tests.worker.workflow_sandbox.testmodules.lazy_module was not intentionally passed through to the sandbox.", + }, + ) + + +async def test_workflow_sandbox_import_errors(client: Client): + restrictions = dataclasses.replace( + SandboxRestrictions.default, + import_notification_policy=SandboxImportNotificationPolicy.WARN_ON_DYNAMIC_IMPORT + | SandboxImportNotificationPolicy.RAISE_ON_UNINTENTIONAL_PASSTHROUGH, + # passthrough this test module to avoid a ton of noisy warnings + passthrough_modules=SandboxRestrictions.passthrough_modules_default + | {"tests.worker.workflow_sandbox.test_runner"}, + ) + + async with Worker( + client, + task_queue=str(uuid.uuid4()), + workflows=[LazyImportWorkflow], + workflow_runner=SandboxedWorkflowRunner(restrictions), + ) as worker: + with pytest.raises(WorkflowFailureError) as err: + await client.execute_workflow( + LazyImportWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + assert isinstance(err.value.cause, ApplicationError) + assert err.value.cause.type == "UnintentionalPassthroughError" + assert ( + "Module tests.worker.workflow_sandbox.testmodules.lazy_module was not intentionally passed through to the sandbox." + == err.value.cause.message + ) + + +@workflow.defn +class SupressWarningsLazyImportWorkflow: + @workflow.run + async def run(self) -> None: + with workflow.unsafe.sandbox_import_notification_policy( + SandboxImportNotificationPolicy.SILENT + ): + try: + import tests.worker.workflow_sandbox.testmodules.lazy_module # type:ignore[reportUnusedImport] # noqa: F401 + except UserWarning: + raise ApplicationError("No warnings were expected") + + +async def test_workflow_sandbox_import_suppress_warnings(client: Client): + restrictions = dataclasses.replace( + SandboxRestrictions.default, + # passthrough this test module to avoid a ton of noisy warnings + passthrough_modules=SandboxRestrictions.passthrough_modules_default + | {"tests.worker.workflow_sandbox.test_runner"}, + ) + + async with Worker( + client, + task_queue=str(uuid.uuid4()), + workflows=[SupressWarningsLazyImportWorkflow], + workflow_runner=SandboxedWorkflowRunner(restrictions), + ) as worker: + with warnings.catch_warnings(record=True) as recorder: + warnings.simplefilter("always") + await client.execute_workflow( + SupressWarningsLazyImportWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + assert len(recorder) == 0, "Expected no warnings to be issued" + + +def _assert_expected_warnings( + recorder: pytest.WarningsRecorder, expected_warnings: set[str] +): + actual_warnings = {str(w.message) for w in recorder} + assert expected_warnings <= actual_warnings diff --git a/tests/worker/workflow_sandbox/testmodules/lazy_module.py b/tests/worker/workflow_sandbox/testmodules/lazy_module.py new file mode 100644 index 000000000..3c378a328 --- /dev/null +++ b/tests/worker/workflow_sandbox/testmodules/lazy_module.py @@ -0,0 +1,2 @@ +# intentionally empty +# used during import warning tests diff --git a/tests/worker/workflow_sandbox/testmodules/lazy_module_interceptor.py b/tests/worker/workflow_sandbox/testmodules/lazy_module_interceptor.py new file mode 100644 index 000000000..3c378a328 --- /dev/null +++ b/tests/worker/workflow_sandbox/testmodules/lazy_module_interceptor.py @@ -0,0 +1,2 @@ +# intentionally empty +# used during import warning tests diff --git a/uv.lock b/uv.lock index c5fb60fcd..f64424dca 100644 --- a/uv.lock +++ b/uv.lock @@ -1,23 +1,74 @@ version = 1 -revision = 2 -requires-python = ">=3.9" +revision = 3 +requires-python = ">=3.10" resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version < '3.10'", + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version >= '3.11' and python_full_version < '3.13'", + "python_full_version < '3.11'", +] + +[options] +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer-span = "P2W" + +[[package]] +name = "aioboto3" +version = "15.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiobotocore", extra = ["boto3"] }, + { name = "aiofiles" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/01/92e9ab00f36e2899315f49eefcd5b4685fbb19016c7f19a9edf06da80bb0/aioboto3-15.5.0.tar.gz", hash = "sha256:ea8d8787d315594842fbfcf2c4dce3bac2ad61be275bc8584b2ce9a3402a6979", size = 255069, upload-time = "2025-10-30T13:37:16.122Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/3e/e8f5b665bca646d43b916763c901e00a07e40f7746c9128bdc912a089424/aioboto3-15.5.0-py3-none-any.whl", hash = "sha256:cc880c4d6a8481dd7e05da89f41c384dbd841454fc1998ae25ca9c39201437a6", size = 35913, upload-time = "2025-10-30T13:37:14.549Z" }, +] + +[[package]] +name = "aiobotocore" +version = "2.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "aioitertools" }, + { name = "botocore" }, + { name = "jmespath" }, + { name = "multidict" }, + { name = "python-dateutil" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/94/2e4ec48cf1abb89971cb2612d86f979a6240520f0a659b53a43116d344dc/aiobotocore-2.25.1.tar.gz", hash = "sha256:ea9be739bfd7ece8864f072ec99bb9ed5c7e78ebb2b0b15f29781fbe02daedbc", size = 120560, upload-time = "2025-10-28T22:33:21.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/2a/d275ec4ce5cd0096665043995a7d76f5d0524853c76a3d04656de49f8808/aiobotocore-2.25.1-py3-none-any.whl", hash = "sha256:eb6daebe3cbef5b39a0bb2a97cffbe9c7cb46b2fcc399ad141f369f3c2134b1f", size = 86039, upload-time = "2025-10-28T22:33:19.949Z" }, +] + +[package.optional-dependencies] +boto3 = [ + { name = "boto3" }, +] + +[[package]] +name = "aiofiles" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" }, ] [[package]] name = "aiohappyeyeballs" -version = "2.6.1" +version = "2.7.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/f4/eec0465c2f67b2664688d0240b3212d5196fd89e741df67ddb81f8d35658/aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d", size = 24757, upload-time = "2026-07-01T17:11:55.501Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/1947f06babed6b3f1d7f38b0c767f52df66bfb2bc10b468c4a7de9eceff2/aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472", size = 15038, upload-time = "2026-07-01T17:11:54.055Z" }, ] [[package]] name = "aiohttp" -version = "3.12.15" +version = "3.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -27,95 +78,138 @@ dependencies = [ { name = "frozenlist" }, { name = "multidict" }, { name = "propcache" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9b/e7/d92a237d8802ca88483906c388f7c201bbe96cd80a165ffd0ac2f6a8d59f/aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2", size = 7823716, upload-time = "2025-07-29T05:52:32.215Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/dc/ef9394bde9080128ad401ac7ede185267ed637df03b51f05d14d1c99ad67/aiohttp-3.12.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b6fc902bff74d9b1879ad55f5404153e2b33a82e72a95c89cec5eb6cc9e92fbc", size = 703921, upload-time = "2025-07-29T05:49:43.584Z" }, - { url = "https://files.pythonhosted.org/packages/8f/42/63fccfc3a7ed97eb6e1a71722396f409c46b60a0552d8a56d7aad74e0df5/aiohttp-3.12.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:098e92835b8119b54c693f2f88a1dec690e20798ca5f5fe5f0520245253ee0af", size = 480288, upload-time = "2025-07-29T05:49:47.851Z" }, - { url = "https://files.pythonhosted.org/packages/9c/a2/7b8a020549f66ea2a68129db6960a762d2393248f1994499f8ba9728bbed/aiohttp-3.12.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:40b3fee496a47c3b4a39a731954c06f0bd9bd3e8258c059a4beb76ac23f8e421", size = 468063, upload-time = "2025-07-29T05:49:49.789Z" }, - { url = "https://files.pythonhosted.org/packages/8f/f5/d11e088da9176e2ad8220338ae0000ed5429a15f3c9dfd983f39105399cd/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ce13fcfb0bb2f259fb42106cdc63fa5515fb85b7e87177267d89a771a660b79", size = 1650122, upload-time = "2025-07-29T05:49:51.874Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6b/b60ce2757e2faed3d70ed45dafee48cee7bfb878785a9423f7e883f0639c/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3beb14f053222b391bf9cf92ae82e0171067cc9c8f52453a0f1ec7c37df12a77", size = 1624176, upload-time = "2025-07-29T05:49:53.805Z" }, - { url = "https://files.pythonhosted.org/packages/dd/de/8c9fde2072a1b72c4fadecf4f7d4be7a85b1d9a4ab333d8245694057b4c6/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c39e87afe48aa3e814cac5f535bc6199180a53e38d3f51c5e2530f5aa4ec58c", size = 1696583, upload-time = "2025-07-29T05:49:55.338Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ad/07f863ca3d895a1ad958a54006c6dafb4f9310f8c2fdb5f961b8529029d3/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f1b4ce5bc528a6ee38dbf5f39bbf11dd127048726323b72b8e85769319ffc4", size = 1738896, upload-time = "2025-07-29T05:49:57.045Z" }, - { url = "https://files.pythonhosted.org/packages/20/43/2bd482ebe2b126533e8755a49b128ec4e58f1a3af56879a3abdb7b42c54f/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1004e67962efabbaf3f03b11b4c43b834081c9e3f9b32b16a7d97d4708a9abe6", size = 1643561, upload-time = "2025-07-29T05:49:58.762Z" }, - { url = "https://files.pythonhosted.org/packages/23/40/2fa9f514c4cf4cbae8d7911927f81a1901838baf5e09a8b2c299de1acfe5/aiohttp-3.12.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8faa08fcc2e411f7ab91d1541d9d597d3a90e9004180edb2072238c085eac8c2", size = 1583685, upload-time = "2025-07-29T05:50:00.375Z" }, - { url = "https://files.pythonhosted.org/packages/b8/c3/94dc7357bc421f4fb978ca72a201a6c604ee90148f1181790c129396ceeb/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fe086edf38b2222328cdf89af0dde2439ee173b8ad7cb659b4e4c6f385b2be3d", size = 1627533, upload-time = "2025-07-29T05:50:02.306Z" }, - { url = "https://files.pythonhosted.org/packages/bf/3f/1f8911fe1844a07001e26593b5c255a685318943864b27b4e0267e840f95/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:79b26fe467219add81d5e47b4a4ba0f2394e8b7c7c3198ed36609f9ba161aecb", size = 1638319, upload-time = "2025-07-29T05:50:04.282Z" }, - { url = "https://files.pythonhosted.org/packages/4e/46/27bf57a99168c4e145ffee6b63d0458b9c66e58bb70687c23ad3d2f0bd17/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b761bac1192ef24e16706d761aefcb581438b34b13a2f069a6d343ec8fb693a5", size = 1613776, upload-time = "2025-07-29T05:50:05.863Z" }, - { url = "https://files.pythonhosted.org/packages/0f/7e/1d2d9061a574584bb4ad3dbdba0da90a27fdc795bc227def3a46186a8bc1/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e153e8adacfe2af562861b72f8bc47f8a5c08e010ac94eebbe33dc21d677cd5b", size = 1693359, upload-time = "2025-07-29T05:50:07.563Z" }, - { url = "https://files.pythonhosted.org/packages/08/98/bee429b52233c4a391980a5b3b196b060872a13eadd41c3a34be9b1469ed/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fc49c4de44977aa8601a00edbf157e9a421f227aa7eb477d9e3df48343311065", size = 1716598, upload-time = "2025-07-29T05:50:09.33Z" }, - { url = "https://files.pythonhosted.org/packages/57/39/b0314c1ea774df3392751b686104a3938c63ece2b7ce0ba1ed7c0b4a934f/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2776c7ec89c54a47029940177e75c8c07c29c66f73464784971d6a81904ce9d1", size = 1644940, upload-time = "2025-07-29T05:50:11.334Z" }, - { url = "https://files.pythonhosted.org/packages/1b/83/3dacb8d3f8f512c8ca43e3fa8a68b20583bd25636ffa4e56ee841ffd79ae/aiohttp-3.12.15-cp310-cp310-win32.whl", hash = "sha256:2c7d81a277fa78b2203ab626ced1487420e8c11a8e373707ab72d189fcdad20a", size = 429239, upload-time = "2025-07-29T05:50:12.803Z" }, - { url = "https://files.pythonhosted.org/packages/eb/f9/470b5daba04d558c9673ca2034f28d067f3202a40e17804425f0c331c89f/aiohttp-3.12.15-cp310-cp310-win_amd64.whl", hash = "sha256:83603f881e11f0f710f8e2327817c82e79431ec976448839f3cd05d7afe8f830", size = 452297, upload-time = "2025-07-29T05:50:14.266Z" }, - { url = "https://files.pythonhosted.org/packages/20/19/9e86722ec8e835959bd97ce8c1efa78cf361fa4531fca372551abcc9cdd6/aiohttp-3.12.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d3ce17ce0220383a0f9ea07175eeaa6aa13ae5a41f30bc61d84df17f0e9b1117", size = 711246, upload-time = "2025-07-29T05:50:15.937Z" }, - { url = "https://files.pythonhosted.org/packages/71/f9/0a31fcb1a7d4629ac9d8f01f1cb9242e2f9943f47f5d03215af91c3c1a26/aiohttp-3.12.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:010cc9bbd06db80fe234d9003f67e97a10fe003bfbedb40da7d71c1008eda0fe", size = 483515, upload-time = "2025-07-29T05:50:17.442Z" }, - { url = "https://files.pythonhosted.org/packages/62/6c/94846f576f1d11df0c2e41d3001000527c0fdf63fce7e69b3927a731325d/aiohttp-3.12.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f9d7c55b41ed687b9d7165b17672340187f87a773c98236c987f08c858145a9", size = 471776, upload-time = "2025-07-29T05:50:19.568Z" }, - { url = "https://files.pythonhosted.org/packages/f8/6c/f766d0aaafcee0447fad0328da780d344489c042e25cd58fde566bf40aed/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc4fbc61bb3548d3b482f9ac7ddd0f18c67e4225aaa4e8552b9f1ac7e6bda9e5", size = 1741977, upload-time = "2025-07-29T05:50:21.665Z" }, - { url = "https://files.pythonhosted.org/packages/17/e5/fb779a05ba6ff44d7bc1e9d24c644e876bfff5abe5454f7b854cace1b9cc/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7fbc8a7c410bb3ad5d595bb7118147dfbb6449d862cc1125cf8867cb337e8728", size = 1690645, upload-time = "2025-07-29T05:50:23.333Z" }, - { url = "https://files.pythonhosted.org/packages/37/4e/a22e799c2035f5d6a4ad2cf8e7c1d1bd0923192871dd6e367dafb158b14c/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74dad41b3458dbb0511e760fb355bb0b6689e0630de8a22b1b62a98777136e16", size = 1789437, upload-time = "2025-07-29T05:50:25.007Z" }, - { url = "https://files.pythonhosted.org/packages/28/e5/55a33b991f6433569babb56018b2fb8fb9146424f8b3a0c8ecca80556762/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b6f0af863cf17e6222b1735a756d664159e58855da99cfe965134a3ff63b0b0", size = 1828482, upload-time = "2025-07-29T05:50:26.693Z" }, - { url = "https://files.pythonhosted.org/packages/c6/82/1ddf0ea4f2f3afe79dffed5e8a246737cff6cbe781887a6a170299e33204/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5b7fe4972d48a4da367043b8e023fb70a04d1490aa7d68800e465d1b97e493b", size = 1730944, upload-time = "2025-07-29T05:50:28.382Z" }, - { url = "https://files.pythonhosted.org/packages/1b/96/784c785674117b4cb3877522a177ba1b5e4db9ce0fd519430b5de76eec90/aiohttp-3.12.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6443cca89553b7a5485331bc9bedb2342b08d073fa10b8c7d1c60579c4a7b9bd", size = 1668020, upload-time = "2025-07-29T05:50:30.032Z" }, - { url = "https://files.pythonhosted.org/packages/12/8a/8b75f203ea7e5c21c0920d84dd24a5c0e971fe1e9b9ebbf29ae7e8e39790/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c5f40ec615e5264f44b4282ee27628cea221fcad52f27405b80abb346d9f3f8", size = 1716292, upload-time = "2025-07-29T05:50:31.983Z" }, - { url = "https://files.pythonhosted.org/packages/47/0b/a1451543475bb6b86a5cfc27861e52b14085ae232896a2654ff1231c0992/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2abbb216a1d3a2fe86dbd2edce20cdc5e9ad0be6378455b05ec7f77361b3ab50", size = 1711451, upload-time = "2025-07-29T05:50:33.989Z" }, - { url = "https://files.pythonhosted.org/packages/55/fd/793a23a197cc2f0d29188805cfc93aa613407f07e5f9da5cd1366afd9d7c/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:db71ce547012a5420a39c1b744d485cfb823564d01d5d20805977f5ea1345676", size = 1691634, upload-time = "2025-07-29T05:50:35.846Z" }, - { url = "https://files.pythonhosted.org/packages/ca/bf/23a335a6670b5f5dfc6d268328e55a22651b440fca341a64fccf1eada0c6/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ced339d7c9b5030abad5854aa5413a77565e5b6e6248ff927d3e174baf3badf7", size = 1785238, upload-time = "2025-07-29T05:50:37.597Z" }, - { url = "https://files.pythonhosted.org/packages/57/4f/ed60a591839a9d85d40694aba5cef86dde9ee51ce6cca0bb30d6eb1581e7/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:7c7dd29c7b5bda137464dc9bfc738d7ceea46ff70309859ffde8c022e9b08ba7", size = 1805701, upload-time = "2025-07-29T05:50:39.591Z" }, - { url = "https://files.pythonhosted.org/packages/85/e0/444747a9455c5de188c0f4a0173ee701e2e325d4b2550e9af84abb20cdba/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:421da6fd326460517873274875c6c5a18ff225b40da2616083c5a34a7570b685", size = 1718758, upload-time = "2025-07-29T05:50:41.292Z" }, - { url = "https://files.pythonhosted.org/packages/36/ab/1006278d1ffd13a698e5dd4bfa01e5878f6bddefc296c8b62649753ff249/aiohttp-3.12.15-cp311-cp311-win32.whl", hash = "sha256:4420cf9d179ec8dfe4be10e7d0fe47d6d606485512ea2265b0d8c5113372771b", size = 428868, upload-time = "2025-07-29T05:50:43.063Z" }, - { url = "https://files.pythonhosted.org/packages/10/97/ad2b18700708452400278039272032170246a1bf8ec5d832772372c71f1a/aiohttp-3.12.15-cp311-cp311-win_amd64.whl", hash = "sha256:edd533a07da85baa4b423ee8839e3e91681c7bfa19b04260a469ee94b778bf6d", size = 453273, upload-time = "2025-07-29T05:50:44.613Z" }, - { url = "https://files.pythonhosted.org/packages/63/97/77cb2450d9b35f517d6cf506256bf4f5bda3f93a66b4ad64ba7fc917899c/aiohttp-3.12.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:802d3868f5776e28f7bf69d349c26fc0efadb81676d0afa88ed00d98a26340b7", size = 702333, upload-time = "2025-07-29T05:50:46.507Z" }, - { url = "https://files.pythonhosted.org/packages/83/6d/0544e6b08b748682c30b9f65640d006e51f90763b41d7c546693bc22900d/aiohttp-3.12.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2800614cd560287be05e33a679638e586a2d7401f4ddf99e304d98878c29444", size = 476948, upload-time = "2025-07-29T05:50:48.067Z" }, - { url = "https://files.pythonhosted.org/packages/3a/1d/c8c40e611e5094330284b1aea8a4b02ca0858f8458614fa35754cab42b9c/aiohttp-3.12.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8466151554b593909d30a0a125d638b4e5f3836e5aecde85b66b80ded1cb5b0d", size = 469787, upload-time = "2025-07-29T05:50:49.669Z" }, - { url = "https://files.pythonhosted.org/packages/38/7d/b76438e70319796bfff717f325d97ce2e9310f752a267bfdf5192ac6082b/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e5a495cb1be69dae4b08f35a6c4579c539e9b5706f606632102c0f855bcba7c", size = 1716590, upload-time = "2025-07-29T05:50:51.368Z" }, - { url = "https://files.pythonhosted.org/packages/79/b1/60370d70cdf8b269ee1444b390cbd72ce514f0d1cd1a715821c784d272c9/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6404dfc8cdde35c69aaa489bb3542fb86ef215fc70277c892be8af540e5e21c0", size = 1699241, upload-time = "2025-07-29T05:50:53.628Z" }, - { url = "https://files.pythonhosted.org/packages/a3/2b/4968a7b8792437ebc12186db31523f541943e99bda8f30335c482bea6879/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ead1c00f8521a5c9070fcb88f02967b1d8a0544e6d85c253f6968b785e1a2ab", size = 1754335, upload-time = "2025-07-29T05:50:55.394Z" }, - { url = "https://files.pythonhosted.org/packages/fb/c1/49524ed553f9a0bec1a11fac09e790f49ff669bcd14164f9fab608831c4d/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6990ef617f14450bc6b34941dba4f12d5613cbf4e33805932f853fbd1cf18bfb", size = 1800491, upload-time = "2025-07-29T05:50:57.202Z" }, - { url = "https://files.pythonhosted.org/packages/de/5e/3bf5acea47a96a28c121b167f5ef659cf71208b19e52a88cdfa5c37f1fcc/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd736ed420f4db2b8148b52b46b88ed038d0354255f9a73196b7bbce3ea97545", size = 1719929, upload-time = "2025-07-29T05:50:59.192Z" }, - { url = "https://files.pythonhosted.org/packages/39/94/8ae30b806835bcd1cba799ba35347dee6961a11bd507db634516210e91d8/aiohttp-3.12.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c5092ce14361a73086b90c6efb3948ffa5be2f5b6fbcf52e8d8c8b8848bb97c", size = 1635733, upload-time = "2025-07-29T05:51:01.394Z" }, - { url = "https://files.pythonhosted.org/packages/7a/46/06cdef71dd03acd9da7f51ab3a9107318aee12ad38d273f654e4f981583a/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aaa2234bb60c4dbf82893e934d8ee8dea30446f0647e024074237a56a08c01bd", size = 1696790, upload-time = "2025-07-29T05:51:03.657Z" }, - { url = "https://files.pythonhosted.org/packages/02/90/6b4cfaaf92ed98d0ec4d173e78b99b4b1a7551250be8937d9d67ecb356b4/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6d86a2fbdd14192e2f234a92d3b494dd4457e683ba07e5905a0b3ee25389ac9f", size = 1718245, upload-time = "2025-07-29T05:51:05.911Z" }, - { url = "https://files.pythonhosted.org/packages/2e/e6/2593751670fa06f080a846f37f112cbe6f873ba510d070136a6ed46117c6/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a041e7e2612041a6ddf1c6a33b883be6a421247c7afd47e885969ee4cc58bd8d", size = 1658899, upload-time = "2025-07-29T05:51:07.753Z" }, - { url = "https://files.pythonhosted.org/packages/8f/28/c15bacbdb8b8eb5bf39b10680d129ea7410b859e379b03190f02fa104ffd/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5015082477abeafad7203757ae44299a610e89ee82a1503e3d4184e6bafdd519", size = 1738459, upload-time = "2025-07-29T05:51:09.56Z" }, - { url = "https://files.pythonhosted.org/packages/00/de/c269cbc4faa01fb10f143b1670633a8ddd5b2e1ffd0548f7aa49cb5c70e2/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56822ff5ddfd1b745534e658faba944012346184fbfe732e0d6134b744516eea", size = 1766434, upload-time = "2025-07-29T05:51:11.423Z" }, - { url = "https://files.pythonhosted.org/packages/52/b0/4ff3abd81aa7d929b27d2e1403722a65fc87b763e3a97b3a2a494bfc63bc/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2acbbfff69019d9014508c4ba0401822e8bae5a5fdc3b6814285b71231b60f3", size = 1726045, upload-time = "2025-07-29T05:51:13.689Z" }, - { url = "https://files.pythonhosted.org/packages/71/16/949225a6a2dd6efcbd855fbd90cf476052e648fb011aa538e3b15b89a57a/aiohttp-3.12.15-cp312-cp312-win32.whl", hash = "sha256:d849b0901b50f2185874b9a232f38e26b9b3d4810095a7572eacea939132d4e1", size = 423591, upload-time = "2025-07-29T05:51:15.452Z" }, - { url = "https://files.pythonhosted.org/packages/2b/d8/fa65d2a349fe938b76d309db1a56a75c4fb8cc7b17a398b698488a939903/aiohttp-3.12.15-cp312-cp312-win_amd64.whl", hash = "sha256:b390ef5f62bb508a9d67cb3bba9b8356e23b3996da7062f1a57ce1a79d2b3d34", size = 450266, upload-time = "2025-07-29T05:51:17.239Z" }, - { url = "https://files.pythonhosted.org/packages/f2/33/918091abcf102e39d15aba2476ad9e7bd35ddb190dcdd43a854000d3da0d/aiohttp-3.12.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9f922ffd05034d439dde1c77a20461cf4a1b0831e6caa26151fe7aa8aaebc315", size = 696741, upload-time = "2025-07-29T05:51:19.021Z" }, - { url = "https://files.pythonhosted.org/packages/b5/2a/7495a81e39a998e400f3ecdd44a62107254803d1681d9189be5c2e4530cd/aiohttp-3.12.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ee8a8ac39ce45f3e55663891d4b1d15598c157b4d494a4613e704c8b43112cd", size = 474407, upload-time = "2025-07-29T05:51:21.165Z" }, - { url = "https://files.pythonhosted.org/packages/49/fc/a9576ab4be2dcbd0f73ee8675d16c707cfc12d5ee80ccf4015ba543480c9/aiohttp-3.12.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3eae49032c29d356b94eee45a3f39fdf4b0814b397638c2f718e96cfadf4c4e4", size = 466703, upload-time = "2025-07-29T05:51:22.948Z" }, - { url = "https://files.pythonhosted.org/packages/09/2f/d4bcc8448cf536b2b54eed48f19682031ad182faa3a3fee54ebe5b156387/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97752ff12cc12f46a9b20327104448042fce5c33a624f88c18f66f9368091c7", size = 1705532, upload-time = "2025-07-29T05:51:25.211Z" }, - { url = "https://files.pythonhosted.org/packages/f1/f3/59406396083f8b489261e3c011aa8aee9df360a96ac8fa5c2e7e1b8f0466/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:894261472691d6fe76ebb7fcf2e5870a2ac284c7406ddc95823c8598a1390f0d", size = 1686794, upload-time = "2025-07-29T05:51:27.145Z" }, - { url = "https://files.pythonhosted.org/packages/dc/71/164d194993a8d114ee5656c3b7ae9c12ceee7040d076bf7b32fb98a8c5c6/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fa5d9eb82ce98959fc1031c28198b431b4d9396894f385cb63f1e2f3f20ca6b", size = 1738865, upload-time = "2025-07-29T05:51:29.366Z" }, - { url = "https://files.pythonhosted.org/packages/1c/00/d198461b699188a93ead39cb458554d9f0f69879b95078dce416d3209b54/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0fa751efb11a541f57db59c1dd821bec09031e01452b2b6217319b3a1f34f3d", size = 1788238, upload-time = "2025-07-29T05:51:31.285Z" }, - { url = "https://files.pythonhosted.org/packages/85/b8/9e7175e1fa0ac8e56baa83bf3c214823ce250d0028955dfb23f43d5e61fd/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5346b93e62ab51ee2a9d68e8f73c7cf96ffb73568a23e683f931e52450e4148d", size = 1710566, upload-time = "2025-07-29T05:51:33.219Z" }, - { url = "https://files.pythonhosted.org/packages/59/e4/16a8eac9df39b48ae102ec030fa9f726d3570732e46ba0c592aeeb507b93/aiohttp-3.12.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:049ec0360f939cd164ecbfd2873eaa432613d5e77d6b04535e3d1fbae5a9e645", size = 1624270, upload-time = "2025-07-29T05:51:35.195Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f8/cd84dee7b6ace0740908fd0af170f9fab50c2a41ccbc3806aabcb1050141/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b52dcf013b57464b6d1e51b627adfd69a8053e84b7103a7cd49c030f9ca44461", size = 1677294, upload-time = "2025-07-29T05:51:37.215Z" }, - { url = "https://files.pythonhosted.org/packages/ce/42/d0f1f85e50d401eccd12bf85c46ba84f947a84839c8a1c2c5f6e8ab1eb50/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b2af240143dd2765e0fb661fd0361a1b469cab235039ea57663cda087250ea9", size = 1708958, upload-time = "2025-07-29T05:51:39.328Z" }, - { url = "https://files.pythonhosted.org/packages/d5/6b/f6fa6c5790fb602538483aa5a1b86fcbad66244997e5230d88f9412ef24c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac77f709a2cde2cc71257ab2d8c74dd157c67a0558a0d2799d5d571b4c63d44d", size = 1651553, upload-time = "2025-07-29T05:51:41.356Z" }, - { url = "https://files.pythonhosted.org/packages/04/36/a6d36ad545fa12e61d11d1932eef273928b0495e6a576eb2af04297fdd3c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:47f6b962246f0a774fbd3b6b7be25d59b06fdb2f164cf2513097998fc6a29693", size = 1727688, upload-time = "2025-07-29T05:51:43.452Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c8/f195e5e06608a97a4e52c5d41c7927301bf757a8e8bb5bbf8cef6c314961/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:760fb7db442f284996e39cf9915a94492e1896baac44f06ae551974907922b64", size = 1761157, upload-time = "2025-07-29T05:51:45.643Z" }, - { url = "https://files.pythonhosted.org/packages/05/6a/ea199e61b67f25ba688d3ce93f63b49b0a4e3b3d380f03971b4646412fc6/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad702e57dc385cae679c39d318def49aef754455f237499d5b99bea4ef582e51", size = 1710050, upload-time = "2025-07-29T05:51:48.203Z" }, - { url = "https://files.pythonhosted.org/packages/b4/2e/ffeb7f6256b33635c29dbed29a22a723ff2dd7401fff42ea60cf2060abfb/aiohttp-3.12.15-cp313-cp313-win32.whl", hash = "sha256:f813c3e9032331024de2eb2e32a88d86afb69291fbc37a3a3ae81cc9917fb3d0", size = 422647, upload-time = "2025-07-29T05:51:50.718Z" }, - { url = "https://files.pythonhosted.org/packages/1b/8e/78ee35774201f38d5e1ba079c9958f7629b1fd079459aea9467441dbfbf5/aiohttp-3.12.15-cp313-cp313-win_amd64.whl", hash = "sha256:1a649001580bdb37c6fdb1bebbd7e3bc688e8ec2b5c6f52edbb664662b17dc84", size = 449067, upload-time = "2025-07-29T05:51:52.549Z" }, - { url = "https://files.pythonhosted.org/packages/18/8d/da08099af8db234d1cd43163e6ffc8e9313d0e988cee1901610f2fa5c764/aiohttp-3.12.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:691d203c2bdf4f4637792efbbcdcd157ae11e55eaeb5e9c360c1206fb03d4d98", size = 706829, upload-time = "2025-07-29T05:51:54.434Z" }, - { url = "https://files.pythonhosted.org/packages/4e/94/8eed385cfb60cf4fdb5b8a165f6148f3bebeb365f08663d83c35a5f273ef/aiohttp-3.12.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e995e1abc4ed2a454c731385bf4082be06f875822adc4c6d9eaadf96e20d406", size = 481806, upload-time = "2025-07-29T05:51:56.355Z" }, - { url = "https://files.pythonhosted.org/packages/38/68/b13e1a34584fbf263151b3a72a084e89f2102afe38df1dce5a05a15b83e9/aiohttp-3.12.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bd44d5936ab3193c617bfd6c9a7d8d1085a8dc8c3f44d5f1dcf554d17d04cf7d", size = 469205, upload-time = "2025-07-29T05:51:58.277Z" }, - { url = "https://files.pythonhosted.org/packages/38/14/3d7348bf53aa4af54416bc64cbef3a2ac5e8b9bfa97cc45f1cf9a94d9c8d/aiohttp-3.12.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46749be6e89cd78d6068cdf7da51dbcfa4321147ab8e4116ee6678d9a056a0cf", size = 1644174, upload-time = "2025-07-29T05:52:00.23Z" }, - { url = "https://files.pythonhosted.org/packages/ba/ed/fd9b5b22b0f6ca1a85c33bb4868cbcc6ae5eae070a0f4c9c5cad003c89d7/aiohttp-3.12.15-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0c643f4d75adea39e92c0f01b3fb83d57abdec8c9279b3078b68a3a52b3933b6", size = 1618672, upload-time = "2025-07-29T05:52:02.272Z" }, - { url = "https://files.pythonhosted.org/packages/39/f7/f6530ab5f8c8c409e44a63fcad35e839c87aabecdfe5b8e96d671ed12f64/aiohttp-3.12.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a23918fedc05806966a2438489dcffccbdf83e921a1170773b6178d04ade142", size = 1692295, upload-time = "2025-07-29T05:52:04.546Z" }, - { url = "https://files.pythonhosted.org/packages/cb/dc/3cf483bb0106566dc97ebaa2bb097f5e44d4bc4ab650a6f107151cd7b193/aiohttp-3.12.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:74bdd8c864b36c3673741023343565d95bfbd778ffe1eb4d412c135a28a8dc89", size = 1731609, upload-time = "2025-07-29T05:52:06.552Z" }, - { url = "https://files.pythonhosted.org/packages/de/a4/fd04bf807851197077d9cac9381d58f86d91c95c06cbaf9d3a776ac4467a/aiohttp-3.12.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a146708808c9b7a988a4af3821379e379e0f0e5e466ca31a73dbdd0325b0263", size = 1637852, upload-time = "2025-07-29T05:52:08.975Z" }, - { url = "https://files.pythonhosted.org/packages/98/03/29d626ca3bcdcafbd74b45d77ca42645a5c94d396f2ee3446880ad2405fb/aiohttp-3.12.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7011a70b56facde58d6d26da4fec3280cc8e2a78c714c96b7a01a87930a9530", size = 1572852, upload-time = "2025-07-29T05:52:11.508Z" }, - { url = "https://files.pythonhosted.org/packages/5f/cd/b4777a9e204f4e01091091027e5d1e2fa86decd0fee5067bc168e4fa1e76/aiohttp-3.12.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3bdd6e17e16e1dbd3db74d7f989e8af29c4d2e025f9828e6ef45fbdee158ec75", size = 1620813, upload-time = "2025-07-29T05:52:13.891Z" }, - { url = "https://files.pythonhosted.org/packages/ae/26/1a44a6e8417e84057beaf8c462529b9e05d4b53b8605784f1eb571f0ff68/aiohttp-3.12.15-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:57d16590a351dfc914670bd72530fd78344b885a00b250e992faea565b7fdc05", size = 1630951, upload-time = "2025-07-29T05:52:15.955Z" }, - { url = "https://files.pythonhosted.org/packages/dd/7f/10c605dbd01c40e2b27df7ef9004bec75d156f0705141e11047ecdfe264d/aiohttp-3.12.15-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:bc9a0f6569ff990e0bbd75506c8d8fe7214c8f6579cca32f0546e54372a3bb54", size = 1607595, upload-time = "2025-07-29T05:52:18.089Z" }, - { url = "https://files.pythonhosted.org/packages/66/f6/2560dcb01731c1d7df1d34b64de95bc4b3ed02bb78830fd82299c1eb314e/aiohttp-3.12.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:536ad7234747a37e50e7b6794ea868833d5220b49c92806ae2d7e8a9d6b5de02", size = 1695194, upload-time = "2025-07-29T05:52:20.255Z" }, - { url = "https://files.pythonhosted.org/packages/e7/02/ee105ae82dc2b981039fd25b0cf6eaa52b493731960f9bc861375a72b463/aiohttp-3.12.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f0adb4177fa748072546fb650d9bd7398caaf0e15b370ed3317280b13f4083b0", size = 1710872, upload-time = "2025-07-29T05:52:22.769Z" }, - { url = "https://files.pythonhosted.org/packages/88/16/70c4e42ed6a04f78fb58d1a46500a6ce560741d13afde2a5f33840746a5f/aiohttp-3.12.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:14954a2988feae3987f1eb49c706bff39947605f4b6fa4027c1d75743723eb09", size = 1640539, upload-time = "2025-07-29T05:52:25.733Z" }, - { url = "https://files.pythonhosted.org/packages/fe/1d/a7eb5fa8a6967117c5c0ad5ab4b1dec0d21e178c89aa08bc442a0b836392/aiohttp-3.12.15-cp39-cp39-win32.whl", hash = "sha256:b784d6ed757f27574dca1c336f968f4e81130b27595e458e69457e6878251f5d", size = 430164, upload-time = "2025-07-29T05:52:27.905Z" }, - { url = "https://files.pythonhosted.org/packages/14/25/e0cf8793aedc41c6d7f2aad646a27e27bdacafe3b402bb373d7651c94d73/aiohttp-3.12.15-cp39-cp39-win_amd64.whl", hash = "sha256:86ceded4e78a992f835209e236617bffae649371c4a50d5e5a3987f237db84b8", size = 453370, upload-time = "2025-07-29T05:52:29.936Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/82/78/8ea7308cac6934de8c74a14f3d5f65d1c89287426688be79538d0e5c013d/aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035", size = 7955794, upload-time = "2026-06-07T21:09:35.529Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/67/58ded4b3f2e10f94972d8928050c85330e249a31dd45a0e5f3c0e9c3fa05/aiohttp-3.14.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8f6bb621e5863cfe8fe5ff5468002d200ec31f30f1280b259dc505b02595099e", size = 766140, upload-time = "2026-06-07T21:05:37.471Z" }, + { url = "https://files.pythonhosted.org/packages/18/68/4ae5b4e08943f316594bb68da89957d3baf5760588fa09509594bd777e4b/aiohttp-3.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f7215cb3933784f79ed20e5f050e15984f390424339b22375d5a53c933a0491", size = 519430, upload-time = "2026-06-07T21:05:40.751Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c1/316c8f3549dbe5245f92bfd523ec6f32dd4d98cafe21df3f6a19b1184c75/aiohttp-3.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9d4e294455b23a68c9b8f042d0e8e377a265bcb15332753695f6e5b6819e0ce", size = 514406, upload-time = "2026-06-07T21:05:42.111Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ee/fb0ac28684e8d753b83c8a4eebc19a5846912aa0a4daaabb6a9936363840/aiohttp-3.14.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b238af795833d5731d049d82bc84b768ae6f8f97f0495963b3ed9935c5901cc3", size = 1703649, upload-time = "2026-06-07T21:05:43.427Z" }, + { url = "https://files.pythonhosted.org/packages/3b/57/aa2beab673331f111885db8a7b69dfe3ab0e53e446a0ace18ca694b4dc58/aiohttp-3.14.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4e5e0ae56914ecdbf446493addefc0159053dd53962cef37d7839f37f73d505", size = 1675126, upload-time = "2026-06-07T21:05:44.897Z" }, + { url = "https://files.pythonhosted.org/packages/47/ea/dad128abe365e79be03b16ed464198ac73e0d257e8260c6f7d6f31cbef26/aiohttp-3.14.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:092e4ce3619a7c6dee52a6bdabda973d9b34b66781f840ce93c7e0cec30cf521", size = 1771558, upload-time = "2026-06-07T21:05:46.405Z" }, + { url = "https://files.pythonhosted.org/packages/63/f3/b5b4e10327cb85d34d24232c6b71b64602f190b3ccb238a043ac6b187dac/aiohttp-3.14.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb33777ea21e8b7ecde0e6fc84f598be0a1192eab1a63bc746d75aa75d38e7bd", size = 1856631, upload-time = "2026-06-07T21:05:47.844Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9d/93294c3045775c708ac8310eb3d3622a11d2951345ad590d532d62a1faa4/aiohttp-3.14.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23119f8fd4f5d16902ed459b63b100bcd269628075162bddac56cc7b5273b3fb", size = 1714139, upload-time = "2026-06-07T21:05:49.982Z" }, + { url = "https://files.pythonhosted.org/packages/29/c4/93067c85a0373492ce8e577435203c5947c454af074ac48ed4f3a1b9dd4a/aiohttp-3.14.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:57fc6745a4b7d0f5a9eb4f40a69718be6c0bc1b8368cc9fe89e90118719f4f42", size = 1588321, upload-time = "2026-06-07T21:05:51.431Z" }, + { url = "https://files.pythonhosted.org/packages/c4/39/9ff91aaf02af8b7b8222a987466da539f154c3e01732c22b5f5a20a8ee66/aiohttp-3.14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6fd35beba67c4183b09375c5fff9accb47524191a244a99f95fd4472f5402c2b", size = 1670375, upload-time = "2026-06-07T21:05:53.109Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e4/77452a3676b8d99ac1375f77691d6bf65ea6e9f4b201b82ef77c916dc767/aiohttp-3.14.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:672b9d65f42eb877f5c3f234a4547e4e1a226ca8c2eed879bb34670a0ce51192", size = 1690933, upload-time = "2026-06-07T21:05:54.902Z" }, + { url = "https://files.pythonhosted.org/packages/7d/84/b0059a7c7fc05ea23f3bc1596ba91c12f79588b9450564a24cac37536d0a/aiohttp-3.14.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:24ba13339fed9251d9b1a1bec8c7ab84c0d1675d79d33501e11f94f8b9a84e05", size = 1740798, upload-time = "2026-06-07T21:05:56.458Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3a/e2a513ecbfc362591caa51a7f7e011b3bfc8938b388ae44cd95560d36999/aiohttp-3.14.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:94da27378da0610e341c4d30de29a191672683cc82b8f9556e8f7c7212a020fe", size = 1576412, upload-time = "2026-06-07T21:05:57.953Z" }, + { url = "https://files.pythonhosted.org/packages/a1/10/08f1654f538f93d36dcac66310a06eefce4641cdafca83f9f0a5317be254/aiohttp-3.14.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:52cdac9432d8b4a719f35094a818d95adcae0f0b4fe9b9b921909e0c87de9e7d", size = 1750199, upload-time = "2026-06-07T21:05:59.488Z" }, + { url = "https://files.pythonhosted.org/packages/99/e4/d91b70c57d8b8e9611e4a2e52238ca3698d3dc1c2efe25b7a9bf594ac584/aiohttp-3.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:672ac254412a24d0d0cf00a9e6c238877e4be5e5fa2d188832c1244f45f31966", size = 1699356, upload-time = "2026-06-07T21:06:01.131Z" }, + { url = "https://files.pythonhosted.org/packages/3d/f1/15340176f35ff61b95dbe34020bcf43f9e624a2d7bbac934715ff97d2033/aiohttp-3.14.1-cp310-cp310-win32.whl", hash = "sha256:2fe3607e71acc6ebb0ec8e492a247bf7a291226192dc0084236dfc12478916f6", size = 458939, upload-time = "2026-06-07T21:06:02.86Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c2/a2f1ec5b37f903109e43ae2862268cfe4a67a60c1b2cf43169fcdff5995f/aiohttp-3.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:30099eda75a53c32efb0920e9c33c195314d2cc1c680fbfd30894932ac5f27df", size = 482583, upload-time = "2026-06-07T21:06:04.666Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7a/7b56f6732ef79530afaa72aa335d41b67c8d79b946995f0b11ad72985435/aiohttp-3.14.1-cp310-cp310-win_arm64.whl", hash = "sha256:5a837f49d901f9e368651b676912bff1104ed8c1a83b280bcd7b29adccef5c9c", size = 453470, upload-time = "2026-06-07T21:06:06.322Z" }, + { url = "https://files.pythonhosted.org/packages/26/dd/bf526e6f0a1120dd6f2df2e97bacfe4d358f13d17a0ff5847301a1375a51/aiohttp-3.14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa00140699487bd435fde4342d85c94cb256b7cd3a5b9c3396c67f19922afda2", size = 765225, upload-time = "2026-06-07T21:06:07.957Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e1/a2872aa55495a70f61310d411541c6ee23812d9a884e000c716e1bc3edbf/aiohttp-3.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c1af67559445498b502030c35c59db59966f47041ca9de5b4e707f86bd10b5f", size = 518743, upload-time = "2026-06-07T21:06:09.749Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e7/c60c7b209e509cc787de3cea0550a518538cfc08003e1c1e14c1c63fff71/aiohttp-3.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d44ec478e713ee7f29b439f7eb8dc2b9d4079e11ae114d2c2ac3d5daf30516c8", size = 514139, upload-time = "2026-06-07T21:06:11.26Z" }, + { url = "https://files.pythonhosted.org/packages/5b/8d/614ace2f579702c9840ab1e1447fd8509e35b0b904f7196418fa2f57b25d/aiohttp-3.14.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3b1a184a9a8f548a6b73f1e26b96b052193e4b3175ed7342aaf1151a1f00a04", size = 1784088, upload-time = "2026-06-07T21:06:12.887Z" }, + { url = "https://files.pythonhosted.org/packages/49/e0/726e90f99542bf292f81a96a12cc4847deb86f3ccf62c6f4014a201f4d33/aiohttp-3.14.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5f2504bc0322437c9a1ff6d3333ca56c7477b727c995f036b976ae17b98372c8", size = 1737835, upload-time = "2026-06-07T21:06:14.564Z" }, + { url = "https://files.pythonhosted.org/packages/0b/4b/d176d5c4db9d33dacf0543102ea59503bc1d528af4cfd0b719949ca49389/aiohttp-3.14.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73f05ea02013e02512c3bf42714f1208c57168c779cc6fe23516e4543089d0a6", size = 1842801, upload-time = "2026-06-07T21:06:16.228Z" }, + { url = "https://files.pythonhosted.org/packages/dc/d6/5a99b563690ea0cbed912ae94a2ce33993a5709a651a3a4fe761e7dd973a/aiohttp-3.14.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:797457503c2d426bee06eef808d07b31ede30b65e054444e7de64cad0061b7af", size = 1929992, upload-time = "2026-06-07T21:06:17.947Z" }, + { url = "https://files.pythonhosted.org/packages/76/7f/a987b14a3859094b3cea3f4825219c3e5536242564af6e3f9c2f6c994eb2/aiohttp-3.14.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b821a1f7dedf7e37450654e620038ac3b2e81e8fa6ea269337e97101978ec730", size = 1786989, upload-time = "2026-06-07T21:06:19.677Z" }, + { url = "https://files.pythonhosted.org/packages/f1/1a/420e5c85a3e73349372ed22ce0b6af86bfa6ce16a4b20a64a2e94608c781/aiohttp-3.14.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4cd96b5ba05d67ed0cf00b5b405c8cd99586d8e3481e8ee0a831057591af7621", size = 1640129, upload-time = "2026-06-07T21:06:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/a7/80/18a592ed3be0a402cc03670bd72ee1f8563ddbe1d8d5542dbf868f274136/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d459b98a932296c6f0e94f87511a0b1b90a8a02c30a50e60a297619cd5a58ee", size = 1756576, upload-time = "2026-06-07T21:06:24.8Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0b/8b3d5713373858ff71a617daf6e3b0e81ad63e79d09a3cf2f6b6b983939c/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:764457a7be60825fb770a644852ff717bcbb5042f189f2bd16df61a81b3f6573", size = 1754668, upload-time = "2026-06-07T21:06:26.528Z" }, + { url = "https://files.pythonhosted.org/packages/9f/49/fd564575cf225821d7ba5a117cb8bc27213d8a7e1811162afb43ae077039/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f7a16ef45b081454ef844502d87a848876c490c4cb5c650c230f6ec79ed2c1e7", size = 1817019, upload-time = "2026-06-07T21:06:28.297Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/e850c9ae6fc91356552ae668bb6c51e93fa29c8aef13398a10b56678557f/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2fbc3ed048b3475b9f0cbcb9978e9d2d3511acd91ead203af26ed9f0056004cf", size = 1631638, upload-time = "2026-06-07T21:06:30.242Z" }, + { url = "https://files.pythonhosted.org/packages/eb/94/3c337ba72451a89806ace6f75bddc92bafc5b8d53d90115a512858024b63/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bedb0cd073cc2dc035e30aeb99444389d3cd2113afe4ef9fcd23d439f5bade85", size = 1835660, upload-time = "2026-06-07T21:06:31.943Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9c/9c18cf367a0498212d9ba7daf990b504a5e8ae064cda4b504e2647c89c03/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b6feea921016eb3d4e04d65fc4e9ca402d1a3801f562aef94989f54694917af3", size = 1775698, upload-time = "2026-06-07T21:06:33.72Z" }, + { url = "https://files.pythonhosted.org/packages/b5/63/a251a9d2a6cb45065b2ddc0bde2b3dd10108740a9a42f632c66405a761a2/aiohttp-3.14.1-cp311-cp311-win32.whl", hash = "sha256:313701e488100074ce99850404ee36e741abf6330179fec908a1944ecf570126", size = 458386, upload-time = "2026-06-07T21:06:35.279Z" }, + { url = "https://files.pythonhosted.org/packages/17/ca/69274c51dcd6e8947d77b2806cf47a4a15f2c846e2cbeb1882547d3da283/aiohttp-3.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:03ab4530fdcb3a543a122ba4b65ac9919da9fe9f78a03d328a6e38ff962f7aa5", size = 483406, upload-time = "2026-06-07T21:06:36.824Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8a/c25904f77690c3688ec140f87591ef11a0cfe36bf3d5c0f1f38056fb62b3/aiohttp-3.14.1-cp311-cp311-win_arm64.whl", hash = "sha256:486f7d16ed54c39c2cbd7ca71fd8ba2b8bb7860df65bd7b6ed640bab96a38a8b", size = 452987, upload-time = "2026-06-07T21:06:38.371Z" }, + { url = "https://files.pythonhosted.org/packages/1d/21/151624b51cd92553d95424daf4bf19f19ce9be9002d19253e7e7ce67197b/aiohttp-3.14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d35143e27778b4bb0fb189562d7f275bff79c62ab8e98459717c0ea617ff2480", size = 757402, upload-time = "2026-06-07T21:06:40.311Z" }, + { url = "https://files.pythonhosted.org/packages/c2/82/280619e0bd7bf2454987e19282616e84762255dd9c8468f62382e8c191f1/aiohttp-3.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bcfb80a2cc36fba2534e5e5b5264dc7ae6fcd9bf15256da3e53d2f499e6fa29d", size = 512310, upload-time = "2026-06-07T21:06:42.207Z" }, + { url = "https://files.pythonhosted.org/packages/55/b2/2aac325583aaa1353045f96dffa586d8a34e8322e14a7ba49cffeb103ab4/aiohttp-3.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27fd7c91e51729b4f7e1577865fa6d34c9adccbc39aabe9000285b48af9f0ec2", size = 512448, upload-time = "2026-06-07T21:06:43.813Z" }, + { url = "https://files.pythonhosted.org/packages/8a/72/a60607cb849faa8af8a356c9329ea2eb6f395d49e82cc82ccba1fd8deb8f/aiohttp-3.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c567bf9eaf664280116a8688f63016e6b32db2505908e2bdaca1b6438142f2", size = 1766854, upload-time = "2026-06-07T21:06:45.391Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d3/d9fe1c9ec7557ab4d0d82bebaa728c6418f0b93295ec2f4ab015f7710cc7/aiohttp-3.14.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f5e6ff2bdbb8f4cd3fbe41f99e25bbcd58e3bf9f13d3dd31a11e7917251cc77a", size = 1740884, upload-time = "2026-06-07T21:06:47.413Z" }, + { url = "https://files.pythonhosted.org/packages/c1/dc/f2cecfaf9337ba3e63f181500814ff502aa3d00d9c7ec93a9d23d10a27b2/aiohttp-3.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f73e01dc37122325caf079982621262f96d74823c179038a82fddfc50359264", size = 1810034, upload-time = "2026-06-07T21:06:50.165Z" }, + { url = "https://files.pythonhosted.org/packages/66/d7/2ff65c5e65c0d7476daf7e15c032e0805e36811185b9623e3238ad6c763e/aiohttp-3.14.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb2c0c80d431c0d03f2c7dbf125150fedd4f0de17366a7ca33f7ccb822391842", size = 1904054, upload-time = "2026-06-07T21:06:52.035Z" }, + { url = "https://files.pythonhosted.org/packages/20/9c/d445818389df371f56d141d881153ba23183c4735a03f7356ffb43f7757d/aiohttp-3.14.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e6fc1a85fa7194a1a7d19f44e8609180f4a8eb5fa4c7ed8b4355f080fad235c", size = 1790278, upload-time = "2026-06-07T21:06:54.049Z" }, + { url = "https://files.pythonhosted.org/packages/4d/aa/bf04cb4d865fc6101c2229a294ad744973b72e513fdc5a6b791e6983d72a/aiohttp-3.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:686b6c0d3911ec387b444ddf5dc62fb7f7c0a7d5186a7861626496a5ab4aff95", size = 1591795, upload-time = "2026-06-07T21:06:55.911Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b4/4dac0038960427ba832f6609dfb4ea5437d7fd80c72001b9e48f834f428b/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c6fa4dc7ad6f8109c70bb1499e589f76b0b792baf39f9b017eb92c8a81d0a199", size = 1728397, upload-time = "2026-06-07T21:06:57.777Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/7cd4e8ad7aa3b75f17d56bb5498dd604a93d4e6eece822ba0568c413fff0/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:87a5eea1b2a5e21e1ebdbb33ad4165359189327e63fc4e4894693e7f821ac817", size = 1766504, upload-time = "2026-06-07T21:07:00.009Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/fc01d9fcad0f73fed3f3d361f1f94f975947b50dff82919f6dc2bf4316cc/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c1421eb01d4fd608d88cc8290211d177a58532b55ad94076fb349c5bf467f0a", size = 1777806, upload-time = "2026-06-07T21:07:02.064Z" }, + { url = "https://files.pythonhosted.org/packages/41/09/47e2d090bddcc8fb4ccb4c314aadc32d7c5d9bb55f50f6ad1c92fc15d501/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:34b257ec41345c1e8f2df68fa908a7952f5de932723871eb633ecbbff396c9a4", size = 1580707, upload-time = "2026-06-07T21:07:03.942Z" }, + { url = "https://files.pythonhosted.org/packages/3d/36/f1a4ce904ae0b6930cfe9afc96d0896f7ec1a620c400405d63783bb95a9c/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:de538791a80e5d862addbc183f70f0158ac9b9bb872bb147f1fd2a683691e087", size = 1798121, upload-time = "2026-06-07T21:07:05.987Z" }, + { url = "https://files.pythonhosted.org/packages/70/0a/e0075ce9ca0279ee1d4f0c0b85f54fea02ebc83c3007651a72bece658fec/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f71173be42d3241d428f760122febb748de0623f44308a6f120d0dd9ec572e3", size = 1767580, upload-time = "2026-06-07T21:07:07.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/a0c0a8f327a9c52095cdd8e312391b00d3ed64ab6c72bb5c33d8ec251cf7/aiohttp-3.14.1-cp312-cp312-win32.whl", hash = "sha256:ec8dc383ee57ea3e883477dcca3f11b65d58199f1080acaf4cd6ad9a99698be4", size = 452771, upload-time = "2026-06-07T21:07:09.669Z" }, + { url = "https://files.pythonhosted.org/packages/df/d9/ea367c75f16ac9c6cdc8febb25e8318fa21a2b1bc8d6514d4b2d890bface/aiohttp-3.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2aa92c87868cd13674989f9ee83e5f9f7ea4237589b728048e1f0c8f6caa3271", size = 479873, upload-time = "2026-06-07T21:07:11.538Z" }, + { url = "https://files.pythonhosted.org/packages/03/64/8d96784a7851156db8a4c6c3f6f91042fdf39fb15a4cc38c8b3c14833c45/aiohttp-3.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:2c840c90759922cb5e6dda94596e079a30fb5a5ba548e7e0dc00574703940847", size = 448073, upload-time = "2026-06-07T21:07:13.637Z" }, + { url = "https://files.pythonhosted.org/packages/bc/97/bd137012dd97e1649162b099135a80e1fd59aaa807b2430fc448d1029aff/aiohttp-3.14.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178", size = 506882, upload-time = "2026-06-07T21:07:15.501Z" }, + { url = "https://files.pythonhosted.org/packages/ef/79/e5cc690e9d922a66887ceeaca53a8ffd5a7b0be3816142b7abc433742d89/aiohttp-3.14.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf", size = 515270, upload-time = "2026-06-07T21:07:17.53Z" }, + { url = "https://files.pythonhosted.org/packages/fe/22/a73ccbf9dbd6e26dda0b24d5fd5db7da92ee3383a79f47677ffb834c5c5b/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd", size = 485841, upload-time = "2026-06-07T21:07:19.555Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b9/57ed8eaf596321c2ad747bd480fb1700dbd7177c60dfc9e4c187f629662e/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe", size = 492088, upload-time = "2026-06-07T21:07:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/78/c0/5ebe5270a7c140d7c6f79dcb018640225f14d406c149e4eec04a7d82fe71/aiohttp-3.14.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4", size = 501564, upload-time = "2026-06-07T21:07:23.388Z" }, + { url = "https://files.pythonhosted.org/packages/75/7f/8cdaa24fc7983865e0915153b96a9ac5bcdd3548d64c5a27d17cecccad2d/aiohttp-3.14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876", size = 751998, upload-time = "2026-06-07T21:07:25.046Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f4/c4227aacfacc5cb0cc2d119b65301d177912a6842cd64e120c47af76064f/aiohttp-3.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da", size = 510918, upload-time = "2026-06-07T21:07:27.28Z" }, + { url = "https://files.pythonhosted.org/packages/ab/01/a2d5f96cd4e74424864d30bc0a7e44d0a12dacdcfa91b5b2d1bd3dca6bf3/aiohttp-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6", size = 508657, upload-time = "2026-06-07T21:07:29.252Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ed/3c0fb5c500fdd8e7ebc10d1889c04384fffa1a9163eac1356088ca9da1b1/aiohttp-3.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96", size = 1757907, upload-time = "2026-06-07T21:07:31.03Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ab/d4c924d9bd5be3050c226612413ce68cb54c70d2c31b661bfc8d9a5b6a70/aiohttp-3.14.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f", size = 1737565, upload-time = "2026-06-07T21:07:33.031Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/37326821ff779084020cdc33224d20b19f42f4183a500ff92022a739eda7/aiohttp-3.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296", size = 1799018, upload-time = "2026-06-07T21:07:35.003Z" }, + { url = "https://files.pythonhosted.org/packages/b3/4f/6e947ba73e4ce09070761c05ed3a8ceb7c21f5e46798671d8b2aac0e4626/aiohttp-3.14.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa", size = 1894416, upload-time = "2026-06-07T21:07:36.956Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6e/dbf1d0625dc711fb2851f4f3c3055c39ed58bae92082d8c627dbe6013736/aiohttp-3.14.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451", size = 1783881, upload-time = "2026-06-07T21:07:39.063Z" }, + { url = "https://files.pythonhosted.org/packages/44/c2/5e25098a67268ed369483ae7d1a58bd0a13d03aab860d2a0e4a6eb25b046/aiohttp-3.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c", size = 1587572, upload-time = "2026-06-07T21:07:41.058Z" }, + { url = "https://files.pythonhosted.org/packages/2a/bd/cf9cee17e140f942a3de73e658a543aa8fbf35a5fc67a9d2538d52d77f0b/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca", size = 1722137, upload-time = "2026-06-07T21:07:43.014Z" }, + { url = "https://files.pythonhosted.org/packages/89/6d/5684f8c59045c96f81a18cefbc1fbbd79d25b88f1c622f2a5c5c08fcb632/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09", size = 1755953, upload-time = "2026-06-07T21:07:45.933Z" }, + { url = "https://files.pythonhosted.org/packages/a8/40/35caf3170f8359760740a7d9aa0fff2e344bef98e1d1186f5a0f6dec17e6/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397", size = 1766479, upload-time = "2026-06-07T21:07:48.047Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a1/b0c61e7a137f0d81de49a82023a6df73c3c16d6fefb0f8e4a93d21639002/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080", size = 1580077, upload-time = "2026-06-07T21:07:50.069Z" }, + { url = "https://files.pythonhosted.org/packages/0b/41/194ea4623693009fcefebef7aef63c141754f153e9cd0d39d3b9e36c175c/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345", size = 1791688, upload-time = "2026-06-07T21:07:52.106Z" }, + { url = "https://files.pythonhosted.org/packages/ba/45/4de841f005cfe1fd63e2a2fe011262c515e2a62aa6994b15947e7d717ac9/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588", size = 1761094, upload-time = "2026-06-07T21:07:54.113Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ae/dbce10533d3896d544d5053939ed75b7dc31a1b0973d959b1b5ae21028d6/aiohttp-3.14.1-cp313-cp313-win32.whl", hash = "sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780", size = 452662, upload-time = "2026-06-07T21:07:56.06Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/0bf1a19362c32f06229da5e7ddfcec91f93474d6307f7a2d3135e9c674dc/aiohttp-3.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a", size = 479748, upload-time = "2026-06-07T21:07:58.319Z" }, + { url = "https://files.pythonhosted.org/packages/22/0a/62e7232dc9484fbec112ceb32efb6a624cc7994ec6e2b019286f17c4e8f2/aiohttp-3.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8", size = 447723, upload-time = "2026-06-07T21:08:00.154Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a1/5fafa04e1ca91ddb47608699d60649c1c6db3cf41c99e78fc4056f9513db/aiohttp-3.14.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15", size = 508531, upload-time = "2026-06-07T21:08:02.093Z" }, + { url = "https://files.pythonhosted.org/packages/fa/2e/bfa02f699d87ffc86d5959270b28f1cb410add3ccaced8ed2e0b8a5238fc/aiohttp-3.14.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8", size = 514718, upload-time = "2026-06-07T21:08:04.476Z" }, + { url = "https://files.pythonhosted.org/packages/85/a5/9594ad6289eebbc97d167c44213d557807f90e59115caad24de21ad2c3b1/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3", size = 487918, upload-time = "2026-06-07T21:08:06.377Z" }, + { url = "https://files.pythonhosted.org/packages/b4/61/16a32c36c3c49edec122a3dc811f2057df2f94d3b14aa107c8017d981618/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba", size = 494014, upload-time = "2026-06-07T21:08:08.263Z" }, + { url = "https://files.pythonhosted.org/packages/9b/89/3ebcf96ed99c05bec9c434aaac6963fd3cbab4a786ae739908a144d9ce44/aiohttp-3.14.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397", size = 502398, upload-time = "2026-06-07T21:08:10.244Z" }, + { url = "https://files.pythonhosted.org/packages/fd/3d/b74870a0c2d40c355928cd5b96c7a11fa821b8a40fc41365e64479b151fb/aiohttp-3.14.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448", size = 758018, upload-time = "2026-06-07T21:08:12.447Z" }, + { url = "https://files.pythonhosted.org/packages/d3/66/f42f5c984d99e49c6cff5f26f590750f2e2f7ef1fcfb99966ab5be1b632e/aiohttp-3.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004", size = 512462, upload-time = "2026-06-07T21:08:14.624Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a7/248e1aebe0c7810b0271e021a0f2a5eb6e78a051885b3c9df49f42a5802d/aiohttp-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983", size = 512824, upload-time = "2026-06-07T21:08:16.572Z" }, + { url = "https://files.pythonhosted.org/packages/26/97/2aa0e5ba0727dc3bd5aaebb7ccbc510f7dfb7fb961ec87497cd496635ab1/aiohttp-3.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe", size = 1749898, upload-time = "2026-06-07T21:08:18.635Z" }, + { url = "https://files.pythonhosted.org/packages/00/8d/e97f6c96c891d457c8479d92a514ba194d0412f981d72c70341ee18488ed/aiohttp-3.14.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333", size = 1710114, upload-time = "2026-06-07T21:08:20.892Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e6/aa8d7e863048c8fceb5cd6ce74017311cec3ead07847387e12265fb4444e/aiohttp-3.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0", size = 1802541, upload-time = "2026-06-07T21:08:23.044Z" }, + { url = "https://files.pythonhosted.org/packages/83/a8/72193137de57fda4ebfae4563182d082c8856e3b6e9871d0b46f028fb369/aiohttp-3.14.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a", size = 1875776, upload-time = "2026-06-07T21:08:25.288Z" }, + { url = "https://files.pythonhosted.org/packages/a0/18/938441025db6769a3464596b2410af3afde0b21eb2f204c6f766f68af4bd/aiohttp-3.14.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602", size = 1760329, upload-time = "2026-06-07T21:08:27.363Z" }, + { url = "https://files.pythonhosted.org/packages/60/29/bf2496b4065e76e09fe48015aaffe5ce161d8f089b06ac6982070f653076/aiohttp-3.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca", size = 1587293, upload-time = "2026-06-07T21:08:29.805Z" }, + { url = "https://files.pythonhosted.org/packages/49/a2/2136674d52123b1354bd05dd5753c318db47dc0c927cc70b27bab3755456/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35", size = 1714756, upload-time = "2026-06-07T21:08:32.094Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b9/e5fd2e6f915503081c0f9b1e8540947037929c70c191da2e4d54b31a21a1/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844", size = 1721052, upload-time = "2026-06-07T21:08:34.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/5a/2833e324a2263e104e31e2e91bc5bbee81bc499afd32203faee048a883f0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496", size = 1766888, upload-time = "2026-06-07T21:08:36.95Z" }, + { url = "https://files.pythonhosted.org/packages/57/fa/dea6511870913162f3b2e8c42a7614eb203a4540b8c2da43e0bfb0548f3c/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5", size = 1581679, upload-time = "2026-06-07T21:08:39.292Z" }, + { url = "https://files.pythonhosted.org/packages/14/bd/3cf0d55e71784b33534e9710a67d382d900598b4787fbce6cc7317f8c42a/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95", size = 1782021, upload-time = "2026-06-07T21:08:41.407Z" }, + { url = "https://files.pythonhosted.org/packages/c1/af/14bb5843eccbe234f4dfb78ab73e549d99727247e62ae5d62cbd22eaf5b0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444", size = 1742574, upload-time = "2026-06-07T21:08:43.795Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1e/fbeb7af9210a67ac0f9c9bec0f8f4568497924e33137a3d5b48e1cf85f3f/aiohttp-3.14.1-cp314-cp314-win32.whl", hash = "sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0", size = 457773, upload-time = "2026-06-07T21:08:46.168Z" }, + { url = "https://files.pythonhosted.org/packages/f0/2b/13e8d741a9ec5db7d900c060554cf8352ab85e44e2a4469ebb9d377bda17/aiohttp-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719", size = 485001, upload-time = "2026-06-07T21:08:48.401Z" }, + { url = "https://files.pythonhosted.org/packages/df/30/491acfa2c4d6c3ff59c49a14fc1b50be3241e25bbb0c84c09e2da4d11395/aiohttp-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec", size = 453809, upload-time = "2026-06-07T21:08:50.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/e3/19dbe1a1f4cc6230eb9e314de7fe68053b0992f9302b27d12141a0b5db53/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2", size = 793320, upload-time = "2026-06-07T21:08:52.775Z" }, + { url = "https://files.pythonhosted.org/packages/7f/20/1b7182219ba1b108430d6e4dc53d25ae02dcfcf5a045b33af4e8c5167527/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340", size = 529077, upload-time = "2026-06-07T21:08:55Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c8/14ce60ec31a2e5f5274bb17d383a6f7a3aabca31ac04eee05585bbadab16/aiohttp-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d", size = 532476, upload-time = "2026-06-07T21:08:57.176Z" }, + { url = "https://files.pythonhosted.org/packages/7e/02/9ac85e081e53da2e061b02fa7758fe0a12d17b8ce2d1f5e6c7cb76730328/aiohttp-3.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94", size = 1922347, upload-time = "2026-06-07T21:08:59.563Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3e/d3ba07a0ab38b5389e10bec4362d21e10a4f667cba2d79ba30837b3a5059/aiohttp-3.14.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc", size = 1786465, upload-time = "2026-06-07T21:09:01.909Z" }, + { url = "https://files.pythonhosted.org/packages/0b/cb/e2ee978a00cfb2df829704a69528b18154eba5939f45bc1efa8f33aee4c5/aiohttp-3.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251", size = 1909423, upload-time = "2026-06-07T21:09:04.357Z" }, + { url = "https://files.pythonhosted.org/packages/73/5d/1430334858b1022b58ae50399a918f0bd6fe8fa7fa183598d657ff61e040/aiohttp-3.14.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1", size = 2001906, upload-time = "2026-06-07T21:09:06.722Z" }, + { url = "https://files.pythonhosted.org/packages/66/4e/560c7472d3d198a23aa5c8b19a5115bf6a9b77b7d3e4bb363da320430ad2/aiohttp-3.14.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3", size = 1877095, upload-time = "2026-06-07T21:09:09.011Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f1/4745806578d447db4a784a8591e2dae3afdfc2bcb96f8f81271b13df6543/aiohttp-3.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c", size = 1676222, upload-time = "2026-06-07T21:09:11.461Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c9/48255813cca749a229ef0ab476004ec623728ad79a9c0840616f6c076325/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203", size = 1842922, upload-time = "2026-06-07T21:09:14.118Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c0/bbd054e2bee909f529523a5af3891052606af5143c09f5f183ec3b234676/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1", size = 1825035, upload-time = "2026-06-07T21:09:16.447Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ae/90395d4376deceb74e09ec26b6adf7d2015a6f8802d6d84446af860fef04/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665", size = 1849512, upload-time = "2026-06-07T21:09:18.742Z" }, + { url = "https://files.pythonhosted.org/packages/93/bd/fb25f3049957553d4ce0ba6ae480aa2f592a6985497fca590837d16c1be0/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1", size = 1668571, upload-time = "2026-06-07T21:09:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/3f/22/7f73303d64dd567ff3addca90b556690ed1233a47b8f55d242fb90af3681/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d", size = 1881159, upload-time = "2026-06-07T21:09:23.813Z" }, + { url = "https://files.pythonhosted.org/packages/44/be/0474c5a8b5640e1e4aa1923430a91f4151be82e511373fe764189b89aef5/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c", size = 1841409, upload-time = "2026-06-07T21:09:26.207Z" }, + { url = "https://files.pythonhosted.org/packages/7b/3c/bb4a7cba26956cb3da4553cc2056cf67be5b5ff6e6d8fa4fbdff73bfb7ae/aiohttp-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365", size = 494166, upload-time = "2026-06-07T21:09:28.505Z" }, + { url = "https://files.pythonhosted.org/packages/8a/84/ec80c2c1f66a952555a9f86df6b33af65108a6febfa0471b69013a12f807/aiohttp-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9", size = 530255, upload-time = "2026-06-07T21:09:30.843Z" }, + { url = "https://files.pythonhosted.org/packages/2a/71/6e22be134a4061ada85a92951b842f2657f17d926b727f3f94c56ae963d6/aiohttp-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6", size = 469640, upload-time = "2026-06-07T21:09:33.028Z" }, +] + +[[package]] +name = "aioitertools" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/3c/53c4a17a05fb9ea2313ee1777ff53f5e001aefd5cc85aa2f4c2d982e1e38/aioitertools-0.13.0.tar.gz", hash = "sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c", size = 19322, upload-time = "2025-11-06T22:17:07.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl", hash = "sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be", size = 24182, upload-time = "2025-11-06T22:17:06.502Z" }, ] [[package]] @@ -131,6 +225,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] +[[package]] +name = "aiosqlite" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/8a/64761f4005f17809769d23e518d915db74e6310474e733e3593cfc854ef1/aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650", size = 14821, upload-time = "2025-12-23T19:25:43.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/b7/e3bf5133d697a08128598c8d0abc5e16377b51465a33756de24fa7dee953/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb", size = 17405, upload-time = "2025-12-23T19:25:42.139Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -140,19 +252,46 @@ 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 = "anthropic" +version = "0.117.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/0d/8f71d535edb0d438f023bd825fb65f67c14fa88a2bd6b75f292a58a63de4/anthropic-0.117.0.tar.gz", hash = "sha256:98107f2b76439641e0ae2a1754087534b8f178dbab99d6eb1bc4b7bc8c744496", size = 989933, upload-time = "2026-07-16T19:36:13.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/4c/917d21d6619a4475cdafc6d13a69fdb3b901ddac57e76caca5a25c117b6d/anthropic-0.117.0-py3-none-any.whl", hash = "sha256:451a0a6905f11dff7663d13e4ee5dbf909eb8942b1d049803c7b937a13ac47ec", size = 998327, upload-time = "2026-07-16T19:36:11.225Z" }, +] + +[[package]] +name = "antlr4-python3-runtime" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/5f/2cdf6f7aca3b20d3f316e9f505292e1f256a32089bd702034c29ebde6242/antlr4_python3_runtime-4.13.2.tar.gz", hash = "sha256:909b647e1d2fc2b70180ac586df3933e38919c85f98ccc656a96cd3f25ef3916", size = 117467, upload-time = "2024-08-03T19:00:12.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/03/a851e84fcbb85214dc637b6378121ef9a0dd61b4c65264675d8a5c9b1ae7/antlr4_python3_runtime-4.13.2-py3-none-any.whl", hash = "sha256:fe3835eb8d33daece0e799090eda89719dbccee7aa39ef94eed3818cafa5a7e8", size = 144462, upload-time = "2024-08-03T19:00:11.134Z" }, +] + [[package]] name = "anyio" -version = "4.10.0" +version = "4.14.2" 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/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, ] [[package]] @@ -166,25 +305,75 @@ wheels = [ [[package]] name = "attrs" -version = "25.3.0" +version = "26.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] [[package]] -name = "automat" -version = "25.4.16" +name = "authlib" +version = "1.7.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, + { name = "cryptography" }, + { name = "joserfc" }, ] +sdist = { url = "https://files.pythonhosted.org/packages/36/98/7d93f30d029643c0275dbc0bd6d5a6f670661ee6c9a94d93af7ab4887600/authlib-1.7.2.tar.gz", hash = "sha256:2cea25fefcd4e7173bdf1372c0afc265c8034b23a8cd5dcb6a9164b826c64231", size = 176511, upload-time = "2026-05-06T08:10:23.116Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/95/adcb68e20c34162e9135f370d6e31737719c2b6f94bc953fe7ed1f10fe21/authlib-1.7.2-py2.py3-none-any.whl", hash = "sha256:3e1faedc9d87e7d56a164eca3ccb6ace0d61b94abe83e92242f8dc8bba9b4a9f", size = 259548, upload-time = "2026-05-06T08:10:21.436Z" }, +] + +[[package]] +name = "automat" +version = "25.4.16" +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e3/0f/d40bbe294bbf004d436a8bcbcfaadca8b5140d39ad0ad3d73d1a8ba15f14/automat-25.4.16.tar.gz", hash = "sha256:0017591a5477066e90d26b0e696ddc143baafd87b588cfac8100bc6be9634de0", size = 129977, upload-time = "2025-04-16T20:12:16.002Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/02/ff/1175b0b7371e46244032d43a56862d0af455823b5280a50c63d99cc50f18/automat-25.4.16-py3-none-any.whl", hash = "sha256:04e9bce696a8d5671ee698005af6e5a9fa15354140a87f4870744604dcdd3ba1", size = 42842, upload-time = "2025-04-16T20:12:14.447Z" }, ] +[[package]] +name = "aws-requests-auth" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/b2/455c0bfcbd772dafd4c9e93c4b713e36790abf9ccbca9b8e661968b29798/aws-requests-auth-0.4.3.tar.gz", hash = "sha256:33593372018b960a31dbbe236f89421678b885c35f0b6a7abfae35bb77e069b2", size = 10096, upload-time = "2020-05-27T23:10:34.742Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/11/5dc8be418e1d54bed15eaf3a7461797e5ebb9e6a34869ad750561f35fa5b/aws_requests_auth-0.4.3-py2.py3-none-any.whl", hash = "sha256:646bc37d62140ea1c709d20148f5d43197e6bd2d63909eb36fa4bb2345759977", size = 6838, upload-time = "2020-05-27T23:10:33.658Z" }, +] + +[[package]] +name = "aws-sam-translator" +version = "1.111.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boto3" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/31/4e6d6f0b9d4ead8eaa1c13a14d86834e7691acf5726fb49de98f8e195028/aws_sam_translator-1.111.0.tar.gz", hash = "sha256:6884d94e28dc20384e5e0396e9386a456fe59303d706924deb2646329b4d97d3", size = 374368, upload-time = "2026-07-02T00:31:39.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/e3/505c9db9c4a4270ad12bac621f370cb02eabb224886c1b81960c658d9baa/aws_sam_translator-1.111.0-py3-none-any.whl", hash = "sha256:510d0ad8cd40b245a62004f2dc974ddbb6ff0d496bdec0bc7d9cd209b46d5fad", size = 440579, upload-time = "2026-07-02T00:31:38.077Z" }, +] + +[[package]] +name = "aws-xray-sdk" +version = "2.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/25/0cbd7a440080def5e6f063720c3b190a25f8aa2938c1e34415dc18241596/aws_xray_sdk-2.15.0.tar.gz", hash = "sha256:794381b96e835314345068ae1dd3b9120bd8b4e21295066c37e8814dbb341365", size = 76315, upload-time = "2025-10-29T20:59:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/c3/f30a7a63e664acc7c2545ca0491b6ce8264536e0e5cad3965f1d1b91e960/aws_xray_sdk-2.15.0-py2.py3-none-any.whl", hash = "sha256:422d62ad7d52e373eebb90b642eb1bb24657afe03b22a8df4a8b2e5108e278a3", size = 103228, upload-time = "2025-10-29T21:00:24.12Z" }, +] + [[package]] name = "backports-tarfile" version = "1.2.0" @@ -194,6 +383,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, ] +[[package]] +name = "basedpyright" +version = "1.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodejs-wheel-binaries" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/77/ded02ba2b400807b291fa2b9d29ac7f473e86a45d1f5212d8276e9029107/basedpyright-1.34.0.tar.gz", hash = "sha256:7ae3b06f644fac15fdd14a00d0d1f12f92a8205ae1609aabd5a0799b1a68be1d", size = 22803348, upload-time = "2025-11-19T14:48:16.38Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/9e/ced31964ed49f06be6197bd530958b6ddca9a079a8d7ee0ee7429cae9e27/basedpyright-1.34.0-py3-none-any.whl", hash = "sha256:e76015c1ebb671d2c6d7fef8a12bc0f1b9d15d74e17847b7b95a3a66e187c70f", size = 11865958, upload-time = "2025-11-19T14:48:13.724Z" }, +] + [[package]] name = "bashlex" version = "0.18" @@ -203,26 +404,88 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/be/6985abb1011fda8a523cfe21ed9629e397d6e06fb5bae99750402b25c95b/bashlex-0.18-py2.py3-none-any.whl", hash = "sha256:91d73a23a3e51711919c1c899083890cdecffc91d8c088942725ac13e9dcfffa", size = 69539, upload-time = "2023-01-18T15:21:24.167Z" }, ] +[[package]] +name = "beautifulsoup4" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "boto3" +version = "1.40.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/f9/6ef8feb52c3cce5ec3967a535a6114b57ac7949fd166b0f3090c2b06e4e5/boto3-1.40.61.tar.gz", hash = "sha256:d6c56277251adf6c2bdd25249feae625abe4966831676689ff23b4694dea5b12", size = 111535, upload-time = "2025-10-28T19:26:57.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/24/3bf865b07d15fea85b63504856e137029b6acbc73762496064219cdb265d/boto3-1.40.61-py3-none-any.whl", hash = "sha256:6b9c57b2a922b5d8c17766e29ed792586a818098efe84def27c8f582b33f898c", size = 139321, upload-time = "2025-10-28T19:26:55.007Z" }, +] + +[[package]] +name = "botocore" +version = "1.40.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/a3/81d3a47c2dbfd76f185d3b894f2ad01a75096c006a2dd91f237dca182188/botocore-1.40.61.tar.gz", hash = "sha256:a2487ad69b090f9cccd64cf07c7021cd80ee9c0655ad974f87045b02f3ef52cd", size = 14393956, upload-time = "2025-10-28T19:26:46.108Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/c5/f6ce561004db45f0b847c2cd9b19c67c6bf348a82018a48cb718be6b58b0/botocore-1.40.61-py3-none-any.whl", hash = "sha256:17ebae412692fd4824f99cde0f08d50126dc97954008e5ba2b522eb049238aa7", size = 14055973, upload-time = "2025-10-28T19:26:42.15Z" }, +] + +[[package]] +name = "botocore-stubs" +version = "1.43.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-awscrt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/81/79693e833291c00dc89ee610e5e915381b6f08233912e28df50106840780/botocore_stubs-1.43.14.tar.gz", hash = "sha256:9e3bc1fdd51da7473f0df726c82747a1b0ae913449d629659765c247fecc2039", size = 42738, upload-time = "2026-05-25T06:06:37.484Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/ca/f017727b11895908c5dedc829cf2ec35e0c4b2a26ba875db325fef2cefdf/botocore_stubs-1.43.14-py3-none-any.whl", hash = "sha256:fb98f1475c92fd718644e786b5c543a20f1b1f610e89e0a7191c3f1f429c75aa", size = 67093, upload-time = "2026-05-25T06:06:34.532Z" }, +] + [[package]] name = "bracex" -version = "2.6" +version = "3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/63/9a/fec38644694abfaaeca2798b58e276a8e61de49e2e37494ace423395febc/bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7", size = 26642, upload-time = "2025-06-22T19:12:31.254Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/f5/4473ad9b48cd0420a2d762a3750fa0e078e23e060b1af72662e5987e5530/bracex-3.0.tar.gz", hash = "sha256:b73f718d6bd98d8419e45df02426c86e9967c179949f779340d6c3a8c83b9111", size = 43162, upload-time = "2026-06-30T00:43:35.279Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2e/68781b78e764e5ccc4af1e3d27e060069c73af90234853fa80000e7ee79d/bracex-3.0-py3-none-any.whl", hash = "sha256:3833e61c2f092d5aa0468fa2e6c6e990a306185abf763b6d122f0158e59c58a5", size = 11738, upload-time = "2026-06-30T00:43:34.196Z" }, ] [[package]] name = "cachecontrol" -version = "0.14.3" +version = "0.14.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "msgpack" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/3a/0cbeb04ea57d2493f3ec5a069a117ab467f85e4a10017c6d854ddcbff104/cachecontrol-0.14.3.tar.gz", hash = "sha256:73e7efec4b06b20d9267b441c1f733664f989fb8688391b670ca812d70795d11", size = 28985, upload-time = "2025-04-30T16:45:06.135Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f6/c972b32d80760fb79d6b9eeb0b3010a46b89c0b23cf6329417ff7886cd22/cachecontrol-0.14.4.tar.gz", hash = "sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1", size = 16150, upload-time = "2025-11-14T04:32:13.138Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/4c/800b0607b00b3fd20f1087f80ab53d6b4d005515b0f773e4831e37cfa83f/cachecontrol-0.14.3-py3-none-any.whl", hash = "sha256:b35e44a3113f17d2a31c1e6b27b9de6d4405f84ae51baa8c1d3cc5b633010cae", size = 21802, upload-time = "2025-04-30T16:45:03.863Z" }, + { url = "https://files.pythonhosted.org/packages/ef/79/c45f2d53efe6ada1110cf6f9fca095e4ff47a0454444aefdde6ac4789179/cachecontrol-0.14.4-py3-none-any.whl", hash = "sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b", size = 22247, upload-time = "2025-11-14T04:32:11.733Z" }, ] [package.optional-dependencies] @@ -232,140 +495,232 @@ filecache = [ [[package]] name = "certifi" -version = "2025.8.3" +version = "2026.6.17" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, ] [[package]] name = "cffi" -version = "1.17.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" }, - { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" }, - { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" }, - { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" }, - { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" }, - { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" }, - { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" }, - { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, - { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, - { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, - { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, - { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, - { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, - { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, - { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, - { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, - { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, - { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, - { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, - { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, - { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, - { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, - { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, - { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, - { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, - { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, - { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910, upload-time = "2024-09-04T20:45:05.315Z" }, - { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200, upload-time = "2024-09-04T20:45:06.903Z" }, - { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565, upload-time = "2024-09-04T20:45:08.975Z" }, - { url = "https://files.pythonhosted.org/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635, upload-time = "2024-09-04T20:45:10.64Z" }, - { url = "https://files.pythonhosted.org/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218, upload-time = "2024-09-04T20:45:12.366Z" }, - { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486, upload-time = "2024-09-04T20:45:13.935Z" }, - { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911, upload-time = "2024-09-04T20:45:15.696Z" }, - { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632, upload-time = "2024-09-04T20:45:17.284Z" }, +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/e9/6d7724983b3d5a0908dbf74f64038ade77c18646ff6636ec7894fd392ce1/cffi-2.1.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0", size = 183837, upload-time = "2026-07-06T21:32:09.655Z" }, + { url = "https://files.pythonhosted.org/packages/69/aa/24580a278de21fd7322635556334d9b535f1cbc00b0a3919447cdf464c65/cffi-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd", size = 184226, upload-time = "2026-07-06T21:32:11.196Z" }, + { url = "https://files.pythonhosted.org/packages/88/a9/02cae418ec4beb282ace11958d9d4737793439d561fadc7e6d56f2e2b354/cffi-2.1.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46", size = 211107, upload-time = "2026-07-06T21:32:12.328Z" }, + { url = "https://files.pythonhosted.org/packages/3b/30/c806937ed5e4c2c7ac30d9d6b76b5dc57ff8b75d83800d9bb11a8253cf2a/cffi-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2", size = 218733, upload-time = "2026-07-06T21:32:13.67Z" }, + { url = "https://files.pythonhosted.org/packages/f9/cf/398272b8bbfd58aa314fda5a7f1cdbb26d1d78ae324a11211521315dd1f0/cffi-2.1.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd", size = 205543, upload-time = "2026-07-06T21:32:15.148Z" }, + { url = "https://files.pythonhosted.org/packages/45/ca/f91641185cdd90c36d317a9dc7f85e88ef8682d8b300977baff5e23c35d8/cffi-2.1.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3", size = 205460, upload-time = "2026-07-06T21:32:16.479Z" }, + { url = "https://files.pythonhosted.org/packages/38/66/04781a77b411f0bb5b234d62c1814754ab75ebe455ccff1b08e8d7aae98f/cffi-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0", size = 218760, upload-time = "2026-07-06T21:32:17.98Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9a/bb1d5ed9c3fcae158e9f6391bf309c95d98c2ac37ed56573228471d0af5e/cffi-2.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43", size = 221230, upload-time = "2026-07-06T21:32:19.407Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/3c1409cdd26094efacd1c36c66e0a6eb9d4296e4fd4f9901b8b2042f4323/cffi-2.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c", size = 213524, upload-time = "2026-07-06T21:32:20.828Z" }, + { url = "https://files.pythonhosted.org/packages/fa/75/74dfb7c3fc6ebbd408038476bd4c1d7e925c62614e7b9c534ecc34218288/cffi-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd", size = 220341, upload-time = "2026-07-06T21:32:21.9Z" }, + { url = "https://files.pythonhosted.org/packages/70/b6/9003c33a3e7d2c1306f5962e646457dcfe5a8cd8fce6bbe02d7af25db783/cffi-2.1.0-cp310-cp310-win32.whl", hash = "sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f", size = 174578, upload-time = "2026-07-06T21:32:23.073Z" }, + { url = "https://files.pythonhosted.org/packages/8a/26/710688310447531c7a22f857c7f79d9855ec18b03e04494ced723fb37e2f/cffi-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da", size = 185071, upload-time = "2026-07-06T21:32:24.671Z" }, + { url = "https://files.pythonhosted.org/packages/d3/67/85c89a59ba36a671e79638f44d466749f08179266a57e4f2ffdf92174072/cffi-2.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc", size = 183845, upload-time = "2026-07-06T21:32:26.32Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dd/e3b0baa2d3d6a857ac72b7efbf18e32e487c9cdafcc13049ad765495b15e/cffi-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7", size = 184186, upload-time = "2026-07-06T21:32:28.025Z" }, + { url = "https://files.pythonhosted.org/packages/65/68/9f3ef890cf3c6ab97bd531c5677f67613d302165d16f8142b2811782a614/cffi-2.1.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93", size = 211892, upload-time = "2026-07-06T21:32:29.565Z" }, + { url = "https://files.pythonhosted.org/packages/22/d7/1a74539db16d8bfd839ff1515948948efbb162e574650fd3d846896eea95/cffi-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2", size = 218793, upload-time = "2026-07-06T21:32:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d1/9a5b7169499e8e8d8e636de70b97ac7c9447104d2ff1a2cd94790cea5162/cffi-2.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c", size = 205737, upload-time = "2026-07-06T21:32:32.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b0/e131a9c41f10607926278453d9596163594fe1c4ebc46efe3b5e5b34eb84/cffi-2.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f", size = 204909, upload-time = "2026-07-06T21:32:33.655Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d2/4398416cd699b35167947c6e22aca52c47e69ad5695073c9f1f2c52e04aa/cffi-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565", size = 217883, upload-time = "2026-07-06T21:32:35.173Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a5/d4fe77b589e5e82d43ebc809bf2e6474afe8e48e32ea050b9357645b6471/cffi-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c", size = 221251, upload-time = "2026-07-06T21:32:36.527Z" }, + { url = "https://files.pythonhosted.org/packages/22/f0/a2fc43084c0433caf7f461bccc013e28f848d04ee1c5ed7fce71423cf4d9/cffi-2.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02", size = 214250, upload-time = "2026-07-06T21:32:37.852Z" }, + { url = "https://files.pythonhosted.org/packages/04/8c/b925975448cf20634a9fbd5efceb807219db452653648d2897c0989cab2d/cffi-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e", size = 219441, upload-time = "2026-07-06T21:32:39.146Z" }, + { url = "https://files.pythonhosted.org/packages/eb/da/5c4918a2d61d86fa927d716cb3d8e4626ef8dc8f605a599d32f33897f59a/cffi-2.1.0-cp311-cp311-win32.whl", hash = "sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479", size = 174496, upload-time = "2026-07-06T21:32:40.467Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c8/6c2de1d55cf35ef8b92885d5ef280790f0fb9634d87ea1cc315176aecd61/cffi-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458", size = 185113, upload-time = "2026-07-06T21:32:41.761Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4e/e8d7cb5783f1841a3c8fb3a7735838d7484d08ec08c9f984b14cac1ac0e9/cffi-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d", size = 179927, upload-time = "2026-07-06T21:32:42.961Z" }, + { url = "https://files.pythonhosted.org/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829, upload-time = "2026-07-06T21:32:44.324Z" }, + { url = "https://files.pythonhosted.org/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728, upload-time = "2026-07-06T21:32:45.683Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" }, + { url = "https://files.pythonhosted.org/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429, upload-time = "2026-07-06T21:32:49.848Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", size = 210315, upload-time = "2026-07-06T21:32:51.221Z" }, + { url = "https://files.pythonhosted.org/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", size = 208859, upload-time = "2026-07-06T21:32:52.512Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844, upload-time = "2026-07-06T21:32:53.704Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287, upload-time = "2026-07-06T21:32:54.907Z" }, + { url = "https://files.pythonhosted.org/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681, upload-time = "2026-07-06T21:32:56.329Z" }, + { url = "https://files.pythonhosted.org/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", size = 175269, upload-time = "2026-07-06T21:32:57.777Z" }, + { url = "https://files.pythonhosted.org/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", size = 185881, upload-time = "2026-07-06T21:32:59.253Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", size = 180088, upload-time = "2026-07-06T21:33:02.278Z" }, + { url = "https://files.pythonhosted.org/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", size = 194331, upload-time = "2026-07-06T21:33:03.697Z" }, + { url = "https://files.pythonhosted.org/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", size = 196966, upload-time = "2026-07-06T21:33:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", size = 184795, upload-time = "2026-07-06T21:33:06.699Z" }, + { url = "https://files.pythonhosted.org/packages/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", size = 184746, upload-time = "2026-07-06T21:33:08.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", size = 214747, upload-time = "2026-07-06T21:33:09.671Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", size = 222392, upload-time = "2026-07-06T21:33:10.896Z" }, + { url = "https://files.pythonhosted.org/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", size = 210285, upload-time = "2026-07-06T21:33:12.251Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", size = 208801, upload-time = "2026-07-06T21:33:13.617Z" }, + { url = "https://files.pythonhosted.org/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", size = 221808, upload-time = "2026-07-06T21:33:15.466Z" }, + { url = "https://files.pythonhosted.org/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", size = 225241, upload-time = "2026-07-06T21:33:16.869Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", size = 223588, upload-time = "2026-07-06T21:33:18.239Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", size = 175248, upload-time = "2026-07-06T21:33:19.799Z" }, + { url = "https://files.pythonhosted.org/packages/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", size = 185717, upload-time = "2026-07-06T21:33:21.47Z" }, + { url = "https://files.pythonhosted.org/packages/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", size = 180114, upload-time = "2026-07-06T21:33:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", size = 194142, upload-time = "2026-07-06T21:33:23.657Z" }, + { url = "https://files.pythonhosted.org/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", size = 196819, upload-time = "2026-07-06T21:33:25.007Z" }, + { url = "https://files.pythonhosted.org/packages/20/71/7c8372d30e42415602ed9f268f7cfd66f1b855fed881ecd168bcb45dbc0b/cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d", size = 184965, upload-time = "2026-07-06T21:33:26.605Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5c/584e626835f0375c928176c04137c96927165cb8733cdb3150ec04e5ee5e/cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac", size = 184952, upload-time = "2026-07-06T21:33:27.823Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", size = 222353, upload-time = "2026-07-06T21:33:29.178Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", size = 210051, upload-time = "2026-07-06T21:33:30.534Z" }, + { url = "https://files.pythonhosted.org/packages/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", size = 208630, upload-time = "2026-07-06T21:33:31.753Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", size = 221593, upload-time = "2026-07-06T21:33:33.044Z" }, + { url = "https://files.pythonhosted.org/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", size = 225146, upload-time = "2026-07-06T21:33:34.224Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", size = 223240, upload-time = "2026-07-06T21:33:35.57Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6f/ade5ce9863a57992a6ea3d0d10d7e29b8749fc127204b3d493d667b2815f/cffi-2.1.0-cp314-cp314-win32.whl", hash = "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd", size = 177723, upload-time = "2026-07-06T21:33:51.626Z" }, + { url = "https://files.pythonhosted.org/packages/41/de/92b9eeed4ae4a21d6fd9b2a2c8505cbed573299902ea73981cc13f7ff62c/cffi-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb", size = 187937, upload-time = "2026-07-06T21:33:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1a/cc6ae6c2913a03aab8898eee57963cf1035b8df5872ed8b9115fcc7e2be8/cffi-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804", size = 183001, upload-time = "2026-07-06T21:33:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/14/f0/134c00ce0779ec86dea2aa1aac69339c2741a8045072676763512363a2ea/cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714", size = 188538, upload-time = "2026-07-06T21:33:36.792Z" }, + { url = "https://files.pythonhosted.org/packages/50/d8/3b86aba791cb610d24e8a3e1b2cd529e71fa15096b04e4d4e360049d4a4c/cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376", size = 188230, upload-time = "2026-07-06T21:33:38.011Z" }, + { url = "https://files.pythonhosted.org/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", size = 223899, upload-time = "2026-07-06T21:33:39.514Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", size = 211652, upload-time = "2026-07-06T21:33:40.851Z" }, + { url = "https://files.pythonhosted.org/packages/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", size = 210755, upload-time = "2026-07-06T21:33:42.183Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", size = 223933, upload-time = "2026-07-06T21:33:43.603Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", size = 226749, upload-time = "2026-07-06T21:33:45.046Z" }, + { url = "https://files.pythonhosted.org/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", size = 225703, upload-time = "2026-07-06T21:33:46.374Z" }, + { url = "https://files.pythonhosted.org/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", size = 182857, upload-time = "2026-07-06T21:33:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", size = 194065, upload-time = "2026-07-06T21:33:48.953Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", size = 186404, upload-time = "2026-07-06T21:33:50.309Z" }, + { url = "https://files.pythonhosted.org/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", size = 194121, upload-time = "2026-07-06T21:33:56.109Z" }, + { url = "https://files.pythonhosted.org/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", size = 196820, upload-time = "2026-07-06T21:33:57.288Z" }, + { url = "https://files.pythonhosted.org/packages/ed/da/4bbe583a3b3a5c8c60892124fe17f3fa3656523faf0d3484eae90f091853/cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3", size = 184936, upload-time = "2026-07-06T21:33:58.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4b/1f4c36ab273980d7aa75bb126ea4f8971f24a96108acad3a0a084028c57b/cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc", size = 185045, upload-time = "2026-07-06T21:34:00.085Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", size = 222342, upload-time = "2026-07-06T21:34:01.814Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", size = 210073, upload-time = "2026-07-06T21:34:03.255Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", size = 208551, upload-time = "2026-07-06T21:34:04.433Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", size = 221649, upload-time = "2026-07-06T21:34:06.157Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", size = 225203, upload-time = "2026-07-06T21:34:07.489Z" }, + { url = "https://files.pythonhosted.org/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", size = 223263, upload-time = "2026-07-06T21:34:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d8/772b8259bf75749adffb1c546828978381fb516f60cf701f6c83daf60c85/cffi-2.1.0-cp315-cp315-win32.whl", hash = "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6", size = 177696, upload-time = "2026-07-06T21:34:26.355Z" }, + { url = "https://files.pythonhosted.org/packages/2f/dd/afa2191fc6d57fedd26e5844a2fe2fcc0bbfa00961bbaa5a41e4921e7cca/cffi-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853", size = 187914, upload-time = "2026-07-06T21:34:27.58Z" }, + { url = "https://files.pythonhosted.org/packages/05/ef/6cd4f8c671517162379dc79cfae5aea9106bc38abb89628d5c16adf6a838/cffi-2.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda", size = 183004, upload-time = "2026-07-06T21:34:28.905Z" }, + { url = "https://files.pythonhosted.org/packages/11/b6/12fc55092817a5faa26fb8c40c7f9d662e11a46ee248c137aafc42517d92/cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc", size = 188378, upload-time = "2026-07-06T21:34:09.926Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2e/cdac88979f295fde5daa69622c7d2111e56e7ceb94f211357fbe452339e4/cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca", size = 188319, upload-time = "2026-07-06T21:34:11.101Z" }, + { url = "https://files.pythonhosted.org/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", size = 223904, upload-time = "2026-07-06T21:34:12.606Z" }, + { url = "https://files.pythonhosted.org/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", size = 211554, upload-time = "2026-07-06T21:34:13.987Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", size = 210795, upload-time = "2026-07-06T21:34:15.972Z" }, + { url = "https://files.pythonhosted.org/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", size = 223843, upload-time = "2026-07-06T21:34:17.509Z" }, + { url = "https://files.pythonhosted.org/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", size = 226773, upload-time = "2026-07-06T21:34:19.05Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", size = 225719, upload-time = "2026-07-06T21:34:20.576Z" }, + { url = "https://files.pythonhosted.org/packages/68/5a/e536c528bc8057496c360c0978559a2dc45653f89dd6151078aa7d8fca1a/cffi-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b", size = 182760, upload-time = "2026-07-06T21:34:22.059Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/0ffe8b82d3875bced5fa1e7986a7a46b748262a40ab7f60b475eb9fb1bb3/cffi-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5", size = 193769, upload-time = "2026-07-06T21:34:23.589Z" }, + { url = "https://files.pythonhosted.org/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", size = 186405, upload-time = "2026-07-06T21:34:24.857Z" }, +] + +[[package]] +name = "cfn-lint" +version = "1.53.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aws-sam-translator" }, + { name = "jsonpatch" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "sympy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/93/36cea246a0ec155eaa16444386675260bbfd38d259cfa26deffc8f417628/cfn_lint-1.53.0.tar.gz", hash = "sha256:dcf285939dea3c15e06bcb23a817bbdecb6c3e0ab19d98147877f04e02b5b07c", size = 4517403, upload-time = "2026-07-09T18:09:16.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/eb/f9e2ce6888b5e77072169d884afcd957e69d3f293b03b4eb4dfad48fe358/cfn_lint-1.53.0-py3-none-any.whl", hash = "sha256:8a91d1f0d8d18614410a0c1676213df8cd0ac55fa462f3f6369fb9fdaa7ae28d", size = 5049598, upload-time = "2026-07-09T18:09:14.108Z" }, ] [[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/c2/ca/9a0983dd5c8e9733565cf3db4df2b0a2e9a82659fd8aa2a868ac6e4a991f/charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05", size = 207520, upload-time = "2025-08-09T07:57:11.026Z" }, - { url = "https://files.pythonhosted.org/packages/39/c6/99271dc37243a4f925b09090493fb96c9333d7992c6187f5cfe5312008d2/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e", size = 147307, upload-time = "2025-08-09T07:57:12.4Z" }, - { url = "https://files.pythonhosted.org/packages/e4/69/132eab043356bba06eb333cc2cc60c6340857d0a2e4ca6dc2b51312886b3/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99", size = 160448, upload-time = "2025-08-09T07:57:13.712Z" }, - { url = "https://files.pythonhosted.org/packages/04/9a/914d294daa4809c57667b77470533e65def9c0be1ef8b4c1183a99170e9d/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7", size = 157758, upload-time = "2025-08-09T07:57:14.979Z" }, - { url = "https://files.pythonhosted.org/packages/b0/a8/6f5bcf1bcf63cb45625f7c5cadca026121ff8a6c8a3256d8d8cd59302663/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7", size = 152487, upload-time = "2025-08-09T07:57:16.332Z" }, - { url = "https://files.pythonhosted.org/packages/c4/72/d3d0e9592f4e504f9dea08b8db270821c909558c353dc3b457ed2509f2fb/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19", size = 150054, upload-time = "2025-08-09T07:57:17.576Z" }, - { url = "https://files.pythonhosted.org/packages/20/30/5f64fe3981677fe63fa987b80e6c01042eb5ff653ff7cec1b7bd9268e54e/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312", size = 161703, upload-time = "2025-08-09T07:57:20.012Z" }, - { url = "https://files.pythonhosted.org/packages/e1/ef/dd08b2cac9284fd59e70f7d97382c33a3d0a926e45b15fc21b3308324ffd/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc", size = 159096, upload-time = "2025-08-09T07:57:21.329Z" }, - { url = "https://files.pythonhosted.org/packages/45/8c/dcef87cfc2b3f002a6478f38906f9040302c68aebe21468090e39cde1445/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34", size = 153852, upload-time = "2025-08-09T07:57:22.608Z" }, - { url = "https://files.pythonhosted.org/packages/63/86/9cbd533bd37883d467fcd1bd491b3547a3532d0fbb46de2b99feeebf185e/charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432", size = 99840, upload-time = "2025-08-09T07:57:23.883Z" }, - { url = "https://files.pythonhosted.org/packages/ce/d6/7e805c8e5c46ff9729c49950acc4ee0aeb55efb8b3a56687658ad10c3216/charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca", size = 107438, upload-time = "2025-08-09T07:57:25.287Z" }, - { 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" }, +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/81/8e983840c6e5b93b33c2ba81aa3d52c2e42f0e9a690ce7607a2e61da4a5c/charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a", size = 322240, upload-time = "2026-07-07T14:32:36.236Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/b4319dc3229d8272fba305e206fc0a148e2de8d4087917ce62ae6382f359/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616", size = 216475, upload-time = "2026-07-07T14:32:38.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/33/6c99c1b3e6b8bf730e1bc809b9a2608f224145069114c479a2e9e1494346/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209", size = 238670, upload-time = "2026-07-07T14:32:39.658Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f4/ffbb83546e1f198ecc70ecd372b65cf2b50f9068b380abd67640f17a8e18/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99", size = 233476, upload-time = "2026-07-07T14:32:41.155Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5f/b98b8da398637b551e427e7be922bdec19177dc54d6811dcdaa503f23aac/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8", size = 223817, upload-time = "2026-07-07T14:32:42.592Z" }, + { url = "https://files.pythonhosted.org/packages/36/31/a276bb2e66243072a3fd06fdcab9cbb61a305b02143d70d2bda21d888fa8/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b", size = 207974, upload-time = "2026-07-07T14:32:44.258Z" }, + { url = "https://files.pythonhosted.org/packages/5e/be/7ee4453d7e88dfbc4104ccd34900b9f2c7c17dac22881865fe0e82424a25/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2", size = 221655, upload-time = "2026-07-07T14:32:45.64Z" }, + { url = "https://files.pythonhosted.org/packages/1d/85/181c652953eb5276d198f375b1dd641047392050098100a3a02d6534f657/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9", size = 219229, upload-time = "2026-07-07T14:32:47.376Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e7/aaf6da33fc9f4691cda8f7efbc9f69179d3d39ec8a4799baf273ee1d8db0/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15", size = 209704, upload-time = "2026-07-07T14:32:48.855Z" }, + { url = "https://files.pythonhosted.org/packages/63/01/f2fb3bd3a73be48b173ee0c6aa8d2497af97d5663a8c4c4b491de4c62f7a/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d", size = 226243, upload-time = "2026-07-07T14:32:50.239Z" }, + { url = "https://files.pythonhosted.org/packages/c4/02/c57a22739fe05246b0b5783b3bfb6afaac4eebb46f3ececdfb2f048f780e/charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381", size = 150935, upload-time = "2026-07-07T14:32:51.676Z" }, + { url = "https://files.pythonhosted.org/packages/37/8d/ca39a7559a4797505530d084fd3a49a2c959efbbbff146302fb7be4e3b35/charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee", size = 162314, upload-time = "2026-07-07T14:32:53.193Z" }, + { url = "https://files.pythonhosted.org/packages/01/da/a44bd7a13d426e69e4894557106cd58669097bfad4a8681123b618fbfc5d/charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419", size = 153075, upload-time = "2026-07-07T14:32:54.554Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" }, + { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" }, + { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" }, + { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" }, + { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" }, + { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, + { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, + { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, + { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, ] [[package]] name = "cibuildwheel" -version = "2.23.3" +version = "2.23.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "bashlex" }, @@ -378,39 +733,21 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/50/f5/2c06c8229e291e121cb26ed2efa1ba5d89053a93631d8f1d795f2dacabb8/cibuildwheel-2.23.3.tar.gz", hash = "sha256:d85dd15b7eb81711900d8129e67efb32b12f99cc00fc271ab060fa6270c38397", size = 295383, upload-time = "2025-04-26T10:41:28.258Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/17/8e/127e75e087c0a55903deb447a938e97935c6a56bfd20e6070bcc26c06d1b/cibuildwheel-2.23.3-py3-none-any.whl", hash = "sha256:0fa40073ae23a56d5f995d8405e82c1206049999bb89b92aa0835ee62ab8a891", size = 91792, upload-time = "2025-04-26T10:41:26.148Z" }, -] - -[[package]] -name = "click" -version = "8.1.8" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/6c/1934be8118ea842c3ee7542e6e2a0ff9299f632db531f61368b412bb75a7/cibuildwheel-2.23.4.tar.gz", hash = "sha256:5a3e4a3d9c18e77838c37c483642e4f071da06ec05c05336826cc46c97abb708", size = 294641, upload-time = "2026-03-16T19:33:13.649Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, + { url = "https://files.pythonhosted.org/packages/66/00/c9e20a77dafae0ff9b02dfca1629950232278dc327d6231d0a0df128d99c/cibuildwheel-2.23.4-py3-none-any.whl", hash = "sha256:2f4300e9709bff0a83fd89d88f418df2c8da69bf40c010aa4a4b80156be3fbd5", size = 91284, upload-time = "2026-03-16T19:33:12.231Z" }, ] [[package]] name = "click" -version = "8.2.1" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", -] dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, ] [[package]] @@ -424,11 +761,11 @@ wheels = [ [[package]] name = "configargparse" -version = "1.7.1" +version = "1.7.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/4d/6c9ef746dfcc2a32e26f3860bb4a011c008c392b83eabdfb598d1a8bbe5d/configargparse-1.7.1.tar.gz", hash = "sha256:79c2ddae836a1e5914b71d58e4b9adbd9f7779d4e6351a637b7d2d9b6c46d3d9", size = 43958, upload-time = "2025-05-23T14:26:17.369Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/0b/30328302903c55218ffc5199646d0e9d28348ff26c02ba77b2ffc58d294a/configargparse-1.7.5.tar.gz", hash = "sha256:e3f9a7bb6be34d66b2e3c4a2f58e3045f8dfae47b0dc039f87bcfaa0f193fb0f", size = 53548, upload-time = "2026-03-11T02:19:38.144Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/28/d28211d29bcc3620b1fece85a65ce5bb22f18670a03cd28ea4b75ede270c/configargparse-1.7.1-py3-none-any.whl", hash = "sha256:8b586a31f9d873abd1ca527ffbe58863c99f36d896e2829779803125e83be4b6", size = 25607, upload-time = "2025-05-23T14:26:15.923Z" }, + { url = "https://files.pythonhosted.org/packages/fe/19/3ba5e1b0bcc7b91aeab6c258afd70e4907d220fed3972febe38feb40db30/configargparse-1.7.5-py3-none-any.whl", hash = "sha256:1e63fdffedf94da9cd435fc13a1cd24777e76879dd2343912c1f871d4ac8c592", size = 27692, upload-time = "2026-03-11T02:19:36.442Z" }, ] [[package]] @@ -442,97 +779,100 @@ wheels = [ [[package]] name = "coverage" -version = "7.10.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/14/70/025b179c993f019105b79575ac6edb5e084fb0f0e63f15cdebef4e454fb5/coverage-7.10.6.tar.gz", hash = "sha256:f644a3ae5933a552a29dbb9aa2f90c677a875f80ebea028e5a52a4f429044b90", size = 823736, upload-time = "2025-08-29T15:35:16.668Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/1d/2e64b43d978b5bd184e0756a41415597dfef30fcbd90b747474bd749d45f/coverage-7.10.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:70e7bfbd57126b5554aa482691145f798d7df77489a177a6bef80de78860a356", size = 217025, upload-time = "2025-08-29T15:32:57.169Z" }, - { url = "https://files.pythonhosted.org/packages/23/62/b1e0f513417c02cc10ef735c3ee5186df55f190f70498b3702d516aad06f/coverage-7.10.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e41be6f0f19da64af13403e52f2dec38bbc2937af54df8ecef10850ff8d35301", size = 217419, upload-time = "2025-08-29T15:32:59.908Z" }, - { url = "https://files.pythonhosted.org/packages/e7/16/b800640b7a43e7c538429e4d7223e0a94fd72453a1a048f70bf766f12e96/coverage-7.10.6-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c61fc91ab80b23f5fddbee342d19662f3d3328173229caded831aa0bd7595460", size = 244180, upload-time = "2025-08-29T15:33:01.608Z" }, - { url = "https://files.pythonhosted.org/packages/fb/6f/5e03631c3305cad187eaf76af0b559fff88af9a0b0c180d006fb02413d7a/coverage-7.10.6-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10356fdd33a7cc06e8051413140bbdc6f972137508a3572e3f59f805cd2832fd", size = 245992, upload-time = "2025-08-29T15:33:03.239Z" }, - { url = "https://files.pythonhosted.org/packages/eb/a1/f30ea0fb400b080730125b490771ec62b3375789f90af0bb68bfb8a921d7/coverage-7.10.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80b1695cf7c5ebe7b44bf2521221b9bb8cdf69b1f24231149a7e3eb1ae5fa2fb", size = 247851, upload-time = "2025-08-29T15:33:04.603Z" }, - { url = "https://files.pythonhosted.org/packages/02/8e/cfa8fee8e8ef9a6bb76c7bef039f3302f44e615d2194161a21d3d83ac2e9/coverage-7.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2e4c33e6378b9d52d3454bd08847a8651f4ed23ddbb4a0520227bd346382bbc6", size = 245891, upload-time = "2025-08-29T15:33:06.176Z" }, - { url = "https://files.pythonhosted.org/packages/93/a9/51be09b75c55c4f6c16d8d73a6a1d46ad764acca0eab48fa2ffaef5958fe/coverage-7.10.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c8a3ec16e34ef980a46f60dc6ad86ec60f763c3f2fa0db6d261e6e754f72e945", size = 243909, upload-time = "2025-08-29T15:33:07.74Z" }, - { url = "https://files.pythonhosted.org/packages/e9/a6/ba188b376529ce36483b2d585ca7bdac64aacbe5aa10da5978029a9c94db/coverage-7.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7d79dabc0a56f5af990cc6da9ad1e40766e82773c075f09cc571e2076fef882e", size = 244786, upload-time = "2025-08-29T15:33:08.965Z" }, - { url = "https://files.pythonhosted.org/packages/d0/4c/37ed872374a21813e0d3215256180c9a382c3f5ced6f2e5da0102fc2fd3e/coverage-7.10.6-cp310-cp310-win32.whl", hash = "sha256:86b9b59f2b16e981906e9d6383eb6446d5b46c278460ae2c36487667717eccf1", size = 219521, upload-time = "2025-08-29T15:33:10.599Z" }, - { url = "https://files.pythonhosted.org/packages/8e/36/9311352fdc551dec5b973b61f4e453227ce482985a9368305880af4f85dd/coverage-7.10.6-cp310-cp310-win_amd64.whl", hash = "sha256:e132b9152749bd33534e5bd8565c7576f135f157b4029b975e15ee184325f528", size = 220417, upload-time = "2025-08-29T15:33:11.907Z" }, - { url = "https://files.pythonhosted.org/packages/d4/16/2bea27e212c4980753d6d563a0803c150edeaaddb0771a50d2afc410a261/coverage-7.10.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c706db3cabb7ceef779de68270150665e710b46d56372455cd741184f3868d8f", size = 217129, upload-time = "2025-08-29T15:33:13.575Z" }, - { url = "https://files.pythonhosted.org/packages/2a/51/e7159e068831ab37e31aac0969d47b8c5ee25b7d307b51e310ec34869315/coverage-7.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e0c38dc289e0508ef68ec95834cb5d2e96fdbe792eaccaa1bccac3966bbadcc", size = 217532, upload-time = "2025-08-29T15:33:14.872Z" }, - { url = "https://files.pythonhosted.org/packages/e7/c0/246ccbea53d6099325d25cd208df94ea435cd55f0db38099dd721efc7a1f/coverage-7.10.6-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:752a3005a1ded28f2f3a6e8787e24f28d6abe176ca64677bcd8d53d6fe2ec08a", size = 247931, upload-time = "2025-08-29T15:33:16.142Z" }, - { url = "https://files.pythonhosted.org/packages/7d/fb/7435ef8ab9b2594a6e3f58505cc30e98ae8b33265d844007737946c59389/coverage-7.10.6-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:689920ecfd60f992cafca4f5477d55720466ad2c7fa29bb56ac8d44a1ac2b47a", size = 249864, upload-time = "2025-08-29T15:33:17.434Z" }, - { url = "https://files.pythonhosted.org/packages/51/f8/d9d64e8da7bcddb094d511154824038833c81e3a039020a9d6539bf303e9/coverage-7.10.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec98435796d2624d6905820a42f82149ee9fc4f2d45c2c5bc5a44481cc50db62", size = 251969, upload-time = "2025-08-29T15:33:18.822Z" }, - { url = "https://files.pythonhosted.org/packages/43/28/c43ba0ef19f446d6463c751315140d8f2a521e04c3e79e5c5fe211bfa430/coverage-7.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b37201ce4a458c7a758ecc4efa92fa8ed783c66e0fa3c42ae19fc454a0792153", size = 249659, upload-time = "2025-08-29T15:33:20.407Z" }, - { url = "https://files.pythonhosted.org/packages/79/3e/53635bd0b72beaacf265784508a0b386defc9ab7fad99ff95f79ce9db555/coverage-7.10.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2904271c80898663c810a6b067920a61dd8d38341244a3605bd31ab55250dad5", size = 247714, upload-time = "2025-08-29T15:33:21.751Z" }, - { url = "https://files.pythonhosted.org/packages/4c/55/0964aa87126624e8c159e32b0bc4e84edef78c89a1a4b924d28dd8265625/coverage-7.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5aea98383463d6e1fa4e95416d8de66f2d0cb588774ee20ae1b28df826bcb619", size = 248351, upload-time = "2025-08-29T15:33:23.105Z" }, - { url = "https://files.pythonhosted.org/packages/eb/ab/6cfa9dc518c6c8e14a691c54e53a9433ba67336c760607e299bfcf520cb1/coverage-7.10.6-cp311-cp311-win32.whl", hash = "sha256:e3fb1fa01d3598002777dd259c0c2e6d9d5e10e7222976fc8e03992f972a2cba", size = 219562, upload-time = "2025-08-29T15:33:24.717Z" }, - { url = "https://files.pythonhosted.org/packages/5b/18/99b25346690cbc55922e7cfef06d755d4abee803ef335baff0014268eff4/coverage-7.10.6-cp311-cp311-win_amd64.whl", hash = "sha256:f35ed9d945bece26553d5b4c8630453169672bea0050a564456eb88bdffd927e", size = 220453, upload-time = "2025-08-29T15:33:26.482Z" }, - { url = "https://files.pythonhosted.org/packages/d8/ed/81d86648a07ccb124a5cf1f1a7788712b8d7216b593562683cd5c9b0d2c1/coverage-7.10.6-cp311-cp311-win_arm64.whl", hash = "sha256:99e1a305c7765631d74b98bf7dbf54eeea931f975e80f115437d23848ee8c27c", size = 219127, upload-time = "2025-08-29T15:33:27.777Z" }, - { url = "https://files.pythonhosted.org/packages/26/06/263f3305c97ad78aab066d116b52250dd316e74fcc20c197b61e07eb391a/coverage-7.10.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5b2dd6059938063a2c9fee1af729d4f2af28fd1a545e9b7652861f0d752ebcea", size = 217324, upload-time = "2025-08-29T15:33:29.06Z" }, - { url = "https://files.pythonhosted.org/packages/e9/60/1e1ded9a4fe80d843d7d53b3e395c1db3ff32d6c301e501f393b2e6c1c1f/coverage-7.10.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:388d80e56191bf846c485c14ae2bc8898aa3124d9d35903fef7d907780477634", size = 217560, upload-time = "2025-08-29T15:33:30.748Z" }, - { url = "https://files.pythonhosted.org/packages/b8/25/52136173c14e26dfed8b106ed725811bb53c30b896d04d28d74cb64318b3/coverage-7.10.6-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:90cb5b1a4670662719591aa92d0095bb41714970c0b065b02a2610172dbf0af6", size = 249053, upload-time = "2025-08-29T15:33:32.041Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1d/ae25a7dc58fcce8b172d42ffe5313fc267afe61c97fa872b80ee72d9515a/coverage-7.10.6-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:961834e2f2b863a0e14260a9a273aff07ff7818ab6e66d2addf5628590c628f9", size = 251802, upload-time = "2025-08-29T15:33:33.625Z" }, - { url = "https://files.pythonhosted.org/packages/f5/7a/1f561d47743710fe996957ed7c124b421320f150f1d38523d8d9102d3e2a/coverage-7.10.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf9a19f5012dab774628491659646335b1928cfc931bf8d97b0d5918dd58033c", size = 252935, upload-time = "2025-08-29T15:33:34.909Z" }, - { url = "https://files.pythonhosted.org/packages/6c/ad/8b97cd5d28aecdfde792dcbf646bac141167a5cacae2cd775998b45fabb5/coverage-7.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99c4283e2a0e147b9c9cc6bc9c96124de9419d6044837e9799763a0e29a7321a", size = 250855, upload-time = "2025-08-29T15:33:36.922Z" }, - { url = "https://files.pythonhosted.org/packages/33/6a/95c32b558d9a61858ff9d79580d3877df3eb5bc9eed0941b1f187c89e143/coverage-7.10.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:282b1b20f45df57cc508c1e033403f02283adfb67d4c9c35a90281d81e5c52c5", size = 248974, upload-time = "2025-08-29T15:33:38.175Z" }, - { url = "https://files.pythonhosted.org/packages/0d/9c/8ce95dee640a38e760d5b747c10913e7a06554704d60b41e73fdea6a1ffd/coverage-7.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cdbe264f11afd69841bd8c0d83ca10b5b32853263ee62e6ac6a0ab63895f972", size = 250409, upload-time = "2025-08-29T15:33:39.447Z" }, - { url = "https://files.pythonhosted.org/packages/04/12/7a55b0bdde78a98e2eb2356771fd2dcddb96579e8342bb52aa5bc52e96f0/coverage-7.10.6-cp312-cp312-win32.whl", hash = "sha256:a517feaf3a0a3eca1ee985d8373135cfdedfbba3882a5eab4362bda7c7cf518d", size = 219724, upload-time = "2025-08-29T15:33:41.172Z" }, - { url = "https://files.pythonhosted.org/packages/36/4a/32b185b8b8e327802c9efce3d3108d2fe2d9d31f153a0f7ecfd59c773705/coverage-7.10.6-cp312-cp312-win_amd64.whl", hash = "sha256:856986eadf41f52b214176d894a7de05331117f6035a28ac0016c0f63d887629", size = 220536, upload-time = "2025-08-29T15:33:42.524Z" }, - { url = "https://files.pythonhosted.org/packages/08/3a/d5d8dc703e4998038c3099eaf77adddb00536a3cec08c8dcd556a36a3eb4/coverage-7.10.6-cp312-cp312-win_arm64.whl", hash = "sha256:acf36b8268785aad739443fa2780c16260ee3fa09d12b3a70f772ef100939d80", size = 219171, upload-time = "2025-08-29T15:33:43.974Z" }, - { url = "https://files.pythonhosted.org/packages/bd/e7/917e5953ea29a28c1057729c1d5af9084ab6d9c66217523fd0e10f14d8f6/coverage-7.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ffea0575345e9ee0144dfe5701aa17f3ba546f8c3bb48db62ae101afb740e7d6", size = 217351, upload-time = "2025-08-29T15:33:45.438Z" }, - { url = "https://files.pythonhosted.org/packages/eb/86/2e161b93a4f11d0ea93f9bebb6a53f113d5d6e416d7561ca41bb0a29996b/coverage-7.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95d91d7317cde40a1c249d6b7382750b7e6d86fad9d8eaf4fa3f8f44cf171e80", size = 217600, upload-time = "2025-08-29T15:33:47.269Z" }, - { url = "https://files.pythonhosted.org/packages/0e/66/d03348fdd8df262b3a7fb4ee5727e6e4936e39e2f3a842e803196946f200/coverage-7.10.6-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e23dd5408fe71a356b41baa82892772a4cefcf758f2ca3383d2aa39e1b7a003", size = 248600, upload-time = "2025-08-29T15:33:48.953Z" }, - { url = "https://files.pythonhosted.org/packages/73/dd/508420fb47d09d904d962f123221bc249f64b5e56aa93d5f5f7603be475f/coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f3f56e4cb573755e96a16501a98bf211f100463d70275759e73f3cbc00d4f27", size = 251206, upload-time = "2025-08-29T15:33:50.697Z" }, - { url = "https://files.pythonhosted.org/packages/e9/1f/9020135734184f439da85c70ea78194c2730e56c2d18aee6e8ff1719d50d/coverage-7.10.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db4a1d897bbbe7339946ffa2fe60c10cc81c43fab8b062d3fcb84188688174a4", size = 252478, upload-time = "2025-08-29T15:33:52.303Z" }, - { url = "https://files.pythonhosted.org/packages/a4/a4/3d228f3942bb5a2051fde28c136eea23a761177dc4ff4ef54533164ce255/coverage-7.10.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fd7879082953c156d5b13c74aa6cca37f6a6f4747b39538504c3f9c63d043d", size = 250637, upload-time = "2025-08-29T15:33:53.67Z" }, - { url = "https://files.pythonhosted.org/packages/36/e3/293dce8cdb9a83de971637afc59b7190faad60603b40e32635cbd15fbf61/coverage-7.10.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:28395ca3f71cd103b8c116333fa9db867f3a3e1ad6a084aa3725ae002b6583bc", size = 248529, upload-time = "2025-08-29T15:33:55.022Z" }, - { url = "https://files.pythonhosted.org/packages/90/26/64eecfa214e80dd1d101e420cab2901827de0e49631d666543d0e53cf597/coverage-7.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:61c950fc33d29c91b9e18540e1aed7d9f6787cc870a3e4032493bbbe641d12fc", size = 250143, upload-time = "2025-08-29T15:33:56.386Z" }, - { url = "https://files.pythonhosted.org/packages/3e/70/bd80588338f65ea5b0d97e424b820fb4068b9cfb9597fbd91963086e004b/coverage-7.10.6-cp313-cp313-win32.whl", hash = "sha256:160c00a5e6b6bdf4e5984b0ef21fc860bc94416c41b7df4d63f536d17c38902e", size = 219770, upload-time = "2025-08-29T15:33:58.063Z" }, - { url = "https://files.pythonhosted.org/packages/a7/14/0b831122305abcc1060c008f6c97bbdc0a913ab47d65070a01dc50293c2b/coverage-7.10.6-cp313-cp313-win_amd64.whl", hash = "sha256:628055297f3e2aa181464c3808402887643405573eb3d9de060d81531fa79d32", size = 220566, upload-time = "2025-08-29T15:33:59.766Z" }, - { url = "https://files.pythonhosted.org/packages/83/c6/81a83778c1f83f1a4a168ed6673eeedc205afb562d8500175292ca64b94e/coverage-7.10.6-cp313-cp313-win_arm64.whl", hash = "sha256:df4ec1f8540b0bcbe26ca7dd0f541847cc8a108b35596f9f91f59f0c060bfdd2", size = 219195, upload-time = "2025-08-29T15:34:01.191Z" }, - { url = "https://files.pythonhosted.org/packages/d7/1c/ccccf4bf116f9517275fa85047495515add43e41dfe8e0bef6e333c6b344/coverage-7.10.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c9a8b7a34a4de3ed987f636f71881cd3b8339f61118b1aa311fbda12741bff0b", size = 218059, upload-time = "2025-08-29T15:34:02.91Z" }, - { url = "https://files.pythonhosted.org/packages/92/97/8a3ceff833d27c7492af4f39d5da6761e9ff624831db9e9f25b3886ddbca/coverage-7.10.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd5af36092430c2b075cee966719898f2ae87b636cefb85a653f1d0ba5d5393", size = 218287, upload-time = "2025-08-29T15:34:05.106Z" }, - { url = "https://files.pythonhosted.org/packages/92/d8/50b4a32580cf41ff0423777a2791aaf3269ab60c840b62009aec12d3970d/coverage-7.10.6-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0353b0f0850d49ada66fdd7d0c7cdb0f86b900bb9e367024fd14a60cecc1e27", size = 259625, upload-time = "2025-08-29T15:34:06.575Z" }, - { url = "https://files.pythonhosted.org/packages/7e/7e/6a7df5a6fb440a0179d94a348eb6616ed4745e7df26bf2a02bc4db72c421/coverage-7.10.6-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d6b9ae13d5d3e8aeca9ca94198aa7b3ebbc5acfada557d724f2a1f03d2c0b0df", size = 261801, upload-time = "2025-08-29T15:34:08.006Z" }, - { url = "https://files.pythonhosted.org/packages/3a/4c/a270a414f4ed5d196b9d3d67922968e768cd971d1b251e1b4f75e9362f75/coverage-7.10.6-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:675824a363cc05781b1527b39dc2587b8984965834a748177ee3c37b64ffeafb", size = 264027, upload-time = "2025-08-29T15:34:09.806Z" }, - { url = "https://files.pythonhosted.org/packages/9c/8b/3210d663d594926c12f373c5370bf1e7c5c3a427519a8afa65b561b9a55c/coverage-7.10.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:692d70ea725f471a547c305f0d0fc6a73480c62fb0da726370c088ab21aed282", size = 261576, upload-time = "2025-08-29T15:34:11.585Z" }, - { url = "https://files.pythonhosted.org/packages/72/d0/e1961eff67e9e1dba3fc5eb7a4caf726b35a5b03776892da8d79ec895775/coverage-7.10.6-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:851430a9a361c7a8484a36126d1d0ff8d529d97385eacc8dfdc9bfc8c2d2cbe4", size = 259341, upload-time = "2025-08-29T15:34:13.159Z" }, - { url = "https://files.pythonhosted.org/packages/3a/06/d6478d152cd189b33eac691cba27a40704990ba95de49771285f34a5861e/coverage-7.10.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d9369a23186d189b2fc95cc08b8160ba242057e887d766864f7adf3c46b2df21", size = 260468, upload-time = "2025-08-29T15:34:14.571Z" }, - { url = "https://files.pythonhosted.org/packages/ed/73/737440247c914a332f0b47f7598535b29965bf305e19bbc22d4c39615d2b/coverage-7.10.6-cp313-cp313t-win32.whl", hash = "sha256:92be86fcb125e9bda0da7806afd29a3fd33fdf58fba5d60318399adf40bf37d0", size = 220429, upload-time = "2025-08-29T15:34:16.394Z" }, - { url = "https://files.pythonhosted.org/packages/bd/76/b92d3214740f2357ef4a27c75a526eb6c28f79c402e9f20a922c295c05e2/coverage-7.10.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6b3039e2ca459a70c79523d39347d83b73f2f06af5624905eba7ec34d64d80b5", size = 221493, upload-time = "2025-08-29T15:34:17.835Z" }, - { url = "https://files.pythonhosted.org/packages/fc/8e/6dcb29c599c8a1f654ec6cb68d76644fe635513af16e932d2d4ad1e5ac6e/coverage-7.10.6-cp313-cp313t-win_arm64.whl", hash = "sha256:3fb99d0786fe17b228eab663d16bee2288e8724d26a199c29325aac4b0319b9b", size = 219757, upload-time = "2025-08-29T15:34:19.248Z" }, - { url = "https://files.pythonhosted.org/packages/d3/aa/76cf0b5ec00619ef208da4689281d48b57f2c7fde883d14bf9441b74d59f/coverage-7.10.6-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6008a021907be8c4c02f37cdc3ffb258493bdebfeaf9a839f9e71dfdc47b018e", size = 217331, upload-time = "2025-08-29T15:34:20.846Z" }, - { url = "https://files.pythonhosted.org/packages/65/91/8e41b8c7c505d398d7730206f3cbb4a875a35ca1041efc518051bfce0f6b/coverage-7.10.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5e75e37f23eb144e78940b40395b42f2321951206a4f50e23cfd6e8a198d3ceb", size = 217607, upload-time = "2025-08-29T15:34:22.433Z" }, - { url = "https://files.pythonhosted.org/packages/87/7f/f718e732a423d442e6616580a951b8d1ec3575ea48bcd0e2228386805e79/coverage-7.10.6-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0f7cb359a448e043c576f0da00aa8bfd796a01b06aa610ca453d4dde09cc1034", size = 248663, upload-time = "2025-08-29T15:34:24.425Z" }, - { url = "https://files.pythonhosted.org/packages/e6/52/c1106120e6d801ac03e12b5285e971e758e925b6f82ee9b86db3aa10045d/coverage-7.10.6-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c68018e4fc4e14b5668f1353b41ccf4bc83ba355f0e1b3836861c6f042d89ac1", size = 251197, upload-time = "2025-08-29T15:34:25.906Z" }, - { url = "https://files.pythonhosted.org/packages/3d/ec/3a8645b1bb40e36acde9c0609f08942852a4af91a937fe2c129a38f2d3f5/coverage-7.10.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd4b2b0707fc55afa160cd5fc33b27ccbf75ca11d81f4ec9863d5793fc6df56a", size = 252551, upload-time = "2025-08-29T15:34:27.337Z" }, - { url = "https://files.pythonhosted.org/packages/a1/70/09ecb68eeb1155b28a1d16525fd3a9b65fbe75337311a99830df935d62b6/coverage-7.10.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cec13817a651f8804a86e4f79d815b3b28472c910e099e4d5a0e8a3b6a1d4cb", size = 250553, upload-time = "2025-08-29T15:34:29.065Z" }, - { url = "https://files.pythonhosted.org/packages/c6/80/47df374b893fa812e953b5bc93dcb1427a7b3d7a1a7d2db33043d17f74b9/coverage-7.10.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f2a6a8e06bbda06f78739f40bfb56c45d14eb8249d0f0ea6d4b3d48e1f7c695d", size = 248486, upload-time = "2025-08-29T15:34:30.897Z" }, - { url = "https://files.pythonhosted.org/packages/4a/65/9f98640979ecee1b0d1a7164b589de720ddf8100d1747d9bbdb84be0c0fb/coverage-7.10.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:081b98395ced0d9bcf60ada7661a0b75f36b78b9d7e39ea0790bb4ed8da14747", size = 249981, upload-time = "2025-08-29T15:34:32.365Z" }, - { url = "https://files.pythonhosted.org/packages/1f/55/eeb6603371e6629037f47bd25bef300387257ed53a3c5fdb159b7ac8c651/coverage-7.10.6-cp314-cp314-win32.whl", hash = "sha256:6937347c5d7d069ee776b2bf4e1212f912a9f1f141a429c475e6089462fcecc5", size = 220054, upload-time = "2025-08-29T15:34:34.124Z" }, - { url = "https://files.pythonhosted.org/packages/15/d1/a0912b7611bc35412e919a2cd59ae98e7ea3b475e562668040a43fb27897/coverage-7.10.6-cp314-cp314-win_amd64.whl", hash = "sha256:adec1d980fa07e60b6ef865f9e5410ba760e4e1d26f60f7e5772c73b9a5b0713", size = 220851, upload-time = "2025-08-29T15:34:35.651Z" }, - { url = "https://files.pythonhosted.org/packages/ef/2d/11880bb8ef80a45338e0b3e0725e4c2d73ffbb4822c29d987078224fd6a5/coverage-7.10.6-cp314-cp314-win_arm64.whl", hash = "sha256:a80f7aef9535442bdcf562e5a0d5a5538ce8abe6bb209cfbf170c462ac2c2a32", size = 219429, upload-time = "2025-08-29T15:34:37.16Z" }, - { url = "https://files.pythonhosted.org/packages/83/c0/1f00caad775c03a700146f55536ecd097a881ff08d310a58b353a1421be0/coverage-7.10.6-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:0de434f4fbbe5af4fa7989521c655c8c779afb61c53ab561b64dcee6149e4c65", size = 218080, upload-time = "2025-08-29T15:34:38.919Z" }, - { url = "https://files.pythonhosted.org/packages/a9/c4/b1c5d2bd7cc412cbeb035e257fd06ed4e3e139ac871d16a07434e145d18d/coverage-7.10.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6e31b8155150c57e5ac43ccd289d079eb3f825187d7c66e755a055d2c85794c6", size = 218293, upload-time = "2025-08-29T15:34:40.425Z" }, - { url = "https://files.pythonhosted.org/packages/3f/07/4468d37c94724bf6ec354e4ec2f205fda194343e3e85fd2e59cec57e6a54/coverage-7.10.6-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:98cede73eb83c31e2118ae8d379c12e3e42736903a8afcca92a7218e1f2903b0", size = 259800, upload-time = "2025-08-29T15:34:41.996Z" }, - { url = "https://files.pythonhosted.org/packages/82/d8/f8fb351be5fee31690cd8da768fd62f1cfab33c31d9f7baba6cd8960f6b8/coverage-7.10.6-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f863c08f4ff6b64fa8045b1e3da480f5374779ef187f07b82e0538c68cb4ff8e", size = 261965, upload-time = "2025-08-29T15:34:43.61Z" }, - { url = "https://files.pythonhosted.org/packages/e8/70/65d4d7cfc75c5c6eb2fed3ee5cdf420fd8ae09c4808723a89a81d5b1b9c3/coverage-7.10.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b38261034fda87be356f2c3f42221fdb4171c3ce7658066ae449241485390d5", size = 264220, upload-time = "2025-08-29T15:34:45.387Z" }, - { url = "https://files.pythonhosted.org/packages/98/3c/069df106d19024324cde10e4ec379fe2fb978017d25e97ebee23002fbadf/coverage-7.10.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e93b1476b79eae849dc3872faeb0bf7948fd9ea34869590bc16a2a00b9c82a7", size = 261660, upload-time = "2025-08-29T15:34:47.288Z" }, - { url = "https://files.pythonhosted.org/packages/fc/8a/2974d53904080c5dc91af798b3a54a4ccb99a45595cc0dcec6eb9616a57d/coverage-7.10.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ff8a991f70f4c0cf53088abf1e3886edcc87d53004c7bb94e78650b4d3dac3b5", size = 259417, upload-time = "2025-08-29T15:34:48.779Z" }, - { url = "https://files.pythonhosted.org/packages/30/38/9616a6b49c686394b318974d7f6e08f38b8af2270ce7488e879888d1e5db/coverage-7.10.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ac765b026c9f33044419cbba1da913cfb82cca1b60598ac1c7a5ed6aac4621a0", size = 260567, upload-time = "2025-08-29T15:34:50.718Z" }, - { url = "https://files.pythonhosted.org/packages/76/16/3ed2d6312b371a8cf804abf4e14895b70e4c3491c6e53536d63fd0958a8d/coverage-7.10.6-cp314-cp314t-win32.whl", hash = "sha256:441c357d55f4936875636ef2cfb3bee36e466dcf50df9afbd398ce79dba1ebb7", size = 220831, upload-time = "2025-08-29T15:34:52.653Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e5/d38d0cb830abede2adb8b147770d2a3d0e7fecc7228245b9b1ae6c24930a/coverage-7.10.6-cp314-cp314t-win_amd64.whl", hash = "sha256:073711de3181b2e204e4870ac83a7c4853115b42e9cd4d145f2231e12d670930", size = 221950, upload-time = "2025-08-29T15:34:54.212Z" }, - { url = "https://files.pythonhosted.org/packages/f4/51/e48e550f6279349895b0ffcd6d2a690e3131ba3a7f4eafccc141966d4dea/coverage-7.10.6-cp314-cp314t-win_arm64.whl", hash = "sha256:137921f2bac5559334ba66122b753db6dc5d1cf01eb7b64eb412bb0d064ef35b", size = 219969, upload-time = "2025-08-29T15:34:55.83Z" }, - { url = "https://files.pythonhosted.org/packages/91/70/f73ad83b1d2fd2d5825ac58c8f551193433a7deaf9b0d00a8b69ef61cd9a/coverage-7.10.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90558c35af64971d65fbd935c32010f9a2f52776103a259f1dee865fe8259352", size = 217009, upload-time = "2025-08-29T15:34:57.381Z" }, - { url = "https://files.pythonhosted.org/packages/01/e8/099b55cd48922abbd4b01ddd9ffa352408614413ebfc965501e981aced6b/coverage-7.10.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8953746d371e5695405806c46d705a3cd170b9cc2b9f93953ad838f6c1e58612", size = 217400, upload-time = "2025-08-29T15:34:58.985Z" }, - { url = "https://files.pythonhosted.org/packages/ee/d1/c6bac7c9e1003110a318636fef3b5c039df57ab44abcc41d43262a163c28/coverage-7.10.6-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c83f6afb480eae0313114297d29d7c295670a41c11b274e6bca0c64540c1ce7b", size = 243835, upload-time = "2025-08-29T15:35:00.541Z" }, - { url = "https://files.pythonhosted.org/packages/01/f9/82c6c061838afbd2172e773156c0aa84a901d59211b4975a4e93accf5c89/coverage-7.10.6-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7eb68d356ba0cc158ca535ce1381dbf2037fa8cb5b1ae5ddfc302e7317d04144", size = 245658, upload-time = "2025-08-29T15:35:02.135Z" }, - { url = "https://files.pythonhosted.org/packages/81/6a/35674445b1d38161148558a3ff51b0aa7f0b54b1def3abe3fbd34efe05bc/coverage-7.10.6-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b15a87265e96307482746d86995f4bff282f14b027db75469c446da6127433b", size = 247433, upload-time = "2025-08-29T15:35:03.777Z" }, - { url = "https://files.pythonhosted.org/packages/18/27/98c99e7cafb288730a93535092eb433b5503d529869791681c4f2e2012a8/coverage-7.10.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fc53ba868875bfbb66ee447d64d6413c2db91fddcfca57025a0e7ab5b07d5862", size = 245315, upload-time = "2025-08-29T15:35:05.629Z" }, - { url = "https://files.pythonhosted.org/packages/09/05/123e0dba812408c719c319dea05782433246f7aa7b67e60402d90e847545/coverage-7.10.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:efeda443000aa23f276f4df973cb82beca682fd800bb119d19e80504ffe53ec2", size = 243385, upload-time = "2025-08-29T15:35:07.494Z" }, - { url = "https://files.pythonhosted.org/packages/67/52/d57a42502aef05c6325f28e2e81216c2d9b489040132c18725b7a04d1448/coverage-7.10.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9702b59d582ff1e184945d8b501ffdd08d2cee38d93a2206aa5f1365ce0b8d78", size = 244343, upload-time = "2025-08-29T15:35:09.55Z" }, - { url = "https://files.pythonhosted.org/packages/6b/22/7f6fad7dbb37cf99b542c5e157d463bd96b797078b1ec506691bc836f476/coverage-7.10.6-cp39-cp39-win32.whl", hash = "sha256:2195f8e16ba1a44651ca684db2ea2b2d4b5345da12f07d9c22a395202a05b23c", size = 219530, upload-time = "2025-08-29T15:35:11.167Z" }, - { url = "https://files.pythonhosted.org/packages/62/30/e2fda29bfe335026027e11e6a5e57a764c9df13127b5cf42af4c3e99b937/coverage-7.10.6-cp39-cp39-win_amd64.whl", hash = "sha256:f32ff80e7ef6a5b5b606ea69a36e97b219cd9dc799bcf2963018a4d8f788cfbf", size = 220432, upload-time = "2025-08-29T15:35:12.902Z" }, - { url = "https://files.pythonhosted.org/packages/44/0c/50db5379b615854b5cf89146f8f5bd1d5a9693d7f3a987e269693521c404/coverage-7.10.6-py3-none-any.whl", hash = "sha256:92c4ecf6bf11b2e85fd4d8204814dc26e6a19f0c9d938c207c5cb0eadfcabbe3", size = 208986, upload-time = "2025-08-29T15:35:14.506Z" }, +version = "7.15.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d0/55fe630f4cf94e3fcba868240fad8c8cdd1f764e2a932f8926347e6ec4cd/coverage-7.15.2.tar.gz", hash = "sha256:3df60dc267f0a2ca23cb7a9ab1109c62b9335ffbf519fcfe167157c28c09b81d", size = 927741, upload-time = "2026-07-15T18:56:19.558Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/03/060ce69008ac97bbc01b1411b3e55b61f6f015659400b46749b662107831/coverage-7.15.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b5bd92ff1ec22e535eab0de75fa6db021992791f461a2aceb7822c625a1187d", size = 221284, upload-time = "2026-07-15T18:53:29.52Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a3/d936e8b53edd9684100a6aefaf3fcabaa54728fe33324436c8d279c047aa/coverage-7.15.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:44826758cfe73fcd0e6af5deb4ba6d5417cc1d13df3acb35c93484a11160f846", size = 221799, upload-time = "2026-07-15T18:53:31.708Z" }, + { url = "https://files.pythonhosted.org/packages/ae/a3/ca234b06aec7ee28226f11d39a696b4481fe5eddfce8e03bf39979bb8ffb/coverage-7.15.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:09f5c6ec5901f667bd97dd140b5b9a2586b10efec66f46fb1e6d8135f8b95bdf", size = 248544, upload-time = "2026-07-15T18:53:33.212Z" }, + { url = "https://files.pythonhosted.org/packages/2b/89/dda79527bb7573ba91828b2fb91b3105d87378d6a2749ca0c0924ce0addd/coverage-7.15.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1d16e3a7104ea84f03e614611b3edbf6fb6892554b3ab0fe7fbb3f2b2ef04376", size = 250374, upload-time = "2026-07-15T18:53:34.683Z" }, + { url = "https://files.pythonhosted.org/packages/67/c6/c33755a34572f81f49a8c0cdf6b622f35ccb3238b136e1909daf0cdd4319/coverage-7.15.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d46e62cb35d91e6e2589fda6d28074426b0e276422b5d2ebef2c6b11dc60dbfd", size = 252239, upload-time = "2026-07-15T18:53:36.205Z" }, + { url = "https://files.pythonhosted.org/packages/b9/6f/dc341741b375be53a5baeee5b4bf0f0e525d38caed428f7932d23bb7bcb1/coverage-7.15.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dfd3db045e95960ae3683059571e597fda7cc610106a8916f77c5839048c1deb", size = 254150, upload-time = "2026-07-15T18:53:37.863Z" }, + { url = "https://files.pythonhosted.org/packages/e9/8d/966a18a5b195cb4e77b14c53f5f3dce22b5da05e6de7fafd1e08f2d2067a/coverage-7.15.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:affd532502d34c0472d0cdb181325c89f1d2c44992fef0c17e88e7b1576259a1", size = 249234, upload-time = "2026-07-15T18:53:39.394Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8b/8b2e367496ab48484d48e79984fec76cdc1b7cb5d3a00ee799a5602e3ec9/coverage-7.15.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d17d7512151fedfcc64c1821a8977fc9be0dbf495754669afcab7b57abc98ae9", size = 250276, upload-time = "2026-07-15T18:53:41.027Z" }, + { url = "https://files.pythonhosted.org/packages/63/92/1199318a200eb6c8c6ce0192c892c8710ac791abbe0f35099294620bbfda/coverage-7.15.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e26ff680768b8095e8874aabe0e9d3a47a2a9f176a8340d05f8604c56457c23a", size = 248283, upload-time = "2026-07-15T18:53:42.557Z" }, + { url = "https://files.pythonhosted.org/packages/56/da/be284a55c5619bda891a89c27dfd59324a2c6a14d755cf6aac6960ceebeb/coverage-7.15.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:7e8f27131dc7cd53de2c137dd207b3720919320b3c20d499dc30aa9ee6173287", size = 252093, upload-time = "2026-07-15T18:53:44.271Z" }, + { url = "https://files.pythonhosted.org/packages/d4/53/ee112da833ddd77b73c6d781a98029b45b584b136615b4900ed0569f887e/coverage-7.15.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:728a33676d4c3f0db977990a4bd421dcaa3be3e53b5b6273036fff6666008e89", size = 248552, upload-time = "2026-07-15T18:53:45.7Z" }, + { url = "https://files.pythonhosted.org/packages/82/6a/802cfc802e9113494c80bf3f284cd4d72faeb1f24e244f61046af364f2ca/coverage-7.15.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:29c052f7c83ccfcc5c577eaae025d2e4a9bb80daf03c0ac31c996e83b000ce88", size = 249154, upload-time = "2026-07-15T18:53:47.256Z" }, + { url = "https://files.pythonhosted.org/packages/2c/65/529808e91d651147edae408fd9e894abc3b8cad7f3e594bbc36719a3e13a/coverage-7.15.2-cp310-cp310-win32.whl", hash = "sha256:1268ac8fb9ddcd783d3948dbabaf80a5d53bfdaa0575e873e2139a692f797443", size = 223334, upload-time = "2026-07-15T18:53:48.768Z" }, + { url = "https://files.pythonhosted.org/packages/68/0f/0e1829d7001130876dfbc0b4e1c737ea7c155b809e3e4a98a0aa268e2369/coverage-7.15.2-cp310-cp310-win_amd64.whl", hash = "sha256:9f4432898c4bf2fba0435bbe35dd4437d7264565e5a88a21f5b49d8662a6b629", size = 223959, upload-time = "2026-07-15T18:53:50.429Z" }, + { url = "https://files.pythonhosted.org/packages/7d/3a/54536704f507d4573bf9161c4d0dd3dd59b6d85e48c664e901b6844d8e33/coverage-7.15.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f1ec6f304b156669cfde653b4e9a953f5de87e247ea02ac599bce0ab2744036", size = 221414, upload-time = "2026-07-15T18:53:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d9/8ba925d29743e3577b21e4d8c11a702b76bc93c41e7fdfd1177af63d4b8d/coverage-7.15.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d3361879d736f469f45723c11ea1a5bbdaf1f6928f0e632c940378b5aa9b660", size = 221913, upload-time = "2026-07-15T18:53:53.682Z" }, + { url = "https://files.pythonhosted.org/packages/09/54/a855f3aa0187f2b431ade4e4791b77b56282cfb5d201c83ec26a31b5b36a/coverage-7.15.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c6a98d698f9e2c8008d0370ec7fc452ebfcc530002ae2d0061170d768b992589", size = 252332, upload-time = "2026-07-15T18:53:55.467Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d3/13ac97b4370640ba3452fc8559b06cc2f479ce3ba4a0b632a73e44c38a7d/coverage-7.15.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d50dd325e18ec25bfcc10cd7f99b04df1ab9ec76b0918c260e60817ad0643dee", size = 254243, upload-time = "2026-07-15T18:53:57.055Z" }, + { url = "https://files.pythonhosted.org/packages/88/83/5eca144942d8d0659d3f55176517f4a59cdc65eefd17146a0770935a3ebd/coverage-7.15.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67d7602480a47bdf5b675635403625553ebaa70d5a62a657c035149fd401cea0", size = 256352, upload-time = "2026-07-15T18:53:58.83Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ba/d3db2e01a50fc88cdb4c0f19542bcf6f61489e34dc9aa3538413e2459a38/coverage-7.15.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cee0f89f4767a6057c8fbf168f8135f18be651300496086bd873e3189fed0487", size = 258313, upload-time = "2026-07-15T18:54:00.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/b3/aba83416e9177df28e5186d856c19158c59fc0e7e814aaa61a4a2354ad1b/coverage-7.15.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a29ec5305a7335aacee2d799e3422e91e1c8a12474986e2b3b07e315c91be82f", size = 252449, upload-time = "2026-07-15T18:54:02.456Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a5/4b00ecac0194431ab451b0f6710f8e2517d04cef60f821b14dec4637d575/coverage-7.15.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:48ccc6395958eda89093ecdc35644c86f23a8b23a7f4d44958812b721aad67c1", size = 254043, upload-time = "2026-07-15T18:54:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/75/b6/cfa209b4313ee7f1b34da47efcd789ea51c024ad35af390e00f5a3c10a2e/coverage-7.15.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:81f382c5a94b434ec1f6da607edb904c76d7212e618cd4d1bc9f97bed4120ef5", size = 252107, upload-time = "2026-07-15T18:54:06.745Z" }, + { url = "https://files.pythonhosted.org/packages/36/67/e8cac5a6954038c98d7fe7eb9802afe7ab3ecb637bb7cc00e69b4148b56d/coverage-7.15.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bbc808daf4f5cd567af8075ecc72d21c6dfef9a254709a621a84c217c935ebc0", size = 255873, upload-time = "2026-07-15T18:54:08.48Z" }, + { url = "https://files.pythonhosted.org/packages/2c/92/395cca9f330a86c3fe3471d73e2c102116c4c58fdc619dbbc125c6e93a54/coverage-7.15.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a4c46b247b5d4b78f613bd89fea926d32b25c6cc61a50bd1e99ba310348f3dad", size = 251826, upload-time = "2026-07-15T18:54:10.083Z" }, + { url = "https://files.pythonhosted.org/packages/51/60/3e91b20295439652424f426b7086ec5bf4fbe3f604c73eda22b986c4fd6b/coverage-7.15.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:094dd37f3ef7b2da8b068b583d1f4c40f91c65197e16c52a71962d5d537fc5db", size = 252735, upload-time = "2026-07-15T18:54:11.878Z" }, + { url = "https://files.pythonhosted.org/packages/a5/eb/8c07839005e5e3c6b3877d3a6e2a80ce766589f31dd2b6882b78d59a7b8c/coverage-7.15.2-cp311-cp311-win32.whl", hash = "sha256:a63b9e190711134d581c4d703df5df09851b1acf99792c7aacbbe9f41f0283c9", size = 223500, upload-time = "2026-07-15T18:54:13.525Z" }, + { url = "https://files.pythonhosted.org/packages/2e/98/59d83c257cd59f0fbaf9d9ddb26b744a576760dfd1ae16e516408894a02b/coverage-7.15.2-cp311-cp311-win_amd64.whl", hash = "sha256:8bb9f4b4279187560796a4cdaca3b0a93dd97e48ee667df005f4ed9a97403688", size = 223973, upload-time = "2026-07-15T18:54:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/ea/09/2d285c8bef5c4f695d120c1c96dc11715638aa8e134069f210bb6a62a9fe/coverage-7.15.2-cp311-cp311-win_arm64.whl", hash = "sha256:8c726b232659cbd2ae57ade46509eb068c9bd7a06df9fcbff6fe484870006934", size = 223519, upload-time = "2026-07-15T18:54:16.803Z" }, + { url = "https://files.pythonhosted.org/packages/6a/50/eb5bf42e531611a9f8d272556b1ed4de503f84a91413584094487cf69f8f/coverage-7.15.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1adac78e5abc7c5438f7a209c9ca69d06542f0bf481d728b6989ea80b813fdf9", size = 221587, upload-time = "2026-07-15T18:54:18.439Z" }, + { url = "https://files.pythonhosted.org/packages/06/d1/da99af464c335d4e023a6efcd7ec30f63b88a43c93745154ab74ffb31cea/coverage-7.15.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b868acc62aa5de3be7a9d05c2333bf8359ca987e43f9cb30ff8fbda6a024ab73", size = 221943, upload-time = "2026-07-15T18:54:20.062Z" }, + { url = "https://files.pythonhosted.org/packages/5b/8a/13c42723d61ca447eafa18732e8141dd6a63f2732e1c7e1502c182dd88d7/coverage-7.15.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6f6966fc30e6f06ca8f98fb0ce51eda6b111b3ee8d066a8b1ec9e77fa06ab55d", size = 253450, upload-time = "2026-07-15T18:54:21.765Z" }, + { url = "https://files.pythonhosted.org/packages/d7/29/99021303f98fbdcb63504b4d07bea4cc025b9b2dd907c4f07c85d50a0dab/coverage-7.15.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:68af907f595ab01a78f794932ff3bdf929c316d3000810d38dbc247129e26f8b", size = 256187, upload-time = "2026-07-15T18:54:23.4Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a8/fd503715ed6ca9c5d742923aa5209257340b367a867b2ced0c7d4ba8a0b9/coverage-7.15.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:afa29e2eff3d5729267e2cb2fd4ce9d61c952932fb2694e34ccb5d9540c6a296", size = 257301, upload-time = "2026-07-15T18:54:25.183Z" }, + { url = "https://files.pythonhosted.org/packages/da/40/3f4b8fb409810036ebc2857d36adc0498c6e957b5df0290c5036b2e143f1/coverage-7.15.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bbf44513ceb1589e31948e20eafbde9deaface90e1a1afa5f5f77b4423d17ce6", size = 259562, upload-time = "2026-07-15T18:54:27.204Z" }, + { url = "https://files.pythonhosted.org/packages/0b/8a/9bdffbef47db77cce3d6b02a28f7e919b19f0106c4b080c2c2246040f885/coverage-7.15.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9deddf09eecb717b7f980414b43d90a5b22ff3967d2949ab29cb0aa83d9e9098", size = 253841, upload-time = "2026-07-15T18:54:29.134Z" }, + { url = "https://files.pythonhosted.org/packages/1b/1e/9031efde019d31a06646261fce6dfc5c3c74e951e27a71e5c9a424563178/coverage-7.15.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae901f7e55ba405c84ee1cab3d3e962e4e871e4a2bcb9c90911adbd69b42ac5a", size = 255221, upload-time = "2026-07-15T18:54:31.142Z" }, + { url = "https://files.pythonhosted.org/packages/56/db/787acde872389fc84a9ef9d8cd1ccc658e391ab4cb5b28092a714426a394/coverage-7.15.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a0f47002c6eeb7c280228467a4cb0cc15ca2103a8421b986b2d3ec04a0f9bd8b", size = 253366, upload-time = "2026-07-15T18:54:32.886Z" }, + { url = "https://files.pythonhosted.org/packages/2f/9b/6f57bc4b93c842eef1695f8cdaf2318e35e7ba54f5ba80d84be213ab7858/coverage-7.15.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd7a5beb7af3e864a13b1f0fb26efd3695da43ef0daf71e586adfffaf34d5b2", size = 257434, upload-time = "2026-07-15T18:54:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/88/26/b3186a21b2acc83e451118978905c81c7072c3333707804db09a78c096a2/coverage-7.15.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:97a5c5457a9fb1d6c4e06cfb5dc835871fbfb6a6a51addc9e925bdeff5ef7440", size = 252935, upload-time = "2026-07-15T18:54:36.548Z" }, + { url = "https://files.pythonhosted.org/packages/20/c2/c9f3376b2e717ea69ed7a6e9a5fcab968fb0b290db6cf4bd9a1fc7541b75/coverage-7.15.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0901cfe6c13bcd2302da4f83e884555d2a22bda6e4c476f09ef204ba20ca536e", size = 254807, upload-time = "2026-07-15T18:54:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e1/dfc15401f4a8aaeb486e1ba3e9e3c40522a6e38bd0ecf0b3f29cb8082957/coverage-7.15.2-cp312-cp312-win32.whl", hash = "sha256:b171bdd71cb7ff792bf32e376173b0ace7e7963e7e57c58dfc42063a6a7174cd", size = 223641, upload-time = "2026-07-15T18:54:40.103Z" }, + { url = "https://files.pythonhosted.org/packages/91/40/81b6d809d320cd366ec5bdf8176575e897dcb8efe7fb4b489ef9e93e4d13/coverage-7.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:582edc45c2040543fef83341be23c43024a3ab3ae0c2d8bc498a06282905ad40", size = 224172, upload-time = "2026-07-15T18:54:41.882Z" }, + { url = "https://files.pythonhosted.org/packages/ef/28/9f14ec438149f7de557f45518f09b4a7917b795cc37083aa7db482693f8c/coverage-7.15.2-cp312-cp312-win_arm64.whl", hash = "sha256:a638db90c61cd219aeee65e83a24fdaa57269a741ae0cf773309208ac862cee3", size = 223556, upload-time = "2026-07-15T18:54:43.674Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d5/f8c838e6b7282976f7c918884b792df7a0c42c5bba5d99c60ad2d221d56d/coverage-7.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1121caa19159a38b5463eaae4b1e1fde81e525b15ecc5e000cd5b1a108f743a8", size = 221606, upload-time = "2026-07-15T18:54:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/bf/37/97c926376364f66298cc44893b89cdf17b8bc406376497c4061ae4b8a8ff/coverage-7.15.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a300c6934e0989c327b9e8a1e110329da4641149f872bbe9f70168be66da76c1", size = 221982, upload-time = "2026-07-15T18:54:47.341Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/a36050a6e83c2135ee0776f452ca3948224befc6d7f26acecc082d0c106a/coverage-7.15.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2617f8799d268fabdeef42a7e89ac3a23e1deee9025427db2df970f99a89a578", size = 252972, upload-time = "2026-07-15T18:54:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/31/d3/06b5f1daf95f0f15ab05bd75f26ba5f3c8b33d0bb72f3aaa3cf41d1bad3a/coverage-7.15.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7dc2950a2992cd676d35c20ae63522836deeb034f08874699d14068710af3dc1", size = 255569, upload-time = "2026-07-15T18:54:51.098Z" }, + { url = "https://files.pythonhosted.org/packages/81/1c/9afb3f8de2b8d36960391c48559a2e3ff96594b58099f115921549ea8d0d/coverage-7.15.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e36686f7a442185db2400b3df171aac520869faf9deb59df687d28659eda2a6", size = 256806, upload-time = "2026-07-15T18:54:53.145Z" }, + { url = "https://files.pythonhosted.org/packages/64/d8/b989f96061a5e32d82fddd1b1b9ff48a7c8f8ae7606f0e80fd9de54b1e33/coverage-7.15.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d29ca7bd67af6e12e74632d65f026eabc1364da5c254494cd914446a28a3ef7", size = 258936, upload-time = "2026-07-15T18:54:55.015Z" }, + { url = "https://files.pythonhosted.org/packages/b8/fa/f99771f5110457c7b511c1935ca49ddf288218eaa84322e028b9334146ae/coverage-7.15.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:db9c8438057e5b0f6a22a0af99c0c1d26b57fbbdbd1be5861ddb8f897fcc3a2d", size = 253178, upload-time = "2026-07-15T18:54:57.527Z" }, + { url = "https://files.pythonhosted.org/packages/f6/96/c098a6044d119c751ceede7be91035fa8310170ec24a6523aff72f0a5793/coverage-7.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:63022c4c8dec1d0342f05c3ede99842fe3d007689acc45e86f123a1746e4a026", size = 254934, upload-time = "2026-07-15T18:54:59.41Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a2/1457b3a7a50c8d77500103b97a046db863e2f59a1cf6d2f814595f349885/coverage-7.15.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6c0be82b4d4aa5b2704e08518e2252f3e3d110164bcca826816801052e48a7aa", size = 252898, upload-time = "2026-07-15T18:55:01.338Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0e/76958874c471ecfcdde0d2b2747bb2c61bdbf34a40636f4ce9db9923e643/coverage-7.15.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4510fb9cdf6bb02dfa6af0be4a534b8102d086e22e4a33f8836df663da3d660d", size = 257056, upload-time = "2026-07-15T18:55:03.243Z" }, + { url = "https://files.pythonhosted.org/packages/7c/7c/3d7c4e3bf58baa40327dc7edc2272b17cf02299366d52763db1b0ca1556a/coverage-7.15.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:42ec3d989421b174a2ab607c1539f24127ad362757b7f1c0c0d7a2993f7eb37b", size = 252718, upload-time = "2026-07-15T18:55:05.029Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b8/1cecffed9ce14fb25be9ba42d37b6bb61485c9a3ddd43cd3dde36b6087d8/coverage-7.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8f91bce78e32343af184c3b7fa28fcf5a9e2641f4b6623d392038f804939188", size = 254490, upload-time = "2026-07-15T18:55:06.889Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2c/42984561bc7f4c045dca67516a0c50ee5ef8d84352dbeb5559dc86c4823e/coverage-7.15.2-cp313-cp313-win32.whl", hash = "sha256:434e68d531858205895eb0d74b73d20b84260de426387d53c422a5acda2cf050", size = 223647, upload-time = "2026-07-15T18:55:08.941Z" }, + { url = "https://files.pythonhosted.org/packages/41/9f/39c7c9245efc583beddf89a87683574e663ed93637f3afb6cd7b88405676/coverage-7.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:26c3b04a6377fd7c09800921fa934e3a17c0020439cd59df73e73ae1d4b6a78c", size = 224190, upload-time = "2026-07-15T18:55:10.789Z" }, + { url = "https://files.pythonhosted.org/packages/c7/de/3a2883cf8a213659280ef4b403059e17a9acaeb7fc7fd4105e1226ff2e6d/coverage-7.15.2-cp313-cp313-win_arm64.whl", hash = "sha256:3ed010aa1b69cda8e827aabfca9866216c980e2dca82ab9a78c5f83689964c8b", size = 223583, upload-time = "2026-07-15T18:55:12.678Z" }, + { url = "https://files.pythonhosted.org/packages/81/5f/aed265fd7a3551a394f36dfe41868aee709b7f95db4052205b4ad1563ac3/coverage-7.15.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:40f633c5c5fc783732f6312280122e859538fa24461235597c13d803ea9a108a", size = 221650, upload-time = "2026-07-15T18:55:14.527Z" }, + { url = "https://files.pythonhosted.org/packages/6b/2c/222ba12a545189017120f8eddfc1a0bd4616b47d5d4a8d99421edb2fe4c6/coverage-7.15.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:075560438765b7a2ef43bf7aa7758661b53d889df47f062a31bda6c1ade553a2", size = 221988, upload-time = "2026-07-15T18:55:16.674Z" }, + { url = "https://files.pythonhosted.org/packages/aa/38/304b5877ab46e6c290b4292cfcf3fe28245f0e5597cad7f6acc91fc7e0a4/coverage-7.15.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:25fd15dd40a0a2c51a500d664ca29053c09c3259d998407bf982b6e114696138", size = 253029, upload-time = "2026-07-15T18:55:18.856Z" }, + { url = "https://files.pythonhosted.org/packages/6c/58/821b533b8db9e44cf1d8a97bd525149ced40dde1d0093da02cb78e715244/coverage-7.15.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f", size = 255536, upload-time = "2026-07-15T18:55:21.027Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f2/7aa06604c389d32ea7f0a6a988359a7eafc3cd3f8e7bc2e88cd2fdf0b877/coverage-7.15.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9854ca62c152874b2060772503535be2e8f53f70b8aaa7686b094888d872f984", size = 256881, upload-time = "2026-07-15T18:55:23.125Z" }, + { url = "https://files.pythonhosted.org/packages/a2/4f/1ef342339c7916d0096bc5888cc0f653882cc7bc8f897d5cb89143287c9b/coverage-7.15.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:913b6c56e110da40e035bbd168353bf7aaa2544a5eaccea5d98a4629aac156c7", size = 259196, upload-time = "2026-07-15T18:55:25.099Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f4/7ed055d7a9c5ec13b161773a115a5ccc6b0081d568c31fad830806306cc7/coverage-7.15.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aaccad4129d735a8a4d526f26929894c9a4e8ef7034566f210b176749d6906e3", size = 253036, upload-time = "2026-07-15T18:55:27.018Z" }, + { url = "https://files.pythonhosted.org/packages/14/79/ea82cca18c242a3a38b6c017da39726aa62dcb64aa635abf79b92009975c/coverage-7.15.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a164b50081fc7357331c4024ef4d17b78ba325f8380d05f5a69599a7e05257ee", size = 254887, upload-time = "2026-07-15T18:55:29.084Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ba/a136db3c0d9562b00e10b72540dbf3a33cd3bc5b95060c9308e247494623/coverage-7.15.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bfd341ccf78128e72c094bc70cc25b3ef309c33c7c2c66ba3ed4309549e02de1", size = 252852, upload-time = "2026-07-15T18:55:31.184Z" }, + { url = "https://files.pythonhosted.org/packages/17/17/ea334246b16b7d059953fad6fdefa11e33c68efbd3fe37b1098120a1fac2/coverage-7.15.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1473b3ba8e7ee0f076117b1a72c23f579a2b9e2bb742f48a8d86ea27ca93f91a", size = 257128, upload-time = "2026-07-15T18:55:33.163Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c3/074fb66d46d607855f710876b117cbda562c5ab08363528e78820449f937/coverage-7.15.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:17c432b5f73ad52ef46fb06019f6fa7c66ce381961cf0f7dfd1d3a4bd3a98145", size = 252668, upload-time = "2026-07-15T18:55:35.063Z" }, + { url = "https://files.pythonhosted.org/packages/e1/c1/f620850ada9b36435921c9a3a8057013422b1d964eb4bf37fe138724d192/coverage-7.15.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:77f0ef5011df53a4bd1b35211ab122287f8d9b8d7aa1c4553e5c2deb24b1d446", size = 254325, upload-time = "2026-07-15T18:55:37.125Z" }, + { url = "https://files.pythonhosted.org/packages/cc/31/a729ca3689404493af82ef8e6ff70bd88bdda8da89aeef6ca9b387aeb2b4/coverage-7.15.2-cp314-cp314-win32.whl", hash = "sha256:f653e5d7248c1191ec988a85c72edeab46c3ff44f90639a4ed4874ec0be90243", size = 223844, upload-time = "2026-07-15T18:55:39.078Z" }, + { url = "https://files.pythonhosted.org/packages/c6/83/5d809dc808fb1698c671f3e372259bb9158e64b7ea526fc6ab7de64de9fe/coverage-7.15.2-cp314-cp314-win_amd64.whl", hash = "sha256:9911f31aad8906abe337c271343485cf20df5e70df5d2f57f9f136e7b55f26bc", size = 224331, upload-time = "2026-07-15T18:55:41.346Z" }, + { url = "https://files.pythonhosted.org/packages/16/4e/35e488548e952795829e129995c4174df33bf432b591d1aa42c8d9e4e7ad/coverage-7.15.2-cp314-cp314-win_arm64.whl", hash = "sha256:e38def96ad59853824c97953fdcd2c320a84ba3ce99b417db78af8bb6c3db635", size = 223760, upload-time = "2026-07-15T18:55:43.518Z" }, + { url = "https://files.pythonhosted.org/packages/ed/49/dd2c86cd6374038f6e415fb5bfb86db5218553209c081384a020369dee79/coverage-7.15.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:835ec4e20b45f0a7f63ed78f94065aca00de033403df8377bfe8b9c6abc0a7be", size = 222384, upload-time = "2026-07-15T18:55:45.569Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/173ff17a1c0808e5a438f549f6f145d5ac7528f2791310b63523e3200ac7/coverage-7.15.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7466cc7ab6dc0db871d264bf99e8779f0917ee63d40730af0552f71535a6e072", size = 222647, upload-time = "2026-07-15T18:55:47.544Z" }, + { url = "https://files.pythonhosted.org/packages/84/f8/b8cba872162356fb44ac79c10309d987206a4461e32072fc29228dad7331/coverage-7.15.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e370c12133095ff18432de8c044962be85a5a96d90c6fcbce8e17e76236d2328", size = 264013, upload-time = "2026-07-15T18:55:49.768Z" }, + { url = "https://files.pythonhosted.org/packages/ee/67/a807a7586d0b8cae485308ddd55756f0806c92f8e0b411bacbf23c48edf3/coverage-7.15.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fe41909c9515c3bfdb5f02c4d1f857dba322d9a9a1178069b91eea77889df63a", size = 266135, upload-time = "2026-07-15T18:55:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/67/cd78771dc985f7e4ebdcc82b1a96d9a932af9e806f01f2f91a89f4c72e80/coverage-7.15.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6aa28cfb6488e5453b5b762d65f73aa586380f6693a04d58078ce228a29b06c0", size = 268555, upload-time = "2026-07-15T18:55:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/18/3e/10134cf81275188c58568f324fc74aedff32c63ca4d5bbc513a91944a6f0/coverage-7.15.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcc0aae933921d03096f53b0b03eeb702129fd406dee59f08d2efacc68681fa5", size = 269674, upload-time = "2026-07-15T18:55:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/75/4a/771b77de446cba985dc414bbc5844bd21604da05dbc044286df8318a48a7/coverage-7.15.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7c63387e21ab21f512c69c9756a8c7dadd322c7275edb064064433c9a09c3743", size = 263101, upload-time = "2026-07-15T18:55:58.107Z" }, + { url = "https://files.pythonhosted.org/packages/5f/b5/70a7011da15f4071943361183aefa27847f3e3aec4fd335f1cb3d3a622b1/coverage-7.15.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e55510bc98ae943cece9e667a6c0fe94c6a92913720dea34243657a17993d0c", size = 266007, upload-time = "2026-07-15T18:56:00.468Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0d/f9547e804ce7ad49646ffeffac26699510efbe6c0f751b66fdc960c4e825/coverage-7.15.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2ff08701be2d1556fc78b326c80a3e8042da09352ecb3819105f8e386c8a3071", size = 263611, upload-time = "2026-07-15T18:56:02.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/59/f576a396659c0efd351f5c1544f67c3560e89c7761cabf7f65e412beeda5/coverage-7.15.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:38c9518b7103826c403a461544e3c2e77151e8676d06eaed85911a97e962584a", size = 267344, upload-time = "2026-07-15T18:56:04.622Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5d/c2e4fce3579c0cb635024293f1a32bbe26df101b3e3a69f22243d1352b6c/coverage-7.15.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:dee88b1ed88587abd8c0269a1fc1f4cc77f7750d1dfde2869e2a123af420e67d", size = 262456, upload-time = "2026-07-15T18:56:06.641Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/956287d69436b66094bc4b57ac2da71e43bfd2a5524e958900b9f582fcf8/coverage-7.15.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fbeeeecea279727f8ac16c8e1133ddfeee793e985c86ae343d6a5ce744eef8c", size = 264771, upload-time = "2026-07-15T18:56:08.795Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5a/6f979530c2734c575de77cf58f5f28d51f7123a94b5030fd9156fe5f363c/coverage-7.15.2-cp314-cp314t-win32.whl", hash = "sha256:cb0fddaa6884be6aae36ced9544b5e90f7d5f03845a2853bf47a14953a4e8688", size = 224151, upload-time = "2026-07-15T18:56:10.856Z" }, + { url = "https://files.pythonhosted.org/packages/54/7e/27f6b2a74d484742f4017553e710b01e396b23d809df3e95ca0bb9a2824b/coverage-7.15.2-cp314-cp314t-win_amd64.whl", hash = "sha256:77f091ea3a9cc611cd29f433565476bc1936c084ac8eee00ea0e7e70c27e4199", size = 224981, upload-time = "2026-07-15T18:56:12.928Z" }, + { url = "https://files.pythonhosted.org/packages/b1/48/284863423aa474240f6842bd00d680da22f4e6ea2e466618ef7c9c9e69a9/coverage-7.15.2-cp314-cp314t-win_arm64.whl", hash = "sha256:6fc448c377d6eeb00a47c673494bd9bae29280ca53987e1869e67ebedfe20658", size = 224294, upload-time = "2026-07-15T18:56:15.156Z" }, + { url = "https://files.pythonhosted.org/packages/ec/82/32e3bd191d498e64f6f911ad55d14006a0861e54869d2d32452326399e65/coverage-7.15.2-py3-none-any.whl", hash = "sha256:eb6bcae8d1a9d305351ecb108232441d11c5cfe9de840a04388ba5d2db8d735c", size = 213375, upload-time = "2026-07-15T18:56:17.305Z" }, ] [package.optional-dependencies] @@ -542,39 +882,76 @@ toml = [ [[package]] name = "cryptography" -version = "45.0.7" +version = "49.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a7/35/c495bffc2056f2dadb32434f1feedd79abde2a7f8363e1974afa9c33c7e2/cryptography-45.0.7.tar.gz", hash = "sha256:4b1654dfc64ea479c242508eb8c724044f1e964a47d1d1cacc5132292d851971", size = 744980, upload-time = "2025-09-01T11:15:03.146Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/63/43641c5acce3a6105cf8bd5baeceeb1846bb63067d26dae3e5db59f1513a/cryptography-45.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:67285f8a611b0ebc0857ced2081e30302909f571a46bfa7a3cc0ad303fe015c6", size = 4205799, upload-time = "2025-09-01T11:14:02.517Z" }, - { url = "https://files.pythonhosted.org/packages/bc/29/c238dd9107f10bfde09a4d1c52fd38828b1aa353ced11f358b5dd2507d24/cryptography-45.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:577470e39e60a6cd7780793202e63536026d9b8641de011ed9d8174da9ca5339", size = 4430504, upload-time = "2025-09-01T11:14:04.522Z" }, - { url = "https://files.pythonhosted.org/packages/62/62/24203e7cbcc9bd7c94739428cd30680b18ae6b18377ae66075c8e4771b1b/cryptography-45.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4bd3e5c4b9682bc112d634f2c6ccc6736ed3635fc3319ac2bb11d768cc5a00d8", size = 4209542, upload-time = "2025-09-01T11:14:06.309Z" }, - { url = "https://files.pythonhosted.org/packages/cd/e3/e7de4771a08620eef2389b86cd87a2c50326827dea5528feb70595439ce4/cryptography-45.0.7-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:465ccac9d70115cd4de7186e60cfe989de73f7bb23e8a7aa45af18f7412e75bf", size = 3889244, upload-time = "2025-09-01T11:14:08.152Z" }, - { url = "https://files.pythonhosted.org/packages/96/b8/bca71059e79a0bb2f8e4ec61d9c205fbe97876318566cde3b5092529faa9/cryptography-45.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:16ede8a4f7929b4b7ff3642eba2bf79aa1d71f24ab6ee443935c0d269b6bc513", size = 4461975, upload-time = "2025-09-01T11:14:09.755Z" }, - { url = "https://files.pythonhosted.org/packages/58/67/3f5b26937fe1218c40e95ef4ff8d23c8dc05aa950d54200cc7ea5fb58d28/cryptography-45.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8978132287a9d3ad6b54fcd1e08548033cc09dc6aacacb6c004c73c3eb5d3ac3", size = 4209082, upload-time = "2025-09-01T11:14:11.229Z" }, - { url = "https://files.pythonhosted.org/packages/0e/e4/b3e68a4ac363406a56cf7b741eeb80d05284d8c60ee1a55cdc7587e2a553/cryptography-45.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b6a0e535baec27b528cb07a119f321ac024592388c5681a5ced167ae98e9fff3", size = 4460397, upload-time = "2025-09-01T11:14:12.924Z" }, - { url = "https://files.pythonhosted.org/packages/22/49/2c93f3cd4e3efc8cb22b02678c1fad691cff9dd71bb889e030d100acbfe0/cryptography-45.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a24ee598d10befaec178efdff6054bc4d7e883f615bfbcd08126a0f4931c83a6", size = 4337244, upload-time = "2025-09-01T11:14:14.431Z" }, - { url = "https://files.pythonhosted.org/packages/04/19/030f400de0bccccc09aa262706d90f2ec23d56bc4eb4f4e8268d0ddf3fb8/cryptography-45.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa26fa54c0a9384c27fcdc905a2fb7d60ac6e47d14bc2692145f2b3b1e2cfdbd", size = 4568862, upload-time = "2025-09-01T11:14:16.185Z" }, - { url = "https://files.pythonhosted.org/packages/bc/4c/8f57f2500d0ccd2675c5d0cc462095adf3faa8c52294ba085c036befb901/cryptography-45.0.7-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:81823935e2f8d476707e85a78a405953a03ef7b7b4f55f93f7c2d9680e5e0691", size = 4202233, upload-time = "2025-09-01T11:14:22.454Z" }, - { url = "https://files.pythonhosted.org/packages/eb/ac/59b7790b4ccaed739fc44775ce4645c9b8ce54cbec53edf16c74fd80cb2b/cryptography-45.0.7-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3994c809c17fc570c2af12c9b840d7cea85a9fd3e5c0e0491f4fa3c029216d59", size = 4423075, upload-time = "2025-09-01T11:14:24.287Z" }, - { url = "https://files.pythonhosted.org/packages/b8/56/d4f07ea21434bf891faa088a6ac15d6d98093a66e75e30ad08e88aa2b9ba/cryptography-45.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dad43797959a74103cb59c5dac71409f9c27d34c8a05921341fb64ea8ccb1dd4", size = 4204517, upload-time = "2025-09-01T11:14:25.679Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ac/924a723299848b4c741c1059752c7cfe09473b6fd77d2920398fc26bfb53/cryptography-45.0.7-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ce7a453385e4c4693985b4a4a3533e041558851eae061a58a5405363b098fcd3", size = 3882893, upload-time = "2025-09-01T11:14:27.1Z" }, - { url = "https://files.pythonhosted.org/packages/83/dc/4dab2ff0a871cc2d81d3ae6d780991c0192b259c35e4d83fe1de18b20c70/cryptography-45.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b04f85ac3a90c227b6e5890acb0edbaf3140938dbecf07bff618bf3638578cf1", size = 4450132, upload-time = "2025-09-01T11:14:28.58Z" }, - { url = "https://files.pythonhosted.org/packages/12/dd/b2882b65db8fc944585d7fb00d67cf84a9cef4e77d9ba8f69082e911d0de/cryptography-45.0.7-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:48c41a44ef8b8c2e80ca4527ee81daa4c527df3ecbc9423c41a420a9559d0e27", size = 4204086, upload-time = "2025-09-01T11:14:30.572Z" }, - { url = "https://files.pythonhosted.org/packages/5d/fa/1d5745d878048699b8eb87c984d4ccc5da4f5008dfd3ad7a94040caca23a/cryptography-45.0.7-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f3df7b3d0f91b88b2106031fd995802a2e9ae13e02c36c1fc075b43f420f3a17", size = 4449383, upload-time = "2025-09-01T11:14:32.046Z" }, - { url = "https://files.pythonhosted.org/packages/36/8b/fc61f87931bc030598e1876c45b936867bb72777eac693e905ab89832670/cryptography-45.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd342f085542f6eb894ca00ef70236ea46070c8a13824c6bde0dfdcd36065b9b", size = 4332186, upload-time = "2025-09-01T11:14:33.95Z" }, - { url = "https://files.pythonhosted.org/packages/0b/11/09700ddad7443ccb11d674efdbe9a832b4455dc1f16566d9bd3834922ce5/cryptography-45.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1993a1bb7e4eccfb922b6cd414f072e08ff5816702a0bdb8941c247a6b1b287c", size = 4561639, upload-time = "2025-09-01T11:14:35.343Z" }, - { url = "https://files.pythonhosted.org/packages/59/aa/e947693ab08674a2663ed2534cd8d345cf17bf6a1facf99273e8ec8986dc/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a20e442e917889d1a6b3c570c9e3fa2fdc398c20868abcea268ea33c024c4083", size = 4142233, upload-time = "2025-09-01T11:14:41.305Z" }, - { url = "https://files.pythonhosted.org/packages/24/06/09b6f6a2fc43474a32b8fe259038eef1500ee3d3c141599b57ac6c57612c/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:258e0dff86d1d891169b5af222d362468a9570e2532923088658aa866eb11130", size = 4376202, upload-time = "2025-09-01T11:14:43.047Z" }, - { url = "https://files.pythonhosted.org/packages/00/f2/c166af87e95ce6ae6d38471a7e039d3a0549c2d55d74e059680162052824/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d97cf502abe2ab9eff8bd5e4aca274da8d06dd3ef08b759a8d6143f4ad65d4b4", size = 4141900, upload-time = "2025-09-01T11:14:45.089Z" }, - { url = "https://files.pythonhosted.org/packages/16/b9/e96e0b6cb86eae27ea51fa8a3151535a18e66fe7c451fa90f7f89c85f541/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:c987dad82e8c65ebc985f5dae5e74a3beda9d0a2a4daf8a1115f3772b59e5141", size = 4375562, upload-time = "2025-09-01T11:14:47.166Z" }, - { url = "https://files.pythonhosted.org/packages/16/ce/5f6ff59ea9c7779dba51b84871c19962529bdcc12e1a6ea172664916c550/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:06ce84dc14df0bf6ea84666f958e6080cdb6fe1231be2a51f3fc1267d9f3fb34", size = 4149533, upload-time = "2025-09-01T11:14:52.091Z" }, - { url = "https://files.pythonhosted.org/packages/ce/13/b3cfbd257ac96da4b88b46372e662009b7a16833bfc5da33bb97dd5631ae/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d0c5c6bac22b177bf8da7435d9d27a6834ee130309749d162b26c3105c0795a9", size = 4385557, upload-time = "2025-09-01T11:14:53.551Z" }, - { url = "https://files.pythonhosted.org/packages/1c/c5/8c59d6b7c7b439ba4fc8d0cab868027fd095f215031bc123c3a070962912/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:2f641b64acc00811da98df63df7d59fd4706c0df449da71cb7ac39a0732b40ae", size = 4149023, upload-time = "2025-09-01T11:14:55.022Z" }, - { url = "https://files.pythonhosted.org/packages/55/32/05385c86d6ca9ab0b4d5bb442d2e3d85e727939a11f3e163fc776ce5eb40/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:f5414a788ecc6ee6bc58560e85ca624258a55ca434884445440a810796ea0e0b", size = 4385722, upload-time = "2025-09-01T11:14:57.319Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, + { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, + { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, + { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, + { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, + { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, + { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, + { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" }, + { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, + { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, + { url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" }, + { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, + { url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" }, + { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, + { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, + { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, + { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, + { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/63/d3/4a83af35d65e3fad632c926fad684c193ea4398569ccb0bbbc7fe8f5dc9a/cryptography-49.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b", size = 3993685, upload-time = "2026-06-12T20:02:14.883Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a7/f9dac0ab7f80368c56993a7bf638ef9935f825c91902798481fac0898138/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838", size = 4676239, upload-time = "2026-06-12T20:02:28.793Z" }, + { url = "https://files.pythonhosted.org/packages/d7/70/2ba3769dd0ae167e2f33dfa9592d45db6ff9a61d62ca1a5b3d1bdd09068f/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5", size = 4715584, upload-time = "2026-06-12T20:01:27.495Z" }, + { url = "https://files.pythonhosted.org/packages/94/64/2923570ac1c0bd3a737aa366ac3abbbbde273042308b8cde95e2364a6e6a/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615", size = 4675885, upload-time = "2026-06-12T20:01:55.49Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f8/614dc7e051418cfe53d55173c1e24c6b0085e89996fe90508c2fdf769aef/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6", size = 4715449, upload-time = "2026-06-12T20:02:05.469Z" }, + { url = "https://files.pythonhosted.org/packages/aa/50/a9caea39ad19c431c1a3f8a31114df65b260cdfe67786b6c7e7c040c4c44/cryptography-49.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6", size = 3783731, upload-time = "2026-06-12T20:02:43.319Z" }, +] + +[[package]] +name = "deepagents" +version = "0.6.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain" }, + { name = "langchain-anthropic" }, + { name = "langchain-core" }, + { name = "langchain-google-genai" }, + { name = "langsmith" }, + { name = "wcmatch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/db/a6acdc72a9e90c3f07ed10de35c951734a02d4facb693bb59684ad368801/deepagents-0.6.12.tar.gz", hash = "sha256:1f281c0bc5a63132f62e2ee345c1dc593b23188da6e23016401f6879fbe54b5f", size = 211364, upload-time = "2026-06-25T17:26:52.775Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/49/af7219b3c13520fee047bb807cfaefba17f8e4584c551d946773589a4f08/deepagents-0.6.12-py3-none-any.whl", hash = "sha256:28b8fa0119ca0a689e3e18e288c4634e4046062acfc87a1cb34289d3af3a1c88", size = 236120, upload-time = "2026-06-25T17:26:51.736Z" }, ] [[package]] @@ -590,6 +967,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/c7/d1ec24fb280caa5a79b6b950db565dab30210a66259d17d5bb2b3a9f878d/dependency_groups-1.3.1-py3-none-any.whl", hash = "sha256:51aeaa0dfad72430fcfb7bcdbefbd75f3792e5919563077f30bc0d73f4493030", size = 8664, upload-time = "2025-05-02T00:34:27.085Z" }, ] +[[package]] +name = "dill" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, +] + [[package]] name = "distro" version = "1.9.0" @@ -600,411 +986,660 @@ wheels = [ ] [[package]] -name = "docutils" -version = "0.22" +name = "docker" +version = "7.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e9/86/5b41c32ecedcfdb4c77b28b6cb14234f252075f8cdb254531727a35547dd/docutils-0.22.tar.gz", hash = "sha256:ba9d57750e92331ebe7c08a1bbf7a7f8143b86c476acd51528b042216a6aad0f", size = 2277984, upload-time = "2025-07-29T15:20:31.06Z" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/7f/731ff914b0255d3d065f45fd4e626d4b8c95dbcbaada049f337a6ac16410/docker-7.2.0.tar.gz", hash = "sha256:cebb93773d334f778e023a7ee352a8d6e13ab1bd3b863a4d4a59dec897df43ac", size = 118731, upload-time = "2026-07-09T14:53:46.39Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/57/8db39bc5f98f042e0153b1de9fb88e1a409a33cda4dd7f723c2ed71e01f6/docutils-0.22-py3-none-any.whl", hash = "sha256:4ed966a0e96a0477d852f7af31bdcb3adc049fbb35ccba358c2ea8a03287615e", size = 630709, upload-time = "2025-07-29T15:20:28.335Z" }, + { url = "https://files.pythonhosted.org/packages/75/23/529140fe1aab80fc6992f93a706deec709140a6397439139a054e1515c45/docker-7.2.0-py3-none-any.whl", hash = "sha256:a3f45fdeb9165e2d25d9a1d02ddf3bc70fb572cf5ebbf9b58558c22caf29b71f", size = 148775, upload-time = "2026-07-09T14:53:45.224Z" }, ] [[package]] -name = "eval-type-backport" -version = "0.2.2" +name = "docstring-parser" +version = "0.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/ea/8b0ac4469d4c347c6a385ff09dc3c048c2d021696664e26c7ee6791631b5/eval_type_backport-0.2.2.tar.gz", hash = "sha256:f0576b4cf01ebb5bd358d02314d31846af5e07678387486e2c798af0e7d849c1", size = 9079, upload-time = "2024-12-21T20:09:46.005Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/31/55cd413eaccd39125368be33c46de24a1f639f2e12349b0361b4678f3915/eval_type_backport-0.2.2-py3-none-any.whl", hash = "sha256:cb6ad7c393517f476f96d456d0412ea80f0a8cf96f6892834cd9340149111b0a", size = 5830, upload-time = "2024-12-21T20:09:44.175Z" }, + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, ] [[package]] -name = "exceptiongroup" -version = "1.3.0" +name = "docutils" +version = "0.23" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/39/a4/5180d9afc57e8fca05601dd652bdff19604c218814037fe90ffc7625a50a/docutils-0.23.tar.gz", hash = "sha256:746f5060322511280a1e50eb76846ed6bf2342984b2ac04dc42caa1a8d78799e", size = 2303823, upload-time = "2026-05-27T17:41:06.934Z" } 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 = "fastuuid" -version = "0.12.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/17/13146a1e916bd2971d0a58db5e0a4ad23efdd49f78f33ac871c161f8007b/fastuuid-0.12.0.tar.gz", hash = "sha256:d0bd4e5b35aad2826403f4411937c89e7c88857b1513fe10f696544c03e9bd8e", size = 19180, upload-time = "2025-01-27T18:04:14.387Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/c3/9db9aee6f34e6dfd1f909d3d7432ac26e491a0471f8bb8b676c44b625b3f/fastuuid-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:22a900ef0956aacf862b460e20541fdae2d7c340594fe1bd6fdcb10d5f0791a9", size = 247356, upload-time = "2025-01-27T18:04:45.397Z" }, - { url = "https://files.pythonhosted.org/packages/14/a5/999e6e017af3d85841ce1e172d32fd27c8700804c125f496f71bfddc1a9f/fastuuid-0.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0302f5acf54dc75de30103025c5a95db06d6c2be36829043a0aa16fc170076bc", size = 258384, upload-time = "2025-01-27T18:04:03.562Z" }, - { url = "https://files.pythonhosted.org/packages/c4/e6/beae8411cac5b3b0b9d59ee08405eb39c3abe81dad459114363eff55c14a/fastuuid-0.12.0-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:7946b4a310cfc2d597dcba658019d72a2851612a2cebb949d809c0e2474cf0a6", size = 278480, upload-time = "2025-01-27T18:04:05.663Z" }, - { url = "https://files.pythonhosted.org/packages/f1/f6/c598b9a052435716fc5a084ef17049edd35ca2c8241161269bfea4905ab4/fastuuid-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:a1b6764dd42bf0c46c858fb5ade7b7a3d93b7a27485a7a5c184909026694cd88", size = 156799, upload-time = "2025-01-27T18:05:41.867Z" }, - { url = "https://files.pythonhosted.org/packages/d4/99/555eab31381c7912103d4c8654082611e5e82a7bb88ad5ab067e36b622d7/fastuuid-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2bced35269315d16fe0c41003f8c9d63f2ee16a59295d90922cad5e6a67d0418", size = 247249, upload-time = "2025-01-27T18:03:23.092Z" }, - { url = "https://files.pythonhosted.org/packages/6d/3b/d62ce7f2af3d50a8e787603d44809770f43a3f2ff708bf10c252bf479109/fastuuid-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82106e4b0a24f4f2f73c88f89dadbc1533bb808900740ca5db9bbb17d3b0c824", size = 258369, upload-time = "2025-01-27T18:04:08.903Z" }, - { url = "https://files.pythonhosted.org/packages/86/23/33ec5355036745cf83ea9ca7576d2e0750ff8d268c03b4af40ed26f1a303/fastuuid-0.12.0-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:4db1bc7b8caa1d7412e1bea29b016d23a8d219131cff825b933eb3428f044dca", size = 278316, upload-time = "2025-01-27T18:04:12.74Z" }, - { url = "https://files.pythonhosted.org/packages/40/91/32ce82a14650148b6979ccd1a0089fd63d92505a90fb7156d2acc3245cbd/fastuuid-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:07afc8e674e67ac3d35a608c68f6809da5fab470fb4ef4469094fdb32ba36c51", size = 156643, upload-time = "2025-01-27T18:05:59.266Z" }, - { url = "https://files.pythonhosted.org/packages/f6/28/442e79d6219b90208cb243ac01db05d89cc4fdf8ecd563fb89476baf7122/fastuuid-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:328694a573fe9dce556b0b70c9d03776786801e028d82f0b6d9db1cb0521b4d1", size = 247372, upload-time = "2025-01-27T18:03:40.967Z" }, - { url = "https://files.pythonhosted.org/packages/40/eb/e0fd56890970ca7a9ec0d116844580988b692b1a749ac38e0c39e1dbdf23/fastuuid-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02acaea2c955bb2035a7d8e7b3fba8bd623b03746ae278e5fa932ef54c702f9f", size = 258200, upload-time = "2025-01-27T18:04:12.138Z" }, - { url = "https://files.pythonhosted.org/packages/f5/3c/4b30e376e65597a51a3dc929461a0dec77c8aec5d41d930f482b8f43e781/fastuuid-0.12.0-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:ed9f449cba8cf16cced252521aee06e633d50ec48c807683f21cc1d89e193eb0", size = 278446, upload-time = "2025-01-27T18:04:15.877Z" }, - { url = "https://files.pythonhosted.org/packages/fe/96/cc5975fd23d2197b3e29f650a7a9beddce8993eaf934fa4ac595b77bb71f/fastuuid-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:0df2ea4c9db96fd8f4fa38d0e88e309b3e56f8fd03675a2f6958a5b082a0c1e4", size = 157185, upload-time = "2025-01-27T18:06:19.21Z" }, - { url = "https://files.pythonhosted.org/packages/a9/e8/d2bb4f19e5ee15f6f8e3192a54a897678314151aa17d0fb766d2c2cbc03d/fastuuid-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7fe2407316a04ee8f06d3dbc7eae396d0a86591d92bafe2ca32fce23b1145786", size = 247512, upload-time = "2025-01-27T18:04:08.115Z" }, - { url = "https://files.pythonhosted.org/packages/bc/53/25e811d92fd60f5c65e098c3b68bd8f1a35e4abb6b77a153025115b680de/fastuuid-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9b31dd488d0778c36f8279b306dc92a42f16904cba54acca71e107d65b60b0c", size = 258257, upload-time = "2025-01-27T18:03:56.408Z" }, - { url = "https://files.pythonhosted.org/packages/10/23/73618e7793ea0b619caae2accd9e93e60da38dd78dd425002d319152ef2f/fastuuid-0.12.0-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:b19361ee649365eefc717ec08005972d3d1eb9ee39908022d98e3bfa9da59e37", size = 278559, upload-time = "2025-01-27T18:03:58.661Z" }, - { url = "https://files.pythonhosted.org/packages/e4/41/6317ecfc4757d5f2a604e5d3993f353ba7aee85fa75ad8b86fce6fc2fa40/fastuuid-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:8fc66b11423e6f3e1937385f655bedd67aebe56a3dcec0cb835351cfe7d358c9", size = 157276, upload-time = "2025-01-27T18:06:39.245Z" }, - { url = "https://files.pythonhosted.org/packages/f6/ea/6c37bc6ad2bf0885331def02a9cdc7738fb35863139432b49df089098de4/fastuuid-0.12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9303617e887429c193d036d47d0b32b774ed3618431123e9106f610d601eb57e", size = 247940, upload-time = "2025-01-27T18:04:59.675Z" }, - { url = "https://files.pythonhosted.org/packages/e3/a0/29d238df7d9add849504f360341c9cb77c871fb8db04d65a9c7930f02dc7/fastuuid-0.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8790221325b376e1122e95f865753ebf456a9fb8faf0dca4f9bf7a3ff620e413", size = 259367, upload-time = "2025-01-27T18:04:07.055Z" }, - { url = "https://files.pythonhosted.org/packages/e4/5a/615e54222583fe04de3c18d716f94fe2af14cefa8b0f8a4c15348e90a3ba/fastuuid-0.12.0-cp39-cp39-manylinux_2_34_x86_64.whl", hash = "sha256:e4b12d3e23515e29773fa61644daa660ceb7725e05397a986c2109f512579a48", size = 279200, upload-time = "2025-01-27T18:04:09.821Z" }, - { url = "https://files.pythonhosted.org/packages/0a/63/4c13dfe29c113857a05a55303d87417d6fb83095e8f3e815ccf421a21c58/fastuuid-0.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:e41656457c34b5dcb784729537ea64c7d9bbaf7047b480c6c6a64c53379f455a", size = 157672, upload-time = "2025-01-27T18:07:37.77Z" }, + { url = "https://files.pythonhosted.org/packages/32/91/30151a39f7570f448ed84529390628a651d7f27c87d73c9b887f8189695e/docutils-0.23-py3-none-any.whl", hash = "sha256:25d013af9bf23bc1c7b2b093dff4208166c53a94786c9e447808335ef1185fea", size = 634701, upload-time = "2026-05-27T17:40:58.442Z" }, ] [[package]] -name = "filelock" -version = "3.19.1" +name = "exceptiongroup" +version = "1.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" }, +dependencies = [ + { name = "typing-extensions" }, ] - -[[package]] -name = "frozenlist" -version = "1.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078, upload-time = "2025-06-09T23:02:35.538Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/af/36/0da0a49409f6b47cc2d060dc8c9040b897b5902a8a4e37d9bc1deb11f680/frozenlist-1.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cc4df77d638aa2ed703b878dd093725b72a824c3c546c076e8fdf276f78ee84a", size = 81304, upload-time = "2025-06-09T22:59:46.226Z" }, - { url = "https://files.pythonhosted.org/packages/77/f0/77c11d13d39513b298e267b22eb6cb559c103d56f155aa9a49097221f0b6/frozenlist-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:716a9973a2cc963160394f701964fe25012600f3d311f60c790400b00e568b61", size = 47735, upload-time = "2025-06-09T22:59:48.133Z" }, - { url = "https://files.pythonhosted.org/packages/37/12/9d07fa18971a44150593de56b2f2947c46604819976784bcf6ea0d5db43b/frozenlist-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0fd1bad056a3600047fb9462cff4c5322cebc59ebf5d0a3725e0ee78955001d", size = 46775, upload-time = "2025-06-09T22:59:49.564Z" }, - { url = "https://files.pythonhosted.org/packages/70/34/f73539227e06288fcd1f8a76853e755b2b48bca6747e99e283111c18bcd4/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3789ebc19cb811163e70fe2bd354cea097254ce6e707ae42e56f45e31e96cb8e", size = 224644, upload-time = "2025-06-09T22:59:51.35Z" }, - { url = "https://files.pythonhosted.org/packages/fb/68/c1d9c2f4a6e438e14613bad0f2973567586610cc22dcb1e1241da71de9d3/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af369aa35ee34f132fcfad5be45fbfcde0e3a5f6a1ec0712857f286b7d20cca9", size = 222125, upload-time = "2025-06-09T22:59:52.884Z" }, - { url = "https://files.pythonhosted.org/packages/b9/d0/98e8f9a515228d708344d7c6986752be3e3192d1795f748c24bcf154ad99/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac64b6478722eeb7a3313d494f8342ef3478dff539d17002f849101b212ef97c", size = 233455, upload-time = "2025-06-09T22:59:54.74Z" }, - { url = "https://files.pythonhosted.org/packages/79/df/8a11bcec5600557f40338407d3e5bea80376ed1c01a6c0910fcfdc4b8993/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f89f65d85774f1797239693cef07ad4c97fdd0639544bad9ac4b869782eb1981", size = 227339, upload-time = "2025-06-09T22:59:56.187Z" }, - { url = "https://files.pythonhosted.org/packages/50/82/41cb97d9c9a5ff94438c63cc343eb7980dac4187eb625a51bdfdb7707314/frozenlist-1.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1073557c941395fdfcfac13eb2456cb8aad89f9de27bae29fabca8e563b12615", size = 212969, upload-time = "2025-06-09T22:59:57.604Z" }, - { url = "https://files.pythonhosted.org/packages/13/47/f9179ee5ee4f55629e4f28c660b3fdf2775c8bfde8f9c53f2de2d93f52a9/frozenlist-1.7.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed8d2fa095aae4bdc7fdd80351009a48d286635edffee66bf865e37a9125c50", size = 222862, upload-time = "2025-06-09T22:59:59.498Z" }, - { url = "https://files.pythonhosted.org/packages/1a/52/df81e41ec6b953902c8b7e3a83bee48b195cb0e5ec2eabae5d8330c78038/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:24c34bea555fe42d9f928ba0a740c553088500377448febecaa82cc3e88aa1fa", size = 222492, upload-time = "2025-06-09T23:00:01.026Z" }, - { url = "https://files.pythonhosted.org/packages/84/17/30d6ea87fa95a9408245a948604b82c1a4b8b3e153cea596421a2aef2754/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:69cac419ac6a6baad202c85aaf467b65ac860ac2e7f2ac1686dc40dbb52f6577", size = 238250, upload-time = "2025-06-09T23:00:03.401Z" }, - { url = "https://files.pythonhosted.org/packages/8f/00/ecbeb51669e3c3df76cf2ddd66ae3e48345ec213a55e3887d216eb4fbab3/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:960d67d0611f4c87da7e2ae2eacf7ea81a5be967861e0c63cf205215afbfac59", size = 218720, upload-time = "2025-06-09T23:00:05.282Z" }, - { url = "https://files.pythonhosted.org/packages/1a/c0/c224ce0e0eb31cc57f67742071bb470ba8246623c1823a7530be0e76164c/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:41be2964bd4b15bf575e5daee5a5ce7ed3115320fb3c2b71fca05582ffa4dc9e", size = 232585, upload-time = "2025-06-09T23:00:07.962Z" }, - { url = "https://files.pythonhosted.org/packages/55/3c/34cb694abf532f31f365106deebdeac9e45c19304d83cf7d51ebbb4ca4d1/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:46d84d49e00c9429238a7ce02dc0be8f6d7cd0cd405abd1bebdc991bf27c15bd", size = 234248, upload-time = "2025-06-09T23:00:09.428Z" }, - { url = "https://files.pythonhosted.org/packages/98/c0/2052d8b6cecda2e70bd81299e3512fa332abb6dcd2969b9c80dfcdddbf75/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15900082e886edb37480335d9d518cec978afc69ccbc30bd18610b7c1b22a718", size = 221621, upload-time = "2025-06-09T23:00:11.32Z" }, - { url = "https://files.pythonhosted.org/packages/c5/bf/7dcebae315436903b1d98ffb791a09d674c88480c158aa171958a3ac07f0/frozenlist-1.7.0-cp310-cp310-win32.whl", hash = "sha256:400ddd24ab4e55014bba442d917203c73b2846391dd42ca5e38ff52bb18c3c5e", size = 39578, upload-time = "2025-06-09T23:00:13.526Z" }, - { url = "https://files.pythonhosted.org/packages/8f/5f/f69818f017fa9a3d24d1ae39763e29b7f60a59e46d5f91b9c6b21622f4cd/frozenlist-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:6eb93efb8101ef39d32d50bce242c84bcbddb4f7e9febfa7b524532a239b4464", size = 43830, upload-time = "2025-06-09T23:00:14.98Z" }, - { url = "https://files.pythonhosted.org/packages/34/7e/803dde33760128acd393a27eb002f2020ddb8d99d30a44bfbaab31c5f08a/frozenlist-1.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa51e147a66b2d74de1e6e2cf5921890de6b0f4820b257465101d7f37b49fb5a", size = 82251, upload-time = "2025-06-09T23:00:16.279Z" }, - { url = "https://files.pythonhosted.org/packages/75/a9/9c2c5760b6ba45eae11334db454c189d43d34a4c0b489feb2175e5e64277/frozenlist-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9b35db7ce1cd71d36ba24f80f0c9e7cff73a28d7a74e91fe83e23d27c7828750", size = 48183, upload-time = "2025-06-09T23:00:17.698Z" }, - { url = "https://files.pythonhosted.org/packages/47/be/4038e2d869f8a2da165f35a6befb9158c259819be22eeaf9c9a8f6a87771/frozenlist-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34a69a85e34ff37791e94542065c8416c1afbf820b68f720452f636d5fb990cd", size = 47107, upload-time = "2025-06-09T23:00:18.952Z" }, - { url = "https://files.pythonhosted.org/packages/79/26/85314b8a83187c76a37183ceed886381a5f992975786f883472fcb6dc5f2/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a646531fa8d82c87fe4bb2e596f23173caec9185bfbca5d583b4ccfb95183e2", size = 237333, upload-time = "2025-06-09T23:00:20.275Z" }, - { url = "https://files.pythonhosted.org/packages/1f/fd/e5b64f7d2c92a41639ffb2ad44a6a82f347787abc0c7df5f49057cf11770/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:79b2ffbba483f4ed36a0f236ccb85fbb16e670c9238313709638167670ba235f", size = 231724, upload-time = "2025-06-09T23:00:21.705Z" }, - { url = "https://files.pythonhosted.org/packages/20/fb/03395c0a43a5976af4bf7534759d214405fbbb4c114683f434dfdd3128ef/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a26f205c9ca5829cbf82bb2a84b5c36f7184c4316617d7ef1b271a56720d6b30", size = 245842, upload-time = "2025-06-09T23:00:23.148Z" }, - { url = "https://files.pythonhosted.org/packages/d0/15/c01c8e1dffdac5d9803507d824f27aed2ba76b6ed0026fab4d9866e82f1f/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bcacfad3185a623fa11ea0e0634aac7b691aa925d50a440f39b458e41c561d98", size = 239767, upload-time = "2025-06-09T23:00:25.103Z" }, - { url = "https://files.pythonhosted.org/packages/14/99/3f4c6fe882c1f5514b6848aa0a69b20cb5e5d8e8f51a339d48c0e9305ed0/frozenlist-1.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72c1b0fe8fe451b34f12dce46445ddf14bd2a5bcad7e324987194dc8e3a74c86", size = 224130, upload-time = "2025-06-09T23:00:27.061Z" }, - { url = "https://files.pythonhosted.org/packages/4d/83/220a374bd7b2aeba9d0725130665afe11de347d95c3620b9b82cc2fcab97/frozenlist-1.7.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d1a5baeaac6c0798ff6edfaeaa00e0e412d49946c53fae8d4b8e8b3566c4ae", size = 235301, upload-time = "2025-06-09T23:00:29.02Z" }, - { url = "https://files.pythonhosted.org/packages/03/3c/3e3390d75334a063181625343e8daab61b77e1b8214802cc4e8a1bb678fc/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7edf5c043c062462f09b6820de9854bf28cc6cc5b6714b383149745e287181a8", size = 234606, upload-time = "2025-06-09T23:00:30.514Z" }, - { url = "https://files.pythonhosted.org/packages/23/1e/58232c19608b7a549d72d9903005e2d82488f12554a32de2d5fb59b9b1ba/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d50ac7627b3a1bd2dcef6f9da89a772694ec04d9a61b66cf87f7d9446b4a0c31", size = 248372, upload-time = "2025-06-09T23:00:31.966Z" }, - { url = "https://files.pythonhosted.org/packages/c0/a4/e4a567e01702a88a74ce8a324691e62a629bf47d4f8607f24bf1c7216e7f/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce48b2fece5aeb45265bb7a58259f45027db0abff478e3077e12b05b17fb9da7", size = 229860, upload-time = "2025-06-09T23:00:33.375Z" }, - { url = "https://files.pythonhosted.org/packages/73/a6/63b3374f7d22268b41a9db73d68a8233afa30ed164c46107b33c4d18ecdd/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:fe2365ae915a1fafd982c146754e1de6ab3478def8a59c86e1f7242d794f97d5", size = 245893, upload-time = "2025-06-09T23:00:35.002Z" }, - { url = "https://files.pythonhosted.org/packages/6d/eb/d18b3f6e64799a79673c4ba0b45e4cfbe49c240edfd03a68be20002eaeaa/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:45a6f2fdbd10e074e8814eb98b05292f27bad7d1883afbe009d96abdcf3bc898", size = 246323, upload-time = "2025-06-09T23:00:36.468Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f5/720f3812e3d06cd89a1d5db9ff6450088b8f5c449dae8ffb2971a44da506/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:21884e23cffabb157a9dd7e353779077bf5b8f9a58e9b262c6caad2ef5f80a56", size = 233149, upload-time = "2025-06-09T23:00:37.963Z" }, - { url = "https://files.pythonhosted.org/packages/69/68/03efbf545e217d5db8446acfd4c447c15b7c8cf4dbd4a58403111df9322d/frozenlist-1.7.0-cp311-cp311-win32.whl", hash = "sha256:284d233a8953d7b24f9159b8a3496fc1ddc00f4db99c324bd5fb5f22d8698ea7", size = 39565, upload-time = "2025-06-09T23:00:39.753Z" }, - { url = "https://files.pythonhosted.org/packages/58/17/fe61124c5c333ae87f09bb67186d65038834a47d974fc10a5fadb4cc5ae1/frozenlist-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:387cbfdcde2f2353f19c2f66bbb52406d06ed77519ac7ee21be0232147c2592d", size = 44019, upload-time = "2025-06-09T23:00:40.988Z" }, - { url = "https://files.pythonhosted.org/packages/ef/a2/c8131383f1e66adad5f6ecfcce383d584ca94055a34d683bbb24ac5f2f1c/frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2", size = 81424, upload-time = "2025-06-09T23:00:42.24Z" }, - { url = "https://files.pythonhosted.org/packages/4c/9d/02754159955088cb52567337d1113f945b9e444c4960771ea90eb73de8db/frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb", size = 47952, upload-time = "2025-06-09T23:00:43.481Z" }, - { url = "https://files.pythonhosted.org/packages/01/7a/0046ef1bd6699b40acd2067ed6d6670b4db2f425c56980fa21c982c2a9db/frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478", size = 46688, upload-time = "2025-06-09T23:00:44.793Z" }, - { url = "https://files.pythonhosted.org/packages/d6/a2/a910bafe29c86997363fb4c02069df4ff0b5bc39d33c5198b4e9dd42d8f8/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8", size = 243084, upload-time = "2025-06-09T23:00:46.125Z" }, - { url = "https://files.pythonhosted.org/packages/64/3e/5036af9d5031374c64c387469bfcc3af537fc0f5b1187d83a1cf6fab1639/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08", size = 233524, upload-time = "2025-06-09T23:00:47.73Z" }, - { url = "https://files.pythonhosted.org/packages/06/39/6a17b7c107a2887e781a48ecf20ad20f1c39d94b2a548c83615b5b879f28/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4", size = 248493, upload-time = "2025-06-09T23:00:49.742Z" }, - { url = "https://files.pythonhosted.org/packages/be/00/711d1337c7327d88c44d91dd0f556a1c47fb99afc060ae0ef66b4d24793d/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b", size = 244116, upload-time = "2025-06-09T23:00:51.352Z" }, - { url = "https://files.pythonhosted.org/packages/24/fe/74e6ec0639c115df13d5850e75722750adabdc7de24e37e05a40527ca539/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e", size = 224557, upload-time = "2025-06-09T23:00:52.855Z" }, - { url = "https://files.pythonhosted.org/packages/8d/db/48421f62a6f77c553575201e89048e97198046b793f4a089c79a6e3268bd/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca", size = 241820, upload-time = "2025-06-09T23:00:54.43Z" }, - { url = "https://files.pythonhosted.org/packages/1d/fa/cb4a76bea23047c8462976ea7b7a2bf53997a0ca171302deae9d6dd12096/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df", size = 236542, upload-time = "2025-06-09T23:00:56.409Z" }, - { url = "https://files.pythonhosted.org/packages/5d/32/476a4b5cfaa0ec94d3f808f193301debff2ea42288a099afe60757ef6282/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5", size = 249350, upload-time = "2025-06-09T23:00:58.468Z" }, - { url = "https://files.pythonhosted.org/packages/8d/ba/9a28042f84a6bf8ea5dbc81cfff8eaef18d78b2a1ad9d51c7bc5b029ad16/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025", size = 225093, upload-time = "2025-06-09T23:01:00.015Z" }, - { url = "https://files.pythonhosted.org/packages/bc/29/3a32959e68f9cf000b04e79ba574527c17e8842e38c91d68214a37455786/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01", size = 245482, upload-time = "2025-06-09T23:01:01.474Z" }, - { url = "https://files.pythonhosted.org/packages/80/e8/edf2f9e00da553f07f5fa165325cfc302dead715cab6ac8336a5f3d0adc2/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08", size = 249590, upload-time = "2025-06-09T23:01:02.961Z" }, - { url = "https://files.pythonhosted.org/packages/1c/80/9a0eb48b944050f94cc51ee1c413eb14a39543cc4f760ed12657a5a3c45a/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43", size = 237785, upload-time = "2025-06-09T23:01:05.095Z" }, - { url = "https://files.pythonhosted.org/packages/f3/74/87601e0fb0369b7a2baf404ea921769c53b7ae00dee7dcfe5162c8c6dbf0/frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3", size = 39487, upload-time = "2025-06-09T23:01:06.54Z" }, - { url = "https://files.pythonhosted.org/packages/0b/15/c026e9a9fc17585a9d461f65d8593d281fedf55fbf7eb53f16c6df2392f9/frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a", size = 43874, upload-time = "2025-06-09T23:01:07.752Z" }, - { url = "https://files.pythonhosted.org/packages/24/90/6b2cebdabdbd50367273c20ff6b57a3dfa89bd0762de02c3a1eb42cb6462/frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee", size = 79791, upload-time = "2025-06-09T23:01:09.368Z" }, - { url = "https://files.pythonhosted.org/packages/83/2e/5b70b6a3325363293fe5fc3ae74cdcbc3e996c2a11dde2fd9f1fb0776d19/frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d", size = 47165, upload-time = "2025-06-09T23:01:10.653Z" }, - { url = "https://files.pythonhosted.org/packages/f4/25/a0895c99270ca6966110f4ad98e87e5662eab416a17e7fd53c364bf8b954/frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43", size = 45881, upload-time = "2025-06-09T23:01:12.296Z" }, - { url = "https://files.pythonhosted.org/packages/19/7c/71bb0bbe0832793c601fff68cd0cf6143753d0c667f9aec93d3c323f4b55/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d", size = 232409, upload-time = "2025-06-09T23:01:13.641Z" }, - { url = "https://files.pythonhosted.org/packages/c0/45/ed2798718910fe6eb3ba574082aaceff4528e6323f9a8570be0f7028d8e9/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee", size = 225132, upload-time = "2025-06-09T23:01:15.264Z" }, - { url = "https://files.pythonhosted.org/packages/ba/e2/8417ae0f8eacb1d071d4950f32f229aa6bf68ab69aab797b72a07ea68d4f/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb", size = 237638, upload-time = "2025-06-09T23:01:16.752Z" }, - { url = "https://files.pythonhosted.org/packages/f8/b7/2ace5450ce85f2af05a871b8c8719b341294775a0a6c5585d5e6170f2ce7/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f", size = 233539, upload-time = "2025-06-09T23:01:18.202Z" }, - { url = "https://files.pythonhosted.org/packages/46/b9/6989292c5539553dba63f3c83dc4598186ab2888f67c0dc1d917e6887db6/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60", size = 215646, upload-time = "2025-06-09T23:01:19.649Z" }, - { url = "https://files.pythonhosted.org/packages/72/31/bc8c5c99c7818293458fe745dab4fd5730ff49697ccc82b554eb69f16a24/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00", size = 232233, upload-time = "2025-06-09T23:01:21.175Z" }, - { url = "https://files.pythonhosted.org/packages/59/52/460db4d7ba0811b9ccb85af996019f5d70831f2f5f255f7cc61f86199795/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b", size = 227996, upload-time = "2025-06-09T23:01:23.098Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c9/f4b39e904c03927b7ecf891804fd3b4df3db29b9e487c6418e37988d6e9d/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c", size = 242280, upload-time = "2025-06-09T23:01:24.808Z" }, - { url = "https://files.pythonhosted.org/packages/b8/33/3f8d6ced42f162d743e3517781566b8481322be321b486d9d262adf70bfb/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949", size = 217717, upload-time = "2025-06-09T23:01:26.28Z" }, - { url = "https://files.pythonhosted.org/packages/3e/e8/ad683e75da6ccef50d0ab0c2b2324b32f84fc88ceee778ed79b8e2d2fe2e/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca", size = 236644, upload-time = "2025-06-09T23:01:27.887Z" }, - { url = "https://files.pythonhosted.org/packages/b2/14/8d19ccdd3799310722195a72ac94ddc677541fb4bef4091d8e7775752360/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b", size = 238879, upload-time = "2025-06-09T23:01:29.524Z" }, - { url = "https://files.pythonhosted.org/packages/ce/13/c12bf657494c2fd1079a48b2db49fa4196325909249a52d8f09bc9123fd7/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e", size = 232502, upload-time = "2025-06-09T23:01:31.287Z" }, - { url = "https://files.pythonhosted.org/packages/d7/8b/e7f9dfde869825489382bc0d512c15e96d3964180c9499efcec72e85db7e/frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1", size = 39169, upload-time = "2025-06-09T23:01:35.503Z" }, - { url = "https://files.pythonhosted.org/packages/35/89/a487a98d94205d85745080a37860ff5744b9820a2c9acbcdd9440bfddf98/frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba", size = 43219, upload-time = "2025-06-09T23:01:36.784Z" }, - { url = "https://files.pythonhosted.org/packages/56/d5/5c4cf2319a49eddd9dd7145e66c4866bdc6f3dbc67ca3d59685149c11e0d/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d", size = 84345, upload-time = "2025-06-09T23:01:38.295Z" }, - { url = "https://files.pythonhosted.org/packages/a4/7d/ec2c1e1dc16b85bc9d526009961953df9cec8481b6886debb36ec9107799/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d", size = 48880, upload-time = "2025-06-09T23:01:39.887Z" }, - { url = "https://files.pythonhosted.org/packages/69/86/f9596807b03de126e11e7d42ac91e3d0b19a6599c714a1989a4e85eeefc4/frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b", size = 48498, upload-time = "2025-06-09T23:01:41.318Z" }, - { url = "https://files.pythonhosted.org/packages/5e/cb/df6de220f5036001005f2d726b789b2c0b65f2363b104bbc16f5be8084f8/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146", size = 292296, upload-time = "2025-06-09T23:01:42.685Z" }, - { url = "https://files.pythonhosted.org/packages/83/1f/de84c642f17c8f851a2905cee2dae401e5e0daca9b5ef121e120e19aa825/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74", size = 273103, upload-time = "2025-06-09T23:01:44.166Z" }, - { url = "https://files.pythonhosted.org/packages/88/3c/c840bfa474ba3fa13c772b93070893c6e9d5c0350885760376cbe3b6c1b3/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1", size = 292869, upload-time = "2025-06-09T23:01:45.681Z" }, - { url = "https://files.pythonhosted.org/packages/a6/1c/3efa6e7d5a39a1d5ef0abeb51c48fb657765794a46cf124e5aca2c7a592c/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1", size = 291467, upload-time = "2025-06-09T23:01:47.234Z" }, - { url = "https://files.pythonhosted.org/packages/4f/00/d5c5e09d4922c395e2f2f6b79b9a20dab4b67daaf78ab92e7729341f61f6/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384", size = 266028, upload-time = "2025-06-09T23:01:48.819Z" }, - { url = "https://files.pythonhosted.org/packages/4e/27/72765be905619dfde25a7f33813ac0341eb6b076abede17a2e3fbfade0cb/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb", size = 284294, upload-time = "2025-06-09T23:01:50.394Z" }, - { url = "https://files.pythonhosted.org/packages/88/67/c94103a23001b17808eb7dd1200c156bb69fb68e63fcf0693dde4cd6228c/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c", size = 281898, upload-time = "2025-06-09T23:01:52.234Z" }, - { url = "https://files.pythonhosted.org/packages/42/34/a3e2c00c00f9e2a9db5653bca3fec306349e71aff14ae45ecc6d0951dd24/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65", size = 290465, upload-time = "2025-06-09T23:01:53.788Z" }, - { url = "https://files.pythonhosted.org/packages/bb/73/f89b7fbce8b0b0c095d82b008afd0590f71ccb3dee6eee41791cf8cd25fd/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3", size = 266385, upload-time = "2025-06-09T23:01:55.769Z" }, - { url = "https://files.pythonhosted.org/packages/cd/45/e365fdb554159462ca12df54bc59bfa7a9a273ecc21e99e72e597564d1ae/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657", size = 288771, upload-time = "2025-06-09T23:01:57.4Z" }, - { url = "https://files.pythonhosted.org/packages/00/11/47b6117002a0e904f004d70ec5194fe9144f117c33c851e3d51c765962d0/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104", size = 288206, upload-time = "2025-06-09T23:01:58.936Z" }, - { url = "https://files.pythonhosted.org/packages/40/37/5f9f3c3fd7f7746082ec67bcdc204db72dad081f4f83a503d33220a92973/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf", size = 282620, upload-time = "2025-06-09T23:02:00.493Z" }, - { url = "https://files.pythonhosted.org/packages/0b/31/8fbc5af2d183bff20f21aa743b4088eac4445d2bb1cdece449ae80e4e2d1/frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81", size = 43059, upload-time = "2025-06-09T23:02:02.072Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ed/41956f52105b8dbc26e457c5705340c67c8cc2b79f394b79bffc09d0e938/frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e", size = 47516, upload-time = "2025-06-09T23:02:03.779Z" }, - { url = "https://files.pythonhosted.org/packages/dd/b1/ee59496f51cd244039330015d60f13ce5a54a0f2bd8d79e4a4a375ab7469/frozenlist-1.7.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cea3dbd15aea1341ea2de490574a4a37ca080b2ae24e4b4f4b51b9057b4c3630", size = 82434, upload-time = "2025-06-09T23:02:05.195Z" }, - { url = "https://files.pythonhosted.org/packages/75/e1/d518391ce36a6279b3fa5bc14327dde80bcb646bb50d059c6ca0756b8d05/frozenlist-1.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7d536ee086b23fecc36c2073c371572374ff50ef4db515e4e503925361c24f71", size = 48232, upload-time = "2025-06-09T23:02:07.728Z" }, - { url = "https://files.pythonhosted.org/packages/b7/8d/a0d04f28b6e821a9685c22e67b5fb798a5a7b68752f104bfbc2dccf080c4/frozenlist-1.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dfcebf56f703cb2e346315431699f00db126d158455e513bd14089d992101e44", size = 47186, upload-time = "2025-06-09T23:02:09.243Z" }, - { url = "https://files.pythonhosted.org/packages/93/3a/a5334c0535c8b7c78eeabda1579179e44fe3d644e07118e59a2276dedaf1/frozenlist-1.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:974c5336e61d6e7eb1ea5b929cb645e882aadab0095c5a6974a111e6479f8878", size = 226617, upload-time = "2025-06-09T23:02:10.949Z" }, - { url = "https://files.pythonhosted.org/packages/0a/67/8258d971f519dc3f278c55069a775096cda6610a267b53f6248152b72b2f/frozenlist-1.7.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c70db4a0ab5ab20878432c40563573229a7ed9241506181bba12f6b7d0dc41cb", size = 224179, upload-time = "2025-06-09T23:02:12.603Z" }, - { url = "https://files.pythonhosted.org/packages/fc/89/8225905bf889b97c6d935dd3aeb45668461e59d415cb019619383a8a7c3b/frozenlist-1.7.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1137b78384eebaf70560a36b7b229f752fb64d463d38d1304939984d5cb887b6", size = 235783, upload-time = "2025-06-09T23:02:14.678Z" }, - { url = "https://files.pythonhosted.org/packages/54/6e/ef52375aa93d4bc510d061df06205fa6dcfd94cd631dd22956b09128f0d4/frozenlist-1.7.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e793a9f01b3e8b5c0bc646fb59140ce0efcc580d22a3468d70766091beb81b35", size = 229210, upload-time = "2025-06-09T23:02:16.313Z" }, - { url = "https://files.pythonhosted.org/packages/ee/55/62c87d1a6547bfbcd645df10432c129100c5bd0fd92a384de6e3378b07c1/frozenlist-1.7.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74739ba8e4e38221d2c5c03d90a7e542cb8ad681915f4ca8f68d04f810ee0a87", size = 215994, upload-time = "2025-06-09T23:02:17.9Z" }, - { url = "https://files.pythonhosted.org/packages/45/d2/263fea1f658b8ad648c7d94d18a87bca7e8c67bd6a1bbf5445b1bd5b158c/frozenlist-1.7.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e63344c4e929b1a01e29bc184bbb5fd82954869033765bfe8d65d09e336a677", size = 225122, upload-time = "2025-06-09T23:02:19.479Z" }, - { url = "https://files.pythonhosted.org/packages/7b/22/7145e35d12fb368d92124f679bea87309495e2e9ddf14c6533990cb69218/frozenlist-1.7.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2ea2a7369eb76de2217a842f22087913cdf75f63cf1307b9024ab82dfb525938", size = 224019, upload-time = "2025-06-09T23:02:20.969Z" }, - { url = "https://files.pythonhosted.org/packages/44/1e/7dae8c54301beb87bcafc6144b9a103bfd2c8f38078c7902984c9a0c4e5b/frozenlist-1.7.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:836b42f472a0e006e02499cef9352ce8097f33df43baaba3e0a28a964c26c7d2", size = 239925, upload-time = "2025-06-09T23:02:22.466Z" }, - { url = "https://files.pythonhosted.org/packages/4b/1e/99c93e54aa382e949a98976a73b9b20c3aae6d9d893f31bbe4991f64e3a8/frozenlist-1.7.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e22b9a99741294b2571667c07d9f8cceec07cb92aae5ccda39ea1b6052ed4319", size = 220881, upload-time = "2025-06-09T23:02:24.521Z" }, - { url = "https://files.pythonhosted.org/packages/5e/9c/ca5105fa7fb5abdfa8837581be790447ae051da75d32f25c8f81082ffc45/frozenlist-1.7.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:9a19e85cc503d958abe5218953df722748d87172f71b73cf3c9257a91b999890", size = 234046, upload-time = "2025-06-09T23:02:26.206Z" }, - { url = "https://files.pythonhosted.org/packages/8d/4d/e99014756093b4ddbb67fb8f0df11fe7a415760d69ace98e2ac6d5d43402/frozenlist-1.7.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f22dac33bb3ee8fe3e013aa7b91dc12f60d61d05b7fe32191ffa84c3aafe77bd", size = 235756, upload-time = "2025-06-09T23:02:27.79Z" }, - { url = "https://files.pythonhosted.org/packages/8b/72/a19a40bcdaa28a51add2aaa3a1a294ec357f36f27bd836a012e070c5e8a5/frozenlist-1.7.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9ccec739a99e4ccf664ea0775149f2749b8a6418eb5b8384b4dc0a7d15d304cb", size = 222894, upload-time = "2025-06-09T23:02:29.848Z" }, - { url = "https://files.pythonhosted.org/packages/08/49/0042469993e023a758af81db68c76907cd29e847d772334d4d201cbe9a42/frozenlist-1.7.0-cp39-cp39-win32.whl", hash = "sha256:b3950f11058310008a87757f3eee16a8e1ca97979833239439586857bc25482e", size = 39848, upload-time = "2025-06-09T23:02:31.413Z" }, - { url = "https://files.pythonhosted.org/packages/5a/45/827d86ee475c877f5f766fbc23fb6acb6fada9e52f1c9720e2ba3eae32da/frozenlist-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:43a82fce6769c70f2f5a06248b614a7d268080a9d20f7457ef10ecee5af82b63", size = 44102, upload-time = "2025-06-09T23:02:32.808Z" }, - { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] [[package]] -name = "fsspec" -version = "2025.7.0" +name = "execnet" +version = "2.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8b/02/0835e6ab9cfc03916fe3f78c0956cfcdb6ff2669ffa6651065d5ebf7fc98/fsspec-2025.7.0.tar.gz", hash = "sha256:786120687ffa54b8283d942929540d8bc5ccfa820deb555a2b5d0ed2b737bf58", size = 304432, upload-time = "2025-07-15T16:05:21.19Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/e0/014d5d9d7a4564cf1c40b5039bc882db69fd881111e03ab3657ac0b218e2/fsspec-2025.7.0-py3-none-any.whl", hash = "sha256:8b012e39f63c7d5f10474de957f3ab793b47b45ae7d39f2fb735f8bbe25c0e21", size = 199597, upload-time = "2025-07-15T16:05:19.529Z" }, + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, ] [[package]] -name = "griffe" -version = "1.13.0" +name = "fastapi" +version = "0.139.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama" }, + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/b5/23b91f22b7b3a7f8f62223f6664946271c0f5cb4179605a3e6bbae863920/griffe-1.13.0.tar.gz", hash = "sha256:246ea436a5e78f7fbf5f24ca8a727bb4d2a4b442a2959052eea3d0bfe9a076e0", size = 412759, upload-time = "2025-08-26T13:27:11.422Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/95/d3f0ae10836324a2eab98a52b61210ac609f08200bf4bb0dc8132d32f78a/fastapi-0.139.2.tar.gz", hash = "sha256:333145a6891e9b5b3cfceb69baf817e8240cde4d4588ae5a10bf56ffacb6255e", size = 423428, upload-time = "2026-07-16T15:06:17.912Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/8c/b7cfdd8dfe48f6b09f7353323732e1a290c388bd14f216947928dc85f904/griffe-1.13.0-py3-none-any.whl", hash = "sha256:470fde5b735625ac0a36296cd194617f039e9e83e301fcbd493e2b58382d0559", size = 139365, upload-time = "2025-08-26T13:27:09.882Z" }, -] - -[[package]] -name = "grpcio" -version = "1.74.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/38/b4/35feb8f7cab7239c5b94bd2db71abb3d6adb5f335ad8f131abb6060840b6/grpcio-1.74.0.tar.gz", hash = "sha256:80d1f4fbb35b0742d3e3d3bb654b7381cd5f015f8497279a1e9c21ba623e01b1", size = 12756048, upload-time = "2025-07-24T18:54:23.039Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/66/54/68e51a90797ad7afc5b0a7881426c337f6a9168ebab73c3210b76aa7c90d/grpcio-1.74.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:85bd5cdf4ed7b2d6438871adf6afff9af7096486fcf51818a81b77ef4dd30907", size = 5481935, upload-time = "2025-07-24T18:52:43.756Z" }, - { url = "https://files.pythonhosted.org/packages/32/2a/af817c7e9843929e93e54d09c9aee2555c2e8d81b93102a9426b36e91833/grpcio-1.74.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:68c8ebcca945efff9d86d8d6d7bfb0841cf0071024417e2d7f45c5e46b5b08eb", size = 10986796, upload-time = "2025-07-24T18:52:47.219Z" }, - { url = "https://files.pythonhosted.org/packages/d5/94/d67756638d7bb07750b07d0826c68e414124574b53840ba1ff777abcd388/grpcio-1.74.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:e154d230dc1bbbd78ad2fdc3039fa50ad7ffcf438e4eb2fa30bce223a70c7486", size = 5983663, upload-time = "2025-07-24T18:52:49.463Z" }, - { url = "https://files.pythonhosted.org/packages/35/f5/c5e4853bf42148fea8532d49e919426585b73eafcf379a712934652a8de9/grpcio-1.74.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8978003816c7b9eabe217f88c78bc26adc8f9304bf6a594b02e5a49b2ef9c11", size = 6653765, upload-time = "2025-07-24T18:52:51.094Z" }, - { url = "https://files.pythonhosted.org/packages/fd/75/a1991dd64b331d199935e096cc9daa3415ee5ccbe9f909aa48eded7bba34/grpcio-1.74.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3d7bd6e3929fd2ea7fbc3f562e4987229ead70c9ae5f01501a46701e08f1ad9", size = 6215172, upload-time = "2025-07-24T18:52:53.282Z" }, - { url = "https://files.pythonhosted.org/packages/01/a4/7cef3dbb3b073d0ce34fd507efc44ac4c9442a0ef9fba4fb3f5c551efef5/grpcio-1.74.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:136b53c91ac1d02c8c24201bfdeb56f8b3ac3278668cbb8e0ba49c88069e1bdc", size = 6329142, upload-time = "2025-07-24T18:52:54.927Z" }, - { url = "https://files.pythonhosted.org/packages/bf/d3/587920f882b46e835ad96014087054655312400e2f1f1446419e5179a383/grpcio-1.74.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fe0f540750a13fd8e5da4b3eaba91a785eea8dca5ccd2bc2ffe978caa403090e", size = 7018632, upload-time = "2025-07-24T18:52:56.523Z" }, - { url = "https://files.pythonhosted.org/packages/1f/95/c70a3b15a0bc83334b507e3d2ae20ee8fa38d419b8758a4d838f5c2a7d32/grpcio-1.74.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4e4181bfc24413d1e3a37a0b7889bea68d973d4b45dd2bc68bb766c140718f82", size = 6509641, upload-time = "2025-07-24T18:52:58.495Z" }, - { url = "https://files.pythonhosted.org/packages/4b/06/2e7042d06247d668ae69ea6998eca33f475fd4e2855f94dcb2aa5daef334/grpcio-1.74.0-cp310-cp310-win32.whl", hash = "sha256:1733969040989f7acc3d94c22f55b4a9501a30f6aaacdbccfaba0a3ffb255ab7", size = 3817478, upload-time = "2025-07-24T18:53:00.128Z" }, - { url = "https://files.pythonhosted.org/packages/93/20/e02b9dcca3ee91124060b65bbf5b8e1af80b3b76a30f694b44b964ab4d71/grpcio-1.74.0-cp310-cp310-win_amd64.whl", hash = "sha256:9e912d3c993a29df6c627459af58975b2e5c897d93287939b9d5065f000249b5", size = 4493971, upload-time = "2025-07-24T18:53:02.068Z" }, - { url = "https://files.pythonhosted.org/packages/e7/77/b2f06db9f240a5abeddd23a0e49eae2b6ac54d85f0e5267784ce02269c3b/grpcio-1.74.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:69e1a8180868a2576f02356565f16635b99088da7df3d45aaa7e24e73a054e31", size = 5487368, upload-time = "2025-07-24T18:53:03.548Z" }, - { url = "https://files.pythonhosted.org/packages/48/99/0ac8678a819c28d9a370a663007581744a9f2a844e32f0fa95e1ddda5b9e/grpcio-1.74.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8efe72fde5500f47aca1ef59495cb59c885afe04ac89dd11d810f2de87d935d4", size = 10999804, upload-time = "2025-07-24T18:53:05.095Z" }, - { url = "https://files.pythonhosted.org/packages/45/c6/a2d586300d9e14ad72e8dc211c7aecb45fe9846a51e558c5bca0c9102c7f/grpcio-1.74.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:a8f0302f9ac4e9923f98d8e243939a6fb627cd048f5cd38595c97e38020dffce", size = 5987667, upload-time = "2025-07-24T18:53:07.157Z" }, - { url = "https://files.pythonhosted.org/packages/c9/57/5f338bf56a7f22584e68d669632e521f0de460bb3749d54533fc3d0fca4f/grpcio-1.74.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f609a39f62a6f6f05c7512746798282546358a37ea93c1fcbadf8b2fed162e3", size = 6655612, upload-time = "2025-07-24T18:53:09.244Z" }, - { url = "https://files.pythonhosted.org/packages/82/ea/a4820c4c44c8b35b1903a6c72a5bdccec92d0840cf5c858c498c66786ba5/grpcio-1.74.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c98e0b7434a7fa4e3e63f250456eaef52499fba5ae661c58cc5b5477d11e7182", size = 6219544, upload-time = "2025-07-24T18:53:11.221Z" }, - { url = "https://files.pythonhosted.org/packages/a4/17/0537630a921365928f5abb6d14c79ba4dcb3e662e0dbeede8af4138d9dcf/grpcio-1.74.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:662456c4513e298db6d7bd9c3b8df6f75f8752f0ba01fb653e252ed4a59b5a5d", size = 6334863, upload-time = "2025-07-24T18:53:12.925Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a6/85ca6cb9af3f13e1320d0a806658dca432ff88149d5972df1f7b51e87127/grpcio-1.74.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3d14e3c4d65e19d8430a4e28ceb71ace4728776fd6c3ce34016947474479683f", size = 7019320, upload-time = "2025-07-24T18:53:15.002Z" }, - { url = "https://files.pythonhosted.org/packages/4f/a7/fe2beab970a1e25d2eff108b3cf4f7d9a53c185106377a3d1989216eba45/grpcio-1.74.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bf949792cee20d2078323a9b02bacbbae002b9e3b9e2433f2741c15bdeba1c4", size = 6514228, upload-time = "2025-07-24T18:53:16.999Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c2/2f9c945c8a248cebc3ccda1b7a1bf1775b9d7d59e444dbb18c0014e23da6/grpcio-1.74.0-cp311-cp311-win32.whl", hash = "sha256:55b453812fa7c7ce2f5c88be3018fb4a490519b6ce80788d5913f3f9d7da8c7b", size = 3817216, upload-time = "2025-07-24T18:53:20.564Z" }, - { url = "https://files.pythonhosted.org/packages/ff/d1/a9cf9c94b55becda2199299a12b9feef0c79946b0d9d34c989de6d12d05d/grpcio-1.74.0-cp311-cp311-win_amd64.whl", hash = "sha256:86ad489db097141a907c559988c29718719aa3e13370d40e20506f11b4de0d11", size = 4495380, upload-time = "2025-07-24T18:53:22.058Z" }, - { url = "https://files.pythonhosted.org/packages/4c/5d/e504d5d5c4469823504f65687d6c8fb97b7f7bf0b34873b7598f1df24630/grpcio-1.74.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:8533e6e9c5bd630ca98062e3a1326249e6ada07d05acf191a77bc33f8948f3d8", size = 5445551, upload-time = "2025-07-24T18:53:23.641Z" }, - { url = "https://files.pythonhosted.org/packages/43/01/730e37056f96f2f6ce9f17999af1556df62ee8dab7fa48bceeaab5fd3008/grpcio-1.74.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:2918948864fec2a11721d91568effffbe0a02b23ecd57f281391d986847982f6", size = 10979810, upload-time = "2025-07-24T18:53:25.349Z" }, - { url = "https://files.pythonhosted.org/packages/79/3d/09fd100473ea5c47083889ca47ffd356576173ec134312f6aa0e13111dee/grpcio-1.74.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:60d2d48b0580e70d2e1954d0d19fa3c2e60dd7cbed826aca104fff518310d1c5", size = 5941946, upload-time = "2025-07-24T18:53:27.387Z" }, - { url = "https://files.pythonhosted.org/packages/8a/99/12d2cca0a63c874c6d3d195629dcd85cdf5d6f98a30d8db44271f8a97b93/grpcio-1.74.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3601274bc0523f6dc07666c0e01682c94472402ac2fd1226fd96e079863bfa49", size = 6621763, upload-time = "2025-07-24T18:53:29.193Z" }, - { url = "https://files.pythonhosted.org/packages/9d/2c/930b0e7a2f1029bbc193443c7bc4dc2a46fedb0203c8793dcd97081f1520/grpcio-1.74.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:176d60a5168d7948539def20b2a3adcce67d72454d9ae05969a2e73f3a0feee7", size = 6180664, upload-time = "2025-07-24T18:53:30.823Z" }, - { url = "https://files.pythonhosted.org/packages/db/d5/ff8a2442180ad0867717e670f5ec42bfd8d38b92158ad6bcd864e6d4b1ed/grpcio-1.74.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e759f9e8bc908aaae0412642afe5416c9f983a80499448fcc7fab8692ae044c3", size = 6301083, upload-time = "2025-07-24T18:53:32.454Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ba/b361d390451a37ca118e4ec7dccec690422e05bc85fba2ec72b06cefec9f/grpcio-1.74.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9e7c4389771855a92934b2846bd807fc25a3dfa820fd912fe6bd8136026b2707", size = 6994132, upload-time = "2025-07-24T18:53:34.506Z" }, - { url = "https://files.pythonhosted.org/packages/3b/0c/3a5fa47d2437a44ced74141795ac0251bbddeae74bf81df3447edd767d27/grpcio-1.74.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cce634b10aeab37010449124814b05a62fb5f18928ca878f1bf4750d1f0c815b", size = 6489616, upload-time = "2025-07-24T18:53:36.217Z" }, - { url = "https://files.pythonhosted.org/packages/ae/95/ab64703b436d99dc5217228babc76047d60e9ad14df129e307b5fec81fd0/grpcio-1.74.0-cp312-cp312-win32.whl", hash = "sha256:885912559974df35d92219e2dc98f51a16a48395f37b92865ad45186f294096c", size = 3807083, upload-time = "2025-07-24T18:53:37.911Z" }, - { url = "https://files.pythonhosted.org/packages/84/59/900aa2445891fc47a33f7d2f76e00ca5d6ae6584b20d19af9c06fa09bf9a/grpcio-1.74.0-cp312-cp312-win_amd64.whl", hash = "sha256:42f8fee287427b94be63d916c90399ed310ed10aadbf9e2e5538b3e497d269bc", size = 4490123, upload-time = "2025-07-24T18:53:39.528Z" }, - { url = "https://files.pythonhosted.org/packages/d4/d8/1004a5f468715221450e66b051c839c2ce9a985aa3ee427422061fcbb6aa/grpcio-1.74.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:2bc2d7d8d184e2362b53905cb1708c84cb16354771c04b490485fa07ce3a1d89", size = 5449488, upload-time = "2025-07-24T18:53:41.174Z" }, - { url = "https://files.pythonhosted.org/packages/94/0e/33731a03f63740d7743dced423846c831d8e6da808fcd02821a4416df7fa/grpcio-1.74.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:c14e803037e572c177ba54a3e090d6eb12efd795d49327c5ee2b3bddb836bf01", size = 10974059, upload-time = "2025-07-24T18:53:43.066Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c6/3d2c14d87771a421205bdca991467cfe473ee4c6a1231c1ede5248c62ab8/grpcio-1.74.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:f6ec94f0e50eb8fa1744a731088b966427575e40c2944a980049798b127a687e", size = 5945647, upload-time = "2025-07-24T18:53:45.269Z" }, - { url = "https://files.pythonhosted.org/packages/c5/83/5a354c8aaff58594eef7fffebae41a0f8995a6258bbc6809b800c33d4c13/grpcio-1.74.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:566b9395b90cc3d0d0c6404bc8572c7c18786ede549cdb540ae27b58afe0fb91", size = 6626101, upload-time = "2025-07-24T18:53:47.015Z" }, - { url = "https://files.pythonhosted.org/packages/3f/ca/4fdc7bf59bf6994aa45cbd4ef1055cd65e2884de6113dbd49f75498ddb08/grpcio-1.74.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1ea6176d7dfd5b941ea01c2ec34de9531ba494d541fe2057c904e601879f249", size = 6182562, upload-time = "2025-07-24T18:53:48.967Z" }, - { url = "https://files.pythonhosted.org/packages/fd/48/2869e5b2c1922583686f7ae674937986807c2f676d08be70d0a541316270/grpcio-1.74.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:64229c1e9cea079420527fa8ac45d80fc1e8d3f94deaa35643c381fa8d98f362", size = 6303425, upload-time = "2025-07-24T18:53:50.847Z" }, - { url = "https://files.pythonhosted.org/packages/a6/0e/bac93147b9a164f759497bc6913e74af1cb632c733c7af62c0336782bd38/grpcio-1.74.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:0f87bddd6e27fc776aacf7ebfec367b6d49cad0455123951e4488ea99d9b9b8f", size = 6996533, upload-time = "2025-07-24T18:53:52.747Z" }, - { url = "https://files.pythonhosted.org/packages/84/35/9f6b2503c1fd86d068b46818bbd7329db26a87cdd8c01e0d1a9abea1104c/grpcio-1.74.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3b03d8f2a07f0fea8c8f74deb59f8352b770e3900d143b3d1475effcb08eec20", size = 6491489, upload-time = "2025-07-24T18:53:55.06Z" }, - { url = "https://files.pythonhosted.org/packages/75/33/a04e99be2a82c4cbc4039eb3a76f6c3632932b9d5d295221389d10ac9ca7/grpcio-1.74.0-cp313-cp313-win32.whl", hash = "sha256:b6a73b2ba83e663b2480a90b82fdae6a7aa6427f62bf43b29912c0cfd1aa2bfa", size = 3805811, upload-time = "2025-07-24T18:53:56.798Z" }, - { url = "https://files.pythonhosted.org/packages/34/80/de3eb55eb581815342d097214bed4c59e806b05f1b3110df03b2280d6dfd/grpcio-1.74.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd3c71aeee838299c5887230b8a1822795325ddfea635edd82954c1eaa831e24", size = 4489214, upload-time = "2025-07-24T18:53:59.771Z" }, - { url = "https://files.pythonhosted.org/packages/0d/de/dd7db504703c3b669f1b83265e2fbb5d79c8d3da86ea52cbd9202b9a8b05/grpcio-1.74.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:4bc5fca10aaf74779081e16c2bcc3d5ec643ffd528d9e7b1c9039000ead73bae", size = 5480998, upload-time = "2025-07-24T18:54:01.868Z" }, - { url = "https://files.pythonhosted.org/packages/1c/57/6537ace3af4c97f2b013ceff1f2e789c52b8448334ca3a0c36e7421cf6ed/grpcio-1.74.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:6bab67d15ad617aff094c382c882e0177637da73cbc5532d52c07b4ee887a87b", size = 10990945, upload-time = "2025-07-24T18:54:03.854Z" }, - { url = "https://files.pythonhosted.org/packages/d8/f2/579410017de16cd27f35326df6fecde81bff6e9b43c871d28263fa8a77a4/grpcio-1.74.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:655726919b75ab3c34cdad39da5c530ac6fa32696fb23119e36b64adcfca174a", size = 5983968, upload-time = "2025-07-24T18:54:06.434Z" }, - { url = "https://files.pythonhosted.org/packages/a0/6a/0f3571003663d991f4ea953b82dc518fed094c182decc48c9b0242bec7e3/grpcio-1.74.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a2b06afe2e50ebfd46247ac3ba60cac523f54ec7792ae9ba6073c12daf26f0a", size = 6654768, upload-time = "2025-07-24T18:54:08.632Z" }, - { url = "https://files.pythonhosted.org/packages/24/e3/1d42cb00e0390bacab3c9ee79e37416140d907c8c7c7a92654c535805963/grpcio-1.74.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f251c355167b2360537cf17bea2cf0197995e551ab9da6a0a59b3da5e8704f9", size = 6215627, upload-time = "2025-07-24T18:54:10.584Z" }, - { url = "https://files.pythonhosted.org/packages/77/84/4f8312bc4430eda1cdbc4e8689f54daa807b5d304d4ea53e9d27c448889b/grpcio-1.74.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8f7b5882fb50632ab1e48cb3122d6df55b9afabc265582808036b6e51b9fd6b7", size = 6330938, upload-time = "2025-07-24T18:54:12.557Z" }, - { url = "https://files.pythonhosted.org/packages/2f/c0/422d2b40110716a4775212256a56ac71586be2403a7b7055818bfd0fc203/grpcio-1.74.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:834988b6c34515545b3edd13e902c1acdd9f2465d386ea5143fb558f153a7176", size = 7019216, upload-time = "2025-07-24T18:54:14.475Z" }, - { url = "https://files.pythonhosted.org/packages/6f/84/668ab6df27fb35886dfa1242f2d302d0cd319c72e3dd3845a322ecabf61b/grpcio-1.74.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:22b834cef33429ca6cc28303c9c327ba9a3fafecbf62fae17e9a7b7163cc43ac", size = 6510719, upload-time = "2025-07-24T18:54:16.775Z" }, - { url = "https://files.pythonhosted.org/packages/ed/6a/981150f20dd435b9c46cd504038b4fbae2171b43fe70019914d80159e156/grpcio-1.74.0-cp39-cp39-win32.whl", hash = "sha256:7d95d71ff35291bab3f1c52f52f474c632db26ea12700c2ff0ea0532cb0b5854", size = 3819185, upload-time = "2025-07-24T18:54:18.673Z" }, - { url = "https://files.pythonhosted.org/packages/75/5f/d64b9745bb9def186e1be11b42d4d310570799d6170ac75829ef1c67c176/grpcio-1.74.0-cp39-cp39-win_amd64.whl", hash = "sha256:ecde9ab49f58433abe02f9ed076c7b5be839cf0153883a6d23995937a82392fa", size = 4495789, upload-time = "2025-07-24T18:54:20.582Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c7/cb03251d9dfb177246a9809a76f189d21df32dbd4a845951881d11323b7f/fastapi-0.139.2-py3-none-any.whl", hash = "sha256:b9ad015a835173d59865e2f5d8296fbc2b317bf56a2ba1a5bfbdd03de2fd4b1c", size = 130234, upload-time = "2026-07-16T15:06:19.557Z" }, ] [[package]] -name = "grpcio-tools" -version = "1.71.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "grpcio" }, - { name = "protobuf" }, - { name = "setuptools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ad/9a/edfefb47f11ef6b0f39eea4d8f022c5bb05ac1d14fcc7058e84a51305b73/grpcio_tools-1.71.2.tar.gz", hash = "sha256:b5304d65c7569b21270b568e404a5a843cf027c66552a6a0978b23f137679c09", size = 5330655, upload-time = "2025-06-28T04:22:00.308Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/ad/e74a4d1cffff628c2ef1ec5b9944fb098207cc4af6eb8db4bc52e6d99236/grpcio_tools-1.71.2-cp310-cp310-linux_armv7l.whl", hash = "sha256:ab8a28c2e795520d6dc6ffd7efaef4565026dbf9b4f5270de2f3dd1ce61d2318", size = 2385557, upload-time = "2025-06-28T04:20:38.833Z" }, - { url = "https://files.pythonhosted.org/packages/63/bf/30b63418279d6fdc4fd4a3781a7976c40c7e8ee052333b9ce6bd4ce63f30/grpcio_tools-1.71.2-cp310-cp310-macosx_10_14_universal2.whl", hash = "sha256:654ecb284a592d39a85556098b8c5125163435472a20ead79b805cf91814b99e", size = 5446915, upload-time = "2025-06-28T04:20:40.947Z" }, - { url = "https://files.pythonhosted.org/packages/83/cd/2994e0a0a67714fdb00c207c4bec60b9b356fbd6b0b7a162ecaabe925155/grpcio_tools-1.71.2-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:b49aded2b6c890ff690d960e4399a336c652315c6342232c27bd601b3705739e", size = 2348301, upload-time = "2025-06-28T04:20:42.766Z" }, - { url = "https://files.pythonhosted.org/packages/5b/8b/4f2315927af306af1b35793b332b9ca9dc5b5a2cde2d55811c9577b5f03f/grpcio_tools-1.71.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7811a6fc1c4b4e5438e5eb98dbd52c2dc4a69d1009001c13356e6636322d41a", size = 2742159, upload-time = "2025-06-28T04:20:44.206Z" }, - { url = "https://files.pythonhosted.org/packages/8d/98/d513f6c09df405c82583e7083c20718ea615ed0da69ec42c80ceae7ebdc5/grpcio_tools-1.71.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:393a9c80596aa2b3f05af854e23336ea8c295593bbb35d9adae3d8d7943672bd", size = 2473444, upload-time = "2025-06-28T04:20:45.5Z" }, - { url = "https://files.pythonhosted.org/packages/fa/fe/00af17cc841916d5e4227f11036bf443ce006629212c876937c7904b0ba3/grpcio_tools-1.71.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:823e1f23c12da00f318404c4a834bb77cd150d14387dee9789ec21b335249e46", size = 2850339, upload-time = "2025-06-28T04:20:46.758Z" }, - { url = "https://files.pythonhosted.org/packages/7d/59/745fc50dfdbed875fcfd6433883270d39d23fb1aa4ecc9587786f772dce3/grpcio_tools-1.71.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9bfbea79d6aec60f2587133ba766ede3dc3e229641d1a1e61d790d742a3d19eb", size = 3300795, upload-time = "2025-06-28T04:20:48.327Z" }, - { url = "https://files.pythonhosted.org/packages/62/3e/d9d0fb2df78e601c28d02ef0cd5d007f113c1b04fc21e72bf56e8c3df66b/grpcio_tools-1.71.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:32f3a67b10728835b5ffb63fbdbe696d00e19a27561b9cf5153e72dbb93021ba", size = 2913729, upload-time = "2025-06-28T04:20:49.641Z" }, - { url = "https://files.pythonhosted.org/packages/09/ae/ddb264b4a10c6c10336a7c177f8738b230c2c473d0c91dd5d8ce8ea1b857/grpcio_tools-1.71.2-cp310-cp310-win32.whl", hash = "sha256:7fcf9d92c710bfc93a1c0115f25e7d49a65032ff662b38b2f704668ce0a938df", size = 945997, upload-time = "2025-06-28T04:20:50.9Z" }, - { url = "https://files.pythonhosted.org/packages/ad/8d/5efd93698fe359f63719d934ebb2d9337e82d396e13d6bf00f4b06793e37/grpcio_tools-1.71.2-cp310-cp310-win_amd64.whl", hash = "sha256:914b4275be810290266e62349f2d020bb7cc6ecf9edb81da3c5cddb61a95721b", size = 1117474, upload-time = "2025-06-28T04:20:52.54Z" }, - { url = "https://files.pythonhosted.org/packages/17/e4/0568d38b8da6237ea8ea15abb960fb7ab83eb7bb51e0ea5926dab3d865b1/grpcio_tools-1.71.2-cp311-cp311-linux_armv7l.whl", hash = "sha256:0acb8151ea866be5b35233877fbee6445c36644c0aa77e230c9d1b46bf34b18b", size = 2385557, upload-time = "2025-06-28T04:20:54.323Z" }, - { url = "https://files.pythonhosted.org/packages/76/fb/700d46f72b0f636cf0e625f3c18a4f74543ff127471377e49a071f64f1e7/grpcio_tools-1.71.2-cp311-cp311-macosx_10_14_universal2.whl", hash = "sha256:b28f8606f4123edb4e6da281547465d6e449e89f0c943c376d1732dc65e6d8b3", size = 5447590, upload-time = "2025-06-28T04:20:55.836Z" }, - { url = "https://files.pythonhosted.org/packages/12/69/d9bb2aec3de305162b23c5c884b9f79b1a195d42b1e6dabcc084cc9d0804/grpcio_tools-1.71.2-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:cbae6f849ad2d1f5e26cd55448b9828e678cb947fa32c8729d01998238266a6a", size = 2348495, upload-time = "2025-06-28T04:20:57.33Z" }, - { url = "https://files.pythonhosted.org/packages/d5/83/f840aba1690461b65330efbca96170893ee02fae66651bcc75f28b33a46c/grpcio_tools-1.71.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4d1027615cfb1e9b1f31f2f384251c847d68c2f3e025697e5f5c72e26ed1316", size = 2742333, upload-time = "2025-06-28T04:20:59.051Z" }, - { url = "https://files.pythonhosted.org/packages/30/34/c02cd9b37de26045190ba665ee6ab8597d47f033d098968f812d253bbf8c/grpcio_tools-1.71.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bac95662dc69338edb9eb727cc3dd92342131b84b12b3e8ec6abe973d4cbf1b", size = 2473490, upload-time = "2025-06-28T04:21:00.614Z" }, - { url = "https://files.pythonhosted.org/packages/4d/c7/375718ae091c8f5776828ce97bdcb014ca26244296f8b7f70af1a803ed2f/grpcio_tools-1.71.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c50250c7248055040f89eb29ecad39d3a260a4b6d3696af1575945f7a8d5dcdc", size = 2850333, upload-time = "2025-06-28T04:21:01.95Z" }, - { url = "https://files.pythonhosted.org/packages/19/37/efc69345bd92a73b2bc80f4f9e53d42dfdc234b2491ae58c87da20ca0ea5/grpcio_tools-1.71.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6ab1ad955e69027ef12ace4d700c5fc36341bdc2f420e87881e9d6d02af3d7b8", size = 3300748, upload-time = "2025-06-28T04:21:03.451Z" }, - { url = "https://files.pythonhosted.org/packages/d2/1f/15f787eb25ae42086f55ed3e4260e85f385921c788debf0f7583b34446e3/grpcio_tools-1.71.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dd75dde575781262b6b96cc6d0b2ac6002b2f50882bf5e06713f1bf364ee6e09", size = 2913178, upload-time = "2025-06-28T04:21:04.879Z" }, - { url = "https://files.pythonhosted.org/packages/12/aa/69cb3a9dff7d143a05e4021c3c9b5cde07aacb8eb1c892b7c5b9fb4973e3/grpcio_tools-1.71.2-cp311-cp311-win32.whl", hash = "sha256:9a3cb244d2bfe0d187f858c5408d17cb0e76ca60ec9a274c8fd94cc81457c7fc", size = 946256, upload-time = "2025-06-28T04:21:06.518Z" }, - { url = "https://files.pythonhosted.org/packages/1e/df/fb951c5c87eadb507a832243942e56e67d50d7667b0e5324616ffd51b845/grpcio_tools-1.71.2-cp311-cp311-win_amd64.whl", hash = "sha256:00eb909997fd359a39b789342b476cbe291f4dd9c01ae9887a474f35972a257e", size = 1117661, upload-time = "2025-06-28T04:21:08.18Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d3/3ed30a9c5b2424627b4b8411e2cd6a1a3f997d3812dbc6a8630a78bcfe26/grpcio_tools-1.71.2-cp312-cp312-linux_armv7l.whl", hash = "sha256:bfc0b5d289e383bc7d317f0e64c9dfb59dc4bef078ecd23afa1a816358fb1473", size = 2385479, upload-time = "2025-06-28T04:21:10.413Z" }, - { url = "https://files.pythonhosted.org/packages/54/61/e0b7295456c7e21ef777eae60403c06835160c8d0e1e58ebfc7d024c51d3/grpcio_tools-1.71.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b4669827716355fa913b1376b1b985855d5cfdb63443f8d18faf210180199006", size = 5431521, upload-time = "2025-06-28T04:21:12.261Z" }, - { url = "https://files.pythonhosted.org/packages/75/d7/7bcad6bcc5f5b7fab53e6bce5db87041f38ef3e740b1ec2d8c49534fa286/grpcio_tools-1.71.2-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:d4071f9b44564e3f75cdf0f05b10b3e8c7ea0ca5220acbf4dc50b148552eef2f", size = 2350289, upload-time = "2025-06-28T04:21:13.625Z" }, - { url = "https://files.pythonhosted.org/packages/b2/8a/e4c1c4cb8c9ff7f50b7b2bba94abe8d1e98ea05f52a5db476e7f1c1a3c70/grpcio_tools-1.71.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a28eda8137d587eb30081384c256f5e5de7feda34776f89848b846da64e4be35", size = 2743321, upload-time = "2025-06-28T04:21:15.007Z" }, - { url = "https://files.pythonhosted.org/packages/fd/aa/95bc77fda5c2d56fb4a318c1b22bdba8914d5d84602525c99047114de531/grpcio_tools-1.71.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b19c083198f5eb15cc69c0a2f2c415540cbc636bfe76cea268e5894f34023b40", size = 2474005, upload-time = "2025-06-28T04:21:16.443Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ff/ca11f930fe1daa799ee0ce1ac9630d58a3a3deed3dd2f465edb9a32f299d/grpcio_tools-1.71.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:784c284acda0d925052be19053d35afbf78300f4d025836d424cf632404f676a", size = 2851559, upload-time = "2025-06-28T04:21:18.139Z" }, - { url = "https://files.pythonhosted.org/packages/64/10/c6fc97914c7e19c9bb061722e55052fa3f575165da9f6510e2038d6e8643/grpcio_tools-1.71.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:381e684d29a5d052194e095546eef067201f5af30fd99b07b5d94766f44bf1ae", size = 3300622, upload-time = "2025-06-28T04:21:20.291Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d6/965f36cfc367c276799b730d5dd1311b90a54a33726e561393b808339b04/grpcio_tools-1.71.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3e4b4801fabd0427fc61d50d09588a01b1cfab0ec5e8a5f5d515fbdd0891fd11", size = 2913863, upload-time = "2025-06-28T04:21:22.196Z" }, - { url = "https://files.pythonhosted.org/packages/8d/f0/c05d5c3d0c1d79ac87df964e9d36f1e3a77b60d948af65bec35d3e5c75a3/grpcio_tools-1.71.2-cp312-cp312-win32.whl", hash = "sha256:84ad86332c44572305138eafa4cc30040c9a5e81826993eae8227863b700b490", size = 945744, upload-time = "2025-06-28T04:21:23.463Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e9/c84c1078f0b7af7d8a40f5214a9bdd8d2a567ad6c09975e6e2613a08d29d/grpcio_tools-1.71.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e1108d37eecc73b1c4a27350a6ed921b5dda25091700c1da17cfe30761cd462", size = 1117695, upload-time = "2025-06-28T04:21:25.22Z" }, - { url = "https://files.pythonhosted.org/packages/60/9c/bdf9c5055a1ad0a09123402d73ecad3629f75b9cf97828d547173b328891/grpcio_tools-1.71.2-cp313-cp313-linux_armv7l.whl", hash = "sha256:b0f0a8611614949c906e25c225e3360551b488d10a366c96d89856bcef09f729", size = 2384758, upload-time = "2025-06-28T04:21:26.712Z" }, - { url = "https://files.pythonhosted.org/packages/49/d0/6aaee4940a8fb8269c13719f56d69c8d39569bee272924086aef81616d4a/grpcio_tools-1.71.2-cp313-cp313-macosx_10_14_universal2.whl", hash = "sha256:7931783ea7ac42ac57f94c5047d00a504f72fbd96118bf7df911bb0e0435fc0f", size = 5443127, upload-time = "2025-06-28T04:21:28.383Z" }, - { url = "https://files.pythonhosted.org/packages/d9/11/50a471dcf301b89c0ed5ab92c533baced5bd8f796abfd133bbfadf6b60e5/grpcio_tools-1.71.2-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:d188dc28e069aa96bb48cb11b1338e47ebdf2e2306afa58a8162cc210172d7a8", size = 2349627, upload-time = "2025-06-28T04:21:30.254Z" }, - { url = "https://files.pythonhosted.org/packages/bb/66/e3dc58362a9c4c2fbe98a7ceb7e252385777ebb2bbc7f42d5ab138d07ace/grpcio_tools-1.71.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f36c4b3cc42ad6ef67430639174aaf4a862d236c03c4552c4521501422bfaa26", size = 2742932, upload-time = "2025-06-28T04:21:32.325Z" }, - { url = "https://files.pythonhosted.org/packages/b7/1e/1e07a07ed8651a2aa9f56095411198385a04a628beba796f36d98a5a03ec/grpcio_tools-1.71.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bd9ed12ce93b310f0cef304176049d0bc3b9f825e9c8c6a23e35867fed6affd", size = 2473627, upload-time = "2025-06-28T04:21:33.752Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f9/3b7b32e4acb419f3a0b4d381bc114fe6cd48e3b778e81273fc9e4748caad/grpcio_tools-1.71.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7ce27e76dd61011182d39abca38bae55d8a277e9b7fe30f6d5466255baccb579", size = 2850879, upload-time = "2025-06-28T04:21:35.241Z" }, - { url = "https://files.pythonhosted.org/packages/1e/99/cd9e1acd84315ce05ad1fcdfabf73b7df43807cf00c3b781db372d92b899/grpcio_tools-1.71.2-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:dcc17bf59b85c3676818f2219deacac0156492f32ca165e048427d2d3e6e1157", size = 3300216, upload-time = "2025-06-28T04:21:36.826Z" }, - { url = "https://files.pythonhosted.org/packages/9f/c0/66eab57b14550c5b22404dbf60635c9e33efa003bd747211981a9859b94b/grpcio_tools-1.71.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:706360c71bdd722682927a1fb517c276ccb816f1e30cb71f33553e5817dc4031", size = 2913521, upload-time = "2025-06-28T04:21:38.347Z" }, - { url = "https://files.pythonhosted.org/packages/05/9b/7c90af8f937d77005625d705ab1160bc42a7e7b021ee5c788192763bccd6/grpcio_tools-1.71.2-cp313-cp313-win32.whl", hash = "sha256:bcf751d5a81c918c26adb2d6abcef71035c77d6eb9dd16afaf176ee096e22c1d", size = 945322, upload-time = "2025-06-28T04:21:39.864Z" }, - { url = "https://files.pythonhosted.org/packages/5f/80/6db6247f767c94fe551761772f89ceea355ff295fd4574cb8efc8b2d1199/grpcio_tools-1.71.2-cp313-cp313-win_amd64.whl", hash = "sha256:b1581a1133552aba96a730178bc44f6f1a071f0eb81c5b6bc4c0f89f5314e2b8", size = 1117234, upload-time = "2025-06-28T04:21:41.893Z" }, - { url = "https://files.pythonhosted.org/packages/c1/80/d1f60c9122d473cf357ea5763bca44c3216dd96caf4da3790cef7a8e92da/grpcio_tools-1.71.2-cp39-cp39-linux_armv7l.whl", hash = "sha256:344aa8973850bc36fd0ce81aa6443bd5ab41dc3a25903b36cd1e70f71ceb53c9", size = 2385641, upload-time = "2025-06-28T04:21:43.826Z" }, - { url = "https://files.pythonhosted.org/packages/87/a2/7d14b8820a5d7ada023bc4ad5ab8827a17acb3d3b0a3cc2eb298fbe6685a/grpcio_tools-1.71.2-cp39-cp39-macosx_10_14_universal2.whl", hash = "sha256:4d32450a4c8a97567b32154379d97398b7eba090bce756aff57aef5d80d8c953", size = 5448004, upload-time = "2025-06-28T04:21:45.84Z" }, - { url = "https://files.pythonhosted.org/packages/c7/71/b6c5d96ec3e2159100dc150d65b97cb4c64ab7b546fe9543edc794258c36/grpcio_tools-1.71.2-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:f596dbc1e46f9e739e09af553bf3c3321be3d603e579f38ffa9f2e0e4a25f4f7", size = 2349258, upload-time = "2025-06-28T04:21:47.962Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a6/de0124b08a71c3a01a928c4ad79f965a2c5930f96e92a5fe4a67addd5801/grpcio_tools-1.71.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d7723ff599104188cb870d01406b65e67e2493578347cc13d50e9dc372db36ef", size = 2742628, upload-time = "2025-06-28T04:21:49.399Z" }, - { url = "https://files.pythonhosted.org/packages/63/93/76a9aa5859d6e72ff9ddf7b6aa7cafa3c96f960b5fb52f7a3ce5b9019e59/grpcio_tools-1.71.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b018b6b69641b10864a3f19dd3c2b7ca3dfce4460eb836ab28b058e7deb3e", size = 2474173, upload-time = "2025-06-28T04:21:50.965Z" }, - { url = "https://files.pythonhosted.org/packages/9f/5c/b96571881bd0819a6c5aaf7e9911d1eb8c192a6259e9b55ef16d7284e41a/grpcio_tools-1.71.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0dd058c06ce95a99f78851c05db30af507227878013d46a8339e44fb24855ff7", size = 2851495, upload-time = "2025-06-28T04:21:52.574Z" }, - { url = "https://files.pythonhosted.org/packages/f4/ec/aae1e3f75e5c0dcee452b053117532cfafcca86f8497040ab150a26b0600/grpcio_tools-1.71.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b3312bdd5952bba2ef8e4314b2e2f886fa23b2f6d605cd56097605ae65d30515", size = 3301170, upload-time = "2025-06-28T04:21:54.06Z" }, - { url = "https://files.pythonhosted.org/packages/07/2b/5ba649cdda68b34d189301c66e149af1886c82a94fa2040ac3330ded46f0/grpcio_tools-1.71.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:085de63843946b967ae561e7dd832fa03147f01282f462a0a0cbe1571d9ee986", size = 2913866, upload-time = "2025-06-28T04:21:55.589Z" }, - { url = "https://files.pythonhosted.org/packages/dc/8d/a0f0f68ef8848daf4e85cd75876891f358dc35194a090669fca14029c235/grpcio_tools-1.71.2-cp39-cp39-win32.whl", hash = "sha256:c1ff5f79f49768d4c561508b62878f27198b3420a87390e0c51969b8dbfcfca8", size = 946082, upload-time = "2025-06-28T04:21:57.176Z" }, - { url = "https://files.pythonhosted.org/packages/5b/9f/94fa8421d48f8aed87f64bdc87e006249eb11d34a75c4aa8d728a0324650/grpcio_tools-1.71.2-cp39-cp39-win_amd64.whl", hash = "sha256:c3e02b345cf96673dcf77599a61482f68c318a62c9cde20a5ae0882619ff8c98", size = 1117954, upload-time = "2025-06-28T04:21:58.61Z" }, +name = "fastuuid" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/7d/d9daedf0f2ebcacd20d599928f8913e9d2aea1d56d2d355a93bfa2b611d7/fastuuid-0.14.0.tar.gz", hash = "sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26", size = 18232, upload-time = "2025-10-19T22:19:22.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/b2/731a6696e37cd20eed353f69a09f37a984a43c9713764ee3f7ad5f57f7f9/fastuuid-0.14.0-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6e6243d40f6c793c3e2ee14c13769e341b90be5ef0c23c82fa6515a96145181a", size = 516760, upload-time = "2025-10-19T22:25:21.509Z" }, + { url = "https://files.pythonhosted.org/packages/c5/79/c73c47be2a3b8734d16e628982653517f80bbe0570e27185d91af6096507/fastuuid-0.14.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:13ec4f2c3b04271f62be2e1ce7e95ad2dd1cf97e94503a3760db739afbd48f00", size = 264748, upload-time = "2025-10-19T22:41:52.873Z" }, + { url = "https://files.pythonhosted.org/packages/24/c5/84c1eea05977c8ba5173555b0133e3558dc628bcf868d6bf1689ff14aedc/fastuuid-0.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b2fdd48b5e4236df145a149d7125badb28e0a383372add3fbaac9a6b7a394470", size = 254537, upload-time = "2025-10-19T22:33:55.603Z" }, + { url = "https://files.pythonhosted.org/packages/0e/23/4e362367b7fa17dbed646922f216b9921efb486e7abe02147e4b917359f8/fastuuid-0.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f74631b8322d2780ebcf2d2d75d58045c3e9378625ec51865fe0b5620800c39d", size = 278994, upload-time = "2025-10-19T22:26:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/b2/72/3985be633b5a428e9eaec4287ed4b873b7c4c53a9639a8b416637223c4cd/fastuuid-0.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83cffc144dc93eb604b87b179837f2ce2af44871a7b323f2bfed40e8acb40ba8", size = 280003, upload-time = "2025-10-19T22:23:45.415Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6d/6ef192a6df34e2266d5c9deb39cd3eea986df650cbcfeaf171aa52a059c3/fastuuid-0.14.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a771f135ab4523eb786e95493803942a5d1fc1610915f131b363f55af53b219", size = 303583, upload-time = "2025-10-19T22:26:00.756Z" }, + { url = "https://files.pythonhosted.org/packages/9d/11/8a2ea753c68d4fece29d5d7c6f3f903948cc6e82d1823bc9f7f7c0355db3/fastuuid-0.14.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4edc56b877d960b4eda2c4232f953a61490c3134da94f3c28af129fb9c62a4f6", size = 460955, upload-time = "2025-10-19T22:36:25.196Z" }, + { url = "https://files.pythonhosted.org/packages/23/42/7a32c93b6ce12642d9a152ee4753a078f372c9ebb893bc489d838dd4afd5/fastuuid-0.14.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bcc96ee819c282e7c09b2eed2b9bd13084e3b749fdb2faf58c318d498df2efbe", size = 480763, upload-time = "2025-10-19T22:24:28.451Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e9/a5f6f686b46e3ed4ed3b93770111c233baac87dd6586a411b4988018ef1d/fastuuid-0.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7a3c0bca61eacc1843ea97b288d6789fbad7400d16db24e36a66c28c268cfe3d", size = 452613, upload-time = "2025-10-19T22:25:06.827Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c9/18abc73c9c5b7fc0e476c1733b678783b2e8a35b0be9babd423571d44e98/fastuuid-0.14.0-cp310-cp310-win32.whl", hash = "sha256:7f2f3efade4937fae4e77efae1af571902263de7b78a0aee1a1653795a093b2a", size = 155045, upload-time = "2025-10-19T22:28:32.732Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8a/d9e33f4eb4d4f6d9f2c5c7d7e96b5cdbb535c93f3b1ad6acce97ee9d4bf8/fastuuid-0.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:ae64ba730d179f439b0736208b4c279b8bc9c089b102aec23f86512ea458c8a4", size = 156122, upload-time = "2025-10-19T22:23:15.59Z" }, + { url = "https://files.pythonhosted.org/packages/98/f3/12481bda4e5b6d3e698fbf525df4443cc7dce746f246b86b6fcb2fba1844/fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:73946cb950c8caf65127d4e9a325e2b6be0442a224fd51ba3b6ac44e1912ce34", size = 516386, upload-time = "2025-10-19T22:42:40.176Z" }, + { url = "https://files.pythonhosted.org/packages/59/19/2fc58a1446e4d72b655648eb0879b04e88ed6fa70d474efcf550f640f6ec/fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:12ac85024637586a5b69645e7ed986f7535106ed3013640a393a03e461740cb7", size = 264569, upload-time = "2025-10-19T22:25:50.977Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/3c74756e5b02c40cfcc8b1d8b5bac4edbd532b55917a6bcc9113550e99d1/fastuuid-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:05a8dde1f395e0c9b4be515b7a521403d1e8349443e7641761af07c7ad1624b1", size = 254366, upload-time = "2025-10-19T22:29:49.166Z" }, + { url = "https://files.pythonhosted.org/packages/52/96/d761da3fccfa84f0f353ce6e3eb8b7f76b3aa21fd25e1b00a19f9c80a063/fastuuid-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09378a05020e3e4883dfdab438926f31fea15fd17604908f3d39cbeb22a0b4dc", size = 278978, upload-time = "2025-10-19T22:35:41.306Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c2/f84c90167cc7765cb82b3ff7808057608b21c14a38531845d933a4637307/fastuuid-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbb0c4b15d66b435d2538f3827f05e44e2baafcc003dd7d8472dc67807ab8fd8", size = 279692, upload-time = "2025-10-19T22:25:36.997Z" }, + { url = "https://files.pythonhosted.org/packages/af/7b/4bacd03897b88c12348e7bd77943bac32ccf80ff98100598fcff74f75f2e/fastuuid-0.14.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cd5a7f648d4365b41dbf0e38fe8da4884e57bed4e77c83598e076ac0c93995e7", size = 303384, upload-time = "2025-10-19T22:29:46.578Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a2/584f2c29641df8bd810d00c1f21d408c12e9ad0c0dafdb8b7b29e5ddf787/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c0a94245afae4d7af8c43b3159d5e3934c53f47140be0be624b96acd672ceb73", size = 460921, upload-time = "2025-10-19T22:36:42.006Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/c6b77443bb7764c760e211002c8638c0c7cce11cb584927e723215ba1398/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2b29e23c97e77c3a9514d70ce343571e469098ac7f5a269320a0f0b3e193ab36", size = 480575, upload-time = "2025-10-19T22:28:18.975Z" }, + { url = "https://files.pythonhosted.org/packages/5a/87/93f553111b33f9bb83145be12868c3c475bf8ea87c107063d01377cc0e8e/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1e690d48f923c253f28151b3a6b4e335f2b06bf669c68a02665bc150b7839e94", size = 452317, upload-time = "2025-10-19T22:25:32.75Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8c/a04d486ca55b5abb7eaa65b39df8d891b7b1635b22db2163734dc273579a/fastuuid-0.14.0-cp311-cp311-win32.whl", hash = "sha256:a6f46790d59ab38c6aa0e35c681c0484b50dc0acf9e2679c005d61e019313c24", size = 154804, upload-time = "2025-10-19T22:24:15.615Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b2/2d40bf00820de94b9280366a122cbaa60090c8cf59e89ac3938cf5d75895/fastuuid-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:e150eab56c95dc9e3fefc234a0eedb342fac433dacc273cd4d150a5b0871e1fa", size = 156099, upload-time = "2025-10-19T22:24:31.646Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/e78fcc5df65467f0d207661b7ef86c5b7ac62eea337c0c0fcedbeee6fb13/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77e94728324b63660ebf8adb27055e92d2e4611645bf12ed9d88d30486471d0a", size = 510164, upload-time = "2025-10-19T22:31:45.635Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b3/c846f933f22f581f558ee63f81f29fa924acd971ce903dab1a9b6701816e/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:caa1f14d2102cb8d353096bc6ef6c13b2c81f347e6ab9d6fbd48b9dea41c153d", size = 261837, upload-time = "2025-10-19T22:38:38.53Z" }, + { url = "https://files.pythonhosted.org/packages/54/ea/682551030f8c4fa9a769d9825570ad28c0c71e30cf34020b85c1f7ee7382/fastuuid-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070", size = 251370, upload-time = "2025-10-19T22:40:26.07Z" }, + { url = "https://files.pythonhosted.org/packages/14/dd/5927f0a523d8e6a76b70968e6004966ee7df30322f5fc9b6cdfb0276646a/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c9ec605ace243b6dbe3bd27ebdd5d33b00d8d1d3f580b39fdd15cd96fd71796", size = 277766, upload-time = "2025-10-19T22:37:23.779Z" }, + { url = "https://files.pythonhosted.org/packages/16/6e/c0fb547eef61293153348f12e0f75a06abb322664b34a1573a7760501336/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:808527f2407f58a76c916d6aa15d58692a4a019fdf8d4c32ac7ff303b7d7af09", size = 278105, upload-time = "2025-10-19T22:26:56.821Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b1/b9c75e03b768f61cf2e84ee193dc18601aeaf89a4684b20f2f0e9f52b62c/fastuuid-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fb3c0d7fef6674bbeacdd6dbd386924a7b60b26de849266d1ff6602937675c8", size = 301564, upload-time = "2025-10-19T22:30:31.604Z" }, + { url = "https://files.pythonhosted.org/packages/fc/fa/f7395fdac07c7a54f18f801744573707321ca0cee082e638e36452355a9d/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab3f5d36e4393e628a4df337c2c039069344db5f4b9d2a3c9cea48284f1dd741", size = 459659, upload-time = "2025-10-19T22:31:32.341Z" }, + { url = "https://files.pythonhosted.org/packages/66/49/c9fd06a4a0b1f0f048aacb6599e7d96e5d6bc6fa680ed0d46bf111929d1b/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b9a0ca4f03b7e0b01425281ffd44e99d360e15c895f1907ca105854ed85e2057", size = 478430, upload-time = "2025-10-19T22:26:22.962Z" }, + { url = "https://files.pythonhosted.org/packages/be/9c/909e8c95b494e8e140e8be6165d5fc3f61fdc46198c1554df7b3e1764471/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3acdf655684cc09e60fb7e4cf524e8f42ea760031945aa8086c7eae2eeeabeb8", size = 450894, upload-time = "2025-10-19T22:27:01.647Z" }, + { url = "https://files.pythonhosted.org/packages/90/eb/d29d17521976e673c55ef7f210d4cdd72091a9ec6755d0fd4710d9b3c871/fastuuid-0.14.0-cp312-cp312-win32.whl", hash = "sha256:9579618be6280700ae36ac42c3efd157049fe4dd40ca49b021280481c78c3176", size = 154374, upload-time = "2025-10-19T22:29:19.879Z" }, + { url = "https://files.pythonhosted.org/packages/cc/fc/f5c799a6ea6d877faec0472d0b27c079b47c86b1cdc577720a5386483b36/fastuuid-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:d9e4332dc4ba054434a9594cbfaf7823b57993d7d8e7267831c3e059857cf397", size = 156550, upload-time = "2025-10-19T22:27:49.658Z" }, + { url = "https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77a09cb7427e7af74c594e409f7731a0cf887221de2f698e1ca0ebf0f3139021", size = 510720, upload-time = "2025-10-19T22:42:34.633Z" }, + { url = "https://files.pythonhosted.org/packages/53/b0/a4b03ff5d00f563cc7546b933c28cb3f2a07344b2aec5834e874f7d44143/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9bd57289daf7b153bfa3e8013446aa144ce5e8c825e9e366d455155ede5ea2dc", size = 262024, upload-time = "2025-10-19T22:30:25.482Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6d/64aee0a0f6a58eeabadd582e55d0d7d70258ffdd01d093b30c53d668303b/fastuuid-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ac60fc860cdf3c3f327374db87ab8e064c86566ca8c49d2e30df15eda1b0c2d5", size = 251679, upload-time = "2025-10-19T22:36:14.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/a7e9cda8369e4f7919d36552db9b2ae21db7915083bc6336f1b0082c8b2e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab32f74bd56565b186f036e33129da77db8be09178cd2f5206a5d4035fb2a23f", size = 277862, upload-time = "2025-10-19T22:36:23.302Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/8ce11827c783affffd5bd4d6378b28eb6cc6d2ddf41474006b8d62e7448e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e678459cf4addaedd9936bbb038e35b3f6b2061330fd8f2f6a1d80414c0f87", size = 278278, upload-time = "2025-10-19T22:29:43.809Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/680fb6352d0bbade04036da46264a8001f74b7484e2fd1f4da9e3db1c666/fastuuid-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e3cc56742f76cd25ecb98e4b82a25f978ccffba02e4bdce8aba857b6d85d87b", size = 301788, upload-time = "2025-10-19T22:36:06.825Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7c/2014b5785bd8ebdab04ec857635ebd84d5ee4950186a577db9eff0fb8ff6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cb9a030f609194b679e1660f7e32733b7a0f332d519c5d5a6a0a580991290022", size = 459819, upload-time = "2025-10-19T22:35:31.623Z" }, + { url = "https://files.pythonhosted.org/packages/01/d2/524d4ceeba9160e7a9bc2ea3e8f4ccf1ad78f3bde34090ca0c51f09a5e91/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:09098762aad4f8da3a888eb9ae01c84430c907a297b97166b8abc07b640f2995", size = 478546, upload-time = "2025-10-19T22:26:03.023Z" }, + { url = "https://files.pythonhosted.org/packages/bc/17/354d04951ce114bf4afc78e27a18cfbd6ee319ab1829c2d5fb5e94063ac6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1383fff584fa249b16329a059c68ad45d030d5a4b70fb7c73a08d98fd53bcdab", size = 450921, upload-time = "2025-10-19T22:31:02.151Z" }, + { url = "https://files.pythonhosted.org/packages/fb/be/d7be8670151d16d88f15bb121c5b66cdb5ea6a0c2a362d0dcf30276ade53/fastuuid-0.14.0-cp313-cp313-win32.whl", hash = "sha256:a0809f8cc5731c066c909047f9a314d5f536c871a7a22e815cc4967c110ac9ad", size = 154559, upload-time = "2025-10-19T22:36:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/22/1d/5573ef3624ceb7abf4a46073d3554e37191c868abc3aecd5289a72f9810a/fastuuid-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0df14e92e7ad3276327631c9e7cec09e32572ce82089c55cb1bb8df71cf394ed", size = 156539, upload-time = "2025-10-19T22:33:35.898Z" }, + { url = "https://files.pythonhosted.org/packages/16/c9/8c7660d1fe3862e3f8acabd9be7fc9ad71eb270f1c65cce9a2b7a31329ab/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b852a870a61cfc26c884af205d502881a2e59cc07076b60ab4a951cc0c94d1ad", size = 510600, upload-time = "2025-10-19T22:43:44.17Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f4/a989c82f9a90d0ad995aa957b3e572ebef163c5299823b4027986f133dfb/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c7502d6f54cd08024c3ea9b3514e2d6f190feb2f46e6dbcd3747882264bb5f7b", size = 262069, upload-time = "2025-10-19T22:43:38.38Z" }, + { url = "https://files.pythonhosted.org/packages/da/6c/a1a24f73574ac995482b1326cf7ab41301af0fabaa3e37eeb6b3df00e6e2/fastuuid-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ca61b592120cf314cfd66e662a5b54a578c5a15b26305e1b8b618a6f22df714", size = 251543, upload-time = "2025-10-19T22:32:22.537Z" }, + { url = "https://files.pythonhosted.org/packages/1a/20/2a9b59185ba7a6c7b37808431477c2d739fcbdabbf63e00243e37bd6bf49/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa75b6657ec129d0abded3bec745e6f7ab642e6dba3a5272a68247e85f5f316f", size = 277798, upload-time = "2025-10-19T22:33:53.821Z" }, + { url = "https://files.pythonhosted.org/packages/ef/33/4105ca574f6ded0af6a797d39add041bcfb468a1255fbbe82fcb6f592da2/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8a0dfea3972200f72d4c7df02c8ac70bad1bb4c58d7e0ec1e6f341679073a7f", size = 278283, upload-time = "2025-10-19T22:29:02.812Z" }, + { url = "https://files.pythonhosted.org/packages/fe/8c/fca59f8e21c4deb013f574eae05723737ddb1d2937ce87cb2a5d20992dc3/fastuuid-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1bf539a7a95f35b419f9ad105d5a8a35036df35fdafae48fb2fd2e5f318f0d75", size = 301627, upload-time = "2025-10-19T22:35:54.985Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e2/f78c271b909c034d429218f2798ca4e89eeda7983f4257d7865976ddbb6c/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:9a133bf9cc78fdbd1179cb58a59ad0100aa32d8675508150f3658814aeefeaa4", size = 459778, upload-time = "2025-10-19T22:28:00.999Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f0/5ff209d865897667a2ff3e7a572267a9ced8f7313919f6d6043aed8b1caa/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:f54d5b36c56a2d5e1a31e73b950b28a0d83eb0c37b91d10408875a5a29494bad", size = 478605, upload-time = "2025-10-19T22:36:21.764Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c8/2ce1c78f983a2c4987ea865d9516dbdfb141a120fd3abb977ae6f02ba7ca/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:ec27778c6ca3393ef662e2762dba8af13f4ec1aaa32d08d77f71f2a70ae9feb8", size = 450837, upload-time = "2025-10-19T22:34:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/df/60/dad662ec9a33b4a5fe44f60699258da64172c39bd041da2994422cdc40fe/fastuuid-0.14.0-cp314-cp314-win32.whl", hash = "sha256:e23fc6a83f112de4be0cc1990e5b127c27663ae43f866353166f87df58e73d06", size = 154532, upload-time = "2025-10-19T22:35:18.217Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/da4db31001e854025ffd26bc9ba0740a9cbba2c3259695f7c5834908b336/fastuuid-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:df61342889d0f5e7a32f7284e55ef95103f2110fee433c2ae7c2c0956d76ac8a", size = 156457, upload-time = "2025-10-19T22:33:44.579Z" }, ] [[package]] -name = "h11" -version = "0.16.0" +name = "filelock" +version = "3.30.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" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2b/8b6480a70a647035334a604d0931926de4b5cd1f57835d45ad5eed2b1a1e/filelock-3.30.0.tar.gz", hash = "sha256:1774e682dbe443bd60f9609162fc596e2c80dc84ffc2957068953406d0520090", size = 174927, upload-time = "2026-07-16T03:53:58.152Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/52/af/9b01bcf5c91e81899bb890b87bd9077732a9b3365c098e67fe77958c39ed/filelock-3.30.0-py3-none-any.whl", hash = "sha256:40632998f0772e64183bb819f086a1b9def6be1090cf1dcb9d45f46806ef279b", size = 93131, upload-time = "2026-07-16T03:53:56.727Z" }, ] [[package]] -name = "hf-xet" -version = "1.1.9" +name = "filetype" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/0f/5b60fc28ee7f8cc17a5114a584fd6b86e11c3e0a6e142a7f97a161e9640a/hf_xet-1.1.9.tar.gz", hash = "sha256:c99073ce404462e909f1d5839b2d14a3827b8fe75ed8aed551ba6609c026c803", size = 484242, upload-time = "2025-08-27T23:05:19.441Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/29/745f7d30d47fe0f251d3ad3dc2978a23141917661998763bebb6da007eb1/filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", size = 998020, upload-time = "2022-11-02T17:34:04.141Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/12/56e1abb9a44cdef59a411fe8a8673313195711b5ecce27880eb9c8fa90bd/hf_xet-1.1.9-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:a3b6215f88638dd7a6ff82cb4e738dcbf3d863bf667997c093a3c990337d1160", size = 2762553, upload-time = "2025-08-27T23:05:15.153Z" }, - { url = "https://files.pythonhosted.org/packages/3a/e6/2d0d16890c5f21b862f5df3146519c182e7f0ae49b4b4bf2bd8a40d0b05e/hf_xet-1.1.9-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:9b486de7a64a66f9a172f4b3e0dfe79c9f0a93257c501296a2521a13495a698a", size = 2623216, upload-time = "2025-08-27T23:05:13.778Z" }, - { url = "https://files.pythonhosted.org/packages/81/42/7e6955cf0621e87491a1fb8cad755d5c2517803cea174229b0ec00ff0166/hf_xet-1.1.9-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4c5a840c2c4e6ec875ed13703a60e3523bc7f48031dfd750923b2a4d1a5fc3c", size = 3186789, upload-time = "2025-08-27T23:05:12.368Z" }, - { url = "https://files.pythonhosted.org/packages/df/8b/759233bce05457f5f7ec062d63bbfd2d0c740b816279eaaa54be92aa452a/hf_xet-1.1.9-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:96a6139c9e44dad1c52c52520db0fffe948f6bce487cfb9d69c125f254bb3790", size = 3088747, upload-time = "2025-08-27T23:05:10.439Z" }, - { url = "https://files.pythonhosted.org/packages/6c/3c/28cc4db153a7601a996985bcb564f7b8f5b9e1a706c7537aad4b4809f358/hf_xet-1.1.9-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ad1022e9a998e784c97b2173965d07fe33ee26e4594770b7785a8cc8f922cd95", size = 3251429, upload-time = "2025-08-27T23:05:16.471Z" }, - { url = "https://files.pythonhosted.org/packages/84/17/7caf27a1d101bfcb05be85850d4aa0a265b2e1acc2d4d52a48026ef1d299/hf_xet-1.1.9-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:86754c2d6d5afb11b0a435e6e18911a4199262fe77553f8c50d75e21242193ea", size = 3354643, upload-time = "2025-08-27T23:05:17.828Z" }, - { url = "https://files.pythonhosted.org/packages/cd/50/0c39c9eed3411deadcc98749a6699d871b822473f55fe472fad7c01ec588/hf_xet-1.1.9-cp37-abi3-win_amd64.whl", hash = "sha256:5aad3933de6b725d61d51034e04174ed1dce7a57c63d530df0014dea15a40127", size = 2804797, upload-time = "2025-08-27T23:05:20.77Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970, upload-time = "2022-11-02T17:34:01.425Z" }, ] [[package]] -name = "httpcore" -version = "1.0.9" +name = "flask" +version = "3.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "certifi" }, - { name = "h11" }, + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, ] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, ] [[package]] -name = "httpx" -version = "0.28.1" +name = "flask-cors" +version = "6.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, + { name = "flask" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +sdist = { url = "https://files.pythonhosted.org/packages/47/03/4e464a50860f9adf08b5c1d3479cb8ea1f12af2aa69535c7042c6e628135/flask_cors-6.0.5.tar.gz", hash = "sha256:30c5031552cd59f620ac0c8211dac45b345d3b2df310e7721879e4f46ef9c601", size = 101386, upload-time = "2026-06-08T20:20:17.765Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, + { url = "https://files.pythonhosted.org/packages/49/55/5bb1a2d918e9f02f131e47a59032bae70e48050e986e941511fd737a935c/flask_cors-6.0.5-py3-none-any.whl", hash = "sha256:68fcf75693e961f3af26683b23c4b9a8fb6b64de17d20d0c37b95e8de7ab2ed8", size = 16692, upload-time = "2026-06-08T20:20:16.247Z" }, ] [[package]] -name = "httpx-sse" -version = "0.4.1" +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230, upload-time = "2025-10-06T05:35:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621, upload-time = "2025-10-06T05:35:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889, upload-time = "2025-10-06T05:35:26.797Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464, upload-time = "2025-10-06T05:35:28.254Z" }, + { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649, upload-time = "2025-10-06T05:35:29.454Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188, upload-time = "2025-10-06T05:35:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748, upload-time = "2025-10-06T05:35:32.101Z" }, + { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351, upload-time = "2025-10-06T05:35:33.834Z" }, + { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767, upload-time = "2025-10-06T05:35:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887, upload-time = "2025-10-06T05:35:36.354Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" }, + { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659, upload-time = "2025-10-06T05:35:41.863Z" }, + { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837, upload-time = "2025-10-06T05:35:43.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989, upload-time = "2025-10-06T05:35:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6e/fa/66bd985dd0b7c109a3bcb89272ee0bfb7e2b4d06309ad7b38ff866734b2a/httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e", size = 12998, upload-time = "2025-06-24T13:21:05.71Z" } +sdist = { url = "https://files.pythonhosted.org/packages/10/a1/ae4e3e5003468d6391d2c77b6fa1cd73bd5d13511d81c642d7b28ac90ed4/fsspec-2026.6.0.tar.gz", hash = "sha256:f5bac145310fe30e16e1471bd6840b2d990d609e872251d7e674241822abf01a", size = 313646, upload-time = "2026-06-16T01:57:28.105Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/0a/6269e3473b09aed2dab8aa1a600c70f31f00ae1349bee30658f7e358a159/httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37", size = 8054, upload-time = "2025-06-24T13:21:04.772Z" }, + { url = "https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl", hash = "sha256:02e0b71817df9b2169dc30a16832045764def1191b43dcff5bb85bdee212d2a1", size = 203949, upload-time = "2026-06-16T01:57:26.358Z" }, ] [[package]] -name = "huggingface-hub" -version = "0.34.4" +name = "google-adk" +version = "2.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "filelock" }, - { name = "fsspec" }, - { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "aiosqlite" }, + { name = "authlib" }, + { name = "click" }, + { name = "fastapi" }, + { name = "google-auth", extra = ["pyopenssl"] }, + { name = "google-genai" }, + { name = "graphviz" }, + { name = "httpx" }, + { name = "jsonschema" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, { name = "packaging" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "python-multipart" }, { name = "pyyaml" }, { name = "requests" }, - { name = "tqdm" }, + { name = "starlette" }, + { name = "tenacity" }, { name = "typing-extensions" }, + { name = "tzlocal" }, + { name = "uvicorn" }, + { name = "watchdog" }, + { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/45/c9/bdbe19339f76d12985bc03572f330a01a93c04dffecaaea3061bdd7fb892/huggingface_hub-0.34.4.tar.gz", hash = "sha256:a4228daa6fb001be3f4f4bdaf9a0db00e1739235702848df00885c9b5742c85c", size = 459768, upload-time = "2025-08-08T09:14:52.365Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/a1/6048b1c22817859bafc1101f8ba26f704233d9acb07e715dff5fb41b9b55/google_adk-2.4.0.tar.gz", hash = "sha256:5a2996b288d591deefcb277eeeeb7da838d72056675763bfef52ad3b36975dde", size = 3566788, upload-time = "2026-07-07T19:46:14.802Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/7b/bb06b061991107cd8783f300adff3e7b7f284e330fd82f507f2a1417b11d/huggingface_hub-0.34.4-py3-none-any.whl", hash = "sha256:9b365d781739c93ff90c359844221beef048403f1bc1f1c123c191257c3c890a", size = 561452, upload-time = "2025-08-08T09:14:50.159Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ab/12ec18054990ac69f37dae8327f5d8d5e04557bada0d8d725afd872d3020/google_adk-2.4.0-py3-none-any.whl", hash = "sha256:fba91f1a693e5fc2fd13dc40d625562bd52e44a7baaeecedb01811a68063d847", size = 4123277, upload-time = "2026-07-07T19:46:13.026Z" }, ] [[package]] -name = "hyperlink" -version = "21.0.0" +name = "google-auth" +version = "2.56.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "idna" }, + { name = "cryptography" }, + { name = "pyasn1-modules" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/66/b4ba60005743e01933e22b4f62313e063f7460458b7d8a358427b4930013/google_auth-2.56.0.tar.gz", hash = "sha256:f90fa030b569a92654b9d690665a073841df33d57487be53db583a9a0867a553", size = 364629, upload-time = "2026-07-13T19:09:57.143Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/7d/cd3e187f14ce832e419e70709bfcc40cb0dc11517d5d03c9d3919bcc3101/google_auth-2.56.0-py3-none-any.whl", hash = "sha256:6e88c10217e07a92bfd01cac8ee99e32ccfb08414c3102e6c5b8d58f37a0d1e0", size = 257976, upload-time = "2026-07-13T19:09:42.685Z" }, +] + +[package.optional-dependencies] +pyopenssl = [ + { name = "cryptography" }, +] +requests = [ + { name = "requests" }, +] + +[[package]] +name = "google-genai" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "google-auth", extra = ["requests"] }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "sniffio" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/01/e7b5f3aac89200c78318ed7643401e7f5ed3131b0cd353c07483606b1e61/google_genai-2.11.0.tar.gz", hash = "sha256:4c5e524d24b145c96be327f9a7f8f04b0fe4efee0533877795e9848afed01749", size = 622366, upload-time = "2026-07-09T17:49:43.862Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/ef/d296c23390160a8b0b1dafb36dd3cb36a39ed40c81cd27e04e6233334186/google_genai-2.11.0-py3-none-any.whl", hash = "sha256:5bc8186100e1d34d691fbe0cba392b7e04e98d286ca952323a6672d054accf95", size = 984162, upload-time = "2026-07-09T17:49:42.15Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.75.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, +] + +[[package]] +name = "graphql-core" +version = "3.2.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/90/f2aff026ab4aebd80eb71905106a0885f4cfde85dcf965543f45bed0d9ee/graphql_core-3.2.11.tar.gz", hash = "sha256:e7e156d10beb127cab5c89ff0da71416fc73d27c484a4757d3b2d35633774802", size = 528407, upload-time = "2026-06-05T13:45:22.915Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/15/b92b4e1d88d02c6eff9733c9eea21846ab435cc4d813d84ccc5d335955df/graphql_core-3.2.11-py3-none-any.whl", hash = "sha256:0b3e35ff41e9adba53021ab0cef475eb18f57c7f53f0f2ca55567fbf3c537ea0", size = 214879, upload-time = "2026-06-05T13:45:21.245Z" }, +] + +[[package]] +name = "graphviz" +version = "0.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/b3/3ac91e9be6b761a4b30d66ff165e54439dcd48b83f4e20d644867215f6ca/graphviz-0.21.tar.gz", hash = "sha256:20743e7183be82aaaa8ad6c93f8893c923bd6658a04c32ee115edb3c8a835f78", size = 200434, upload-time = "2025-06-15T09:35:05.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl", hash = "sha256:54f33de9f4f911d7e84e4191749cac8cc5653f815b06738c54db9a15ab8b1e42", size = 47300, upload-time = "2025-06-15T09:35:04.433Z" }, +] + +[[package]] +name = "griffelib" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/e4/8d187ea29c2e30b3a09505c567513077d6117861bde1fbd997a167f262ec/griffelib-2.1.0.tar.gz", hash = "sha256:762a186d2c6fd6794d4ea20d428d597ffb857cb56b66421651cbba15bdd5e813", size = 216234, upload-time = "2026-06-19T12:05:42.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl", hash = "sha256:cc7b3d2d2865ad0b909fcc38086e3f554b5ea7acbaa7bbb7ecaa3f5dfb7d9f00", size = 142560, upload-time = "2026-06-19T12:05:38.742Z" }, +] + +[[package]] +name = "grpcio" +version = "1.82.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/bc/656b89387d6f4ed7e0686c7b64c2ae7e554a759aa58122c8e5fb99392c32/grpcio-1.82.1.tar.gz", hash = "sha256:707b24abd90fcb1e45bcc080577da1dbf9971d107490589b9539af8e1e77b4b5", size = 13187300, upload-time = "2026-07-08T12:36:16.588Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/14/5d05bfd85c101cbe44a12d7c1cea9c40698e0438cddf3a70019f735b5a27/grpcio-1.82.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:91859d1cac5f47caec5fc40e9f827500cdb54ce5b36450dc9a65616b5af49c17", size = 6177087, upload-time = "2026-07-08T12:34:06.825Z" }, + { url = "https://files.pythonhosted.org/packages/19/2e/c906f8e6d0b54c0137885fff6f7b5883c6bbc381b44a0ba5ea07d7d1579b/grpcio-1.82.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c80c9741dcef192f669876a81957cf7713b441c2f0c43631350d75fa49321d31", size = 11960907, upload-time = "2026-07-08T12:34:10.583Z" }, + { url = "https://files.pythonhosted.org/packages/de/be/ec4aa76cdf25539b9e960cbb9d5739f892ea6cde58078b5293860c1159d3/grpcio-1.82.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b89cff456796d2f0581783726ad017a2c70aff2d27b0f05504c34e2e417f7560", size = 6754802, upload-time = "2026-07-08T12:34:13.082Z" }, + { url = "https://files.pythonhosted.org/packages/e6/dd/47519c2a8fd9db47ec4493f44bd9f5b0175307e07089b1132e54b7b5b19c/grpcio-1.82.1-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:d6e8a08f7038ba7a77f71e250804e4aba84fe91d22cfc54ff43c07b7529c4728", size = 7484535, upload-time = "2026-07-08T12:34:15.164Z" }, + { url = "https://files.pythonhosted.org/packages/63/99/659711e9689c4dd553bcd4eacff9cb9f458f34b60edf7afb3bbc1b0a58a2/grpcio-1.82.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:50fd2fe83426b1b1c6cdc4d72d555223b7dddf8ce07c5bac218b13fc6d684c6f", size = 6919066, upload-time = "2026-07-08T12:34:17.367Z" }, + { url = "https://files.pythonhosted.org/packages/29/39/f2b772356b4f593ffe439795509fcbf675b0ff98211ae8ce2a180f2e559f/grpcio-1.82.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b758540a24d5394a9c578bf9f6126389f474b106ac3d9df1d53de56cb14c9fd9", size = 7525855, upload-time = "2026-07-08T12:34:19.479Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/b28cfffb989a84d8272593498bddd2d68148cce1813ad55189c469b0f1f8/grpcio-1.82.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c4ba4aac238f685743575d9d700003ac16537cce26e7c774993134f530652464", size = 8565122, upload-time = "2026-07-08T12:34:21.951Z" }, + { url = "https://files.pythonhosted.org/packages/97/f9/54956cb0c701190cbc9d7e535c3f84acf0285c6b9ed198a902766e17c3cd/grpcio-1.82.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed6fc621d6f366c88a60f0b971d5afd21d441d9aa561ee688de5b7acdb2cf901", size = 7933872, upload-time = "2026-07-08T12:34:24.539Z" }, + { url = "https://files.pythonhosted.org/packages/76/85/5f9cd1f965bbe4329556a212f178ae0c072b18b446cae05ed32fa8847c53/grpcio-1.82.1-cp310-cp310-win32.whl", hash = "sha256:bd2f45e46fff5b91c10997d0743a987517a7dde67c64c592835c2dcaac66f587", size = 4257373, upload-time = "2026-07-08T12:34:26.566Z" }, + { url = "https://files.pythonhosted.org/packages/93/b0/c4f42f7c69c53d27ed41643421b55908bcbe885b68f5a208135c72917c98/grpcio-1.82.1-cp310-cp310-win_amd64.whl", hash = "sha256:5e171d5f0d6a0af78ea7512783f170a44f80c165259d8773e3a354a7f991f2b5", size = 5006571, upload-time = "2026-07-08T12:34:28.778Z" }, + { url = "https://files.pythonhosted.org/packages/26/5b/e5092af97fa671ca279b3e373251af4bf87d5fbda7dc85f6a616899562a7/grpcio-1.82.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:0ddb18a9a9e1f46692b3567ae4abb3f8d117ce6afea48650f8eca06d8ab5d06f", size = 6181472, upload-time = "2026-07-08T12:34:31.009Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/18053a3a2ca03d0c2a1b8cc7271e705007a16aa5dae84bac00935c5b1a7f/grpcio-1.82.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:cf855b1af246720f567b0ce5d0724d45dfa4188eecc3296a2a69257b11b9e94b", size = 11970995, upload-time = "2026-07-08T12:34:33.603Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/21b1acb052876ad00959ec4d1b05fe08607d650bcfa282073bb164c2703c/grpcio-1.82.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb30cb13e25bc13cea70ffc69d6d90c49d36ea6c1d4549e6912f70177834cac", size = 6760127, upload-time = "2026-07-08T12:34:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/3e/12/25eef9c245c54f0061317d13a302357fe8ea03bac240b2b02ececcf54da4/grpcio-1.82.1-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1e822b2774f719c017cbe700b6e47173b6ae290fb84906f52a5a3c2c60b62e1e", size = 7484377, upload-time = "2026-07-08T12:34:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/a0/41/1a348767eb9d9bd7765dc4fa8a01723d3bb386d67f981ee5c6f9c02b8b1c/grpcio-1.82.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5dafb1ece8ed45dee7c738f166ec82e19673221ed5ab8967f72858a4685345b2", size = 6924269, upload-time = "2026-07-08T12:34:40.583Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b9/3aae7a03d34c86ea27988db859a6087c186f6c3f53f9b551e07afd989bfa/grpcio-1.82.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e06503106e7271e0a49fd5a1ac04747f1e47e87d900476db6fe45bc87ee411f4", size = 7531848, upload-time = "2026-07-08T12:34:43.277Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/3c4afa625d0dac9090707966916284c035fc5b2fb3e2c51e156accee6735/grpcio-1.82.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ff99bc8cafb6a952201c37b995f425e641c93ffa6e072258525feab57290141d", size = 8568217, upload-time = "2026-07-08T12:34:45.502Z" }, + { url = "https://files.pythonhosted.org/packages/20/9c/d8489c628e73e20a3d034e7f66912de7b1acb405f01d388f056a88e47924/grpcio-1.82.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:644ae1b94266ac785330f4590a69e52b6a7eb73029043a02209db81c81397d69", size = 7938771, upload-time = "2026-07-08T12:34:48.323Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b7/0a92cfd1658f3a896d4aa12d4efeb7dd4ddfc723725ae22741a5241ea710/grpcio-1.82.1-cp311-cp311-win32.whl", hash = "sha256:e203d2e19d471630084a16c815616f8211dff21c268ab3c5f5bf38417832e074", size = 4256432, upload-time = "2026-07-08T12:34:50.432Z" }, + { url = "https://files.pythonhosted.org/packages/c7/6a/2872c761b025d9ec74386f22a4a7d59c5a5b00ebf718761b33739ffc45de/grpcio-1.82.1-cp311-cp311-win_amd64.whl", hash = "sha256:0d8299c285fe6cc6a1f56badf8d3bc5078c8d20273ee64bafa3783b4bc29a769", size = 5009633, upload-time = "2026-07-08T12:34:52.67Z" }, + { url = "https://files.pythonhosted.org/packages/dc/88/d1350bf3343a2ed87d801584e40609f6c6bd3087926eeca03de50348cf4a/grpcio-1.82.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:c09bd5fa0d5b1fbd773ec349fe61441c3e4ebf168c229aa7538a820bdfad6a58", size = 6144689, upload-time = "2026-07-08T12:34:55.567Z" }, + { url = "https://files.pythonhosted.org/packages/e6/33/71875cdecd27c24ac1385d4783a09853f01b84a825a36aec2a2bc7d0d080/grpcio-1.82.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:1eae24810720734598e3e6a1a528d5de0f265fe3fc86575e9ecce424b9ec7379", size = 11952034, upload-time = "2026-07-08T12:34:58.128Z" }, + { url = "https://files.pythonhosted.org/packages/82/b2/d9125df3d8a140dec12cc82c05b7deafedeababcff6496f28b2fd5634d10/grpcio-1.82.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a6bd5daf5bde7b24d7ad2cbaf8bf9eac620d96222016bb5e7ddde930dec0673f", size = 6710772, upload-time = "2026-07-08T12:35:01.33Z" }, + { url = "https://files.pythonhosted.org/packages/88/9b/69e2d1627398b964f34437dc476a5aff5a2cc8e7f247d26272b5674b5faf/grpcio-1.82.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1ecfde669cb687ac020d31ff76debe5dc7a62213335f02262eb6625628da1c03", size = 7450677, upload-time = "2026-07-08T12:35:03.926Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e7/8f855ca29c294956122a2a73023655b9b02602d5111dad2b9b00e7631c68/grpcio-1.82.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:011c8badee95734dee8bf05ce3464756a0ac3ebb8d443afd20c0e2b5e4640ad9", size = 6886855, upload-time = "2026-07-08T12:35:06.174Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f0/fa87e85f49925f44c479d07e58b051e69bcfef6b6d5fbc6749d140f6730a/grpcio-1.82.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b85f4564926fb23114d239392bdcae200db1e6179629edd7d7ab0ab89c96a197", size = 7501323, upload-time = "2026-07-08T12:35:08.49Z" }, + { url = "https://files.pythonhosted.org/packages/67/55/2e0b10ae1d3ef9dcc480b91dc2158f4931fc4675d3af0a2836e39b2a744f/grpcio-1.82.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2c0c8270833395644c3fe6b6a806397955a2bc0538000a19a78b90c05a6c16e0", size = 8536899, upload-time = "2026-07-08T12:35:10.975Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/a3d8b0431fa221efc51ee39d73595ede74ba82a43b7c4313192e580face2/grpcio-1.82.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2ba199205ff46c7778290fe1673c91ac8e7e45678dd5c86e9e56fa33ec8788f6", size = 7913892, upload-time = "2026-07-08T12:35:13.944Z" }, + { url = "https://files.pythonhosted.org/packages/b8/92/f2651ec704d9852a56faef394775038afba435b50ce82ab2404d119c3355/grpcio-1.82.1-cp312-cp312-win32.whl", hash = "sha256:06127691866e295c14e84a1fb86356dd962254f6abd0da4ca4b001eea9e89438", size = 4240985, upload-time = "2026-07-08T12:35:16.048Z" }, + { url = "https://files.pythonhosted.org/packages/96/4f/a5fe8bf0d0a1b24855f370293075c931f27de4eb55f0f158786095bf3c11/grpcio-1.82.1-cp312-cp312-win_amd64.whl", hash = "sha256:1fa3223a3a2e1db74f4c2b255189eb7ea875dfba56e221d252ee3fc7b204778e", size = 5001580, upload-time = "2026-07-08T12:35:18.689Z" }, + { url = "https://files.pythonhosted.org/packages/1b/3e/496992d08c0aaa11272eb6228dc8ab947da01fe835de243cd00521bce4c4/grpcio-1.82.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:b454a2d97bfab7565683a02345f86bd182ab69fd7c2bdb7414171e7538f266b1", size = 6146068, upload-time = "2026-07-08T12:35:21.365Z" }, + { url = "https://files.pythonhosted.org/packages/e7/8f/f263d6f14fdba6b56cfadd91fd3e158a52682b72c6016d1f8723d435659f/grpcio-1.82.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:3dde70abfc80b3be11de53ba0d601c439e7fb2afd3583ad1788d1146bec92fdc", size = 11948600, upload-time = "2026-07-08T12:35:24.312Z" }, + { url = "https://files.pythonhosted.org/packages/8c/14/3a02e6ee49c2d85bc15eaae321e0e11ab3542cad3c5b2de121ecce0c4296/grpcio-1.82.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5523099c98c292ea1ae08e617249db760c56a78f8deae879027fe7d1ffbcbf6", size = 6714591, upload-time = "2026-07-08T12:35:27.027Z" }, + { url = "https://files.pythonhosted.org/packages/69/80/58e3738696f48ab7645347b98d8a7f93d10e00e6218388fbfcd6c9310e3d/grpcio-1.82.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:5e5c4dc0a59b0f8490a6bdfd6fc8395b9d8ad8a8407c7d67ca7b5bba15c0877f", size = 7454995, upload-time = "2026-07-08T12:35:29.599Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6c/2557c1a889363072fbf2285ecd0e8c44860d4dbd60f017a32537c5b863e2/grpcio-1.82.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c40d94ba820329cc191981bc22fa6f6eed0799c6d921f3c6709521d59d4a2fd7", size = 6888621, upload-time = "2026-07-08T12:35:32.38Z" }, + { url = "https://files.pythonhosted.org/packages/d2/66/907706ccaff1223f1e10fd5b37fc16faead43392fccb4e786e7e390ac141/grpcio-1.82.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4c816180e31e273caaec6f8bd86a8392499d5bbb26f41da44e3dce48bde69095", size = 7505069, upload-time = "2026-07-08T12:35:35.072Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7c/ff97b0d0f635987ee5ec80dfedafa1aad629303745d48e8637d10eec5b80/grpcio-1.82.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e31fd780b261830720cb70b0fd8f0aa51d49e75a66d7464ad2e31d4b765f2580", size = 8535384, upload-time = "2026-07-08T12:35:37.954Z" }, + { url = "https://files.pythonhosted.org/packages/62/9e/a97fddd970a8d1588cade06eca20443761c1858b0ad6590a5c835aa18062/grpcio-1.82.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d76152d7c31d7210d4a106e5d8b64da5bba5d6abf11be30e2f7b0a0c59bbcbf", size = 7910707, upload-time = "2026-07-08T12:35:40.797Z" }, + { url = "https://files.pythonhosted.org/packages/20/e4/eaba1517888af483a88d449eb7566f0f7f63446d46f339c5891798435875/grpcio-1.82.1-cp313-cp313-win32.whl", hash = "sha256:38e9dcb5258226fb3282630b31b16a968df52c8c6ad514af540646e0a4578f8a", size = 4240363, upload-time = "2026-07-08T12:35:43.298Z" }, + { url = "https://files.pythonhosted.org/packages/b0/42/66a98d47732e35290bef722f6149fed3709cd4cf61166f6f53a12f417302/grpcio-1.82.1-cp313-cp313-win_amd64.whl", hash = "sha256:3dbfb52c36d9511ac2b8e6c94fdde837b393ae520cc321f52a333a2deedf5a90", size = 5000980, upload-time = "2026-07-08T12:35:46.262Z" }, + { url = "https://files.pythonhosted.org/packages/b4/cb/cf9ae9e164c6e6dc8a494faa9771763df9da150eefe19671009624d1559f/grpcio-1.82.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:35f990f7784c8fd2872644f07f96ebb4d9e48e145a190ab80d0280af91a1bfb2", size = 6146901, upload-time = "2026-07-08T12:35:49.261Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/eccf26dbcfb7f7cab8027c5490a16c8937c5aa7a2ec20a3eab2cf7a43165/grpcio-1.82.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:46536a4a1f4434df3c851b9254ff6fc7df5705b273681a15ca277d5921c178a0", size = 11954756, upload-time = "2026-07-08T12:35:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/ad/75/3b3b4a3cc9f084b026af96e1d3e539b1af29ec7f41ed0dfff3cb99cc8626/grpcio-1.82.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d6650a7c1ebb7921c70e12a385439a8118efb99e669fa9ed31cf25db1843937c", size = 6723087, upload-time = "2026-07-08T12:35:54.973Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8b/b0f0c9b1400a99a4da4c09b114f101b192f8f11192e76f620b8962f5d90b/grpcio-1.82.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b8e110c66df5204c0506d6c8787b35d48b8b699ef5aa366d6c4d67325c67fe9a", size = 7454542, upload-time = "2026-07-08T12:35:57.586Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bd/428e38868382aa193697a5aa53973f29c58e58ba4268aa0c86a2715ee58b/grpcio-1.82.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f853eae07235a51a27bb5d6a9a175a59ca55dc9b99edc6ce2f76f07332d333ae", size = 6889588, upload-time = "2026-07-08T12:36:00.012Z" }, + { url = "https://files.pythonhosted.org/packages/49/ce/03e01d5e10259bf5c08ee50570cc94724e79c956f61fd2f09b341af0956c/grpcio-1.82.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:60b0f2c95337694fc094b77d9f60f50566c84b5677393e342eb98daeee242d98", size = 7514166, upload-time = "2026-07-08T12:36:02.693Z" }, + { url = "https://files.pythonhosted.org/packages/ff/59/278b4b600329e2ba3849f3c1ea3c820b3a01b38a7ad184ba09595e8d2733/grpcio-1.82.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:b064fc444812bdaa9825d33c26f8d732d63ee6a5d78557c1faf92c98687fed27", size = 8536166, upload-time = "2026-07-08T12:36:05.349Z" }, + { url = "https://files.pythonhosted.org/packages/44/27/7ccf2ef00f27a8e47a79d641c8ceaf7d3028c7a03d9a97b4c8a9a783c086/grpcio-1.82.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7d7ede11d747b4e1bd05e3bc0260e155b65a88735a895a10f6521f19b889511e", size = 7912572, upload-time = "2026-07-08T12:36:08.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/be/33742482d2753f2d3a1b7641664b6622262d44f2f3b609f13425dd86d36f/grpcio-1.82.1-cp314-cp314-win32.whl", hash = "sha256:3d21f19838dc255ecbb79321b15ae9b98fbddff4c3d4aedb0a81bdd7f4ab572a", size = 4321856, upload-time = "2026-07-08T12:36:10.899Z" }, + { url = "https://files.pythonhosted.org/packages/cc/67/03329c847172c78ddeb1eb9be6b444fdbc12775a84c958b27e427e7b926d/grpcio-1.82.1-cp314-cp314-win_amd64.whl", hash = "sha256:e20f1edbb15f99e3128ec86433f9785fd5a451d8f115e74fe0056134f092a9d5", size = 5141114, upload-time = "2026-07-08T12:36:13.595Z" }, +] + +[[package]] +name = "grpcio-tools" +version = "1.81.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio" }, + { name = "protobuf" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/b3/1c5951352d6777fd7f99a0ccee04617fdfd8a5dbf2918a1f58c8b2b280b8/grpcio_tools-1.81.1.tar.gz", hash = "sha256:a22a3870180927fdd84e2b27d079ef5b7f5f8c6110181b6736afc17a463481f1", size = 6236155, upload-time = "2026-06-11T12:51:21.235Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/e1/1fcf884902ae7255d8da224cfa638ea88a46d50f62a33d06d35c8960b029/grpcio_tools-1.81.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:9b6ba8a72cfda576508701a7c0bbeebe6f6f9843320d4f12e74efd19ddccd965", size = 2586261, upload-time = "2026-06-11T12:49:21.447Z" }, + { url = "https://files.pythonhosted.org/packages/a2/d7/1815110b2d40ec99dbb0a7e6d7eafd591cd1f1e9bf9d3858cd9cf3ffacbd/grpcio_tools-1.81.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:ac47a9ea1224df8b653072614e6f0207e9fbfe63fdabaa5918a60ca5fc931b88", size = 5817509, upload-time = "2026-06-11T12:49:25.958Z" }, + { url = "https://files.pythonhosted.org/packages/23/e8/af99579842b5a555312fa782f32ce0f99bd35b2b7a1243294b2755468857/grpcio_tools-1.81.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eac4bb645ceff0c147cc720a40ae68f97427eaafb4968e866dd8fcc20d3d4831", size = 2634112, upload-time = "2026-06-11T12:49:27.937Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cd/235ad56ac728c49c17e9218c4daccd5831e6ec7af94236bec0cc66c71c68/grpcio_tools-1.81.1-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:cc410b621dd85193766c12dca2e238696199a27a65d2b31b6f0a4c6c0043ff26", size = 2957950, upload-time = "2026-06-11T12:49:29.619Z" }, + { url = "https://files.pythonhosted.org/packages/77/3e/9103e8b4610597bf89db49eb112091c91bf5d63ddef2a951e11a4be05f2b/grpcio_tools-1.81.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b62d254c214faa3773eac709376ae25cf7abff1a76ba5fc4dbcd7b14fc4e4ae6", size = 2697765, upload-time = "2026-06-11T12:49:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/3c/86/beb2a43fbb93570a2305696083f6736566301d957869f463308ec6839f95/grpcio_tools-1.81.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bd0b68dc76b10b3384b9b6e9f59202b83dcaafd8098eb644759a69316686acf8", size = 3147588, upload-time = "2026-06-11T12:49:33.748Z" }, + { url = "https://files.pythonhosted.org/packages/af/4d/b0182d9948631cd837a372b6625cf59d6e335d4aab0f425d4b7306619074/grpcio_tools-1.81.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a28d231455ab6e3558299f7d831a73c8be8ee6b7ec614ecf39eb50c0ed15767f", size = 3708798, upload-time = "2026-06-11T12:49:35.979Z" }, + { url = "https://files.pythonhosted.org/packages/23/9b/f452a189d399051d85cf82fe2f27a070efaa52512a2c5e3ae6ef1ae99a1f/grpcio_tools-1.81.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:82740248eb6f3b6a38988cb5e64adb7303af9ea5cb4197c8ed08c1fabc767440", size = 3366969, upload-time = "2026-06-11T12:49:37.911Z" }, + { url = "https://files.pythonhosted.org/packages/9b/48/0075cb4f6ae7db280f461de2dbba700b22ae62e351ae13e6e461cd6804de/grpcio_tools-1.81.1-cp310-cp310-win32.whl", hash = "sha256:801d9d8ab5cddf8f8e064225292f0713427011252a07828a6b54e2ed64d534de", size = 1008713, upload-time = "2026-06-11T12:49:39.791Z" }, + { url = "https://files.pythonhosted.org/packages/17/bd/7692bc698259e5645b68720e77e7b176d376f6ae0c9db8b5b750a02f1958/grpcio_tools-1.81.1-cp310-cp310-win_amd64.whl", hash = "sha256:3c8611d6e4e859ac5373422ef27c4b7540cf98c9991c9abc6722613ef72b13aa", size = 1174752, upload-time = "2026-06-11T12:49:41.43Z" }, + { url = "https://files.pythonhosted.org/packages/18/76/14ff87090199a36f914388299a1148d0734a20cea1b0ca8480bae1f373f1/grpcio_tools-1.81.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:8161f398f957a376cae7385ea7c8684f439d460ef702b528912da3bcb31fc515", size = 2586251, upload-time = "2026-06-11T12:49:43.514Z" }, + { url = "https://files.pythonhosted.org/packages/87/a8/d5aa99de9d8b2dd2a8192c1779796eda8b0d0f1dd915422e0a8a61b80391/grpcio_tools-1.81.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:53ef76cc3b0493ff734a5e8c39d5b519e1822236fcccdfe7677c5e1efd767761", size = 5818063, upload-time = "2026-06-11T12:49:45.975Z" }, + { url = "https://files.pythonhosted.org/packages/ba/cb/2e9a6dbc6a514dd3cd264fb3bf9217937453a4d45dbc3ca6ca4ee34ba1a7/grpcio_tools-1.81.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:690e6dcaa8b8a7886ce206ba344e2127211597e1a1ddab73df9f3d80c8f6707e", size = 2634061, upload-time = "2026-06-11T12:49:48.13Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2b/2ccd1a929e6c8ad84a0aa8d66ad9f615b4a8e79d9927373d86aa36b4ba2e/grpcio_tools-1.81.1-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ad7a997c07bd345e84842e60561e7e2cc090ce6c4e1d2f0407e31b85b40fc49a", size = 2958029, upload-time = "2026-06-11T12:49:50.466Z" }, + { url = "https://files.pythonhosted.org/packages/e7/67/2da8cd312edc348f44f26f82096b25cdb7d2905cd786acc6bf777b169502/grpcio_tools-1.81.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b6bd163ece4535726e5292b845ed80ae9b2cae73ba091c7d6c66033c430e3857", size = 2698031, upload-time = "2026-06-11T12:49:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ba/ad1680fbdf9317c4f1e54c37c96d1f422370df66ac9adbd175c7cb3531d7/grpcio_tools-1.81.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2baa7e735f35b2a648144c03348a126097b13e101d3c242d5edb6ac91437ccbe", size = 3147541, upload-time = "2026-06-11T12:49:54.43Z" }, + { url = "https://files.pythonhosted.org/packages/57/c1/57cd08eef293d713cb8935295e4f08d8f0013480b2ba3aad1af0271eb7ba/grpcio_tools-1.81.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1d602b410b2b2addc434cace9ce4fe2035974a3078228f98ffa049a5c90acc2f", size = 3708524, upload-time = "2026-06-11T12:49:56.544Z" }, + { url = "https://files.pythonhosted.org/packages/52/31/01ea8ca9c82fe2c79b5b594c3ae427d56699bc106b2d91caca129add8b10/grpcio_tools-1.81.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f8cb64f87c45ccca8234fa47e6b21f09e43801ff11b556deecb461b3b3e9f292", size = 3367022, upload-time = "2026-06-11T12:49:59.608Z" }, + { url = "https://files.pythonhosted.org/packages/7d/35/8140cd175602df3d17215cfb28a7ea55b7a67e2b872be76e1ee4af5c4df9/grpcio_tools-1.81.1-cp311-cp311-win32.whl", hash = "sha256:87b25ca0e27373a4a32a629a4ba976f5764b9887dd50d6fe017d38009a0363e8", size = 1008980, upload-time = "2026-06-11T12:50:01.422Z" }, + { url = "https://files.pythonhosted.org/packages/be/86/1bd29ab3c52457702b96536f1f208ab27695322d855f95c9666dfb713019/grpcio_tools-1.81.1-cp311-cp311-win_amd64.whl", hash = "sha256:204de03b539a4b08772c6553b92bcc112cbc965e0ac22f909f6d133b8ac33a8c", size = 1174840, upload-time = "2026-06-11T12:50:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8a/824a9ca20bcdce8a568bb8c9f98bfeb7fad62129235e6d2ae7576fd1250a/grpcio_tools-1.81.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:353b1fafcc739c31ed42271052709595b340d34f27c459beeb78a32938305bb5", size = 2585927, upload-time = "2026-06-11T12:50:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/2f/35/e5f9f671378b1b89a896150d3e4fa2c6ec61a5e1e9e5107ce4c140ccc931/grpcio_tools-1.81.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:768f584c2423cbeb6cb6867817a39365b987ff16b8259a3adbc6546b9e303a4e", size = 5815665, upload-time = "2026-06-11T12:50:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/c6/02/631b628e4072e988c669bd8f1b2406ef3c9a4cfcb2625bbf2a308a07b71d/grpcio_tools-1.81.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1680b35a84f4694401819ac4acac42dda6dbc7bb8fc74112fd1a60425a07adf4", size = 2635518, upload-time = "2026-06-11T12:50:10.391Z" }, + { url = "https://files.pythonhosted.org/packages/de/7c/2e3537e3ea3d1c0ddd6766cf6a7c62b487d89fb005713df2781d5f21483a/grpcio_tools-1.81.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f64e665c8ec639278ecf009beb92cbdcc5994f617c1af3d58036e1f70b1423ec", size = 2958252, upload-time = "2026-06-11T12:50:12.677Z" }, + { url = "https://files.pythonhosted.org/packages/35/68/14013cb2942bdac354746b643b4c37dd91906da8dce00f41c616e88bf33d/grpcio_tools-1.81.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba8f1ae82ad199f43448995715445cc623fb20d3882382e4be61f0da8ccb3f0e", size = 2698439, upload-time = "2026-06-11T12:50:15.017Z" }, + { url = "https://files.pythonhosted.org/packages/bd/45/000c14c0338a7ad36054b9f17ea41842deb7841c05c067dd36cc831bc0f4/grpcio_tools-1.81.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b7b6d1e986d5923751bfe2b5cca9c4cb3d5653446e4fa4aacd438033e2dc360a", size = 3152160, upload-time = "2026-06-11T12:50:17.3Z" }, + { url = "https://files.pythonhosted.org/packages/41/97/881930ca3967d2c8a95649bea8ebc991a7cf2331bc96679fd3600450dccc/grpcio_tools-1.81.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7f208c207aca639dcb34648d3826c38d7cf3485118fb2065117e9fc4827406b3", size = 3710468, upload-time = "2026-06-11T12:50:19.479Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b5/67baeba7366162652cdc1dbd962289accde07241bc8f42f6f02b305efcc6/grpcio_tools-1.81.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:724ecb69af63d2f6d4ccea3e6fa0ca110ed9c5824d48c2f887c631bbb03c1c3c", size = 3370797, upload-time = "2026-06-11T12:50:21.501Z" }, + { url = "https://files.pythonhosted.org/packages/f2/5d/34f2dce2125ccb107e32b57f5a9c1257edcc0793b0d2fef1e8b13a6bac3c/grpcio_tools-1.81.1-cp312-cp312-win32.whl", hash = "sha256:895a6782cec86beac71ccebb4b9848259c6f04a3028b8e42fa8d40cfe5146593", size = 1008453, upload-time = "2026-06-11T12:50:23.358Z" }, + { url = "https://files.pythonhosted.org/packages/8a/be/09da8256ec8d2a5ce8a1acc51cbbc4ca52a462d78ed3412778440a56502e/grpcio_tools-1.81.1-cp312-cp312-win_amd64.whl", hash = "sha256:0265fd1386b7458302f79542558345880d484f8fa92ae196c0c0268242c5f23a", size = 1174857, upload-time = "2026-06-11T12:50:25.685Z" }, + { url = "https://files.pythonhosted.org/packages/76/90/5faa8b26e03495e5117f93bef8293cbada4af136362745dad7d1813ef0b0/grpcio_tools-1.81.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:3d604b4fd114b79ebb9f865bf3e04fd3ae93c704e1fad96f7fd03b0865c263b7", size = 2586071, upload-time = "2026-06-11T12:50:28.4Z" }, + { url = "https://files.pythonhosted.org/packages/e8/9a/85dc589fa6ae2439451eaa81a1578de31e29c676980d38bef7549b8a1f45/grpcio_tools-1.81.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:3389e705460efa3f3758141ba5520e6743b131c9576197c944fb9cbe49048126", size = 5813299, upload-time = "2026-06-11T12:50:31.295Z" }, + { url = "https://files.pythonhosted.org/packages/77/fd/c53994e58a837e6eefe48f53eb3492afc04f2b8af255df4adb37d14378f8/grpcio_tools-1.81.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8a17d8ceeb6a855fadf39f5171c80a382d97c4db98d5943eca553497fdebf84b", size = 2634668, upload-time = "2026-06-11T12:50:33.938Z" }, + { url = "https://files.pythonhosted.org/packages/34/32/de988e86688686a2117e7ce6ce9eff4f638c929bb55b0afe60d6fbd2e45c/grpcio_tools-1.81.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:43baf71dc60fd653062da2e95e95c73b35dd130be8f9fa3d544c3af3f808a290", size = 2957930, upload-time = "2026-06-11T12:50:36.726Z" }, + { url = "https://files.pythonhosted.org/packages/72/97/3f18a0ea32b5f809d21961dbd0bc382b589a4c3d501e3d67c345d5456ed3/grpcio_tools-1.81.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:136e90906af0df51ad929713244ba812d0dbb1844b4f467d5d86bdb054698f90", size = 2697760, upload-time = "2026-06-11T12:50:39.108Z" }, + { url = "https://files.pythonhosted.org/packages/49/c0/dbf5cbc877290ff7504a59959a8af4fdcfdaa1e84237948405ccf1aa82a6/grpcio_tools-1.81.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd6c3bf3ea6a61eb58c54368d72ada591f2a270f3a31a32e8536e773337e76d9", size = 3151456, upload-time = "2026-06-11T12:50:41.983Z" }, + { url = "https://files.pythonhosted.org/packages/de/ea/16fe2dc83140a59e5c0a0b9dc2693dd36bfaa6bd835724b4ec66a68eab7b/grpcio_tools-1.81.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c306c307f8f74cddc4056fdbb6f1da55de087a21120efbd02bd915daa5a52fd", size = 3710469, upload-time = "2026-06-11T12:50:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/22/7d/df987d7d81e7ad2f7516d9e9d56ff29c54dbc6d8587e425688dca9a28e49/grpcio_tools-1.81.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bdbdc927be2e0ea13c32564a72ee31d712a716fb6f8c0d53d37a77d8277c272c", size = 3370488, upload-time = "2026-06-11T12:50:47.199Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c5/5a63444d694ea47bf670138208f71830cc1759c402c8818092b28ab2dc5f/grpcio_tools-1.81.1-cp313-cp313-win32.whl", hash = "sha256:9d383724bcd67244b6def9e9164c640ee9380c0b7534ee7545a6fb0022a59afe", size = 1008229, upload-time = "2026-06-11T12:50:49.527Z" }, + { url = "https://files.pythonhosted.org/packages/00/75/3945e26d5c94ae6ed9be5caef73d4d66c47dc8cfdd7b4995efaf942754e0/grpcio_tools-1.81.1-cp313-cp313-win_amd64.whl", hash = "sha256:f3eb15849979ca7bb864ce81a74d68b0f225a7f111ed3fe212bfc08cf9812b10", size = 1174523, upload-time = "2026-06-11T12:50:51.755Z" }, + { url = "https://files.pythonhosted.org/packages/0d/08/e581ad42ae517a61172285047e4d710e2ac75f2f1915f7c91f284254e6d5/grpcio_tools-1.81.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:7d168ea26390717d0462c0d0408331dc98a60fc7f7e6118afac9b73f5a66d87c", size = 2585944, upload-time = "2026-06-11T12:50:54.528Z" }, + { url = "https://files.pythonhosted.org/packages/78/c8/200d90ebad685af7eea5ff7e0360c504dd01ec053fe0f1f9c4abe3ea2d5a/grpcio_tools-1.81.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:43c528655b226375013036692d8db4cd59060c1f41dd62c77f4d17b69f6ce828", size = 5813492, upload-time = "2026-06-11T12:50:57.291Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c0/60da2a1af37aa8eb47308cec24d9f7709a8976fdec3a53fd35b56b358326/grpcio_tools-1.81.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a9c6fcc68c9d5a208967bfe4fd3224d3c3be9a950c3e827e8f4b17e15c2dc555", size = 2634991, upload-time = "2026-06-11T12:50:59.767Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7f/dede28b579ae9bf9079ba1aa913e8088d1dc0cdbe21c85caa22f0790cad2/grpcio_tools-1.81.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:a987c85dcbe1b32066d7acd46266d1a428aecbd629331bf5b853e74c835bf876", size = 2957913, upload-time = "2026-06-11T12:51:02.31Z" }, + { url = "https://files.pythonhosted.org/packages/4c/38/4de2118adb58ec7ffba65ec623b5836db769665c192517cbf187db3f6145/grpcio_tools-1.81.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a882382507bb5ec6d7edc9648053dfd3bc8f9285cde56a6fa9b9a83b4bd07f1c", size = 2697709, upload-time = "2026-06-11T12:51:05.016Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e1/762ced51059e4f694fd337ecae491581d42a4e61dcb0415d8c5c60e6ddcb/grpcio_tools-1.81.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7746e508d4239a02f7e93638be5bc0ebb0120ddb796f7506aaae9d47a4599d97", size = 3151884, upload-time = "2026-06-11T12:51:07.593Z" }, + { url = "https://files.pythonhosted.org/packages/19/d8/9823090dc801e7229944874e7429c3b98e741ac778d8dc373f60240e1c43/grpcio_tools-1.81.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fc3d2a41a7a4467fa03b391394fffada9291fe8feebc8679b526f6bc36942b25", size = 3710404, upload-time = "2026-06-11T12:51:10.172Z" }, + { url = "https://files.pythonhosted.org/packages/64/4e/4eae98d02148cb6f9f452f09942afba407afa6851e6c1fddc5ae9ec0b4ed/grpcio_tools-1.81.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:21bb3ba90e6d8df1ff663d4ee39a4e5b25a64e8ed4902476ca9ded0954d3917a", size = 3370525, upload-time = "2026-06-11T12:51:12.678Z" }, + { url = "https://files.pythonhosted.org/packages/e0/3e/2206e597a128da6a03a6106d2eaf2c3e72c7d80843d4be933e3a3d10d02a/grpcio_tools-1.81.1-cp314-cp314-win32.whl", hash = "sha256:3dca56016d90a710c4d9861bae793dc089c1430a90c79ce672e948ddb65fa539", size = 1030582, upload-time = "2026-06-11T12:51:14.906Z" }, + { url = "https://files.pythonhosted.org/packages/cf/f2/bbeef86c687225b7bbc7c0acdfbd25c8bcaa3f5b1c941db053e5c3d9e859/grpcio_tools-1.81.1-cp314-cp314-win_amd64.whl", hash = "sha256:cb08172b7b629e75cb33866928d319a3196540a725eaab628ba721007140f1af", size = 1207490, upload-time = "2026-06-11T12:51:17.598Z" }, +] + +[[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 = "hf-xet" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/2d/57fd21d84d93efb4bd0b962383790e19dd1bc053501b4264c97903b4e83e/hf_xet-1.5.1.tar.gz", hash = "sha256:51ef4500dab3764b41135ee1381a4b62ce56fc54d4c92b719b59e597d6df5bf6", size = 876636, upload-time = "2026-06-08T23:02:53.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/ee/dd9ba7beae1005e54131b7d45263cc74c8a066d47d354e6d58ae9445a388/hf_xet-1.5.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:dbf48c0d02cf0b2e568944330c60d9120c272dabe013bd892d48e25bc6797577", size = 4069485, upload-time = "2026-06-08T23:02:13.193Z" }, + { url = "https://files.pythonhosted.org/packages/b6/bc/9cae6cfeb4e03070874e73e5c97c66eb90369d3206b6a2b1ef5f96520888/hf_xet-1.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78e4e5192ad2b674c2e1160b651cb9134db974f8ae1835bdfbfb0166b894a43", size = 3838493, upload-time = "2026-06-08T23:02:15.282Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b4/d5c01e0eb6d9f2ca2dacd84d0d1b71e6cfbb2ef3208c968528e010e9b3d7/hf_xet-1.5.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6f7a04a8ad962422e225bc49fbbac99dc1806764b1f3e54dbd154bffa7593947", size = 4505658, upload-time = "2026-06-08T23:02:17.196Z" }, + { url = "https://files.pythonhosted.org/packages/76/c5/29a7598c0c6383c523dc22186d577f4e04267a626cd95ae60f67c00bfe66/hf_xet-1.5.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d48199c2bf4f8df0adc55d31d1368b6ec0e4d4f45bc86b08038089c23db0bed8", size = 4292822, upload-time = "2026-06-08T23:02:18.608Z" }, + { url = "https://files.pythonhosted.org/packages/04/9a/dceaf6ca69390126b86ea825fb354b93d01163199070b7bd849225de9468/hf_xet-1.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:97f212a88d14bbf573619a74b7fecb238de77d08fc702e54dec6f78276ca3283", size = 4491255, upload-time = "2026-06-08T23:02:20.124Z" }, + { url = "https://files.pythonhosted.org/packages/48/a7/e5a7afaacf6c1791fdbeeac42951fb81c3d2bc482992b115dedcc86d963e/hf_xet-1.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f61e3665892a6c8c5e765395838b8ddf36185da835253d4bc4509a81e49fb342", size = 4711062, upload-time = "2026-06-08T23:02:21.863Z" }, + { url = "https://files.pythonhosted.org/packages/53/49/2802f8433c9742ce281bddc1e65c02c32268ca3098d66828b05e12e45ee2/hf_xet-1.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f4ad3ebd4c32dd2b27099d69dc7b2df821e30767e46fb6ee6a0713778243b8ff", size = 4017205, upload-time = "2026-06-08T23:02:23.495Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5a/50c71195b9fb883659f596e7252faf4c18c58e753a9013bdbf9bac5d2250/hf_xet-1.5.1-cp313-cp313t-win_arm64.whl", hash = "sha256:8298485c1e36e7e67cbd01eeb1376619b7af43d4f1ec245caae306f890a8a32d", size = 3845426, upload-time = "2026-06-08T23:02:25.124Z" }, + { url = "https://files.pythonhosted.org/packages/05/24/5e0c28f80371c17d49fed004597d9d132cb75c1f6f53db2cb95f459d2312/hf_xet-1.5.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3474760d10e3bb6f92ff3f024fcb00c0b3e4001e9b035c7483e49a5dd17aa70f", size = 4069676, upload-time = "2026-06-08T23:02:26.759Z" }, + { url = "https://files.pythonhosted.org/packages/d2/17/261ba565b6a4d960fb478f61fdf919c0be5824645aaf1c319eca660c1611/hf_xet-1.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6762d89b9e3267dfd502b29b2a327b4525f33b17e7b509a78d94e2151a30ce30", size = 3838509, upload-time = "2026-06-08T23:02:28.573Z" }, + { url = "https://files.pythonhosted.org/packages/4e/44/7ffdc2e184b0d41fc0f683ba3936ef669ab63cf242cf36ef50e57d683668/hf_xet-1.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bf67e6ed10260cef62e852789dc91ebb03f382d5bdc4b1dbeb64763ea275e7d6", size = 4505881, upload-time = "2026-06-08T23:02:30.257Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/788060d5aa4d5e671f1a31bf69624c314eb2d8babab3aa562f9e5d53444e/hf_xet-1.5.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c6b6cd08ca095058780b50b8ce4d6cbf6787bcf27841705d58a9d32246e3e47a", size = 4292995, upload-time = "2026-06-08T23:02:31.993Z" }, + { url = "https://files.pythonhosted.org/packages/22/93/c5540cbd6b55529b7dc42f6734e88cebee21aefbea34128b66229df56c57/hf_xet-1.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e1af0de8ca6f190d4294a28b88023db64a1e2d1d719cab044baf75bec569e7a9", size = 4491570, upload-time = "2026-06-08T23:02:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/03/f3/9d8ceab30f44f36c1679b1b8683054c71a0dadc787dbf07421891742d3ca/hf_xet-1.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4f561cbbb92f80960772059864b7fb07eae879adde1b2e781ec6f86f6ac26c59", size = 4711565, upload-time = "2026-06-08T23:02:35.454Z" }, + { url = "https://files.pythonhosted.org/packages/cd/54/27ed9a5e2cc583b4df82f75a03a4df8dbf55f5a9fa1f47f1fadfb20dbeac/hf_xet-1.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:e7dbb40617410f432182d918e37c12303fe6700fd6aa6c5964e30a535a4461d6", size = 4017343, upload-time = "2026-06-08T23:02:37.14Z" }, + { url = "https://files.pythonhosted.org/packages/ae/12/ecb2fc8d45e767580e3a37faa97cb895608b614965567efb4f18cff67e27/hf_xet-1.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6071d5ccb4d8d2cbd5fea5cc798da4f0ba3f44e25369591c4e89a4987050e61d", size = 3845716, upload-time = "2026-06-08T23:02:39.073Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d8/5e54cf37434759d1f4f2ba9b66077ff9d4c4e1f37b6bd7975da5c40d94ab/hf_xet-1.5.1-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6abd35c3221eff63836618ddfb954dcf84798603f71d8e33e3ed7b04acfdbe6e", size = 4077794, upload-time = "2026-06-08T23:02:40.656Z" }, + { url = "https://files.pythonhosted.org/packages/35/94/4b2ecfbad8f8b04701a23aefb62f540b9137d058b7e1dbef16a32676f0e9/hf_xet-1.5.1-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:94e761bbd266bf4c03cee73753916062665ce8365aa40ed321f45afcb934b41e", size = 3845354, upload-time = "2026-06-08T23:02:42.702Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/f99f4bc7295023d7bd9ebbfd51f75cc530ca262c1227666268b8208f4b77/hf_xet-1.5.1-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:892e3a3a3aecc12aded8b93cf4f9cd059282c7de0732f7d55026f3abdf474350", size = 4514864, upload-time = "2026-06-08T23:02:44.497Z" }, + { url = "https://files.pythonhosted.org/packages/cd/6e/21f7e5a2381278bd3b7b7a5a4d90038518bb6308a0c1daf5d9f8268bb178/hf_xet-1.5.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a93df2039190502835b1db8cd7e178b0b7b889fe9ab51299d5ced26e0dd879a4", size = 4303784, upload-time = "2026-06-08T23:02:46.203Z" }, + { url = "https://files.pythonhosted.org/packages/35/0e/f992bb6927ac1cb30ef74e62268f551f338bc32b2191f7c96a44c6f7283e/hf_xet-1.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0c97106032ef70467b4f6bc2d0ccc266d7613ee076afc56516c502f87ce1c4a6", size = 4500703, upload-time = "2026-06-08T23:02:47.628Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d1/90a498d05447980b977b1669246eeeeae4cfb0ea3e7a286eaba627f91bf9/hf_xet-1.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6208adb15d192b90e4c2ad2a27ed864359b2cb0f2494eb6d7c7f3699ac02e2bf", size = 4719498, upload-time = "2026-06-08T23:02:49.268Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b6/20f99cfe97cc663a711f7b33cc21d4793e51968e9a26125b4afcd77315ba/hf_xet-1.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:f7b3002f95d1c13e24bcb4537baa8f0eb3838957067c91bb4959bc004a6435f5", size = 4026419, upload-time = "2026-06-08T23:02:50.829Z" }, + { url = "https://files.pythonhosted.org/packages/f9/fa/77453694888f03e5a8c8852d1514a0894d8e81c622d39edbaf308ea0dcf4/hf_xet-1.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:93d090b57b211133f6c0dab0205ef5cb6d89162979ba75a74845045cc3063b8e", size = 3855178, upload-time = "2026-06-08T23:02:52.452Z" }, +] + +[[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 = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/8f/999e4dda11c6187c78f090eac00895a47e11a0049308f07579bcb7aa3aa2/huggingface_hub-1.23.0.tar.gz", hash = "sha256:c04997fb8bbdace1e57b7703d30ed7678af51f70d00d241819ff411b92ae9a88", size = 919163, upload-time = "2026-07-09T14:49:32.315Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/ce/13b2ba57838b8db1e6bd033c1b21ce0b9f6153b87d4e4939f77074e41eb0/huggingface_hub-1.23.0-py3-none-any.whl", hash = "sha256:b1d604788f5adc7f0eb246e03e0ec19011ca06e38400218c347dccc3dffa64a2", size = 770336, upload-time = "2026-07-09T14:49:30.597Z" }, +] + +[[package]] +name = "hyperlink" +version = "21.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3a/51/1947bd81d75af87e3bb9e34593a4cf118115a8feb451ce7a69044ef1412e/hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", size = 140743, upload-time = "2021-01-08T05:51:20.972Z" } wheels = [ @@ -1013,45 +1648,54 @@ wheels = [ [[package]] name = "idna" -version = "3.10" +version = "3.18" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] [[package]] name = "importlib-metadata" -version = "8.7.0" +version = "8.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/72/c600ae4f68c28fc19f9c31b9403053e5dbb8cace2e6842c7b7c3e4d42fe9/importlib_metadata-8.9.0.tar.gz", hash = "sha256:58850626cef4bd2df100378b0f2aea9724a7b92f10770d547725b047078f99ee", size = 56140, upload-time = "2026-03-20T16:56:26.362Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f9/97f2ca8bb3ec6e4b1d64f983ebe98b9a192faddff67fac3d6303a537e670/importlib_metadata-8.9.0-py3-none-any.whl", hash = "sha256:e0f761b6ea91ced3b0844c14c9d955224d538105921f8e6754c00f6ca79fba7f", size = 27220, upload-time = "2026-03-20T16:56:25.07Z" }, ] [[package]] name = "incremental" -version = "24.7.2" +version = "24.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "setuptools" }, + { name = "packaging" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/27/87/156b374ff6578062965afe30cc57627d35234369b3336cf244b240c8d8e6/incremental-24.7.2.tar.gz", hash = "sha256:fb4f1d47ee60efe87d4f6f0ebb5f70b9760db2b2574c59c8e8912be4ebd464c9", size = 28157, upload-time = "2024-07-29T20:03:55.441Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/3c/82e84109e02c492f382c711c58a3dd91badda6d746def81a1465f74dc9f5/incremental-24.11.0.tar.gz", hash = "sha256:87d3480dbb083c1d736222511a8cf380012a8176c2456d01ef483242abbbcf8c", size = 24000, upload-time = "2025-11-28T02:30:17.861Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/38/221e5b2ae676a3938c2c1919131410c342b6efc2baffeda395dd66eeca8f/incremental-24.7.2-py3-none-any.whl", hash = "sha256:8cb2c3431530bec48ad70513931a760f446ad6c25e8333ca5d95e24b0ed7b8fe", size = 20516, upload-time = "2024-07-29T20:03:53.677Z" }, + { url = "https://files.pythonhosted.org/packages/1d/55/0f4df2a44053867ea9cbea73fc588b03c55605cd695cee0a3d86f0029cb2/incremental-24.11.0-py3-none-any.whl", hash = "sha256:a34450716b1c4341fe6676a0598e88a39e04189f4dce5dc96f656e040baa10b3", size = 21109, upload-time = "2025-11-28T02:30:16.442Z" }, ] [[package]] name = "iniconfig" -version = "2.1.0" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.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" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, ] [[package]] @@ -1068,26 +1712,26 @@ wheels = [ [[package]] name = "jaraco-context" -version = "6.0.1" +version = "6.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912, upload-time = "2024-08-20T03:39:27.358Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3", size = 16801, upload-time = "2026-03-20T22:13:33.922Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825, upload-time = "2024-08-20T03:39:25.966Z" }, + { url = "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535", size = 7871, upload-time = "2026-03-20T22:13:32.808Z" }, ] [[package]] name = "jaraco-functools" -version = "4.3.0" +version = "4.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "more-itertools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f7/ed/1aa2d585304ec07262e1a83a9889880701079dde796ac7b1d1826f40c63d/jaraco_functools-4.3.0.tar.gz", hash = "sha256:cfd13ad0dd2c47a3600b439ef72d8615d482cedcff1632930d6f28924d92f294", size = 19755, upload-time = "2025-08-18T20:05:09.91Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/1f/c23395957d41ccf27c4e535c3d334c4051e5395b3752057ba4cbaec35c56/jaraco_functools-4.6.0.tar.gz", hash = "sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280", size = 20837, upload-time = "2026-07-14T01:28:02.544Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/09/726f168acad366b11e420df31bf1c702a54d373a83f968d94141a8c3fde0/jaraco_functools-4.3.0-py3-none-any.whl", hash = "sha256:227ff8ed6f7b8f62c56deff101545fa7543cf2c8e7b82a7c2116e672f29c26e8", size = 10408, upload-time = "2025-08-18T20:05:08.69Z" }, + { url = "https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl", hash = "sha256:99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30", size = 11677, upload-time = "2026-07-14T01:28:01.59Z" }, ] [[package]] @@ -1113,118 +1757,200 @@ wheels = [ [[package]] name = "jiter" -version = "0.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b51faf4deb36cb48a4fbbd7cb36bad6a5fca4741306f7/jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500", size = 162759, upload-time = "2025-05-18T19:04:59.73Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/be/7e/4011b5c77bec97cb2b572f566220364e3e21b51c48c5bd9c4a9c26b41b67/jiter-0.10.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cd2fb72b02478f06a900a5782de2ef47e0396b3e1f7d5aba30daeb1fce66f303", size = 317215, upload-time = "2025-05-18T19:03:04.303Z" }, - { url = "https://files.pythonhosted.org/packages/8a/4f/144c1b57c39692efc7ea7d8e247acf28e47d0912800b34d0ad815f6b2824/jiter-0.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:32bb468e3af278f095d3fa5b90314728a6916d89ba3d0ffb726dd9bf7367285e", size = 322814, upload-time = "2025-05-18T19:03:06.433Z" }, - { url = "https://files.pythonhosted.org/packages/63/1f/db977336d332a9406c0b1f0b82be6f71f72526a806cbb2281baf201d38e3/jiter-0.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8b3e0068c26ddedc7abc6fac37da2d0af16b921e288a5a613f4b86f050354f", size = 345237, upload-time = "2025-05-18T19:03:07.833Z" }, - { url = "https://files.pythonhosted.org/packages/d7/1c/aa30a4a775e8a672ad7f21532bdbfb269f0706b39c6ff14e1f86bdd9e5ff/jiter-0.10.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:286299b74cc49e25cd42eea19b72aa82c515d2f2ee12d11392c56d8701f52224", size = 370999, upload-time = "2025-05-18T19:03:09.338Z" }, - { url = "https://files.pythonhosted.org/packages/35/df/f8257abc4207830cb18880781b5f5b716bad5b2a22fb4330cfd357407c5b/jiter-0.10.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ed5649ceeaeffc28d87fb012d25a4cd356dcd53eff5acff1f0466b831dda2a7", size = 491109, upload-time = "2025-05-18T19:03:11.13Z" }, - { url = "https://files.pythonhosted.org/packages/06/76/9e1516fd7b4278aa13a2cc7f159e56befbea9aa65c71586305e7afa8b0b3/jiter-0.10.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2ab0051160cb758a70716448908ef14ad476c3774bd03ddce075f3c1f90a3d6", size = 388608, upload-time = "2025-05-18T19:03:12.911Z" }, - { url = "https://files.pythonhosted.org/packages/6d/64/67750672b4354ca20ca18d3d1ccf2c62a072e8a2d452ac3cf8ced73571ef/jiter-0.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03997d2f37f6b67d2f5c475da4412be584e1cec273c1cfc03d642c46db43f8cf", size = 352454, upload-time = "2025-05-18T19:03:14.741Z" }, - { url = "https://files.pythonhosted.org/packages/96/4d/5c4e36d48f169a54b53a305114be3efa2bbffd33b648cd1478a688f639c1/jiter-0.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c404a99352d839fed80d6afd6c1d66071f3bacaaa5c4268983fc10f769112e90", size = 391833, upload-time = "2025-05-18T19:03:16.426Z" }, - { url = "https://files.pythonhosted.org/packages/0b/de/ce4a6166a78810bd83763d2fa13f85f73cbd3743a325469a4a9289af6dae/jiter-0.10.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66e989410b6666d3ddb27a74c7e50d0829704ede652fd4c858e91f8d64b403d0", size = 523646, upload-time = "2025-05-18T19:03:17.704Z" }, - { url = "https://files.pythonhosted.org/packages/a2/a6/3bc9acce53466972964cf4ad85efecb94f9244539ab6da1107f7aed82934/jiter-0.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b532d3af9ef4f6374609a3bcb5e05a1951d3bf6190dc6b176fdb277c9bbf15ee", size = 514735, upload-time = "2025-05-18T19:03:19.44Z" }, - { url = "https://files.pythonhosted.org/packages/b4/d8/243c2ab8426a2a4dea85ba2a2ba43df379ccece2145320dfd4799b9633c5/jiter-0.10.0-cp310-cp310-win32.whl", hash = "sha256:da9be20b333970e28b72edc4dff63d4fec3398e05770fb3205f7fb460eb48dd4", size = 210747, upload-time = "2025-05-18T19:03:21.184Z" }, - { url = "https://files.pythonhosted.org/packages/37/7a/8021bd615ef7788b98fc76ff533eaac846322c170e93cbffa01979197a45/jiter-0.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:f59e533afed0c5b0ac3eba20d2548c4a550336d8282ee69eb07b37ea526ee4e5", size = 207484, upload-time = "2025-05-18T19:03:23.046Z" }, - { url = "https://files.pythonhosted.org/packages/1b/dd/6cefc6bd68b1c3c979cecfa7029ab582b57690a31cd2f346c4d0ce7951b6/jiter-0.10.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3bebe0c558e19902c96e99217e0b8e8b17d570906e72ed8a87170bc290b1e978", size = 317473, upload-time = "2025-05-18T19:03:25.942Z" }, - { url = "https://files.pythonhosted.org/packages/be/cf/fc33f5159ce132be1d8dd57251a1ec7a631c7df4bd11e1cd198308c6ae32/jiter-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:558cc7e44fd8e507a236bee6a02fa17199ba752874400a0ca6cd6e2196cdb7dc", size = 321971, upload-time = "2025-05-18T19:03:27.255Z" }, - { url = "https://files.pythonhosted.org/packages/68/a4/da3f150cf1d51f6c472616fb7650429c7ce053e0c962b41b68557fdf6379/jiter-0.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d613e4b379a07d7c8453c5712ce7014e86c6ac93d990a0b8e7377e18505e98d", size = 345574, upload-time = "2025-05-18T19:03:28.63Z" }, - { url = "https://files.pythonhosted.org/packages/84/34/6e8d412e60ff06b186040e77da5f83bc158e9735759fcae65b37d681f28b/jiter-0.10.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f62cf8ba0618eda841b9bf61797f21c5ebd15a7a1e19daab76e4e4b498d515b2", size = 371028, upload-time = "2025-05-18T19:03:30.292Z" }, - { url = "https://files.pythonhosted.org/packages/fb/d9/9ee86173aae4576c35a2f50ae930d2ccb4c4c236f6cb9353267aa1d626b7/jiter-0.10.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:919d139cdfa8ae8945112398511cb7fca58a77382617d279556b344867a37e61", size = 491083, upload-time = "2025-05-18T19:03:31.654Z" }, - { url = "https://files.pythonhosted.org/packages/d9/2c/f955de55e74771493ac9e188b0f731524c6a995dffdcb8c255b89c6fb74b/jiter-0.10.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13ddbc6ae311175a3b03bd8994881bc4635c923754932918e18da841632349db", size = 388821, upload-time = "2025-05-18T19:03:33.184Z" }, - { url = "https://files.pythonhosted.org/packages/81/5a/0e73541b6edd3f4aada586c24e50626c7815c561a7ba337d6a7eb0a915b4/jiter-0.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c440ea003ad10927a30521a9062ce10b5479592e8a70da27f21eeb457b4a9c5", size = 352174, upload-time = "2025-05-18T19:03:34.965Z" }, - { url = "https://files.pythonhosted.org/packages/1c/c0/61eeec33b8c75b31cae42be14d44f9e6fe3ac15a4e58010256ac3abf3638/jiter-0.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc347c87944983481e138dea467c0551080c86b9d21de6ea9306efb12ca8f606", size = 391869, upload-time = "2025-05-18T19:03:36.436Z" }, - { url = "https://files.pythonhosted.org/packages/41/22/5beb5ee4ad4ef7d86f5ea5b4509f680a20706c4a7659e74344777efb7739/jiter-0.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:13252b58c1f4d8c5b63ab103c03d909e8e1e7842d302473f482915d95fefd605", size = 523741, upload-time = "2025-05-18T19:03:38.168Z" }, - { url = "https://files.pythonhosted.org/packages/ea/10/768e8818538e5817c637b0df52e54366ec4cebc3346108a4457ea7a98f32/jiter-0.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7d1bbf3c465de4a24ab12fb7766a0003f6f9bce48b8b6a886158c4d569452dc5", size = 514527, upload-time = "2025-05-18T19:03:39.577Z" }, - { url = "https://files.pythonhosted.org/packages/73/6d/29b7c2dc76ce93cbedabfd842fc9096d01a0550c52692dfc33d3cc889815/jiter-0.10.0-cp311-cp311-win32.whl", hash = "sha256:db16e4848b7e826edca4ccdd5b145939758dadf0dc06e7007ad0e9cfb5928ae7", size = 210765, upload-time = "2025-05-18T19:03:41.271Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c9/d394706deb4c660137caf13e33d05a031d734eb99c051142e039d8ceb794/jiter-0.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c9c1d5f10e18909e993f9641f12fe1c77b3e9b533ee94ffa970acc14ded3812", size = 209234, upload-time = "2025-05-18T19:03:42.918Z" }, - { url = "https://files.pythonhosted.org/packages/6d/b5/348b3313c58f5fbfb2194eb4d07e46a35748ba6e5b3b3046143f3040bafa/jiter-0.10.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1e274728e4a5345a6dde2d343c8da018b9d4bd4350f5a472fa91f66fda44911b", size = 312262, upload-time = "2025-05-18T19:03:44.637Z" }, - { url = "https://files.pythonhosted.org/packages/9c/4a/6a2397096162b21645162825f058d1709a02965606e537e3304b02742e9b/jiter-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7202ae396446c988cb2a5feb33a543ab2165b786ac97f53b59aafb803fef0744", size = 320124, upload-time = "2025-05-18T19:03:46.341Z" }, - { url = "https://files.pythonhosted.org/packages/2a/85/1ce02cade7516b726dd88f59a4ee46914bf79d1676d1228ef2002ed2f1c9/jiter-0.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23ba7722d6748b6920ed02a8f1726fb4b33e0fd2f3f621816a8b486c66410ab2", size = 345330, upload-time = "2025-05-18T19:03:47.596Z" }, - { url = "https://files.pythonhosted.org/packages/75/d0/bb6b4f209a77190ce10ea8d7e50bf3725fc16d3372d0a9f11985a2b23eff/jiter-0.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:371eab43c0a288537d30e1f0b193bc4eca90439fc08a022dd83e5e07500ed026", size = 369670, upload-time = "2025-05-18T19:03:49.334Z" }, - { url = "https://files.pythonhosted.org/packages/a0/f5/a61787da9b8847a601e6827fbc42ecb12be2c925ced3252c8ffcb56afcaf/jiter-0.10.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c675736059020365cebc845a820214765162728b51ab1e03a1b7b3abb70f74c", size = 489057, upload-time = "2025-05-18T19:03:50.66Z" }, - { url = "https://files.pythonhosted.org/packages/12/e4/6f906272810a7b21406c760a53aadbe52e99ee070fc5c0cb191e316de30b/jiter-0.10.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c5867d40ab716e4684858e4887489685968a47e3ba222e44cde6e4a2154f959", size = 389372, upload-time = "2025-05-18T19:03:51.98Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ba/77013b0b8ba904bf3762f11e0129b8928bff7f978a81838dfcc958ad5728/jiter-0.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395bb9a26111b60141757d874d27fdea01b17e8fac958b91c20128ba8f4acc8a", size = 352038, upload-time = "2025-05-18T19:03:53.703Z" }, - { url = "https://files.pythonhosted.org/packages/67/27/c62568e3ccb03368dbcc44a1ef3a423cb86778a4389e995125d3d1aaa0a4/jiter-0.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6842184aed5cdb07e0c7e20e5bdcfafe33515ee1741a6835353bb45fe5d1bd95", size = 391538, upload-time = "2025-05-18T19:03:55.046Z" }, - { url = "https://files.pythonhosted.org/packages/c0/72/0d6b7e31fc17a8fdce76164884edef0698ba556b8eb0af9546ae1a06b91d/jiter-0.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:62755d1bcea9876770d4df713d82606c8c1a3dca88ff39046b85a048566d56ea", size = 523557, upload-time = "2025-05-18T19:03:56.386Z" }, - { url = "https://files.pythonhosted.org/packages/2f/09/bc1661fbbcbeb6244bd2904ff3a06f340aa77a2b94e5a7373fd165960ea3/jiter-0.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:533efbce2cacec78d5ba73a41756beff8431dfa1694b6346ce7af3a12c42202b", size = 514202, upload-time = "2025-05-18T19:03:57.675Z" }, - { url = "https://files.pythonhosted.org/packages/1b/84/5a5d5400e9d4d54b8004c9673bbe4403928a00d28529ff35b19e9d176b19/jiter-0.10.0-cp312-cp312-win32.whl", hash = "sha256:8be921f0cadd245e981b964dfbcd6fd4bc4e254cdc069490416dd7a2632ecc01", size = 211781, upload-time = "2025-05-18T19:03:59.025Z" }, - { url = "https://files.pythonhosted.org/packages/9b/52/7ec47455e26f2d6e5f2ea4951a0652c06e5b995c291f723973ae9e724a65/jiter-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7c7d785ae9dda68c2678532a5a1581347e9c15362ae9f6e68f3fdbfb64f2e49", size = 206176, upload-time = "2025-05-18T19:04:00.305Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b0/279597e7a270e8d22623fea6c5d4eeac328e7d95c236ed51a2b884c54f70/jiter-0.10.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0588107ec8e11b6f5ef0e0d656fb2803ac6cf94a96b2b9fc675c0e3ab5e8644", size = 311617, upload-time = "2025-05-18T19:04:02.078Z" }, - { url = "https://files.pythonhosted.org/packages/91/e3/0916334936f356d605f54cc164af4060e3e7094364add445a3bc79335d46/jiter-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cafc4628b616dc32530c20ee53d71589816cf385dd9449633e910d596b1f5c8a", size = 318947, upload-time = "2025-05-18T19:04:03.347Z" }, - { url = "https://files.pythonhosted.org/packages/6a/8e/fd94e8c02d0e94539b7d669a7ebbd2776e51f329bb2c84d4385e8063a2ad/jiter-0.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:520ef6d981172693786a49ff5b09eda72a42e539f14788124a07530f785c3ad6", size = 344618, upload-time = "2025-05-18T19:04:04.709Z" }, - { url = "https://files.pythonhosted.org/packages/6f/b0/f9f0a2ec42c6e9c2e61c327824687f1e2415b767e1089c1d9135f43816bd/jiter-0.10.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:554dedfd05937f8fc45d17ebdf298fe7e0c77458232bcb73d9fbbf4c6455f5b3", size = 368829, upload-time = "2025-05-18T19:04:06.912Z" }, - { url = "https://files.pythonhosted.org/packages/e8/57/5bbcd5331910595ad53b9fd0c610392ac68692176f05ae48d6ce5c852967/jiter-0.10.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bc299da7789deacf95f64052d97f75c16d4fc8c4c214a22bf8d859a4288a1c2", size = 491034, upload-time = "2025-05-18T19:04:08.222Z" }, - { url = "https://files.pythonhosted.org/packages/9b/be/c393df00e6e6e9e623a73551774449f2f23b6ec6a502a3297aeeece2c65a/jiter-0.10.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5161e201172de298a8a1baad95eb85db4fb90e902353b1f6a41d64ea64644e25", size = 388529, upload-time = "2025-05-18T19:04:09.566Z" }, - { url = "https://files.pythonhosted.org/packages/42/3e/df2235c54d365434c7f150b986a6e35f41ebdc2f95acea3036d99613025d/jiter-0.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e2227db6ba93cb3e2bf67c87e594adde0609f146344e8207e8730364db27041", size = 350671, upload-time = "2025-05-18T19:04:10.98Z" }, - { url = "https://files.pythonhosted.org/packages/c6/77/71b0b24cbcc28f55ab4dbfe029f9a5b73aeadaba677843fc6dc9ed2b1d0a/jiter-0.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15acb267ea5e2c64515574b06a8bf393fbfee6a50eb1673614aa45f4613c0cca", size = 390864, upload-time = "2025-05-18T19:04:12.722Z" }, - { url = "https://files.pythonhosted.org/packages/6a/d3/ef774b6969b9b6178e1d1e7a89a3bd37d241f3d3ec5f8deb37bbd203714a/jiter-0.10.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:901b92f2e2947dc6dfcb52fd624453862e16665ea909a08398dde19c0731b7f4", size = 522989, upload-time = "2025-05-18T19:04:14.261Z" }, - { url = "https://files.pythonhosted.org/packages/0c/41/9becdb1d8dd5d854142f45a9d71949ed7e87a8e312b0bede2de849388cb9/jiter-0.10.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d0cb9a125d5a3ec971a094a845eadde2db0de85b33c9f13eb94a0c63d463879e", size = 513495, upload-time = "2025-05-18T19:04:15.603Z" }, - { url = "https://files.pythonhosted.org/packages/9c/36/3468e5a18238bdedae7c4d19461265b5e9b8e288d3f86cd89d00cbb48686/jiter-0.10.0-cp313-cp313-win32.whl", hash = "sha256:48a403277ad1ee208fb930bdf91745e4d2d6e47253eedc96e2559d1e6527006d", size = 211289, upload-time = "2025-05-18T19:04:17.541Z" }, - { url = "https://files.pythonhosted.org/packages/7e/07/1c96b623128bcb913706e294adb5f768fb7baf8db5e1338ce7b4ee8c78ef/jiter-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:75f9eb72ecb640619c29bf714e78c9c46c9c4eaafd644bf78577ede459f330d4", size = 205074, upload-time = "2025-05-18T19:04:19.21Z" }, - { url = "https://files.pythonhosted.org/packages/54/46/caa2c1342655f57d8f0f2519774c6d67132205909c65e9aa8255e1d7b4f4/jiter-0.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:28ed2a4c05a1f32ef0e1d24c2611330219fed727dae01789f4a335617634b1ca", size = 318225, upload-time = "2025-05-18T19:04:20.583Z" }, - { url = "https://files.pythonhosted.org/packages/43/84/c7d44c75767e18946219ba2d703a5a32ab37b0bc21886a97bc6062e4da42/jiter-0.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a4c418b1ec86a195f1ca69da8b23e8926c752b685af665ce30777233dfe070", size = 350235, upload-time = "2025-05-18T19:04:22.363Z" }, - { url = "https://files.pythonhosted.org/packages/01/16/f5a0135ccd968b480daad0e6ab34b0c7c5ba3bc447e5088152696140dcb3/jiter-0.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d7bfed2fe1fe0e4dda6ef682cee888ba444b21e7a6553e03252e4feb6cf0adca", size = 207278, upload-time = "2025-05-18T19:04:23.627Z" }, - { url = "https://files.pythonhosted.org/packages/1c/9b/1d646da42c3de6c2188fdaa15bce8ecb22b635904fc68be025e21249ba44/jiter-0.10.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:5e9251a5e83fab8d87799d3e1a46cb4b7f2919b895c6f4483629ed2446f66522", size = 310866, upload-time = "2025-05-18T19:04:24.891Z" }, - { url = "https://files.pythonhosted.org/packages/ad/0e/26538b158e8a7c7987e94e7aeb2999e2e82b1f9d2e1f6e9874ddf71ebda0/jiter-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:023aa0204126fe5b87ccbcd75c8a0d0261b9abdbbf46d55e7ae9f8e22424eeb8", size = 318772, upload-time = "2025-05-18T19:04:26.161Z" }, - { url = "https://files.pythonhosted.org/packages/7b/fb/d302893151caa1c2636d6574d213e4b34e31fd077af6050a9c5cbb42f6fb/jiter-0.10.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c189c4f1779c05f75fc17c0c1267594ed918996a231593a21a5ca5438445216", size = 344534, upload-time = "2025-05-18T19:04:27.495Z" }, - { url = "https://files.pythonhosted.org/packages/01/d8/5780b64a149d74e347c5128d82176eb1e3241b1391ac07935693466d6219/jiter-0.10.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15720084d90d1098ca0229352607cd68256c76991f6b374af96f36920eae13c4", size = 369087, upload-time = "2025-05-18T19:04:28.896Z" }, - { url = "https://files.pythonhosted.org/packages/e8/5b/f235a1437445160e777544f3ade57544daf96ba7e96c1a5b24a6f7ac7004/jiter-0.10.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4f2fb68e5f1cfee30e2b2a09549a00683e0fde4c6a2ab88c94072fc33cb7426", size = 490694, upload-time = "2025-05-18T19:04:30.183Z" }, - { url = "https://files.pythonhosted.org/packages/85/a9/9c3d4617caa2ff89cf61b41e83820c27ebb3f7b5fae8a72901e8cd6ff9be/jiter-0.10.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce541693355fc6da424c08b7edf39a2895f58d6ea17d92cc2b168d20907dee12", size = 388992, upload-time = "2025-05-18T19:04:32.028Z" }, - { url = "https://files.pythonhosted.org/packages/68/b1/344fd14049ba5c94526540af7eb661871f9c54d5f5601ff41a959b9a0bbd/jiter-0.10.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31c50c40272e189d50006ad5c73883caabb73d4e9748a688b216e85a9a9ca3b9", size = 351723, upload-time = "2025-05-18T19:04:33.467Z" }, - { url = "https://files.pythonhosted.org/packages/41/89/4c0e345041186f82a31aee7b9d4219a910df672b9fef26f129f0cda07a29/jiter-0.10.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fa3402a2ff9815960e0372a47b75c76979d74402448509ccd49a275fa983ef8a", size = 392215, upload-time = "2025-05-18T19:04:34.827Z" }, - { url = "https://files.pythonhosted.org/packages/55/58/ee607863e18d3f895feb802154a2177d7e823a7103f000df182e0f718b38/jiter-0.10.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:1956f934dca32d7bb647ea21d06d93ca40868b505c228556d3373cbd255ce853", size = 522762, upload-time = "2025-05-18T19:04:36.19Z" }, - { url = "https://files.pythonhosted.org/packages/15/d0/9123fb41825490d16929e73c212de9a42913d68324a8ce3c8476cae7ac9d/jiter-0.10.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:fcedb049bdfc555e261d6f65a6abe1d5ad68825b7202ccb9692636c70fcced86", size = 513427, upload-time = "2025-05-18T19:04:37.544Z" }, - { url = "https://files.pythonhosted.org/packages/d8/b3/2bd02071c5a2430d0b70403a34411fc519c2f227da7b03da9ba6a956f931/jiter-0.10.0-cp314-cp314-win32.whl", hash = "sha256:ac509f7eccca54b2a29daeb516fb95b6f0bd0d0d8084efaf8ed5dfc7b9f0b357", size = 210127, upload-time = "2025-05-18T19:04:38.837Z" }, - { url = "https://files.pythonhosted.org/packages/03/0c/5fe86614ea050c3ecd728ab4035534387cd41e7c1855ef6c031f1ca93e3f/jiter-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5ed975b83a2b8639356151cef5c0d597c68376fc4922b45d0eb384ac058cfa00", size = 318527, upload-time = "2025-05-18T19:04:40.612Z" }, - { url = "https://files.pythonhosted.org/packages/b3/4a/4175a563579e884192ba6e81725fc0448b042024419be8d83aa8a80a3f44/jiter-0.10.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa96f2abba33dc77f79b4cf791840230375f9534e5fac927ccceb58c5e604a5", size = 354213, upload-time = "2025-05-18T19:04:41.894Z" }, - { url = "https://files.pythonhosted.org/packages/98/fd/aced428e2bd3c6c1132f67c5a708f9e7fd161d0ca8f8c5862b17b93cdf0a/jiter-0.10.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bd6292a43c0fc09ce7c154ec0fa646a536b877d1e8f2f96c19707f65355b5a4d", size = 317665, upload-time = "2025-05-18T19:04:43.417Z" }, - { url = "https://files.pythonhosted.org/packages/b6/2e/47d42f15d53ed382aef8212a737101ae2720e3697a954f9b95af06d34e89/jiter-0.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:39de429dcaeb6808d75ffe9effefe96a4903c6a4b376b2f6d08d77c1aaee2f18", size = 312152, upload-time = "2025-05-18T19:04:44.797Z" }, - { url = "https://files.pythonhosted.org/packages/7b/02/aae834228ef4834fc18718724017995ace8da5f70aa1ec225b9bc2b2d7aa/jiter-0.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52ce124f13a7a616fad3bb723f2bfb537d78239d1f7f219566dc52b6f2a9e48d", size = 346708, upload-time = "2025-05-18T19:04:46.127Z" }, - { url = "https://files.pythonhosted.org/packages/35/d4/6ff39dee2d0a9abd69d8a3832ce48a3aa644eed75e8515b5ff86c526ca9a/jiter-0.10.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:166f3606f11920f9a1746b2eea84fa2c0a5d50fd313c38bdea4edc072000b0af", size = 371360, upload-time = "2025-05-18T19:04:47.448Z" }, - { url = "https://files.pythonhosted.org/packages/a9/67/c749d962b4eb62445867ae4e64a543cbb5d63cc7d78ada274ac515500a7f/jiter-0.10.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:28dcecbb4ba402916034fc14eba7709f250c4d24b0c43fc94d187ee0580af181", size = 492105, upload-time = "2025-05-18T19:04:48.792Z" }, - { url = "https://files.pythonhosted.org/packages/f6/d3/8fe1b1bae5161f27b1891c256668f598fa4c30c0a7dacd668046a6215fca/jiter-0.10.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86c5aa6910f9bebcc7bc4f8bc461aff68504388b43bfe5e5c0bd21efa33b52f4", size = 389577, upload-time = "2025-05-18T19:04:50.13Z" }, - { url = "https://files.pythonhosted.org/packages/ef/28/ecb19d789b4777898a4252bfaac35e3f8caf16c93becd58dcbaac0dc24ad/jiter-0.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ceeb52d242b315d7f1f74b441b6a167f78cea801ad7c11c36da77ff2d42e8a28", size = 353849, upload-time = "2025-05-18T19:04:51.443Z" }, - { url = "https://files.pythonhosted.org/packages/77/69/261f798f84790da6482ebd8c87ec976192b8c846e79444d0a2e0d33ebed8/jiter-0.10.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ff76d8887c8c8ee1e772274fcf8cc1071c2c58590d13e33bd12d02dc9a560397", size = 392029, upload-time = "2025-05-18T19:04:52.792Z" }, - { url = "https://files.pythonhosted.org/packages/cb/08/b8d15140d4d91f16faa2f5d416c1a71ab1bbe2b66c57197b692d04c0335f/jiter-0.10.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a9be4d0fa2b79f7222a88aa488bd89e2ae0a0a5b189462a12def6ece2faa45f1", size = 524386, upload-time = "2025-05-18T19:04:54.203Z" }, - { url = "https://files.pythonhosted.org/packages/9b/1d/23c41765cc95c0e23ac492a88450d34bf0fd87a37218d1b97000bffe0f53/jiter-0.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9ab7fd8738094139b6c1ab1822d6f2000ebe41515c537235fd45dabe13ec9324", size = 515234, upload-time = "2025-05-18T19:04:55.838Z" }, - { url = "https://files.pythonhosted.org/packages/9f/14/381d8b151132e79790579819c3775be32820569f23806769658535fe467f/jiter-0.10.0-cp39-cp39-win32.whl", hash = "sha256:5f51e048540dd27f204ff4a87f5d79294ea0aa3aa552aca34934588cf27023cf", size = 211436, upload-time = "2025-05-18T19:04:57.183Z" }, - { url = "https://files.pythonhosted.org/packages/59/66/f23ae51dea8ee8ce429027b60008ca895d0fa0704f0c7fe5f09014a6cffb/jiter-0.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:1b28302349dc65703a9e4ead16f163b1c339efffbe1049c30a44b001a2a4fff9", size = 208777, upload-time = "2025-05-18T19:04:58.454Z" }, +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/1f/10936e16d8860c70698a1aa939a46aa0224813b782bce4e000e637da0b2d/jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c", size = 176431, upload-time = "2026-06-29T13:05:13.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/d8/b959609e44012a42b1f3e5ba98ea3b33c7e41e6d4b77cd8f00fd19b1d3ad/jiter-0.16.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c5fc4f8def331036a7b8e981b4347ebe409981edbc8308a5ea842b8c3614fa6c", size = 310082, upload-time = "2026-06-29T13:02:31.356Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3d/4d7f5667ea0e0548534ba880b84bb3d12924fd133aa83ad6c6c80fca3d76/jiter-0.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5a71d0d2014c3275043e1170bf3d4e771493cb0dcf07be54c567155f4d8ee64b", size = 315643, upload-time = "2026-06-29T13:02:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/9b/83/bed2dcb5c9f3e1ccfcbc67dda48265fe7d5ad0c9cadda5fe95f6e3b87f94/jiter-0.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:741eed508c233a76313a1c7b001f8f21b82f14327e9196ae8bd29a2cc164ae84", size = 341363, upload-time = "2026-06-29T13:02:34.853Z" }, + { url = "https://files.pythonhosted.org/packages/f4/2f/6bb3c3dda668ebc0445689c81a2b0f26a82b10843d67ed9c9b2c3edc177f/jiter-0.16.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fb7bc819187b56dc48aa5c833aaf92257da8e07efdb9306156667bd2eeb491c", size = 365483, upload-time = "2026-06-29T13:02:36.295Z" }, + { url = "https://files.pythonhosted.org/packages/92/35/8a045ccb39164e70dcdae696413b661771f148b68b12b175c3a04d901937/jiter-0.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c9610fd25ebccb43fca584136f5c2fbb26802447eccd430dfdbab95a0fd5126", size = 461219, upload-time = "2026-06-29T13:02:38.116Z" }, + { url = "https://files.pythonhosted.org/packages/e7/99/22292dbbf0ed0c610cfe5ddc7f3bd67237a412f121318f865196e62a07bd/jiter-0.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4a1d68ff7ca1d3b5dee20a97a3decda7d5f15003823bf6d140c81f8561d3bc5c", size = 374905, upload-time = "2026-06-29T13:02:40.357Z" }, + { url = "https://files.pythonhosted.org/packages/29/ac/2f55ccb1f0eeafa6d89d24caf52f6f0944a59290ee199e9ade62177dca42/jiter-0.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb08c276dd02dac3a284acdd02cacc630d2e3cd6572a4b85519f35cbd133c3de", size = 348320, upload-time = "2026-06-29T13:02:41.923Z" }, + { url = "https://files.pythonhosted.org/packages/50/e3/7d88b9174c40064fabc07c84a9b62e6b10f5644562ec0e0a29392edbe978/jiter-0.16.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:8fc4d94713c4697347e38faf7d6ef91547c142219bdcfc7220c4870879974244", size = 356519, upload-time = "2026-06-29T13:02:43.436Z" }, + { url = "https://files.pythonhosted.org/packages/27/57/c4a33aeef513a9d5e26e31534e0bcc752d6ea0e54c94ddb7b68bade669c2/jiter-0.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a0f05e229edb29e68cdd0ccb83cea13b64263416120cf943767a6fd72e6787f", size = 394204, upload-time = "2026-06-29T13:02:44.987Z" }, + { url = "https://files.pythonhosted.org/packages/9d/70/c6c23e76ebb3766b111bc399437bbc9f870a76e2a92e10b2a5f561d57372/jiter-0.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2c842cbf374a8daf50b2c04212995bee34ca2ac2cdc29a901b4cdb072c9c4131", size = 521477, upload-time = "2026-06-29T13:02:46.724Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d3/0001c8c0c5976af2625bb1cfb1895e8ec693b6589fe4574b8e6fc2c85501/jiter-0.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5ed466aee31294d7cdcd4d37dfe5c42c97bc29d9a5f00eacf24504358309cb9b", size = 552187, upload-time = "2026-06-29T13:02:48.144Z" }, + { url = "https://files.pythonhosted.org/packages/f6/76/311b718e07e85740e48619c0632b36f7e0b8d113984499e436452ed13a9a/jiter-0.16.0-cp310-cp310-win32.whl", hash = "sha256:b42e9ff5376819c053da25809a8d4b6fa6e473b4856ebe42e298ac958be3d7f9", size = 206513, upload-time = "2026-06-29T13:02:49.515Z" }, + { url = "https://files.pythonhosted.org/packages/db/7f/ac680eeb0777dc0eb7dc824800ba27880d7f6bc712e362d34ad8ee559f36/jiter-0.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:10438939205546132189c8e74a2d536a707841f3a25cd7c74ee91fe503407a26", size = 199505, upload-time = "2026-06-29T13:02:50.829Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3f/fae6cc967d120ec89e31c5418a51176d8278b3087fbb384a9176754f353c/jiter-0.16.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:67fddeda1688f0cce2d2ae83ccf8a80f79936f2d2997d6cc2261f82fdb54a4d3", size = 309289, upload-time = "2026-06-29T13:02:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/97c6c3562c077f6247d6e6ce5c82562500b6316c0d928e97e106b7a1321a/jiter-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c90c0f63df322be920eda6ce622e3083d8906ba267f8220fe7873213b8b4430e", size = 315181, upload-time = "2026-06-29T13:02:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/7b/89/d8d073f8aa2667e46c6c0873f86fe4a512bba4293cc730f626a076211a62/jiter-0.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64c0203212098470032aabcde9356fc168f377aade3e43def61dfe17e92f2037", size = 340939, upload-time = "2026-06-29T13:02:55.412Z" }, + { url = "https://files.pythonhosted.org/packages/87/c9/db4fda3ed73fb864139305e935e5b8b38a5a24692a5a9dd356c22f1b9c8d/jiter-0.16.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12288303c9844e61e1651d02a9a6f6633e47d39f897d6991d1427161ce6b746e", size = 364932, upload-time = "2026-06-29T13:02:57.28Z" }, + { url = "https://files.pythonhosted.org/packages/a2/74/52b5e86241057f52ddd7c9a580f90effb51f9d06239f6fc612279b91a838/jiter-0.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cf109d010b4b05a105afb3d43be36a21322d345ad3111e13d15f680afef0e5b", size = 461132, upload-time = "2026-06-29T13:02:58.994Z" }, + { url = "https://files.pythonhosted.org/packages/a9/87/544a700f7447c1f31c5d7833821a4daa5683165c2d5a094fbf5b5800c3dc/jiter-0.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62c1b7fe1f77925acf5af68b6140b8810fa87dfd4dc0a9c8568ec2fa2a10429c", size = 374857, upload-time = "2026-06-29T13:03:00.455Z" }, + { url = "https://files.pythonhosted.org/packages/40/cd/0fcc3f7d39183674d5bfa9ec640faaeb506c60be7c8f94625dfba366e37c/jiter-0.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8597d23c87f59294f83bcb6229b9ed1fccee13dbba967b46930d2f1759466fee", size = 347053, upload-time = "2026-06-29T13:03:02.045Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ae/c7e64e7932ad597fa395b61440b249ada6366716e25c6e08dd2afbd021e6/jiter-0.16.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3126a5dbad56401989ac769aca0cb56005bfb3e2366eea0ca99d1a91c3c1ee03", size = 356153, upload-time = "2026-06-29T13:03:03.706Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/1c719044f14da814e1a060191ab19b96f3e99207bc5b4bfc6d6be34b3f80/jiter-0.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c4b4717bdb35ae456f831a6b08d01880fff399887a6bbc526a583a406e484eea", size = 393956, upload-time = "2026-06-29T13:03:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/3b/dc/7b2f303a2847207e265503853a2d964a55354cffd62a5f2936c155486798/jiter-0.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:adff21bc78edfe086c15eb495b900306076de378dc2337c132401fc39bd79c91", size = 521081, upload-time = "2026-06-29T13:03:06.886Z" }, + { url = "https://files.pythonhosted.org/packages/c2/5f/501cf6e1e09caeb420195179ffc6f62aca603f1220ec53fd80d0d70b3e56/jiter-0.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dab907db06fc593645e73109acf4581ba5b548897d28b9348dc41ddc8343b2d3", size = 552085, upload-time = "2026-06-29T13:03:08.339Z" }, + { url = "https://files.pythonhosted.org/packages/79/54/aa5be86520113b79455c3877f3d1f07a348098df4083ba3688e9537e52dd/jiter-0.16.0-cp311-cp311-win32.whl", hash = "sha256:560b2cf3fb03240cd34f27409a238547488708f05b7c3924f571a60422251ec7", size = 206755, upload-time = "2026-06-29T13:03:09.653Z" }, + { url = "https://files.pythonhosted.org/packages/64/ec/2feb893eb330bd69b413866f4d5daada33c3962f1c6f270c91ca2d87fdf9/jiter-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:e431cfc9caf44c1d5459ff77d4e64cbf85fddb6a35dad836a15c6a9ec23087c1", size = 199155, upload-time = "2026-06-29T13:03:10.979Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9c/ca040d94415048a3666fc237774df8151c96f8d2b661cbe3b184acc95876/jiter-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:2a8e9e39cf083016137aa5cadafe3188adc2ba6ba1fbf1e5d18889ad3e9ad056", size = 194403, upload-time = "2026-06-29T13:03:12.341Z" }, + { url = "https://files.pythonhosted.org/packages/83/2b/52ace16ed031354f0539749a49e4bf33797d82bea5137910835fa4b09793/jiter-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a", size = 306943, upload-time = "2026-06-29T13:03:14.035Z" }, + { url = "https://files.pythonhosted.org/packages/94/2e/34957c2c1b661c252ba9bcc60ae0bddc27e0f7202c6073326a13c5390eec/jiter-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9", size = 307779, upload-time = "2026-06-29T13:03:15.418Z" }, + { url = "https://files.pythonhosted.org/packages/88/6c/59bd309cab4460c54cf1079f3eb7fe7af6a4c895c5c957a53378693bad2b/jiter-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72", size = 335826, upload-time = "2026-06-29T13:03:17.11Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8c/f5ef7b65f0df47afa16596969defb281ebb86e96df346d62be6fd853d620/jiter-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e", size = 362573, upload-time = "2026-06-29T13:03:18.781Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0b/ace4354da061ee38844a0c27dc2c21eecd27aea119e8da324bea987522d0/jiter-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290", size = 457979, upload-time = "2026-06-29T13:03:20.293Z" }, + { url = "https://files.pythonhosted.org/packages/55/40/c0253d3772eb9dcd8e6606ee9b2d53ec8e5b814589c47f140aa585f21eaa/jiter-0.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01", size = 372302, upload-time = "2026-06-29T13:03:21.739Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d2/4839422241aa12860ce597b20068727094ba0bc480723c74924ca5bad483/jiter-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48", size = 343805, upload-time = "2026-06-29T13:03:23.384Z" }, + { url = "https://files.pythonhosted.org/packages/e2/59/e196888a05befdda7dbe299b722d56f2f6eec65402bc34c0a3306d595feb/jiter-0.16.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9", size = 351107, upload-time = "2026-06-29T13:03:24.815Z" }, + { url = "https://files.pythonhosted.org/packages/ec/74/4cd9e0fca65232136400354b630fbfcd2de634e22ccbb96567725981b548/jiter-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5", size = 388441, upload-time = "2026-06-29T13:03:26.266Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8c/554691e48bc711299c0a293dd8a6179e24b2d66a54dc295421fcf64569c0/jiter-0.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730", size = 516354, upload-time = "2026-06-29T13:03:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/a4/cb/01e9d69dc2cc6759d4f91e230b34489c4fdb2518992650633f9e20bece89/jiter-0.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f", size = 547880, upload-time = "2026-06-29T13:03:29.534Z" }, + { url = "https://files.pythonhosted.org/packages/79/70/2953195f1c6ad00f49fa67e13df7e60acb3dd4f387101bc15abccddd905e/jiter-0.16.0-cp312-cp312-win32.whl", hash = "sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274", size = 203473, upload-time = "2026-06-29T13:03:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/05/2909a8b10699a4d560f8c502b6b2c5f3991b682b1922c1eedda242b225bd/jiter-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7", size = 196905, upload-time = "2026-06-29T13:03:32.472Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a9/6b82bb1c8d7790d602489b967b982a909e5d092875a6c2ade96444c8dfc5/jiter-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331", size = 190618, upload-time = "2026-06-29T13:03:34.672Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/555fc60473d30d66894ba825e63615e3be7524fac23858356afa7a38906c/jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195", size = 306203, upload-time = "2026-06-29T13:03:36.243Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2b/c3eaf16f5d7c9bad66ea32f40a95bd169b29a91217fcc7f081375157e99c/jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09", size = 306489, upload-time = "2026-06-29T13:03:37.846Z" }, + { url = "https://files.pythonhosted.org/packages/96/3f/02fdfc6705cad96127d883af5c34e4867f554f29ec7705ec1a46156400a9/jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536", size = 335453, upload-time = "2026-06-29T13:03:39.221Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a6/e4bda5920d4b0d7c5dfb7174ce4a6b2e4d3e11c9162c452ef0eab4cdbdbd/jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450", size = 361625, upload-time = "2026-06-29T13:03:40.597Z" }, + { url = "https://files.pythonhosted.org/packages/b7/97/4e6b59b2c6e55cbb3e183595f81ad65dcfb21c915fee5e19e335df21bc55/jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e", size = 456958, upload-time = "2026-06-29T13:03:42.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/e0/97e9557686d2f94f4b93786eccb7eed28e9228ad132ea8237f44727314a7/jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8", size = 372017, upload-time = "2026-06-29T13:03:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/0f/94/db768b6938e0df35c86beeba3dfbbb025c9ee5c19e1aa271f2396e50864d/jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026", size = 343320, upload-time = "2026-06-29T13:03:45.226Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d6/5a59d938244a30735fe62d9433fd325f9021ea29d89780ea4596ea93bc89/jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053", size = 350520, upload-time = "2026-06-29T13:03:46.671Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/c4a857f49c9af125f6bbcac7e3eee7f7978ed89682833062e2dbf62576b1/jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e", size = 387550, upload-time = "2026-06-29T13:03:48.361Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d6/5fbc2f7d6b67b754caa61a993a2e626e815dec47ffc2f9e35f01adfebec7/jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb", size = 515424, upload-time = "2026-06-29T13:03:49.881Z" }, + { url = "https://files.pythonhosted.org/packages/ed/54/284f0164b64a5fed915fea6ba7e9ba9b3d8d37c67d59cf2e3bb99d45cdfe/jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84", size = 546981, upload-time = "2026-06-29T13:03:51.363Z" }, + { url = "https://files.pythonhosted.org/packages/13/c5/2a467585a576594384e1d2c43e1224deaafc085f24e243529cf98beef8e1/jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e", size = 202853, upload-time = "2026-06-29T13:03:53.015Z" }, + { url = "https://files.pythonhosted.org/packages/88/6a/de61d04b9eec69c71719968d2f716532a3bc121170c44a39e14979c6be81/jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd", size = 196160, upload-time = "2026-06-29T13:03:54.447Z" }, + { url = "https://files.pythonhosted.org/packages/19/4b/b390ed59bafb3f31d008d1218578f10327714484b334439947f7e5b11e7f/jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a", size = 189862, upload-time = "2026-06-29T13:03:55.754Z" }, + { url = "https://files.pythonhosted.org/packages/a7/89/bc4f1b57d5da938fd344a466396541e586d161320d70bffd929aaafcd8f4/jiter-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee", size = 308239, upload-time = "2026-06-29T13:03:57.205Z" }, + { url = "https://files.pythonhosted.org/packages/65/7a/c415453e5213001bf3b411ff65dec3d303b0e76a4a2cfea9768cd4960994/jiter-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2", size = 308928, upload-time = "2026-06-29T13:03:58.643Z" }, + { url = "https://files.pythonhosted.org/packages/11/fc/1f4fb7ebf9a724c7741994f4aae18fba1e2f3133df14521a79194952c34a/jiter-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883", size = 336998, upload-time = "2026-06-29T13:04:00.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8d/72cadaac05ccfa7cc3a0a2232862e6c72443ca40cf300ba8b57f9f18b69b/jiter-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898", size = 362112, upload-time = "2026-06-29T13:04:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/58/4a/c4b0d5f651fda90a24ffce9f8d56cde462a2e09d31ae3de3c68cef34c04e/jiter-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5", size = 459807, upload-time = "2026-06-29T13:04:03.214Z" }, + { url = "https://files.pythonhosted.org/packages/80/58/ef77879ea9aa56b50824edc5a445e226422c7a8d211f3fd2a56bcb9493cf/jiter-0.16.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567", size = 373181, upload-time = "2026-06-29T13:04:04.629Z" }, + { url = "https://files.pythonhosted.org/packages/49/2e/ffbc3f254e4d8a66da3062c624a7df4b7c2b2cf9e1fe43cf394b3e104041/jiter-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69", size = 344927, upload-time = "2026-06-29T13:04:06.067Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/0be5dc6d64a89f80aa8fec984f94dedb2973e251edcae55841d60786d578/jiter-0.16.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29", size = 352754, upload-time = "2026-06-29T13:04:07.477Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/7d31243b3b91cd261dd19e9d3557fc3251a80883d3d8049c86174e7ab7af/jiter-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93", size = 390553, upload-time = "2026-06-29T13:04:08.92Z" }, + { url = "https://files.pythonhosted.org/packages/25/33/51ae371fde3c88897520f62b4d5f8b27ad7103e2bb10812ff52195609853/jiter-0.16.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a", size = 516900, upload-time = "2026-06-29T13:04:10.407Z" }, + { url = "https://files.pythonhosted.org/packages/a0/45/6449b3d123ea439ba79507c657288f461d55049e7bcbdc2cf8eb8210f491/jiter-0.16.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00", size = 548754, upload-time = "2026-06-29T13:04:12.046Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e7/fd2fb11ae3e2649333da3aa170d04d7b3000bbdc3b270f6513382fdf4e04/jiter-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe", size = 122381, upload-time = "2026-06-29T13:04:13.413Z" }, + { url = "https://files.pythonhosted.org/packages/26/80/f0b147a62c315a164ed2168908286ca302310824c218d3aae52b06c0c9a9/jiter-0.16.0-cp314-cp314-win32.whl", hash = "sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106", size = 204578, upload-time = "2026-06-29T13:04:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e6/4758a14304b4523a6f5adb2419340086aa3593bd4327c2b25b5948a90548/jiter-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8", size = 198154, upload-time = "2026-06-29T13:04:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/26/be/41fa54a2e7ea41d6c99f1dc5b1f0fd4cb474680304b5d268dd518e81da3a/jiter-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585", size = 191458, upload-time = "2026-06-29T13:04:17.707Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/59127338b86d9fe4d99418f5a15118bea778103ee0fe9d9dd7e0af174e95/jiter-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207", size = 316739, upload-time = "2026-06-29T13:04:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/2d/95/49461034d5388196d3dabf98748935f017b7785d8f3f5349f834bcc4ed0d/jiter-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818", size = 340911, upload-time = "2026-06-29T13:04:21.257Z" }, + { url = "https://files.pythonhosted.org/packages/cd/97/a4369f2fb82cb3dda13b98622f31249b2e014b223fe64ee534413ad72294/jiter-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3", size = 361747, upload-time = "2026-06-29T13:04:22.677Z" }, + { url = "https://files.pythonhosted.org/packages/28/51/49b6ed456261646e1906016a6760367a28aacd3c24805e4e5fe64116c1db/jiter-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284", size = 460225, upload-time = "2026-06-29T13:04:24.441Z" }, + { url = "https://files.pythonhosted.org/packages/33/b5/5689aff4f66c5b60be63106e591dbfcba2190df97d2c9c7cf052361ddb98/jiter-0.16.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929", size = 373169, upload-time = "2026-06-29T13:04:25.884Z" }, + { url = "https://files.pythonhosted.org/packages/a2/96/3ae1b85ee0d6d6cab254fb7f8da018272b932bbf2d69b07e98aa2a96c746/jiter-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620", size = 350332, upload-time = "2026-06-29T13:04:27.302Z" }, + { url = "https://files.pythonhosted.org/packages/15/32/c99d7bafd78986556c95bf60ce84c6cc98786eac56066c12d7f828bb6747/jiter-0.16.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af", size = 353377, upload-time = "2026-06-29T13:04:28.731Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/f99a8e571287c3dec766bcc18528bbe8e8fb5365522ab5e6d64c93e87066/jiter-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e", size = 387746, upload-time = "2026-06-29T13:04:30.319Z" }, + { url = "https://files.pythonhosted.org/packages/75/69/c78a5b3f71040e34eb5917df26fb7ae9a2174cad1ccbf277512507c53a6e/jiter-0.16.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077", size = 517292, upload-time = "2026-06-29T13:04:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f7/095b38eda4c70d03651c403f29a5590f16d12ddc5d544aac9f9cddf72277/jiter-0.16.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734", size = 549259, upload-time = "2026-06-29T13:04:33.721Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c5/6a0207d90e5f656d95af98ebd0934f382d37674416f215aeda2ff8063e51/jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf", size = 206523, upload-time = "2026-06-29T13:04:35.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/31/c757d5f30a8980fd945ce7b98be10be9e4ff59c7c42f5fd86804c2e87db8/jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db", size = 200366, upload-time = "2026-06-29T13:04:36.61Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a2/d88de6d313d734a544a7901353ad5db67cb38dcfcd91713b7979dafc345d/jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce", size = 190516, upload-time = "2026-06-29T13:04:38.004Z" }, + { url = "https://files.pythonhosted.org/packages/06/d3/8e278946d43eeca2585b4dd0834a887cd71136329b837f3a16ed86a8b4b0/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:850ccb1d7eedb4200f4014b1c0e8a577de114fc3cd88faad646dcc9bc4bb12ad", size = 304518, upload-time = "2026-06-29T13:05:00.172Z" }, + { url = "https://files.pythonhosted.org/packages/72/43/28d4ef495028bf0506a413d4db3f4eb3e7288a382e0f065f306a17bbeb5e/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:e34e97bda77eb63242a410243c071e28ac7e0d8c0948c5ee658498690a4b2f2f", size = 310207, upload-time = "2026-06-29T13:05:02.123Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ca/c366b1012da1d640de975d9683acd44e4d150d9068845d0ca2610435253f/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7dc85ea77d4abbae8bad0d3538678aedee75bceec4e2f6c8dfb1c74772e5aa5", size = 342771, upload-time = "2026-06-29T13:05:03.55Z" }, + { url = "https://files.pythonhosted.org/packages/16/52/50cc4056fc1ae02e7154704e7ecc89df0afb8300222cfe8a52d3f67e4730/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17ca7fae79f6d99cd9a042b75f917eaada7b895cfc7dd2ee3a16089dcaec7a85", size = 346468, upload-time = "2026-06-29T13:05:05.452Z" }, + { url = "https://files.pythonhosted.org/packages/98/ab/664fd8c4be028b2bedd3d2ff08769c4ede23d0dbc87a77c62384a0515b5d/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127", size = 303106, upload-time = "2026-06-29T13:05:07.118Z" }, + { url = "https://files.pythonhosted.org/packages/1a/07/421f1d5b65493a76e16027b848aba6a7d28073ae75944fa4289cc914d39f/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3", size = 304658, upload-time = "2026-06-29T13:05:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/0a/db/bba1155f01a01c3c37a89425d571da751bbedf5c54247b831a04cb971798/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702", size = 339719, upload-time = "2026-06-29T13:05:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/78/f7/18a1afcd64f35314b68c1f23afcd9994d0bc13e65cc77517afff4e83986d/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2", size = 343885, upload-time = "2026-06-29T13:05:12.087Z" }, +] + +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + +[[package]] +name = "joserfc" +version = "1.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/c6/b1cac0280f8efc57626ea8804866b37099f23cae11b1485a42b213245e31/joserfc-1.7.3.tar.gz", hash = "sha256:116955c2587139dba20621fd0bd7fc9255fa960c9fe7f43c43ebef2e801dcfcf", size = 233821, upload-time = "2026-07-08T12:41:42.66Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/f5/650b59d1b74f5befb7a7a7e7d7c92a26b94256df3541e2b4914152cd177a/joserfc-1.7.3-py3-none-any.whl", hash = "sha256:7c39f3f2c943dbc03122747fa8ebbd8e156e54904cf25651b452f4d2634a6075", size = 70982, upload-time = "2026-07-08T12:41:41.521Z" }, +] + +[[package]] +name = "jsonpatch" +version = "1.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpointer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, +] + +[[package]] +name = "jsonpath-ng" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/32/58/250751940d75c8019659e15482d548a4aa3b6ce122c515102a4bfdac50e3/jsonpath_ng-1.8.0.tar.gz", hash = "sha256:54252968134b5e549ea5b872f1df1168bd7defe1a52fed5a358c194e1943ddc3", size = 74513, upload-time = "2026-02-24T14:42:06.182Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/99/33c7d78a3fb70d545fd5411ac67a651c81602cc09c9cf0df383733f068c5/jsonpath_ng-1.8.0-py3-none-any.whl", hash = "sha256:b8dde192f8af58d646fc031fac9c99fe4d00326afc4148f1f043c601a8cfe138", size = 67844, upload-time = "2026-02-28T00:53:19.637Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" }, ] [[package]] name = "jsonschema" -version = "4.25.1" +version = "4.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "jsonschema-specifications" }, { name = "referencing" }, - { name = "rpds-py" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "rpds-py", version = "2026.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-path" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "pathable" }, + { name = "pyyaml" }, + { name = "referencing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +sdist = { url = "https://files.pythonhosted.org/packages/39/79/cd02a4df6d9270efdc7d3feefe6edd730b0820c39eeaa107a2faee8322d5/jsonschema_path-0.5.0.tar.gz", hash = "sha256:493b156ba895c97602655b620a8456caa2ce08c1aa389f5a7addec065e6e855c", size = 19597, upload-time = "2026-05-19T20:45:00.971Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/04/2c/9e69d73c4297508be9e3b64a970ea3971b3eb8db64ffc5802d40bd25981f/jsonschema_path-0.5.0-py3-none-any.whl", hash = "sha256:2790a070bc7abb08ea3dbe4d340ece4efadf639223001f020c7503229ba068e2", size = 24077, upload-time = "2026-05-19T20:44:59.225Z" }, ] [[package]] name = "jsonschema-specifications" -version = "2025.4.1" +version = "2025.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "referencing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513, upload-time = "2025-04-23T12:34:07.418Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" }, + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] [[package]] name = "keyring" -version = "25.6.0" +version = "25.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, @@ -1235,19 +1961,218 @@ dependencies = [ { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, { name = "secretstorage", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/70/09/d904a6e96f76ff214be59e7aa6ef7190008f52a0ab6689760a98de0bf37d/keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", size = 62750, upload-time = "2024-12-25T15:26:45.782Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, +] + +[[package]] +name = "langchain" +version = "1.3.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/68/a6dbad9c22df4087a0f9e79ddd46226c442b30128bfeee538d5889492a73/langchain-1.3.14.tar.gz", hash = "sha256:1b6696c72ba3bbbce54d745e0180742c9f6ece8bbc59ed5a46c3e20b9a435929", size = 645181, upload-time = "2026-07-16T13:28:18.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/ec/0f942e78a621f8e3162ff1ed24284f469aaf51fb4607ee5831c626f2b2bc/langchain-1.3.14-py3-none-any.whl", hash = "sha256:4d10dbe91005952cddd56d0dc77aa108964da6bae90ab20063653957e901f782", size = 139560, upload-time = "2026-07-16T13:28:16.498Z" }, +] + +[[package]] +name = "langchain-anthropic" +version = "1.4.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anthropic" }, + { name = "langchain-core" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/22/40ab129b08329ca295b391aa1d48267692b42594757084c6918e22b655ac/langchain_anthropic-1.4.8.tar.gz", hash = "sha256:c76891b2044d56105ff13c106ed12650637b53bd598a4bdf15b4796eefa2a4ec", size = 708524, upload-time = "2026-06-26T21:28:46.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/14/746235c4da89d9bc6a608c5f489f628e03feb8f697195c146e452c8f23c8/langchain_anthropic-1.4.8-py3-none-any.whl", hash = "sha256:778e9301b6fd517824f76ec1776975ce8add97a1f6a36c50ae3c2f4b03a66f7f", size = 52366, upload-time = "2026-06-26T21:28:45.535Z" }, +] + +[[package]] +name = "langchain-core" +version = "1.4.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpatch" }, + { name = "langchain-protocol" }, + { name = "langsmith" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "uuid-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/b9/e937d0a90b26540bff07e7a7c64349f3b29c2dcc36257cd1cd3fdce17f2a/langchain_core-1.4.9.tar.gz", hash = "sha256:f8078901145bed0466755277500a5a22822a7b628808c4c0a28d4fc88895fcf2", size = 967294, upload-time = "2026-07-08T20:06:54.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/70/ade2fada52772798ef815b6352b59e71b116aa0c32c3aef5be3dc2cbed12/langchain_core-1.4.9-py3-none-any.whl", hash = "sha256:28e3909e2a10cc81504952d795ac0a9e014c0018121ef89d48dd396fa09ec624", size = 558293, upload-time = "2026-07-08T20:06:52.382Z" }, +] + +[[package]] +name = "langchain-google-genai" +version = "4.2.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filetype" }, + { name = "google-genai" }, + { name = "langchain-core" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/0c/bc60dabc362ca7c6ffe8c4bcc2f724c7e566b43eb230cee51419f88f784c/langchain_google_genai-4.2.7.tar.gz", hash = "sha256:03b1463ffe4d42435f43c7870467f2215f684bb46400d2543435d10157c80ac7", size = 281605, upload-time = "2026-07-06T13:51:58.724Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/f9/d73d1e712591723aaddb7a7b1e94978cd2320c29acfe0d26b6169a2f26f0/langchain_google_genai-4.2.7-py3-none-any.whl", hash = "sha256:0d9c388d0e6c629718fca6abb19c6fdca728a9a7873d0324c1ec821288b5b571", size = 70702, upload-time = "2026-07-06T13:51:57.499Z" }, +] + +[[package]] +name = "langchain-protocol" +version = "0.0.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/59/b5959aea96faa9146e2e49a7a22882b3528c62efafe9a6a95beab30c2305/langchain_protocol-0.0.18.tar.gz", hash = "sha256:ec3e11782f1ed0c9db38e5a9ed01b0e7a0d3fba406faa8aef6594b73c56a63e6", size = 6150, upload-time = "2026-06-18T17:08:26.959Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/2e/d82db9eec13ad0f72e7aaad5c4bc730ab111934fdc83c85523206eb9b0a0/langchain_protocol-0.0.18-py3-none-any.whl", hash = "sha256:70b53a86fbf9cedc863555effe44da192ab02d556ddbf2cf95b8873adcf41b5a", size = 7221, upload-time = "2026-06-18T17:08:25.996Z" }, +] + +[[package]] +name = "langgraph" +version = "1.2.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph-checkpoint" }, + { name = "langgraph-prebuilt" }, + { name = "langgraph-sdk" }, + { name = "pydantic" }, + { name = "xxhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/4b/0d1130e26b41a99dcc88353bbe7162a1f255c4db746bd94024268e6af27b/langgraph-1.2.9.tar.gz", hash = "sha256:385f87bc1802c35af7e0aa479278ecba8582d103515eb48256cb2ddcd42d0bd4", size = 722869, upload-time = "2026-07-10T01:30:14.985Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/16/0b8dc48823f1326f3e0c8012a3c07a40da6f194299e2ec080df236287baf/langgraph-1.2.9-py3-none-any.whl", hash = "sha256:c2d98ad94333937922ba04148641c1da2bfe45b5b8e55d7b6dcb0bb2df809e76", size = 247473, upload-time = "2026-07-10T01:30:13.733Z" }, +] + +[[package]] +name = "langgraph-checkpoint" +version = "4.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "ormsgpack" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/47/886af6f886f0bff2273164a45f008694e48a96ff3cd25ff0228f2aa9480e/langgraph_checkpoint-4.1.1.tar.gz", hash = "sha256:6c2bdb530c91f91d7d9c1bd100925d0fc4f498d418c17f3587d1526279482a25", size = 184020, upload-time = "2026-05-22T16:57:38.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/b4/71425e3e38be92611300b9cc5e46a5bf98ab23f5ea8a75b73d02a2f1413c/langgraph_checkpoint-4.1.1-py3-none-any.whl", hash = "sha256:25d29144b082827218e7bc3f1e9b0566a4bb007895cd6cc26f66a8428739f56e", size = 56212, upload-time = "2026-05-22T16:57:37.203Z" }, +] + +[[package]] +name = "langgraph-prebuilt" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph-checkpoint" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/66/ed9b93f56bc17ef22d551892f0ac2b225a97fe0fcf23a511b857f70d590b/langgraph_prebuilt-1.1.0.tar.gz", hash = "sha256:3c579cf6eed2d17f9c157c2d0fcaddcd8688524e7022d3b22b37a3bf4589d528", size = 178833, upload-time = "2026-05-12T03:37:49.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/43/3fe1a700b8490ed02679cdbbc8c915eb23a092faf496c9c1118abcd10be3/langgraph_prebuilt-1.1.0-py3-none-any.whl", hash = "sha256:51e311747d755b751d5c6b39b0c1446124d3a7643d2515017e6714b323508fc9", size = 41043, upload-time = "2026-05-12T03:37:48.007Z" }, +] + +[[package]] +name = "langgraph-sdk" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "langchain-core" }, + { name = "langchain-protocol" }, + { name = "orjson" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/2b/bd8ac26d4e97f6df88ef05ce5b6a38945a3903e1025d926f4752aa88aa97/langgraph_sdk-0.4.2.tar.gz", hash = "sha256:b88f0f5f6328ac0680d6790614a905b2bcfa257f2276dba4e38f0e86db0aa738", size = 348327, upload-time = "2026-06-01T17:51:19.856Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085, upload-time = "2024-12-25T15:26:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/a0/05/aac507337cceae773c2cc9ab91eb6301963af7aeeb55b4217a00e15aff17/langgraph_sdk-0.4.2-py3-none-any.whl", hash = "sha256:75fa5096c1177ce39c847096a8fe3745ffd480ddb412995f836e9f5f884c43dd", size = 160521, upload-time = "2026-06-01T17:51:18.849Z" }, +] + +[[package]] +name = "langsmith" +version = "0.8.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "orjson", marker = "platform_python_implementation != 'PyPy'" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "uuid-utils" }, + { name = "websockets" }, + { name = "xxhash" }, + { name = "zstandard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/d9/a6681aa9847bbbc5ec21abe20a5e233b94e5edcfe39624db607ac7e8ccb4/langsmith-0.8.18.tar.gz", hash = "sha256:32dde9c0e67e053e0fb738921fc8ced768af7b8fa83d7a0e3fd63597cf8776dd", size = 4526988, upload-time = "2026-06-19T13:12:17.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/70/0e0cc80a3b064c8d6c8d697c3125ed86e39d5a7393ec6dc8b07cb1cf13c4/langsmith-0.8.18-py3-none-any.whl", hash = "sha256:3940183349993faef48e6c7d08e4822ee9cefd906b362d0e3c2d650314d2f282", size = 508108, upload-time = "2026-06-19T13:12:15.348Z" }, +] + +[[package]] +name = "lazy-object-proxy" +version = "1.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/08/a2/69df9c6ba6d316cfd81fe2381e464db3e6de5db45f8c43c6a23504abf8cb/lazy_object_proxy-1.12.0.tar.gz", hash = "sha256:1f5a462d92fd0cfb82f1fab28b51bfb209fabbe6aabf7f0d51472c0c124c0c61", size = 43681, upload-time = "2025-08-22T13:50:06.783Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/2b/d5e8915038acbd6c6a9fcb8aaf923dc184222405d3710285a1fec6e262bc/lazy_object_proxy-1.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:61d5e3310a4aa5792c2b599a7a78ccf8687292c8eb09cf187cca8f09cf6a7519", size = 26658, upload-time = "2025-08-22T13:42:23.373Z" }, + { url = "https://files.pythonhosted.org/packages/da/8f/91fc00eeea46ee88b9df67f7c5388e60993341d2a406243d620b2fdfde57/lazy_object_proxy-1.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1ca33565f698ac1aece152a10f432415d1a2aa9a42dfe23e5ba2bc255ab91f6", size = 68412, upload-time = "2025-08-22T13:42:24.727Z" }, + { url = "https://files.pythonhosted.org/packages/07/d2/b7189a0e095caedfea4d42e6b6949d2685c354263bdf18e19b21ca9b3cd6/lazy_object_proxy-1.12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01c7819a410f7c255b20799b65d36b414379a30c6f1684c7bd7eb6777338c1b", size = 67559, upload-time = "2025-08-22T13:42:25.875Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b013840cc43971582ff1ceaf784d35d3a579650eb6cc348e5e6ed7e34d28/lazy_object_proxy-1.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:029d2b355076710505c9545aef5ab3f750d89779310e26ddf2b7b23f6ea03cd8", size = 66651, upload-time = "2025-08-22T13:42:27.427Z" }, + { url = "https://files.pythonhosted.org/packages/7e/6f/b7368d301c15612fcc4cd00412b5d6ba55548bde09bdae71930e1a81f2ab/lazy_object_proxy-1.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc6e3614eca88b1c8a625fc0a47d0d745e7c3255b21dac0e30b3037c5e3deeb8", size = 66901, upload-time = "2025-08-22T13:42:28.585Z" }, + { url = "https://files.pythonhosted.org/packages/61/1b/c6b1865445576b2fc5fa0fbcfce1c05fee77d8979fd1aa653dd0f179aefc/lazy_object_proxy-1.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:be5fe974e39ceb0d6c9db0663c0464669cf866b2851c73971409b9566e880eab", size = 26536, upload-time = "2025-08-22T13:42:29.636Z" }, + { url = "https://files.pythonhosted.org/packages/01/b3/4684b1e128a87821e485f5a901b179790e6b5bc02f89b7ee19c23be36ef3/lazy_object_proxy-1.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1cf69cd1a6c7fe2dbcc3edaa017cf010f4192e53796538cc7d5e1fedbfa4bcff", size = 26656, upload-time = "2025-08-22T13:42:30.605Z" }, + { url = "https://files.pythonhosted.org/packages/3a/03/1bdc21d9a6df9ff72d70b2ff17d8609321bea4b0d3cffd2cea92fb2ef738/lazy_object_proxy-1.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efff4375a8c52f55a145dc8487a2108c2140f0bec4151ab4e1843e52eb9987ad", size = 68832, upload-time = "2025-08-22T13:42:31.675Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4b/5788e5e8bd01d19af71e50077ab020bc5cce67e935066cd65e1215a09ff9/lazy_object_proxy-1.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1192e8c2f1031a6ff453ee40213afa01ba765b3dc861302cd91dbdb2e2660b00", size = 69148, upload-time = "2025-08-22T13:42:32.876Z" }, + { url = "https://files.pythonhosted.org/packages/79/0e/090bf070f7a0de44c61659cb7f74c2fe02309a77ca8c4b43adfe0b695f66/lazy_object_proxy-1.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3605b632e82a1cbc32a1e5034278a64db555b3496e0795723ee697006b980508", size = 67800, upload-time = "2025-08-22T13:42:34.054Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d2/b320325adbb2d119156f7c506a5fbfa37fcab15c26d13cf789a90a6de04e/lazy_object_proxy-1.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a61095f5d9d1a743e1e20ec6d6db6c2ca511961777257ebd9b288951b23b44fa", size = 68085, upload-time = "2025-08-22T13:42:35.197Z" }, + { url = "https://files.pythonhosted.org/packages/6a/48/4b718c937004bf71cd82af3713874656bcb8d0cc78600bf33bb9619adc6c/lazy_object_proxy-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:997b1d6e10ecc6fb6fe0f2c959791ae59599f41da61d652f6c903d1ee58b7370", size = 26535, upload-time = "2025-08-22T13:42:36.521Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1b/b5f5bd6bda26f1e15cd3232b223892e4498e34ec70a7f4f11c401ac969f1/lazy_object_proxy-1.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ee0d6027b760a11cc18281e702c0309dd92da458a74b4c15025d7fc490deede", size = 26746, upload-time = "2025-08-22T13:42:37.572Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/314889b618075c2bfc19293ffa9153ce880ac6153aacfd0a52fcabf21a66/lazy_object_proxy-1.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4ab2c584e3cc8be0dfca422e05ad30a9abe3555ce63e9ab7a559f62f8dbc6ff9", size = 71457, upload-time = "2025-08-22T13:42:38.743Z" }, + { url = "https://files.pythonhosted.org/packages/11/53/857fc2827fc1e13fbdfc0ba2629a7d2579645a06192d5461809540b78913/lazy_object_proxy-1.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14e348185adbd03ec17d051e169ec45686dcd840a3779c9d4c10aabe2ca6e1c0", size = 71036, upload-time = "2025-08-22T13:42:40.184Z" }, + { url = "https://files.pythonhosted.org/packages/2b/24/e581ffed864cd33c1b445b5763d617448ebb880f48675fc9de0471a95cbc/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c4fcbe74fb85df8ba7825fa05eddca764138da752904b378f0ae5ab33a36c308", size = 69329, upload-time = "2025-08-22T13:42:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/15f8f5a0b0b2e668e756a152257d26370132c97f2f1943329b08f057eff0/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:563d2ec8e4d4b68ee7848c5ab4d6057a6d703cb7963b342968bb8758dda33a23", size = 70690, upload-time = "2025-08-22T13:42:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/5d/aa/f02be9bbfb270e13ee608c2b28b8771f20a5f64356c6d9317b20043c6129/lazy_object_proxy-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:53c7fd99eb156bbb82cbc5d5188891d8fdd805ba6c1e3b92b90092da2a837073", size = 26563, upload-time = "2025-08-22T13:42:43.685Z" }, + { url = "https://files.pythonhosted.org/packages/f4/26/b74c791008841f8ad896c7f293415136c66cc27e7c7577de4ee68040c110/lazy_object_proxy-1.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:86fd61cb2ba249b9f436d789d1356deae69ad3231dc3c0f17293ac535162672e", size = 26745, upload-time = "2025-08-22T13:42:44.982Z" }, + { url = "https://files.pythonhosted.org/packages/9b/52/641870d309e5d1fb1ea7d462a818ca727e43bfa431d8c34b173eb090348c/lazy_object_proxy-1.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81d1852fb30fab81696f93db1b1e55a5d1ff7940838191062f5f56987d5fcc3e", size = 71537, upload-time = "2025-08-22T13:42:46.141Z" }, + { url = "https://files.pythonhosted.org/packages/47/b6/919118e99d51c5e76e8bf5a27df406884921c0acf2c7b8a3b38d847ab3e9/lazy_object_proxy-1.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9045646d83f6c2664c1330904b245ae2371b5c57a3195e4028aedc9f999655", size = 71141, upload-time = "2025-08-22T13:42:47.375Z" }, + { url = "https://files.pythonhosted.org/packages/e5/47/1d20e626567b41de085cf4d4fb3661a56c159feaa73c825917b3b4d4f806/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:67f07ab742f1adfb3966c40f630baaa7902be4222a17941f3d85fd1dae5565ff", size = 69449, upload-time = "2025-08-22T13:42:48.49Z" }, + { url = "https://files.pythonhosted.org/packages/58/8d/25c20ff1a1a8426d9af2d0b6f29f6388005fc8cd10d6ee71f48bff86fdd0/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ba769017b944fcacbf6a80c18b2761a1795b03f8899acdad1f1c39db4409be", size = 70744, upload-time = "2025-08-22T13:42:49.608Z" }, + { url = "https://files.pythonhosted.org/packages/c0/67/8ec9abe15c4f8a4bcc6e65160a2c667240d025cbb6591b879bea55625263/lazy_object_proxy-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:7b22c2bbfb155706b928ac4d74c1a63ac8552a55ba7fff4445155523ea4067e1", size = 26568, upload-time = "2025-08-22T13:42:57.719Z" }, + { url = "https://files.pythonhosted.org/packages/23/12/cd2235463f3469fd6c62d41d92b7f120e8134f76e52421413a0ad16d493e/lazy_object_proxy-1.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4a79b909aa16bde8ae606f06e6bbc9d3219d2e57fb3e0076e17879072b742c65", size = 27391, upload-time = "2025-08-22T13:42:50.62Z" }, + { url = "https://files.pythonhosted.org/packages/60/9e/f1c53e39bbebad2e8609c67d0830cc275f694d0ea23d78e8f6db526c12d3/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:338ab2f132276203e404951205fe80c3fd59429b3a724e7b662b2eb539bb1be9", size = 80552, upload-time = "2025-08-22T13:42:51.731Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b6/6c513693448dcb317d9d8c91d91f47addc09553613379e504435b4cc8b3e/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c40b3c9faee2e32bfce0df4ae63f4e73529766893258eca78548bac801c8f66", size = 82857, upload-time = "2025-08-22T13:42:53.225Z" }, + { url = "https://files.pythonhosted.org/packages/12/1c/d9c4aaa4c75da11eb7c22c43d7c90a53b4fca0e27784a5ab207768debea7/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:717484c309df78cedf48396e420fa57fc8a2b1f06ea889df7248fdd156e58847", size = 80833, upload-time = "2025-08-22T13:42:54.391Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ae/29117275aac7d7d78ae4f5a4787f36ff33262499d486ac0bf3e0b97889f6/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a6b7ea5ea1ffe15059eb44bcbcb258f97bcb40e139b88152c40d07b1a1dfc9ac", size = 79516, upload-time = "2025-08-22T13:42:55.812Z" }, + { url = "https://files.pythonhosted.org/packages/19/40/b4e48b2c38c69392ae702ae7afa7b6551e0ca5d38263198b7c79de8b3bdf/lazy_object_proxy-1.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:08c465fb5cd23527512f9bd7b4c7ba6cec33e28aad36fbbe46bf7b858f9f3f7f", size = 27656, upload-time = "2025-08-22T13:42:56.793Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3a/277857b51ae419a1574557c0b12e0d06bf327b758ba94cafc664cb1e2f66/lazy_object_proxy-1.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c9defba70ab943f1df98a656247966d7729da2fe9c2d5d85346464bf320820a3", size = 26582, upload-time = "2025-08-22T13:49:49.366Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b6/c5e0fa43535bb9c87880e0ba037cdb1c50e01850b0831e80eb4f4762f270/lazy_object_proxy-1.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6763941dbf97eea6b90f5b06eb4da9418cc088fce0e3883f5816090f9afcde4a", size = 71059, upload-time = "2025-08-22T13:49:50.488Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/7dcad19c685963c652624702f1a968ff10220b16bfcc442257038216bf55/lazy_object_proxy-1.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fdc70d81235fc586b9e3d1aeef7d1553259b62ecaae9db2167a5d2550dcc391a", size = 71034, upload-time = "2025-08-22T13:49:54.224Z" }, + { url = "https://files.pythonhosted.org/packages/12/ac/34cbfb433a10e28c7fd830f91c5a348462ba748413cbb950c7f259e67aa7/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0a83c6f7a6b2bfc11ef3ed67f8cbe99f8ff500b05655d8e7df9aab993a6abc95", size = 69529, upload-time = "2025-08-22T13:49:55.29Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6a/11ad7e349307c3ca4c0175db7a77d60ce42a41c60bcb11800aabd6a8acb8/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:256262384ebd2a77b023ad02fbcc9326282bcfd16484d5531154b02bc304f4c5", size = 70391, upload-time = "2025-08-22T13:49:56.35Z" }, + { url = "https://files.pythonhosted.org/packages/59/97/9b410ed8fbc6e79c1ee8b13f8777a80137d4bc189caf2c6202358e66192c/lazy_object_proxy-1.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7601ec171c7e8584f8ff3f4e440aa2eebf93e854f04639263875b8c2971f819f", size = 26988, upload-time = "2025-08-22T13:49:57.302Z" }, + { url = "https://files.pythonhosted.org/packages/41/a0/b91504515c1f9a299fc157967ffbd2f0321bce0516a3d5b89f6f4cad0355/lazy_object_proxy-1.12.0-pp39.pp310.pp311.graalpy311-none-any.whl", hash = "sha256:c3b2e0af1f7f77c4263759c4824316ce458fabe0fceadcd24ef8ca08b2d1e402", size = 15072, upload-time = "2025-08-22T13:50:05.498Z" }, ] [[package]] name = "litellm" -version = "1.76.1" +version = "1.91.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "click" }, { name = "fastuuid" }, { name = "httpx" }, { name = "importlib-metadata" }, @@ -1259,162 +2184,177 @@ dependencies = [ { name = "tiktoken" }, { name = "tokenizers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/fd/aa87c0a598377786521bee585f4d525e846f5339b816903298bfbb9daef5/litellm-1.76.1.tar.gz", hash = "sha256:d5a3a3efda04999b60ec0d1c29c1eaaa12f89a7b29db4bda691c7fb55b4fa6ad", size = 10178100, upload-time = "2025-08-30T21:05:48.578Z" } +sdist = { url = "https://files.pythonhosted.org/packages/86/1e/90cfeada42170986a290feebaca0a90923b3c310cddeb0f8690abd239ad4/litellm-1.91.0.tar.gz", hash = "sha256:4fd469fe7356ba8fcc86f4efdf332e3426b760962ab12331fdaf1a01aeec065f", size = 14872290, upload-time = "2026-07-04T19:18:28.466Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/d3/16423b6d399540eeff357f00abc85f62dc337d347a0c98ccadc448a61df5/litellm-1.76.1-py3-none-any.whl", hash = "sha256:938f05075372f26098211ea9b3cb0a6bb7b46111330226b70d42d40bd307812f", size = 8965465, upload-time = "2025-08-30T21:05:46.068Z" }, + { url = "https://files.pythonhosted.org/packages/a4/15/81fc2d162513803fe08b359c2e2a98f7a33195034fb94b005e263e272c6b/litellm-1.91.0-py3-none-any.whl", hash = "sha256:c3eb52dd2c6a5779e9efd67350f3640f9055d9c05d4b572fc1dd01a217e562ad", size = 16669331, upload-time = "2026-07-04T19:18:25.203Z" }, ] [[package]] name = "lunr" -version = "0.7.0.post1" +version = "0.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8b/92/885c5e6251b76d3a171ff757a4e167cbb44c02fd9aff67b545a246778a6a/lunr-0.7.0.post1.tar.gz", hash = "sha256:00fc98f59b53c7ee0f6384c99e6c099f28cb746ecfff865bbc3705c3e9104bda", size = 1146070, upload-time = "2023-08-16T16:51:34.135Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/e9/b3dee02312eaa2a1b3212b6e20a90a81adba489b404d4f0ffbbe8258b761/lunr-0.8.0.tar.gz", hash = "sha256:b46cf5059578d277a14bfc901bb3d5666d013bf73c035331ac0222fdac358228", size = 1147598, upload-time = "2025-03-08T13:31:40.907Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/6c/9209b793fc98f9211846f3b2ec63e0780d30c26b9a0f2985100430dcd238/lunr-0.7.0.post1-py3-none-any.whl", hash = "sha256:77cce585d195d412cff362698799c9571ff3e285fc6bd8816ecbc9ec82dbb368", size = 35209, upload-time = "2023-08-16T16:51:31.589Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8b/bf975fabd26195915ebdf3e4252baa936f1863bcd9eb49598b705638f5d5/lunr-0.8.0-py3-none-any.whl", hash = "sha256:a2bc4e08dbb35b32723006bf2edbe6dc1f4f4b95955eea0d23165a184d276ce8", size = 35211, upload-time = "2025-03-08T13:31:38.657Z" }, ] [[package]] name = "markdown-it-py" -version = "3.0.0" +version = "4.2.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] dependencies = [ - { name = "mdurl", marker = "python_full_version < '3.10'" }, + { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, ] [[package]] -name = "markdown-it-py" -version = "4.0.0" +name = "markdownify" +version = "1.2.3" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", -] dependencies = [ - { name = "mdurl", marker = "python_full_version >= '3.10'" }, + { name = "beautifulsoup4" }, + { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +sdist = { url = "https://files.pythonhosted.org/packages/92/ab/d1297139c0e2ceb151ae564c8c4f57ac0155d8f1f8b4cbd5d6523c82ea36/markdownify-1.2.3.tar.gz", hash = "sha256:1a176f05522c8a2cb1dd3ab9d307dcdadbed5c26ae717855bfc42b3b6d38d937", size = 18852, upload-time = "2026-06-30T20:27:39.06Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, + { url = "https://files.pythonhosted.org/packages/04/10/fa543d484e8b1199243fe20eedd02cc5af050edebce98a7293a5773df592/markdownify-1.2.3-py3-none-any.whl", hash = "sha256:a189a0bedfd14009030fde5f85bb6f77c56897cb839b5c25315dd7d4e3e290ba", size = 15732, upload-time = "2026-06-30T20:27:38.094Z" }, ] [[package]] name = "markupsafe" -version = "3.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" }, - { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" }, - { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" }, - { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" }, - { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" }, - { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" }, - { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" }, - { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" }, - { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" }, - { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" }, - { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, - { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, - { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, - { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, - { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, - { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, - { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, - { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, - { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, - { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, - { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, - { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, - { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, - { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, - { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, - { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, - { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, - { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, - { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, - { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, - { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, - { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, - { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, - { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, - { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, - { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, - { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, - { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, - { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, - { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, - { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, - { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, - { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, - { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, - { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, - { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, - { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344, upload-time = "2024-10-18T15:21:43.721Z" }, - { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389, upload-time = "2024-10-18T15:21:44.666Z" }, - { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607, upload-time = "2024-10-18T15:21:45.452Z" }, - { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728, upload-time = "2024-10-18T15:21:46.295Z" }, - { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826, upload-time = "2024-10-18T15:21:47.134Z" }, - { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843, upload-time = "2024-10-18T15:21:48.334Z" }, - { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219, upload-time = "2024-10-18T15:21:49.587Z" }, - { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946, upload-time = "2024-10-18T15:21:50.441Z" }, - { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063, upload-time = "2024-10-18T15:21:51.385Z" }, - { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506, upload-time = "2024-10-18T15:21:52.974Z" }, +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] [[package]] name = "maturin" -version = "1.9.4" +version = "1.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/13/7c/b11b870fc4fd84de2099906314ce45488ae17be32ff5493519a6cddc518a/maturin-1.9.4.tar.gz", hash = "sha256:235163a0c99bc6f380fb8786c04fd14dcf6cd622ff295ea3de525015e6ac40cf", size = 213647, upload-time = "2025-08-27T11:37:57.079Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/b3/addd877f871fb1860d46d3a4f206ecb10b946c85846805e6367631926fd3/maturin-1.14.1.tar.gz", hash = "sha256:9d6577a62cd08e0ceba7a0db06fb098e0c9b1b3429bad747a4f3a18215a1b3df", size = 369637, upload-time = "2026-06-19T05:19:49.774Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/90/0d99389eea1939116fca841cad0763600c8d3183a02a9478d066736c60e8/maturin-1.9.4-py3-none-linux_armv6l.whl", hash = "sha256:6ff37578e3f5fdbe685110d45f60af1f5a7dfce70a1e26dfe3810af66853ecae", size = 8276133, upload-time = "2025-08-27T11:37:23.325Z" }, - { url = "https://files.pythonhosted.org/packages/f4/ed/c8ec68b383e50f084bf1fa9605e62a90cd32a3f75d9894ed3a6e5d4cc5b3/maturin-1.9.4-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f3837bb53611b2dafa1c090436c330f2d743ba305ef00d8801a371f4495e7e1b", size = 15994496, upload-time = "2025-08-27T11:37:27.092Z" }, - { url = "https://files.pythonhosted.org/packages/84/4e/401ff5f3cfc6b123364d4b94379bf910d7baee32c9c95b72784ff2329357/maturin-1.9.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4227d627d8e3bfe45877a8d65e9d8351a9d01434549f0da75d2c06a1b570de58", size = 8362228, upload-time = "2025-08-27T11:37:31.181Z" }, - { url = "https://files.pythonhosted.org/packages/51/8e/c56176dd360da9650c62b8a5ecfb85432cf011e97e46c186901e6996002e/maturin-1.9.4-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:1bb2aa0fa29032e9c5aac03ac400396ddea12cadef242f8967e9c8ef715313a1", size = 8271397, upload-time = "2025-08-27T11:37:33.672Z" }, - { url = "https://files.pythonhosted.org/packages/d2/46/001fcc5c6ad509874896418d6169a61acd619df5b724f99766308c44a99f/maturin-1.9.4-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:a0868d52934c8a5d1411b42367633fdb5cd5515bec47a534192282167448ec30", size = 8775625, upload-time = "2025-08-27T11:37:35.86Z" }, - { url = "https://files.pythonhosted.org/packages/b4/2e/26fa7574f01c19b7a74680fd70e5bae2e8c40fed9683d1752e765062cc2b/maturin-1.9.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:68b7b833b25741c0f553b78e8b9e095b31ae7c6611533b3c7b71f84c2cb8fc44", size = 8051117, upload-time = "2025-08-27T11:37:38.278Z" }, - { url = "https://files.pythonhosted.org/packages/73/ee/ca7308832d4f5b521c1aa176d9265f6f93e0bd1ad82a90fd9cd799f6b28c/maturin-1.9.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:08dc86312afee55af778af919818632e35d8d0464ccd79cb86700d9ea560ccd7", size = 8132122, upload-time = "2025-08-27T11:37:40.499Z" }, - { url = "https://files.pythonhosted.org/packages/45/e8/c623955da75e801a06942edf1fdc4e772a9e8fbc1ceebbdc85d59584dc10/maturin-1.9.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:ef20ffdd943078c4c3699c29fb2ed722bb6b4419efdade6642d1dbf248f94a70", size = 10586762, upload-time = "2025-08-27T11:37:42.718Z" }, - { url = "https://files.pythonhosted.org/packages/3c/4b/19ad558fdf54e151b1b4916ed45f1952ada96684ee6db64f9cd91cabec09/maturin-1.9.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:368e958468431dfeec80f75eea9639b4356d8c42428b0128444424b083fecfb0", size = 8926988, upload-time = "2025-08-27T11:37:45.492Z" }, - { url = "https://files.pythonhosted.org/packages/7e/27/153ad15eccae26921e8a01812da9f3b7f9013368f8f92c36853f2043b2a3/maturin-1.9.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:273f879214f63f79bfe851cd7d541f8150bdbfae5dfdc3c0c4d125d02d1f41b4", size = 8536758, upload-time = "2025-08-27T11:37:48.213Z" }, - { url = "https://files.pythonhosted.org/packages/43/e3/f304c3bdc3fba9adebe5348d4d2dd015f1152c0a9027aaf52cae0bb182c8/maturin-1.9.4-py3-none-win32.whl", hash = "sha256:ed2e54d132ace7e61829bd49709331007dd9a2cc78937f598aa76a4f69b6804d", size = 7265200, upload-time = "2025-08-27T11:37:50.881Z" }, - { url = "https://files.pythonhosted.org/packages/14/14/f86d0124bf1816b99005c058a1dbdca7cb5850d9cf4b09dcae07a1bc6201/maturin-1.9.4-py3-none-win_amd64.whl", hash = "sha256:8e450bb2c9afdf38a0059ee2e1ec2b17323f152b59c16f33eb9c74edaf1f9f79", size = 8237391, upload-time = "2025-08-27T11:37:53.23Z" }, - { url = "https://files.pythonhosted.org/packages/3f/25/8320fc2591e45b750c3ae71fa596b47aefa802d07d6abaaa719034a85160/maturin-1.9.4-py3-none-win_arm64.whl", hash = "sha256:7a6f980a9b67a5c13c844c268eabd855b54a6a765df4b4bb07d15a990572a4c9", size = 6988277, upload-time = "2025-08-27T11:37:55.429Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f0/97c5a5bd9c71653a066c0976a484eaaae50b9369557838a4176b7b0bdaa5/maturin-1.14.1-py3-none-linux_armv6l.whl", hash = "sha256:522292398945442cdafa9daeb2271b2340fbde57027b818f923f88eab04174f8", size = 10207496, upload-time = "2026-06-19T05:19:09.321Z" }, + { url = "https://files.pythonhosted.org/packages/fe/83/294bca639b0e052f1e2f65199b3db258780c7d4e31408b934c9c974a1379/maturin-1.14.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ffe5ad71f21d1e6603c4dd75f7fee34adf5ed5ebcebb692886549888ebb329ed", size = 19680113, upload-time = "2026-06-19T05:19:13.43Z" }, + { url = "https://files.pythonhosted.org/packages/43/b6/79c881410a3b1c187f7eb3d407aecae646c6a4433d630d72200359015e83/maturin-1.14.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f3306078070c1508fd715b9116070cbcaff5959024272a9f1e6f5cb29768b86c", size = 10169205, upload-time = "2026-06-19T05:19:16.615Z" }, + { url = "https://files.pythonhosted.org/packages/93/9d/44b6f26dcb7f7a04c5501ac2dbb6ca1490150682baa525ca5860504f9eab/maturin-1.14.1-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:cd457cd88961156e26379e1155bd287cc0ec1c8b2f1582b0660fb31b87c8842d", size = 10188098, upload-time = "2026-06-19T05:19:19.736Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bd/9c0d5d6983905ce2c9edaa073a7e89355a9cf7f396988e05d32f1c37785d/maturin-1.14.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:dfc54ae32e6fcb18302193ab9a30b0b25eefffba994ae13238974805533ef75e", size = 10627576, upload-time = "2026-06-19T05:19:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/e5/33/b096412bd6a7cb399652b260666f901adf88a687181a6dbd6a3f89f0a94e/maturin-1.14.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:a131d912b5267e640bc96d70f4914e10590aed64082ec9abacba7cea52004224", size = 10085181, upload-time = "2026-06-19T05:19:25.69Z" }, + { url = "https://files.pythonhosted.org/packages/56/8d/08c3bf469c38a23c9e6c877e338193001eb604d010fedc08341974e38528/maturin-1.14.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:be18fc568fb76884c0205456336892a75105ec398e6b667cd777c6268bd06d69", size = 10026363, upload-time = "2026-06-19T05:19:28.904Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a4/c4d1a92839f8745ab4aab988a7db884a79d6d710bd3b286fcf9316dece1a/maturin-1.14.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:994a0c8ba3ad8a92b3a9ee1b02645d200d610216b15cff5102b0fe65e8e08666", size = 13321347, upload-time = "2026-06-19T05:19:32.411Z" }, + { url = "https://files.pythonhosted.org/packages/b3/fa/170f04624d03fd07d2a8b1b67de83a127af93aef9eaa425839553347297b/maturin-1.14.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be80866363e605d137991b491a741a84cde9ae350183c4c85f49690ca9aaaa65", size = 10877609, upload-time = "2026-06-19T05:19:35.448Z" }, + { url = "https://files.pythonhosted.org/packages/61/ad/1ae2e1d0ded282bf2c55ac13f0811d87deb425e200ae64a15785675dede9/maturin-1.14.1-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:5282dffd4b539d2be245f4e5b1a5ab6bc1033b58f4a4872f5833f9d43c954aa4", size = 10417316, upload-time = "2026-06-19T05:19:38.28Z" }, + { url = "https://files.pythonhosted.org/packages/fb/27/bf677183920718da49cd7982d6a3ffc440aad8919329f571d189f81b7bdf/maturin-1.14.1-py3-none-win32.whl", hash = "sha256:1a04de0a20188f95c721b5702eed18140bdcccb28c386797093eca3f62f4d4e0", size = 8931293, upload-time = "2026-06-19T05:19:41.183Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/585adeb9167b08d3cdff0032a938b0e72655c92003df4f52c3f696a1bcc2/maturin-1.14.1-py3-none-win_amd64.whl", hash = "sha256:3c9f94640ecc4895e94abaf834a0684430032c865b2748a36c12461fd9252fdd", size = 10314067, upload-time = "2026-06-19T05:19:44.389Z" }, + { url = "https://files.pythonhosted.org/packages/51/d4/dac8c0720ae246be1700afb6fbdbbea20fe35b13f6570b2f70faa005df77/maturin-1.14.1-py3-none-win_arm64.whl", hash = "sha256:15cea8fcb3ba47dd636f50092bb34baea8b04ac777392f23e6bf8a9a61efb894", size = 9718943, upload-time = "2026-06-19T05:19:47.49Z" }, ] [[package]] name = "mcp" -version = "1.13.1" +version = "1.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", marker = "python_full_version >= '3.10'" }, - { name = "httpx", marker = "python_full_version >= '3.10'" }, - { name = "httpx-sse", marker = "python_full_version >= '3.10'" }, - { name = "jsonschema", marker = "python_full_version >= '3.10'" }, - { name = "pydantic", marker = "python_full_version >= '3.10'" }, - { name = "pydantic-settings", marker = "python_full_version >= '3.10'" }, - { name = "python-multipart", marker = "python_full_version >= '3.10'" }, - { name = "pywin32", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, - { name = "sse-starlette", marker = "python_full_version >= '3.10'" }, - { name = "starlette", marker = "python_full_version >= '3.10'" }, - { name = "uvicorn", marker = "python_full_version >= '3.10' and sys_platform != 'emscripten'" }, + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/3c/82c400c2d50afdac4fbefb5b4031fd327e2ad1f23ccef8eee13c5909aa48/mcp-1.13.1.tar.gz", hash = "sha256:165306a8fd7991dc80334edd2de07798175a56461043b7ae907b279794a834c5", size = 438198, upload-time = "2025-08-22T09:22:16.061Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/3f/d085c7f49ade6d273b185d61ec9405e672b6433f710ea64a90135a8dd445/mcp-1.13.1-py3-none-any.whl", hash = "sha256:c314e7c8bd477a23ba3ef472ee5a32880316c42d03e06dcfa31a1cc7a73b65df", size = 161494, upload-time = "2025-08-22T09:22:14.705Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" }, ] [[package]] @@ -1428,218 +2368,316 @@ wheels = [ [[package]] name = "more-itertools" -version = "10.8.0" +version = "11.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/1d/f4da6f02cdffe04d6362210b807146a26044c88d839208aec273bb0d9184/more_itertools-11.1.0.tar.gz", hash = "sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d", size = 145772, upload-time = "2026-05-22T14:14:29.909Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, + { url = "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" }, ] [[package]] -name = "msgpack" -version = "1.1.1" +name = "moto" +version = "5.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boto3" }, + { name = "botocore" }, + { name = "cryptography" }, + { name = "requests" }, + { name = "responses" }, + { name = "werkzeug" }, + { name = "xmltodict" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/63/d944f387582cc53f53febbff2b3fa36a6d2ed7c1feef8990bf646cfa9cba/moto-5.2.2.tar.gz", hash = "sha256:aac8023a429e125e91c91f8f4730a67b54f518cda587352f7e67252fe3168f75", size = 8678761, upload-time = "2026-06-06T18:57:54.931Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/45/13cff46f4f617a6e97e1d497d75abd913e250bb4c823a4985668c6e593e4/moto-5.2.2-py3-none-any.whl", hash = "sha256:3817f1e39721ca833579b921e53e3b68547ace6a34d848c9486fbb5905808de9", size = 6698689, upload-time = "2026-06-06T18:57:51.435Z" }, +] + +[package.optional-dependencies] +s3 = [ + { name = "py-partiql-parser" }, + { name = "pyyaml" }, +] +server = [ + { name = "antlr4-python3-runtime" }, + { name = "aws-xray-sdk" }, + { name = "cfn-lint" }, + { name = "docker" }, + { name = "flask" }, + { name = "flask-cors" }, + { name = "graphql-core" }, + { name = "joserfc" }, + { name = "jsonpath-ng" }, + { name = "openapi-spec-validator" }, + { name = "py-partiql-parser" }, + { name = "pyparsing" }, + { name = "pyyaml" }, + { name = "setuptools" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/b1/ea4f68038a18c77c9467400d166d74c4ffa536f34761f7983a104357e614/msgpack-1.1.1.tar.gz", hash = "sha256:77b79ce34a2bdab2594f490c8e80dd62a02d650b91a75159a63ec413b8d104cd", size = 173555, upload-time = "2025-06-13T06:52:51.324Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/52/f30da112c1dc92cf64f57d08a273ac771e7b29dea10b4b30369b2d7e8546/msgpack-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:353b6fc0c36fde68b661a12949d7d49f8f51ff5fa019c1e47c87c4ff34b080ed", size = 81799, upload-time = "2025-06-13T06:51:37.228Z" }, - { url = "https://files.pythonhosted.org/packages/e4/35/7bfc0def2f04ab4145f7f108e3563f9b4abae4ab0ed78a61f350518cc4d2/msgpack-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:79c408fcf76a958491b4e3b103d1c417044544b68e96d06432a189b43d1215c8", size = 78278, upload-time = "2025-06-13T06:51:38.534Z" }, - { url = "https://files.pythonhosted.org/packages/e8/c5/df5d6c1c39856bc55f800bf82778fd4c11370667f9b9e9d51b2f5da88f20/msgpack-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78426096939c2c7482bf31ef15ca219a9e24460289c00dd0b94411040bb73ad2", size = 402805, upload-time = "2025-06-13T06:51:39.538Z" }, - { url = "https://files.pythonhosted.org/packages/20/8e/0bb8c977efecfe6ea7116e2ed73a78a8d32a947f94d272586cf02a9757db/msgpack-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b17ba27727a36cb73aabacaa44b13090feb88a01d012c0f4be70c00f75048b4", size = 408642, upload-time = "2025-06-13T06:51:41.092Z" }, - { url = "https://files.pythonhosted.org/packages/59/a1/731d52c1aeec52006be6d1f8027c49fdc2cfc3ab7cbe7c28335b2910d7b6/msgpack-1.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a17ac1ea6ec3c7687d70201cfda3b1e8061466f28f686c24f627cae4ea8efd0", size = 395143, upload-time = "2025-06-13T06:51:42.575Z" }, - { url = "https://files.pythonhosted.org/packages/2b/92/b42911c52cda2ba67a6418ffa7d08969edf2e760b09015593c8a8a27a97d/msgpack-1.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:88d1e966c9235c1d4e2afac21ca83933ba59537e2e2727a999bf3f515ca2af26", size = 395986, upload-time = "2025-06-13T06:51:43.807Z" }, - { url = "https://files.pythonhosted.org/packages/61/dc/8ae165337e70118d4dab651b8b562dd5066dd1e6dd57b038f32ebc3e2f07/msgpack-1.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f6d58656842e1b2ddbe07f43f56b10a60f2ba5826164910968f5933e5178af75", size = 402682, upload-time = "2025-06-13T06:51:45.534Z" }, - { url = "https://files.pythonhosted.org/packages/58/27/555851cb98dcbd6ce041df1eacb25ac30646575e9cd125681aa2f4b1b6f1/msgpack-1.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:96decdfc4adcbc087f5ea7ebdcfd3dee9a13358cae6e81d54be962efc38f6338", size = 406368, upload-time = "2025-06-13T06:51:46.97Z" }, - { url = "https://files.pythonhosted.org/packages/d4/64/39a26add4ce16f24e99eabb9005e44c663db00e3fce17d4ae1ae9d61df99/msgpack-1.1.1-cp310-cp310-win32.whl", hash = "sha256:6640fd979ca9a212e4bcdf6eb74051ade2c690b862b679bfcb60ae46e6dc4bfd", size = 65004, upload-time = "2025-06-13T06:51:48.582Z" }, - { url = "https://files.pythonhosted.org/packages/7d/18/73dfa3e9d5d7450d39debde5b0d848139f7de23bd637a4506e36c9800fd6/msgpack-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:8b65b53204fe1bd037c40c4148d00ef918eb2108d24c9aaa20bc31f9810ce0a8", size = 71548, upload-time = "2025-06-13T06:51:49.558Z" }, - { url = "https://files.pythonhosted.org/packages/7f/83/97f24bf9848af23fe2ba04380388216defc49a8af6da0c28cc636d722502/msgpack-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:71ef05c1726884e44f8b1d1773604ab5d4d17729d8491403a705e649116c9558", size = 82728, upload-time = "2025-06-13T06:51:50.68Z" }, - { url = "https://files.pythonhosted.org/packages/aa/7f/2eaa388267a78401f6e182662b08a588ef4f3de6f0eab1ec09736a7aaa2b/msgpack-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:36043272c6aede309d29d56851f8841ba907a1a3d04435e43e8a19928e243c1d", size = 79279, upload-time = "2025-06-13T06:51:51.72Z" }, - { url = "https://files.pythonhosted.org/packages/f8/46/31eb60f4452c96161e4dfd26dbca562b4ec68c72e4ad07d9566d7ea35e8a/msgpack-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a32747b1b39c3ac27d0670122b57e6e57f28eefb725e0b625618d1b59bf9d1e0", size = 423859, upload-time = "2025-06-13T06:51:52.749Z" }, - { url = "https://files.pythonhosted.org/packages/45/16/a20fa8c32825cc7ae8457fab45670c7a8996d7746ce80ce41cc51e3b2bd7/msgpack-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a8b10fdb84a43e50d38057b06901ec9da52baac6983d3f709d8507f3889d43f", size = 429975, upload-time = "2025-06-13T06:51:53.97Z" }, - { url = "https://files.pythonhosted.org/packages/86/ea/6c958e07692367feeb1a1594d35e22b62f7f476f3c568b002a5ea09d443d/msgpack-1.1.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba0c325c3f485dc54ec298d8b024e134acf07c10d494ffa24373bea729acf704", size = 413528, upload-time = "2025-06-13T06:51:55.507Z" }, - { url = "https://files.pythonhosted.org/packages/75/05/ac84063c5dae79722bda9f68b878dc31fc3059adb8633c79f1e82c2cd946/msgpack-1.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:88daaf7d146e48ec71212ce21109b66e06a98e5e44dca47d853cbfe171d6c8d2", size = 413338, upload-time = "2025-06-13T06:51:57.023Z" }, - { url = "https://files.pythonhosted.org/packages/69/e8/fe86b082c781d3e1c09ca0f4dacd457ede60a13119b6ce939efe2ea77b76/msgpack-1.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8b55ea20dc59b181d3f47103f113e6f28a5e1c89fd5b67b9140edb442ab67f2", size = 422658, upload-time = "2025-06-13T06:51:58.419Z" }, - { url = "https://files.pythonhosted.org/packages/3b/2b/bafc9924df52d8f3bb7c00d24e57be477f4d0f967c0a31ef5e2225e035c7/msgpack-1.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4a28e8072ae9779f20427af07f53bbb8b4aa81151054e882aee333b158da8752", size = 427124, upload-time = "2025-06-13T06:51:59.969Z" }, - { url = "https://files.pythonhosted.org/packages/a2/3b/1f717e17e53e0ed0b68fa59e9188f3f610c79d7151f0e52ff3cd8eb6b2dc/msgpack-1.1.1-cp311-cp311-win32.whl", hash = "sha256:7da8831f9a0fdb526621ba09a281fadc58ea12701bc709e7b8cbc362feabc295", size = 65016, upload-time = "2025-06-13T06:52:01.294Z" }, - { url = "https://files.pythonhosted.org/packages/48/45/9d1780768d3b249accecc5a38c725eb1e203d44a191f7b7ff1941f7df60c/msgpack-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:5fd1b58e1431008a57247d6e7cc4faa41c3607e8e7d4aaf81f7c29ea013cb458", size = 72267, upload-time = "2025-06-13T06:52:02.568Z" }, - { url = "https://files.pythonhosted.org/packages/e3/26/389b9c593eda2b8551b2e7126ad3a06af6f9b44274eb3a4f054d48ff7e47/msgpack-1.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ae497b11f4c21558d95de9f64fff7053544f4d1a17731c866143ed6bb4591238", size = 82359, upload-time = "2025-06-13T06:52:03.909Z" }, - { url = "https://files.pythonhosted.org/packages/ab/65/7d1de38c8a22cf8b1551469159d4b6cf49be2126adc2482de50976084d78/msgpack-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:33be9ab121df9b6b461ff91baac6f2731f83d9b27ed948c5b9d1978ae28bf157", size = 79172, upload-time = "2025-06-13T06:52:05.246Z" }, - { url = "https://files.pythonhosted.org/packages/0f/bd/cacf208b64d9577a62c74b677e1ada005caa9b69a05a599889d6fc2ab20a/msgpack-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f64ae8fe7ffba251fecb8408540c34ee9df1c26674c50c4544d72dbf792e5ce", size = 425013, upload-time = "2025-06-13T06:52:06.341Z" }, - { url = "https://files.pythonhosted.org/packages/4d/ec/fd869e2567cc9c01278a736cfd1697941ba0d4b81a43e0aa2e8d71dab208/msgpack-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a494554874691720ba5891c9b0b39474ba43ffb1aaf32a5dac874effb1619e1a", size = 426905, upload-time = "2025-06-13T06:52:07.501Z" }, - { url = "https://files.pythonhosted.org/packages/55/2a/35860f33229075bce803a5593d046d8b489d7ba2fc85701e714fc1aaf898/msgpack-1.1.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb643284ab0ed26f6957d969fe0dd8bb17beb567beb8998140b5e38a90974f6c", size = 407336, upload-time = "2025-06-13T06:52:09.047Z" }, - { url = "https://files.pythonhosted.org/packages/8c/16/69ed8f3ada150bf92745fb4921bd621fd2cdf5a42e25eb50bcc57a5328f0/msgpack-1.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d275a9e3c81b1093c060c3837e580c37f47c51eca031f7b5fb76f7b8470f5f9b", size = 409485, upload-time = "2025-06-13T06:52:10.382Z" }, - { url = "https://files.pythonhosted.org/packages/c6/b6/0c398039e4c6d0b2e37c61d7e0e9d13439f91f780686deb8ee64ecf1ae71/msgpack-1.1.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fd6b577e4541676e0cc9ddc1709d25014d3ad9a66caa19962c4f5de30fc09ef", size = 412182, upload-time = "2025-06-13T06:52:11.644Z" }, - { url = "https://files.pythonhosted.org/packages/b8/d0/0cf4a6ecb9bc960d624c93effaeaae75cbf00b3bc4a54f35c8507273cda1/msgpack-1.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb29aaa613c0a1c40d1af111abf025f1732cab333f96f285d6a93b934738a68a", size = 419883, upload-time = "2025-06-13T06:52:12.806Z" }, - { url = "https://files.pythonhosted.org/packages/62/83/9697c211720fa71a2dfb632cad6196a8af3abea56eece220fde4674dc44b/msgpack-1.1.1-cp312-cp312-win32.whl", hash = "sha256:870b9a626280c86cff9c576ec0d9cbcc54a1e5ebda9cd26dab12baf41fee218c", size = 65406, upload-time = "2025-06-13T06:52:14.271Z" }, - { url = "https://files.pythonhosted.org/packages/c0/23/0abb886e80eab08f5e8c485d6f13924028602829f63b8f5fa25a06636628/msgpack-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:5692095123007180dca3e788bb4c399cc26626da51629a31d40207cb262e67f4", size = 72558, upload-time = "2025-06-13T06:52:15.252Z" }, - { url = "https://files.pythonhosted.org/packages/a1/38/561f01cf3577430b59b340b51329803d3a5bf6a45864a55f4ef308ac11e3/msgpack-1.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3765afa6bd4832fc11c3749be4ba4b69a0e8d7b728f78e68120a157a4c5d41f0", size = 81677, upload-time = "2025-06-13T06:52:16.64Z" }, - { url = "https://files.pythonhosted.org/packages/09/48/54a89579ea36b6ae0ee001cba8c61f776451fad3c9306cd80f5b5c55be87/msgpack-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8ddb2bcfd1a8b9e431c8d6f4f7db0773084e107730ecf3472f1dfe9ad583f3d9", size = 78603, upload-time = "2025-06-13T06:52:17.843Z" }, - { url = "https://files.pythonhosted.org/packages/a0/60/daba2699b308e95ae792cdc2ef092a38eb5ee422f9d2fbd4101526d8a210/msgpack-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:196a736f0526a03653d829d7d4c5500a97eea3648aebfd4b6743875f28aa2af8", size = 420504, upload-time = "2025-06-13T06:52:18.982Z" }, - { url = "https://files.pythonhosted.org/packages/20/22/2ebae7ae43cd8f2debc35c631172ddf14e2a87ffcc04cf43ff9df9fff0d3/msgpack-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d592d06e3cc2f537ceeeb23d38799c6ad83255289bb84c2e5792e5a8dea268a", size = 423749, upload-time = "2025-06-13T06:52:20.211Z" }, - { url = "https://files.pythonhosted.org/packages/40/1b/54c08dd5452427e1179a40b4b607e37e2664bca1c790c60c442c8e972e47/msgpack-1.1.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4df2311b0ce24f06ba253fda361f938dfecd7b961576f9be3f3fbd60e87130ac", size = 404458, upload-time = "2025-06-13T06:52:21.429Z" }, - { url = "https://files.pythonhosted.org/packages/2e/60/6bb17e9ffb080616a51f09928fdd5cac1353c9becc6c4a8abd4e57269a16/msgpack-1.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e4141c5a32b5e37905b5940aacbc59739f036930367d7acce7a64e4dec1f5e0b", size = 405976, upload-time = "2025-06-13T06:52:22.995Z" }, - { url = "https://files.pythonhosted.org/packages/ee/97/88983e266572e8707c1f4b99c8fd04f9eb97b43f2db40e3172d87d8642db/msgpack-1.1.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b1ce7f41670c5a69e1389420436f41385b1aa2504c3b0c30620764b15dded2e7", size = 408607, upload-time = "2025-06-13T06:52:24.152Z" }, - { url = "https://files.pythonhosted.org/packages/bc/66/36c78af2efaffcc15a5a61ae0df53a1d025f2680122e2a9eb8442fed3ae4/msgpack-1.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4147151acabb9caed4e474c3344181e91ff7a388b888f1e19ea04f7e73dc7ad5", size = 424172, upload-time = "2025-06-13T06:52:25.704Z" }, - { url = "https://files.pythonhosted.org/packages/8c/87/a75eb622b555708fe0427fab96056d39d4c9892b0c784b3a721088c7ee37/msgpack-1.1.1-cp313-cp313-win32.whl", hash = "sha256:500e85823a27d6d9bba1d057c871b4210c1dd6fb01fbb764e37e4e8847376323", size = 65347, upload-time = "2025-06-13T06:52:26.846Z" }, - { url = "https://files.pythonhosted.org/packages/ca/91/7dc28d5e2a11a5ad804cf2b7f7a5fcb1eb5a4966d66a5d2b41aee6376543/msgpack-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:6d489fba546295983abd142812bda76b57e33d0b9f5d5b71c09a583285506f69", size = 72341, upload-time = "2025-06-13T06:52:27.835Z" }, - { url = "https://files.pythonhosted.org/packages/1f/bd/0792be119d7fe7dc2148689ef65c90507d82d20a204aab3b98c74a1f8684/msgpack-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5be6b6bc52fad84d010cb45433720327ce886009d862f46b26d4d154001994b", size = 81882, upload-time = "2025-06-13T06:52:39.316Z" }, - { url = "https://files.pythonhosted.org/packages/75/77/ce06c8e26a816ae8730a8e030d263c5289adcaff9f0476f9b270bdd7c5c2/msgpack-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3a89cd8c087ea67e64844287ea52888239cbd2940884eafd2dcd25754fb72232", size = 78414, upload-time = "2025-06-13T06:52:40.341Z" }, - { url = "https://files.pythonhosted.org/packages/73/27/190576c497677fb4a0d05d896b24aea6cdccd910f206aaa7b511901befed/msgpack-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d75f3807a9900a7d575d8d6674a3a47e9f227e8716256f35bc6f03fc597ffbf", size = 400927, upload-time = "2025-06-13T06:52:41.399Z" }, - { url = "https://files.pythonhosted.org/packages/ed/af/6a0aa5a06762e70726ec3c10fb966600d84a7220b52635cb0ab2dc64d32f/msgpack-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d182dac0221eb8faef2e6f44701812b467c02674a322c739355c39e94730cdbf", size = 405903, upload-time = "2025-06-13T06:52:42.699Z" }, - { url = "https://files.pythonhosted.org/packages/1e/80/3f3da358cecbbe8eb12360814bd1277d59d2608485934742a074d99894a9/msgpack-1.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b13fe0fb4aac1aa5320cd693b297fe6fdef0e7bea5518cbc2dd5299f873ae90", size = 393192, upload-time = "2025-06-13T06:52:43.986Z" }, - { url = "https://files.pythonhosted.org/packages/98/c6/3a0ec7fdebbb4f3f8f254696cd91d491c29c501dbebd86286c17e8f68cd7/msgpack-1.1.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:435807eeb1bc791ceb3247d13c79868deb22184e1fc4224808750f0d7d1affc1", size = 393851, upload-time = "2025-06-13T06:52:45.177Z" }, - { url = "https://files.pythonhosted.org/packages/39/37/df50d5f8e68514b60fbe70f6e8337ea2b32ae2be030871bcd9d1cf7d4b62/msgpack-1.1.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4835d17af722609a45e16037bb1d4d78b7bdf19d6c0128116d178956618c4e88", size = 400292, upload-time = "2025-06-13T06:52:46.381Z" }, - { url = "https://files.pythonhosted.org/packages/fc/ec/1e067292e02d2ceb4c8cb5ba222c4f7bb28730eef5676740609dc2627e0f/msgpack-1.1.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a8ef6e342c137888ebbfb233e02b8fbd689bb5b5fcc59b34711ac47ebd504478", size = 401873, upload-time = "2025-06-13T06:52:47.957Z" }, - { url = "https://files.pythonhosted.org/packages/d3/31/e8c9c6b5b58d64c9efa99c8d181fcc25f38ead357b0360379fbc8a4234ad/msgpack-1.1.1-cp39-cp39-win32.whl", hash = "sha256:61abccf9de335d9efd149e2fff97ed5974f2481b3353772e8e2dd3402ba2bd57", size = 65028, upload-time = "2025-06-13T06:52:49.166Z" }, - { url = "https://files.pythonhosted.org/packages/20/d6/cd62cded572e5e25892747a5d27850170bcd03c855e9c69c538e024de6f9/msgpack-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:40eae974c873b2992fd36424a5d9407f93e97656d999f43fca9d29f820899084", size = 71700, upload-time = "2025-06-13T06:52:50.244Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "msgpack" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/f9/c0a1c127f9049db9155afc316952ea571720dd01833ff5e4d7e8e6352dbb/msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647", size = 183960, upload-time = "2026-06-18T16:13:52.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/16/f70100614b69feb3ade7285f08c9c52d6cda0a5c03f3f5e2facd63acb211/msgpack-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c7b398c56ff125feae96c2737abfec5595f1fa0aa186df60c56040b8accb95c", size = 82926, upload-time = "2026-06-18T16:12:31.531Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3c/08ecd5cdfe4e2de43aec79062028ad0f7b2d9b1fea5430068c198ba570da/msgpack-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1548006a91aa93c5da81f3bdcebc1a0d10cea2d25969754fbe848da622b2b895", size = 82730, upload-time = "2026-06-18T16:12:32.894Z" }, + { url = "https://files.pythonhosted.org/packages/19/9f/a70c9cb1a04ecc134005149367dcfe35d167284e8f65035a1e4156ad17b5/msgpack-1.2.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dabedcd0f23559f3596428c6589c1cd8c6eaed3a0d720795b07b0225d769203", size = 400729, upload-time = "2026-06-18T16:12:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7f/5ce020168cf0439041526e95aa068c722c016aee21624e331aeabeee2e8e/msgpack-1.2.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83efa1c898e0fc5380fc0cabbf75164c52e3b5cbb45973710d75821928380c73", size = 407625, upload-time = "2026-06-18T16:12:35.239Z" }, + { url = "https://files.pythonhosted.org/packages/79/70/fb7668ce0386819303047057aef6fc1da73b584291d9cff82b821744e2ef/msgpack-1.2.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01e2dd6c9b19d333a00282330cc8a73d38d8dabc306dc5b42cd668c3ac82e833", size = 377891, upload-time = "2026-06-18T16:12:36.684Z" }, + { url = "https://files.pythonhosted.org/packages/3d/dc/9ebe654a73c3aed2e40aa6b52e3c2a02b5f53ef0085fa235a45d5b367f87/msgpack-1.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:350cb813d0af6e65d2f7ef0d729f7ff5be5a8bce03665892f43e5883d4ecc1b8", size = 391987, upload-time = "2026-06-18T16:12:37.839Z" }, + { url = "https://files.pythonhosted.org/packages/42/eb/b67cf64218a2fa25e1c671fe1d3dbb06cbeb973e71bc4b822da079862d0b/msgpack-1.2.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ee1d9ed27d0497b848923746cf762ed2e7db24f4be7eec8e5cbe8c766aa707b7", size = 374603, upload-time = "2026-06-18T16:12:39.221Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2e/9ee200cde32fd1a0101b4006202fde554c1860adfb9bf7bff31ea4c08df8/msgpack-1.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:633727297ed063441fd1cda2288865487f33ad14eeb8831afb5f0c396a62cfce", size = 405121, upload-time = "2026-06-18T16:12:40.524Z" }, + { url = "https://files.pythonhosted.org/packages/43/b6/f10117be7ca7a51e8feed699a907b8e663a8cd66e115ae6b4fb30cc7945c/msgpack-1.2.1-cp310-cp310-win32.whl", hash = "sha256:298872ecf9e61950f1c6af4ca969b859ee91783bb920ef6e6172697d0c8aad74", size = 64088, upload-time = "2026-06-18T16:12:41.762Z" }, + { url = "https://files.pythonhosted.org/packages/ba/93/89976c696fb0224662239d952c47b4d1661b34d79a332ef5584facaa8579/msgpack-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2ff164c1b0bcb740b073b99e945234d0212852fa378e44a208c425379140dbeb", size = 70113, upload-time = "2026-06-18T16:12:42.78Z" }, + { url = "https://files.pythonhosted.org/packages/f4/6b/e9b1cdc042c4458801d2545ed782a95f3d6ba8e270cce8745b8603c7f748/msgpack-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:29a3f6e9667868429d8240dfd063ea5ffdc1321c13d783aa23827a38de0dcb22", size = 82812, upload-time = "2026-06-18T16:12:45.022Z" }, + { url = "https://files.pythonhosted.org/packages/0c/3a/dd518a1bf78ed1e9ad8afe57307c079a00eafe4b3068932a27ca1ea56b4f/msgpack-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aded5bdf32609dc7987a49bbbd15a8ef096193f96dd8bbeb791de729e650acf5", size = 82739, upload-time = "2026-06-18T16:12:46.025Z" }, + { url = "https://files.pythonhosted.org/packages/70/e0/7ba9e1542bf0771a27b8b37c1316e3f95ae9d748fd765284655c476ad4ef/msgpack-1.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:146ee4e9ce80b365c6d4c47073da9da7bcec473e58194ceee5dd7620ace77e06", size = 414233, upload-time = "2026-06-18T16:12:47.029Z" }, + { url = "https://files.pythonhosted.org/packages/03/8d/671d81534ea0e2b0e8a121be100020da09eb78861fe3aa8f3ef7dcd3bed1/msgpack-1.2.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4", size = 423843, upload-time = "2026-06-18T16:12:48.19Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b6/e5c737515ed1f166664b87601b532f58cbb73d8aa6a90b99f7c2c5037e8e/msgpack-1.2.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d31c0ac0c640f877804c67cb2bc9f4e23dc2db97e96c2e67fa27d38283b41f8", size = 390772, upload-time = "2026-06-18T16:12:49.624Z" }, + { url = "https://files.pythonhosted.org/packages/a8/46/62ed8c2e87d7021eab19921594d961ef3aa3794eec76c716dc30f3bfd433/msgpack-1.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ff92d7feeaf5bc26c51495b69e2f99ed97ab79346fb6555f44be7dd2ac6503b", size = 409559, upload-time = "2026-06-18T16:12:50.936Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/59aa3887b860bbf43532835e192b1c388a17590d6068ae4f8b2bc74c906e/msgpack-1.2.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:779197a6513bab3c3632265e3d0f7cb3227e62510841a6f34f1eaa37efbb345e", size = 387838, upload-time = "2026-06-18T16:12:52.161Z" }, + { url = "https://files.pythonhosted.org/packages/09/11/f8563e471093420cf6478cb3271a0175d8402b82d879783d4035d2d03360/msgpack-1.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67f6dd22fa72a93752643f07889796d62739a13415ee630169a8ce764f86cf9f", size = 421732, upload-time = "2026-06-18T16:12:53.556Z" }, + { url = "https://files.pythonhosted.org/packages/57/cf/e673683c4c6c90c1022b24c65af4b03eda72b182a1176ef6449069d66acc/msgpack-1.2.1-cp311-cp311-win32.whl", hash = "sha256:91054a783328e0ea7954b8771095705c8d2243b814743fbaadf14552c9c52c5d", size = 64091, upload-time = "2026-06-18T16:12:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/3f/07/ca212739d179f9083bff2c7c08c24101c3555a334fadc2b876b18768a3ae/msgpack-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2eda0b7ebb1283a98d3e4492ac933c8af6aff59fd3df1c3ed024f536af4b1dc8", size = 70462, upload-time = "2026-06-18T16:12:55.898Z" }, + { url = "https://files.pythonhosted.org/packages/6d/be/6798347b425e26f35db82e69dd83c09716c856a3714e7bffc4c0860fd830/msgpack-1.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ee967f7c7e1df2890c671ff2ee51a28ded0efc95da3e507176dee881ce36c66", size = 65059, upload-time = "2026-06-18T16:12:57.053Z" }, + { url = "https://files.pythonhosted.org/packages/bc/dd/9e8cbd8f5582ca4b590336f2b91ee5662f6a6ca562b565abaf696a0f81ff/msgpack-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35", size = 83531, upload-time = "2026-06-18T16:12:58.249Z" }, + { url = "https://files.pythonhosted.org/packages/50/2e/ebdb85a8da151397a2790363676b7ed7c125924fe618e4c6d8befb0cc62c/msgpack-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c", size = 82657, upload-time = "2026-06-18T16:12:59.396Z" }, + { url = "https://files.pythonhosted.org/packages/26/aa/753ad8b007b464e1d8aa0c8e650b9c5f4f725e658fc5ac8a7635c55b7f6e/msgpack-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0", size = 410634, upload-time = "2026-06-18T16:13:00.383Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/6adabd4f6d5e686f97dd02ce7fce3fe4cf672cbac36b8f67ff4040e8ad8b/msgpack-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a", size = 419989, upload-time = "2026-06-18T16:13:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cc/85039b7b0eb168aaad7383a23c97e291a11f08351cb45a606ce865e4e3f1/msgpack-1.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6", size = 377544, upload-time = "2026-06-18T16:13:03.637Z" }, + { url = "https://files.pythonhosted.org/packages/ed/bf/35963899493b32030c85fc513b723ae66144ac70c11ebc52e889e16e3d99/msgpack-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a", size = 400842, upload-time = "2026-06-18T16:13:05.012Z" }, + { url = "https://files.pythonhosted.org/packages/a6/df/8e2ac970c8f99264cd9997d1c73df5466bc19da3301d7dc5500862a9b089/msgpack-1.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1", size = 374108, upload-time = "2026-06-18T16:13:06.232Z" }, + { url = "https://files.pythonhosted.org/packages/17/dd/fa8bd265110dfa51c20cb529f9e6d240a16fafe7e645004c6af2d01353ba/msgpack-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64", size = 414939, upload-time = "2026-06-18T16:13:07.478Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b9/8377a5ad8953fc0437c70cc98d9ae29f27fe5ac5109fbec0812085865735/msgpack-1.2.1-cp312-cp312-win32.whl", hash = "sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac", size = 64504, upload-time = "2026-06-18T16:13:08.822Z" }, + { url = "https://files.pythonhosted.org/packages/57/7f/ce1e377df7e62461fefd9eb23bfb93a4a523f40a517b377b8f844d836828/msgpack-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24", size = 71421, upload-time = "2026-06-18T16:13:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/8f/32/ebfe84c9929f08f188d56c7a2fd913406a9ddad76a634697c1c43b8112e6/msgpack-1.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07", size = 64775, upload-time = "2026-06-18T16:13:11.056Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ac/dcddcab6f6c20ecb387ca5e980371cdb3f87ff69aeca388be97eebc4c074/msgpack-1.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064", size = 83151, upload-time = "2026-06-18T16:13:12.173Z" }, + { url = "https://files.pythonhosted.org/packages/64/71/fbcfa83a1d6a9c6091942d1cfd070962244664b87427a9a49a6897b1b219/msgpack-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056", size = 82351, upload-time = "2026-06-18T16:13:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/e3/10/ddf7b06db879e8792d13934ddda09ff20bd2a583fd84c9b59aae9b0e650b/msgpack-1.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc", size = 407518, upload-time = "2026-06-18T16:13:14.233Z" }, + { url = "https://files.pythonhosted.org/packages/79/d3/36a46a8ed992b781acbc05928bd5bee3c810cb0c3563bf81a7b0c04a1a76/msgpack-1.2.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d", size = 416405, upload-time = "2026-06-18T16:13:15.435Z" }, + { url = "https://files.pythonhosted.org/packages/f9/84/e8e9598b557c0ba6ddae901a73780a4c75ac667dddf59414b1e56a42fb34/msgpack-1.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155", size = 376257, upload-time = "2026-06-18T16:13:17.022Z" }, + { url = "https://files.pythonhosted.org/packages/40/16/738fe6d875ad7e2a9429c165322a4ec088f4f273cdfae63d96a89c467961/msgpack-1.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402", size = 397469, upload-time = "2026-06-18T16:13:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/ca/be/6d5952df75a7f24f35833af764c3a6860780364cb3a0030beb8099e1b2b4/msgpack-1.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c", size = 372802, upload-time = "2026-06-18T16:13:19.685Z" }, + { url = "https://files.pythonhosted.org/packages/e1/39/e2ef7dbf0473bcb8dc7c50bf782a892d67414877b63e47fc88eb189ef5e6/msgpack-1.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6", size = 411273, upload-time = "2026-06-18T16:13:21.028Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c5/133f4512a56e983a93445c836c9d94d88f3bc2e0980ff4b9e577bd8416ce/msgpack-1.2.1-cp313-cp313-win32.whl", hash = "sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707", size = 64471, upload-time = "2026-06-18T16:13:22.293Z" }, + { url = "https://files.pythonhosted.org/packages/e2/98/577e10b055096a7dd40732358cabaf7180a20c79ed1dcdbb618e4b9deac7/msgpack-1.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9", size = 71274, upload-time = "2026-06-18T16:13:23.455Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ee/0c0048e7cfbef23c6a94791b8959ab28155232e7956de8a305b5ff588f05/msgpack-1.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a", size = 64795, upload-time = "2026-06-18T16:13:24.687Z" }, + { url = "https://files.pythonhosted.org/packages/77/58/cce442852c6b9e1639c7c8ac8fd9143121cb32dab0f308df4d1426a8eb9c/msgpack-1.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d", size = 83610, upload-time = "2026-06-18T16:13:25.724Z" }, + { url = "https://files.pythonhosted.org/packages/60/5c/15b4c7a0182f75ffa90751958ba36a9c01cafee367d49a3edc10ed140b01/msgpack-1.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7", size = 83138, upload-time = "2026-06-18T16:13:26.781Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/99e58722feaffc5f2fbcc0c8c0d1451ab9f84097f7af87291b46af2390f4/msgpack-1.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889", size = 406090, upload-time = "2026-06-18T16:13:28.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/03/8c63e8cf52958534ef688625965ab04c269a6cadd8caef16758b380a821a/msgpack-1.2.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720", size = 412106, upload-time = "2026-06-18T16:13:29.427Z" }, + { url = "https://files.pythonhosted.org/packages/63/d2/155d9e71b40e41fd934bc0c48b9b2770f22263e1ac20aad8e29fdca7be3f/msgpack-1.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190", size = 374851, upload-time = "2026-06-18T16:13:30.631Z" }, + { url = "https://files.pythonhosted.org/packages/98/48/deaf2326262a8d5ea3295ce9649912ecd3f551ba7ec8e33c665d2ba583f3/msgpack-1.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d", size = 396168, upload-time = "2026-06-18T16:13:31.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/2a/b4410f906c2ec0008f1608d3ab5143afc3ad3f4e6da0fed3ea2231d0bef4/msgpack-1.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24", size = 371959, upload-time = "2026-06-18T16:13:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/59/86/1edc67270099a528fa2093ea60fe191233cd238e4bd30cfacf7db79fc959/msgpack-1.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7", size = 408457, upload-time = "2026-06-18T16:13:34.567Z" }, + { url = "https://files.pythonhosted.org/packages/82/90/8b630fef07d8c5ab457b71ff2c217910c83d333c7a68472c186e87cc504a/msgpack-1.2.1-cp314-cp314-win32.whl", hash = "sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb", size = 65942, upload-time = "2026-06-18T16:13:36.056Z" }, + { url = "https://files.pythonhosted.org/packages/16/f1/467b81e98b24dd3885d7b1857728797b4ffc76a7a7483af4fb321a07de3c/msgpack-1.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b", size = 72627, upload-time = "2026-06-18T16:13:37.079Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1d/5d8c4c89985feb6acefb82a09e501c60392261856d2408d20bfe4f0360b1/msgpack-1.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7", size = 66908, upload-time = "2026-06-18T16:13:38.23Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ad2afb678b4de94496cd432b581759b756a92c1192d8c767edd6b132efdc/msgpack-1.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273", size = 86000, upload-time = "2026-06-18T16:13:39.44Z" }, + { url = "https://files.pythonhosted.org/packages/54/74/0b797484013128837f3b1cbb6cea019277c4de4e377dc512b4d9a0f92940/msgpack-1.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1", size = 86544, upload-time = "2026-06-18T16:13:40.447Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b4/b774d7eb95561739907fec675582f83203cf41c597a418c2589b4bfb8e9d/msgpack-1.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc", size = 427661, upload-time = "2026-06-18T16:13:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f9/3243191dc9937e00756c8bc1b0272fed8f23758e43df2a3b46f533e5090f/msgpack-1.2.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde", size = 426375, upload-time = "2026-06-18T16:13:42.936Z" }, + { url = "https://files.pythonhosted.org/packages/23/c7/1693111db9944ba4ad4b67a1e788400d78a0b6af7a6523dc7e4e58f8274b/msgpack-1.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4", size = 380495, upload-time = "2026-06-18T16:13:44.306Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2b/92f86956a0c13e8662f7e2ad630c4eb4db07497b967589bd5245e018b2c1/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d", size = 410897, upload-time = "2026-06-18T16:13:45.629Z" }, + { url = "https://files.pythonhosted.org/packages/da/ea/1479f72d200313a76fc2f823a79d1e07ed052ab7b8a0280640aa7b95de42/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355", size = 378519, upload-time = "2026-06-18T16:13:46.998Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4d/fa006060ffa1011d32bfae826fe766fe73e02982183601633b7121058ab3/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c", size = 419815, upload-time = "2026-06-18T16:13:48.205Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/aab6c946570496b78e67804721f3d5e2d62a93081b9b37df77764ef56347/msgpack-1.2.1-cp314-cp314t-win32.whl", hash = "sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1", size = 70914, upload-time = "2026-06-18T16:13:49.385Z" }, + { url = "https://files.pythonhosted.org/packages/13/0a/e608956488a2af014cfe6e3d665e090b8ee42aa14b07f8f95b8880d66b09/msgpack-1.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2", size = 77999, upload-time = "2026-06-18T16:13:50.467Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8a/27e2e57055176e366a46b85d02d68e7a5bcfbdd8474c9706375d965f24d3/msgpack-1.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107", size = 71160, upload-time = "2026-06-18T16:13:51.498Z" }, ] [[package]] name = "multidict" -version = "6.6.4" +version = "6.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/7f/0652e6ed47ab288e3756ea9c0df8b14950781184d4bd7883f4d87dd41245/multidict-6.6.4.tar.gz", hash = "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd", size = 101843, upload-time = "2025-08-11T12:08:48.217Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/6b/86f353088c1358e76fd30b0146947fddecee812703b604ee901e85cd2a80/multidict-6.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b8aa6f0bd8125ddd04a6593437bad6a7e70f300ff4180a531654aa2ab3f6d58f", size = 77054, upload-time = "2025-08-11T12:06:02.99Z" }, - { url = "https://files.pythonhosted.org/packages/19/5d/c01dc3d3788bb877bd7f5753ea6eb23c1beeca8044902a8f5bfb54430f63/multidict-6.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b9e5853bbd7264baca42ffc53391b490d65fe62849bf2c690fa3f6273dbcd0cb", size = 44914, upload-time = "2025-08-11T12:06:05.264Z" }, - { url = "https://files.pythonhosted.org/packages/46/44/964dae19ea42f7d3e166474d8205f14bb811020e28bc423d46123ddda763/multidict-6.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0af5f9dee472371e36d6ae38bde009bd8ce65ac7335f55dcc240379d7bed1495", size = 44601, upload-time = "2025-08-11T12:06:06.627Z" }, - { url = "https://files.pythonhosted.org/packages/31/20/0616348a1dfb36cb2ab33fc9521de1f27235a397bf3f59338e583afadd17/multidict-6.6.4-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:d24f351e4d759f5054b641c81e8291e5d122af0fca5c72454ff77f7cbe492de8", size = 224821, upload-time = "2025-08-11T12:06:08.06Z" }, - { url = "https://files.pythonhosted.org/packages/14/26/5d8923c69c110ff51861af05bd27ca6783011b96725d59ccae6d9daeb627/multidict-6.6.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db6a3810eec08280a172a6cd541ff4a5f6a97b161d93ec94e6c4018917deb6b7", size = 242608, upload-time = "2025-08-11T12:06:09.697Z" }, - { url = "https://files.pythonhosted.org/packages/5c/cc/e2ad3ba9459aa34fa65cf1f82a5c4a820a2ce615aacfb5143b8817f76504/multidict-6.6.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a1b20a9d56b2d81e2ff52ecc0670d583eaabaa55f402e8d16dd062373dbbe796", size = 222324, upload-time = "2025-08-11T12:06:10.905Z" }, - { url = "https://files.pythonhosted.org/packages/19/db/4ed0f65701afbc2cb0c140d2d02928bb0fe38dd044af76e58ad7c54fd21f/multidict-6.6.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8c9854df0eaa610a23494c32a6f44a3a550fb398b6b51a56e8c6b9b3689578db", size = 253234, upload-time = "2025-08-11T12:06:12.658Z" }, - { url = "https://files.pythonhosted.org/packages/94/c1/5160c9813269e39ae14b73debb907bfaaa1beee1762da8c4fb95df4764ed/multidict-6.6.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4bb7627fd7a968f41905a4d6343b0d63244a0623f006e9ed989fa2b78f4438a0", size = 251613, upload-time = "2025-08-11T12:06:13.97Z" }, - { url = "https://files.pythonhosted.org/packages/05/a9/48d1bd111fc2f8fb98b2ed7f9a115c55a9355358432a19f53c0b74d8425d/multidict-6.6.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caebafea30ed049c57c673d0b36238b1748683be2593965614d7b0e99125c877", size = 241649, upload-time = "2025-08-11T12:06:15.204Z" }, - { url = "https://files.pythonhosted.org/packages/85/2a/f7d743df0019408768af8a70d2037546a2be7b81fbb65f040d76caafd4c5/multidict-6.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ad887a8250eb47d3ab083d2f98db7f48098d13d42eb7a3b67d8a5c795f224ace", size = 239238, upload-time = "2025-08-11T12:06:16.467Z" }, - { url = "https://files.pythonhosted.org/packages/cb/b8/4f4bb13323c2d647323f7919201493cf48ebe7ded971717bfb0f1a79b6bf/multidict-6.6.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:ed8358ae7d94ffb7c397cecb62cbac9578a83ecefc1eba27b9090ee910e2efb6", size = 233517, upload-time = "2025-08-11T12:06:18.107Z" }, - { url = "https://files.pythonhosted.org/packages/33/29/4293c26029ebfbba4f574febd2ed01b6f619cfa0d2e344217d53eef34192/multidict-6.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ecab51ad2462197a4c000b6d5701fc8585b80eecb90583635d7e327b7b6923eb", size = 243122, upload-time = "2025-08-11T12:06:19.361Z" }, - { url = "https://files.pythonhosted.org/packages/20/60/a1c53628168aa22447bfde3a8730096ac28086704a0d8c590f3b63388d0c/multidict-6.6.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c5c97aa666cf70e667dfa5af945424ba1329af5dd988a437efeb3a09430389fb", size = 248992, upload-time = "2025-08-11T12:06:20.661Z" }, - { url = "https://files.pythonhosted.org/packages/a3/3b/55443a0c372f33cae5d9ec37a6a973802884fa0ab3586659b197cf8cc5e9/multidict-6.6.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9a950b7cf54099c1209f455ac5970b1ea81410f2af60ed9eb3c3f14f0bfcf987", size = 243708, upload-time = "2025-08-11T12:06:21.891Z" }, - { url = "https://files.pythonhosted.org/packages/7c/60/a18c6900086769312560b2626b18e8cca22d9e85b1186ba77f4755b11266/multidict-6.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:163c7ea522ea9365a8a57832dea7618e6cbdc3cd75f8c627663587459a4e328f", size = 237498, upload-time = "2025-08-11T12:06:23.206Z" }, - { url = "https://files.pythonhosted.org/packages/11/3d/8bdd8bcaff2951ce2affccca107a404925a2beafedd5aef0b5e4a71120a6/multidict-6.6.4-cp310-cp310-win32.whl", hash = "sha256:17d2cbbfa6ff20821396b25890f155f40c986f9cfbce5667759696d83504954f", size = 41415, upload-time = "2025-08-11T12:06:24.77Z" }, - { url = "https://files.pythonhosted.org/packages/c0/53/cab1ad80356a4cd1b685a254b680167059b433b573e53872fab245e9fc95/multidict-6.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:ce9a40fbe52e57e7edf20113a4eaddfacac0561a0879734e636aa6d4bb5e3fb0", size = 46046, upload-time = "2025-08-11T12:06:25.893Z" }, - { url = "https://files.pythonhosted.org/packages/cf/9a/874212b6f5c1c2d870d0a7adc5bb4cfe9b0624fa15cdf5cf757c0f5087ae/multidict-6.6.4-cp310-cp310-win_arm64.whl", hash = "sha256:01d0959807a451fe9fdd4da3e139cb5b77f7328baf2140feeaf233e1d777b729", size = 43147, upload-time = "2025-08-11T12:06:27.534Z" }, - { url = "https://files.pythonhosted.org/packages/6b/7f/90a7f01e2d005d6653c689039977f6856718c75c5579445effb7e60923d1/multidict-6.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c7a0e9b561e6460484318a7612e725df1145d46b0ef57c6b9866441bf6e27e0c", size = 76472, upload-time = "2025-08-11T12:06:29.006Z" }, - { url = "https://files.pythonhosted.org/packages/54/a3/bed07bc9e2bb302ce752f1dabc69e884cd6a676da44fb0e501b246031fdd/multidict-6.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6bf2f10f70acc7a2446965ffbc726e5fc0b272c97a90b485857e5c70022213eb", size = 44634, upload-time = "2025-08-11T12:06:30.374Z" }, - { url = "https://files.pythonhosted.org/packages/a7/4b/ceeb4f8f33cf81277da464307afeaf164fb0297947642585884f5cad4f28/multidict-6.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66247d72ed62d5dd29752ffc1d3b88f135c6a8de8b5f63b7c14e973ef5bda19e", size = 44282, upload-time = "2025-08-11T12:06:31.958Z" }, - { url = "https://files.pythonhosted.org/packages/03/35/436a5da8702b06866189b69f655ffdb8f70796252a8772a77815f1812679/multidict-6.6.4-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:105245cc6b76f51e408451a844a54e6823bbd5a490ebfe5bdfc79798511ceded", size = 229696, upload-time = "2025-08-11T12:06:33.087Z" }, - { url = "https://files.pythonhosted.org/packages/b6/0e/915160be8fecf1fca35f790c08fb74ca684d752fcba62c11daaf3d92c216/multidict-6.6.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cbbc54e58b34c3bae389ef00046be0961f30fef7cb0dd9c7756aee376a4f7683", size = 246665, upload-time = "2025-08-11T12:06:34.448Z" }, - { url = "https://files.pythonhosted.org/packages/08/ee/2f464330acd83f77dcc346f0b1a0eaae10230291450887f96b204b8ac4d3/multidict-6.6.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:56c6b3652f945c9bc3ac6c8178cd93132b8d82dd581fcbc3a00676c51302bc1a", size = 225485, upload-time = "2025-08-11T12:06:35.672Z" }, - { url = "https://files.pythonhosted.org/packages/71/cc/9a117f828b4d7fbaec6adeed2204f211e9caf0a012692a1ee32169f846ae/multidict-6.6.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b95494daf857602eccf4c18ca33337dd2be705bccdb6dddbfc9d513e6addb9d9", size = 257318, upload-time = "2025-08-11T12:06:36.98Z" }, - { url = "https://files.pythonhosted.org/packages/25/77/62752d3dbd70e27fdd68e86626c1ae6bccfebe2bb1f84ae226363e112f5a/multidict-6.6.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e5b1413361cef15340ab9dc61523e653d25723e82d488ef7d60a12878227ed50", size = 254689, upload-time = "2025-08-11T12:06:38.233Z" }, - { url = "https://files.pythonhosted.org/packages/00/6e/fac58b1072a6fc59af5e7acb245e8754d3e1f97f4f808a6559951f72a0d4/multidict-6.6.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e167bf899c3d724f9662ef00b4f7fef87a19c22b2fead198a6f68b263618df52", size = 246709, upload-time = "2025-08-11T12:06:39.517Z" }, - { url = "https://files.pythonhosted.org/packages/01/ef/4698d6842ef5e797c6db7744b0081e36fb5de3d00002cc4c58071097fac3/multidict-6.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aaea28ba20a9026dfa77f4b80369e51cb767c61e33a2d4043399c67bd95fb7c6", size = 243185, upload-time = "2025-08-11T12:06:40.796Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c9/d82e95ae1d6e4ef396934e9b0e942dfc428775f9554acf04393cce66b157/multidict-6.6.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8c91cdb30809a96d9ecf442ec9bc45e8cfaa0f7f8bdf534e082c2443a196727e", size = 237838, upload-time = "2025-08-11T12:06:42.595Z" }, - { url = "https://files.pythonhosted.org/packages/57/cf/f94af5c36baaa75d44fab9f02e2a6bcfa0cd90acb44d4976a80960759dbc/multidict-6.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1a0ccbfe93ca114c5d65a2471d52d8829e56d467c97b0e341cf5ee45410033b3", size = 246368, upload-time = "2025-08-11T12:06:44.304Z" }, - { url = "https://files.pythonhosted.org/packages/4a/fe/29f23460c3d995f6a4b678cb2e9730e7277231b981f0b234702f0177818a/multidict-6.6.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:55624b3f321d84c403cb7d8e6e982f41ae233d85f85db54ba6286f7295dc8a9c", size = 253339, upload-time = "2025-08-11T12:06:45.597Z" }, - { url = "https://files.pythonhosted.org/packages/29/b6/fd59449204426187b82bf8a75f629310f68c6adc9559dc922d5abe34797b/multidict-6.6.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4a1fb393a2c9d202cb766c76208bd7945bc194eba8ac920ce98c6e458f0b524b", size = 246933, upload-time = "2025-08-11T12:06:46.841Z" }, - { url = "https://files.pythonhosted.org/packages/19/52/d5d6b344f176a5ac3606f7a61fb44dc746e04550e1a13834dff722b8d7d6/multidict-6.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:43868297a5759a845fa3a483fb4392973a95fb1de891605a3728130c52b8f40f", size = 242225, upload-time = "2025-08-11T12:06:48.588Z" }, - { url = "https://files.pythonhosted.org/packages/ec/d3/5b2281ed89ff4d5318d82478a2a2450fcdfc3300da48ff15c1778280ad26/multidict-6.6.4-cp311-cp311-win32.whl", hash = "sha256:ed3b94c5e362a8a84d69642dbeac615452e8af9b8eb825b7bc9f31a53a1051e2", size = 41306, upload-time = "2025-08-11T12:06:49.95Z" }, - { url = "https://files.pythonhosted.org/packages/74/7d/36b045c23a1ab98507aefd44fd8b264ee1dd5e5010543c6fccf82141ccef/multidict-6.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:d8c112f7a90d8ca5d20213aa41eac690bb50a76da153e3afb3886418e61cb22e", size = 46029, upload-time = "2025-08-11T12:06:51.082Z" }, - { url = "https://files.pythonhosted.org/packages/0f/5e/553d67d24432c5cd52b49047f2d248821843743ee6d29a704594f656d182/multidict-6.6.4-cp311-cp311-win_arm64.whl", hash = "sha256:3bb0eae408fa1996d87247ca0d6a57b7fc1dcf83e8a5c47ab82c558c250d4adf", size = 43017, upload-time = "2025-08-11T12:06:52.243Z" }, - { url = "https://files.pythonhosted.org/packages/05/f6/512ffd8fd8b37fb2680e5ac35d788f1d71bbaf37789d21a820bdc441e565/multidict-6.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0ffb87be160942d56d7b87b0fdf098e81ed565add09eaa1294268c7f3caac4c8", size = 76516, upload-time = "2025-08-11T12:06:53.393Z" }, - { url = "https://files.pythonhosted.org/packages/99/58/45c3e75deb8855c36bd66cc1658007589662ba584dbf423d01df478dd1c5/multidict-6.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d191de6cbab2aff5de6c5723101705fd044b3e4c7cfd587a1929b5028b9714b3", size = 45394, upload-time = "2025-08-11T12:06:54.555Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ca/e8c4472a93a26e4507c0b8e1f0762c0d8a32de1328ef72fd704ef9cc5447/multidict-6.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38a0956dd92d918ad5feff3db8fcb4a5eb7dba114da917e1a88475619781b57b", size = 43591, upload-time = "2025-08-11T12:06:55.672Z" }, - { url = "https://files.pythonhosted.org/packages/05/51/edf414f4df058574a7265034d04c935aa84a89e79ce90fcf4df211f47b16/multidict-6.6.4-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6865f6d3b7900ae020b495d599fcf3765653bc927951c1abb959017f81ae8287", size = 237215, upload-time = "2025-08-11T12:06:57.213Z" }, - { url = "https://files.pythonhosted.org/packages/c8/45/8b3d6dbad8cf3252553cc41abea09ad527b33ce47a5e199072620b296902/multidict-6.6.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a2088c126b6f72db6c9212ad827d0ba088c01d951cee25e758c450da732c138", size = 258299, upload-time = "2025-08-11T12:06:58.946Z" }, - { url = "https://files.pythonhosted.org/packages/3c/e8/8ca2e9a9f5a435fc6db40438a55730a4bf4956b554e487fa1b9ae920f825/multidict-6.6.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0f37bed7319b848097085d7d48116f545985db988e2256b2e6f00563a3416ee6", size = 242357, upload-time = "2025-08-11T12:07:00.301Z" }, - { url = "https://files.pythonhosted.org/packages/0f/84/80c77c99df05a75c28490b2af8f7cba2a12621186e0a8b0865d8e745c104/multidict-6.6.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:01368e3c94032ba6ca0b78e7ccb099643466cf24f8dc8eefcfdc0571d56e58f9", size = 268369, upload-time = "2025-08-11T12:07:01.638Z" }, - { url = "https://files.pythonhosted.org/packages/0d/e9/920bfa46c27b05fb3e1ad85121fd49f441492dca2449c5bcfe42e4565d8a/multidict-6.6.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe323540c255db0bffee79ad7f048c909f2ab0edb87a597e1c17da6a54e493c", size = 269341, upload-time = "2025-08-11T12:07:02.943Z" }, - { url = "https://files.pythonhosted.org/packages/af/65/753a2d8b05daf496f4a9c367fe844e90a1b2cac78e2be2c844200d10cc4c/multidict-6.6.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8eb3025f17b0a4c3cd08cda49acf312a19ad6e8a4edd9dbd591e6506d999402", size = 256100, upload-time = "2025-08-11T12:07:04.564Z" }, - { url = "https://files.pythonhosted.org/packages/09/54/655be13ae324212bf0bc15d665a4e34844f34c206f78801be42f7a0a8aaa/multidict-6.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbc14f0365534d35a06970d6a83478b249752e922d662dc24d489af1aa0d1be7", size = 253584, upload-time = "2025-08-11T12:07:05.914Z" }, - { url = "https://files.pythonhosted.org/packages/5c/74/ab2039ecc05264b5cec73eb018ce417af3ebb384ae9c0e9ed42cb33f8151/multidict-6.6.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:75aa52fba2d96bf972e85451b99d8e19cc37ce26fd016f6d4aa60da9ab2b005f", size = 251018, upload-time = "2025-08-11T12:07:08.301Z" }, - { url = "https://files.pythonhosted.org/packages/af/0a/ccbb244ac848e56c6427f2392741c06302bbfba49c0042f1eb3c5b606497/multidict-6.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fefd4a815e362d4f011919d97d7b4a1e566f1dde83dc4ad8cfb5b41de1df68d", size = 251477, upload-time = "2025-08-11T12:07:10.248Z" }, - { url = "https://files.pythonhosted.org/packages/0e/b0/0ed49bba775b135937f52fe13922bc64a7eaf0a3ead84a36e8e4e446e096/multidict-6.6.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:db9801fe021f59a5b375ab778973127ca0ac52429a26e2fd86aa9508f4d26eb7", size = 263575, upload-time = "2025-08-11T12:07:11.928Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d9/7fb85a85e14de2e44dfb6a24f03c41e2af8697a6df83daddb0e9b7569f73/multidict-6.6.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a650629970fa21ac1fb06ba25dabfc5b8a2054fcbf6ae97c758aa956b8dba802", size = 259649, upload-time = "2025-08-11T12:07:13.244Z" }, - { url = "https://files.pythonhosted.org/packages/03/9e/b3a459bcf9b6e74fa461a5222a10ff9b544cb1cd52fd482fb1b75ecda2a2/multidict-6.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:452ff5da78d4720d7516a3a2abd804957532dd69296cb77319c193e3ffb87e24", size = 251505, upload-time = "2025-08-11T12:07:14.57Z" }, - { url = "https://files.pythonhosted.org/packages/86/a2/8022f78f041dfe6d71e364001a5cf987c30edfc83c8a5fb7a3f0974cff39/multidict-6.6.4-cp312-cp312-win32.whl", hash = "sha256:8c2fcb12136530ed19572bbba61b407f655e3953ba669b96a35036a11a485793", size = 41888, upload-time = "2025-08-11T12:07:15.904Z" }, - { url = "https://files.pythonhosted.org/packages/c7/eb/d88b1780d43a56db2cba24289fa744a9d216c1a8546a0dc3956563fd53ea/multidict-6.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:047d9425860a8c9544fed1b9584f0c8bcd31bcde9568b047c5e567a1025ecd6e", size = 46072, upload-time = "2025-08-11T12:07:17.045Z" }, - { url = "https://files.pythonhosted.org/packages/9f/16/b929320bf5750e2d9d4931835a4c638a19d2494a5b519caaaa7492ebe105/multidict-6.6.4-cp312-cp312-win_arm64.whl", hash = "sha256:14754eb72feaa1e8ae528468f24250dd997b8e2188c3d2f593f9eba259e4b364", size = 43222, upload-time = "2025-08-11T12:07:18.328Z" }, - { url = "https://files.pythonhosted.org/packages/3a/5d/e1db626f64f60008320aab00fbe4f23fc3300d75892a3381275b3d284580/multidict-6.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f46a6e8597f9bd71b31cc708195d42b634c8527fecbcf93febf1052cacc1f16e", size = 75848, upload-time = "2025-08-11T12:07:19.912Z" }, - { url = "https://files.pythonhosted.org/packages/4c/aa/8b6f548d839b6c13887253af4e29c939af22a18591bfb5d0ee6f1931dae8/multidict-6.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:22e38b2bc176c5eb9c0a0e379f9d188ae4cd8b28c0f53b52bce7ab0a9e534657", size = 45060, upload-time = "2025-08-11T12:07:21.163Z" }, - { url = "https://files.pythonhosted.org/packages/eb/c6/f5e97e5d99a729bc2aa58eb3ebfa9f1e56a9b517cc38c60537c81834a73f/multidict-6.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5df8afd26f162da59e218ac0eefaa01b01b2e6cd606cffa46608f699539246da", size = 43269, upload-time = "2025-08-11T12:07:22.392Z" }, - { url = "https://files.pythonhosted.org/packages/dc/31/d54eb0c62516776f36fe67f84a732f97e0b0e12f98d5685bebcc6d396910/multidict-6.6.4-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:49517449b58d043023720aa58e62b2f74ce9b28f740a0b5d33971149553d72aa", size = 237158, upload-time = "2025-08-11T12:07:23.636Z" }, - { url = "https://files.pythonhosted.org/packages/c4/1c/8a10c1c25b23156e63b12165a929d8eb49a6ed769fdbefb06e6f07c1e50d/multidict-6.6.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9408439537c5afdca05edd128a63f56a62680f4b3c234301055d7a2000220f", size = 257076, upload-time = "2025-08-11T12:07:25.049Z" }, - { url = "https://files.pythonhosted.org/packages/ad/86/90e20b5771d6805a119e483fd3d1e8393e745a11511aebca41f0da38c3e2/multidict-6.6.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:87a32d20759dc52a9e850fe1061b6e41ab28e2998d44168a8a341b99ded1dba0", size = 240694, upload-time = "2025-08-11T12:07:26.458Z" }, - { url = "https://files.pythonhosted.org/packages/e7/49/484d3e6b535bc0555b52a0a26ba86e4d8d03fd5587d4936dc59ba7583221/multidict-6.6.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52e3c8d43cdfff587ceedce9deb25e6ae77daba560b626e97a56ddcad3756879", size = 266350, upload-time = "2025-08-11T12:07:27.94Z" }, - { url = "https://files.pythonhosted.org/packages/bf/b4/aa4c5c379b11895083d50021e229e90c408d7d875471cb3abf721e4670d6/multidict-6.6.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ad8850921d3a8d8ff6fbef790e773cecfc260bbfa0566998980d3fa8f520bc4a", size = 267250, upload-time = "2025-08-11T12:07:29.303Z" }, - { url = "https://files.pythonhosted.org/packages/80/e5/5e22c5bf96a64bdd43518b1834c6d95a4922cc2066b7d8e467dae9b6cee6/multidict-6.6.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:497a2954adc25c08daff36f795077f63ad33e13f19bfff7736e72c785391534f", size = 254900, upload-time = "2025-08-11T12:07:30.764Z" }, - { url = "https://files.pythonhosted.org/packages/17/38/58b27fed927c07035abc02befacab42491e7388ca105e087e6e0215ead64/multidict-6.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:024ce601f92d780ca1617ad4be5ac15b501cc2414970ffa2bb2bbc2bd5a68fa5", size = 252355, upload-time = "2025-08-11T12:07:32.205Z" }, - { url = "https://files.pythonhosted.org/packages/d0/a1/dad75d23a90c29c02b5d6f3d7c10ab36c3197613be5d07ec49c7791e186c/multidict-6.6.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a693fc5ed9bdd1c9e898013e0da4dcc640de7963a371c0bd458e50e046bf6438", size = 250061, upload-time = "2025-08-11T12:07:33.623Z" }, - { url = "https://files.pythonhosted.org/packages/b8/1a/ac2216b61c7f116edab6dc3378cca6c70dc019c9a457ff0d754067c58b20/multidict-6.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:190766dac95aab54cae5b152a56520fd99298f32a1266d66d27fdd1b5ac00f4e", size = 249675, upload-time = "2025-08-11T12:07:34.958Z" }, - { url = "https://files.pythonhosted.org/packages/d4/79/1916af833b800d13883e452e8e0977c065c4ee3ab7a26941fbfdebc11895/multidict-6.6.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:34d8f2a5ffdceab9dcd97c7a016deb2308531d5f0fced2bb0c9e1df45b3363d7", size = 261247, upload-time = "2025-08-11T12:07:36.588Z" }, - { url = "https://files.pythonhosted.org/packages/c5/65/d1f84fe08ac44a5fc7391cbc20a7cedc433ea616b266284413fd86062f8c/multidict-6.6.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:59e8d40ab1f5a8597abcef00d04845155a5693b5da00d2c93dbe88f2050f2812", size = 257960, upload-time = "2025-08-11T12:07:39.735Z" }, - { url = "https://files.pythonhosted.org/packages/13/b5/29ec78057d377b195ac2c5248c773703a6b602e132a763e20ec0457e7440/multidict-6.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:467fe64138cfac771f0e949b938c2e1ada2b5af22f39692aa9258715e9ea613a", size = 250078, upload-time = "2025-08-11T12:07:41.525Z" }, - { url = "https://files.pythonhosted.org/packages/c4/0e/7e79d38f70a872cae32e29b0d77024bef7834b0afb406ddae6558d9e2414/multidict-6.6.4-cp313-cp313-win32.whl", hash = "sha256:14616a30fe6d0a48d0a48d1a633ab3b8bec4cf293aac65f32ed116f620adfd69", size = 41708, upload-time = "2025-08-11T12:07:43.405Z" }, - { url = "https://files.pythonhosted.org/packages/9d/34/746696dffff742e97cd6a23da953e55d0ea51fa601fa2ff387b3edcfaa2c/multidict-6.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:40cd05eaeb39e2bc8939451f033e57feaa2ac99e07dbca8afe2be450a4a3b6cf", size = 45912, upload-time = "2025-08-11T12:07:45.082Z" }, - { url = "https://files.pythonhosted.org/packages/c7/87/3bac136181e271e29170d8d71929cdeddeb77f3e8b6a0c08da3a8e9da114/multidict-6.6.4-cp313-cp313-win_arm64.whl", hash = "sha256:f6eb37d511bfae9e13e82cb4d1af36b91150466f24d9b2b8a9785816deb16605", size = 43076, upload-time = "2025-08-11T12:07:46.746Z" }, - { url = "https://files.pythonhosted.org/packages/64/94/0a8e63e36c049b571c9ae41ee301ada29c3fee9643d9c2548d7d558a1d99/multidict-6.6.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6c84378acd4f37d1b507dfa0d459b449e2321b3ba5f2338f9b085cf7a7ba95eb", size = 82812, upload-time = "2025-08-11T12:07:48.402Z" }, - { url = "https://files.pythonhosted.org/packages/25/1a/be8e369dfcd260d2070a67e65dd3990dd635cbd735b98da31e00ea84cd4e/multidict-6.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0e0558693063c75f3d952abf645c78f3c5dfdd825a41d8c4d8156fc0b0da6e7e", size = 48313, upload-time = "2025-08-11T12:07:49.679Z" }, - { url = "https://files.pythonhosted.org/packages/26/5a/dd4ade298674b2f9a7b06a32c94ffbc0497354df8285f27317c66433ce3b/multidict-6.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3f8e2384cb83ebd23fd07e9eada8ba64afc4c759cd94817433ab8c81ee4b403f", size = 46777, upload-time = "2025-08-11T12:07:51.318Z" }, - { url = "https://files.pythonhosted.org/packages/89/db/98aa28bc7e071bfba611ac2ae803c24e96dd3a452b4118c587d3d872c64c/multidict-6.6.4-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f996b87b420995a9174b2a7c1a8daf7db4750be6848b03eb5e639674f7963773", size = 229321, upload-time = "2025-08-11T12:07:52.965Z" }, - { url = "https://files.pythonhosted.org/packages/c7/bc/01ddda2a73dd9d167bd85d0e8ef4293836a8f82b786c63fb1a429bc3e678/multidict-6.6.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc356250cffd6e78416cf5b40dc6a74f1edf3be8e834cf8862d9ed5265cf9b0e", size = 249954, upload-time = "2025-08-11T12:07:54.423Z" }, - { url = "https://files.pythonhosted.org/packages/06/78/6b7c0f020f9aa0acf66d0ab4eb9f08375bac9a50ff5e3edb1c4ccd59eafc/multidict-6.6.4-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:dadf95aa862714ea468a49ad1e09fe00fcc9ec67d122f6596a8d40caf6cec7d0", size = 228612, upload-time = "2025-08-11T12:07:55.914Z" }, - { url = "https://files.pythonhosted.org/packages/00/44/3faa416f89b2d5d76e9d447296a81521e1c832ad6e40b92f990697b43192/multidict-6.6.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dd57515bebffd8ebd714d101d4c434063322e4fe24042e90ced41f18b6d3395", size = 257528, upload-time = "2025-08-11T12:07:57.371Z" }, - { url = "https://files.pythonhosted.org/packages/05/5f/77c03b89af0fcb16f018f668207768191fb9dcfb5e3361a5e706a11db2c9/multidict-6.6.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:967af5f238ebc2eb1da4e77af5492219fbd9b4b812347da39a7b5f5c72c0fa45", size = 256329, upload-time = "2025-08-11T12:07:58.844Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e9/ed750a2a9afb4f8dc6f13dc5b67b514832101b95714f1211cd42e0aafc26/multidict-6.6.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a4c6875c37aae9794308ec43e3530e4aa0d36579ce38d89979bbf89582002bb", size = 247928, upload-time = "2025-08-11T12:08:01.037Z" }, - { url = "https://files.pythonhosted.org/packages/1f/b5/e0571bc13cda277db7e6e8a532791d4403dacc9850006cb66d2556e649c0/multidict-6.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f683a551e92bdb7fac545b9c6f9fa2aebdeefa61d607510b3533286fcab67f5", size = 245228, upload-time = "2025-08-11T12:08:02.96Z" }, - { url = "https://files.pythonhosted.org/packages/f3/a3/69a84b0eccb9824491f06368f5b86e72e4af54c3067c37c39099b6687109/multidict-6.6.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:3ba5aaf600edaf2a868a391779f7a85d93bed147854925f34edd24cc70a3e141", size = 235869, upload-time = "2025-08-11T12:08:04.746Z" }, - { url = "https://files.pythonhosted.org/packages/a9/9d/28802e8f9121a6a0804fa009debf4e753d0a59969ea9f70be5f5fdfcb18f/multidict-6.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:580b643b7fd2c295d83cad90d78419081f53fd532d1f1eb67ceb7060f61cff0d", size = 243446, upload-time = "2025-08-11T12:08:06.332Z" }, - { url = "https://files.pythonhosted.org/packages/38/ea/6c98add069b4878c1d66428a5f5149ddb6d32b1f9836a826ac764b9940be/multidict-6.6.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:37b7187197da6af3ee0b044dbc9625afd0c885f2800815b228a0e70f9a7f473d", size = 252299, upload-time = "2025-08-11T12:08:07.931Z" }, - { url = "https://files.pythonhosted.org/packages/3a/09/8fe02d204473e14c0af3affd50af9078839dfca1742f025cca765435d6b4/multidict-6.6.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e1b93790ed0bc26feb72e2f08299691ceb6da5e9e14a0d13cc74f1869af327a0", size = 246926, upload-time = "2025-08-11T12:08:09.467Z" }, - { url = "https://files.pythonhosted.org/packages/37/3d/7b1e10d774a6df5175ecd3c92bff069e77bed9ec2a927fdd4ff5fe182f67/multidict-6.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a506a77ddee1efcca81ecbeae27ade3e09cdf21a8ae854d766c2bb4f14053f92", size = 243383, upload-time = "2025-08-11T12:08:10.981Z" }, - { url = "https://files.pythonhosted.org/packages/50/b0/a6fae46071b645ae98786ab738447de1ef53742eaad949f27e960864bb49/multidict-6.6.4-cp313-cp313t-win32.whl", hash = "sha256:f93b2b2279883d1d0a9e1bd01f312d6fc315c5e4c1f09e112e4736e2f650bc4e", size = 47775, upload-time = "2025-08-11T12:08:12.439Z" }, - { url = "https://files.pythonhosted.org/packages/b2/0a/2436550b1520091af0600dff547913cb2d66fbac27a8c33bc1b1bccd8d98/multidict-6.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:6d46a180acdf6e87cc41dc15d8f5c2986e1e8739dc25dbb7dac826731ef381a4", size = 53100, upload-time = "2025-08-11T12:08:13.823Z" }, - { url = "https://files.pythonhosted.org/packages/97/ea/43ac51faff934086db9c072a94d327d71b7d8b40cd5dcb47311330929ef0/multidict-6.6.4-cp313-cp313t-win_arm64.whl", hash = "sha256:756989334015e3335d087a27331659820d53ba432befdef6a718398b0a8493ad", size = 45501, upload-time = "2025-08-11T12:08:15.173Z" }, - { url = "https://files.pythonhosted.org/packages/d4/d3/f04c5db316caee9b5b2cbba66270b358c922a959855995bedde87134287c/multidict-6.6.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:af7618b591bae552b40dbb6f93f5518328a949dac626ee75927bba1ecdeea9f4", size = 76977, upload-time = "2025-08-11T12:08:16.667Z" }, - { url = "https://files.pythonhosted.org/packages/70/39/a6200417d883e510728ab3caec02d3b66ff09e1c85e0aab2ba311abfdf06/multidict-6.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b6819f83aef06f560cb15482d619d0e623ce9bf155115150a85ab11b8342a665", size = 44878, upload-time = "2025-08-11T12:08:18.157Z" }, - { url = "https://files.pythonhosted.org/packages/6f/7e/815be31ed35571b137d65232816f61513fcd97b2717d6a9d7800b5a0c6e0/multidict-6.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4d09384e75788861e046330308e7af54dd306aaf20eb760eb1d0de26b2bea2cb", size = 44546, upload-time = "2025-08-11T12:08:19.694Z" }, - { url = "https://files.pythonhosted.org/packages/e2/f1/21b5bff6a8c3e2aff56956c241941ace6b8820e1abe6b12d3c52868a773d/multidict-6.6.4-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a59c63061f1a07b861c004e53869eb1211ffd1a4acbca330e3322efa6dd02978", size = 223020, upload-time = "2025-08-11T12:08:21.554Z" }, - { url = "https://files.pythonhosted.org/packages/15/59/37083f1dd3439979a0ffeb1906818d978d88b4cc7f4600a9f89b1cb6713c/multidict-6.6.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:350f6b0fe1ced61e778037fdc7613f4051c8baf64b1ee19371b42a3acdb016a0", size = 240528, upload-time = "2025-08-11T12:08:23.45Z" }, - { url = "https://files.pythonhosted.org/packages/d1/f0/f054d123c87784307a27324c829eb55bcfd2e261eb785fcabbd832c8dc4a/multidict-6.6.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0c5cbac6b55ad69cb6aa17ee9343dfbba903118fd530348c330211dc7aa756d1", size = 219540, upload-time = "2025-08-11T12:08:24.965Z" }, - { url = "https://files.pythonhosted.org/packages/e8/26/8f78ce17b7118149c17f238f28fba2a850b660b860f9b024a34d0191030f/multidict-6.6.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:630f70c32b8066ddfd920350bc236225814ad94dfa493fe1910ee17fe4365cbb", size = 251182, upload-time = "2025-08-11T12:08:26.511Z" }, - { url = "https://files.pythonhosted.org/packages/00/c3/a21466322d69f6594fe22d9379200f99194d21c12a5bbf8c2a39a46b83b6/multidict-6.6.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8d4916a81697faec6cb724a273bd5457e4c6c43d82b29f9dc02c5542fd21fc9", size = 249371, upload-time = "2025-08-11T12:08:28.075Z" }, - { url = "https://files.pythonhosted.org/packages/c2/8e/2e673124eb05cf8dc82e9265eccde01a36bcbd3193e27799b8377123c976/multidict-6.6.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e42332cf8276bb7645d310cdecca93a16920256a5b01bebf747365f86a1675b", size = 239235, upload-time = "2025-08-11T12:08:29.937Z" }, - { url = "https://files.pythonhosted.org/packages/2b/2d/bdd9f05e7c89e30a4b0e4faf0681a30748f8d1310f68cfdc0e3571e75bd5/multidict-6.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f3be27440f7644ab9a13a6fc86f09cdd90b347c3c5e30c6d6d860de822d7cb53", size = 237410, upload-time = "2025-08-11T12:08:31.872Z" }, - { url = "https://files.pythonhosted.org/packages/46/4c/3237b83f8ca9a2673bb08fc340c15da005a80f5cc49748b587c8ae83823b/multidict-6.6.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:21f216669109e02ef3e2415ede07f4f8987f00de8cdfa0cc0b3440d42534f9f0", size = 232979, upload-time = "2025-08-11T12:08:33.399Z" }, - { url = "https://files.pythonhosted.org/packages/55/a6/a765decff625ae9bc581aed303cd1837955177dafc558859a69f56f56ba8/multidict-6.6.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:d9890d68c45d1aeac5178ded1d1cccf3bc8d7accf1f976f79bf63099fb16e4bd", size = 240979, upload-time = "2025-08-11T12:08:35.02Z" }, - { url = "https://files.pythonhosted.org/packages/6b/2d/9c75975cb0c66ea33cae1443bb265b2b3cd689bffcbc68872565f401da23/multidict-6.6.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:edfdcae97cdc5d1a89477c436b61f472c4d40971774ac4729c613b4b133163cb", size = 246849, upload-time = "2025-08-11T12:08:37.038Z" }, - { url = "https://files.pythonhosted.org/packages/3e/71/d21ac0843c1d8751fb5dcf8a1f436625d39d4577bc27829799d09b419af7/multidict-6.6.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:0b2e886624be5773e69cf32bcb8534aecdeb38943520b240fed3d5596a430f2f", size = 241798, upload-time = "2025-08-11T12:08:38.669Z" }, - { url = "https://files.pythonhosted.org/packages/94/3d/1d8911e53092837bd11b1c99d71de3e2a9a26f8911f864554677663242aa/multidict-6.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:be5bf4b3224948032a845d12ab0f69f208293742df96dc14c4ff9b09e508fc17", size = 235315, upload-time = "2025-08-11T12:08:40.266Z" }, - { url = "https://files.pythonhosted.org/packages/86/c5/4b758df96376f73e936b1942c6c2dfc17e37ed9d5ff3b01a811496966ca0/multidict-6.6.4-cp39-cp39-win32.whl", hash = "sha256:10a68a9191f284fe9d501fef4efe93226e74df92ce7a24e301371293bd4918ae", size = 41434, upload-time = "2025-08-11T12:08:41.965Z" }, - { url = "https://files.pythonhosted.org/packages/58/16/f1dfa2a0f25f2717a5e9e5fe8fd30613f7fe95e3530cec8d11f5de0b709c/multidict-6.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:ee25f82f53262f9ac93bd7e58e47ea1bdcc3393cef815847e397cba17e284210", size = 46186, upload-time = "2025-08-11T12:08:43.367Z" }, - { url = "https://files.pythonhosted.org/packages/88/7d/a0568bac65438c494cb6950b29f394d875a796a237536ac724879cf710c9/multidict-6.6.4-cp39-cp39-win_arm64.whl", hash = "sha256:f9867e55590e0855bcec60d4f9a092b69476db64573c9fe17e92b0c50614c16a", size = 43115, upload-time = "2025-08-11T12:08:45.126Z" }, - { url = "https://files.pythonhosted.org/packages/fd/69/b547032297c7e63ba2af494edba695d781af8a0c6e89e4d06cf848b21d80/multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", size = 12313, upload-time = "2025-08-11T12:08:46.891Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/0b/19348d4c98980c4851d2f943f8ebafdece2ae7ef737adcfa5994ce8e5f10/multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5", size = 77176, upload-time = "2026-01-26T02:42:59.784Z" }, + { url = "https://files.pythonhosted.org/packages/ef/04/9de3f8077852e3d438215c81e9b691244532d2e05b4270e89ce67b7d103c/multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8", size = 44996, upload-time = "2026-01-26T02:43:01.674Z" }, + { url = "https://files.pythonhosted.org/packages/31/5c/08c7f7fe311f32e83f7621cd3f99d805f45519cd06fafb247628b861da7d/multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872", size = 44631, upload-time = "2026-01-26T02:43:03.169Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7f/0e3b1390ae772f27501199996b94b52ceeb64fe6f9120a32c6c3f6b781be/multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991", size = 242561, upload-time = "2026-01-26T02:43:04.733Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f4/8719f4f167586af317b69dd3e90f913416c91ca610cac79a45c53f590312/multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03", size = 242223, upload-time = "2026-01-26T02:43:06.695Z" }, + { url = "https://files.pythonhosted.org/packages/47/ab/7c36164cce64a6ad19c6d9a85377b7178ecf3b89f8fd589c73381a5eedfd/multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981", size = 222322, upload-time = "2026-01-26T02:43:08.472Z" }, + { url = "https://files.pythonhosted.org/packages/f5/79/a25add6fb38035b5337bc5734f296d9afc99163403bbcf56d4170f97eb62/multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6", size = 254005, upload-time = "2026-01-26T02:43:10.127Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7b/64a87cf98e12f756fc8bd444b001232ffff2be37288f018ad0d3f0aae931/multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190", size = 251173, upload-time = "2026-01-26T02:43:11.731Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ac/b605473de2bb404e742f2cc3583d12aedb2352a70e49ae8fce455b50c5aa/multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92", size = 243273, upload-time = "2026-01-26T02:43:13.063Z" }, + { url = "https://files.pythonhosted.org/packages/03/65/11492d6a0e259783720f3bc1d9ea55579a76f1407e31ed44045c99542004/multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee", size = 238956, upload-time = "2026-01-26T02:43:14.843Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a7/7ee591302af64e7c196fb63fe856c788993c1372df765102bd0448e7e165/multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2", size = 233477, upload-time = "2026-01-26T02:43:16.025Z" }, + { url = "https://files.pythonhosted.org/packages/9c/99/c109962d58756c35fd9992fed7f2355303846ea2ff054bb5f5e9d6b888de/multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568", size = 243615, upload-time = "2026-01-26T02:43:17.84Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5f/1973e7c771c86e93dcfe1c9cc55a5481b610f6614acfc28c0d326fe6bfad/multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40", size = 249930, upload-time = "2026-01-26T02:43:19.06Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a5/f170fc2268c3243853580203378cd522446b2df632061e0a5409817854c7/multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962", size = 243807, upload-time = "2026-01-26T02:43:20.286Z" }, + { url = "https://files.pythonhosted.org/packages/de/01/73856fab6d125e5bc652c3986b90e8699a95e84b48d72f39ade6c0e74a8c/multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505", size = 239103, upload-time = "2026-01-26T02:43:21.508Z" }, + { url = "https://files.pythonhosted.org/packages/e7/46/f1220bd9944d8aa40d8ccff100eeeee19b505b857b6f603d6078cb5315b0/multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122", size = 41416, upload-time = "2026-01-26T02:43:22.703Z" }, + { url = "https://files.pythonhosted.org/packages/68/00/9b38e272a770303692fc406c36e1a4c740f401522d5787691eb38a8925a8/multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df", size = 46022, upload-time = "2026-01-26T02:43:23.77Z" }, + { url = "https://files.pythonhosted.org/packages/64/65/d8d42490c02ee07b6bbe00f7190d70bb4738b3cce7629aaf9f213ef730dd/multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db", size = 43238, upload-time = "2026-01-26T02:43:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, + { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, + { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" }, + { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] [[package]] name = "mypy" -version = "1.4.1" +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/b3/28/d8a8233ff167d06108e53b7aefb4a8d7350adbbf9d7abd980f17fdb7a3a6/mypy-1.4.1.tar.gz", hash = "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b", size = 2855162, upload-time = "2023-06-25T23:22:54.364Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/3b/1c7363863b56c059f60a1dfdca9ac774a22ba64b7a4da0ee58ee53e5243f/mypy-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8", size = 10451043, upload-time = "2023-06-25T23:22:02.502Z" }, - { url = "https://files.pythonhosted.org/packages/a7/24/6f0df1874118839db1155fed62a4bd7e80c181367ff8ea07d40fbaffcfb4/mypy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878", size = 9542079, upload-time = "2023-06-25T23:22:37.916Z" }, - { url = "https://files.pythonhosted.org/packages/04/5c/deeac94fcccd11aa621e6b350df333e1b809b11443774ea67582cc0205da/mypy-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd", size = 11974913, upload-time = "2023-06-25T23:21:14.603Z" }, - { url = "https://files.pythonhosted.org/packages/e5/2f/de3c455c54e8cf5e37ea38705c1920f2df470389f8fc051084d2dd8c9c59/mypy-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc", size = 12044492, upload-time = "2023-06-25T23:22:17.551Z" }, - { url = "https://files.pythonhosted.org/packages/e7/d3/6f65357dcb68109946de70cd55bd2e60f10114f387471302f48d54ff5dae/mypy-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1", size = 8831655, upload-time = "2023-06-25T23:21:40.201Z" }, - { url = "https://files.pythonhosted.org/packages/94/01/e34e37a044325af4d4af9825c15e8a0d26d89b5a9624b4d0908449d3411b/mypy-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462", size = 10338636, upload-time = "2023-06-25T23:22:43.45Z" }, - { url = "https://files.pythonhosted.org/packages/92/58/ccc0b714ecbd1a64b34d8ce1c38763ff6431de1d82551904ecc3711fbe05/mypy-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258", size = 9444172, upload-time = "2023-06-25T23:21:25.502Z" }, - { url = "https://files.pythonhosted.org/packages/73/72/dfc0b46e6905eafd598e7c48c0c4f2e232647e4e36547425c64e6c850495/mypy-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2", size = 11855450, upload-time = "2023-06-25T23:21:37.234Z" }, - { url = "https://files.pythonhosted.org/packages/66/f4/60739a2d336f3adf5628e7c9b920d16e8af6dc078550d615e4ba2a1d7759/mypy-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7", size = 11928679, upload-time = "2023-06-25T23:22:40.757Z" }, - { url = "https://files.pythonhosted.org/packages/8c/26/6ff2b55bf8b605a4cc898883654c2ca4dd4feedf0bb04ecaacf60d165cde/mypy-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01", size = 8831134, upload-time = "2023-06-25T23:22:09.178Z" }, - { url = "https://files.pythonhosted.org/packages/1d/1b/9050b5c444ef82c3d59bdbf21f91b259cf20b2ac1df37d55bc6b91d609a1/mypy-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828", size = 10447897, upload-time = "2023-06-25T23:21:22.06Z" }, - { url = "https://files.pythonhosted.org/packages/da/00/ac2b58b321d85cac25be0dcd1bc2427dfc6cf403283fc205a0031576f14b/mypy-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3", size = 9534091, upload-time = "2023-06-25T23:22:14.634Z" }, - { url = "https://files.pythonhosted.org/packages/c4/10/26240f14e854a95af87d577b288d607ebe0ccb75cb37052f6386402f022d/mypy-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816", size = 11970165, upload-time = "2023-06-25T23:22:05.673Z" }, - { url = "https://files.pythonhosted.org/packages/b7/34/a3edaec8762181bfe97439c7e094f4c2f411ed9b79ac8f4d72156e88d5ce/mypy-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c", size = 12040792, upload-time = "2023-06-25T23:21:49.878Z" }, - { url = "https://files.pythonhosted.org/packages/d1/f3/0d0622d5a83859a992b01741a7b97949d6fb9efc9f05f20a09f0df10dc1e/mypy-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f", size = 8831367, upload-time = "2023-06-25T23:21:43.065Z" }, - { url = "https://files.pythonhosted.org/packages/3d/9a/e13addb8d652cb068f835ac2746d9d42f85b730092f581bb17e2059c28f1/mypy-1.4.1-py3-none-any.whl", hash = "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4", size = 2451741, upload-time = "2023-06-25T23:22:49.033Z" }, +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]] @@ -1653,74 +2691,117 @@ wheels = [ [[package]] name = "mypy-protobuf" -version = "3.6.0" +version = "3.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, { name = "types-protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4d/6f/282d64d66bf48ce60e38a6560753f784e0f88ab245ac2fb5e93f701a36cd/mypy-protobuf-3.6.0.tar.gz", hash = "sha256:02f242eb3409f66889f2b1a3aa58356ec4d909cdd0f93115622e9e70366eca3c", size = 24445, upload-time = "2024-04-01T20:24:42.837Z" } +sdist = { url = "https://files.pythonhosted.org/packages/99/bd/92ee9e7d2ff30cb57b36bd6f61ee4da8a05acf32f6a6121883b58720e9d4/mypy_protobuf-3.7.0.tar.gz", hash = "sha256:912fb281f7c7b3e3a7c9b8695712618a716fddbab70f6ad63eaf68eda80c5efe", size = 25690, upload-time = "2025-11-17T22:11:12.809Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/32/709df4390d155bd72e75f2a9bce6004c2958a21f31817f3a2c7750299f70/mypy_protobuf-3.7.0-py3-none-any.whl", hash = "sha256:85256e9d4da935722ce8fbaa8d19397e1a2989aa8075c96577987de9fe7cea4d", size = 17488, upload-time = "2025-11-17T22:11:11.62Z" }, +] + +[[package]] +name = "networkx" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version >= '3.11' and python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/73/d6b999782ae22f16971cc05378b3b33f6a89ede3b9619e8366aa23484bca/mypy_protobuf-3.6.0-py3-none-any.whl", hash = "sha256:56176e4d569070e7350ea620262478b49b7efceba4103d468448f1d21492fd6c", size = 16434, upload-time = "2024-04-01T20:24:40.583Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, ] [[package]] name = "nexus-rpc" -version = "1.1.0" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ef/66/540687556bd28cf1ec370cc6881456203dfddb9dab047b8979c6865b5984/nexus_rpc-1.1.0.tar.gz", hash = "sha256:d65ad6a2f54f14e53ebe39ee30555eaeb894102437125733fb13034a04a44553", size = 77383, upload-time = "2025-07-07T19:03:58.368Z" } +sdist = { url = "https://files.pythonhosted.org/packages/35/d5/cd1ffb202b76ebc1b33c1332a3416e55a39929006982adc2b1eb069aaa9b/nexus_rpc-1.4.0.tar.gz", hash = "sha256:3b8b373d4865671789cc43623e3dc0bcbf192562e40e13727e17f1c149050fba", size = 82367, upload-time = "2026-02-25T22:01:34.053Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/2f/9e9d0dcaa4c6ffa22b7aa31069a8a264c753ff8027b36af602cce038c92f/nexus_rpc-1.1.0-py3-none-any.whl", hash = "sha256:d1b007af2aba186a27e736f8eaae39c03aed05b488084ff6c3d1785c9ba2ad38", size = 27743, upload-time = "2025-07-07T19:03:57.556Z" }, + { url = "https://files.pythonhosted.org/packages/11/52/6327a5f4fda01207205038a106a99848a41c83e933cd23ea2cab3d2ebc6c/nexus_rpc-1.4.0-py3-none-any.whl", hash = "sha256:14c953d3519113f8ccec533a9efdb6b10c28afef75d11cdd6d422640c40b3a49", size = 29645, upload-time = "2026-02-25T22:01:33.122Z" }, ] [[package]] name = "nh3" -version = "0.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/a4/96cff0977357f60f06ec4368c4c7a7a26cccfe7c9fcd54f5378bf0428fd3/nh3-0.3.0.tar.gz", hash = "sha256:d8ba24cb31525492ea71b6aac11a4adac91d828aadeff7c4586541bf5dc34d2f", size = 19655, upload-time = "2025-07-17T14:43:37.05Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/11/340b7a551916a4b2b68c54799d710f86cf3838a4abaad8e74d35360343bb/nh3-0.3.0-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a537ece1bf513e5a88d8cff8a872e12fe8d0f42ef71dd15a5e7520fecd191bbb", size = 1427992, upload-time = "2025-07-17T14:43:06.848Z" }, - { url = "https://files.pythonhosted.org/packages/ad/7f/7c6b8358cf1222921747844ab0eef81129e9970b952fcb814df417159fb9/nh3-0.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c915060a2c8131bef6a29f78debc29ba40859b6dbe2362ef9e5fd44f11487c2", size = 798194, upload-time = "2025-07-17T14:43:08.263Z" }, - { url = "https://files.pythonhosted.org/packages/63/da/c5fd472b700ba37d2df630a9e0d8cc156033551ceb8b4c49cc8a5f606b68/nh3-0.3.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba0caa8aa184196daa6e574d997a33867d6d10234018012d35f86d46024a2a95", size = 837884, upload-time = "2025-07-17T14:43:09.233Z" }, - { url = "https://files.pythonhosted.org/packages/4c/3c/cba7b26ccc0ef150c81646478aa32f9c9535234f54845603c838a1dc955c/nh3-0.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:80fe20171c6da69c7978ecba33b638e951b85fb92059259edd285ff108b82a6d", size = 996365, upload-time = "2025-07-17T14:43:10.243Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ba/59e204d90727c25b253856e456ea61265ca810cda8ee802c35f3fadaab00/nh3-0.3.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e90883f9f85288f423c77b3f5a6f4486375636f25f793165112679a7b6363b35", size = 1071042, upload-time = "2025-07-17T14:43:11.57Z" }, - { url = "https://files.pythonhosted.org/packages/10/71/2fb1834c10fab6d9291d62c95192ea2f4c7518bd32ad6c46aab5d095cb87/nh3-0.3.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0649464ac8eee018644aacbc103874ccbfac80e3035643c3acaab4287e36e7f5", size = 995737, upload-time = "2025-07-17T14:43:12.659Z" }, - { url = "https://files.pythonhosted.org/packages/33/c1/8f8ccc2492a000b6156dce68a43253fcff8b4ce70ab4216d08f90a2ac998/nh3-0.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1adeb1062a1c2974bc75b8d1ecb014c5fd4daf2df646bbe2831f7c23659793f9", size = 980552, upload-time = "2025-07-17T14:43:13.763Z" }, - { url = "https://files.pythonhosted.org/packages/2f/d6/f1c6e091cbe8700401c736c2bc3980c46dca770a2cf6a3b48a175114058e/nh3-0.3.0-cp313-cp313t-win32.whl", hash = "sha256:7275fdffaab10cc5801bf026e3c089d8de40a997afc9e41b981f7ac48c5aa7d5", size = 593618, upload-time = "2025-07-17T14:43:15.098Z" }, - { url = "https://files.pythonhosted.org/packages/23/1e/80a8c517655dd40bb13363fc4d9e66b2f13245763faab1a20f1df67165a7/nh3-0.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:423201bbdf3164a9e09aa01e540adbb94c9962cc177d5b1cbb385f5e1e79216e", size = 598948, upload-time = "2025-07-17T14:43:16.064Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e0/af86d2a974c87a4ba7f19bc3b44a8eaa3da480de264138fec82fe17b340b/nh3-0.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:16f8670201f7e8e0e05ed1a590eb84bfa51b01a69dd5caf1d3ea57733de6a52f", size = 580479, upload-time = "2025-07-17T14:43:17.038Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e0/cf1543e798ba86d838952e8be4cb8d18e22999be2a24b112a671f1c04fd6/nh3-0.3.0-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ec6cfdd2e0399cb79ba4dcffb2332b94d9696c52272ff9d48a630c5dca5e325a", size = 1442218, upload-time = "2025-07-17T14:43:18.087Z" }, - { url = "https://files.pythonhosted.org/packages/5c/86/a96b1453c107b815f9ab8fac5412407c33cc5c7580a4daf57aabeb41b774/nh3-0.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5e7185599f89b0e391e2f29cc12dc2e206167380cea49b33beda4891be2fe1", size = 823791, upload-time = "2025-07-17T14:43:19.721Z" }, - { url = "https://files.pythonhosted.org/packages/97/33/11e7273b663839626f714cb68f6eb49899da5a0d9b6bc47b41fe870259c2/nh3-0.3.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:389d93d59b8214d51c400fb5b07866c2a4f79e4e14b071ad66c92184fec3a392", size = 811143, upload-time = "2025-07-17T14:43:20.779Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1b/b15bd1ce201a1a610aeb44afd478d55ac018b4475920a3118ffd806e2483/nh3-0.3.0-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e9e6a7e4d38f7e8dda9edd1433af5170c597336c1a74b4693c5cb75ab2b30f2a", size = 1064661, upload-time = "2025-07-17T14:43:21.839Z" }, - { url = "https://files.pythonhosted.org/packages/8f/14/079670fb2e848c4ba2476c5a7a2d1319826053f4f0368f61fca9bb4227ae/nh3-0.3.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7852f038a054e0096dac12b8141191e02e93e0b4608c4b993ec7d4ffafea4e49", size = 997061, upload-time = "2025-07-17T14:43:23.179Z" }, - { url = "https://files.pythonhosted.org/packages/a3/e5/ac7fc565f5d8bce7f979d1afd68e8cb415020d62fa6507133281c7d49f91/nh3-0.3.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af5aa8127f62bbf03d68f67a956627b1bd0469703a35b3dad28d0c1195e6c7fb", size = 924761, upload-time = "2025-07-17T14:43:24.23Z" }, - { url = "https://files.pythonhosted.org/packages/39/2c/6394301428b2017a9d5644af25f487fa557d06bc8a491769accec7524d9a/nh3-0.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f416c35efee3e6a6c9ab7716d9e57aa0a49981be915963a82697952cba1353e1", size = 803959, upload-time = "2025-07-17T14:43:26.377Z" }, - { url = "https://files.pythonhosted.org/packages/4e/9a/344b9f9c4bd1c2413a397f38ee6a3d5db30f1a507d4976e046226f12b297/nh3-0.3.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37d3003d98dedca6cd762bf88f2e70b67f05100f6b949ffe540e189cc06887f9", size = 844073, upload-time = "2025-07-17T14:43:27.375Z" }, - { url = "https://files.pythonhosted.org/packages/66/3f/cd37f76c8ca277b02a84aa20d7bd60fbac85b4e2cbdae77cb759b22de58b/nh3-0.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:634e34e6162e0408e14fb61d5e69dbaea32f59e847cfcfa41b66100a6b796f62", size = 1000680, upload-time = "2025-07-17T14:43:28.452Z" }, - { url = "https://files.pythonhosted.org/packages/ee/db/7aa11b44bae4e7474feb1201d8dee04fabe5651c7cb51409ebda94a4ed67/nh3-0.3.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:b0612ccf5de8a480cf08f047b08f9d3fecc12e63d2ee91769cb19d7290614c23", size = 1076613, upload-time = "2025-07-17T14:43:30.031Z" }, - { url = "https://files.pythonhosted.org/packages/97/03/03f79f7e5178eb1ad5083af84faff471e866801beb980cc72943a4397368/nh3-0.3.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:c7a32a7f0d89f7d30cb8f4a84bdbd56d1eb88b78a2434534f62c71dac538c450", size = 1001418, upload-time = "2025-07-17T14:43:31.429Z" }, - { url = "https://files.pythonhosted.org/packages/ce/55/1974bcc16884a397ee699cebd3914e1f59be64ab305533347ca2d983756f/nh3-0.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3f1b4f8a264a0c86ea01da0d0c390fe295ea0bcacc52c2103aca286f6884f518", size = 986499, upload-time = "2025-07-17T14:43:32.459Z" }, - { url = "https://files.pythonhosted.org/packages/c9/50/76936ec021fe1f3270c03278b8af5f2079038116b5d0bfe8538ffe699d69/nh3-0.3.0-cp38-abi3-win32.whl", hash = "sha256:6d68fa277b4a3cf04e5c4b84dd0c6149ff7d56c12b3e3fab304c525b850f613d", size = 599000, upload-time = "2025-07-17T14:43:33.852Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ae/324b165d904dc1672eee5f5661c0a68d4bab5b59fbb07afb6d8d19a30b45/nh3-0.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:bae63772408fd63ad836ec569a7c8f444dd32863d0c67f6e0b25ebbd606afa95", size = 604530, upload-time = "2025-07-17T14:43:34.95Z" }, - { url = "https://files.pythonhosted.org/packages/5b/76/3165e84e5266d146d967a6cc784ff2fbf6ddd00985a55ec006b72bc39d5d/nh3-0.3.0-cp38-abi3-win_arm64.whl", hash = "sha256:d97d3efd61404af7e5721a0e74d81cdbfc6e5f97e11e731bb6d090e30a7b62b2", size = 585971, upload-time = "2025-07-17T14:43:35.936Z" }, +version = "0.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/1b/ef84624f14954d270f74060a19fc550dd4f06656399447569afb584d8c06/nh3-0.3.6.tar.gz", hash = "sha256:f3736c9dd3d1856f80cd031715b84ca75cda2bbb1ac802c3da26bfce590838d7", size = 24684, upload-time = "2026-06-22T00:47:02.008Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/3e/6506aa4f23dc7b7993a2d0a45dca3ce864ec48380adfe15a173e643c63e8/nh3-0.3.6-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2411e8c3cee81a1ddd62c2a5d50585c28aa5566d373ad1db92536b95ddb24ef2", size = 1421679, upload-time = "2026-06-22T00:46:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e1/e96e7864a7a53bd6b6fab7e9632467382a2a2c1f3fed951918ad131542fb/nh3-0.3.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e196fa70c2ff2eb4de7d3df3108f8f358c1d69dff20d45b11f20a5aa227ffb6d", size = 792570, upload-time = "2026-06-22T00:46:22.179Z" }, + { url = "https://files.pythonhosted.org/packages/59/62/5b6108bedaef2b2637fed04c87bdbcb5967b9961758b41f0e466ef22a022/nh3-0.3.6-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:34d2b0d934156b87ee114f599a3ba9b8b9e17b5d79652ba3a13fa50903de965e", size = 842243, upload-time = "2026-06-22T00:46:23.801Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4a/526f199626bfcb496bc01a268051b44737962005553b158e985ed7e64865/nh3-0.3.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2f14b7ae1fca99c4a66c981aac3974e7fbc1ca30a12673d223ae1df76680917", size = 1001468, upload-time = "2026-06-22T00:46:25.481Z" }, + { url = "https://files.pythonhosted.org/packages/49/09/0d8e3101636d9ad88cdefb2914e764cb8e876ebdbb4286bfc251277d9c67/nh3-0.3.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:889932a97fb4abb6f95fef1914c0d269ebfb60011e67121c1163059b9449dbb4", size = 1082933, upload-time = "2026-06-22T00:46:27.15Z" }, + { url = "https://files.pythonhosted.org/packages/09/a1/ea83abe738a3fbaa203dfdb836ca7cbab0e7e9609faaee4fe1d4652599c0/nh3-0.3.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:edb2b4a1a27523e6cc7c417f8d21ce3d005243548b93e56b762b66b0c7f589f9", size = 1043120, upload-time = "2026-06-22T00:46:28.89Z" }, + { url = "https://files.pythonhosted.org/packages/66/69/0654482b8635012fbae67826bd6c381abb05d841ac7388b9b4666300fdad/nh3-0.3.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43bc1ed3fa0716295fabee29ba42b2667e4a51d140b0a68e092170a765474fa6", size = 1023824, upload-time = "2026-06-22T00:46:30.453Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/1f7285ffadc8307c4dbeb08d21b920536d5117785056d1079e998c4dfa44/nh3-0.3.6-cp314-cp314t-win32.whl", hash = "sha256:597a8e843bea00b2eb5520658dc24a9bb032e7fc9e7c2c0c4cd29420220c9796", size = 599253, upload-time = "2026-06-22T00:46:32.072Z" }, + { url = "https://files.pythonhosted.org/packages/36/ea/5542f3c45da4c00290d9d67a65e996702e23e613c4b627de3e09cb9fe357/nh3-0.3.6-cp314-cp314t-win_amd64.whl", hash = "sha256:4713502748f564fee0633b37b3403783ce0a3af3a3d148ad91025a5bdadb7bc6", size = 612553, upload-time = "2026-06-22T00:46:33.53Z" }, + { url = "https://files.pythonhosted.org/packages/66/35/26bd47e6af5915a628281dccdac354ddf4e32f7397047894270acd8c9870/nh3-0.3.6-cp314-cp314t-win_arm64.whl", hash = "sha256:69bbb92865a693d909db3a700d3c01537533844d0948c1e9323561ce06ecda41", size = 595151, upload-time = "2026-06-22T00:46:34.878Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ab/a7653bce9a3b204be6a6931767a9e23595807bb84790ce6685e4d7e5bd08/nh3-0.3.6-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a43ebd7543555c3ac1bc353023d0794e75cb76f6f18f19c32e95441496c0cc25", size = 1443564, upload-time = "2026-06-22T00:46:36.66Z" }, + { url = "https://files.pythonhosted.org/packages/41/21/e1084ab18eb589506335c7c7576f2d4643e9a0c0e33983ef0e549a256b96/nh3-0.3.6-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1b160831c9cdb06a6c79c2f9cdb11386602938f9af260d1c457a85add4f6f69", size = 838002, upload-time = "2026-06-22T00:46:38.101Z" }, + { url = "https://files.pythonhosted.org/packages/b0/94/f48d08e6f72a406300fa11d8acd929fea1a80d4bf750fa292cb10785f126/nh3-0.3.6-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d14bf7982e7a77c0c775634c29c07ce08b38a046df73e1c1f139b3e82f18a38e", size = 823045, upload-time = "2026-06-22T00:46:39.495Z" }, + { url = "https://files.pythonhosted.org/packages/25/bb/431615ba1d1d3eb63cde0f974f2114edf863a8a3f6049a12fed23fc241d3/nh3-0.3.6-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:44673b27010051ab5a5e438a86ec31bbda61d4a77d7e900af6b7be3037c1abae", size = 1093171, upload-time = "2026-06-22T00:46:41.21Z" }, + { url = "https://files.pythonhosted.org/packages/0e/24/a0d80182a18919665fefd19c1c06f1d1df1c9a6455d0252de40c034a0bc3/nh3-0.3.6-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6b7beece07525dc6e6b0fc2f104442de2ba328360ad00e50cbe2e1fd620447d", size = 1049217, upload-time = "2026-06-22T00:46:42.804Z" }, + { url = "https://files.pythonhosted.org/packages/0a/13/6f1e302ca674ac74362e150848ad56a1be5145391204f74facdb8e94df12/nh3-0.3.6-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:455469a29951edc92bc48b47ac2281c3f2609e6c4f6a047056449f8c2c23facf", size = 917372, upload-time = "2026-06-22T00:46:44.495Z" }, + { url = "https://files.pythonhosted.org/packages/5b/67/314f6151bad77a93d751978a344033e1fc890822f05f0416079338e34231/nh3-0.3.6-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:905f877dc66dd7aea4a76e54bcb26acb5ff8216f720c0017ccf63e0e6035698e", size = 806699, upload-time = "2026-06-22T00:46:45.99Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a6/bfaa00046e58603507dcfc266c4778e3ab7adf68a5dedd73b6274b8d9314/nh3-0.3.6-cp38-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:25c733bee928530556b1db0ea46c52cf5aa686146e38e60a6fc7cb801ef91cec", size = 835165, upload-time = "2026-06-22T00:46:47.617Z" }, + { url = "https://files.pythonhosted.org/packages/30/a8/fb2c38845efb703a9173bffdfc745fc64d2b0e55cfc73a3647d2f028250c/nh3-0.3.6-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f90d9a0cfdbee218994fdaaeeb5a0fde62d08f35e4eef0378ec1e2200172fd0", size = 858282, upload-time = "2026-06-22T00:46:49.276Z" }, + { url = "https://files.pythonhosted.org/packages/68/17/06e72a18ee9b572914447338237ca7eb164c0df901f141bc10d1282247a2/nh3-0.3.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:82ca5bf427ad1b216b65ede1a2e2d87dc49bec417ceba0f297213107d3cd9d78", size = 1014328, upload-time = "2026-06-22T00:46:51.026Z" }, + { url = "https://files.pythonhosted.org/packages/11/f9/3966c61455668c08853bf5e33b4bed93c421f3194ce4de896dc248d6f6ce/nh3-0.3.6-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f5ed5fe84aee7f39db95c214a7421bf0499fbf500fec6d86a4e29bfc37971438", size = 1098207, upload-time = "2026-06-22T00:46:52.674Z" }, + { url = "https://files.pythonhosted.org/packages/19/d3/479cb4ae440424825735d60525b53e3c77fd60fd6e6afc0e984f00eb0178/nh3-0.3.6-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:082675ff87b9385ec430ffe6d5847ba7456cc39b73720cd4add472f9f4cffd56", size = 1056961, upload-time = "2026-06-22T00:46:54.335Z" }, + { url = "https://files.pythonhosted.org/packages/17/0c/6cdb5ee1e127be50dc8391e54bddc1f64e87bf4bfad0c55633320e2e02db/nh3-0.3.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36d06341bd501240d320f5942481ed5e6846136b666e1ba4faf802b78ebc875f", size = 1033829, upload-time = "2026-06-22T00:46:56.258Z" }, + { url = "https://files.pythonhosted.org/packages/e9/55/9de666ad975d6ccd77d799ea0add55ee2347aa81286ce21b2a97c070746b/nh3-0.3.6-cp38-abi3-win32.whl", hash = "sha256:5276ef17bdba9ad8040575c74072008b13aae429436e9d0429e718bb5f90f4da", size = 609081, upload-time = "2026-06-22T00:46:57.665Z" }, + { url = "https://files.pythonhosted.org/packages/82/fa/2b5d684e3edf1e81bfd02d298c78c3e3da77ca1d8a2be3183a79544a7548/nh3-0.3.6-cp38-abi3-win_amd64.whl", hash = "sha256:f338ac7d594c067679f1e99b4f5ec3906842979560f9d8f15d6bdfa39a353b10", size = 624461, upload-time = "2026-06-22T00:46:59.163Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e5/7cafee2f0413ca4cb0ef3bd111e94d408a48810008b283ad8aee00dd1809/nh3-0.3.6-cp38-abi3-win_arm64.whl", hash = "sha256:69f365963f63a1e9bff53bdbb3c542c7c2efed3e163c9d5d83a772a2ac468c21", size = 603060, upload-time = "2026-06-22T00:47:00.596Z" }, ] [[package]] name = "nodeenv" -version = "1.9.1" +version = "1.10.0" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "nodejs-wheel-binaries" +version = "24.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/22/2a5beb4e21417c73233d9f65cf6f3e96e891b80d2f550a8f630ebc6b88c6/nodejs_wheel_binaries-24.16.0.tar.gz", hash = "sha256:c973cb69dc5fd16e6f6dc6e579e2c3d5534e2a1f57619dddf5ba070efa7dde37", size = 8056, upload-time = "2026-05-30T16:52:09.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/d1/68b43b53cd0fa83ae6fd406705023ca988d9e0ca41c724d82e66fbeb2ef6/nodejs_wheel_binaries-24.16.0-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:d9f8f677dcf30e37ac244f07869726abe043f01eb0f45722b1df31cc2af7093c", size = 55666374, upload-time = "2026-05-30T16:51:39.588Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b2/40a989159599080da485de966c4c2d207e852ac7aa7864702626d96c8bf5/nodejs_wheel_binaries-24.16.0-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:3d0370fe7120ce9697a4f60d40480d2bd8808d9f30131458d5afc0040d4e5a51", size = 55838487, upload-time = "2026-05-30T16:51:43.383Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a7/cd42174fb5ff6faff7fa8d326a18914d8f232098ab5de055b57c16fa13ca/nodejs_wheel_binaries-24.16.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:85dc92bbb79c851569c5925dcc2a4c915a034efab375f99e4e7e6bbe9cca8342", size = 60179540, upload-time = "2026-05-30T16:51:47.036Z" }, + { url = "https://files.pythonhosted.org/packages/2b/95/c8a1f9ae140aa28df8744d984d01d4b3af7cdd6555af12127f40ceb45a7d/nodejs_wheel_binaries-24.16.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:2f3036292811514ba847b3708492644764f88a833ac425c5f55007014308ddfd", size = 60716262, upload-time = "2026-05-30T16:51:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/7c35b3737f59e36d0249c265397b7bff570519b95301d6e16ea361e904ad/nodejs_wheel_binaries-24.16.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:db8a8a76ebd2b28ecbfc9ad464baa3707241b9e050a30e2efdf6f60c0f886502", size = 62230592, upload-time = "2026-05-30T16:51:55Z" }, + { url = "https://files.pythonhosted.org/packages/04/96/d931255cf9d11a84d6b54d882dba7434646467d568ccf070ea3418638df3/nodejs_wheel_binaries-24.16.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f1a3d8f7b4491cbbd023ba3fc4e901fcca2d9fb80d57f24ba3890de8b1dbac03", size = 62841759, upload-time = "2026-05-30T16:51:59.407Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7b/8b7a3f41bc255411be30b6d7d288aab8ffd9ea2055db8555ced3548007b9/nodejs_wheel_binaries-24.16.0-py2.py3-none-win_amd64.whl", hash = "sha256:bb136be9944f0662dcf1120f45193a6b75b13fac378971a95cc42c9f879a81aa", size = 42027734, upload-time = "2026-05-30T16:52:03.348Z" }, + { url = "https://files.pythonhosted.org/packages/17/66/1ed71f1f529b8ca727d42c7ceb9db0bef145ce4a13dfc86fb50aa44f3be6/nodejs_wheel_binaries-24.16.0-py2.py3-none-win_arm64.whl", hash = "sha256:8308940b5edd0a50dc5267ea36ba21c9f668e83fe0d9f293937174d3a7e31c36", size = 39714528, upload-time = "2026-05-30T16:52:06.421Z" }, ] [[package]] name = "openai" -version = "1.103.0" +version = "2.45.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1732,27 +2813,27 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cb/0b/4cacc14f976601edf35b74f7c5c2d6305f7402257cb13b9956b4eaabf94d/openai-1.103.0.tar.gz", hash = "sha256:f84f8741536f01adfdae1acfe31ec1874fc0985d33f53344f9edca773f150a36", size = 556049, upload-time = "2025-09-02T14:03:11.533Z" } +sdist = { url = "https://files.pythonhosted.org/packages/78/60/d4219875289b11d2c2f7da93c36283da224a2e55865ed865ab64e0ce9217/openai-2.45.0.tar.gz", hash = "sha256:10d34ca9c5643bce775852fddbfc172505cb1d4de1ccd101696c3ecff358765d", size = 1109653, upload-time = "2026-07-09T18:02:44.091Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/c0/f5d440153d96c6be42e5d05c39adf33e0c324c9f035daf0e537a71fe2d11/openai-1.103.0-py3-none-any.whl", hash = "sha256:60a69224f0d210a720e7364947d3b712fe0036373f25dc1cb801fc25abb3f864", size = 926169, upload-time = "2025-09-02T14:03:09.666Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b0/2291689e3ec4723fbf5bbf3b54afcd7b160f9ddc98ca7aedfd0132af5677/openai-2.45.0-py3-none-any.whl", hash = "sha256:5df105f5f8c9b711fcb9d06d2d3888cebc82506db216484c14a4e53cdf651777", size = 1629470, upload-time = "2026-07-09T18:02:42.21Z" }, ] [[package]] name = "openai-agents" -version = "0.2.9" +version = "0.18.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "griffe" }, - { name = "mcp", marker = "python_full_version >= '3.10'" }, + { name = "griffelib" }, + { name = "mcp" }, { name = "openai" }, { name = "pydantic" }, { name = "requests" }, - { name = "types-requests" }, { name = "typing-extensions" }, + { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c3/03/45a1022ae6daa7f0223a3cae8975c5a524858edcad2355f8e1adcd4f000e/openai_agents-0.2.9.tar.gz", hash = "sha256:619c51c8ce49f841474a9e619573d6f7f96a46432900752145ad04309ab70ae9", size = 1664869, upload-time = "2025-08-22T02:03:39.196Z" } +sdist = { url = "https://files.pythonhosted.org/packages/05/0c/52e9aeff5549b225d5666a0eb84a8a22b4c47db08b6f44dbd45876fcfba3/openai_agents-0.18.2.tar.gz", hash = "sha256:9f418bb563eddff1e01f245ae8a4964b7649396f444b569b4113d105e41ca1d3", size = 5546139, upload-time = "2026-07-11T01:08:18.537Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/bf/8a8dd24206763214f364b272371486247744a64ef554e952d92444e6ce14/openai_agents-0.2.9-py3-none-any.whl", hash = "sha256:cca016c28e39b24b17cae232c2bc16769e48dbfc7cbe006775d10822c441f6e4", size = 175106, upload-time = "2025-08-22T02:03:37.738Z" }, + { url = "https://files.pythonhosted.org/packages/9c/23/b5b6b80a3e36f021ca2a8c4637684f0d722d646b19d3616768a68802c302/openai_agents-0.18.2-py3-none-any.whl", hash = "sha256:c7aea341b256a90b87b17b7e444bab29a12655864a2f0094f65561223d867185", size = 874310, upload-time = "2026-07-11T01:08:16.851Z" }, ] [package.optional-dependencies] @@ -1760,53 +2841,478 @@ litellm = [ { name = "litellm" }, ] +[[package]] +name = "openapi-schema-validator" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema" }, + { name = "jsonschema-specifications" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "referencing" }, + { name = "rfc3339-validator" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/e8/ab3f27dbca54ec645f7fab714b640907d5d36c2ebb07e87eebd30bd5c81b/openapi_schema_validator-0.9.0.tar.gz", hash = "sha256:b72db64315b89d21834cd3ffef37e3e6893bc876327be2d366e8424b1029afd3", size = 24686, upload-time = "2026-04-27T17:31:27.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/c0/5467967d95378b2cfce312e09cbd0c9ab64354a0922379b734f793edd04f/openapi_schema_validator-0.9.0-py3-none-any.whl", hash = "sha256:faa3bbe7c3aa8ca2087ad83f709dc3b7d920283153a570c03e24ea182558aa25", size = 19980, upload-time = "2026-04-27T17:31:25.965Z" }, +] + +[[package]] +name = "openapi-spec-validator" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema" }, + { name = "jsonschema-path" }, + { name = "lazy-object-proxy" }, + { name = "openapi-schema-validator" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/d2/640b5149cd5688bc0ad1fdbb4df6a2f7b84a093c8d787c27d566132f8b8b/openapi_spec_validator-0.9.0.tar.gz", hash = "sha256:6d648cff6490ebb799dcfe273792f2941c050158854c721f086599d845da78b8", size = 1756839, upload-time = "2026-05-20T09:23:18.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/d8/321ff889330acca2e3097f3d4f80a40bcc41b6d34d302978ab32c449520b/openapi_spec_validator-0.9.0-py3-none-any.whl", hash = "sha256:222fecffc7714f6d0a6ad62c0e4b66cc2b7dbfafb7b93acfc6c308abbdb51af8", size = 50328, upload-time = "2026-05-20T09:23:17.017Z" }, +] + +[[package]] +name = "openinference-instrumentation" +version = "0.1.54" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "openinference-semantic-conventions" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/cc/62c1175ee7edc2cbdf95b5b73e0b0f305e759d407bf1bc353ff30a763365/openinference_instrumentation-0.1.54.tar.gz", hash = "sha256:9af9817bb38816ed32856fb4cd813c1a5d9f530ab589c3473b37e06cf406ab28", size = 33938, upload-time = "2026-06-30T19:23:15.648Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/e3/c7aa7bb4845e0cfdf477ff87f9a3ec0cd2aa55a34a9f31be84d724cabbb7/openinference_instrumentation-0.1.54-py3-none-any.whl", hash = "sha256:8bc991865c90c804ac9983ef93aa6081a7a2397dd113d3d38b5465cca17892bb", size = 41197, upload-time = "2026-06-30T19:23:14.315Z" }, +] + +[[package]] +name = "openinference-instrumentation-google-adk" +version = "0.1.17" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "openinference-instrumentation" }, + { name = "openinference-semantic-conventions" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/c2/8400125398ac568efe49bc8a9411a4c860b27112279d52d2bb8acbc70126/openinference_instrumentation_google_adk-0.1.17.tar.gz", hash = "sha256:8235d2cf3fce5edaf213136fbd00e876db183add362b2edede87cd42b21ab112", size = 14844, upload-time = "2026-07-01T15:44:19.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/c0/b0c301a0a1de9fe377e666e89b9e87fa368bd5ac1341ab125624e2bbc081/openinference_instrumentation_google_adk-0.1.17-py3-none-any.whl", hash = "sha256:3f45e38cfd5ffb41c18deddc747e48c68595d4dfa1a89b7b1aa1f3d31e46ce0d", size = 16755, upload-time = "2026-07-01T15:44:18.364Z" }, +] + +[[package]] +name = "openinference-instrumentation-openai-agents" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "openinference-instrumentation" }, + { name = "openinference-semantic-conventions" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/6f/281a267b837d33505c8b4ef70644b8ebfeb7ed902a909200333916185851/openinference_instrumentation_openai_agents-1.6.1.tar.gz", hash = "sha256:39b211b7ff28d59a401b2659f3885f967fb3e17580b29b4b4aedac6b8fc98e0e", size = 26089, upload-time = "2026-06-05T19:54:22.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/54/c3d4e67bdad5170e315bb03086f034d2a60ccb26c4200010428456c9f4d6/openinference_instrumentation_openai_agents-1.6.1-py3-none-any.whl", hash = "sha256:ca5dc650fc53461cbce0f8f19bfa78d43de605c439a9d3c44d99fd0b6e10e3ba", size = 28466, upload-time = "2026-06-05T19:54:21.48Z" }, +] + +[[package]] +name = "openinference-semantic-conventions" +version = "0.1.30" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/51/8ba1182ee86fc79793d5ff2d11e7fdcda10ded2d01f3e46ca6fcf0568213/openinference_semantic_conventions-0.1.30.tar.gz", hash = "sha256:81fece76e09c83789e35c393b8b30523481eeabf1008745b955631a53e3221d9", size = 13391, upload-time = "2026-05-22T21:10:44.065Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/76/5b7e78cf0de38589b821bbe8e9c29c59a6e76edfb980488d0854cbb90f7c/openinference_semantic_conventions-0.1.30-py3-none-any.whl", hash = "sha256:36d946d3f95f699b7c4b12324ae9c1f02d6c7750df11eece56aa159cff430b3d", size = 10911, upload-time = "2026-05-22T21:10:43.04Z" }, +] + [[package]] name = "opentelemetry-api" -version = "1.36.0" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "importlib-metadata" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/27/d2/c782c88b8afbf961d6972428821c302bd1e9e7bc361352172f0ca31296e2/opentelemetry_api-1.36.0.tar.gz", hash = "sha256:9a72572b9c416d004d492cbc6e61962c0501eaf945ece9b5a0f56597d8348aa0", size = 64780, upload-time = "2025-07-29T15:12:06.02Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/1c/125e1c936c0873796771b7f04f6c93b9f1bf5d424cea90fda94a99f61da8/opentelemetry_api-1.42.1.tar.gz", hash = "sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716", size = 72296, upload-time = "2026-05-21T16:32:49.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/ca/9520cc1f3dfbbd03ac5903bbf55833e257bc64b1cf30fa8b0d6df374d821/opentelemetry_api-1.42.1-py3-none-any.whl", hash = "sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714", size = 61311, upload-time = "2026-05-21T16:32:28.822Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.42.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/9c/216acfeaedadf2e1937f4373929b20f73197c5c4a2546d4f584b7fa63813/opentelemetry_exporter_otlp_proto_common-1.42.1.tar.gz", hash = "sha256:04f1f01fb597c4249dfcd7f8b861c902c2102369d376d9d346ff38de4469a2ee", size = 21433, upload-time = "2026-05-21T16:32:55.526Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/ee/6b08dde0a022c463b88f55ae81149584b125a42183407dc1045c486cc870/opentelemetry_api-1.36.0-py3-none-any.whl", hash = "sha256:02f20bcacf666e1333b6b1f04e647dc1d5111f86b8e510238fcc56d7762cda8c", size = 65564, upload-time = "2025-07-29T15:11:47.998Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/2375e7612e1121a4518c17603b6e0b03ad94f565aafad53f464dc5be2bf6/opentelemetry_exporter_otlp_proto_common-1.42.1-py3-none-any.whl", hash = "sha256:f48d395ab815b444da118868977e9798ea354c25737d5cf39578ae894011c140", size = 17327, upload-time = "2026-05-21T16:32:33.387Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.42.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/87/ca7fc790dfdbcf4f9e9aab14a39ef1b7508ead13707e283de0b3131478d2/opentelemetry_exporter_otlp_proto_grpc-1.42.1.tar.gz", hash = "sha256:975c4461f167dd8ed8857d68d3b6b25f3d272eab896f6a9470d0f5b90e2faf15", size = 27140, upload-time = "2026-05-21T16:32:56.162Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/2b/28ba5b128f47fe8c3bab541000d6feb4b5a9bd26623ca013406f01c0fb60/opentelemetry_exporter_otlp_proto_grpc-1.42.1-py3-none-any.whl", hash = "sha256:0ae1177e2038b18a929b3098215243631ef91136cba26b7e2b12790ceb7e87cc", size = 19617, upload-time = "2026-05-21T16:32:34.278Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.63b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/6d/4de72d97ff54db1ed270c7a59c9b904b917c0ac7af429c086c388b824ddb/opentelemetry_instrumentation-0.63b1.tar.gz", hash = "sha256:32368d6ae52c8de20aa790a6ad86b10a76f09956092337ae37d675773990e541", size = 41081, upload-time = "2026-05-21T16:36:14.206Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/a1/9314e621c143e4d82a5bf7a43c2ff7a745d31023506336857607c8c543cc/opentelemetry_instrumentation-0.63b1-py3-none-any.whl", hash = "sha256:f1986716d52cc316ea5f60189098726a9071d8ecc0eee96c9ed110be08bade9c", size = 35577, upload-time = "2026-05-21T16:34:56.818Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-threading" +version = "0.63b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/90/7b0279192fab614d57af3d57584ef7ac9e38fa3df0b1d412224f6f55a85b/opentelemetry_instrumentation_threading-0.63b1.tar.gz", hash = "sha256:afa8c2cada8ed136f07b04dc8739bc861a15e9a5edea1a65e4c5e1919c62946c", size = 9080, upload-time = "2026-05-21T16:36:49.977Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/3d/3a991a4fcdf5ac82c04215e38cea4e73ad63713707014f9a70d1ab257f5f/opentelemetry_instrumentation_threading-0.63b1-py3-none-any.whl", hash = "sha256:33059298e68c94b13c38b562ad28799ec16a2fd06182ebfc762bb4e956e55d94", size = 8486, upload-time = "2026-05-21T16:35:58.084Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.42.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/55/63eac3e1089b768ba014091fdd2ae8a9a440c821ef5e2b786909c94c8836/opentelemetry_proto-1.42.1.tar.gz", hash = "sha256:c6a51e6b4f05ae63565f3a113217f3d2bfaec68f78c02d7a6c85f9010d1cfca6", size = 45839, upload-time = "2026-05-21T16:33:03.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/9d/171c02c84a76940b7e601805b3bb536985aded9168fbcc9ba52f0a730fa2/opentelemetry_proto-1.42.1-py3-none-any.whl", hash = "sha256:dedb74cba2886c59c7789b227a7a670613025a07489040050aedff6e5c0fb43c", size = 71782, upload-time = "2026-05-21T16:32:44.867Z" }, ] [[package]] name = "opentelemetry-sdk" -version = "1.36.0" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4c/85/8567a966b85a2d3f971c4d42f781c305b2b91c043724fa08fd37d158e9dc/opentelemetry_sdk-1.36.0.tar.gz", hash = "sha256:19c8c81599f51b71670661ff7495c905d8fdf6976e41622d5245b791b06fa581", size = 162557, upload-time = "2025-07-29T15:12:16.76Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/f7/b390bd9bfd703bf98a68fea1f27786c6872331fd617164a54b8a59bdc008/opentelemetry_sdk-1.42.1.tar.gz", hash = "sha256:8c834e8f8c9ba4171d4ec843d0cb8a67e4c7394d3f9e9297e582cbd9456ddbf7", size = 239262, upload-time = "2026-05-21T16:33:04.641Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/59/7bed362ad1137ba5886dac8439e84cd2df6d087be7c09574ece47ae9b22c/opentelemetry_sdk-1.36.0-py3-none-any.whl", hash = "sha256:19fe048b42e98c5c1ffe85b569b7073576ad4ce0bcb6e9b4c6a39e890a6c45fb", size = 119995, upload-time = "2025-07-29T15:12:03.181Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6b/4287766cfbde577ae2272e8884abac325aeaac0d64f41c61d5b8cc595105/opentelemetry_sdk-1.42.1-py3-none-any.whl", hash = "sha256:083cd4bbfaa5aa7b5a9e552430d9951219967cfb27aa61feb13a77aba1fc839d", size = 170907, upload-time = "2026-05-21T16:32:45.894Z" }, +] + +[[package]] +name = "opentelemetry-sdk-extension-aws" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/b3/825c93fe4c238845f1356297abea33d03b2adaafb5ae98fc257b394de124/opentelemetry_sdk_extension_aws-2.1.0.tar.gz", hash = "sha256:ff68ddecc1910f62c019d22ec0f7461713ead7f662d6a2304d4089c1a0b20416", size = 16334, upload-time = "2024-12-24T15:01:57.387Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/61/47a6a43b7935d54b5734fbf3fb0357dd5a7d0dfaa9677b7318518fe8d507/opentelemetry_sdk_extension_aws-2.1.0-py3-none-any.whl", hash = "sha256:c7cf6efc275d2c24108a468d954287ce5aab9733bac816a080cfb3117374e63a", size = 18776, upload-time = "2024-12-24T15:01:56.053Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.57b0" +version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7e/31/67dfa252ee88476a29200b0255bda8dfc2cf07b56ad66dc9a6221f7dc787/opentelemetry_semantic_conventions-0.57b0.tar.gz", hash = "sha256:609a4a79c7891b4620d64c7aac6898f872d790d75f22019913a660756f27ff32", size = 124225, upload-time = "2025-07-29T15:12:17.873Z" } +sdist = { url = "https://files.pythonhosted.org/packages/93/99/4d7dd6df64795951413ce6e815f8cf1eb191daf7196ae86574589643d5f3/opentelemetry_semantic_conventions-0.63b1.tar.gz", hash = "sha256:3daf963611334b365e98a57438183eb012d3bfb40b2d931a9af613476b8701a9", size = 148340, upload-time = "2026-05-21T16:33:05.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/7a/7fe66f5f3682b1dd47d88cc4e11f1c6c0966b737de2d16671146e23c39a5/opentelemetry_semantic_conventions-0.63b1-py3-none-any.whl", hash = "sha256:dfe5ef4dee82586b746f522b818ceb298d00b3d59f660042bd79404bff8d0682", size = 203713, upload-time = "2026-05-21T16:32:47.016Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/0c/964746fcafbd16f8ff53219ad9f6b412b34f345c75f384ad434ceaadb538/orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f", size = 5599163, upload-time = "2026-05-06T15:11:08.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/5d/b95ca542a001135cc250a49370f282f578c8f4e46cc8617d73775297eea8/orjson-3.11.9-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:135869ef917b8704ea0a94e01620e0c05021c15c52036e4663baffe75e72f8ce", size = 228986, upload-time = "2026-05-06T15:09:14.765Z" }, + { url = "https://files.pythonhosted.org/packages/80/01/be33fbff646e22f93398429ea645f20d2097aea1a6cdc1e6628e70125f83/orjson-3.11.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:115ab5f5f4a0f203cc2a5f0fb09aee503a3f771aa08392949ab5ca230c4fbdbd", size = 132558, upload-time = "2026-05-06T15:09:17.431Z" }, + { url = "https://files.pythonhosted.org/packages/4e/61/73d49333bba660a075daccca10970dc6409ce1cf42ae4046646a19468aad/orjson-3.11.9-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4da3c38a2083ca4aaf9c2a36776cce3e9328e6647b10d118948f3cfb4913ffe4", size = 128213, upload-time = "2026-05-06T15:09:18.719Z" }, + { url = "https://files.pythonhosted.org/packages/1f/7d/30e844b3dac3f74aed66b1f984daf9db3c98c0328c03d965a9e8dc06449e/orjson-3.11.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53b50b0e14084b8f7e29c5ce84c5af0f1160169b30d8a6914231d97d2fe297d4", size = 135430, upload-time = "2026-05-06T15:09:20.257Z" }, + { url = "https://files.pythonhosted.org/packages/16/64/bd815f5c610b3facc204f26ba94e87a9eb49b0d83de3d5fc1eee2402d91b/orjson-3.11.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:231742b4a11dad8d5380a435962c57e91b7c37b79be858f4ef1c0df1a259897e", size = 146178, upload-time = "2026-05-06T15:09:21.616Z" }, + { url = "https://files.pythonhosted.org/packages/c7/35/e744fd36c79b339d27beb06068b5a08a8882ef5418804d0ce545a31f718d/orjson-3.11.9-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34fd2317602587321faab75ab76c623a0117e80841a6413654f04e47f339a8fb", size = 133068, upload-time = "2026-05-06T15:09:23.228Z" }, + { url = "https://files.pythonhosted.org/packages/2a/56/d54152b67b63a0b3e556cfc549d6ce84f74d7f425ddeadc6c8a74d913da7/orjson-3.11.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71f3db16e69b667b132e0f305a833d5497da302d801508cbb051ed9a9819da47", size = 134217, upload-time = "2026-05-06T15:09:24.847Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ee/66154baf69f71c7164a268a5e888908aec5a0819d13c81d5e2755a257758/orjson-3.11.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0b34789fa0da61cf7bef0546b09c738fb195331e017e477096d129e9105ab03d", size = 141917, upload-time = "2026-05-06T15:09:26.647Z" }, + { url = "https://files.pythonhosted.org/packages/09/d3/c5824260ca8b9d7ba82648d042a3f8f4815d18c15bb98a1f30edd1bb2d83/orjson-3.11.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:87e4d4ab280b0c87424d47695bec2182caf8cfc17879ea78dab76680194abc13", size = 415356, upload-time = "2026-05-06T15:09:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/64/cb/509c2e816fe4df641d93dc92f6a89adc8df3ada8ebdee2bd44aba3264c3c/orjson-3.11.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ace6c58523302d3b97b6ac5c38a5298a54b473762b6be82726b4265c41029f92", size = 148112, upload-time = "2026-05-06T15:09:29.783Z" }, + { url = "https://files.pythonhosted.org/packages/db/b5/3ceae56d2e4962979eedb023ba6a46a4bb65f333960379be0ca470686220/orjson-3.11.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:97d0d932803c1b164fde11cb542a9efcb1e0f63b184537cca65887147906ff48", size = 137112, upload-time = "2026-05-06T15:09:31.432Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7a/81fa3f2c7bef79b04cf2ab7838e5ac74b1f12511ceab979759b0275d6bb4/orjson-3.11.9-cp310-cp310-win32.whl", hash = "sha256:b3afcf569c15577a9fe64627292daa3e6b3a70f4fb77a5df246a87ec21681b94", size = 131706, upload-time = "2026-05-06T15:09:32.707Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d8/b64600f9083c7f151ad39717a5877fccbeb0ef6d7efcb55f971ce00b6bee/orjson-3.11.9-cp310-cp310-win_amd64.whl", hash = "sha256:8697ab6a080a5c46edaad50e2bc5bd8c7ca5c66442d24104fa44ec74910a8244", size = 127282, upload-time = "2026-05-06T15:09:33.955Z" }, + { url = "https://files.pythonhosted.org/packages/1e/51/3fb9e65ae76ee97bd611869a503fa3fc0a6e81dd8b737cf3003f682df7ff/orjson-3.11.9-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f01c4818b3fc9b0da8e096722a84318071eaa118df35f6ed2344da0e73a5444f", size = 228522, upload-time = "2026-05-06T15:09:35.362Z" }, + { url = "https://files.pythonhosted.org/packages/16/fa/9d54b07cb3f3b0bfd57841478e42d7a0ece4a9f49f9907eecf5a45461687/orjson-3.11.9-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:3ebca4179031ee716ed076ffadc29428e900512f6fccee8614c9983157fcf19c", size = 128463, upload-time = "2026-05-06T15:09:37.063Z" }, + { url = "https://files.pythonhosted.org/packages/88/b1/6ceafc2eefd0a553e3be77ce6c49d107e772485d9568629376171c50e634/orjson-3.11.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48ee05097750de0ff69ed5b7bbcf0732182fd57a24043dcc2a1da780a5ead3a5", size = 132306, upload-time = "2026-05-06T15:09:38.299Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/f11311285324a40aab1e3031385c50b635a7cd0734fdaf60c7e89a696f60/orjson-3.11.9-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6082706765a95a6680d812e1daf1c0cfe8adec7831b3ff3b625693f3b461b1c", size = 127988, upload-time = "2026-05-06T15:09:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/9e/85/0ef63bcf1337f44031ce9b91b1919563f62a37527b3ea4368bb15a22e5d7/orjson-3.11.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:277fefe9d76ee17eb14debf399e3533d4d63b5f677a4d3719eb763536af1f4bd", size = 135188, upload-time = "2026-05-06T15:09:40.957Z" }, + { url = "https://files.pythonhosted.org/packages/05/94/b0d27090ea8a2095db3c2bd1b1c96f96f19bbb494d7fef33130e846e613d/orjson-3.11.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03db380e3780fa0015ed776a90f20e8e20bb11dde13b216ce19e5718e3dfba62", size = 145937, upload-time = "2026-05-06T15:09:42.249Z" }, + { url = "https://files.pythonhosted.org/packages/09/eb/75d50c29c05b8054013e221e598820a365c8e64065312e75e202ed880709/orjson-3.11.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33d7d766701847dc6729846362dc27895d2f2d2251264f9d10e7cb9878194877", size = 132758, upload-time = "2026-05-06T15:09:43.945Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/360686f39348aa88827cb6fbf7dc606fd41c831a35235e1abf1db8e3a9e6/orjson-3.11.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:147302878da387104b66bb4a8b0227d1d487e976ce41a8501916161072ed87b1", size = 133971, upload-time = "2026-05-06T15:09:45.239Z" }, + { url = "https://files.pythonhosted.org/packages/0e/30/3178eb16f3221aeef068b6f1f1ebe05f656ea5c6dffe9f6c917329fe17a3/orjson-3.11.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3513550321f8c8c811a7c3297b8a630e82dc08e4c10216d07703c997776236cd", size = 141685, upload-time = "2026-05-06T15:09:46.858Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f1/ff2f19ed0225f9680fafa42febca3570dd59444ebf190980738d376214c2/orjson-3.11.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c5d001196b89fa9cf0a4ab79766cd835b991a166e4b621ba95089edc50c429ff", size = 415167, upload-time = "2026-05-06T15:09:48.312Z" }, + { url = "https://files.pythonhosted.org/packages/9b/61/863bddf0da6e9e586765414debd54b4e58db05f560902b6d00658cb88636/orjson-3.11.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:16969c9d369c98eb084889c6e4d2d39b77c7eb38ceccf8da2a9fff62ae908980", size = 147913, upload-time = "2026-05-06T15:09:49.733Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4081492586d75b073d60c5271a8d0f05a0955cabf1e34c8473f6fcd84235/orjson-3.11.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:63e0efbc991250c0b3143488fa57d95affcabbfc63c99c48d625dd37779aafe2", size = 136959, upload-time = "2026-05-06T15:09:51.311Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bd/70b6ab193594d7abb875320c0a7c8335e846f28968c432c31042409c3c8d/orjson-3.11.9-cp311-cp311-win32.whl", hash = "sha256:14ed654580c1ed2bc217352ec82f91b047aef82951aa71c7f64e0dcb03c0e180", size = 131533, upload-time = "2026-05-06T15:09:52.637Z" }, + { url = "https://files.pythonhosted.org/packages/3f/17/1a1a228183d62d1b77e2c30d210f47dd4768b310ebe1607c63e3c0e3a71e/orjson-3.11.9-cp311-cp311-win_amd64.whl", hash = "sha256:57ea77fb70a448ce87d18fca050193202a3da5e54598f6501ca5476fb66cfe02", size = 127106, upload-time = "2026-05-06T15:09:54.204Z" }, + { url = "https://files.pythonhosted.org/packages/b8/95/285de5fa296d09681ee9c546cd4a8aeb773b701cf343dc125994f4d52953/orjson-3.11.9-cp311-cp311-win_arm64.whl", hash = "sha256:19b72ed11572a2ee51a67a903afbe5af504f84ed6f529c0fe44b0ab3fb5cc697", size = 126848, upload-time = "2026-05-06T15:09:55.551Z" }, + { url = "https://files.pythonhosted.org/packages/16/6d/11867a3ffa3a3608d84a4de51ef4dd0896d6b5cc9132fbe1daf593e677bc/orjson-3.11.9-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49", size = 228515, upload-time = "2026-05-06T15:09:57.265Z" }, + { url = "https://files.pythonhosted.org/packages/24/75/05912954c8b288f34fcf5cd4b9b071cb4f6e77b9961e175e56ebb258089f/orjson-3.11.9-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291", size = 128409, upload-time = "2026-05-06T15:09:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/ab/86/1c3a47df3bc8191ea9ac51603bbb872a95167a364320c269f2557911f406/orjson-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09", size = 132106, upload-time = "2026-05-06T15:10:00.798Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cf/b33b5f3e695ae7d63feef9d915c37cc3b8f465493dcd4f8e0b4c697a2366/orjson-3.11.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4", size = 127864, upload-time = "2026-05-06T15:10:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/31/6a/6cf69385a58208024fcb8c014e2141b8ce838aba6492b589f8acfff97fab/orjson-3.11.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882", size = 135213, upload-time = "2026-05-06T15:10:03.515Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f8/0b1bd3e8f2efcdd376af5c8cfd79eaf13f018080c0089c80ebd724e3c7fb/orjson-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff", size = 145994, upload-time = "2026-05-06T15:10:05.083Z" }, + { url = "https://files.pythonhosted.org/packages/f3/59/dab79f61044c529d2c81aecdc589b1f833a1c8dec11ba3b1c2498a02ca7e/orjson-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe", size = 132744, upload-time = "2026-05-06T15:10:06.853Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a4/82b7a2fe5d8a67a59ed831b24d59a3d46ea7d207b66e1602d376541d94a6/orjson-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61", size = 134014, upload-time = "2026-05-06T15:10:08.213Z" }, + { url = "https://files.pythonhosted.org/packages/50/c7/375e83a76851b73b2e39f3bcf0e5a19e2b89bad13e5bca97d0b293d27f24/orjson-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2", size = 141509, upload-time = "2026-05-06T15:10:09.595Z" }, + { url = "https://files.pythonhosted.org/packages/7f/7c/49d5d82a3d3097f641f094f552131f1e2723b0b8cb0fa2874ab65ecfffa6/orjson-3.11.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206", size = 415127, upload-time = "2026-05-06T15:10:11.049Z" }, + { url = "https://files.pythonhosted.org/packages/3a/dc/7446c538590d55f455647e5f3c61fc33f7108714e7afcffa6a2a033f8350/orjson-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f", size = 148025, upload-time = "2026-05-06T15:10:12.842Z" }, + { url = "https://files.pythonhosted.org/packages/df/e5/4d2d8af06f788329b4f78f8cc3679bb395392fcaa1e4d8d3c33e85308fa4/orjson-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa", size = 136943, upload-time = "2026-05-06T15:10:14.405Z" }, + { url = "https://files.pythonhosted.org/packages/06/69/850264ccf6d80f6b174620d30a87f65c9b1490aba33fe6b62798e618cad3/orjson-3.11.9-cp312-cp312-win32.whl", hash = "sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470", size = 131606, upload-time = "2026-05-06T15:10:15.791Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d5/973a43fc9c55e20f2051e9830997649f669be0cb3ca52192087c0143f118/orjson-3.11.9-cp312-cp312-win_amd64.whl", hash = "sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be", size = 127101, upload-time = "2026-05-06T15:10:17.129Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/495470f0e4a18f73fa10b7f6b84b464ec4cc5291c4e0c7c2a6c400bef006/orjson-3.11.9-cp312-cp312-win_arm64.whl", hash = "sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624", size = 126736, upload-time = "2026-05-06T15:10:18.645Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/93fcc25907235c344ae73122f8a4e01d2d393ef062b4af7d2e2487a32c37/orjson-3.11.9-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021", size = 228458, upload-time = "2026-05-06T15:10:20.079Z" }, + { url = "https://files.pythonhosted.org/packages/8f/27/b1e6dadb3c080313c03fdd8067b85e6a0460c7d8d6a1c3984ef77b904e4d/orjson-3.11.9-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c", size = 128368, upload-time = "2026-05-06T15:10:21.549Z" }, + { url = "https://files.pythonhosted.org/packages/21/0f/c9ede0bf052f6b4051e64a7d4fa91b725cccf8321a6a786e86eb03519f00/orjson-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81", size = 132070, upload-time = "2026-05-06T15:10:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/fd/26/d398e28048dc18205bbe812f2c88cb9b40313db2470778e25964796458fe/orjson-3.11.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a", size = 127892, upload-time = "2026-05-06T15:10:24.714Z" }, + { url = "https://files.pythonhosted.org/packages/66/60/52b0054c4c700d5aa7fc5b7ca96917400d8f061307778578e67a10e25852/orjson-3.11.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10", size = 135217, upload-time = "2026-05-06T15:10:26.084Z" }, + { url = "https://files.pythonhosted.org/packages/d5/97/1e3dc2b2a28b7b2528f403d2fc1d79ec5f39af3bc143ab65d3ec26426385/orjson-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362", size = 145980, upload-time = "2026-05-06T15:10:28.062Z" }, + { url = "https://files.pythonhosted.org/packages/fc/39/31fbfe7850f2de32dee7e7e5c09f26d403ab01e440ac96001c6b01ad3c99/orjson-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97", size = 132738, upload-time = "2026-05-06T15:10:29.727Z" }, + { url = "https://files.pythonhosted.org/packages/a1/08/dca0082dd2a194acb93e5457e73455388e2e2ca464a2672449a9ddbb679d/orjson-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218", size = 134033, upload-time = "2026-05-06T15:10:31.152Z" }, + { url = "https://files.pythonhosted.org/packages/11/d4/5bdb0626801230139987385554c5d4c42255218ac906525bf4347f22cd95/orjson-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9", size = 141492, upload-time = "2026-05-06T15:10:32.641Z" }, + { url = "https://files.pythonhosted.org/packages/fa/88/a21fb53b3ede6703aede6dce4710ed4111e5b201cfa6bbff5e544f9d47d7/orjson-3.11.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677", size = 415087, upload-time = "2026-05-06T15:10:34.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/57/1b30daf70f0d8180e9a73cefbfbdd99e4bf19eb020466502b01fba7e0e50/orjson-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4", size = 148031, upload-time = "2026-05-06T15:10:36.358Z" }, + { url = "https://files.pythonhosted.org/packages/04/83/45fbb6d962e260807f99441db9613cee868ceda4baceda59b3720a563f97/orjson-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0", size = 136915, upload-time = "2026-05-06T15:10:38.013Z" }, + { url = "https://files.pythonhosted.org/packages/5f/cc/2d10025f9056d376e4127ec05a5808b218d46f035fdc08178a5411b34250/orjson-3.11.9-cp313-cp313-win32.whl", hash = "sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32", size = 131613, upload-time = "2026-05-06T15:10:39.569Z" }, + { url = "https://files.pythonhosted.org/packages/67/bd/2775ff28bfe883b9aa1ff348300542eb2ef1ee18d8ae0e3a49846817a865/orjson-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979", size = 127086, upload-time = "2026-05-06T15:10:41.262Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/d26799e580939e32a7da9a39531bc9e58e15ca32ffaa6a8cb3e9bb0d22cd/orjson-3.11.9-cp313-cp313-win_arm64.whl", hash = "sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254", size = 126696, upload-time = "2026-05-06T15:10:42.651Z" }, + { url = "https://files.pythonhosted.org/packages/8e/eb/5da01e356015aee6ecfa1187ced87aef51364e306f5e695dd52719bf0e78/orjson-3.11.9-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e", size = 228465, upload-time = "2026-05-06T15:10:44.097Z" }, + { url = "https://files.pythonhosted.org/packages/64/62/3e0e0c14c957133bcd855395c62b55ed4e3b0af23ffea11b032cb1dcbdb1/orjson-3.11.9-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e", size = 128364, upload-time = "2026-05-06T15:10:45.839Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5a/07d8aa117211a8ed7630bda80c8c0b14d04e0f8dcf99bcf49656e4a710eb/orjson-3.11.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0", size = 132063, upload-time = "2026-05-06T15:10:47.267Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ec/4acaf21483e18aa945be74a474c74b434f284b549f275a0a39b9f98956e9/orjson-3.11.9-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124", size = 122356, upload-time = "2026-05-06T15:10:48.765Z" }, + { url = "https://files.pythonhosted.org/packages/13/d8/5f0555e7638801323b7a75850f92e7dfa891bc84fe27a1ba4449170d1200/orjson-3.11.9-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c", size = 129592, upload-time = "2026-05-06T15:10:50.13Z" }, + { url = "https://files.pythonhosted.org/packages/b6/30/ed9860412a3603ceb3c5955bfd72d28b9d0e7ba6ed81add14f83d7114236/orjson-3.11.9-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7", size = 140491, upload-time = "2026-05-06T15:10:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/d0/17/adc514dea7ac7c505527febf884934b815d34f0c7b8693c1a8b39c5c4a57/orjson-3.11.9-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1", size = 127309, upload-time = "2026-05-06T15:10:53.329Z" }, + { url = "https://files.pythonhosted.org/packages/76/3e/c0b690253f0b82d86e99949af13533363acfb5432ecb5d53dd5b3bce9c34/orjson-3.11.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db", size = 134030, upload-time = "2026-05-06T15:10:54.988Z" }, + { url = "https://files.pythonhosted.org/packages/c1/7a/bc82a0bb25e9faaf92dc4d9ef002732efc09737706af83e346788641d4a7/orjson-3.11.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b", size = 141482, upload-time = "2026-05-06T15:10:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/01/55/e69188b939f77d5d32a9833745ace31ea5ccae3ab613a1ec185d3cd2c4fb/orjson-3.11.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972", size = 415178, upload-time = "2026-05-06T15:10:58.446Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1a/b8a5a7ac527e80b9cb11d51e3f6689b709279183264b9ec5c7bc680bb8b5/orjson-3.11.9-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0", size = 148089, upload-time = "2026-05-06T15:11:00.441Z" }, + { url = "https://files.pythonhosted.org/packages/97/4e/00503f64204bf859b37213a63927028f30fb6268cd8677fb0a5ad48155e1/orjson-3.11.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586", size = 136921, upload-time = "2026-05-06T15:11:02.176Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ba/a23b82a0a8d0ed7bed4e5f5035aae751cad4ff6a1e8d2ecd14d8860f5929/orjson-3.11.9-cp314-cp314-win32.whl", hash = "sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673", size = 131638, upload-time = "2026-05-06T15:11:03.696Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/0c6798456bade745c75c452342dabacce5798196483e77e643be1f53877d/orjson-3.11.9-cp314-cp314-win_amd64.whl", hash = "sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b", size = 127078, upload-time = "2026-05-06T15:11:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/16/21/5a3f1e8913103b703a436a5664238e5b965ec392b555fe68943ea3691e6b/orjson-3.11.9-cp314-cp314-win_arm64.whl", hash = "sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9", size = 126687, upload-time = "2026-05-06T15:11:06.602Z" }, +] + +[[package]] +name = "ormsgpack" +version = "1.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/0c/f1761e21486942ab9bb6feaebc610fa074f7c5e496e6962dea5873348077/ormsgpack-1.12.2.tar.gz", hash = "sha256:944a2233640273bee67521795a73cf1e959538e0dfb7ac635505010455e53b33", size = 39031, upload-time = "2026-01-18T20:55:28.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/fa/a91f70829ebccf6387c4946e0a1a109f6ba0d6a28d65f628bedfad94b890/ormsgpack-1.12.2-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c1429217f8f4d7fcb053523bbbac6bed5e981af0b85ba616e6df7cce53c19657", size = 378262, upload-time = "2026-01-18T20:55:22.284Z" }, + { url = "https://files.pythonhosted.org/packages/5f/62/3698a9a0c487252b5c6a91926e5654e79e665708ea61f67a8bdeceb022bf/ormsgpack-1.12.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f13034dc6c84a6280c6c33db7ac420253852ea233fc3ee27c8875f8dd651163", size = 203034, upload-time = "2026-01-18T20:55:53.324Z" }, + { url = "https://files.pythonhosted.org/packages/66/3a/f716f64edc4aec2744e817660b317e2f9bb8de372338a95a96198efa1ac1/ormsgpack-1.12.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:59f5da97000c12bc2d50e988bdc8576b21f6ab4e608489879d35b2c07a8ab51a", size = 210538, upload-time = "2026-01-18T20:55:20.097Z" }, + { url = "https://files.pythonhosted.org/packages/72/30/a436be9ce27d693d4e19fa94900028067133779f09fc45776db3f689c822/ormsgpack-1.12.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e4459c3f27066beadb2b81ea48a076a417aafffff7df1d3c11c519190ed44f2", size = 212401, upload-time = "2026-01-18T20:55:46.447Z" }, + { url = "https://files.pythonhosted.org/packages/10/c5/cde98300fd33fee84ca71de4751b19aeeca675f0cf3c0ec4b043f40f3b76/ormsgpack-1.12.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a1c460655d7288407ffa09065e322a7231997c0d62ce914bf3a96ad2dc6dedd", size = 387080, upload-time = "2026-01-18T20:56:00.884Z" }, + { url = "https://files.pythonhosted.org/packages/6a/31/30bf445ef827546747c10889dd254b3d84f92b591300efe4979d792f4c41/ormsgpack-1.12.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:458e4568be13d311ef7d8877275e7ccbe06c0e01b39baaac874caaa0f46d826c", size = 482346, upload-time = "2026-01-18T20:55:39.831Z" }, + { url = "https://files.pythonhosted.org/packages/2e/f5/e1745ddf4fa246c921b5ca253636c4c700ff768d78032f79171289159f6e/ormsgpack-1.12.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8cde5eaa6c6cbc8622db71e4a23de56828e3d876aeb6460ffbcb5b8aff91093b", size = 425178, upload-time = "2026-01-18T20:55:27.106Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a2/e6532ed7716aed03dede8df2d0d0d4150710c2122647d94b474147ccd891/ormsgpack-1.12.2-cp310-cp310-win_amd64.whl", hash = "sha256:dc7a33be14c347893edbb1ceda89afbf14c467d593a5ee92c11de4f1666b4d4f", size = 117183, upload-time = "2026-01-18T20:55:55.52Z" }, + { url = "https://files.pythonhosted.org/packages/4b/08/8b68f24b18e69d92238aa8f258218e6dfeacf4381d9d07ab8df303f524a9/ormsgpack-1.12.2-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bd5f4bf04c37888e864f08e740c5a573c4017f6fd6e99fa944c5c935fabf2dd9", size = 378266, upload-time = "2026-01-18T20:55:59.876Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/29fc13044ecb7c153523ae0a1972269fcd613650d1fa1a9cec1044c6b666/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34d5b28b3570e9fed9a5a76528fc7230c3c76333bc214798958e58e9b79cc18a", size = 203035, upload-time = "2026-01-18T20:55:30.59Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c2/00169fb25dd8f9213f5e8a549dfb73e4d592009ebc85fbbcd3e1dcac575b/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3708693412c28f3538fb5a65da93787b6bbab3484f6bc6e935bfb77a62400ae5", size = 210539, upload-time = "2026-01-18T20:55:48.569Z" }, + { url = "https://files.pythonhosted.org/packages/1b/33/543627f323ff3c73091f51d6a20db28a1a33531af30873ea90c5ac95a9b5/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43013a3f3e2e902e1d05e72c0f1aeb5bedbb8e09240b51e26792a3c89267e181", size = 212401, upload-time = "2026-01-18T20:56:10.101Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5d/f70e2c3da414f46186659d24745483757bcc9adccb481a6eb93e2b729301/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7c8b1667a72cbba74f0ae7ecf3105a5e01304620ed14528b2cb4320679d2869b", size = 387082, upload-time = "2026-01-18T20:56:12.047Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d6/06e8dc920c7903e051f30934d874d4afccc9bb1c09dcaf0bc03a7de4b343/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:df6961442140193e517303d0b5d7bc2e20e69a879c2d774316125350c4a76b92", size = 482346, upload-time = "2026-01-18T20:56:05.152Z" }, + { url = "https://files.pythonhosted.org/packages/66/c4/f337ac0905eed9c393ef990c54565cd33644918e0a8031fe48c098c71dbf/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6a4c34ddef109647c769d69be65fa1de7a6022b02ad45546a69b3216573eb4a", size = 425181, upload-time = "2026-01-18T20:55:37.83Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/6d5758fabef3babdf4bbbc453738cc7de9cd3334e4c38dd5737e27b85653/ormsgpack-1.12.2-cp311-cp311-win_amd64.whl", hash = "sha256:73670ed0375ecc303858e3613f407628dd1fca18fe6ac57b7b7ce66cc7bb006c", size = 117182, upload-time = "2026-01-18T20:55:31.472Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/17a15549233c37e7fd054c48fe9207492e06b026dbd872b826a0b5f833b6/ormsgpack-1.12.2-cp311-cp311-win_arm64.whl", hash = "sha256:c2be829954434e33601ae5da328cccce3266b098927ca7a30246a0baec2ce7bd", size = 111464, upload-time = "2026-01-18T20:55:38.811Z" }, + { url = "https://files.pythonhosted.org/packages/4c/36/16c4b1921c308a92cef3bf6663226ae283395aa0ff6e154f925c32e91ff5/ormsgpack-1.12.2-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7a29d09b64b9694b588ff2f80e9826bdceb3a2b91523c5beae1fab27d5c940e7", size = 378618, upload-time = "2026-01-18T20:55:50.835Z" }, + { url = "https://files.pythonhosted.org/packages/c0/68/468de634079615abf66ed13bb5c34ff71da237213f29294363beeeca5306/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b39e629fd2e1c5b2f46f99778450b59454d1f901bc507963168985e79f09c5d", size = 203186, upload-time = "2026-01-18T20:56:11.163Z" }, + { url = "https://files.pythonhosted.org/packages/73/a9/d756e01961442688b7939bacd87ce13bfad7d26ce24f910f6028178b2cc8/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:958dcb270d30a7cb633a45ee62b9444433fa571a752d2ca484efdac07480876e", size = 210738, upload-time = "2026-01-18T20:56:09.181Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ba/795b1036888542c9113269a3f5690ab53dd2258c6fb17676ac4bd44fcf94/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d379d72b6c5e964851c77cfedfb386e474adee4fd39791c2c5d9efb53505cc", size = 212569, upload-time = "2026-01-18T20:56:06.135Z" }, + { url = "https://files.pythonhosted.org/packages/6c/aa/bff73c57497b9e0cba8837c7e4bcab584b1a6dbc91a5dd5526784a5030c8/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8463a3fc5f09832e67bdb0e2fda6d518dc4281b133166146a67f54c08496442e", size = 387166, upload-time = "2026-01-18T20:55:36.738Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cf/f8283cba44bcb7b14f97b6274d449db276b3a86589bdb363169b51bc12de/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:eddffb77eff0bad4e67547d67a130604e7e2dfbb7b0cde0796045be4090f35c6", size = 482498, upload-time = "2026-01-18T20:55:29.626Z" }, + { url = "https://files.pythonhosted.org/packages/05/be/71e37b852d723dfcbe952ad04178c030df60d6b78eba26bfd14c9a40575e/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcd55e5f6ba0dbce624942adf9f152062135f991a0126064889f68eb850de0dd", size = 425518, upload-time = "2026-01-18T20:55:49.556Z" }, + { url = "https://files.pythonhosted.org/packages/7a/0c/9803aa883d18c7ef197213cd2cbf73ba76472a11fe100fb7dab2884edf48/ormsgpack-1.12.2-cp312-cp312-win_amd64.whl", hash = "sha256:d024b40828f1dde5654faebd0d824f9cc29ad46891f626272dd5bfd7af2333a4", size = 117462, upload-time = "2026-01-18T20:55:47.726Z" }, + { url = "https://files.pythonhosted.org/packages/c8/9e/029e898298b2cc662f10d7a15652a53e3b525b1e7f07e21fef8536a09bb8/ormsgpack-1.12.2-cp312-cp312-win_arm64.whl", hash = "sha256:da538c542bac7d1c8f3f2a937863dba36f013108ce63e55745941dda4b75dbb6", size = 111559, upload-time = "2026-01-18T20:55:54.273Z" }, + { url = "https://files.pythonhosted.org/packages/eb/29/bb0eba3288c0449efbb013e9c6f58aea79cf5cb9ee1921f8865f04c1a9d7/ormsgpack-1.12.2-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5ea60cb5f210b1cfbad8c002948d73447508e629ec375acb82910e3efa8ff355", size = 378661, upload-time = "2026-01-18T20:55:57.765Z" }, + { url = "https://files.pythonhosted.org/packages/6e/31/5efa31346affdac489acade2926989e019e8ca98129658a183e3add7af5e/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3601f19afdbea273ed70b06495e5794606a8b690a568d6c996a90d7255e51c1", size = 203194, upload-time = "2026-01-18T20:56:08.252Z" }, + { url = "https://files.pythonhosted.org/packages/eb/56/d0087278beef833187e0167f8527235ebe6f6ffc2a143e9de12a98b1ce87/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29a9f17a3dac6054c0dce7925e0f4995c727f7c41859adf9b5572180f640d172", size = 210778, upload-time = "2026-01-18T20:55:17.694Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a2/072343e1413d9443e5a252a8eb591c2d5b1bffbe5e7bfc78c069361b92eb/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39c1bd2092880e413902910388be8715f70b9f15f20779d44e673033a6146f2d", size = 212592, upload-time = "2026-01-18T20:55:32.747Z" }, + { url = "https://files.pythonhosted.org/packages/a2/8b/a0da3b98a91d41187a63b02dda14267eefc2a74fcb43cc2701066cf1510e/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50b7249244382209877deedeee838aef1542f3d0fc28b8fe71ca9d7e1896a0d7", size = 387164, upload-time = "2026-01-18T20:55:40.853Z" }, + { url = "https://files.pythonhosted.org/packages/19/bb/6d226bc4cf9fc20d8eb1d976d027a3f7c3491e8f08289a2e76abe96a65f3/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:5af04800d844451cf102a59c74a841324868d3f1625c296a06cc655c542a6685", size = 482516, upload-time = "2026-01-18T20:55:42.033Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f1/bb2c7223398543dedb3dbf8bb93aaa737b387de61c5feaad6f908841b782/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cec70477d4371cd524534cd16472d8b9cc187e0e3043a8790545a9a9b296c258", size = 425539, upload-time = "2026-01-18T20:55:24.727Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e8/0fb45f57a2ada1fed374f7494c8cd55e2f88ccd0ab0a669aa3468716bf5f/ormsgpack-1.12.2-cp313-cp313-win_amd64.whl", hash = "sha256:21f4276caca5c03a818041d637e4019bc84f9d6ca8baa5ea03e5cc8bf56140e9", size = 117459, upload-time = "2026-01-18T20:55:56.876Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d4/0cfeea1e960d550a131001a7f38a5132c7ae3ebde4c82af1f364ccc5d904/ormsgpack-1.12.2-cp313-cp313-win_arm64.whl", hash = "sha256:baca4b6773d20a82e36d6fd25f341064244f9f86a13dead95dd7d7f996f51709", size = 111577, upload-time = "2026-01-18T20:55:43.605Z" }, + { url = "https://files.pythonhosted.org/packages/94/16/24d18851334be09c25e87f74307c84950f18c324a4d3c0b41dabdbf19c29/ormsgpack-1.12.2-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bc68dd5915f4acf66ff2010ee47c8906dc1cf07399b16f4089f8c71733f6e36c", size = 378717, upload-time = "2026-01-18T20:55:26.164Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a2/88b9b56f83adae8032ac6a6fa7f080c65b3baf9b6b64fd3d37bd202991d4/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46d084427b4132553940070ad95107266656cb646ea9da4975f85cb1a6676553", size = 203183, upload-time = "2026-01-18T20:55:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/a9/80/43e4555963bf602e5bdc79cbc8debd8b6d5456c00d2504df9775e74b450b/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c010da16235806cf1d7bc4c96bf286bfa91c686853395a299b3ddb49499a3e13", size = 210814, upload-time = "2026-01-18T20:55:33.973Z" }, + { url = "https://files.pythonhosted.org/packages/78/e1/7cfbf28de8bca6efe7e525b329c31277d1b64ce08dcba723971c241a9d60/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18867233df592c997154ff942a6503df274b5ac1765215bceba7a231bea2745d", size = 212634, upload-time = "2026-01-18T20:55:28.634Z" }, + { url = "https://files.pythonhosted.org/packages/95/f8/30ae5716e88d792a4e879debee195653c26ddd3964c968594ddef0a3cc7e/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b009049086ddc6b8f80c76b3955df1aa22a5fbd7673c525cd63bf91f23122ede", size = 387139, upload-time = "2026-01-18T20:56:02.013Z" }, + { url = "https://files.pythonhosted.org/packages/dc/81/aee5b18a3e3a0e52f718b37ab4b8af6fae0d9d6a65103036a90c2a8ffb5d/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1dcc17d92b6390d4f18f937cf0b99054824a7815818012ddca925d6e01c2e49e", size = 482578, upload-time = "2026-01-18T20:55:35.117Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/71c9ba472d5d45f7546317f467a5fc941929cd68fb32796ca3d13dcbaec2/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f04b5e896d510b07c0ad733d7fce2d44b260c5e6c402d272128f8941984e4285", size = 425539, upload-time = "2026-01-18T20:56:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/2e/a6/ac99cd7fe77e822fed5250ff4b86fa66dd4238937dd178d2299f10b69816/ormsgpack-1.12.2-cp314-cp314-win_amd64.whl", hash = "sha256:ae3aba7eed4ca7cb79fd3436eddd29140f17ea254b91604aa1eb19bfcedb990f", size = 117493, upload-time = "2026-01-18T20:56:07.343Z" }, + { url = "https://files.pythonhosted.org/packages/3a/67/339872846a1ae4592535385a1c1f93614138566d7af094200c9c3b45d1e5/ormsgpack-1.12.2-cp314-cp314-win_arm64.whl", hash = "sha256:118576ea6006893aea811b17429bfc561b4778fad393f5f538c84af70b01260c", size = 111579, upload-time = "2026-01-18T20:55:21.161Z" }, + { url = "https://files.pythonhosted.org/packages/49/c2/6feb972dc87285ad381749d3882d8aecbde9f6ecf908dd717d33d66df095/ormsgpack-1.12.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7121b3d355d3858781dc40dafe25a32ff8a8242b9d80c692fd548a4b1f7fd3c8", size = 378721, upload-time = "2026-01-18T20:55:52.12Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9a/900a6b9b413e0f8a471cf07830f9cf65939af039a362204b36bd5b581d8b/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ee766d2e78251b7a63daf1cddfac36a73562d3ddef68cacfb41b2af64698033", size = 203170, upload-time = "2026-01-18T20:55:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/87/4c/27a95466354606b256f24fad464d7c97ab62bce6cc529dd4673e1179b8fb/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:292410a7d23de9b40444636b9b8f1e4e4b814af7f1ef476e44887e52a123f09d", size = 212816, upload-time = "2026-01-18T20:55:23.501Z" }, + { url = "https://files.pythonhosted.org/packages/73/cd/29cee6007bddf7a834e6cd6f536754c0535fcb939d384f0f37a38b1cddb8/ormsgpack-1.12.2-cp314-cp314t-win_amd64.whl", hash = "sha256:837dd316584485b72ef451d08dd3e96c4a11d12e4963aedb40e08f89685d8ec2", size = 117232, upload-time = "2026-01-18T20:55:45.448Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pathable" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/f3/5a20387de9bcd0607871bfc2198ee0e15836da7baa4592ccd7f24c27c986/pathable-0.6.0.tar.gz", hash = "sha256:6404b8b82aef5ff0fd478934137128b99b12212ba35afdde5525ca4f8388ea58", size = 18970, upload-time = "2026-05-19T18:15:11.911Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/75/7d591371c6c39c73de5ce5da5a2cc7b72d1d1cd3f8f4638f553c01c37b11/opentelemetry_semantic_conventions-0.57b0-py3-none-any.whl", hash = "sha256:757f7e76293294f124c827e514c2a3144f191ef175b069ce8d1211e1e38e9e78", size = 201627, upload-time = "2025-07-29T15:12:04.174Z" }, + { url = "https://files.pythonhosted.org/packages/a2/e8/6d75ffd9784bce2e93d1ae4415649427e39a53bb172d4672b2b59c6f0a7b/pathable-0.6.0-py3-none-any.whl", hash = "sha256:82c4ca6c98c502ad12e0d4e9779b6210afee93c38990988c8c5d1b49bdcdf566", size = 18983, upload-time = "2026-05-19T18:15:10.728Z" }, ] [[package]] -name = "packaging" -version = "25.0" +name = "pathspec" +version = "1.1.1" 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" }, +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "pillow" +version = "12.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/c2/669d88644cddb1485bd9534e63e8cf476c8e51cb3c3a1297677023505c0e/pillow-12.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a", size = 5392418, upload-time = "2026-07-01T11:53:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ba/3762f376a2948e3036488d773a146e0ae6ecc2ca03ac20e2615bd0b2ba02/pillow-12.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7", size = 4785287, upload-time = "2026-07-01T11:53:29.761Z" }, + { url = "https://files.pythonhosted.org/packages/07/50/b5d688cc9c52d4482f3d5bcab6ce20bc2a74a85d2343841c907444a3be2c/pillow-12.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f", size = 6253754, upload-time = "2026-07-01T11:53:32.298Z" }, + { url = "https://files.pythonhosted.org/packages/4e/89/36f4cd76cf4baf05c50ababb976249153f18c959171c7f6ba09a6f217260/pillow-12.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec", size = 6925605, upload-time = "2026-07-01T11:53:34.487Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c0/4de58cf6633b9e3a6061ef4be6fb91fc3c90b812ece886f531e3c523d777/pillow-12.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468", size = 6327788, upload-time = "2026-07-01T11:53:36.433Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/14d53682a19550dbbaf3b598f807d5457646c510805a44c7d7891cd1cd1a/pillow-12.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed", size = 7036288, upload-time = "2026-07-01T11:53:38.712Z" }, + { url = "https://files.pythonhosted.org/packages/38/1d/36279e3c77efe034e4cc2b0393ee74ffdb5a62391dacbf9b916154f5f0b8/pillow-12.3.0-cp310-cp310-win32.whl", hash = "sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1", size = 6472396, upload-time = "2026-07-01T11:53:40.781Z" }, + { url = "https://files.pythonhosted.org/packages/48/7c/8fa0039574c476d7c6fa57dd7c32a130436877c6ec1e5ce1cc8ec44878c1/pillow-12.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb", size = 7226887, upload-time = "2026-07-01T11:53:42.764Z" }, + { url = "https://files.pythonhosted.org/packages/fa/17/e324be141d173c1c919428066c3259f21c1b8982e564e01a4a81e96dbdcf/pillow-12.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f", size = 2568039, upload-time = "2026-07-01T11:53:45.372Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" }, + { url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" }, + { url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" }, + { url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" }, + { url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" }, + { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, + { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, + { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, + { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" }, + { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" }, + { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" }, + { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" }, + { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" }, + { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" }, + { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" }, + { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" }, + { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" }, + { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" }, + { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" }, + { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" }, + { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" }, + { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" }, + { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" }, + { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" }, + { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" }, + { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" }, + { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" }, + { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" }, + { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" }, + { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" }, + { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" }, + { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" }, + { url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" }, + { url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" }, ] [[package]] @@ -1820,11 +3326,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.4.0" +version = "4.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, + { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, ] [[package]] @@ -1836,125 +3342,159 @@ 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 = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + [[package]] name = "propcache" -version = "0.3.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139, upload-time = "2025-06-09T22:56:06.081Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/14/510deed325e262afeb8b360043c5d7c960da7d3ecd6d6f9496c9c56dc7f4/propcache-0.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:22d9962a358aedbb7a2e36187ff273adeaab9743373a272976d2e348d08c7770", size = 73178, upload-time = "2025-06-09T22:53:40.126Z" }, - { url = "https://files.pythonhosted.org/packages/cd/4e/ad52a7925ff01c1325653a730c7ec3175a23f948f08626a534133427dcff/propcache-0.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d0fda578d1dc3f77b6b5a5dce3b9ad69a8250a891760a548df850a5e8da87f3", size = 43133, upload-time = "2025-06-09T22:53:41.965Z" }, - { url = "https://files.pythonhosted.org/packages/63/7c/e9399ba5da7780871db4eac178e9c2e204c23dd3e7d32df202092a1ed400/propcache-0.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3def3da3ac3ce41562d85db655d18ebac740cb3fa4367f11a52b3da9d03a5cc3", size = 43039, upload-time = "2025-06-09T22:53:43.268Z" }, - { url = "https://files.pythonhosted.org/packages/22/e1/58da211eb8fdc6fc854002387d38f415a6ca5f5c67c1315b204a5d3e9d7a/propcache-0.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bec58347a5a6cebf239daba9bda37dffec5b8d2ce004d9fe4edef3d2815137e", size = 201903, upload-time = "2025-06-09T22:53:44.872Z" }, - { url = "https://files.pythonhosted.org/packages/c4/0a/550ea0f52aac455cb90111c8bab995208443e46d925e51e2f6ebdf869525/propcache-0.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55ffda449a507e9fbd4aca1a7d9aa6753b07d6166140e5a18d2ac9bc49eac220", size = 213362, upload-time = "2025-06-09T22:53:46.707Z" }, - { url = "https://files.pythonhosted.org/packages/5a/af/9893b7d878deda9bb69fcf54600b247fba7317761b7db11fede6e0f28bd0/propcache-0.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64a67fb39229a8a8491dd42f864e5e263155e729c2e7ff723d6e25f596b1e8cb", size = 210525, upload-time = "2025-06-09T22:53:48.547Z" }, - { url = "https://files.pythonhosted.org/packages/7c/bb/38fd08b278ca85cde36d848091ad2b45954bc5f15cce494bb300b9285831/propcache-0.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da1cf97b92b51253d5b68cf5a2b9e0dafca095e36b7f2da335e27dc6172a614", size = 198283, upload-time = "2025-06-09T22:53:50.067Z" }, - { url = "https://files.pythonhosted.org/packages/78/8c/9fe55bd01d362bafb413dfe508c48753111a1e269737fa143ba85693592c/propcache-0.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f559e127134b07425134b4065be45b166183fdcb433cb6c24c8e4149056ad50", size = 191872, upload-time = "2025-06-09T22:53:51.438Z" }, - { url = "https://files.pythonhosted.org/packages/54/14/4701c33852937a22584e08abb531d654c8bcf7948a8f87ad0a4822394147/propcache-0.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aff2e4e06435d61f11a428360a932138d0ec288b0a31dd9bd78d200bd4a2b339", size = 199452, upload-time = "2025-06-09T22:53:53.229Z" }, - { url = "https://files.pythonhosted.org/packages/16/44/447f2253d859602095356007657ee535e0093215ea0b3d1d6a41d16e5201/propcache-0.3.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4927842833830942a5d0a56e6f4839bc484785b8e1ce8d287359794818633ba0", size = 191567, upload-time = "2025-06-09T22:53:54.541Z" }, - { url = "https://files.pythonhosted.org/packages/f2/b3/e4756258749bb2d3b46defcff606a2f47410bab82be5824a67e84015b267/propcache-0.3.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6107ddd08b02654a30fb8ad7a132021759d750a82578b94cd55ee2772b6ebea2", size = 193015, upload-time = "2025-06-09T22:53:56.44Z" }, - { url = "https://files.pythonhosted.org/packages/1e/df/e6d3c7574233164b6330b9fd697beeac402afd367280e6dc377bb99b43d9/propcache-0.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:70bd8b9cd6b519e12859c99f3fc9a93f375ebd22a50296c3a295028bea73b9e7", size = 204660, upload-time = "2025-06-09T22:53:57.839Z" }, - { url = "https://files.pythonhosted.org/packages/b2/53/e4d31dd5170b4a0e2e6b730f2385a96410633b4833dc25fe5dffd1f73294/propcache-0.3.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2183111651d710d3097338dd1893fcf09c9f54e27ff1a8795495a16a469cc90b", size = 206105, upload-time = "2025-06-09T22:53:59.638Z" }, - { url = "https://files.pythonhosted.org/packages/7f/fe/74d54cf9fbe2a20ff786e5f7afcfde446588f0cf15fb2daacfbc267b866c/propcache-0.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fb075ad271405dcad8e2a7ffc9a750a3bf70e533bd86e89f0603e607b93aa64c", size = 196980, upload-time = "2025-06-09T22:54:01.071Z" }, - { url = "https://files.pythonhosted.org/packages/22/ec/c469c9d59dada8a7679625e0440b544fe72e99311a4679c279562051f6fc/propcache-0.3.2-cp310-cp310-win32.whl", hash = "sha256:404d70768080d3d3bdb41d0771037da19d8340d50b08e104ca0e7f9ce55fce70", size = 37679, upload-time = "2025-06-09T22:54:03.003Z" }, - { url = "https://files.pythonhosted.org/packages/38/35/07a471371ac89d418f8d0b699c75ea6dca2041fbda360823de21f6a9ce0a/propcache-0.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:7435d766f978b4ede777002e6b3b6641dd229cd1da8d3d3106a45770365f9ad9", size = 41459, upload-time = "2025-06-09T22:54:04.134Z" }, - { url = "https://files.pythonhosted.org/packages/80/8d/e8b436717ab9c2cfc23b116d2c297305aa4cd8339172a456d61ebf5669b8/propcache-0.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0b8d2f607bd8f80ddc04088bc2a037fdd17884a6fcadc47a96e334d72f3717be", size = 74207, upload-time = "2025-06-09T22:54:05.399Z" }, - { url = "https://files.pythonhosted.org/packages/d6/29/1e34000e9766d112171764b9fa3226fa0153ab565d0c242c70e9945318a7/propcache-0.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06766d8f34733416e2e34f46fea488ad5d60726bb9481d3cddf89a6fa2d9603f", size = 43648, upload-time = "2025-06-09T22:54:08.023Z" }, - { url = "https://files.pythonhosted.org/packages/46/92/1ad5af0df781e76988897da39b5f086c2bf0f028b7f9bd1f409bb05b6874/propcache-0.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2dc1f4a1df4fecf4e6f68013575ff4af84ef6f478fe5344317a65d38a8e6dc9", size = 43496, upload-time = "2025-06-09T22:54:09.228Z" }, - { url = "https://files.pythonhosted.org/packages/b3/ce/e96392460f9fb68461fabab3e095cb00c8ddf901205be4eae5ce246e5b7e/propcache-0.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be29c4f4810c5789cf10ddf6af80b041c724e629fa51e308a7a0fb19ed1ef7bf", size = 217288, upload-time = "2025-06-09T22:54:10.466Z" }, - { url = "https://files.pythonhosted.org/packages/c5/2a/866726ea345299f7ceefc861a5e782b045545ae6940851930a6adaf1fca6/propcache-0.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59d61f6970ecbd8ff2e9360304d5c8876a6abd4530cb752c06586849ac8a9dc9", size = 227456, upload-time = "2025-06-09T22:54:11.828Z" }, - { url = "https://files.pythonhosted.org/packages/de/03/07d992ccb6d930398689187e1b3c718339a1c06b8b145a8d9650e4726166/propcache-0.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62180e0b8dbb6b004baec00a7983e4cc52f5ada9cd11f48c3528d8cfa7b96a66", size = 225429, upload-time = "2025-06-09T22:54:13.823Z" }, - { url = "https://files.pythonhosted.org/packages/5d/e6/116ba39448753b1330f48ab8ba927dcd6cf0baea8a0ccbc512dfb49ba670/propcache-0.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c144ca294a204c470f18cf4c9d78887810d04a3e2fbb30eea903575a779159df", size = 213472, upload-time = "2025-06-09T22:54:15.232Z" }, - { url = "https://files.pythonhosted.org/packages/a6/85/f01f5d97e54e428885a5497ccf7f54404cbb4f906688a1690cd51bf597dc/propcache-0.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5c2a784234c28854878d68978265617aa6dc0780e53d44b4d67f3651a17a9a2", size = 204480, upload-time = "2025-06-09T22:54:17.104Z" }, - { url = "https://files.pythonhosted.org/packages/e3/79/7bf5ab9033b8b8194cc3f7cf1aaa0e9c3256320726f64a3e1f113a812dce/propcache-0.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5745bc7acdafa978ca1642891b82c19238eadc78ba2aaa293c6863b304e552d7", size = 214530, upload-time = "2025-06-09T22:54:18.512Z" }, - { url = "https://files.pythonhosted.org/packages/31/0b/bd3e0c00509b609317df4a18e6b05a450ef2d9a963e1d8bc9c9415d86f30/propcache-0.3.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c0075bf773d66fa8c9d41f66cc132ecc75e5bb9dd7cce3cfd14adc5ca184cb95", size = 205230, upload-time = "2025-06-09T22:54:19.947Z" }, - { url = "https://files.pythonhosted.org/packages/7a/23/fae0ff9b54b0de4e819bbe559508da132d5683c32d84d0dc2ccce3563ed4/propcache-0.3.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5f57aa0847730daceff0497f417c9de353c575d8da3579162cc74ac294c5369e", size = 206754, upload-time = "2025-06-09T22:54:21.716Z" }, - { url = "https://files.pythonhosted.org/packages/b7/7f/ad6a3c22630aaa5f618b4dc3c3598974a72abb4c18e45a50b3cdd091eb2f/propcache-0.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:eef914c014bf72d18efb55619447e0aecd5fb7c2e3fa7441e2e5d6099bddff7e", size = 218430, upload-time = "2025-06-09T22:54:23.17Z" }, - { url = "https://files.pythonhosted.org/packages/5b/2c/ba4f1c0e8a4b4c75910742f0d333759d441f65a1c7f34683b4a74c0ee015/propcache-0.3.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2a4092e8549031e82facf3decdbc0883755d5bbcc62d3aea9d9e185549936dcf", size = 223884, upload-time = "2025-06-09T22:54:25.539Z" }, - { url = "https://files.pythonhosted.org/packages/88/e4/ebe30fc399e98572019eee82ad0caf512401661985cbd3da5e3140ffa1b0/propcache-0.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85871b050f174bc0bfb437efbdb68aaf860611953ed12418e4361bc9c392749e", size = 211480, upload-time = "2025-06-09T22:54:26.892Z" }, - { url = "https://files.pythonhosted.org/packages/96/0a/7d5260b914e01d1d0906f7f38af101f8d8ed0dc47426219eeaf05e8ea7c2/propcache-0.3.2-cp311-cp311-win32.whl", hash = "sha256:36c8d9b673ec57900c3554264e630d45980fd302458e4ac801802a7fd2ef7897", size = 37757, upload-time = "2025-06-09T22:54:28.241Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2d/89fe4489a884bc0da0c3278c552bd4ffe06a1ace559db5ef02ef24ab446b/propcache-0.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53af8cb6a781b02d2ea079b5b853ba9430fcbe18a8e3ce647d5982a3ff69f39", size = 41500, upload-time = "2025-06-09T22:54:29.4Z" }, - { url = "https://files.pythonhosted.org/packages/a8/42/9ca01b0a6f48e81615dca4765a8f1dd2c057e0540f6116a27dc5ee01dfb6/propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10", size = 73674, upload-time = "2025-06-09T22:54:30.551Z" }, - { url = "https://files.pythonhosted.org/packages/af/6e/21293133beb550f9c901bbece755d582bfaf2176bee4774000bd4dd41884/propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154", size = 43570, upload-time = "2025-06-09T22:54:32.296Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c8/0393a0a3a2b8760eb3bde3c147f62b20044f0ddac81e9d6ed7318ec0d852/propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615", size = 43094, upload-time = "2025-06-09T22:54:33.929Z" }, - { url = "https://files.pythonhosted.org/packages/37/2c/489afe311a690399d04a3e03b069225670c1d489eb7b044a566511c1c498/propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db", size = 226958, upload-time = "2025-06-09T22:54:35.186Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ca/63b520d2f3d418c968bf596839ae26cf7f87bead026b6192d4da6a08c467/propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1", size = 234894, upload-time = "2025-06-09T22:54:36.708Z" }, - { url = "https://files.pythonhosted.org/packages/11/60/1d0ed6fff455a028d678df30cc28dcee7af77fa2b0e6962ce1df95c9a2a9/propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c", size = 233672, upload-time = "2025-06-09T22:54:38.062Z" }, - { url = "https://files.pythonhosted.org/packages/37/7c/54fd5301ef38505ab235d98827207176a5c9b2aa61939b10a460ca53e123/propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67", size = 224395, upload-time = "2025-06-09T22:54:39.634Z" }, - { url = "https://files.pythonhosted.org/packages/ee/1a/89a40e0846f5de05fdc6779883bf46ba980e6df4d2ff8fb02643de126592/propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b", size = 212510, upload-time = "2025-06-09T22:54:41.565Z" }, - { url = "https://files.pythonhosted.org/packages/5e/33/ca98368586c9566a6b8d5ef66e30484f8da84c0aac3f2d9aec6d31a11bd5/propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8", size = 222949, upload-time = "2025-06-09T22:54:43.038Z" }, - { url = "https://files.pythonhosted.org/packages/ba/11/ace870d0aafe443b33b2f0b7efdb872b7c3abd505bfb4890716ad7865e9d/propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251", size = 217258, upload-time = "2025-06-09T22:54:44.376Z" }, - { url = "https://files.pythonhosted.org/packages/5b/d2/86fd6f7adffcfc74b42c10a6b7db721d1d9ca1055c45d39a1a8f2a740a21/propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474", size = 213036, upload-time = "2025-06-09T22:54:46.243Z" }, - { url = "https://files.pythonhosted.org/packages/07/94/2d7d1e328f45ff34a0a284cf5a2847013701e24c2a53117e7c280a4316b3/propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535", size = 227684, upload-time = "2025-06-09T22:54:47.63Z" }, - { url = "https://files.pythonhosted.org/packages/b7/05/37ae63a0087677e90b1d14710e532ff104d44bc1efa3b3970fff99b891dc/propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06", size = 234562, upload-time = "2025-06-09T22:54:48.982Z" }, - { url = "https://files.pythonhosted.org/packages/a4/7c/3f539fcae630408d0bd8bf3208b9a647ccad10976eda62402a80adf8fc34/propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1", size = 222142, upload-time = "2025-06-09T22:54:50.424Z" }, - { url = "https://files.pythonhosted.org/packages/7c/d2/34b9eac8c35f79f8a962546b3e97e9d4b990c420ee66ac8255d5d9611648/propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1", size = 37711, upload-time = "2025-06-09T22:54:52.072Z" }, - { url = "https://files.pythonhosted.org/packages/19/61/d582be5d226cf79071681d1b46b848d6cb03d7b70af7063e33a2787eaa03/propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c", size = 41479, upload-time = "2025-06-09T22:54:53.234Z" }, - { url = "https://files.pythonhosted.org/packages/dc/d1/8c747fafa558c603c4ca19d8e20b288aa0c7cda74e9402f50f31eb65267e/propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", size = 71286, upload-time = "2025-06-09T22:54:54.369Z" }, - { url = "https://files.pythonhosted.org/packages/61/99/d606cb7986b60d89c36de8a85d58764323b3a5ff07770a99d8e993b3fa73/propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", size = 42425, upload-time = "2025-06-09T22:54:55.642Z" }, - { url = "https://files.pythonhosted.org/packages/8c/96/ef98f91bbb42b79e9bb82bdd348b255eb9d65f14dbbe3b1594644c4073f7/propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", size = 41846, upload-time = "2025-06-09T22:54:57.246Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ad/3f0f9a705fb630d175146cd7b1d2bf5555c9beaed54e94132b21aac098a6/propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33", size = 208871, upload-time = "2025-06-09T22:54:58.975Z" }, - { url = "https://files.pythonhosted.org/packages/3a/38/2085cda93d2c8b6ec3e92af2c89489a36a5886b712a34ab25de9fbca7992/propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e", size = 215720, upload-time = "2025-06-09T22:55:00.471Z" }, - { url = "https://files.pythonhosted.org/packages/61/c1/d72ea2dc83ac7f2c8e182786ab0fc2c7bd123a1ff9b7975bee671866fe5f/propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1", size = 215203, upload-time = "2025-06-09T22:55:01.834Z" }, - { url = "https://files.pythonhosted.org/packages/af/81/b324c44ae60c56ef12007105f1460d5c304b0626ab0cc6b07c8f2a9aa0b8/propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3", size = 206365, upload-time = "2025-06-09T22:55:03.199Z" }, - { url = "https://files.pythonhosted.org/packages/09/73/88549128bb89e66d2aff242488f62869014ae092db63ccea53c1cc75a81d/propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1", size = 196016, upload-time = "2025-06-09T22:55:04.518Z" }, - { url = "https://files.pythonhosted.org/packages/b9/3f/3bdd14e737d145114a5eb83cb172903afba7242f67c5877f9909a20d948d/propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6", size = 205596, upload-time = "2025-06-09T22:55:05.942Z" }, - { url = "https://files.pythonhosted.org/packages/0f/ca/2f4aa819c357d3107c3763d7ef42c03980f9ed5c48c82e01e25945d437c1/propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387", size = 200977, upload-time = "2025-06-09T22:55:07.792Z" }, - { url = "https://files.pythonhosted.org/packages/cd/4a/e65276c7477533c59085251ae88505caf6831c0e85ff8b2e31ebcbb949b1/propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4", size = 197220, upload-time = "2025-06-09T22:55:09.173Z" }, - { url = "https://files.pythonhosted.org/packages/7c/54/fc7152e517cf5578278b242396ce4d4b36795423988ef39bb8cd5bf274c8/propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88", size = 210642, upload-time = "2025-06-09T22:55:10.62Z" }, - { url = "https://files.pythonhosted.org/packages/b9/80/abeb4a896d2767bf5f1ea7b92eb7be6a5330645bd7fb844049c0e4045d9d/propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206", size = 212789, upload-time = "2025-06-09T22:55:12.029Z" }, - { url = "https://files.pythonhosted.org/packages/b3/db/ea12a49aa7b2b6d68a5da8293dcf50068d48d088100ac016ad92a6a780e6/propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43", size = 205880, upload-time = "2025-06-09T22:55:13.45Z" }, - { url = "https://files.pythonhosted.org/packages/d1/e5/9076a0bbbfb65d1198007059c65639dfd56266cf8e477a9707e4b1999ff4/propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02", size = 37220, upload-time = "2025-06-09T22:55:15.284Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f5/b369e026b09a26cd77aa88d8fffd69141d2ae00a2abaaf5380d2603f4b7f/propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05", size = 40678, upload-time = "2025-06-09T22:55:16.445Z" }, - { url = "https://files.pythonhosted.org/packages/a4/3a/6ece377b55544941a08d03581c7bc400a3c8cd3c2865900a68d5de79e21f/propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b", size = 76560, upload-time = "2025-06-09T22:55:17.598Z" }, - { url = "https://files.pythonhosted.org/packages/0c/da/64a2bb16418740fa634b0e9c3d29edff1db07f56d3546ca2d86ddf0305e1/propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0", size = 44676, upload-time = "2025-06-09T22:55:18.922Z" }, - { url = "https://files.pythonhosted.org/packages/36/7b/f025e06ea51cb72c52fb87e9b395cced02786610b60a3ed51da8af017170/propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e", size = 44701, upload-time = "2025-06-09T22:55:20.106Z" }, - { url = "https://files.pythonhosted.org/packages/a4/00/faa1b1b7c3b74fc277f8642f32a4c72ba1d7b2de36d7cdfb676db7f4303e/propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28", size = 276934, upload-time = "2025-06-09T22:55:21.5Z" }, - { url = "https://files.pythonhosted.org/packages/74/ab/935beb6f1756e0476a4d5938ff44bf0d13a055fed880caf93859b4f1baf4/propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a", size = 278316, upload-time = "2025-06-09T22:55:22.918Z" }, - { url = "https://files.pythonhosted.org/packages/f8/9d/994a5c1ce4389610838d1caec74bdf0e98b306c70314d46dbe4fcf21a3e2/propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c", size = 282619, upload-time = "2025-06-09T22:55:24.651Z" }, - { url = "https://files.pythonhosted.org/packages/2b/00/a10afce3d1ed0287cef2e09506d3be9822513f2c1e96457ee369adb9a6cd/propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725", size = 265896, upload-time = "2025-06-09T22:55:26.049Z" }, - { url = "https://files.pythonhosted.org/packages/2e/a8/2aa6716ffa566ca57c749edb909ad27884680887d68517e4be41b02299f3/propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892", size = 252111, upload-time = "2025-06-09T22:55:27.381Z" }, - { url = "https://files.pythonhosted.org/packages/36/4f/345ca9183b85ac29c8694b0941f7484bf419c7f0fea2d1e386b4f7893eed/propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44", size = 268334, upload-time = "2025-06-09T22:55:28.747Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ca/fcd54f78b59e3f97b3b9715501e3147f5340167733d27db423aa321e7148/propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe", size = 255026, upload-time = "2025-06-09T22:55:30.184Z" }, - { url = "https://files.pythonhosted.org/packages/8b/95/8e6a6bbbd78ac89c30c225210a5c687790e532ba4088afb8c0445b77ef37/propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81", size = 250724, upload-time = "2025-06-09T22:55:31.646Z" }, - { url = "https://files.pythonhosted.org/packages/ee/b0/0dd03616142baba28e8b2d14ce5df6631b4673850a3d4f9c0f9dd714a404/propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba", size = 268868, upload-time = "2025-06-09T22:55:33.209Z" }, - { url = "https://files.pythonhosted.org/packages/c5/98/2c12407a7e4fbacd94ddd32f3b1e3d5231e77c30ef7162b12a60e2dd5ce3/propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770", size = 271322, upload-time = "2025-06-09T22:55:35.065Z" }, - { url = "https://files.pythonhosted.org/packages/35/91/9cb56efbb428b006bb85db28591e40b7736847b8331d43fe335acf95f6c8/propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330", size = 265778, upload-time = "2025-06-09T22:55:36.45Z" }, - { url = "https://files.pythonhosted.org/packages/9a/4c/b0fe775a2bdd01e176b14b574be679d84fc83958335790f7c9a686c1f468/propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394", size = 41175, upload-time = "2025-06-09T22:55:38.436Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ff/47f08595e3d9b5e149c150f88d9714574f1a7cbd89fe2817158a952674bf/propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198", size = 44857, upload-time = "2025-06-09T22:55:39.687Z" }, - { url = "https://files.pythonhosted.org/packages/6c/39/8ea9bcfaaff16fd0b0fc901ee522e24c9ec44b4ca0229cfffb8066a06959/propcache-0.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a7fad897f14d92086d6b03fdd2eb844777b0c4d7ec5e3bac0fbae2ab0602bbe5", size = 74678, upload-time = "2025-06-09T22:55:41.227Z" }, - { url = "https://files.pythonhosted.org/packages/d3/85/cab84c86966e1d354cf90cdc4ba52f32f99a5bca92a1529d666d957d7686/propcache-0.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1f43837d4ca000243fd7fd6301947d7cb93360d03cd08369969450cc6b2ce3b4", size = 43829, upload-time = "2025-06-09T22:55:42.417Z" }, - { url = "https://files.pythonhosted.org/packages/23/f7/9cb719749152d8b26d63801b3220ce2d3931312b2744d2b3a088b0ee9947/propcache-0.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:261df2e9474a5949c46e962065d88eb9b96ce0f2bd30e9d3136bcde84befd8f2", size = 43729, upload-time = "2025-06-09T22:55:43.651Z" }, - { url = "https://files.pythonhosted.org/packages/a2/a2/0b2b5a210ff311260002a315f6f9531b65a36064dfb804655432b2f7d3e3/propcache-0.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e514326b79e51f0a177daab1052bc164d9d9e54133797a3a58d24c9c87a3fe6d", size = 204483, upload-time = "2025-06-09T22:55:45.327Z" }, - { url = "https://files.pythonhosted.org/packages/3f/e0/7aff5de0c535f783b0c8be5bdb750c305c1961d69fbb136939926e155d98/propcache-0.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4a996adb6904f85894570301939afeee65f072b4fd265ed7e569e8d9058e4ec", size = 217425, upload-time = "2025-06-09T22:55:46.729Z" }, - { url = "https://files.pythonhosted.org/packages/92/1d/65fa889eb3b2a7d6e4ed3c2b568a9cb8817547a1450b572de7bf24872800/propcache-0.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76cace5d6b2a54e55b137669b30f31aa15977eeed390c7cbfb1dafa8dfe9a701", size = 214723, upload-time = "2025-06-09T22:55:48.342Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e2/eecf6989870988dfd731de408a6fa366e853d361a06c2133b5878ce821ad/propcache-0.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31248e44b81d59d6addbb182c4720f90b44e1efdc19f58112a3c3a1615fb47ef", size = 200166, upload-time = "2025-06-09T22:55:49.775Z" }, - { url = "https://files.pythonhosted.org/packages/12/06/c32be4950967f18f77489268488c7cdc78cbfc65a8ba8101b15e526b83dc/propcache-0.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abb7fa19dbf88d3857363e0493b999b8011eea856b846305d8c0512dfdf8fbb1", size = 194004, upload-time = "2025-06-09T22:55:51.335Z" }, - { url = "https://files.pythonhosted.org/packages/46/6c/17b521a6b3b7cbe277a4064ff0aa9129dd8c89f425a5a9b6b4dd51cc3ff4/propcache-0.3.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d81ac3ae39d38588ad0549e321e6f773a4e7cc68e7751524a22885d5bbadf886", size = 203075, upload-time = "2025-06-09T22:55:52.681Z" }, - { url = "https://files.pythonhosted.org/packages/62/cb/3bdba2b736b3e45bc0e40f4370f745b3e711d439ffbffe3ae416393eece9/propcache-0.3.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:cc2782eb0f7a16462285b6f8394bbbd0e1ee5f928034e941ffc444012224171b", size = 195407, upload-time = "2025-06-09T22:55:54.048Z" }, - { url = "https://files.pythonhosted.org/packages/29/bd/760c5c6a60a4a2c55a421bc34a25ba3919d49dee411ddb9d1493bb51d46e/propcache-0.3.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:db429c19a6c7e8a1c320e6a13c99799450f411b02251fb1b75e6217cf4a14fcb", size = 196045, upload-time = "2025-06-09T22:55:55.485Z" }, - { url = "https://files.pythonhosted.org/packages/76/58/ced2757a46f55b8c84358d6ab8de4faf57cba831c51e823654da7144b13a/propcache-0.3.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:21d8759141a9e00a681d35a1f160892a36fb6caa715ba0b832f7747da48fb6ea", size = 208432, upload-time = "2025-06-09T22:55:56.884Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ec/d98ea8d5a4d8fe0e372033f5254eddf3254344c0c5dc6c49ab84349e4733/propcache-0.3.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2ca6d378f09adb13837614ad2754fa8afaee330254f404299611bce41a8438cb", size = 210100, upload-time = "2025-06-09T22:55:58.498Z" }, - { url = "https://files.pythonhosted.org/packages/56/84/b6d8a7ecf3f62d7dd09d9d10bbf89fad6837970ef868b35b5ffa0d24d9de/propcache-0.3.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:34a624af06c048946709f4278b4176470073deda88d91342665d95f7c6270fbe", size = 200712, upload-time = "2025-06-09T22:55:59.906Z" }, - { url = "https://files.pythonhosted.org/packages/bf/32/889f4903ddfe4a9dc61da71ee58b763758cf2d608fe1decede06e6467f8d/propcache-0.3.2-cp39-cp39-win32.whl", hash = "sha256:4ba3fef1c30f306b1c274ce0b8baaa2c3cdd91f645c48f06394068f37d3837a1", size = 38187, upload-time = "2025-06-09T22:56:01.212Z" }, - { url = "https://files.pythonhosted.org/packages/67/74/d666795fb9ba1dc139d30de64f3b6fd1ff9c9d3d96ccfdb992cd715ce5d2/propcache-0.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:7a2368eed65fc69a7a7a40b27f22e85e7627b74216f0846b04ba5c116e191ec9", size = 42025, upload-time = "2025-06-09T22:56:02.875Z" }, - { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" }, +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/56/030b7b4719d53085722893e0009dffb9236aa10bca1b12121bdc5626ef16/propcache-0.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b", size = 93417, upload-time = "2026-05-08T20:59:15.597Z" }, + { url = "https://files.pythonhosted.org/packages/1a/55/1140a8e067b8ec093a18a4ae7bb0045d9db65da38a08618ddc5e2f1994aa/propcache-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c", size = 53847, upload-time = "2026-05-08T20:59:17.096Z" }, + { url = "https://files.pythonhosted.org/packages/20/42/0e7443c90310498561addf346e7d57fe3c6ba1914e1ba938b5464c7bbfd2/propcache-0.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb", size = 53512, upload-time = "2026-05-08T20:59:18.64Z" }, + { url = "https://files.pythonhosted.org/packages/b7/db/cf51a71bab2009517d1a7f0ee07657e3bd446c4d69f67e6966cf17bcf956/propcache-0.5.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e", size = 58068, upload-time = "2026-05-08T20:59:20.683Z" }, + { url = "https://files.pythonhosted.org/packages/b7/43/39b6bdee9699fa1e1641c519feeb64a67e2a9f93bb465c70776b37a7333f/propcache-0.5.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e", size = 61020, upload-time = "2026-05-08T20:59:22.112Z" }, + { url = "https://files.pythonhosted.org/packages/26/0b/843726fbb0a29a8c5684fdb25971823638399f31e52e9d1f06a02dc9aa6b/propcache-0.5.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b", size = 62732, upload-time = "2026-05-08T20:59:23.805Z" }, + { url = "https://files.pythonhosted.org/packages/39/6e/899fed76dc1942b8a64193a4f059d7f1a2c7ef65085e8a9366ed8ec0d199/propcache-0.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d", size = 60140, upload-time = "2026-05-08T20:59:25.389Z" }, + { url = "https://files.pythonhosted.org/packages/ab/09/3da4be9b5b879219ad234aa535b3dd4a080ed1ad48d3a73ca07a9e798f22/propcache-0.5.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d", size = 60400, upload-time = "2026-05-08T20:59:27.238Z" }, + { url = "https://files.pythonhosted.org/packages/60/2f/09b72b874a9aa0044faf52a69807a6ed618e267ceaa9ec4a63195fa5b504/propcache-0.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0", size = 58155, upload-time = "2026-05-08T20:59:28.48Z" }, + { url = "https://files.pythonhosted.org/packages/8a/37/97489848c54c95578045473954f10956d619ce6a09e7ac137b71cdcb698b/propcache-0.5.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b", size = 57037, upload-time = "2026-05-08T20:59:30.146Z" }, + { url = "https://files.pythonhosted.org/packages/22/db/6c695285ccfc49012743ee9c98212b8c5dd0aed7b63cfd816d4a0f7a1601/propcache-0.5.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf", size = 61103, upload-time = "2026-05-08T20:59:31.626Z" }, + { url = "https://files.pythonhosted.org/packages/98/a9/1e500401ca593b0bdb6bf75a70bc2d723835fd53360edff6af70692c7546/propcache-0.5.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf", size = 60394, upload-time = "2026-05-08T20:59:32.829Z" }, + { url = "https://files.pythonhosted.org/packages/1f/87/f638b6e375eae0f30a1a2325d8b34fd85fdc785bb9960cf805f3bf1ec69a/propcache-0.5.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e", size = 63084, upload-time = "2026-05-08T20:59:35.964Z" }, + { url = "https://files.pythonhosted.org/packages/f6/18/884573f5d97b6d9eba68de759a82c901b7e39d7904d30f7b8d58d42d2a12/propcache-0.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274", size = 60999, upload-time = "2026-05-08T20:59:38.481Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1a/c3915eb059ceec9e758a56e4cfd955292bc0f201be2176a46b76d94b303a/propcache-0.5.2-cp310-cp310-win32.whl", hash = "sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe", size = 39036, upload-time = "2026-05-08T20:59:40.323Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/1dfd5607501a602d19c1c449d2d193b7d1c611f9246b4059026a1189a80e/propcache-0.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d", size = 42190, upload-time = "2026-05-08T20:59:42.232Z" }, + { url = "https://files.pythonhosted.org/packages/57/93/f71588ad08b3e6f4b555b5ef215808a3c02b042d0151ad82fa6f15be677a/propcache-0.5.2-cp310-cp310-win_arm64.whl", hash = "sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5", size = 38545, upload-time = "2026-05-08T20:59:44.087Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f1/8a8cc1c2c7e7934ab77e0163414f736fadbc0f5e8dd9673b952355ac175b/propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78", size = 90744, upload-time = "2026-05-08T20:59:45.799Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f4/651b1225e976bd1a2ba5cfba0c29d096581c2636b437e3a9a7ab6276270a/propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959", size = 52033, upload-time = "2026-05-08T20:59:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/15/a8/8ede85d6aa1f79fc7dc2f8fd2c8d65920b8272c3892903c8a1affde48cfb/propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7", size = 52754, upload-time = "2026-05-08T20:59:49.202Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fe/b3551b41bbc2f5b5bb088fc6920567cd43101253e68fbaa261339eb96fe1/propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511", size = 57573, upload-time = "2026-05-08T20:59:50.778Z" }, + { url = "https://files.pythonhosted.org/packages/83/27/ab851ebd1b7172e3e161f5f8d39e315d54a91bea246f01f4d872d3376aef/propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660", size = 60645, upload-time = "2026-05-08T20:59:52.227Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/466b3d18022e9897cbda9c735c493c5bd747d7a4c6f5ea1480b4cec434b6/propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66", size = 61563, upload-time = "2026-05-08T20:59:53.866Z" }, + { url = "https://files.pythonhosted.org/packages/27/1b/16ab7f2cf2041da2f60d156ba64c2484eadf9168075b4ff43c3ef60045af/propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b", size = 58888, upload-time = "2026-05-08T20:59:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/0a/67/bb777ffd907633563bf35fd859c4ce97b0512c32f4633cf5d1eb7c33512b/propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67", size = 59253, upload-time = "2026-05-08T20:59:57.075Z" }, + { url = "https://files.pythonhosted.org/packages/b9/42/64f8d90b73fd9cdc1499b48057ff6d9cd2a98a25734c9bb62ecf07e87061/propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f", size = 57558, upload-time = "2026-05-08T20:59:58.602Z" }, + { url = "https://files.pythonhosted.org/packages/eb/02/dba5bc03c9041f2092ea55a449caf5dfe68352c6654511b29ba0654ddb69/propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c", size = 55007, upload-time = "2026-05-08T20:59:59.837Z" }, + { url = "https://files.pythonhosted.org/packages/14/c0/43f649c7aa2a77a3b100d84e9dea3a483120ecb608bfe36ce49eaff517fe/propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0", size = 60355, upload-time = "2026-05-08T21:00:01.144Z" }, + { url = "https://files.pythonhosted.org/packages/83/c0/435dafd27f1cb4a495381dae60e25883ccfe4020bb72818e8184c1678092/propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6", size = 59057, upload-time = "2026-05-08T21:00:02.401Z" }, + { url = "https://files.pythonhosted.org/packages/53/ae/6e292df9135d659944e96cb3389258e4a663e5b2b5f6c217ef0ddc8d2f73/propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27", size = 61938, upload-time = "2026-05-08T21:00:03.638Z" }, + { url = "https://files.pythonhosted.org/packages/0b/42/314ebc50d8159055411fd6b0bda322ff510e4b1f7d2e4927940ad0f6af20/propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f", size = 59731, upload-time = "2026-05-08T21:00:04.881Z" }, + { url = "https://files.pythonhosted.org/packages/b8/9b/2da6dee38871c3c8772fabc2758325a5c9077d6d18c597737dc04dd884cd/propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0", size = 38966, upload-time = "2026-05-08T21:00:06.511Z" }, + { url = "https://files.pythonhosted.org/packages/42/4e/f17363fb58c0afe05b067361cb6d86ed2d29de6506779a27547c4d183075/propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82", size = 42135, upload-time = "2026-05-08T21:00:08.088Z" }, + { url = "https://files.pythonhosted.org/packages/c6/eb/6af6685077d22e8b33358d3c548e3282706a0b3cd85044ffba4e5dd08e3b/propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab", size = 38381, upload-time = "2026-05-08T21:00:09.692Z" }, + { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, + { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, + { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, + { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, + { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, + { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, + { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, + { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, + { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, + { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, + { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, + { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, + { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, + { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, + { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, + { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, + { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, + { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, + { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, + { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, + { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, + { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, + { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, + { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, + { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, + { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, + { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, + { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, + { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, ] [[package]] name = "protobuf" -version = "5.29.5" +version = "6.33.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/29/d09e70352e4e88c9c7a198d5645d7277811448d76c23b00345670f7c8a38/protobuf-5.29.5.tar.gz", hash = "sha256:bc1463bafd4b0929216c35f437a8e28731a2b7fe3d98bb77a600efced5a15c84", size = 425226, upload-time = "2025-05-28T23:51:59.82Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/11/6e40e9fc5bba02988a214c07cf324595789ca7820160bfd1f8be96e48539/protobuf-5.29.5-cp310-abi3-win32.whl", hash = "sha256:3f1c6468a2cfd102ff4703976138844f78ebd1fb45f49011afc5139e9e283079", size = 422963, upload-time = "2025-05-28T23:51:41.204Z" }, - { url = "https://files.pythonhosted.org/packages/81/7f/73cefb093e1a2a7c3ffd839e6f9fcafb7a427d300c7f8aef9c64405d8ac6/protobuf-5.29.5-cp310-abi3-win_amd64.whl", hash = "sha256:3f76e3a3675b4a4d867b52e4a5f5b78a2ef9565549d4037e06cf7b0942b1d3fc", size = 434818, upload-time = "2025-05-28T23:51:44.297Z" }, - { url = "https://files.pythonhosted.org/packages/dd/73/10e1661c21f139f2c6ad9b23040ff36fee624310dc28fba20d33fdae124c/protobuf-5.29.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e38c5add5a311f2a6eb0340716ef9b039c1dfa428b28f25a7838ac329204a671", size = 418091, upload-time = "2025-05-28T23:51:45.907Z" }, - { url = "https://files.pythonhosted.org/packages/6c/04/98f6f8cf5b07ab1294c13f34b4e69b3722bb609c5b701d6c169828f9f8aa/protobuf-5.29.5-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:fa18533a299d7ab6c55a238bf8629311439995f2e7eca5caaff08663606e9015", size = 319824, upload-time = "2025-05-28T23:51:47.545Z" }, - { url = "https://files.pythonhosted.org/packages/85/e4/07c80521879c2d15f321465ac24c70efe2381378c00bf5e56a0f4fbac8cd/protobuf-5.29.5-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:63848923da3325e1bf7e9003d680ce6e14b07e55d0473253a690c3a8b8fd6e61", size = 319942, upload-time = "2025-05-28T23:51:49.11Z" }, - { url = "https://files.pythonhosted.org/packages/e5/59/ca89678bb0352f094fc92f2b358daa40e3acc91a93aa8f922b24762bf841/protobuf-5.29.5-cp39-cp39-win32.whl", hash = "sha256:6f642dc9a61782fa72b90878af134c5afe1917c89a568cd3476d758d3c3a0736", size = 423025, upload-time = "2025-05-28T23:51:54.003Z" }, - { url = "https://files.pythonhosted.org/packages/96/8b/2c62731fe3e92ddbbeca0174f78f0f8739197cdeb7c75ceb5aad3706963b/protobuf-5.29.5-cp39-cp39-win_amd64.whl", hash = "sha256:470f3af547ef17847a28e1f47200a1cbf0ba3ff57b7de50d22776607cd2ea353", size = 434906, upload-time = "2025-05-28T23:51:55.782Z" }, - { url = "https://files.pythonhosted.org/packages/7e/cc/7e77861000a0691aeea8f4566e5d3aa716f2b1dece4a24439437e41d3d25/protobuf-5.29.5-py3-none-any.whl", hash = "sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5", size = 172823, upload-time = "2025-05-28T23:51:58.157Z" }, + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, ] [[package]] @@ -1971,18 +3511,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/05/33/2d74d588408caedd065c2497bdb5ef83ce6082db01289a1e1147f6639802/psutil-5.9.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8", size = 249898, upload-time = "2024-01-19T20:47:59.238Z" }, ] +[[package]] +name = "py-partiql-parser" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/7a/a0f6bda783eb4df8e3dfd55973a1ac6d368a89178c300e1b5b91cd181e5e/py_partiql_parser-0.6.3.tar.gz", hash = "sha256:09cecf916ce6e3da2c050f0cb6106166de42c33d34a078ec2eb19377ea70389a", size = 17456, upload-time = "2025-10-18T13:56:13.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/33/a7cbfccc39056a5cf8126b7aab4c8bafbedd4f0ca68ae40ecb627a2d2cd3/py_partiql_parser-0.6.3-py2.py3-none-any.whl", hash = "sha256:deb0769c3346179d2f590dcbde556f708cdb929059fb654bad75f4cf6e07f582", size = 23752, upload-time = "2025-10-18T13:56:12.256Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + [[package]] name = "pycparser" -version = "2.22" +version = "3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] [[package]] name = "pydantic" -version = "2.11.7" +version = "2.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -1990,132 +3560,139 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, ] [[package]] name = "pydantic-core" -version = "2.33.2" +version = "2.46.4" 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/53/ea/bbe9095cdd771987d13c82d104a9c8559ae9aec1e29f139e286fd2e9256e/pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d", size = 2028677, upload-time = "2025-04-23T18:32:27.227Z" }, - { url = "https://files.pythonhosted.org/packages/49/1d/4ac5ed228078737d457a609013e8f7edc64adc37b91d619ea965758369e5/pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954", size = 1864735, upload-time = "2025-04-23T18:32:29.019Z" }, - { url = "https://files.pythonhosted.org/packages/23/9a/2e70d6388d7cda488ae38f57bc2f7b03ee442fbcf0d75d848304ac7e405b/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb", size = 1898467, upload-time = "2025-04-23T18:32:31.119Z" }, - { url = "https://files.pythonhosted.org/packages/ff/2e/1568934feb43370c1ffb78a77f0baaa5a8b6897513e7a91051af707ffdc4/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7", size = 1983041, upload-time = "2025-04-23T18:32:33.655Z" }, - { url = "https://files.pythonhosted.org/packages/01/1a/1a1118f38ab64eac2f6269eb8c120ab915be30e387bb561e3af904b12499/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4", size = 2136503, upload-time = "2025-04-23T18:32:35.519Z" }, - { url = "https://files.pythonhosted.org/packages/5c/da/44754d1d7ae0f22d6d3ce6c6b1486fc07ac2c524ed8f6eca636e2e1ee49b/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b", size = 2736079, upload-time = "2025-04-23T18:32:37.659Z" }, - { url = "https://files.pythonhosted.org/packages/4d/98/f43cd89172220ec5aa86654967b22d862146bc4d736b1350b4c41e7c9c03/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3", size = 2006508, upload-time = "2025-04-23T18:32:39.637Z" }, - { url = "https://files.pythonhosted.org/packages/2b/cc/f77e8e242171d2158309f830f7d5d07e0531b756106f36bc18712dc439df/pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a", size = 2113693, upload-time = "2025-04-23T18:32:41.818Z" }, - { url = "https://files.pythonhosted.org/packages/54/7a/7be6a7bd43e0a47c147ba7fbf124fe8aaf1200bc587da925509641113b2d/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782", size = 2074224, upload-time = "2025-04-23T18:32:44.033Z" }, - { url = "https://files.pythonhosted.org/packages/2a/07/31cf8fadffbb03be1cb520850e00a8490c0927ec456e8293cafda0726184/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9", size = 2245403, upload-time = "2025-04-23T18:32:45.836Z" }, - { url = "https://files.pythonhosted.org/packages/b6/8d/bbaf4c6721b668d44f01861f297eb01c9b35f612f6b8e14173cb204e6240/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e", size = 2242331, upload-time = "2025-04-23T18:32:47.618Z" }, - { url = "https://files.pythonhosted.org/packages/bb/93/3cc157026bca8f5006250e74515119fcaa6d6858aceee8f67ab6dc548c16/pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9", size = 1910571, upload-time = "2025-04-23T18:32:49.401Z" }, - { url = "https://files.pythonhosted.org/packages/5b/90/7edc3b2a0d9f0dda8806c04e511a67b0b7a41d2187e2003673a996fb4310/pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3", size = 1956504, upload-time = "2025-04-23T18:32:51.287Z" }, - { 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" }, - { url = "https://files.pythonhosted.org/packages/08/98/dbf3fdfabaf81cda5622154fda78ea9965ac467e3239078e0dcd6df159e7/pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101", size = 2024034, upload-time = "2025-04-23T18:33:32.843Z" }, - { url = "https://files.pythonhosted.org/packages/8d/99/7810aa9256e7f2ccd492590f86b79d370df1e9292f1f80b000b6a75bd2fb/pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64", size = 1858578, upload-time = "2025-04-23T18:33:34.912Z" }, - { url = "https://files.pythonhosted.org/packages/d8/60/bc06fa9027c7006cc6dd21e48dbf39076dc39d9abbaf718a1604973a9670/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d", size = 1892858, upload-time = "2025-04-23T18:33:36.933Z" }, - { url = "https://files.pythonhosted.org/packages/f2/40/9d03997d9518816c68b4dfccb88969756b9146031b61cd37f781c74c9b6a/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535", size = 2068498, upload-time = "2025-04-23T18:33:38.997Z" }, - { url = "https://files.pythonhosted.org/packages/d8/62/d490198d05d2d86672dc269f52579cad7261ced64c2df213d5c16e0aecb1/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d", size = 2108428, upload-time = "2025-04-23T18:33:41.18Z" }, - { url = "https://files.pythonhosted.org/packages/9a/ec/4cd215534fd10b8549015f12ea650a1a973da20ce46430b68fc3185573e8/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6", size = 2069854, upload-time = "2025-04-23T18:33:43.446Z" }, - { url = "https://files.pythonhosted.org/packages/1a/1a/abbd63d47e1d9b0d632fee6bb15785d0889c8a6e0a6c3b5a8e28ac1ec5d2/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca", size = 2237859, upload-time = "2025-04-23T18:33:45.56Z" }, - { url = "https://files.pythonhosted.org/packages/80/1c/fa883643429908b1c90598fd2642af8839efd1d835b65af1f75fba4d94fe/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039", size = 2239059, upload-time = "2025-04-23T18:33:47.735Z" }, - { url = "https://files.pythonhosted.org/packages/d4/29/3cade8a924a61f60ccfa10842f75eb12787e1440e2b8660ceffeb26685e7/pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27", size = 2066661, upload-time = "2025-04-23T18:33:49.995Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, + { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, + { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, + { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, ] [[package]] name = "pydantic-settings" -version = "2.10.1" +version = "2.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydantic", marker = "python_full_version >= '3.10'" }, - { name = "python-dotenv", marker = "python_full_version >= '3.10'" }, - { name = "typing-inspection", marker = "python_full_version >= '3.10'" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" }, + { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, ] [[package]] @@ -2132,7 +3709,7 @@ wheels = [ [[package]] name = "pydoctor" -version = "24.11.2" +version = "25.10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -2142,22 +3719,48 @@ dependencies = [ { name = "lunr" }, { name = "platformdirs" }, { name = "requests" }, - { name = "toml", marker = "python_full_version < '3.11'" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "twisted" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7a/7c/c3491dd1666b9fdede4bddb2ebf9a1bc31ff8e0536ee6572ad5a028e109b/pydoctor-24.11.2.tar.gz", hash = "sha256:d52c13caa17b870da1a245981c15cda88406eb73a863dbe23db3fbc43e3b3fc3", size = 946366, upload-time = "2025-01-08T20:04:41.584Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/26/4b49eab9203a0f6939711f6a62a06c04d6e11fbf01e7b2cd9f9dece97686/pydoctor-25.10.1.tar.gz", hash = "sha256:489ec8b96f1e477df8f1892e2c7990836f32481a633ed13abb5e24a3488c83fb", size = 981473, upload-time = "2025-09-29T22:06:49.712Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/c2/016a296de5eb70478c65cc58722f11aedd858bf248431486b0dff479016c/pydoctor-24.11.2-py3-none-any.whl", hash = "sha256:69004e7b4a2b4db6425f1a46869adf7ebcd7c4fd0340515538134a4df181ab82", size = 1584835, upload-time = "2025-01-08T20:04:39.394Z" }, + { url = "https://files.pythonhosted.org/packages/2e/ae/1ae23968390bfd71678c1cd752e66a9823e2fe45ba34a6740f76e84011ab/pydoctor-25.10.1-py3-none-any.whl", hash = "sha256:2aa85f8d64e11c065d71a2317b82724a58361173d945290509367681665bdc7c", size = 1637603, upload-time = "2025-09-29T22:06:47.349Z" }, ] [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] [[package]] @@ -2175,7 +3778,7 @@ wheels = [ [[package]] name = "pytest" -version = "7.4.4" +version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -2183,11 +3786,12 @@ dependencies = [ { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, + { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/80/1f/9d8e98e4133ffb16c90f3b405c43e38d3abb715bb5d7a63a5a684f7e46a3/pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", size = 1357116, upload-time = "2023-12-31T12:00:18.035Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8", size = 325287, upload-time = "2023-12-31T12:00:13.963Z" }, + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] [[package]] @@ -2204,16 +3808,28 @@ wheels = [ [[package]] name = "pytest-cov" -version = "6.2.1" +version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "pytest-flakefinder" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/53/69c56a93ea057895b5761c5318455804873a6cd9d796d7c55d41c2358125/pytest-flakefinder-1.1.0.tar.gz", hash = "sha256:e2412a1920bdb8e7908783b20b3d57e9dad590cc39a93e8596ffdd493b403e0e", size = 6795, upload-time = "2022-10-26T18:27:54.243Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" }, + { url = "https://files.pythonhosted.org/packages/33/8b/06787150d0fd0cbd3a8054262b56f91631c7778c1bc91bf4637e47f909ad/pytest_flakefinder-1.1.0-py2.py3-none-any.whl", hash = "sha256:741e0e8eea427052f5b8c89c2b3c3019a50c39a59ce4df6a305a2c2d9ba2bd13", size = 4644, upload-time = "2022-10-26T18:27:52.128Z" }, ] [[package]] @@ -2229,6 +3845,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/85/2f97a1b65178b0f11c9c77c35417a4cc5b99a80db90dad4734a129844ea5/pytest_pretty-1.3.0-py3-none-any.whl", hash = "sha256:074b9d5783cef9571494543de07e768a4dda92a3e85118d6c7458c67297159b7", size = 5620, upload-time = "2025-06-04T12:54:36.229Z" }, ] +[[package]] +name = "pytest-rerunfailures" +version = "16.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/60/a90ca1cc6cffcb97b4260ed0ad2b7934b999d7c48abe4ea0840344862a3b/pytest_rerunfailures-16.4.tar.gz", hash = "sha256:8222d17c37eb7b9e4d6fc96a3c724ff4e1a5c97a5cc7cbb2c19e9282cfd21a11", size = 36635, upload-time = "2026-07-01T06:30:56.813Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/93/3cdcc4033444e822e01b573414b03fd37fd5533070c750477b8f5fa5224b/pytest_rerunfailures-16.4-py3-none-any.whl", hash = "sha256:f69b5beb39622c90d1e44bd945d826eff6db545dcf0b68f52b7e4ad15eaf6d6c", size = 16955, upload-time = "2026-07-01T06:30:55.333Z" }, +] + [[package]] name = "pytest-timeout" version = "2.4.0" @@ -2241,6 +3870,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, ] +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -2255,45 +3897,45 @@ wheels = [ [[package]] name = "python-dotenv" -version = "1.1.1" +version = "1.2.2" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] [[package]] name = "python-multipart" -version = "0.0.20" +version = "0.0.32" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, + { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, ] [[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" }, - { url = "https://files.pythonhosted.org/packages/59/42/b86689aac0cdaee7ae1c58d464b0ff04ca909c19bb6502d4973cdd9f9544/pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b", size = 8760837, upload-time = "2025-07-14T20:12:59.59Z" }, - { url = "https://files.pythonhosted.org/packages/9f/8a/1403d0353f8c5a2f0829d2b1c4becbf9da2f0a4d040886404fc4a5431e4d/pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91", size = 9590187, upload-time = "2025-07-14T20:13:01.419Z" }, - { url = "https://files.pythonhosted.org/packages/60/22/e0e8d802f124772cec9c75430b01a212f86f9de7546bda715e54140d5aeb/pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d", size = 8778162, upload-time = "2025-07-14T20:13:03.544Z" }, +version = "312" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/1b/9cfdeac80ee45bebbbcb31f1b7b99a0d81a1c72de48d837be984e0e88b1d/pywin32-312-cp310-cp310-win32.whl", hash = "sha256:772235332b5d1024c696f11cea1ae4be7930f0a8b894bb43db14e3f435f1ff7e", size = 6361387, upload-time = "2026-06-04T07:49:14.329Z" }, + { url = "https://files.pythonhosted.org/packages/33/b1/7afc96d041d982c27bc2df6f853d43f01fd273e3d39d04be3647ddeb533d/pywin32-312-cp310-cp310-win_amd64.whl", hash = "sha256:5dbc35d2b5320dc07f25fa31269cfb767471002b17de5eb067d03da68c7cb2db", size = 6926780, upload-time = "2026-06-04T07:49:16.881Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/4140da9ad54108e517f4a16b2d83da3033e08662144623e1239587cb7db6/pywin32-312-cp310-cp310-win_arm64.whl", hash = "sha256:3020656e34f1cf7faeb7bccd2b84653a607c6ff0c55ada85e6487d61716deabd", size = 4307203, upload-time = "2026-06-04T07:49:18.993Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f5/10a6e845a00fc5e7afd0a988b744f403d4d57162a28d160a093c4d9322f0/pywin32-312-cp311-cp311-win32.whl", hash = "sha256:17948aeadbdb091f0ced6ef0841620794e68327b94ee415571c1203594b7215c", size = 6362659, upload-time = "2026-06-04T07:49:21.349Z" }, + { url = "https://files.pythonhosted.org/packages/35/c4/dcd2d62b5944b6d5db53413a5899016ccd57ffcb7278f3f81655d25d2027/pywin32-312-cp311-cp311-win_amd64.whl", hash = "sha256:d11417d84412f859b722fad0841b3614459ed0047f7542d8362e77884f6b6e8a", size = 6928825, upload-time = "2026-06-04T07:49:23.934Z" }, + { url = "https://files.pythonhosted.org/packages/b7/56/3cbb433fe4501cdba2eb9040f56a4e1a8243faa4186b25295564d1a7a79d/pywin32-312-cp311-cp311-win_arm64.whl", hash = "sha256:b2200a054ca6d6625c4842fc56a4976a4b47f96b73dbe5538c3f813a80359f47", size = 6721875, upload-time = "2026-06-04T07:49:26.416Z" }, + { url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877, upload-time = "2026-06-04T07:49:28.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841, upload-time = "2026-06-04T07:49:31.85Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901, upload-time = "2026-06-04T07:49:34.244Z" }, + { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" }, + { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" }, + { url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159, upload-time = "2026-06-04T07:49:50.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293, upload-time = "2026-06-04T07:49:54.857Z" }, + { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" }, ] [[package]] @@ -2307,182 +3949,221 @@ wheels = [ [[package]] name = "pyyaml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, - { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, - { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, - { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, - { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, - { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, - { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, - { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, - { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, - { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777, upload-time = "2024-08-06T20:33:25.896Z" }, - { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318, upload-time = "2024-08-06T20:33:27.212Z" }, - { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891, upload-time = "2024-08-06T20:33:28.974Z" }, - { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614, upload-time = "2024-08-06T20:33:34.157Z" }, - { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360, upload-time = "2024-08-06T20:33:35.84Z" }, - { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006, upload-time = "2024-08-06T20:33:37.501Z" }, - { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577, upload-time = "2024-08-06T20:33:39.389Z" }, - { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593, upload-time = "2024-08-06T20:33:46.63Z" }, - { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312, upload-time = "2024-08-06T20:33:49.073Z" }, +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] [[package]] name = "readme-renderer" -version = "44.0" +version = "45.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docutils" }, { name = "nh3" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", size = 32056, upload-time = "2024-07-08T15:00:57.805Z" } +sdist = { url = "https://files.pythonhosted.org/packages/02/51/d3a6ea424652c60f05600d8c2e01a55c913755e7cdad64afabbd1aa16f44/readme_renderer-45.0.tar.gz", hash = "sha256:030a8fac74904f8fba11ad1bb6964e3f76e896dc7e5e71f16af190c9056696d1", size = 36172, upload-time = "2026-06-09T21:05:17.37Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", size = 13310, upload-time = "2024-07-08T15:00:56.577Z" }, + { url = "https://files.pythonhosted.org/packages/97/1b/295bf2fa3e740131778065e5ffa2c481f0e7210182d408e9a2c244ff5b0c/readme_renderer-45.0-py3-none-any.whl", hash = "sha256:3385ed220117104a2bceb4a9dac8c5fdf6d1f96890d7ea2a9c7174fd5c84091f", size = 14134, upload-time = "2026-06-09T21:05:15.85Z" }, ] [[package]] name = "referencing" -version = "0.36.2" +version = "0.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, - { name = "rpds-py" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "rpds-py", version = "2026.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] [[package]] name = "regex" -version = "2025.9.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/5a/4c63457fbcaf19d138d72b2e9b39405954f98c0349b31c601bfcb151582c/regex-2025.9.1.tar.gz", hash = "sha256:88ac07b38d20b54d79e704e38aa3bd2c0f8027432164226bdee201a1c0c9c9ff", size = 400852, upload-time = "2025-09-01T22:10:10.479Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/46/c1/ed9ef923156105a78aa004f9390e5dd87eadc29f5ca8840f172cadb638de/regex-2025.9.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c5aa2a6a73bf218515484b36a0d20c6ad9dc63f6339ff6224147b0e2c095ee55", size = 484813, upload-time = "2025-09-01T22:07:45.528Z" }, - { url = "https://files.pythonhosted.org/packages/05/de/97957618a774c67f892609eee2fafe3e30703fbbba66de5e6b79d7196dbc/regex-2025.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c2ff5c01d5e47ad5fc9d31bcd61e78c2fa0068ed00cab86b7320214446da766", size = 288981, upload-time = "2025-09-01T22:07:48.464Z" }, - { url = "https://files.pythonhosted.org/packages/3c/b0/441afadd0a6ffccbd58a9663e5bdd182daa237893e5f8ceec6ff9df4418a/regex-2025.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d49dc84e796b666181de8a9973284cad6616335f01b52bf099643253094920fc", size = 286608, upload-time = "2025-09-01T22:07:50.484Z" }, - { url = "https://files.pythonhosted.org/packages/6e/cf/d89aecaf17e999ab11a3ef73fc9ab8b64f4e156f121250ef84340b35338d/regex-2025.9.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9914fe1040874f83c15fcea86d94ea54091b0666eab330aaab69e30d106aabe", size = 780459, upload-time = "2025-09-01T22:07:52.34Z" }, - { url = "https://files.pythonhosted.org/packages/f6/05/05884594a9975a29597917bbdd6837f7b97e8ac23faf22d628aa781e58f7/regex-2025.9.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e71bceb3947362ec5eabd2ca0870bb78eae4edfc60c6c21495133c01b6cd2df4", size = 849276, upload-time = "2025-09-01T22:07:54.591Z" }, - { url = "https://files.pythonhosted.org/packages/8c/8d/2b3067506838d02096bf107beb129b2ce328cdf776d6474b7f542c0a7bfd/regex-2025.9.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67a74456f410fe5e869239ee7a5423510fe5121549af133809d9591a8075893f", size = 897320, upload-time = "2025-09-01T22:07:56.129Z" }, - { url = "https://files.pythonhosted.org/packages/9e/b3/0f9f7766e980b900df0ba9901b52871a2e4203698fb35cdebd219240d5f7/regex-2025.9.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c3b96ed0223b32dbdc53a83149b6de7ca3acd5acd9c8e64b42a166228abe29c", size = 789931, upload-time = "2025-09-01T22:07:57.834Z" }, - { url = "https://files.pythonhosted.org/packages/47/9f/7b2f29c8f8b698eb44be5fc68e8b9c8d32e99635eac5defc98de114e9f35/regex-2025.9.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:113d5aa950f428faf46fd77d452df62ebb4cc6531cb619f6cc30a369d326bfbd", size = 780764, upload-time = "2025-09-01T22:07:59.413Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ac/56176caa86155c14462531eb0a4ddc450d17ba8875001122b3b7c0cb01bf/regex-2025.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fcdeb38de4f7f3d69d798f4f371189061446792a84e7c92b50054c87aae9c07c", size = 773610, upload-time = "2025-09-01T22:08:01.042Z" }, - { url = "https://files.pythonhosted.org/packages/39/e8/9d6b9bd43998268a9de2f35602077519cacc9cb149f7381758cf8f502ba7/regex-2025.9.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4bcdff370509164b67a6c8ec23c9fb40797b72a014766fdc159bb809bd74f7d8", size = 844090, upload-time = "2025-09-01T22:08:02.94Z" }, - { url = "https://files.pythonhosted.org/packages/fd/92/d89743b089005cae4cb81cc2fe177e180b7452e60f29de53af34349640f8/regex-2025.9.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:7383efdf6e8e8c61d85e00cfb2e2e18da1a621b8bfb4b0f1c2747db57b942b8f", size = 834775, upload-time = "2025-09-01T22:08:04.781Z" }, - { url = "https://files.pythonhosted.org/packages/01/8f/86a3e0aaa89295d2a3445bb238e56369963ef6b02a5b4aa3362f4e687413/regex-2025.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1ec2bd3bdf0f73f7e9f48dca550ba7d973692d5e5e9a90ac42cc5f16c4432d8b", size = 778521, upload-time = "2025-09-01T22:08:06.596Z" }, - { url = "https://files.pythonhosted.org/packages/3e/df/72072acb370ee8577c255717f8a58264f1d0de40aa3c9e6ebd5271cac633/regex-2025.9.1-cp310-cp310-win32.whl", hash = "sha256:9627e887116c4e9c0986d5c3b4f52bcfe3df09850b704f62ec3cbf177a0ae374", size = 264105, upload-time = "2025-09-01T22:08:08.708Z" }, - { url = "https://files.pythonhosted.org/packages/97/73/fb82faaf0375aeaa1bb675008246c79b6779fa5688585a35327610ea0e2e/regex-2025.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:94533e32dc0065eca43912ee6649c90ea0681d59f56d43c45b5bcda9a740b3dd", size = 276131, upload-time = "2025-09-01T22:08:10.156Z" }, - { url = "https://files.pythonhosted.org/packages/d3/3a/77d7718a2493e54725494f44da1a1e55704743dc4b8fabe5b0596f7b8014/regex-2025.9.1-cp310-cp310-win_arm64.whl", hash = "sha256:a874a61bb580d48642ffd338570ee24ab13fa023779190513fcacad104a6e251", size = 268462, upload-time = "2025-09-01T22:08:11.651Z" }, - { url = "https://files.pythonhosted.org/packages/06/4d/f741543c0c59f96c6625bc6c11fea1da2e378b7d293ffff6f318edc0ce14/regex-2025.9.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e5bcf112b09bfd3646e4db6bf2e598534a17d502b0c01ea6550ba4eca780c5e6", size = 484811, upload-time = "2025-09-01T22:08:12.834Z" }, - { url = "https://files.pythonhosted.org/packages/c2/bd/27e73e92635b6fbd51afc26a414a3133243c662949cd1cda677fe7bb09bd/regex-2025.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:67a0295a3c31d675a9ee0238d20238ff10a9a2fdb7a1323c798fc7029578b15c", size = 288977, upload-time = "2025-09-01T22:08:14.499Z" }, - { url = "https://files.pythonhosted.org/packages/eb/7d/7dc0c6efc8bc93cd6e9b947581f5fde8a5dbaa0af7c4ec818c5729fdc807/regex-2025.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea8267fbadc7d4bd7c1301a50e85c2ff0de293ff9452a1a9f8d82c6cafe38179", size = 286606, upload-time = "2025-09-01T22:08:15.881Z" }, - { url = "https://files.pythonhosted.org/packages/d1/01/9b5c6dd394f97c8f2c12f6e8f96879c9ac27292a718903faf2e27a0c09f6/regex-2025.9.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6aeff21de7214d15e928fb5ce757f9495214367ba62875100d4c18d293750cc1", size = 792436, upload-time = "2025-09-01T22:08:17.38Z" }, - { url = "https://files.pythonhosted.org/packages/fc/24/b7430cfc6ee34bbb3db6ff933beb5e7692e5cc81e8f6f4da63d353566fb0/regex-2025.9.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d89f1bbbbbc0885e1c230f7770d5e98f4f00b0ee85688c871d10df8b184a6323", size = 858705, upload-time = "2025-09-01T22:08:19.037Z" }, - { url = "https://files.pythonhosted.org/packages/d6/98/155f914b4ea6ae012663188545c4f5216c11926d09b817127639d618b003/regex-2025.9.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca3affe8ddea498ba9d294ab05f5f2d3b5ad5d515bc0d4a9016dd592a03afe52", size = 905881, upload-time = "2025-09-01T22:08:20.377Z" }, - { url = "https://files.pythonhosted.org/packages/8a/a7/a470e7bc8259c40429afb6d6a517b40c03f2f3e455c44a01abc483a1c512/regex-2025.9.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91892a7a9f0a980e4c2c85dd19bc14de2b219a3a8867c4b5664b9f972dcc0c78", size = 798968, upload-time = "2025-09-01T22:08:22.081Z" }, - { url = "https://files.pythonhosted.org/packages/1d/fa/33f6fec4d41449fea5f62fdf5e46d668a1c046730a7f4ed9f478331a8e3a/regex-2025.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e1cb40406f4ae862710615f9f636c1e030fd6e6abe0e0f65f6a695a2721440c6", size = 781884, upload-time = "2025-09-01T22:08:23.832Z" }, - { url = "https://files.pythonhosted.org/packages/42/de/2b45f36ab20da14eedddf5009d370625bc5942d9953fa7e5037a32d66843/regex-2025.9.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:94f6cff6f7e2149c7e6499a6ecd4695379eeda8ccbccb9726e8149f2fe382e92", size = 852935, upload-time = "2025-09-01T22:08:25.536Z" }, - { url = "https://files.pythonhosted.org/packages/1e/f9/878f4fc92c87e125e27aed0f8ee0d1eced9b541f404b048f66f79914475a/regex-2025.9.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6c0226fb322b82709e78c49cc33484206647f8a39954d7e9de1567f5399becd0", size = 844340, upload-time = "2025-09-01T22:08:27.141Z" }, - { url = "https://files.pythonhosted.org/packages/90/c2/5b6f2bce6ece5f8427c718c085eca0de4bbb4db59f54db77aa6557aef3e9/regex-2025.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a12f59c7c380b4fcf7516e9cbb126f95b7a9518902bcf4a852423ff1dcd03e6a", size = 787238, upload-time = "2025-09-01T22:08:28.75Z" }, - { url = "https://files.pythonhosted.org/packages/47/66/1ef1081c831c5b611f6f55f6302166cfa1bc9574017410ba5595353f846a/regex-2025.9.1-cp311-cp311-win32.whl", hash = "sha256:49865e78d147a7a4f143064488da5d549be6bfc3f2579e5044cac61f5c92edd4", size = 264118, upload-time = "2025-09-01T22:08:30.388Z" }, - { url = "https://files.pythonhosted.org/packages/ad/e0/8adc550d7169df1d6b9be8ff6019cda5291054a0107760c2f30788b6195f/regex-2025.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:d34b901f6f2f02ef60f4ad3855d3a02378c65b094efc4b80388a3aeb700a5de7", size = 276151, upload-time = "2025-09-01T22:08:32.073Z" }, - { url = "https://files.pythonhosted.org/packages/cb/bd/46fef29341396d955066e55384fb93b0be7d64693842bf4a9a398db6e555/regex-2025.9.1-cp311-cp311-win_arm64.whl", hash = "sha256:47d7c2dab7e0b95b95fd580087b6ae196039d62306a592fa4e162e49004b6299", size = 268460, upload-time = "2025-09-01T22:08:33.281Z" }, - { url = "https://files.pythonhosted.org/packages/39/ef/a0372febc5a1d44c1be75f35d7e5aff40c659ecde864d7fa10e138f75e74/regex-2025.9.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:84a25164bd8dcfa9f11c53f561ae9766e506e580b70279d05a7946510bdd6f6a", size = 486317, upload-time = "2025-09-01T22:08:34.529Z" }, - { url = "https://files.pythonhosted.org/packages/b5/25/d64543fb7eb41a1024786d518cc57faf1ce64aa6e9ddba097675a0c2f1d2/regex-2025.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:645e88a73861c64c1af558dd12294fb4e67b5c1eae0096a60d7d8a2143a611c7", size = 289698, upload-time = "2025-09-01T22:08:36.162Z" }, - { url = "https://files.pythonhosted.org/packages/d8/dc/fbf31fc60be317bd9f6f87daa40a8a9669b3b392aa8fe4313df0a39d0722/regex-2025.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10a450cba5cd5409526ee1d4449f42aad38dd83ac6948cbd6d7f71ca7018f7db", size = 287242, upload-time = "2025-09-01T22:08:37.794Z" }, - { url = "https://files.pythonhosted.org/packages/0f/74/f933a607a538f785da5021acf5323961b4620972e2c2f1f39b6af4b71db7/regex-2025.9.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9dc5991592933a4192c166eeb67b29d9234f9c86344481173d1bc52f73a7104", size = 797441, upload-time = "2025-09-01T22:08:39.108Z" }, - { url = "https://files.pythonhosted.org/packages/89/d0/71fc49b4f20e31e97f199348b8c4d6e613e7b6a54a90eb1b090c2b8496d7/regex-2025.9.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a32291add816961aab472f4fad344c92871a2ee33c6c219b6598e98c1f0108f2", size = 862654, upload-time = "2025-09-01T22:08:40.586Z" }, - { url = "https://files.pythonhosted.org/packages/59/05/984edce1411a5685ba9abbe10d42cdd9450aab4a022271f9585539788150/regex-2025.9.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:588c161a68a383478e27442a678e3b197b13c5ba51dbba40c1ccb8c4c7bee9e9", size = 910862, upload-time = "2025-09-01T22:08:42.416Z" }, - { url = "https://files.pythonhosted.org/packages/b2/02/5c891bb5fe0691cc1bad336e3a94b9097fbcf9707ec8ddc1dce9f0397289/regex-2025.9.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47829ffaf652f30d579534da9085fe30c171fa2a6744a93d52ef7195dc38218b", size = 801991, upload-time = "2025-09-01T22:08:44.072Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ae/fd10d6ad179910f7a1b3e0a7fde1ef8bb65e738e8ac4fd6ecff3f52252e4/regex-2025.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e978e5a35b293ea43f140c92a3269b6ab13fe0a2bf8a881f7ac740f5a6ade85", size = 786651, upload-time = "2025-09-01T22:08:46.079Z" }, - { url = "https://files.pythonhosted.org/packages/30/cf/9d686b07bbc5bf94c879cc168db92542d6bc9fb67088d03479fef09ba9d3/regex-2025.9.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4cf09903e72411f4bf3ac1eddd624ecfd423f14b2e4bf1c8b547b72f248b7bf7", size = 856556, upload-time = "2025-09-01T22:08:48.376Z" }, - { url = "https://files.pythonhosted.org/packages/91/9d/302f8a29bb8a49528abbab2d357a793e2a59b645c54deae0050f8474785b/regex-2025.9.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d016b0f77be63e49613c9e26aaf4a242f196cd3d7a4f15898f5f0ab55c9b24d2", size = 849001, upload-time = "2025-09-01T22:08:50.067Z" }, - { url = "https://files.pythonhosted.org/packages/93/fa/b4c6dbdedc85ef4caec54c817cd5f4418dbfa2453214119f2538082bf666/regex-2025.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:656563e620de6908cd1c9d4f7b9e0777e3341ca7db9d4383bcaa44709c90281e", size = 788138, upload-time = "2025-09-01T22:08:51.933Z" }, - { url = "https://files.pythonhosted.org/packages/4a/1b/91ee17a3cbf87f81e8c110399279d0e57f33405468f6e70809100f2ff7d8/regex-2025.9.1-cp312-cp312-win32.whl", hash = "sha256:df33f4ef07b68f7ab637b1dbd70accbf42ef0021c201660656601e8a9835de45", size = 264524, upload-time = "2025-09-01T22:08:53.75Z" }, - { url = "https://files.pythonhosted.org/packages/92/28/6ba31cce05b0f1ec6b787921903f83bd0acf8efde55219435572af83c350/regex-2025.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:5aba22dfbc60cda7c0853516104724dc904caa2db55f2c3e6e984eb858d3edf3", size = 275489, upload-time = "2025-09-01T22:08:55.037Z" }, - { url = "https://files.pythonhosted.org/packages/bd/ed/ea49f324db00196e9ef7fe00dd13c6164d5173dd0f1bbe495e61bb1fb09d/regex-2025.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:ec1efb4c25e1849c2685fa95da44bfde1b28c62d356f9c8d861d4dad89ed56e9", size = 268589, upload-time = "2025-09-01T22:08:56.369Z" }, - { url = "https://files.pythonhosted.org/packages/98/25/b2959ce90c6138c5142fe5264ee1f9b71a0c502ca4c7959302a749407c79/regex-2025.9.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bc6834727d1b98d710a63e6c823edf6ffbf5792eba35d3fa119531349d4142ef", size = 485932, upload-time = "2025-09-01T22:08:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/49/2e/6507a2a85f3f2be6643438b7bd976e67ad73223692d6988eb1ff444106d3/regex-2025.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c3dc05b6d579875719bccc5f3037b4dc80433d64e94681a0061845bd8863c025", size = 289568, upload-time = "2025-09-01T22:08:59.258Z" }, - { url = "https://files.pythonhosted.org/packages/c7/d8/de4a4b57215d99868f1640e062a7907e185ec7476b4b689e2345487c1ff4/regex-2025.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22213527df4c985ec4a729b055a8306272d41d2f45908d7bacb79be0fa7a75ad", size = 286984, upload-time = "2025-09-01T22:09:00.835Z" }, - { url = "https://files.pythonhosted.org/packages/03/15/e8cb403403a57ed316e80661db0e54d7aa2efcd85cb6156f33cc18746922/regex-2025.9.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e3f6e3c5a5a1adc3f7ea1b5aec89abfc2f4fbfba55dafb4343cd1d084f715b2", size = 797514, upload-time = "2025-09-01T22:09:02.538Z" }, - { url = "https://files.pythonhosted.org/packages/e4/26/2446f2b9585fed61faaa7e2bbce3aca7dd8df6554c32addee4c4caecf24a/regex-2025.9.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcb89c02a0d6c2bec9b0bb2d8c78782699afe8434493bfa6b4021cc51503f249", size = 862586, upload-time = "2025-09-01T22:09:04.322Z" }, - { url = "https://files.pythonhosted.org/packages/fd/b8/82ffbe9c0992c31bbe6ae1c4b4e21269a5df2559102b90543c9b56724c3c/regex-2025.9.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0e2f95413eb0c651cd1516a670036315b91b71767af83bc8525350d4375ccba", size = 910815, upload-time = "2025-09-01T22:09:05.978Z" }, - { url = "https://files.pythonhosted.org/packages/2f/d8/7303ea38911759c1ee30cc5bc623ee85d3196b733c51fd6703c34290a8d9/regex-2025.9.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a41dc039e1c97d3c2ed3e26523f748e58c4de3ea7a31f95e1cf9ff973fff5a", size = 802042, upload-time = "2025-09-01T22:09:07.865Z" }, - { url = "https://files.pythonhosted.org/packages/fc/0e/6ad51a55ed4b5af512bb3299a05d33309bda1c1d1e1808fa869a0bed31bc/regex-2025.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f0b4258b161094f66857a26ee938d3fe7b8a5063861e44571215c44fbf0e5df", size = 786764, upload-time = "2025-09-01T22:09:09.362Z" }, - { url = "https://files.pythonhosted.org/packages/8d/d5/394e3ffae6baa5a9217bbd14d96e0e5da47bb069d0dbb8278e2681a2b938/regex-2025.9.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bf70e18ac390e6977ea7e56f921768002cb0fa359c4199606c7219854ae332e0", size = 856557, upload-time = "2025-09-01T22:09:11.129Z" }, - { url = "https://files.pythonhosted.org/packages/cd/80/b288d3910c41194ad081b9fb4b371b76b0bbfdce93e7709fc98df27b37dc/regex-2025.9.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b84036511e1d2bb0a4ff1aec26951caa2dea8772b223c9e8a19ed8885b32dbac", size = 849108, upload-time = "2025-09-01T22:09:12.877Z" }, - { url = "https://files.pythonhosted.org/packages/d1/cd/5ec76bf626d0d5abdc277b7a1734696f5f3d14fbb4a3e2540665bc305d85/regex-2025.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c2e05dcdfe224047f2a59e70408274c325d019aad96227ab959403ba7d58d2d7", size = 788201, upload-time = "2025-09-01T22:09:14.561Z" }, - { url = "https://files.pythonhosted.org/packages/b5/36/674672f3fdead107565a2499f3007788b878188acec6d42bc141c5366c2c/regex-2025.9.1-cp313-cp313-win32.whl", hash = "sha256:3b9a62107a7441b81ca98261808fed30ae36ba06c8b7ee435308806bd53c1ed8", size = 264508, upload-time = "2025-09-01T22:09:16.193Z" }, - { url = "https://files.pythonhosted.org/packages/83/ad/931134539515eb64ce36c24457a98b83c1b2e2d45adf3254b94df3735a76/regex-2025.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:b38afecc10c177eb34cfae68d669d5161880849ba70c05cbfbe409f08cc939d7", size = 275469, upload-time = "2025-09-01T22:09:17.462Z" }, - { url = "https://files.pythonhosted.org/packages/24/8c/96d34e61c0e4e9248836bf86d69cb224fd222f270fa9045b24e218b65604/regex-2025.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:ec329890ad5e7ed9fc292858554d28d58d56bf62cf964faf0aa57964b21155a0", size = 268586, upload-time = "2025-09-01T22:09:18.948Z" }, - { url = "https://files.pythonhosted.org/packages/21/b1/453cbea5323b049181ec6344a803777914074b9726c9c5dc76749966d12d/regex-2025.9.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:72fb7a016467d364546f22b5ae86c45680a4e0de6b2a6f67441d22172ff641f1", size = 486111, upload-time = "2025-09-01T22:09:20.734Z" }, - { url = "https://files.pythonhosted.org/packages/f6/0e/92577f197bd2f7652c5e2857f399936c1876978474ecc5b068c6d8a79c86/regex-2025.9.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c9527fa74eba53f98ad86be2ba003b3ebe97e94b6eb2b916b31b5f055622ef03", size = 289520, upload-time = "2025-09-01T22:09:22.249Z" }, - { url = "https://files.pythonhosted.org/packages/af/c6/b472398116cca7ea5a6c4d5ccd0fc543f7fd2492cb0c48d2852a11972f73/regex-2025.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c905d925d194c83a63f92422af7544ec188301451b292c8b487f0543726107ca", size = 287215, upload-time = "2025-09-01T22:09:23.657Z" }, - { url = "https://files.pythonhosted.org/packages/cf/11/f12ecb0cf9ca792a32bb92f758589a84149017467a544f2f6bfb45c0356d/regex-2025.9.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74df7c74a63adcad314426b1f4ea6054a5ab25d05b0244f0c07ff9ce640fa597", size = 797855, upload-time = "2025-09-01T22:09:25.197Z" }, - { url = "https://files.pythonhosted.org/packages/46/88/bbb848f719a540fb5997e71310f16f0b33a92c5d4b4d72d4311487fff2a3/regex-2025.9.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4f6e935e98ea48c7a2e8be44494de337b57a204470e7f9c9c42f912c414cd6f5", size = 863363, upload-time = "2025-09-01T22:09:26.705Z" }, - { url = "https://files.pythonhosted.org/packages/54/a9/2321eb3e2838f575a78d48e03c1e83ea61bd08b74b7ebbdeca8abc50fc25/regex-2025.9.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4a62d033cd9ebefc7c5e466731a508dfabee827d80b13f455de68a50d3c2543d", size = 910202, upload-time = "2025-09-01T22:09:28.906Z" }, - { url = "https://files.pythonhosted.org/packages/33/07/d1d70835d7d11b7e126181f316f7213c4572ecf5c5c97bdbb969fb1f38a2/regex-2025.9.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef971ebf2b93bdc88d8337238be4dfb851cc97ed6808eb04870ef67589415171", size = 801808, upload-time = "2025-09-01T22:09:30.733Z" }, - { url = "https://files.pythonhosted.org/packages/13/d1/29e4d1bed514ef2bf3a4ead3cb8bb88ca8af94130239a4e68aa765c35b1c/regex-2025.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d936a1db208bdca0eca1f2bb2c1ba1d8370b226785c1e6db76e32a228ffd0ad5", size = 786824, upload-time = "2025-09-01T22:09:32.61Z" }, - { url = "https://files.pythonhosted.org/packages/33/27/20d8ccb1bee460faaa851e6e7cc4cfe852a42b70caa1dca22721ba19f02f/regex-2025.9.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:7e786d9e4469698fc63815b8de08a89165a0aa851720eb99f5e0ea9d51dd2b6a", size = 857406, upload-time = "2025-09-01T22:09:34.117Z" }, - { url = "https://files.pythonhosted.org/packages/74/fe/60c6132262dc36430d51e0c46c49927d113d3a38c1aba6a26c7744c84cf3/regex-2025.9.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6b81d7dbc5466ad2c57ce3a0ddb717858fe1a29535c8866f8514d785fdb9fc5b", size = 848593, upload-time = "2025-09-01T22:09:35.598Z" }, - { url = "https://files.pythonhosted.org/packages/cc/ae/2d4ff915622fabbef1af28387bf71e7f2f4944a348b8460d061e85e29bf0/regex-2025.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cd4890e184a6feb0ef195338a6ce68906a8903a0f2eb7e0ab727dbc0a3156273", size = 787951, upload-time = "2025-09-01T22:09:37.139Z" }, - { url = "https://files.pythonhosted.org/packages/85/37/dc127703a9e715a284cc2f7dbdd8a9776fd813c85c126eddbcbdd1ca5fec/regex-2025.9.1-cp314-cp314-win32.whl", hash = "sha256:34679a86230e46164c9e0396b56cab13c0505972343880b9e705083cc5b8ec86", size = 269833, upload-time = "2025-09-01T22:09:39.245Z" }, - { url = "https://files.pythonhosted.org/packages/83/bf/4bed4d3d0570e16771defd5f8f15f7ea2311edcbe91077436d6908956c4a/regex-2025.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:a1196e530a6bfa5f4bde029ac5b0295a6ecfaaffbfffede4bbaf4061d9455b70", size = 278742, upload-time = "2025-09-01T22:09:40.651Z" }, - { url = "https://files.pythonhosted.org/packages/cf/3e/7d7ac6fd085023312421e0d69dfabdfb28e116e513fadbe9afe710c01893/regex-2025.9.1-cp314-cp314-win_arm64.whl", hash = "sha256:f46d525934871ea772930e997d577d48c6983e50f206ff7b66d4ac5f8941e993", size = 271860, upload-time = "2025-09-01T22:09:42.413Z" }, - { url = "https://files.pythonhosted.org/packages/ae/28/3f2b211686160f15ccf2ecd770e25c588fb776332956006e02ab93982a5f/regex-2025.9.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a13d20007dce3c4b00af5d84f6c191ed1c0f70928c6d9b6cd7b8d2f125df7f46", size = 484825, upload-time = "2025-09-01T22:09:44.502Z" }, - { url = "https://files.pythonhosted.org/packages/be/8d/88539f6ca5967022942ab64f2038a037b344db636c80852d2b4b147f6b1a/regex-2025.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d6b046b0a01cb713fd53ef36cb59db4b0062b343db28e83b52ac6aa01ee5b368", size = 288972, upload-time = "2025-09-01T22:09:46.415Z" }, - { url = "https://files.pythonhosted.org/packages/88/ad/0b9316cfe91d8a2917fbcd8cb7c990051745b5852f9f3088b93b4267fe68/regex-2025.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0fa9a7477288717f42dbd02ff5d13057549e9a8cdb81f224c313154cc10bab52", size = 286624, upload-time = "2025-09-01T22:09:47.823Z" }, - { url = "https://files.pythonhosted.org/packages/e9/6e/199003d956dac19ddaebb2ef3c068379918539a497dd4d4eb5561a1c5e3f/regex-2025.9.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2b3ad150c6bc01a8cd5030040675060e2adbe6cbc50aadc4da42c6d32ec266e", size = 779882, upload-time = "2025-09-01T22:09:49.601Z" }, - { url = "https://files.pythonhosted.org/packages/69/0a/a4bed3424beefbf8322b8b581aa2e2a65cbfe358d968f58a2ab910d62927/regex-2025.9.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aa88d5a82dfe80deaf04e8c39c8b0ad166d5d527097eb9431cb932c44bf88715", size = 848938, upload-time = "2025-09-01T22:09:51.155Z" }, - { url = "https://files.pythonhosted.org/packages/d2/01/c6c2bc65e97387523742c54daec1384a50d2575d1189e844e4e320e145e9/regex-2025.9.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6f1dae2cf6c2dbc6fd2526653692c144721b3cf3f769d2a3c3aa44d0f38b9a58", size = 896747, upload-time = "2025-09-01T22:09:52.965Z" }, - { url = "https://files.pythonhosted.org/packages/4a/56/25480407a452198414a68d6d1f4f9920652810ba9f2c7f6279e5bd9b15ef/regex-2025.9.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ff62a3022914fc19adaa76b65e03cf62bc67ea16326cbbeb170d280710a7d719", size = 789466, upload-time = "2025-09-01T22:09:54.564Z" }, - { url = "https://files.pythonhosted.org/packages/82/5c/207422b3a2c3923eb09a1e807435effcbd8b756cd8ece24c153d525463d6/regex-2025.9.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a34ef82216189d823bc82f614d1031cb0b919abef27cecfd7b07d1e9a8bdeeb4", size = 780130, upload-time = "2025-09-01T22:09:56.413Z" }, - { url = "https://files.pythonhosted.org/packages/2c/e3/74a9da339f043006a1c0e0daae143c2d8b70e8987da3c967dbb8b6e8c9d2/regex-2025.9.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6d40e6b49daae9ebbd7fa4e600697372cba85b826592408600068e83a3c47211", size = 773140, upload-time = "2025-09-01T22:09:57.99Z" }, - { url = "https://files.pythonhosted.org/packages/45/11/d2ca4dccec797f9325cfae0cfff8ee2977e1d5db43296ed949f8c63b6db0/regex-2025.9.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:0aeb0fe80331059c152a002142699a89bf3e44352aee28261315df0c9874759b", size = 843538, upload-time = "2025-09-01T22:09:59.74Z" }, - { url = "https://files.pythonhosted.org/packages/d5/9a/652095a5df5a8cd6b550280dc4b885f06beb68e2dc8728b380639b40446d/regex-2025.9.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:a90014d29cb3098403d82a879105d1418edbbdf948540297435ea6e377023ea7", size = 834151, upload-time = "2025-09-01T22:10:01.658Z" }, - { url = "https://files.pythonhosted.org/packages/17/27/f1ca20401df0544a627188fede42ef723b7d88fed8364b0d80fa9833bb84/regex-2025.9.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6ff623271e0b0cc5a95b802666bbd70f17ddd641582d65b10fb260cc0c003529", size = 778009, upload-time = "2025-09-01T22:10:03.609Z" }, - { url = "https://files.pythonhosted.org/packages/20/28/c6c94f642c115e72142e8404464835f03dee55266a158455aeb8216d6d4d/regex-2025.9.1-cp39-cp39-win32.whl", hash = "sha256:d161bfdeabe236290adfd8c7588da7f835d67e9e7bf2945f1e9e120622839ba6", size = 264141, upload-time = "2025-09-01T22:10:05.457Z" }, - { url = "https://files.pythonhosted.org/packages/ff/7e/4ef3cf970b4c7ef9abee4c5a42f3405a452e5abe21095ee36f9a3a83af9d/regex-2025.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:43ebc77a7dfe36661192afd8d7df5e8be81ec32d2ad0c65b536f66ebfec3dece", size = 276219, upload-time = "2025-09-01T22:10:07.106Z" }, - { url = "https://files.pythonhosted.org/packages/60/42/86a45b36b0d8f37388485a2c93d0f3c571732ac4e17b92d8ccd5da53b792/regex-2025.9.1-cp39-cp39-win_arm64.whl", hash = "sha256:5d74b557cf5554001a869cda60b9a619be307df4d10155894aeaad3ee67c9899", size = 268497, upload-time = "2025-09-01T22:10:08.595Z" }, +version = "2026.7.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/37/451aaddbf50922f34d744ad5ca919ae1fcfac112123885d9728f52a484b3/regex-2026.7.10.tar.gz", hash = "sha256:1050fedf0a8a92e843971120c2f57c3a99bea86c0dfa1d63a9fac053fe54b135", size = 416282, upload-time = "2026-07-10T19:49:46.267Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/69/62bb7d63f26698949c905cb7ebe29c7b0659e2a7f2a50c35cc29640b0852/regex-2026.7.10-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:799a369bdab91dcf0eb424ebd7aa9650897025ce22f729248d8f2c72002c4daa", size = 494652, upload-time = "2026-07-10T19:46:28.394Z" }, + { url = "https://files.pythonhosted.org/packages/3a/f2/eed2ce38cc38def9c366d060ec739ff5f235a33647ceb73ae6be37306d39/regex-2026.7.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f0192e5f1cfc70e3cb35347135dd02e7497b3e7d83e378aa226d8b3e53a93f19", size = 295920, upload-time = "2026-07-10T19:46:30.342Z" }, + { url = "https://files.pythonhosted.org/packages/36/57/4d724eeb1c440d71ccd6400d33b62b911bb62ca05385fe1961556e628319/regex-2026.7.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:221f2771cb780186b94bbf125a151bbeb242fa1a971da6ad59d7b0370f19de9a", size = 290696, upload-time = "2026-07-10T19:46:31.953Z" }, + { url = "https://files.pythonhosted.org/packages/c7/9d/76c6779e424c64740d2a564b7ecb389a62c77b6294127d34f52008c91ea7/regex-2026.7.10-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab2fb1f7a2deb4ca3ddebbae6b93905d21480a3b4e11de28d79d9fb0d316fcf8", size = 784833, upload-time = "2026-07-10T19:46:33.145Z" }, + { url = "https://files.pythonhosted.org/packages/54/ec/f777841d88f0c9d46699daf16f86a8086e077e506603be212de2c4584f85/regex-2026.7.10-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f98ef73a13791a387d5c841416ad7f52040ae5caf10bcf46fa12bd2b3d63745", size = 852182, upload-time = "2026-07-10T19:46:34.368Z" }, + { url = "https://files.pythonhosted.org/packages/d1/de/5ba208a0826117851f6c12af9ae7fae5838ccfde69b999b26c5fdc1cedc3/regex-2026.7.10-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9a094ed44a22f9da497453137c3118b531fd783866ab524b0b0fc146e7395e1d", size = 899571, upload-time = "2026-07-10T19:46:35.564Z" }, + { url = "https://files.pythonhosted.org/packages/7f/46/602b7b81d26a53113d3cec6dd845fd664d7b854b0ea45245bfdd6b9dc9ef/regex-2026.7.10-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53bbbd6c610489700f7110db1d85f3623924c3f7c760f987eca033867360788a", size = 794164, upload-time = "2026-07-10T19:46:36.972Z" }, + { url = "https://files.pythonhosted.org/packages/53/cc/c21ebc520c3e17d93e495eb2de2b1c5ae5f6780ccdb298b2d8ce1e940820/regex-2026.7.10-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:87b776cf2890e356e4ab104b9df846e169da3eb5b0f110975547091f4e51854e", size = 786304, upload-time = "2026-07-10T19:46:38.238Z" }, + { url = "https://files.pythonhosted.org/packages/65/d8/69ec8c062bbba8d4d153f9eb70ab43564f591035b81fc4ea7eb059fdf71c/regex-2026.7.10-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ab39d2c967aae3b48a412bff9cdbe7cd7559cd1e277599aceaeada7bc82b7200", size = 769958, upload-time = "2026-07-10T19:46:39.541Z" }, + { url = "https://files.pythonhosted.org/packages/af/82/c56db326ac5f852865bd78d49a275757cc367e8114b13bc1015ed2491db1/regex-2026.7.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b56416091bfd7a429f958f69aaf6823c517be9a49cb5bf1daa3767ce8bf8095e", size = 775056, upload-time = "2026-07-10T19:46:40.808Z" }, + { url = "https://files.pythonhosted.org/packages/af/2a/1ba62462f679eb598d7325ff20798a264ddb9aa34a3f5d2ac388d54c6e4b/regex-2026.7.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:617e8f10472e34a8477931f978ff3a88d46ae2ba0e41927e580b933361f60948", size = 848857, upload-time = "2026-07-10T19:46:42.02Z" }, + { url = "https://files.pythonhosted.org/packages/84/5b/37bf2e2fe810540ca90bbfe457a2f61088721c95d442eee8bb1257165ad7/regex-2026.7.10-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:31fa17378b29519bfd0a1b8ba4e9c10cf0baf1cf4099b39b0689429e7dc2c795", size = 757747, upload-time = "2026-07-10T19:46:43.282Z" }, + { url = "https://files.pythonhosted.org/packages/9a/c5/131dd41f73f766af6b08492073b5f28bc4f907b5571230c9f9e895e88302/regex-2026.7.10-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5c363de7c0339d39341b6181839ed32509820b85ef506deafcf2e7e43baadab4", size = 837183, upload-time = "2026-07-10T19:46:44.91Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ee/00b9332c3c5d7460639f2c10fdc7197a64de6eaafff69478ec3332815335/regex-2026.7.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed7c886a2fcbf14493ceaf9579394b33521730c161ebb8dad7db9c3e9fcab1a8", size = 782151, upload-time = "2026-07-10T19:46:46.385Z" }, + { url = "https://files.pythonhosted.org/packages/e5/a7/31ce26ec6465c12e7136ea9efcc716f5c55cac616f7958c33f0bf171781e/regex-2026.7.10-cp310-cp310-win32.whl", hash = "sha256:b04583e8867136ae66353fa274f45121ab3ec3166dc45aaff3655a5db90d9f0e", size = 266770, upload-time = "2026-07-10T19:46:47.608Z" }, + { url = "https://files.pythonhosted.org/packages/71/00/22a554bb83203eef88a40ec2fe7cf9a6e5df8765d57f7a96ebe1af5d60c3/regex-2026.7.10-cp310-cp310-win_amd64.whl", hash = "sha256:e21e888a6b471b2bb1cdd4247e8d86632672232f29be583e7eafaa5f4634d34c", size = 277941, upload-time = "2026-07-10T19:46:48.915Z" }, + { url = "https://files.pythonhosted.org/packages/76/46/596a7084918ddf18cea6fb0cc047af1f00cec8790962e8f2edee9b6ec749/regex-2026.7.10-cp310-cp310-win_arm64.whl", hash = "sha256:081acf191b4d614d573a56cab69f948b6864daa5e3cc69f209ee92e26e454c2f", size = 276923, upload-time = "2026-07-10T19:46:50.829Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/bfd13770be1acd1c05506b93fc6be15c759d6417595d1ba334d355efbf26/regex-2026.7.10-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:66d2c35587cd601c95965d5c0415058ba5cfd6ffbab7624ce198bd967102b341", size = 494639, upload-time = "2026-07-10T19:46:52.207Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b4/0086215709f0f705661f13ba81516287538886ef0d589c545c12b0484669/regex-2026.7.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:28a0973eeffff4292f5a7ee498ab65d5e94ee8cc9cea364239251eb4a260a0f1", size = 295920, upload-time = "2026-07-10T19:46:53.63Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9e/8e07d0eea46d2cf36bf4d3794634bb0a820f016d31bc349dfef008d96b02/regex-2026.7.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8331484450b3894298bef8abecce532171ff6ac60b71f999eed10f2c01941a8a", size = 290673, upload-time = "2026-07-10T19:46:54.863Z" }, + { url = "https://files.pythonhosted.org/packages/b7/56/d83c446de21c70ff49d2f1b2ff2196ac79a4ac6373d2cfe496011a250600/regex-2026.7.10-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0639b2488b775a0109f55a5a2172deebdedb4b6c5ab0d48c90b43cbf5de58d17", size = 792378, upload-time = "2026-07-10T19:46:56.116Z" }, + { url = "https://files.pythonhosted.org/packages/dc/0e/d265e0cc6da47aea97e90eb896be2d2e8f92d16add13bac04fa46a0fd972/regex-2026.7.10-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:be4223af640d0aa04c05db81d5d96ada3ead9c09187d892fd37f4f97829480be", size = 861790, upload-time = "2026-07-10T19:46:57.611Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a5/62655f6208d1170a3e9188d6a45d4af0a5ae3b9da8b87d474818ac5ff016/regex-2026.7.10-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d3c75d57a00109255e60bc9c623b6ececaf7905eaab845c79f036670ed4750a2", size = 906530, upload-time = "2026-07-10T19:46:59.142Z" }, + { url = "https://files.pythonhosted.org/packages/86/b7/d65aa2e9ffb18677cd0afbcf5990da8519a4e50778deb1bca49f043c5174/regex-2026.7.10-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:724ee9379568658ec06362cf24325c5315cc5a67f61dfe585bfeff58300a355b", size = 799912, upload-time = "2026-07-10T19:47:00.534Z" }, + { url = "https://files.pythonhosted.org/packages/8f/19/3a5ce23ea2eb1fe36306aef49c79746ce297e4b434aeb981b525c661413a/regex-2026.7.10-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:732c19e5828eb287d01edb83b2eb87f283ba8e5fc3441c732709d3e8cbd14aaa", size = 773675, upload-time = "2026-07-10T19:47:01.999Z" }, + { url = "https://files.pythonhosted.org/packages/fe/76/3c0eaa426700dd2ba14f2335f2b700a4e1484202254192ae440b83b8352a/regex-2026.7.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:982d07727c809b42a3968785354f11c3728414e4e90af0754345b431b2c32561", size = 781711, upload-time = "2026-07-10T19:47:03.425Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a8/a5a3fad84f9a7f897619f0f8e0a2c64946e9709044a186a8f869fb5c332f/regex-2026.7.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4574feca202f8c470bf678aed8b5d89df04aaf8dc677f3b83d92825051301c0f", size = 854539, upload-time = "2026-07-10T19:47:04.999Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c7/47e9b8c8ee77723b9eda74f517b6b25d2f555cf276c063a9eeea35bd86d5/regex-2026.7.10-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:80151ca5bfc6c4524186b3e08b499e97319b2001fc265ed2d4fc12c0d5692cdf", size = 763378, upload-time = "2026-07-10T19:47:06.845Z" }, + { url = "https://files.pythonhosted.org/packages/36/09/e27e42d9d42edf71205c7e6f5b2902bc874ea03c557c80da03b8ed16c9bf/regex-2026.7.10-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bb52e10e453b5493afe1f7702a2973bc10f4dd8901c0f2ed869ffaa3f8319296", size = 844663, upload-time = "2026-07-10T19:47:08.923Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b5/2423acb98362184ad9c8eebabafa15188d6a177daab919add8f2120fc6cd/regex-2026.7.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e37aba1994d73b4944053ab65a15f313bd5c28c885dd7f0d494a11749d89db6e", size = 789236, upload-time = "2026-07-10T19:47:10.303Z" }, + { url = "https://files.pythonhosted.org/packages/60/ed/b387e84c8a3d6aa115dfb56865437a3fbaf28f4a6fb3b76cc6cce38ced70/regex-2026.7.10-cp311-cp311-win32.whl", hash = "sha256:6cbedeb5112f59dbd169385459b9943310bdd241c6966c19c5f6e2295055c93a", size = 266774, upload-time = "2026-07-10T19:47:11.904Z" }, + { url = "https://files.pythonhosted.org/packages/c4/64/f30a163a65ed1f07ad12c53af00a6bd2a7251a5329fba5a08adc6f9e81a3/regex-2026.7.10-cp311-cp311-win_amd64.whl", hash = "sha256:b1963ec5ba4d52788fb0eac6aca6eb8040e8e318c7e47ebbdfc09440c802919c", size = 277959, upload-time = "2026-07-10T19:47:13.231Z" }, + { url = "https://files.pythonhosted.org/packages/2b/de/61c8174171134cebb834ca9f8fe2ff8f49d8a3dd43453b48b537d0fbb49b/regex-2026.7.10-cp311-cp311-win_arm64.whl", hash = "sha256:3750c42d47712e362158a04d0fd80131f73a55e8c715b2885442a0ff6f9fc3fc", size = 276918, upload-time = "2026-07-10T19:47:14.693Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9c/2503d4ccf3452dc323f8baa3cf3ee10406037d52735c76cfced81423f183/regex-2026.7.10-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7252b48b0c60100095088fbeb281fca9a4fcf678a4e04b1c520c3f8613c952c4", size = 497114, upload-time = "2026-07-10T19:47:16.22Z" }, + { url = "https://files.pythonhosted.org/packages/91/eb/04534f4263a4f658cd20a511e9d6124350044f2214eb24fee2db96acf318/regex-2026.7.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:da6ef4cb8d457aab0482b50120136ae94238aaa421863eaa7d599759742c72d6", size = 297422, upload-time = "2026-07-10T19:47:17.794Z" }, + { url = "https://files.pythonhosted.org/packages/ca/2d/35809de392ab66ba439b58c3187ae3b8b53c883233f284b59961e5725c99/regex-2026.7.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fe7ff456c22725c9d9017f7a2a7df2b51af6df77314176760b22e2d05278e181", size = 292110, upload-time = "2026-07-10T19:47:19.188Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1e/5ce0fbe9aab071893ce2b7df020d0f561f7b411ec334124302468d587884/regex-2026.7.10-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3463a5f26be513a49e4d497debcf1b252a2db7b92c77d89621aa90b83d2dd38", size = 796800, upload-time = "2026-07-10T19:47:20.639Z" }, + { url = "https://files.pythonhosted.org/packages/d4/67/c1ccbada395c10e334763b583e1039b1660b142303ebb941d4269130b22f/regex-2026.7.10-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:948dfc62683a6947b9b486c4598d8f6e3ecc542478b6767b87d52be68aeb55c6", size = 865509, upload-time = "2026-07-10T19:47:22.135Z" }, + { url = "https://files.pythonhosted.org/packages/0e/06/f0b31afc16c1208f945b66290eb2a9936ab8becdfb23bbcedb91cc5f9d9b/regex-2026.7.10-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c2cbd385d82f63bb35edb60b09b08abad3619bd0a4a492ae59e55afaf98e1b9d", size = 912395, upload-time = "2026-07-10T19:47:24.128Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1c/8687de3a6c3220f4f872a9bf4bcd8dc249f2a96e7dddfa93de8bd4d16399/regex-2026.7.10-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f6222cafe00e072bb2b8f14142cd969637411fbc4dd3b1d73a90a3b817fa046f", size = 801308, upload-time = "2026-07-10T19:47:25.696Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e3/60a40ec02a2315d826414a125640aceb6f30450574c530c8f352110ece0e/regex-2026.7.10-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:65ee5d1ac3cd541325f5ac92625b1c1505f4d171520dd931bda7952895c5321a", size = 777120, upload-time = "2026-07-10T19:47:27.158Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9a/ec579b4f840ac59bc7c192b56e66abd4cbf385615300d59f7c94bf6863ae/regex-2026.7.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa34473fbcc108fea403074f3f45091461b18b2047d136f16ffaa4c65ad46a68", size = 785164, upload-time = "2026-07-10T19:47:28.732Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1c/60d88afd5f98d4b0fb1f8b8969270628140dc01c7ff93a939f2aa83f31a6/regex-2026.7.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d028d189d8f38d7ff292f22187c0df37f2317f554d2ed9a2908ada330af57c0", size = 860161, upload-time = "2026-07-10T19:47:30.605Z" }, + { url = "https://files.pythonhosted.org/packages/2a/40/08ae3ba45fe79e48c9a888a3389a7ee7e2d8c580d2d996da5ece02dfdcb9/regex-2026.7.10-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:396ea70e4ea1f19571940add3bad9fd3eb6a19dc610d0d01f692bc1ba0c10cb4", size = 765829, upload-time = "2026-07-10T19:47:32.06Z" }, + { url = "https://files.pythonhosted.org/packages/12/e6/e613c6755d19aca9d977cdc3418a1991ffc8f386779752dd8fdfa888ea89/regex-2026.7.10-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ebbf0d83ed5271991d666e54bb6c90ac2c55fb2ef3a88740c6af85dc85de2402", size = 852170, upload-time = "2026-07-10T19:47:33.567Z" }, + { url = "https://files.pythonhosted.org/packages/03/33/89072f2060e6b844b4916d5bc40ef01e973640c703025707869264ec75ab/regex-2026.7.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:58a4571b2a093f6f6ee4fd281faa8ebf645abcf575f758173ea2605c7a1e1ecb", size = 789550, upload-time = "2026-07-10T19:47:35.395Z" }, + { url = "https://files.pythonhosted.org/packages/e3/3c/4bc8be9a155035e63780ccac1da101f36194946fdc3f6fce90c7179fc6df/regex-2026.7.10-cp312-cp312-win32.whl", hash = "sha256:eac1207936555aa691ce32df1432b478f2729d54e6d93a1f4db9215bcd8eb47d", size = 267151, upload-time = "2026-07-10T19:47:37.047Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/9f5aade65bb98cc6e99c336e45a49a658300720c16721f3e687f8d754fec/regex-2026.7.10-cp312-cp312-win_amd64.whl", hash = "sha256:ecae626449d00db8c08f8f1fc00047a32d6d7eb5402b3976f5c3fda2b80a7a4f", size = 277751, upload-time = "2026-07-10T19:47:38.488Z" }, + { url = "https://files.pythonhosted.org/packages/36/6f/d069dd12872ea1d50e17319d342f89e2072cae4b62f4245009a1108c74d8/regex-2026.7.10-cp312-cp312-win_arm64.whl", hash = "sha256:87794549a3f5c1c2bdfba2380c1bf87b931e375f4133d929da44f95e396bf5fe", size = 277063, upload-time = "2026-07-10T19:47:40.023Z" }, + { url = "https://files.pythonhosted.org/packages/e0/88/0c977b9f3ba9b08645516eca236388c340f56f7a87054d41a187a04e134c/regex-2026.7.10-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:4db009b4fc533d79af3e841d6c8538730423f82ea8508e353a3713725de7901c", size = 496868, upload-time = "2026-07-10T19:47:41.675Z" }, + { url = "https://files.pythonhosted.org/packages/f6/51/600882cd5d9a3cf083fd66a4064f5b7f243ba2a7de2437d42823e286edaf/regex-2026.7.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b96341cb29a3faa5db05aff29c77d141d827414f145330e5d8846892119351c1", size = 297306, upload-time = "2026-07-10T19:47:43.521Z" }, + { url = "https://files.pythonhosted.org/packages/52/6f/48a912054ffcb756e374207bb8f4430c5c3e0ffa9627b3c7b6661844b30a/regex-2026.7.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:14d27f6bd04beb01f6a25a1153d73e58c290fd45d92ba56af1bb44199fd1010d", size = 291950, upload-time = "2026-07-10T19:47:45.267Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c8/8e1c3c86ebcee7effccbd1f7fc54fe3af22aa0e9204503e2baea4a6ff001/regex-2026.7.10-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e6b6a11bf898cca3ce7bfaa17b646901107f3975677fbd5097f36e5eb5641983", size = 796817, upload-time = "2026-07-10T19:47:48.054Z" }, + { url = "https://files.pythonhosted.org/packages/65/39/3e49d9ff0e0737eb8180a00569b47aabb59b84611f48392eba4d998d91a0/regex-2026.7.10-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:234f8e0d65cf1df9becadae98648f74030ee85a8f12edcb5eb0f60a22a602197", size = 865513, upload-time = "2026-07-10T19:47:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/70/57/6511ad809bb3122c65bbeeffa5b750652bb03d273d29f3acb0754109b183/regex-2026.7.10-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:91b916d495db3e1b473c7c8e68733beec4dce8e487442db61764fff94f59740e", size = 912391, upload-time = "2026-07-10T19:47:51.776Z" }, + { url = "https://files.pythonhosted.org/packages/cc/29/a1b0c109c9e878cb04b931bfe4c54332d692b93c322e127b5ae9f25b0d9e/regex-2026.7.10-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f0d4ccf70b1d13711242de0ba78967db5c35d12ac408378c70e06295c3f6644", size = 801338, upload-time = "2026-07-10T19:47:53.38Z" }, + { url = "https://files.pythonhosted.org/packages/33/be/171c3dad4d77000e1befeff2883ca88734696dfd97b2951e5e074f32e4dd/regex-2026.7.10-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c622f4c638a725c39abcb2e680b1bd592663c83b672a4ed350a17f806d75618e", size = 777149, upload-time = "2026-07-10T19:47:54.944Z" }, + { url = "https://files.pythonhosted.org/packages/33/61/41ab0de0e4574da1071c151f67d1eb9db3d92c43e31d64d2e6863c3d89bf/regex-2026.7.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:41a47c2b28d9421e2509a4583a22510dc31d83212fcf38e1508a7013140f71a8", size = 785216, upload-time = "2026-07-10T19:47:56.56Z" }, + { url = "https://files.pythonhosted.org/packages/66/28/372859ea693736f07cf7023247c7eca8f221d9c6df8697ff9f93371cca08/regex-2026.7.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:13fba679fe035037e9d5286620f88bbfd105df4d5fcd975942edd282ab986775", size = 860229, upload-time = "2026-07-10T19:47:58.278Z" }, + { url = "https://files.pythonhosted.org/packages/50/b1/e1d32cd944b599534ae655d35e8640d0ec790c0fa12e1fb29bf434d50f55/regex-2026.7.10-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8e26a075fa9945b9e44a3d02cc83d776c3b76bb1ff4b133bbfa620d5650131da", size = 765797, upload-time = "2026-07-10T19:48:00.291Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/79a2cd9556a3329351e370929743ef4f0ccc0aaff6b3dc414ae5fa4a1302/regex-2026.7.10-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d0834c84ae8750ae1c4cede59b0afd4d2f775be958e11b18a3eea24ed9d0d9f1", size = 852130, upload-time = "2026-07-10T19:48:01.972Z" }, + { url = "https://files.pythonhosted.org/packages/66/58/76fec29898cf5d359ab63face50f9d4f7135cc2eca3477139227b1d09952/regex-2026.7.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64722a5031aeace7f6c8d5ea9a9b22d9368af0d6e8fa532585da8158549ea963", size = 789644, upload-time = "2026-07-10T19:48:03.748Z" }, + { url = "https://files.pythonhosted.org/packages/f6/06/3c7cec7817bda293e13c8f88aed227bbcf8b37e5990936ff6442a8fdf11a/regex-2026.7.10-cp313-cp313-win32.whl", hash = "sha256:74ae61d8573ecd51b5eeee7be2218e4c56e99c14fa8fcf97cf7519611d4be92e", size = 267130, upload-time = "2026-07-10T19:48:05.677Z" }, + { url = "https://files.pythonhosted.org/packages/88/6c/e2a6f9a6a905f923cfc912298a5949737e9504b1ca24f29eda8d04d05ece/regex-2026.7.10-cp313-cp313-win_amd64.whl", hash = "sha256:5e792367e5f9b4ffb8cad93f1beaa91837056b94da98aa5c65a0db0c1b474927", size = 277722, upload-time = "2026-07-10T19:48:07.318Z" }, + { url = "https://files.pythonhosted.org/packages/00/a6/9d8935aaa940c388496aa1a0c82669cc4b5d06291c2712d595e3f0cf16d3/regex-2026.7.10-cp313-cp313-win_arm64.whl", hash = "sha256:82ab8330e7e2e416c2d42fcec67f02c242393b8681014750d4b70b3f158e1f08", size = 277059, upload-time = "2026-07-10T19:48:08.977Z" }, + { url = "https://files.pythonhosted.org/packages/7d/e9/26decfd3e85c09e42ff7b0d23a6f51085ca4c268db15f084928ca33459c6/regex-2026.7.10-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:2b93eafd92c4128bab2f93500e8912cc9ecb3d3765f6685b902c6820d0909b6b", size = 501508, upload-time = "2026-07-10T19:48:10.668Z" }, + { url = "https://files.pythonhosted.org/packages/38/a5/5b167cebde101945690219bf34361481c9f07e858a4f46d9996b80ec1490/regex-2026.7.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3f03b92fb6ec739df042e45b06423fc717ecf0063e07ffe2897f7b2d5735e1e8", size = 299705, upload-time = "2026-07-10T19:48:12.544Z" }, + { url = "https://files.pythonhosted.org/packages/f6/20/7909be4b9f449f8c282c14b6762d59aa722aeaeebe7ee4f9bb623eeaa5e0/regex-2026.7.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:bb5aab464a0c5e03a97abad5bdf54517061ebbf72340d576e99ff661a42575cc", size = 294605, upload-time = "2026-07-10T19:48:14.495Z" }, + { url = "https://files.pythonhosted.org/packages/82/88/e52550185d6fda68f549b01239698697de47320fd599f5e880b1986b7673/regex-2026.7.10-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fadb07dbe36a541283ff454b1a268afd54b077d917043f2e1e5615372cb5f200", size = 811747, upload-time = "2026-07-10T19:48:16.197Z" }, + { url = "https://files.pythonhosted.org/packages/06/98/16c255c909714de1ee04da6ae30f3ee04170f300cdc0dcf57a314ee4816a/regex-2026.7.10-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:21150500b970b12202879dfd82e7fd809d8e853140fff84d08e57a90cf1e154e", size = 871203, upload-time = "2026-07-10T19:48:18.12Z" }, + { url = "https://files.pythonhosted.org/packages/3b/32/423ed27c9bae2092a453e853da2b6628a658d08bb5a6117db8d591183d85/regex-2026.7.10-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a68b637451d64ba30ed8ae125c973fa834cc2d37dfa7f154c2b479015d477ba8", size = 917334, upload-time = "2026-07-10T19:48:19.952Z" }, + { url = "https://files.pythonhosted.org/packages/73/87/74dac8efb500db31cb000fda6bae2be45fc2fbf1fa9412f445fbb8acbe37/regex-2026.7.10-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e23458d8903e33e7d27196d7a311523dc4e2f4137a5f34e4dbd30c8d37ff33e", size = 816379, upload-time = "2026-07-10T19:48:21.616Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9f/1859403654e3e030b288f06d49233c6a4f889d62b84c4ef3f3a28653173d/regex-2026.7.10-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae27622c094558e519abf3242cf4272db961d12c5c9a9ffb7a1b44b2627d5c6", size = 785563, upload-time = "2026-07-10T19:48:23.643Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d8/35d30d6bdf1ef6a5430e8982607b3a6db4df1ddedbe001e43435585d88ba/regex-2026.7.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ee877b6d78f9dff1da94fef51ae8cf9cce0967e043fdcc864c40b85cf293c192", size = 801415, upload-time = "2026-07-10T19:48:25.499Z" }, + { url = "https://files.pythonhosted.org/packages/f7/22/630f31f5ea4826167b2b064d9cac2093a5b3222af380aa432cfe1a5dabcd/regex-2026.7.10-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:2c66a8a1969cfd506d1e203c0005fd0fc3fe6efc83c945606566b6f9611d4851", size = 866560, upload-time = "2026-07-10T19:48:27.789Z" }, + { url = "https://files.pythonhosted.org/packages/8d/14/f5914a6d9c5bc63b9bed8c9a1169fb0be35dbe05cdc460e17d953031a366/regex-2026.7.10-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:2bc350e1c5fa250f30ab0c3e38e5cfdffcd82cb8af224df69955cab4e3003812", size = 772877, upload-time = "2026-07-10T19:48:29.563Z" }, + { url = "https://files.pythonhosted.org/packages/c1/0f/7c13999eef3e4186f7c79d4950fa56f041bf4de107682fb82c80db605ff9/regex-2026.7.10-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:53f54993b462f3f91fea0f2076b46deb6619a5f45d70dbd1f543f789d8b900ef", size = 856648, upload-time = "2026-07-10T19:48:31.282Z" }, + { url = "https://files.pythonhosted.org/packages/a4/71/a48e43909b6450fb48fa94e783bef2d9a37179258bc32ef2283955df7be7/regex-2026.7.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cfcec18f7da682c4e2d82112829ce906569cb8d69fa6c26f3a50dfbed5ceb682", size = 803520, upload-time = "2026-07-10T19:48:33.275Z" }, + { url = "https://files.pythonhosted.org/packages/e0/b8/f037d1bf2c133cb24ceb6e7d81d08417080390eddab6ddfd701aa7091874/regex-2026.7.10-cp313-cp313t-win32.whl", hash = "sha256:a2d6d30be35ddd70ce0f8ee259a4c25f24d6d689a45a5ac440f03e6bcc5a21d1", size = 269168, upload-time = "2026-07-10T19:48:35.353Z" }, + { url = "https://files.pythonhosted.org/packages/b6/9c/eaac34f8452a838956e7e89852ad049678cdc1af5d14f72d3b3b658b1ea5/regex-2026.7.10-cp313-cp313t-win_amd64.whl", hash = "sha256:c57b6ad3f7a1bdd101b2966f29dc161adf49727b1e8d3e1e89db2eda8a75c344", size = 280004, upload-time = "2026-07-10T19:48:37.106Z" }, + { url = "https://files.pythonhosted.org/packages/cd/a9/e22e997587bc1d588b0b2cd0572027d39dd3a006216e40bbf0361688c51c/regex-2026.7.10-cp313-cp313t-win_arm64.whl", hash = "sha256:3d8ef9df02c8083c7b4b855e3cb87c8e0ebbcfea088d98c7a886aaefdf88d837", size = 279308, upload-time = "2026-07-10T19:48:38.907Z" }, + { url = "https://files.pythonhosted.org/packages/6a/4a/a7fa3ada9bd2d2ce20d56dfceec6b2a51afeed9bf3d8286355ceec5f0628/regex-2026.7.10-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:39f81d1fdf594446495f2f4edd8e62d8eda0f7a802c77ac596dc8448ad4cc5ca", size = 497087, upload-time = "2026-07-10T19:48:40.543Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7e/ca0b1a87192e5828dbc16f16ae6caca9b67f25bf729a3348468a5ff52755/regex-2026.7.10-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:441edc66a54063f8269d1494fc8474d06605e71e8a918f4bcfd079ebda4ce042", size = 297307, upload-time = "2026-07-10T19:48:42.213Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/fb40bb34275d3cd4d7a376d5fb2ea1f0f4a96fd884fa83c0c4ae869001bf/regex-2026.7.10-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cfeb11990f59e59a0df26c648f0adfcbf27be77241250636f5769eb08db662be", size = 292163, upload-time = "2026-07-10T19:48:43.929Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/34cbea16c8fea9a18475a7e8f5837c70af451e738bfeb4eb5b029b7dc07a/regex-2026.7.10-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:460176b2db044a292baaee6891106566739657877af89a251cded228689015a6", size = 797064, upload-time = "2026-07-10T19:48:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/87/77/f6805d97f15f5a710bdfd56a768f3468c978239daf9e1b15efd8935e1967/regex-2026.7.10-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9dc55698737aca028848bde418d6c51d74f2a5fd44872d3c8b56b626729adb89", size = 866155, upload-time = "2026-07-10T19:48:47.589Z" }, + { url = "https://files.pythonhosted.org/packages/a2/e3/a2a905807bba3bcd90d6ebbb67d27af2adf7d41708175cbc6b956a0c75f1/regex-2026.7.10-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d3e10779f60c000213a5b53f518824bd07b3dc119333b26d70c6be1c27b5c794", size = 911596, upload-time = "2026-07-10T19:48:49.473Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ca/a3126888b2c6f33c7e29144fedf85f6d5a52a400024fa045ad8fc0550ef1/regex-2026.7.10-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:38a5926601aaccf379512746b86eb0ac1d29121f6c776dac6ac5b31077432f2c", size = 800713, upload-time = "2026-07-10T19:48:51.452Z" }, + { url = "https://files.pythonhosted.org/packages/66/19/9d252fd969f726c8b56b4bacf910811cc70495a110907b3a7ccb96cd9cad/regex-2026.7.10-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a72ecf5bfd3fc8d57927f7e3ded2487e144472f39010c3acaec3f6f3ff53f361", size = 777286, upload-time = "2026-07-10T19:48:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/40/7a/5f1bf433fa446ecb3aab87bb402603dc9e171ef8052c1bb8690bb4e255a3/regex-2026.7.10-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d50714405845c1010c871098558cfe5718fe39d2a2fab5f95c8863caeb7a82b3", size = 785826, upload-time = "2026-07-10T19:48:55.381Z" }, + { url = "https://files.pythonhosted.org/packages/99/ca/69f3a7281d86f1b592338007f3e535cc219d771448e2b61c0b56e4f9d05b/regex-2026.7.10-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ec1c44cf9bd22079aac37a07cb49a29ced9050ab5bddf24e50aba298f1e34d90", size = 860957, upload-time = "2026-07-10T19:48:57.962Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/487ff55c8d515ec9dd60d7ba3c129eeaa9e527358ed9e8a054a9e9430f81/regex-2026.7.10-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:9e9aaef25a40d1f1e1bbb1d0eb0190c4a64a7a1750f7eb67b8399bed6f4fd2a6", size = 765959, upload-time = "2026-07-10T19:49:00.27Z" }, + { url = "https://files.pythonhosted.org/packages/73/e1/fa034e6fa8896a09bd0d5e19c81fdc024411ab37980950a0401dccee8f6d/regex-2026.7.10-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e54e088dc64dd2766014e7cfe5f8bc45399400fd486816e494f93e3f0f55da06", size = 851447, upload-time = "2026-07-10T19:49:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a5/b9427ed53b0e14c540dc436d56aaf57a19fb9183c6e7abd66f4b4368fbad/regex-2026.7.10-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:834271b1ff2cfa1f67fcd65a48bf11d11e9ab837e21bf79ce554efb648599ae8", size = 789418, upload-time = "2026-07-10T19:49:03.949Z" }, + { url = "https://files.pythonhosted.org/packages/ba/52/aab92420c8aa845c7bcbe68dc65023d4a9e9ea785abf0beb2198f0de5ba1/regex-2026.7.10-cp314-cp314-win32.whl", hash = "sha256:f988a1cec68058f71a38471813fba9e87dffe855582682e8a10e40ece12567a2", size = 272538, upload-time = "2026-07-10T19:49:05.833Z" }, + { url = "https://files.pythonhosted.org/packages/99/16/5c7050e0ef7dd8889441924ff0a2c33b7f0587c0ccb0953fe7ca997d673b/regex-2026.7.10-cp314-cp314-win_amd64.whl", hash = "sha256:2129e4a5e86f26926982d883dff815056f2e98220fdf630e59f961b578a26c43", size = 280796, upload-time = "2026-07-10T19:49:07.593Z" }, + { url = "https://files.pythonhosted.org/packages/e8/1a/4f6099d2ba271502fdb97e697bae2ed0213c0d87f2273fe7d21e2e401d12/regex-2026.7.10-cp314-cp314-win_arm64.whl", hash = "sha256:9cd5b6805396157b4cf993a6940cbb8663161f29b4df2458c1c9991f099299c5", size = 281017, upload-time = "2026-07-10T19:49:09.767Z" }, + { url = "https://files.pythonhosted.org/packages/19/02/4061fc71f64703e0df61e782c2894c3fbc089d277767eff6e16099581c73/regex-2026.7.10-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:103e8f3acc3dcede88c0331c8612766bdcfc47c9250c5477f0e10e0550b9da49", size = 501467, upload-time = "2026-07-10T19:49:11.952Z" }, + { url = "https://files.pythonhosted.org/packages/73/a5/8d42b2f3fd672908a05582effd0f88438bf9bb4e8e02d69a62c723e23601/regex-2026.7.10-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:538ddb143f5ca085e372def17ef3ed9d74b50ad7fc431bd85dc50a9af1a7076f", size = 299700, upload-time = "2026-07-10T19:49:14.067Z" }, + { url = "https://files.pythonhosted.org/packages/65/70/36fa4b46f73d268c0dbe77c40e62da2cd4833ee206d3b2e438c2034e1f36/regex-2026.7.10-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6e3448e86b05ce87d4eb50f9c680860830f3b32493660b39f43957d6263e2eba", size = 294590, upload-time = "2026-07-10T19:49:15.883Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a7/b6db1823f3a233c2a46f854fdc986f4fd424a84ed557b7751f2998efb266/regex-2026.7.10-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5eab9d3f981c423afd1a61db055cfe83553c3f6455949e334db04722469dd0a2", size = 811925, upload-time = "2026-07-10T19:49:17.97Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7d/f8bee4c210c42c7e8b952bb9fb7099dd7fb2f4bd0f33d0d65a8ab08aafc0/regex-2026.7.10-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:177f930af3ad72e1045f8877540e0c43a38f7d328cf05f31963d0bd5f7ecf067", size = 871257, upload-time = "2026-07-10T19:49:19.943Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/22adf72e614ba0216b996e9aaef5712c23699e360ea127bb3d5ee1a7666f/regex-2026.7.10-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dd3b6d97beb39afb412f2c79522b9e099463c31f4c49ab8347c5a2ca3531c478", size = 917551, upload-time = "2026-07-10T19:49:22.069Z" }, + { url = "https://files.pythonhosted.org/packages/03/f7/ebc15a39e81e6b58da5f913b91fc293a25c6700d353c14d5cd25fc85712a/regex-2026.7.10-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8679f0652a183d93da646fcec8da8228db0be40d1595da37e6d74c2dc8c4713c", size = 816436, upload-time = "2026-07-10T19:49:24.131Z" }, + { url = "https://files.pythonhosted.org/packages/5c/33/20bc2bdd57f7e0fcc51be37e4c4d1bca7f0b4af8dc0a148c23220e689da8/regex-2026.7.10-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:494b19a5805438aeb582de99f9d97603d8fd48e6f4cc74d0088bb292b4da3b70", size = 785935, upload-time = "2026-07-10T19:49:26.265Z" }, + { url = "https://files.pythonhosted.org/packages/b4/51/87ff99c849b56309c40214a72b54b0eef320d0516a8a516970cc8be1b725/regex-2026.7.10-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0911e34151a5429d0325dae538ba9851ec0b62426bdfd613060cda8f1c36ec7f", size = 801494, upload-time = "2026-07-10T19:49:28.493Z" }, + { url = "https://files.pythonhosted.org/packages/16/11/fde67d49083fef489b7e0f841e2e5736516795b166c9867f05956c1e494b/regex-2026.7.10-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b862572b7a5f5ed47d2ba5921e63bf8d9e3b682f859d8f11e0e5ca46f7e82173", size = 866549, upload-time = "2026-07-10T19:49:30.592Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b5/31a156c36acf10181d88f55a66c688d5454a344e53ccc03d49f4a48a2297/regex-2026.7.10-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:3f361215e000d68a4aff375106637b83c80be36091d83ee5107ad3b32bd73f48", size = 773089, upload-time = "2026-07-10T19:49:32.661Z" }, + { url = "https://files.pythonhosted.org/packages/27/bb/734e978c904726664df47ae36ce5eca5065de5141185ae46efec063476a2/regex-2026.7.10-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:4533af6099543db32ef26abc2b2f824781d4eebb309ab9296150fd1a0c7eb07d", size = 856710, upload-time = "2026-07-10T19:49:35.289Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e5/dc35cea074dbdcb9776c4b0542a3bc326ff08454af0768ef35f3fc66e7fa/regex-2026.7.10-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:668ab85105361d0200e3545bec198a1acfc6b0aeb5fff8897647a826e5a171be", size = 803621, upload-time = "2026-07-10T19:49:37.704Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b2/124564af46bc0b592785610b3985315610af0a07f4cf21fa36e06c2398dd/regex-2026.7.10-cp314-cp314t-win32.whl", hash = "sha256:dd7715817a187edd7e2a2390908757f7ba42148e59cad755fb8ee1160c628eca", size = 274558, upload-time = "2026-07-10T19:49:39.926Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9c/cd813ce9f3404c0443915175c1e339c5afd8fcda04310102eaf233015eef/regex-2026.7.10-cp314-cp314t-win_amd64.whl", hash = "sha256:78712d4954234df5ca24fdadb65a2ab034213f0cdfde376c272f9fc5e09866bb", size = 283687, upload-time = "2026-07-10T19:49:41.872Z" }, + { url = "https://files.pythonhosted.org/packages/1b/d3/3dae6a6ce46144940e64425e32b8573a393a009aeaf75fa6752a35399056/regex-2026.7.10-cp314-cp314t-win_arm64.whl", hash = "sha256:749b92640e1970e881fdf22a411d74bf9d049b154f4ef7232eeb9a90dd8be7f3", size = 283377, upload-time = "2026-07-10T19:49:43.985Z" }, ] [[package]] name = "requests" -version = "2.32.5" +version = "2.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -2490,9 +4171,9 @@ dependencies = [ { 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" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] [[package]] @@ -2507,6 +4188,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, ] +[[package]] +name = "responses" +version = "0.26.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/1a/4af3e6d659394b809838490b144e4ab8d7ed3b9fecc7ca78f5d2f79b1a3d/responses-0.26.2.tar.gz", hash = "sha256:9c9259b46a8349197edebf43cfa68a87e1a2802ef503ff8b2fecbabc0b45afd8", size = 84030, upload-time = "2026-07-03T16:44:50.325Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/28/693e1d9ebf72baa062ded80d837a035b86ce75eda5a269379e9e2b1008a8/responses-0.26.2-py3-none-any.whl", hash = "sha256:6fdfeabd58e5ec473b98dfe02e6d46d3173bd8dd573eff2ccccf1a05a5135364", size = 35609, upload-time = "2026-07-03T16:44:49.1Z" }, +] + +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, +] + [[package]] name = "rfc3986" version = "2.0.0" @@ -2518,225 +4225,327 @@ wheels = [ [[package]] name = "rich" -version = "14.1.0" +version = "14.3.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/67/cae617f1351490c25a4b8ac3b8b63a4dda609295d8222bad12242dfdc629/rich-14.3.4.tar.gz", hash = "sha256:817e02727f2b25b40ef56f5aa2217f400c8489f79ca8f46ea2b70dd5e14558a9", size = 230524, upload-time = "2026-04-11T02:57:45.419Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, + { url = "https://files.pythonhosted.org/packages/b3/76/6d163cfac87b632216f71879e6b2cf17163f773ff59c00b5ff4900a80fa3/rich-14.3.4-py3-none-any.whl", hash = "sha256:07e7adb4690f68864777b1450859253bed81a99a31ac321ac1817b2313558952", size = 310480, upload-time = "2026-04-11T02:57:47.484Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, ] [[package]] name = "rpds-py" -version = "0.27.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload-time = "2025-08-27T12:16:36.024Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/ed/3aef893e2dd30e77e35d20d4ddb45ca459db59cead748cad9796ad479411/rpds_py-0.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef", size = 371606, upload-time = "2025-08-27T12:12:25.189Z" }, - { url = "https://files.pythonhosted.org/packages/6d/82/9818b443e5d3eb4c83c3994561387f116aae9833b35c484474769c4a8faf/rpds_py-0.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be", size = 353452, upload-time = "2025-08-27T12:12:27.433Z" }, - { url = "https://files.pythonhosted.org/packages/99/c7/d2a110ffaaa397fc6793a83c7bd3545d9ab22658b7cdff05a24a4535cc45/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61", size = 381519, upload-time = "2025-08-27T12:12:28.719Z" }, - { url = "https://files.pythonhosted.org/packages/5a/bc/e89581d1f9d1be7d0247eaef602566869fdc0d084008ba139e27e775366c/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb", size = 394424, upload-time = "2025-08-27T12:12:30.207Z" }, - { url = "https://files.pythonhosted.org/packages/ac/2e/36a6861f797530e74bb6ed53495f8741f1ef95939eed01d761e73d559067/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657", size = 523467, upload-time = "2025-08-27T12:12:31.808Z" }, - { url = "https://files.pythonhosted.org/packages/c4/59/c1bc2be32564fa499f988f0a5c6505c2f4746ef96e58e4d7de5cf923d77e/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013", size = 402660, upload-time = "2025-08-27T12:12:33.444Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ec/ef8bf895f0628dd0a59e54d81caed6891663cb9c54a0f4bb7da918cb88cf/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a", size = 384062, upload-time = "2025-08-27T12:12:34.857Z" }, - { url = "https://files.pythonhosted.org/packages/69/f7/f47ff154be8d9a5e691c083a920bba89cef88d5247c241c10b9898f595a1/rpds_py-0.27.1-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1", size = 401289, upload-time = "2025-08-27T12:12:36.085Z" }, - { url = "https://files.pythonhosted.org/packages/3b/d9/ca410363efd0615814ae579f6829cafb39225cd63e5ea5ed1404cb345293/rpds_py-0.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10", size = 417718, upload-time = "2025-08-27T12:12:37.401Z" }, - { url = "https://files.pythonhosted.org/packages/e3/a0/8cb5c2ff38340f221cc067cc093d1270e10658ba4e8d263df923daa18e86/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808", size = 558333, upload-time = "2025-08-27T12:12:38.672Z" }, - { url = "https://files.pythonhosted.org/packages/6f/8c/1b0de79177c5d5103843774ce12b84caa7164dfc6cd66378768d37db11bf/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8", size = 589127, upload-time = "2025-08-27T12:12:41.48Z" }, - { url = "https://files.pythonhosted.org/packages/c8/5e/26abb098d5e01266b0f3a2488d299d19ccc26849735d9d2b95c39397e945/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9", size = 554899, upload-time = "2025-08-27T12:12:42.925Z" }, - { url = "https://files.pythonhosted.org/packages/de/41/905cc90ced13550db017f8f20c6d8e8470066c5738ba480d7ba63e3d136b/rpds_py-0.27.1-cp310-cp310-win32.whl", hash = "sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4", size = 217450, upload-time = "2025-08-27T12:12:44.813Z" }, - { url = "https://files.pythonhosted.org/packages/75/3d/6bef47b0e253616ccdf67c283e25f2d16e18ccddd38f92af81d5a3420206/rpds_py-0.27.1-cp310-cp310-win_amd64.whl", hash = "sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1", size = 228447, upload-time = "2025-08-27T12:12:46.204Z" }, - { url = "https://files.pythonhosted.org/packages/b5/c1/7907329fbef97cbd49db6f7303893bd1dd5a4a3eae415839ffdfb0762cae/rpds_py-0.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881", size = 371063, upload-time = "2025-08-27T12:12:47.856Z" }, - { url = "https://files.pythonhosted.org/packages/11/94/2aab4bc86228bcf7c48760990273653a4900de89c7537ffe1b0d6097ed39/rpds_py-0.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5", size = 353210, upload-time = "2025-08-27T12:12:49.187Z" }, - { url = "https://files.pythonhosted.org/packages/3a/57/f5eb3ecf434342f4f1a46009530e93fd201a0b5b83379034ebdb1d7c1a58/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e", size = 381636, upload-time = "2025-08-27T12:12:50.492Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f4/ef95c5945e2ceb5119571b184dd5a1cc4b8541bbdf67461998cfeac9cb1e/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c", size = 394341, upload-time = "2025-08-27T12:12:52.024Z" }, - { url = "https://files.pythonhosted.org/packages/5a/7e/4bd610754bf492d398b61725eb9598ddd5eb86b07d7d9483dbcd810e20bc/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195", size = 523428, upload-time = "2025-08-27T12:12:53.779Z" }, - { url = "https://files.pythonhosted.org/packages/9f/e5/059b9f65a8c9149361a8b75094864ab83b94718344db511fd6117936ed2a/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52", size = 402923, upload-time = "2025-08-27T12:12:55.15Z" }, - { url = "https://files.pythonhosted.org/packages/f5/48/64cabb7daced2968dd08e8a1b7988bf358d7bd5bcd5dc89a652f4668543c/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed", size = 384094, upload-time = "2025-08-27T12:12:57.194Z" }, - { url = "https://files.pythonhosted.org/packages/ae/e1/dc9094d6ff566bff87add8a510c89b9e158ad2ecd97ee26e677da29a9e1b/rpds_py-0.27.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a", size = 401093, upload-time = "2025-08-27T12:12:58.985Z" }, - { url = "https://files.pythonhosted.org/packages/37/8e/ac8577e3ecdd5593e283d46907d7011618994e1d7ab992711ae0f78b9937/rpds_py-0.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde", size = 417969, upload-time = "2025-08-27T12:13:00.367Z" }, - { url = "https://files.pythonhosted.org/packages/66/6d/87507430a8f74a93556fe55c6485ba9c259949a853ce407b1e23fea5ba31/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21", size = 558302, upload-time = "2025-08-27T12:13:01.737Z" }, - { url = "https://files.pythonhosted.org/packages/3a/bb/1db4781ce1dda3eecc735e3152659a27b90a02ca62bfeea17aee45cc0fbc/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9", size = 589259, upload-time = "2025-08-27T12:13:03.127Z" }, - { url = "https://files.pythonhosted.org/packages/7b/0e/ae1c8943d11a814d01b482e1f8da903f88047a962dff9bbdadf3bd6e6fd1/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948", size = 554983, upload-time = "2025-08-27T12:13:04.516Z" }, - { url = "https://files.pythonhosted.org/packages/b2/d5/0b2a55415931db4f112bdab072443ff76131b5ac4f4dc98d10d2d357eb03/rpds_py-0.27.1-cp311-cp311-win32.whl", hash = "sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39", size = 217154, upload-time = "2025-08-27T12:13:06.278Z" }, - { url = "https://files.pythonhosted.org/packages/24/75/3b7ffe0d50dc86a6a964af0d1cc3a4a2cdf437cb7b099a4747bbb96d1819/rpds_py-0.27.1-cp311-cp311-win_amd64.whl", hash = "sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15", size = 228627, upload-time = "2025-08-27T12:13:07.625Z" }, - { url = "https://files.pythonhosted.org/packages/8d/3f/4fd04c32abc02c710f09a72a30c9a55ea3cc154ef8099078fd50a0596f8e/rpds_py-0.27.1-cp311-cp311-win_arm64.whl", hash = "sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746", size = 220998, upload-time = "2025-08-27T12:13:08.972Z" }, - { url = "https://files.pythonhosted.org/packages/bd/fe/38de28dee5df58b8198c743fe2bea0c785c6d40941b9950bac4cdb71a014/rpds_py-0.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90", size = 361887, upload-time = "2025-08-27T12:13:10.233Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9a/4b6c7eedc7dd90986bf0fab6ea2a091ec11c01b15f8ba0a14d3f80450468/rpds_py-0.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5", size = 345795, upload-time = "2025-08-27T12:13:11.65Z" }, - { url = "https://files.pythonhosted.org/packages/6f/0e/e650e1b81922847a09cca820237b0edee69416a01268b7754d506ade11ad/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e", size = 385121, upload-time = "2025-08-27T12:13:13.008Z" }, - { url = "https://files.pythonhosted.org/packages/1b/ea/b306067a712988e2bff00dcc7c8f31d26c29b6d5931b461aa4b60a013e33/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881", size = 398976, upload-time = "2025-08-27T12:13:14.368Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0a/26dc43c8840cb8fe239fe12dbc8d8de40f2365e838f3d395835dde72f0e5/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec", size = 525953, upload-time = "2025-08-27T12:13:15.774Z" }, - { url = "https://files.pythonhosted.org/packages/22/14/c85e8127b573aaf3a0cbd7fbb8c9c99e735a4a02180c84da2a463b766e9e/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb", size = 407915, upload-time = "2025-08-27T12:13:17.379Z" }, - { url = "https://files.pythonhosted.org/packages/ed/7b/8f4fee9ba1fb5ec856eb22d725a4efa3deb47f769597c809e03578b0f9d9/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5", size = 386883, upload-time = "2025-08-27T12:13:18.704Z" }, - { url = "https://files.pythonhosted.org/packages/86/47/28fa6d60f8b74fcdceba81b272f8d9836ac0340570f68f5df6b41838547b/rpds_py-0.27.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a", size = 405699, upload-time = "2025-08-27T12:13:20.089Z" }, - { url = "https://files.pythonhosted.org/packages/d0/fd/c5987b5e054548df56953a21fe2ebed51fc1ec7c8f24fd41c067b68c4a0a/rpds_py-0.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444", size = 423713, upload-time = "2025-08-27T12:13:21.436Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ba/3c4978b54a73ed19a7d74531be37a8bcc542d917c770e14d372b8daea186/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a", size = 562324, upload-time = "2025-08-27T12:13:22.789Z" }, - { url = "https://files.pythonhosted.org/packages/b5/6c/6943a91768fec16db09a42b08644b960cff540c66aab89b74be6d4a144ba/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1", size = 593646, upload-time = "2025-08-27T12:13:24.122Z" }, - { url = "https://files.pythonhosted.org/packages/11/73/9d7a8f4be5f4396f011a6bb7a19fe26303a0dac9064462f5651ced2f572f/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998", size = 558137, upload-time = "2025-08-27T12:13:25.557Z" }, - { url = "https://files.pythonhosted.org/packages/6e/96/6772cbfa0e2485bcceef8071de7821f81aeac8bb45fbfd5542a3e8108165/rpds_py-0.27.1-cp312-cp312-win32.whl", hash = "sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39", size = 221343, upload-time = "2025-08-27T12:13:26.967Z" }, - { url = "https://files.pythonhosted.org/packages/67/b6/c82f0faa9af1c6a64669f73a17ee0eeef25aff30bb9a1c318509efe45d84/rpds_py-0.27.1-cp312-cp312-win_amd64.whl", hash = "sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594", size = 232497, upload-time = "2025-08-27T12:13:28.326Z" }, - { url = "https://files.pythonhosted.org/packages/e1/96/2817b44bd2ed11aebacc9251da03689d56109b9aba5e311297b6902136e2/rpds_py-0.27.1-cp312-cp312-win_arm64.whl", hash = "sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502", size = 222790, upload-time = "2025-08-27T12:13:29.71Z" }, - { url = "https://files.pythonhosted.org/packages/cc/77/610aeee8d41e39080c7e14afa5387138e3c9fa9756ab893d09d99e7d8e98/rpds_py-0.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b", size = 361741, upload-time = "2025-08-27T12:13:31.039Z" }, - { url = "https://files.pythonhosted.org/packages/3a/fc/c43765f201c6a1c60be2043cbdb664013def52460a4c7adace89d6682bf4/rpds_py-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf", size = 345574, upload-time = "2025-08-27T12:13:32.902Z" }, - { url = "https://files.pythonhosted.org/packages/20/42/ee2b2ca114294cd9847d0ef9c26d2b0851b2e7e00bf14cc4c0b581df0fc3/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83", size = 385051, upload-time = "2025-08-27T12:13:34.228Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e8/1e430fe311e4799e02e2d1af7c765f024e95e17d651612425b226705f910/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf", size = 398395, upload-time = "2025-08-27T12:13:36.132Z" }, - { url = "https://files.pythonhosted.org/packages/82/95/9dc227d441ff2670651c27a739acb2535ccaf8b351a88d78c088965e5996/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2", size = 524334, upload-time = "2025-08-27T12:13:37.562Z" }, - { url = "https://files.pythonhosted.org/packages/87/01/a670c232f401d9ad461d9a332aa4080cd3cb1d1df18213dbd0d2a6a7ab51/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0", size = 407691, upload-time = "2025-08-27T12:13:38.94Z" }, - { url = "https://files.pythonhosted.org/packages/03/36/0a14aebbaa26fe7fab4780c76f2239e76cc95a0090bdb25e31d95c492fcd/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418", size = 386868, upload-time = "2025-08-27T12:13:40.192Z" }, - { url = "https://files.pythonhosted.org/packages/3b/03/8c897fb8b5347ff6c1cc31239b9611c5bf79d78c984430887a353e1409a1/rpds_py-0.27.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d", size = 405469, upload-time = "2025-08-27T12:13:41.496Z" }, - { url = "https://files.pythonhosted.org/packages/da/07/88c60edc2df74850d496d78a1fdcdc7b54360a7f610a4d50008309d41b94/rpds_py-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274", size = 422125, upload-time = "2025-08-27T12:13:42.802Z" }, - { url = "https://files.pythonhosted.org/packages/6b/86/5f4c707603e41b05f191a749984f390dabcbc467cf833769b47bf14ba04f/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd", size = 562341, upload-time = "2025-08-27T12:13:44.472Z" }, - { url = "https://files.pythonhosted.org/packages/b2/92/3c0cb2492094e3cd9baf9e49bbb7befeceb584ea0c1a8b5939dca4da12e5/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2", size = 592511, upload-time = "2025-08-27T12:13:45.898Z" }, - { url = "https://files.pythonhosted.org/packages/10/bb/82e64fbb0047c46a168faa28d0d45a7851cd0582f850b966811d30f67ad8/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002", size = 557736, upload-time = "2025-08-27T12:13:47.408Z" }, - { url = "https://files.pythonhosted.org/packages/00/95/3c863973d409210da7fb41958172c6b7dbe7fc34e04d3cc1f10bb85e979f/rpds_py-0.27.1-cp313-cp313-win32.whl", hash = "sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3", size = 221462, upload-time = "2025-08-27T12:13:48.742Z" }, - { url = "https://files.pythonhosted.org/packages/ce/2c/5867b14a81dc217b56d95a9f2a40fdbc56a1ab0181b80132beeecbd4b2d6/rpds_py-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83", size = 232034, upload-time = "2025-08-27T12:13:50.11Z" }, - { url = "https://files.pythonhosted.org/packages/c7/78/3958f3f018c01923823f1e47f1cc338e398814b92d83cd278364446fac66/rpds_py-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d", size = 222392, upload-time = "2025-08-27T12:13:52.587Z" }, - { url = "https://files.pythonhosted.org/packages/01/76/1cdf1f91aed5c3a7bf2eba1f1c4e4d6f57832d73003919a20118870ea659/rpds_py-0.27.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228", size = 358355, upload-time = "2025-08-27T12:13:54.012Z" }, - { url = "https://files.pythonhosted.org/packages/c3/6f/bf142541229374287604caf3bb2a4ae17f0a580798fd72d3b009b532db4e/rpds_py-0.27.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92", size = 342138, upload-time = "2025-08-27T12:13:55.791Z" }, - { url = "https://files.pythonhosted.org/packages/1a/77/355b1c041d6be40886c44ff5e798b4e2769e497b790f0f7fd1e78d17e9a8/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2", size = 380247, upload-time = "2025-08-27T12:13:57.683Z" }, - { url = "https://files.pythonhosted.org/packages/d6/a4/d9cef5c3946ea271ce2243c51481971cd6e34f21925af2783dd17b26e815/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723", size = 390699, upload-time = "2025-08-27T12:13:59.137Z" }, - { url = "https://files.pythonhosted.org/packages/3a/06/005106a7b8c6c1a7e91b73169e49870f4af5256119d34a361ae5240a0c1d/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802", size = 521852, upload-time = "2025-08-27T12:14:00.583Z" }, - { url = "https://files.pythonhosted.org/packages/e5/3e/50fb1dac0948e17a02eb05c24510a8fe12d5ce8561c6b7b7d1339ab7ab9c/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f", size = 402582, upload-time = "2025-08-27T12:14:02.034Z" }, - { url = "https://files.pythonhosted.org/packages/cb/b0/f4e224090dc5b0ec15f31a02d746ab24101dd430847c4d99123798661bfc/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2", size = 384126, upload-time = "2025-08-27T12:14:03.437Z" }, - { url = "https://files.pythonhosted.org/packages/54/77/ac339d5f82b6afff1df8f0fe0d2145cc827992cb5f8eeb90fc9f31ef7a63/rpds_py-0.27.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21", size = 399486, upload-time = "2025-08-27T12:14:05.443Z" }, - { url = "https://files.pythonhosted.org/packages/d6/29/3e1c255eee6ac358c056a57d6d6869baa00a62fa32eea5ee0632039c50a3/rpds_py-0.27.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef", size = 414832, upload-time = "2025-08-27T12:14:06.902Z" }, - { url = "https://files.pythonhosted.org/packages/3f/db/6d498b844342deb3fa1d030598db93937a9964fcf5cb4da4feb5f17be34b/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081", size = 557249, upload-time = "2025-08-27T12:14:08.37Z" }, - { url = "https://files.pythonhosted.org/packages/60/f3/690dd38e2310b6f68858a331399b4d6dbb9132c3e8ef8b4333b96caf403d/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd", size = 587356, upload-time = "2025-08-27T12:14:10.034Z" }, - { url = "https://files.pythonhosted.org/packages/86/e3/84507781cccd0145f35b1dc32c72675200c5ce8d5b30f813e49424ef68fc/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7", size = 555300, upload-time = "2025-08-27T12:14:11.783Z" }, - { url = "https://files.pythonhosted.org/packages/e5/ee/375469849e6b429b3516206b4580a79e9ef3eb12920ddbd4492b56eaacbe/rpds_py-0.27.1-cp313-cp313t-win32.whl", hash = "sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688", size = 216714, upload-time = "2025-08-27T12:14:13.629Z" }, - { url = "https://files.pythonhosted.org/packages/21/87/3fc94e47c9bd0742660e84706c311a860dcae4374cf4a03c477e23ce605a/rpds_py-0.27.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797", size = 228943, upload-time = "2025-08-27T12:14:14.937Z" }, - { url = "https://files.pythonhosted.org/packages/70/36/b6e6066520a07cf029d385de869729a895917b411e777ab1cde878100a1d/rpds_py-0.27.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334", size = 362472, upload-time = "2025-08-27T12:14:16.333Z" }, - { url = "https://files.pythonhosted.org/packages/af/07/b4646032e0dcec0df9c73a3bd52f63bc6c5f9cda992f06bd0e73fe3fbebd/rpds_py-0.27.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33", size = 345676, upload-time = "2025-08-27T12:14:17.764Z" }, - { url = "https://files.pythonhosted.org/packages/b0/16/2f1003ee5d0af4bcb13c0cf894957984c32a6751ed7206db2aee7379a55e/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a", size = 385313, upload-time = "2025-08-27T12:14:19.829Z" }, - { url = "https://files.pythonhosted.org/packages/05/cd/7eb6dd7b232e7f2654d03fa07f1414d7dfc980e82ba71e40a7c46fd95484/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b", size = 399080, upload-time = "2025-08-27T12:14:21.531Z" }, - { url = "https://files.pythonhosted.org/packages/20/51/5829afd5000ec1cb60f304711f02572d619040aa3ec033d8226817d1e571/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7", size = 523868, upload-time = "2025-08-27T12:14:23.485Z" }, - { url = "https://files.pythonhosted.org/packages/05/2c/30eebca20d5db95720ab4d2faec1b5e4c1025c473f703738c371241476a2/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136", size = 408750, upload-time = "2025-08-27T12:14:24.924Z" }, - { url = "https://files.pythonhosted.org/packages/90/1a/cdb5083f043597c4d4276eae4e4c70c55ab5accec078da8611f24575a367/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff", size = 387688, upload-time = "2025-08-27T12:14:27.537Z" }, - { url = "https://files.pythonhosted.org/packages/7c/92/cf786a15320e173f945d205ab31585cc43969743bb1a48b6888f7a2b0a2d/rpds_py-0.27.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9", size = 407225, upload-time = "2025-08-27T12:14:28.981Z" }, - { url = "https://files.pythonhosted.org/packages/33/5c/85ee16df5b65063ef26017bef33096557a4c83fbe56218ac7cd8c235f16d/rpds_py-0.27.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60", size = 423361, upload-time = "2025-08-27T12:14:30.469Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8e/1c2741307fcabd1a334ecf008e92c4f47bb6f848712cf15c923becfe82bb/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e", size = 562493, upload-time = "2025-08-27T12:14:31.987Z" }, - { url = "https://files.pythonhosted.org/packages/04/03/5159321baae9b2222442a70c1f988cbbd66b9be0675dd3936461269be360/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212", size = 592623, upload-time = "2025-08-27T12:14:33.543Z" }, - { url = "https://files.pythonhosted.org/packages/ff/39/c09fd1ad28b85bc1d4554a8710233c9f4cefd03d7717a1b8fbfd171d1167/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675", size = 558800, upload-time = "2025-08-27T12:14:35.436Z" }, - { url = "https://files.pythonhosted.org/packages/c5/d6/99228e6bbcf4baa764b18258f519a9035131d91b538d4e0e294313462a98/rpds_py-0.27.1-cp314-cp314-win32.whl", hash = "sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3", size = 221943, upload-time = "2025-08-27T12:14:36.898Z" }, - { url = "https://files.pythonhosted.org/packages/be/07/c802bc6b8e95be83b79bdf23d1aa61d68324cb1006e245d6c58e959e314d/rpds_py-0.27.1-cp314-cp314-win_amd64.whl", hash = "sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456", size = 233739, upload-time = "2025-08-27T12:14:38.386Z" }, - { url = "https://files.pythonhosted.org/packages/c8/89/3e1b1c16d4c2d547c5717377a8df99aee8099ff050f87c45cb4d5fa70891/rpds_py-0.27.1-cp314-cp314-win_arm64.whl", hash = "sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3", size = 223120, upload-time = "2025-08-27T12:14:39.82Z" }, - { url = "https://files.pythonhosted.org/packages/62/7e/dc7931dc2fa4a6e46b2a4fa744a9fe5c548efd70e0ba74f40b39fa4a8c10/rpds_py-0.27.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2", size = 358944, upload-time = "2025-08-27T12:14:41.199Z" }, - { url = "https://files.pythonhosted.org/packages/e6/22/4af76ac4e9f336bfb1a5f240d18a33c6b2fcaadb7472ac7680576512b49a/rpds_py-0.27.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4", size = 342283, upload-time = "2025-08-27T12:14:42.699Z" }, - { url = "https://files.pythonhosted.org/packages/1c/15/2a7c619b3c2272ea9feb9ade67a45c40b3eeb500d503ad4c28c395dc51b4/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e", size = 380320, upload-time = "2025-08-27T12:14:44.157Z" }, - { url = "https://files.pythonhosted.org/packages/a2/7d/4c6d243ba4a3057e994bb5bedd01b5c963c12fe38dde707a52acdb3849e7/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817", size = 391760, upload-time = "2025-08-27T12:14:45.845Z" }, - { url = "https://files.pythonhosted.org/packages/b4/71/b19401a909b83bcd67f90221330bc1ef11bc486fe4e04c24388d28a618ae/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec", size = 522476, upload-time = "2025-08-27T12:14:47.364Z" }, - { url = "https://files.pythonhosted.org/packages/e4/44/1a3b9715c0455d2e2f0f6df5ee6d6f5afdc423d0773a8a682ed2b43c566c/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a", size = 403418, upload-time = "2025-08-27T12:14:49.991Z" }, - { url = "https://files.pythonhosted.org/packages/1c/4b/fb6c4f14984eb56673bc868a66536f53417ddb13ed44b391998100a06a96/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8", size = 384771, upload-time = "2025-08-27T12:14:52.159Z" }, - { url = "https://files.pythonhosted.org/packages/c0/56/d5265d2d28b7420d7b4d4d85cad8ef891760f5135102e60d5c970b976e41/rpds_py-0.27.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48", size = 400022, upload-time = "2025-08-27T12:14:53.859Z" }, - { url = "https://files.pythonhosted.org/packages/8f/e9/9f5fc70164a569bdd6ed9046486c3568d6926e3a49bdefeeccfb18655875/rpds_py-0.27.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb", size = 416787, upload-time = "2025-08-27T12:14:55.673Z" }, - { url = "https://files.pythonhosted.org/packages/d4/64/56dd03430ba491db943a81dcdef115a985aac5f44f565cd39a00c766d45c/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734", size = 557538, upload-time = "2025-08-27T12:14:57.245Z" }, - { url = "https://files.pythonhosted.org/packages/3f/36/92cc885a3129993b1d963a2a42ecf64e6a8e129d2c7cc980dbeba84e55fb/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb", size = 588512, upload-time = "2025-08-27T12:14:58.728Z" }, - { url = "https://files.pythonhosted.org/packages/dd/10/6b283707780a81919f71625351182b4f98932ac89a09023cb61865136244/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0", size = 555813, upload-time = "2025-08-27T12:15:00.334Z" }, - { url = "https://files.pythonhosted.org/packages/04/2e/30b5ea18c01379da6272a92825dd7e53dc9d15c88a19e97932d35d430ef7/rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a", size = 217385, upload-time = "2025-08-27T12:15:01.937Z" }, - { url = "https://files.pythonhosted.org/packages/32/7d/97119da51cb1dd3f2f3c0805f155a3aa4a95fa44fe7d78ae15e69edf4f34/rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772", size = 230097, upload-time = "2025-08-27T12:15:03.961Z" }, - { url = "https://files.pythonhosted.org/packages/7f/6c/252e83e1ce7583c81f26d1d884b2074d40a13977e1b6c9c50bbf9a7f1f5a/rpds_py-0.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c918c65ec2e42c2a78d19f18c553d77319119bf43aa9e2edf7fb78d624355527", size = 372140, upload-time = "2025-08-27T12:15:05.441Z" }, - { url = "https://files.pythonhosted.org/packages/9d/71/949c195d927c5aeb0d0629d329a20de43a64c423a6aa53836290609ef7ec/rpds_py-0.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1fea2b1a922c47c51fd07d656324531adc787e415c8b116530a1d29c0516c62d", size = 354086, upload-time = "2025-08-27T12:15:07.404Z" }, - { url = "https://files.pythonhosted.org/packages/9f/02/e43e332ad8ce4f6c4342d151a471a7f2900ed1d76901da62eb3762663a71/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbf94c58e8e0cd6b6f38d8de67acae41b3a515c26169366ab58bdca4a6883bb8", size = 382117, upload-time = "2025-08-27T12:15:09.275Z" }, - { url = "https://files.pythonhosted.org/packages/d0/05/b0fdeb5b577197ad72812bbdfb72f9a08fa1e64539cc3940b1b781cd3596/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c2a8fed130ce946d5c585eddc7c8eeef0051f58ac80a8ee43bd17835c144c2cc", size = 394520, upload-time = "2025-08-27T12:15:10.727Z" }, - { url = "https://files.pythonhosted.org/packages/67/1f/4cfef98b2349a7585181e99294fa2a13f0af06902048a5d70f431a66d0b9/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:037a2361db72ee98d829bc2c5b7cc55598ae0a5e0ec1823a56ea99374cfd73c1", size = 522657, upload-time = "2025-08-27T12:15:12.613Z" }, - { url = "https://files.pythonhosted.org/packages/44/55/ccf37ddc4c6dce7437b335088b5ca18da864b334890e2fe9aa6ddc3f79a9/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5281ed1cc1d49882f9997981c88df1a22e140ab41df19071222f7e5fc4e72125", size = 402967, upload-time = "2025-08-27T12:15:14.113Z" }, - { url = "https://files.pythonhosted.org/packages/74/e5/5903f92e41e293b07707d5bf00ef39a0eb2af7190aff4beaf581a6591510/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd50659a069c15eef8aa3d64bbef0d69fd27bb4a50c9ab4f17f83a16cbf8905", size = 384372, upload-time = "2025-08-27T12:15:15.842Z" }, - { url = "https://files.pythonhosted.org/packages/8f/e3/fbb409e18aeefc01e49f5922ac63d2d914328430e295c12183ce56ebf76b/rpds_py-0.27.1-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:c4b676c4ae3921649a15d28ed10025548e9b561ded473aa413af749503c6737e", size = 401264, upload-time = "2025-08-27T12:15:17.388Z" }, - { url = "https://files.pythonhosted.org/packages/55/79/529ad07794e05cb0f38e2f965fc5bb20853d523976719400acecc447ec9d/rpds_py-0.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:079bc583a26db831a985c5257797b2b5d3affb0386e7ff886256762f82113b5e", size = 418691, upload-time = "2025-08-27T12:15:19.144Z" }, - { url = "https://files.pythonhosted.org/packages/33/39/6554a7fd6d9906fda2521c6d52f5d723dca123529fb719a5b5e074c15e01/rpds_py-0.27.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4e44099bd522cba71a2c6b97f68e19f40e7d85399de899d66cdb67b32d7cb786", size = 558989, upload-time = "2025-08-27T12:15:21.087Z" }, - { url = "https://files.pythonhosted.org/packages/19/b2/76fa15173b6f9f445e5ef15120871b945fb8dd9044b6b8c7abe87e938416/rpds_py-0.27.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e202e6d4188e53c6661af813b46c37ca2c45e497fc558bacc1a7630ec2695aec", size = 589835, upload-time = "2025-08-27T12:15:22.696Z" }, - { url = "https://files.pythonhosted.org/packages/ee/9e/5560a4b39bab780405bed8a88ee85b30178061d189558a86003548dea045/rpds_py-0.27.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f41f814b8eaa48768d1bb551591f6ba45f87ac76899453e8ccd41dba1289b04b", size = 555227, upload-time = "2025-08-27T12:15:24.278Z" }, - { url = "https://files.pythonhosted.org/packages/52/d7/cd9c36215111aa65724c132bf709c6f35175973e90b32115dedc4ced09cb/rpds_py-0.27.1-cp39-cp39-win32.whl", hash = "sha256:9e71f5a087ead99563c11fdaceee83ee982fd39cf67601f4fd66cb386336ee52", size = 217899, upload-time = "2025-08-27T12:15:25.926Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e0/d75ab7b4dd8ba777f6b365adbdfc7614bbfe7c5f05703031dfa4b61c3d6c/rpds_py-0.27.1-cp39-cp39-win_amd64.whl", hash = "sha256:71108900c9c3c8590697244b9519017a400d9ba26a36c48381b3f64743a44aab", size = 228725, upload-time = "2025-08-27T12:15:27.398Z" }, - { url = "https://files.pythonhosted.org/packages/d5/63/b7cc415c345625d5e62f694ea356c58fb964861409008118f1245f8c3347/rpds_py-0.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf", size = 371360, upload-time = "2025-08-27T12:15:29.218Z" }, - { url = "https://files.pythonhosted.org/packages/e5/8c/12e1b24b560cf378b8ffbdb9dc73abd529e1adcfcf82727dfd29c4a7b88d/rpds_py-0.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3", size = 353933, upload-time = "2025-08-27T12:15:30.837Z" }, - { url = "https://files.pythonhosted.org/packages/9b/85/1bb2210c1f7a1b99e91fea486b9f0f894aa5da3a5ec7097cbad7dec6d40f/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636", size = 382962, upload-time = "2025-08-27T12:15:32.348Z" }, - { url = "https://files.pythonhosted.org/packages/cc/c9/a839b9f219cf80ed65f27a7f5ddbb2809c1b85c966020ae2dff490e0b18e/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8", size = 394412, upload-time = "2025-08-27T12:15:33.839Z" }, - { url = "https://files.pythonhosted.org/packages/02/2d/b1d7f928b0b1f4fc2e0133e8051d199b01d7384875adc63b6ddadf3de7e5/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc", size = 523972, upload-time = "2025-08-27T12:15:35.377Z" }, - { url = "https://files.pythonhosted.org/packages/a9/af/2cbf56edd2d07716df1aec8a726b3159deb47cb5c27e1e42b71d705a7c2f/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8", size = 403273, upload-time = "2025-08-27T12:15:37.051Z" }, - { url = "https://files.pythonhosted.org/packages/c0/93/425e32200158d44ff01da5d9612c3b6711fe69f606f06e3895511f17473b/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc", size = 385278, upload-time = "2025-08-27T12:15:38.571Z" }, - { url = "https://files.pythonhosted.org/packages/eb/1a/1a04a915ecd0551bfa9e77b7672d1937b4b72a0fc204a17deef76001cfb2/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71", size = 402084, upload-time = "2025-08-27T12:15:40.529Z" }, - { url = "https://files.pythonhosted.org/packages/51/f7/66585c0fe5714368b62951d2513b684e5215beaceab2c6629549ddb15036/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad", size = 419041, upload-time = "2025-08-27T12:15:42.191Z" }, - { url = "https://files.pythonhosted.org/packages/8e/7e/83a508f6b8e219bba2d4af077c35ba0e0cdd35a751a3be6a7cba5a55ad71/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab", size = 560084, upload-time = "2025-08-27T12:15:43.839Z" }, - { url = "https://files.pythonhosted.org/packages/66/66/bb945683b958a1b19eb0fe715594630d0f36396ebdef4d9b89c2fa09aa56/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059", size = 590115, upload-time = "2025-08-27T12:15:46.647Z" }, - { url = "https://files.pythonhosted.org/packages/12/00/ccfaafaf7db7e7adace915e5c2f2c2410e16402561801e9c7f96683002d3/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b", size = 556561, upload-time = "2025-08-27T12:15:48.219Z" }, - { url = "https://files.pythonhosted.org/packages/e1/b7/92b6ed9aad103bfe1c45df98453dfae40969eef2cb6c6239c58d7e96f1b3/rpds_py-0.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819", size = 229125, upload-time = "2025-08-27T12:15:49.956Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ed/e1fba02de17f4f76318b834425257c8ea297e415e12c68b4361f63e8ae92/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df", size = 371402, upload-time = "2025-08-27T12:15:51.561Z" }, - { url = "https://files.pythonhosted.org/packages/af/7c/e16b959b316048b55585a697e94add55a4ae0d984434d279ea83442e460d/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3", size = 354084, upload-time = "2025-08-27T12:15:53.219Z" }, - { url = "https://files.pythonhosted.org/packages/de/c1/ade645f55de76799fdd08682d51ae6724cb46f318573f18be49b1e040428/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9", size = 383090, upload-time = "2025-08-27T12:15:55.158Z" }, - { url = "https://files.pythonhosted.org/packages/1f/27/89070ca9b856e52960da1472efcb6c20ba27cfe902f4f23ed095b9cfc61d/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc", size = 394519, upload-time = "2025-08-27T12:15:57.238Z" }, - { url = "https://files.pythonhosted.org/packages/b3/28/be120586874ef906aa5aeeae95ae8df4184bc757e5b6bd1c729ccff45ed5/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4", size = 523817, upload-time = "2025-08-27T12:15:59.237Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ef/70cc197bc11cfcde02a86f36ac1eed15c56667c2ebddbdb76a47e90306da/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66", size = 403240, upload-time = "2025-08-27T12:16:00.923Z" }, - { url = "https://files.pythonhosted.org/packages/cf/35/46936cca449f7f518f2f4996e0e8344db4b57e2081e752441154089d2a5f/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e", size = 385194, upload-time = "2025-08-27T12:16:02.802Z" }, - { url = "https://files.pythonhosted.org/packages/e1/62/29c0d3e5125c3270b51415af7cbff1ec587379c84f55a5761cc9efa8cd06/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c", size = 402086, upload-time = "2025-08-27T12:16:04.806Z" }, - { url = "https://files.pythonhosted.org/packages/8f/66/03e1087679227785474466fdd04157fb793b3b76e3fcf01cbf4c693c1949/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf", size = 419272, upload-time = "2025-08-27T12:16:06.471Z" }, - { url = "https://files.pythonhosted.org/packages/6a/24/e3e72d265121e00b063aef3e3501e5b2473cf1b23511d56e529531acf01e/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf", size = 560003, upload-time = "2025-08-27T12:16:08.06Z" }, - { url = "https://files.pythonhosted.org/packages/26/ca/f5a344c534214cc2d41118c0699fffbdc2c1bc7046f2a2b9609765ab9c92/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6", size = 590482, upload-time = "2025-08-27T12:16:10.137Z" }, - { url = "https://files.pythonhosted.org/packages/ce/08/4349bdd5c64d9d193c360aa9db89adeee6f6682ab8825dca0a3f535f434f/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a", size = 556523, upload-time = "2025-08-27T12:16:12.188Z" }, - { url = "https://files.pythonhosted.org/packages/4e/ea/5463cd5048a7a2fcdae308b6e96432802132c141bfb9420260142632a0f1/rpds_py-0.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:aa8933159edc50be265ed22b401125c9eebff3171f570258854dbce3ecd55475", size = 371778, upload-time = "2025-08-27T12:16:13.851Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c8/f38c099db07f5114029c1467649d308543906933eebbc226d4527a5f4693/rpds_py-0.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a50431bf02583e21bf273c71b89d710e7a710ad5e39c725b14e685610555926f", size = 354394, upload-time = "2025-08-27T12:16:15.609Z" }, - { url = "https://files.pythonhosted.org/packages/7d/79/b76f97704d9dd8ddbd76fed4c4048153a847c5d6003afe20a6b5c3339065/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78af06ddc7fe5cc0e967085a9115accee665fb912c22a3f54bad70cc65b05fe6", size = 382348, upload-time = "2025-08-27T12:16:17.251Z" }, - { url = "https://files.pythonhosted.org/packages/8a/3f/ef23d3c1be1b837b648a3016d5bbe7cfe711422ad110b4081c0a90ef5a53/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:70d0738ef8fee13c003b100c2fbd667ec4f133468109b3472d249231108283a3", size = 394159, upload-time = "2025-08-27T12:16:19.251Z" }, - { url = "https://files.pythonhosted.org/packages/74/8a/9e62693af1a34fd28b1a190d463d12407bd7cf561748cb4745845d9548d3/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2f6fd8a1cea5bbe599b6e78a6e5ee08db434fc8ffea51ff201c8765679698b3", size = 522775, upload-time = "2025-08-27T12:16:20.929Z" }, - { url = "https://files.pythonhosted.org/packages/36/0d/8d5bb122bf7a60976b54c5c99a739a3819f49f02d69df3ea2ca2aff47d5c/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8177002868d1426305bb5de1e138161c2ec9eb2d939be38291d7c431c4712df8", size = 402633, upload-time = "2025-08-27T12:16:22.548Z" }, - { url = "https://files.pythonhosted.org/packages/0f/0e/237948c1f425e23e0cf5a566d702652a6e55c6f8fbd332a1792eb7043daf/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:008b839781d6c9bf3b6a8984d1d8e56f0ec46dc56df61fd669c49b58ae800400", size = 384867, upload-time = "2025-08-27T12:16:24.29Z" }, - { url = "https://files.pythonhosted.org/packages/d6/0a/da0813efcd998d260cbe876d97f55b0f469ada8ba9cbc47490a132554540/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:a55b9132bb1ade6c734ddd2759c8dc132aa63687d259e725221f106b83a0e485", size = 401791, upload-time = "2025-08-27T12:16:25.954Z" }, - { url = "https://files.pythonhosted.org/packages/51/78/c6c9e8a8aaca416a6f0d1b6b4a6ee35b88fe2c5401d02235d0a056eceed2/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a46fdec0083a26415f11d5f236b79fa1291c32aaa4a17684d82f7017a1f818b1", size = 419525, upload-time = "2025-08-27T12:16:27.659Z" }, - { url = "https://files.pythonhosted.org/packages/a3/69/5af37e1d71487cf6d56dd1420dc7e0c2732c1b6ff612aa7a88374061c0a8/rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:8a63b640a7845f2bdd232eb0d0a4a2dd939bcdd6c57e6bb134526487f3160ec5", size = 559255, upload-time = "2025-08-27T12:16:29.343Z" }, - { url = "https://files.pythonhosted.org/packages/40/7f/8b7b136069ef7ac3960eda25d832639bdb163018a34c960ed042dd1707c8/rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:7e32721e5d4922deaaf963469d795d5bde6093207c52fec719bd22e5d1bedbc4", size = 590384, upload-time = "2025-08-27T12:16:31.005Z" }, - { url = "https://files.pythonhosted.org/packages/d8/06/c316d3f6ff03f43ccb0eba7de61376f8ec4ea850067dddfafe98274ae13c/rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:2c426b99a068601b5f4623573df7a7c3d72e87533a2dd2253353a03e7502566c", size = 555959, upload-time = "2025-08-27T12:16:32.73Z" }, - { url = "https://files.pythonhosted.org/packages/60/94/384cf54c430b9dac742bbd2ec26c23feb78ded0d43d6d78563a281aec017/rpds_py-0.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4fc9b7fe29478824361ead6e14e4f5aed570d477e06088826537e202d25fe859", size = 228784, upload-time = "2025-08-27T12:16:34.428Z" }, +version = "2026.6.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version >= '3.11' and python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/1f/a2dca5ffdbf1d475ffc4e80e4d5d720ff3a00f691795910116960ee12511/rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7", size = 342174, upload-time = "2026-06-30T07:14:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/4d/dc/323d08583c0832911768663d1944f0107fcd4088704858d84b5e06d105a0/rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911", size = 345513, upload-time = "2026-06-30T07:14:56.515Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2a/e31989834d18d2f26ec1d2774c5b1eb3331df4ea8ada525175294c94b48a/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4", size = 373783, upload-time = "2026-06-30T07:14:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/fe/e80107ee3639585c9941c17d6a42cd65325022f656c023191fce78c324c8/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261", size = 378316, upload-time = "2026-06-30T07:14:59.077Z" }, + { url = "https://files.pythonhosted.org/packages/22/6f/81e3adf81acfb6fa694de2a6e4e7d8863121e3e0799e0a7725e6cf5679c4/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278", size = 499423, upload-time = "2026-06-30T07:15:00.488Z" }, + { url = "https://files.pythonhosted.org/packages/2d/9a/41263969df0ce3d9af2a96d5005a288200af1989aed3354bfceb5fc0b21f/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9", size = 386077, upload-time = "2026-06-30T07:15:01.911Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/7e98f468bd50346faff5b10e5297374b443bfdddacc8e9fbc65984539597/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7", size = 371315, upload-time = "2026-06-30T07:15:03.317Z" }, + { url = "https://files.pythonhosted.org/packages/99/3c/2b973b4d371906a134b03decfea7f5d9835a2c6d263454392e15b64b5b18/rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3", size = 383502, upload-time = "2026-06-30T07:15:04.627Z" }, + { url = "https://files.pythonhosted.org/packages/98/2a/12e2799500af0a307bca76b63361c51f9fe479223561489c29eea1f2ee41/rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da", size = 402673, upload-time = "2026-06-30T07:15:05.856Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e3/21e5872d165fe08be4f229e3d5ee9d90019c0bf0e5538de60dbd54009450/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4", size = 549964, upload-time = "2026-06-30T07:15:07.159Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d0/5ee0fe36844297de8123bee27bc12078c1a7416ad9f1b8a8ca18d6b0c0ac/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6", size = 615446, upload-time = "2026-06-30T07:15:08.531Z" }, + { url = "https://files.pythonhosted.org/packages/b1/80/1ea5873cb683f2fbe5f21b23ea1f6d179ead19f3c5b249b7eb5dca568ef2/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93", size = 576975, upload-time = "2026-06-30T07:15:09.97Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e1/90ef639217a5ddb15b7f4f61b1c33911fd044ad03c311bafdd2bcab85582/rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a", size = 204453, upload-time = "2026-06-30T07:15:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b7/b7a1695d7af36f521fb11e80d6d3adbd744f73b921859bd3c2a2c0dc706f/rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127", size = 223219, upload-time = "2026-06-30T07:15:12.476Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a2/145afacf796e4506062825941176ad9445c2dcf2b3b6a1f13d3030a15e19/rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804", size = 219137, upload-time = "2026-06-30T07:15:13.631Z" }, + { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" }, + { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" }, + { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" }, + { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" }, + { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" }, + { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" }, + { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" }, + { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" }, + { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" }, + { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" }, + { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" }, + { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" }, + { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" }, + { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" }, + { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" }, + { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" }, + { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" }, + { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" }, + { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" }, + { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" }, + { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" }, + { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" }, + { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" }, + { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" }, + { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" }, + { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" }, + { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" }, + { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" }, + { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" }, + { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" }, + { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" }, + { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" }, + { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" }, + { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" }, + { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" }, + { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9c/f0d19ac587fd0e4ab6b72cda355e9c5a6166b01ef7e064e437aef8eb9fef/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f", size = 349791, upload-time = "2026-06-30T07:17:33.315Z" }, + { url = "https://files.pythonhosted.org/packages/38/c7/1d49d204c9fd2ee6c537601dc4c1ba921e03363ca576bfab94a00254ac9a/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171", size = 352842, upload-time = "2026-06-30T07:17:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e5/c0b5dc93cd0d4c06ce1f438907649514e2ea077bcd911e3154a51e96c38e/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90", size = 382094, upload-time = "2026-06-30T07:17:36.514Z" }, + { url = "https://files.pythonhosted.org/packages/0d/54/ec0e907b4ca8d541112db352409bd15f871c9b243e0c92c9b5a46ae96f01/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca", size = 388662, upload-time = "2026-06-30T07:17:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f4/921c22a4fd0f1c1ac13a3996ffbf0aa67951e2c8ad0d1d9574938a2932e8/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9", size = 504896, upload-time = "2026-06-30T07:17:39.689Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1b/a114b972cefa1ab1cdb3c7bb177cd3844a12826c507c722d3a73516dbbaf/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c", size = 391545, upload-time = "2026-06-30T07:17:41.336Z" }, + { url = "https://files.pythonhosted.org/packages/4e/98/af9b3db77d47fcbe6c8c1f36e2c2147ec70292819e99c325f871584a1c11/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9", size = 380059, upload-time = "2026-06-30T07:17:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ba/0efd8668b97c1d26a61566386c636a7a7a09829e474fdf807caa15a2c844/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41", size = 393235, upload-time = "2026-06-30T07:17:44.637Z" }, + { url = "https://files.pythonhosted.org/packages/62/90/8c139ee9690f73b0829f32647de6f40d826f8f443af6fa72644f96351aac/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c", size = 413008, upload-time = "2026-06-30T07:17:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/0043896fdd7828ce09a1d9a8b06433714d0960fc4ff3fc4aa72b666b764e/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9", size = 558118, upload-time = "2026-06-30T07:17:47.759Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/02355f0e134f783a8f9814c4680a1bd311d37671577a5964ea838573ff37/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76", size = 623138, upload-time = "2026-06-30T07:17:49.355Z" }, + { url = "https://files.pythonhosted.org/packages/10/85/48f0abdcef5cce4e034c7a5b0ceeceba0b01bf0d942824f4bb720afe2dec/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826", size = 586486, upload-time = "2026-06-30T07:17:51.141Z" }, ] [[package]] name = "ruff" -version = "0.5.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bf/2b/69e5e412f9d390adbdbcbf4f64d6914fa61b44b08839a6584655014fc524/ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5", size = 2449817, upload-time = "2024-08-08T15:43:07.467Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/eb/06e06aaf96af30a68e83b357b037008c54a2ddcbad4f989535007c700394/ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a", size = 9570571, upload-time = "2024-08-08T15:41:56.537Z" }, - { url = "https://files.pythonhosted.org/packages/a4/10/1be32aeaab8728f78f673e7a47dd813222364479b2d6573dbcf0085e83ea/ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be", size = 8685138, upload-time = "2024-08-08T15:42:02.833Z" }, - { url = "https://files.pythonhosted.org/packages/3d/1d/c218ce83beb4394ba04d05e9aa2ae6ce9fba8405688fe878b0fdb40ce855/ruff-0.5.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf3d86a1fdac1aec8a3417a63587d93f906c678bb9ed0b796da7b59c1114a1e", size = 8266785, upload-time = "2024-08-08T15:42:08.321Z" }, - { url = "https://files.pythonhosted.org/packages/26/79/7f49509bd844476235b40425756def366b227a9714191c91f02fb2178635/ruff-0.5.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a01c34400097b06cf8a6e61b35d6d456d5bd1ae6961542de18ec81eaf33b4cb8", size = 9983964, upload-time = "2024-08-08T15:42:12.419Z" }, - { url = "https://files.pythonhosted.org/packages/bf/b1/939836b70bf9fcd5e5cd3ea67fdb8abb9eac7631351d32f26544034a35e4/ruff-0.5.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcc8054f1a717e2213500edaddcf1dbb0abad40d98e1bd9d0ad364f75c763eea", size = 9359490, upload-time = "2024-08-08T15:42:16.713Z" }, - { url = "https://files.pythonhosted.org/packages/32/7d/b3db19207de105daad0c8b704b2c6f2a011f9c07017bd58d8d6e7b8eba19/ruff-0.5.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f70284e73f36558ef51602254451e50dd6cc479f8b6f8413a95fcb5db4a55fc", size = 10170833, upload-time = "2024-08-08T15:42:20.54Z" }, - { url = "https://files.pythonhosted.org/packages/a2/45/eae9da55f3357a1ac04220230b8b07800bf516e6dd7e1ad20a2ff3b03b1b/ruff-0.5.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a78ad870ae3c460394fc95437d43deb5c04b5c29297815a2a1de028903f19692", size = 10896360, upload-time = "2024-08-08T15:42:25.2Z" }, - { url = "https://files.pythonhosted.org/packages/99/67/4388b36d145675f4c51ebec561fcd4298a0e2550c81e629116f83ce45a39/ruff-0.5.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ccd078c66a8e419475174bfe60a69adb36ce04f8d4e91b006f1329d5cd44bcf", size = 10477094, upload-time = "2024-08-08T15:42:29.553Z" }, - { url = "https://files.pythonhosted.org/packages/e1/9c/f5e6ed1751dc187a4ecf19a4970dd30a521c0ee66b7941c16e292a4043fb/ruff-0.5.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e31c9bad4ebf8fdb77b59cae75814440731060a09a0e0077d559a556453acbb", size = 11480896, upload-time = "2024-08-08T15:42:33.772Z" }, - { url = "https://files.pythonhosted.org/packages/c8/3b/2b683be597bbd02046678fc3fc1c199c641512b20212073b58f173822bb3/ruff-0.5.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d796327eed8e168164346b769dd9a27a70e0298d667b4ecee6877ce8095ec8e", size = 10179702, upload-time = "2024-08-08T15:42:38.038Z" }, - { url = "https://files.pythonhosted.org/packages/f1/38/c2d94054dc4b3d1ea4c2ba3439b2a7095f08d1c8184bc41e6abe2a688be7/ruff-0.5.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a09ea2c3f7778cc635e7f6edf57d566a8ee8f485f3c4454db7771efb692c499", size = 9982855, upload-time = "2024-08-08T15:42:42.031Z" }, - { url = "https://files.pythonhosted.org/packages/7d/e7/1433db2da505ffa8912dcf5b28a8743012ee780cbc20ad0bf114787385d9/ruff-0.5.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a36d8dcf55b3a3bc353270d544fb170d75d2dff41eba5df57b4e0b67a95bb64e", size = 9433156, upload-time = "2024-08-08T15:42:45.339Z" }, - { url = "https://files.pythonhosted.org/packages/e0/36/4fa43250e67741edeea3d366f59a1dc993d4d89ad493a36cbaa9889895f2/ruff-0.5.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9369c218f789eefbd1b8d82a8cf25017b523ac47d96b2f531eba73770971c9e5", size = 9782971, upload-time = "2024-08-08T15:42:49.354Z" }, - { url = "https://files.pythonhosted.org/packages/80/0e/8c276103d518e5cf9202f70630aaa494abf6fc71c04d87c08b6d3cd07a4b/ruff-0.5.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b88ca3db7eb377eb24fb7c82840546fb7acef75af4a74bd36e9ceb37a890257e", size = 10247775, upload-time = "2024-08-08T15:42:53.294Z" }, - { url = "https://files.pythonhosted.org/packages/cb/b9/673096d61276f39291b729dddde23c831a5833d98048349835782688a0ec/ruff-0.5.7-py3-none-win32.whl", hash = "sha256:33d61fc0e902198a3e55719f4be6b375b28f860b09c281e4bdbf783c0566576a", size = 7841772, upload-time = "2024-08-08T15:42:57.488Z" }, - { url = "https://files.pythonhosted.org/packages/67/1c/4520c98bfc06b9c73cd1457686d4d3935d40046b1ddea08403e5a6deff51/ruff-0.5.7-py3-none-win_amd64.whl", hash = "sha256:083bbcbe6fadb93cd86709037acc510f86eed5a314203079df174c40bbbca6b3", size = 8699779, upload-time = "2024-08-08T15:43:00.429Z" }, - { url = "https://files.pythonhosted.org/packages/38/23/b3763a237d2523d40a31fe2d1a301191fe392dd48d3014977d079cf8c0bd/ruff-0.5.7-py3-none-win_arm64.whl", hash = "sha256:2dca26154ff9571995107221d0aeaad0e75a77b5a682d6236cf89a58c70b76f4", size = 8091891, upload-time = "2024-08-08T15:43:04.162Z" }, +version = "0.15.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/06/ae069393fc66e8ff33036d4b368003833bf6e88ccf182e17e7a2f1c754fd/ruff-0.15.22.tar.gz", hash = "sha256:3f15175b1fb580126f58285a5dae6b2ea89000136d980c64499211f116b54809", size = 4785063, upload-time = "2026-07-16T15:14:13.244Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/18/ee54b7ae1e121be7a28ea6da4b67564ebb0530e183a54415ab7e3bcd2c4e/ruff-0.15.22-py3-none-linux_armv6l.whl", hash = "sha256:44423e73493737f5e7c5b41d475483898ff37afcdae38bc3da5085e29af1c2d8", size = 10781258, upload-time = "2026-07-16T15:13:19.452Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d2/2520cb14761ddbeaf57642a76942fc36adcbdbe53b4532241995f6fc485c/ruff-0.15.22-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b82c6482946e9eda7ff2e091d25b8bad3f718684e1916d41bd56873cee05b697", size = 10999477, upload-time = "2026-07-16T15:13:23.318Z" }, + { url = "https://files.pythonhosted.org/packages/c9/10/74e53572aa758dfaa678c2a2646b5c5515d884b7ca56be4d2ce03ca4b560/ruff-0.15.22-py3-none-macosx_11_0_arm64.whl", hash = "sha256:11c1c715af53a09f714e011106bffc419751ec8232fcb5da42173284ea3fec6f", size = 10466716, upload-time = "2026-07-16T15:13:26.162Z" }, + { url = "https://files.pythonhosted.org/packages/1e/cc/44eaaf0844e028182f2d0a8f2190d0f359159aed0a9e5ab861d892f1ae2a/ruff-0.15.22-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:742a29cf29bddb7c8327895d6a10e0e6c5b38a96dd407af9b5d0857f809c0576", size = 10892644, upload-time = "2026-07-16T15:13:29.229Z" }, + { url = "https://files.pythonhosted.org/packages/9f/21/8edf559014d2b0f82beea19cfb713993ad802ccda16868769979c6090a84/ruff-0.15.22-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72af58b951b0ae395935ae79763dc349bc0eb706319d28f7a33ad2cfb3cfc178", size = 10576719, upload-time = "2026-07-16T15:13:32.35Z" }, + { url = "https://files.pythonhosted.org/packages/bf/1e/3a13abd392a3b50b62e5938a831f9ab6e588358cacad5c18545b716d2182/ruff-0.15.22-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d425005c1835eb24e2ee4161cb90e8db263415f4a71c8c72c33abaa6c0c224", size = 11376494, upload-time = "2026-07-16T15:13:35.958Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3e/422d3d95bcf04dd78e1aeac22184d4f9a8fb2c01865d39d44618484a0317/ruff-0.15.22-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b9b3f8779a4f08c969defc3c8c35abffaa757e601ed5ae66d6d1db6519969a", size = 12208370, upload-time = "2026-07-16T15:13:39.185Z" }, + { url = "https://files.pythonhosted.org/packages/1e/91/5d065a0e0a02bf4813f5119ad278462eed081d2b832eb7c021ade0ec9e65/ruff-0.15.22-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e0dd1b2e4d3d585f897a0d137cbf4eaf6223bef4e8ce34d6bb12556c5f9249e", size = 11581098, upload-time = "2026-07-16T15:13:42.132Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f9/a0d4871d12fae702eb1f41b686caf05f1f8b124dc6db6f784f53d74918fa/ruff-0.15.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365523eb91d9224e1bcb03b022fbf0facb8f9e23792a2c53d9d4b3924bdbdebb", size = 11399422, upload-time = "2026-07-16T15:13:45.2Z" }, + { url = "https://files.pythonhosted.org/packages/18/80/c843a5176cddbceb0b7e8dd41cf9993490796c1c469348d384f5a5c13c56/ruff-0.15.22-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fabfd168afdf29fee5be98b831efa9683c94d7c5a3b58b9ce5a2e38444589a74", size = 11381683, upload-time = "2026-07-16T15:13:48.46Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/8485de0ae92239438a36cfc51350db9b9e85c9ebdfaea91b18e422706662/ruff-0.15.22-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:225dbf095a87f1d9f90f5fd7924d2613ee452a75a4308c63a8f50f761787aa7c", size = 10850295, upload-time = "2026-07-16T15:13:51.655Z" }, + { url = "https://files.pythonhosted.org/packages/fa/91/24977ec2ec72eaf15e4394ace2959fdff2dd1e14f03e005e838023407169/ruff-0.15.22-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1877d63b9d24ed278744f1523fd11b85540566d54641f97c566d7d9dc5ca5296", size = 10579640, upload-time = "2026-07-16T15:13:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/9c/47/9b51216951974df1f263ac19da550d34252e0ed7218c25f10c5ef9ed7517/ruff-0.15.22-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1606c510bd7215680d32efab38965f7cdec3ef69f5170a3f4791404ffdd5262", size = 11105077, upload-time = "2026-07-16T15:13:57.915Z" }, + { url = "https://files.pythonhosted.org/packages/c2/47/20e9d4a3b8016778acea5fc32bb50d35d207500a17ddb529ffa6996feef8/ruff-0.15.22-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:630479b18625f5ffc373f77603a22a9f8ac0acd7ff0501178b5db28ec71e9c64", size = 11490980, upload-time = "2026-07-16T15:14:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/4d/76/3f72d8fc38c1cb77b38c56a70da9d0c17700cc1cc50f9649c9d3c8f5ba71/ruff-0.15.22-py3-none-win32.whl", hash = "sha256:e5ba0e4a13fd14abbed2a77b517a3911290c6c6c59ef67784328d1668fab76cf", size = 10789165, upload-time = "2026-07-16T15:14:04.16Z" }, + { url = "https://files.pythonhosted.org/packages/cb/46/4965251734c2b6fcdca1b1b187d20bcac3af0ee5b083b89c910bb961ce3a/ruff-0.15.22-py3-none-win_amd64.whl", hash = "sha256:9be63ba1eb936acd2d1342fb8337c356353706fce233b2a15a09a97037e6acde", size = 11938297, upload-time = "2026-07-16T15:14:07.316Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/e69b1ff4c8b69093ef08b8919ab767af0569666865b39c30a8795d88d3c6/ruff-0.15.22-py3-none-win_arm64.whl", hash = "sha256:e1168075b72158510839f250027659cdd78476f40507dd517892304c41318661", size = 11298172, upload-time = "2026-07-16T15:14:10.51Z" }, +] + +[[package]] +name = "s3transfer" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/74/8d69dcb7a9efe8baa2046891735e5dfe433ad558ae23d9e3c14c633d1d58/s3transfer-0.14.0.tar.gz", hash = "sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125", size = 151547, upload-time = "2025-09-09T19:23:31.089Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/f0/ae7ca09223a81a1d890b2557186ea015f6e0502e9b8cb8e1813f1d8cfa4e/s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456", size = 85712, upload-time = "2025-09-09T19:23:30.041Z" }, ] [[package]] name = "secretstorage" -version = "3.3.3" +version = "3.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "jeepney" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739, upload-time = "2022-08-13T16:22:46.976Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221, upload-time = "2022-08-13T16:22:44.457Z" }, + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, ] [[package]] name = "setuptools" -version = "80.9.0" +version = "81.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299, upload-time = "2026-02-06T21:10:39.601Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" }, ] [[package]] @@ -2748,6 +4557,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "slack-bolt" +version = "1.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "slack-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/4f/5ba15533d66da2e7174334cc0e2805142e5390c9f4c5f31633df78b17006/slack_bolt-1.30.0.tar.gz", hash = "sha256:af38258d41f801ad9c74503090e0f39accd66c49f667f7e55c97fcdb0e51b886", size = 131180, upload-time = "2026-07-15T20:47:33.679Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/ee/1a7a286cf98fa3f4eeffaabc090e82f58f058ab4812aa1d7421d92c2637a/slack_bolt-1.30.0-py2.py3-none-any.whl", hash = "sha256:81f5bc46e79516d23d5e2a31dded6304dd1b8b6b72c0083f2f31d5d801e262c4", size = 235341, upload-time = "2026-07-15T20:47:32.113Z" }, +] + +[[package]] +name = "slack-sdk" +version = "3.43.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/75/a4964eb771a0c74d79ee7a3bee6fb5d9718909dd1b675e80d62a6a0ad90a/slack_sdk-3.43.0.tar.gz", hash = "sha256:0553152e46c4259eb69f7464cdadc35ba4802ca10f9f5a849c92cf03d6c2ba07", size = 252769, upload-time = "2026-06-30T18:04:41.59Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/55/42141b8338d46323d5b3c6095201b044c670c20f898643b322ea9b1543a1/slack_sdk-3.43.0-py2.py3-none-any.whl", hash = "sha256:4b6557c65577fc172f685af218b811f9f3b4909e24cddd839ada09565f10c585", size = 315866, upload-time = "2026-06-30T18:04:39.636Z" }, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -2759,41 +4589,114 @@ wheels = [ [[package]] name = "snowballstemmer" -version = "3.0.1" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/f8/0a71edf031f03c40db17503cb8ca78a69a171254e568e7db241b0ab57ea1/snowballstemmer-3.1.1.tar.gz", hash = "sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260", size = 123314, upload-time = "2026-06-03T00:56:40.194Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl", hash = "sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752", size = 104164, upload-time = "2026-06-03T00:56:38.614Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +sdist = { url = "https://files.pythonhosted.org/packages/47/2c/0a5f6f8ee0d5589e48c7640213ed5175d52cf540a06725b628cc1a45d6ce/soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e", size = 121110, upload-time = "2026-05-24T13:55:57.154Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" }, ] [[package]] name = "sse-starlette" -version = "3.0.2" +version = "3.4.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", marker = "python_full_version >= '3.10'" }, + { name = "anyio" }, + { name = "starlette" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/6f/22ed6e33f8a9e76ca0a412405f31abb844b779d52c5f96660766edcd737c/sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a", size = 20985, upload-time = "2025-07-27T09:07:44.565Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d2/1b/bc9e3e7a72dcdad7dc7888758f5d00f56f8909ed5cfdff822bd72bb4c520/sse_starlette-3.4.5.tar.gz", hash = "sha256:83072538bc211a2f68b7b0422226c4af3e9b62e106e07034664b832ca019842a", size = 35249, upload-time = "2026-06-20T17:36:58.322Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/10/c78f463b4ef22eef8491f218f692be838282cd65480f6e423d7730dfd1fb/sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a", size = 11297, upload-time = "2025-07-27T09:07:43.268Z" }, + { url = "https://files.pythonhosted.org/packages/78/75/c88d3f5dafd59c791da1ce27650d30bf5b70cbf1cbf01cd00e5f9e360915/sse_starlette-3.4.5-py3-none-any.whl", hash = "sha256:e71bad53323f65573c3864a6c3bd0c1eb6e5f092b2e48082b0c35927d19ca296", size = 16518, upload-time = "2026-06-20T17:36:56.729Z" }, ] [[package]] name = "starlette" -version = "0.47.3" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, +] + +[[package]] +name = "strands-agents" +version = "1.47.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boto3" }, + { name = "botocore" }, + { name = "docstring-parser" }, + { name = "jsonschema" }, + { name = "mcp" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation-threading" }, + { name = "opentelemetry-sdk" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "typing-extensions" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/05/5eb8431ff340739b1c21b0da7d19ec9604b9c4953a1c4638df915321573c/strands_agents-1.47.0.tar.gz", hash = "sha256:97770cb6beb6e5fd1a58849f41201eb7edb43fd67ad3de6544ada2f342c2bcf0", size = 1157139, upload-time = "2026-07-10T14:45:05.809Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/cb/ceae892c5823bea9254160efc02a4553f2ced9d183676110c03c3a9ff0f3/strands_agents-1.47.0-py3-none-any.whl", hash = "sha256:1f6ce17404ff02079244ad7a4a180a2a7150546275e4b91187d71472ba8fe3f2", size = 600611, upload-time = "2026-07-10T14:45:03.942Z" }, +] + +[[package]] +name = "strands-agents-tools" +version = "0.8.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "aws-requests-auth" }, + { name = "botocore" }, + { name = "dill" }, + { name = "markdownify" }, + { name = "pillow" }, + { name = "prompt-toolkit" }, + { name = "pyjwt" }, + { name = "requests" }, + { name = "rich" }, + { name = "slack-bolt" }, + { name = "strands-agents" }, + { name = "sympy" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/dd/aaa1f17ec10db4a9f49e2e48bd3cd5fb646c1c284e4fe6fa0a6a32380914/strands_agents_tools-0.8.3.tar.gz", hash = "sha256:19f6d3d617e9ef85b0a31c475f0a3f81d8ea60a2ed7dc6bd8f17fdd586c94d46", size = 511310, upload-time = "2026-07-09T19:55:59.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/8b/1500ae455cc8434a54f013278320042d88ba9daffb5dafaed1fa8b48c3f3/strands_agents_tools-0.8.3-py3-none-any.whl", hash = "sha256:bc17fbad5ad7957bef33538fb5e0c386f7883dab92c557e92175e7ff16b42dac", size = 327194, upload-time = "2026-07-09T19:55:57.473Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, + { name = "mpmath" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/15/b9/cc3017f9a9c9b6e27c5106cc10cc7904653c3eec0729793aec10479dd669/starlette-0.47.3.tar.gz", hash = "sha256:6bc94f839cc176c4858894f1f8908f0ab79dfec1a6b8402f6da9be26ebea52e9", size = 2584144, upload-time = "2025-08-24T13:36:42.122Z" } +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/fd/901cfa59aaa5b30a99e16876f11abe38b59a1a2c51ffb3d7142bb6089069/starlette-0.47.3-py3-none-any.whl", hash = "sha256:89c0778ca62a76b826101e7c709e70680a1699ca7da6b44d38eb0a7e61fe4b51", size = 72991, upload-time = "2025-08-24T13:36:40.887Z" }, + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, ] [[package]] name = "temporalio" -version = "1.17.0" +version = "1.31.0" source = { virtual = "." } dependencies = [ { name = "nexus-rpc" }, @@ -2804,11 +4707,40 @@ dependencies = [ ] [package.optional-dependencies] +aioboto3 = [ + { name = "aioboto3" }, + { name = "types-aioboto3", extra = ["s3"] }, +] +deepagents = [ + { name = "deepagents", marker = "python_full_version >= '3.11'" }, + { name = "langchain", marker = "python_full_version >= '3.11'" }, + { name = "langchain-core", marker = "python_full_version >= '3.11'" }, +] +google-adk = [ + { name = "google-adk" }, + { name = "mcp" }, +] +google-genai = [ + { name = "google-genai" }, +] grpc = [ { name = "grpcio" }, ] +lambda-worker-otel = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-sdk" }, + { name = "opentelemetry-sdk-extension-aws" }, + { name = "opentelemetry-semantic-conventions" }, +] +langgraph = [ + { name = "langgraph" }, +] +langsmith = [ + { name = "langsmith" }, +] openai-agents = [ - { name = "eval-type-backport", marker = "python_full_version < '3.10'" }, + { name = "mcp" }, { name = "openai-agents" }, ] opentelemetry = [ @@ -2818,16 +4750,38 @@ opentelemetry = [ pydantic = [ { name = "pydantic" }, ] +strands-agents = [ + { name = "strands-agents" }, +] [package.dev-dependencies] dev = [ + { name = "async-timeout", marker = "python_full_version < '3.11'" }, + { name = "basedpyright" }, { name = "cibuildwheel" }, + { name = "cryptography" }, + { name = "deepagents", marker = "python_full_version >= '3.11'" }, + { name = "googleapis-common-protos" }, { name = "grpcio-tools" }, { name = "httpx" }, + { name = "langchain", marker = "python_full_version >= '3.11'" }, + { name = "langchain-anthropic", marker = "python_full_version >= '3.11'" }, + { name = "langchain-core", marker = "python_full_version >= '3.11'" }, + { name = "langgraph" }, + { name = "langsmith" }, + { name = "litellm" }, { name = "maturin" }, + { name = "mcp" }, + { name = "moto", extra = ["s3", "server"] }, { name = "mypy" }, { name = "mypy-protobuf" }, - { name = "openai-agents", extra = ["litellm"] }, + { name = "openai-agents" }, + { name = "openai-agents", extra = ["litellm"], marker = "python_full_version < '3.14'" }, + { name = "openinference-instrumentation-google-adk" }, + { name = "openinference-instrumentation-openai-agents" }, + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-sdk-extension-aws" }, + { name = "opentelemetry-semantic-conventions" }, { name = "psutil" }, { name = "pydocstyle" }, { name = "pydoctor" }, @@ -2835,117 +4789,194 @@ dev = [ { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, + { name = "pytest-flakefinder" }, { name = "pytest-pretty" }, + { name = "pytest-rerunfailures" }, { name = "pytest-timeout" }, + { name = "pytest-xdist" }, { name = "ruff" }, + { name = "setuptools" }, + { name = "strands-agents" }, + { name = "strands-agents-tools" }, { name = "toml" }, { name = "twine" }, ] [package.metadata] requires-dist = [ - { name = "eval-type-backport", marker = "python_full_version < '3.10' and extra == 'openai-agents'", specifier = ">=0.2.2" }, + { name = "aioboto3", marker = "extra == 'aioboto3'", specifier = ">=10.4.0" }, + { name = "deepagents", marker = "python_full_version >= '3.11' and extra == 'deepagents'", specifier = ">=0.6.12,<0.7" }, + { name = "google-adk", marker = "extra == 'google-adk'", specifier = ">=2.2.0,<3" }, + { name = "google-genai", marker = "extra == 'google-genai'", specifier = ">=2.10.0,<3.0.0" }, { name = "grpcio", marker = "extra == 'grpc'", specifier = ">=1.48.2,<2" }, - { name = "nexus-rpc", specifier = "==1.1.0" }, - { name = "openai-agents", marker = "extra == 'openai-agents'", specifier = ">=0.2.3,<=0.2.9" }, + { name = "langchain", marker = "python_full_version >= '3.11' and extra == 'deepagents'", specifier = ">=1.3.11,<2" }, + { name = "langchain-core", marker = "python_full_version >= '3.11' and extra == 'deepagents'", specifier = ">=1.4.8,<2" }, + { name = "langgraph", marker = "extra == 'langgraph'", specifier = ">=1.1.0" }, + { name = "langsmith", marker = "extra == 'langsmith'", specifier = ">=0.7.34,<0.9" }, + { name = "mcp", marker = "extra == 'google-adk'", specifier = ">=1.24,<2" }, + { name = "mcp", marker = "extra == 'openai-agents'", specifier = ">=1.9.4,<2" }, + { name = "nexus-rpc", specifier = "==1.4.0" }, + { name = "openai-agents", marker = "extra == 'openai-agents'", specifier = ">=0.17.5" }, + { name = "opentelemetry-api", marker = "extra == 'lambda-worker-otel'", specifier = ">=1.11.1,<2" }, { name = "opentelemetry-api", marker = "extra == 'opentelemetry'", specifier = ">=1.11.1,<2" }, + { name = "opentelemetry-exporter-otlp-proto-grpc", marker = "extra == 'lambda-worker-otel'", specifier = ">=1.11.1,<2" }, + { name = "opentelemetry-sdk", marker = "extra == 'lambda-worker-otel'", specifier = ">=1.11.1,<2" }, { name = "opentelemetry-sdk", marker = "extra == 'opentelemetry'", specifier = ">=1.11.1,<2" }, - { name = "protobuf", specifier = ">=3.20,<6" }, + { name = "opentelemetry-sdk-extension-aws", marker = "extra == 'lambda-worker-otel'", specifier = ">=2.0.0,<3" }, + { name = "opentelemetry-semantic-conventions", marker = "extra == 'lambda-worker-otel'", specifier = ">=0.40b0,<1" }, + { name = "protobuf", specifier = ">=3.20,<8.0.0" }, { name = "pydantic", marker = "extra == 'pydantic'", specifier = ">=2.0.0,<3" }, { name = "python-dateutil", marker = "python_full_version < '3.11'", specifier = ">=2.8.2,<3" }, - { name = "types-protobuf", specifier = ">=3.20" }, + { name = "strands-agents", marker = "extra == 'strands-agents'", specifier = ">=1.39.0" }, + { name = "types-aioboto3", extras = ["s3"], marker = "extra == 'aioboto3'", specifier = ">=10.4.0" }, + { name = "types-protobuf", specifier = ">=3.20,<8.0.0" }, { name = "typing-extensions", specifier = ">=4.2.0,<5" }, ] -provides-extras = ["grpc", "opentelemetry", "pydantic", "openai-agents"] +provides-extras = ["grpc", "opentelemetry", "pydantic", "openai-agents", "google-adk", "langgraph", "langsmith", "deepagents", "lambda-worker-otel", "aioboto3", "google-genai", "strands-agents"] [package.metadata.requires-dev] dev = [ + { name = "async-timeout", marker = "python_full_version < '3.11'", specifier = ">=4.0,<6" }, + { name = "basedpyright", specifier = "==1.34.0" }, { name = "cibuildwheel", specifier = ">=2.22.0,<3" }, + { name = "cryptography", specifier = ">=46" }, + { name = "deepagents", marker = "python_full_version >= '3.11'", specifier = ">=0.6.12,<0.7" }, + { name = "googleapis-common-protos", specifier = ">=1.75.0,<2" }, { name = "grpcio-tools", specifier = ">=1.48.2,<2" }, { name = "httpx", specifier = ">=0.28.1" }, + { name = "langchain", marker = "python_full_version >= '3.11'", specifier = ">=1.3.11,<2" }, + { name = "langchain-anthropic", marker = "python_full_version >= '3.11'", specifier = ">=1.4.7" }, + { name = "langchain-core", marker = "python_full_version >= '3.11'", specifier = ">=1.4.8,<2" }, + { name = "langgraph", specifier = ">=1.1.0" }, + { name = "langsmith", specifier = ">=0.7.34,<0.9" }, + { name = "litellm", specifier = ">=1.83.0" }, { name = "maturin", specifier = ">=1.8.2" }, - { name = "mypy", specifier = "==1.4.1" }, + { name = "mcp", specifier = ">=1.9.4,<2" }, + { name = "moto", extras = ["s3", "server"], specifier = ">=5" }, + { name = "mypy", specifier = "==1.18.2" }, { name = "mypy-protobuf", specifier = ">=3.3.0,<4" }, - { name = "openai-agents", extras = ["litellm"], specifier = ">=0.2.3,<=0.2.9" }, + { name = "openai-agents", marker = "python_full_version >= '3.14'", specifier = ">=0.14.0" }, + { name = "openai-agents", extras = ["litellm"], marker = "python_full_version < '3.14'", specifier = ">=0.14.0" }, + { name = "openinference-instrumentation-google-adk", specifier = ">=0.1.11" }, + { name = "openinference-instrumentation-openai-agents", specifier = ">=0.1.0" }, + { name = "opentelemetry-exporter-otlp-proto-grpc", specifier = ">=1.11.1,<2" }, + { name = "opentelemetry-sdk-extension-aws", specifier = ">=2.0.0,<3" }, + { name = "opentelemetry-semantic-conventions", specifier = ">=0.40b0,<1" }, { name = "psutil", specifier = ">=5.9.3,<6" }, { name = "pydocstyle", specifier = ">=6.3.0,<7" }, - { name = "pydoctor", specifier = ">=24.11.1,<25" }, + { name = "pydoctor", specifier = ">=25.10.1,<26" }, { name = "pyright", specifier = "==1.1.403" }, - { name = "pytest", specifier = "~=7.4" }, + { name = "pytest", specifier = "~=9.0" }, { name = "pytest-asyncio", specifier = ">=0.21,<0.22" }, { name = "pytest-cov", specifier = ">=6.1.1" }, + { name = "pytest-flakefinder", specifier = ">=1.1.0" }, { name = "pytest-pretty", specifier = ">=1.3.0" }, + { name = "pytest-rerunfailures", specifier = ">=16.1" }, { name = "pytest-timeout", specifier = "~=2.2" }, - { name = "ruff", specifier = ">=0.5.0,<0.6" }, + { name = "pytest-xdist", specifier = ">=3.6,<4" }, + { name = "ruff", specifier = ">=0.15.12,<0.16" }, + { name = "setuptools", specifier = "<82" }, + { name = "strands-agents", specifier = ">=1.39.0" }, + { name = "strands-agents-tools", specifier = ">=0.5.2" }, { name = "toml", specifier = ">=0.10.2,<0.11" }, { name = "twine", specifier = ">=4.0.1,<5" }, ] +[[package]] +name = "tenacity" +version = "9.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, +] + [[package]] name = "tiktoken" -version = "0.11.0" +version = "0.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "regex" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a7/86/ad0155a37c4f310935d5ac0b1ccf9bdb635dcb906e0a9a26b616dd55825a/tiktoken-0.11.0.tar.gz", hash = "sha256:3c518641aee1c52247c2b97e74d8d07d780092af79d5911a6ab5e79359d9b06a", size = 37648, upload-time = "2025-08-08T23:58:08.495Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/4d/c6a2e7dca2b4f2e9e0bfd62b3fe4f114322e2c028cfba905a72bc76ce479/tiktoken-0.11.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:8a9b517d6331d7103f8bef29ef93b3cca95fa766e293147fe7bacddf310d5917", size = 1059937, upload-time = "2025-08-08T23:57:28.57Z" }, - { url = "https://files.pythonhosted.org/packages/41/54/3739d35b9f94cb8dc7b0db2edca7192d5571606aa2369a664fa27e811804/tiktoken-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b4ddb1849e6bf0afa6cc1c5d809fb980ca240a5fffe585a04e119519758788c0", size = 999230, upload-time = "2025-08-08T23:57:30.241Z" }, - { url = "https://files.pythonhosted.org/packages/dd/f4/ec8d43338d28d53513004ebf4cd83732a135d11011433c58bf045890cc10/tiktoken-0.11.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10331d08b5ecf7a780b4fe4d0281328b23ab22cdb4ff65e68d56caeda9940ecc", size = 1130076, upload-time = "2025-08-08T23:57:31.706Z" }, - { url = "https://files.pythonhosted.org/packages/94/80/fb0ada0a882cb453caf519a4bf0d117c2a3ee2e852c88775abff5413c176/tiktoken-0.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b062c82300341dc87e0258c69f79bed725f87e753c21887aea90d272816be882", size = 1183942, upload-time = "2025-08-08T23:57:33.142Z" }, - { url = "https://files.pythonhosted.org/packages/2f/e9/6c104355b463601719582823f3ea658bc3aa7c73d1b3b7553ebdc48468ce/tiktoken-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:195d84bec46169af3b1349a1495c151d37a0ff4cba73fd08282736be7f92cc6c", size = 1244705, upload-time = "2025-08-08T23:57:34.594Z" }, - { url = "https://files.pythonhosted.org/packages/94/75/eaa6068f47e8b3f0aab9e05177cce2cf5aa2cc0ca93981792e620d4d4117/tiktoken-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe91581b0ecdd8783ce8cb6e3178f2260a3912e8724d2f2d49552b98714641a1", size = 884152, upload-time = "2025-08-08T23:57:36.18Z" }, - { url = "https://files.pythonhosted.org/packages/8a/91/912b459799a025d2842566fe1e902f7f50d54a1ce8a0f236ab36b5bd5846/tiktoken-0.11.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4ae374c46afadad0f501046db3da1b36cd4dfbfa52af23c998773682446097cf", size = 1059743, upload-time = "2025-08-08T23:57:37.516Z" }, - { url = "https://files.pythonhosted.org/packages/8c/e9/6faa6870489ce64f5f75dcf91512bf35af5864583aee8fcb0dcb593121f5/tiktoken-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:25a512ff25dc6c85b58f5dd4f3d8c674dc05f96b02d66cdacf628d26a4e4866b", size = 999334, upload-time = "2025-08-08T23:57:38.595Z" }, - { url = "https://files.pythonhosted.org/packages/a1/3e/a05d1547cf7db9dc75d1461cfa7b556a3b48e0516ec29dfc81d984a145f6/tiktoken-0.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2130127471e293d385179c1f3f9cd445070c0772be73cdafb7cec9a3684c0458", size = 1129402, upload-time = "2025-08-08T23:57:39.627Z" }, - { url = "https://files.pythonhosted.org/packages/34/9a/db7a86b829e05a01fd4daa492086f708e0a8b53952e1dbc9d380d2b03677/tiktoken-0.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21e43022bf2c33f733ea9b54f6a3f6b4354b909f5a73388fb1b9347ca54a069c", size = 1184046, upload-time = "2025-08-08T23:57:40.689Z" }, - { url = "https://files.pythonhosted.org/packages/9d/bb/52edc8e078cf062ed749248f1454e9e5cfd09979baadb830b3940e522015/tiktoken-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:adb4e308eb64380dc70fa30493e21c93475eaa11669dea313b6bbf8210bfd013", size = 1244691, upload-time = "2025-08-08T23:57:42.251Z" }, - { url = "https://files.pythonhosted.org/packages/60/d9/884b6cd7ae2570ecdcaffa02b528522b18fef1cbbfdbcaa73799807d0d3b/tiktoken-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:ece6b76bfeeb61a125c44bbefdfccc279b5288e6007fbedc0d32bfec602df2f2", size = 884392, upload-time = "2025-08-08T23:57:43.628Z" }, - { url = "https://files.pythonhosted.org/packages/e7/9e/eceddeffc169fc75fe0fd4f38471309f11cb1906f9b8aa39be4f5817df65/tiktoken-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fd9e6b23e860973cf9526544e220b223c60badf5b62e80a33509d6d40e6c8f5d", size = 1055199, upload-time = "2025-08-08T23:57:45.076Z" }, - { url = "https://files.pythonhosted.org/packages/4f/cf/5f02bfefffdc6b54e5094d2897bc80efd43050e5b09b576fd85936ee54bf/tiktoken-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6a76d53cee2da71ee2731c9caa747398762bda19d7f92665e882fef229cb0b5b", size = 996655, upload-time = "2025-08-08T23:57:46.304Z" }, - { url = "https://files.pythonhosted.org/packages/65/8e/c769b45ef379bc360c9978c4f6914c79fd432400a6733a8afc7ed7b0726a/tiktoken-0.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ef72aab3ea240646e642413cb363b73869fed4e604dcfd69eec63dc54d603e8", size = 1128867, upload-time = "2025-08-08T23:57:47.438Z" }, - { url = "https://files.pythonhosted.org/packages/d5/2d/4d77f6feb9292bfdd23d5813e442b3bba883f42d0ac78ef5fdc56873f756/tiktoken-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f929255c705efec7a28bf515e29dc74220b2f07544a8c81b8d69e8efc4578bd", size = 1183308, upload-time = "2025-08-08T23:57:48.566Z" }, - { url = "https://files.pythonhosted.org/packages/7a/65/7ff0a65d3bb0fc5a1fb6cc71b03e0f6e71a68c5eea230d1ff1ba3fd6df49/tiktoken-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:61f1d15822e4404953d499fd1dcc62817a12ae9fb1e4898033ec8fe3915fdf8e", size = 1244301, upload-time = "2025-08-08T23:57:49.642Z" }, - { url = "https://files.pythonhosted.org/packages/f5/6e/5b71578799b72e5bdcef206a214c3ce860d999d579a3b56e74a6c8989ee2/tiktoken-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:45927a71ab6643dfd3ef57d515a5db3d199137adf551f66453be098502838b0f", size = 884282, upload-time = "2025-08-08T23:57:50.759Z" }, - { url = "https://files.pythonhosted.org/packages/cc/cd/a9034bcee638716d9310443818d73c6387a6a96db93cbcb0819b77f5b206/tiktoken-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a5f3f25ffb152ee7fec78e90a5e5ea5b03b4ea240beed03305615847f7a6ace2", size = 1055339, upload-time = "2025-08-08T23:57:51.802Z" }, - { url = "https://files.pythonhosted.org/packages/f1/91/9922b345f611b4e92581f234e64e9661e1c524875c8eadd513c4b2088472/tiktoken-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7dc6e9ad16a2a75b4c4be7208055a1f707c9510541d94d9cc31f7fbdc8db41d8", size = 997080, upload-time = "2025-08-08T23:57:53.442Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9d/49cd047c71336bc4b4af460ac213ec1c457da67712bde59b892e84f1859f/tiktoken-0.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a0517634d67a8a48fd4a4ad73930c3022629a85a217d256a6e9b8b47439d1e4", size = 1128501, upload-time = "2025-08-08T23:57:54.808Z" }, - { url = "https://files.pythonhosted.org/packages/52/d5/a0dcdb40dd2ea357e83cb36258967f0ae96f5dd40c722d6e382ceee6bba9/tiktoken-0.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fb4effe60574675118b73c6fbfd3b5868e5d7a1f570d6cc0d18724b09ecf318", size = 1182743, upload-time = "2025-08-08T23:57:56.307Z" }, - { url = "https://files.pythonhosted.org/packages/3b/17/a0fc51aefb66b7b5261ca1314afa83df0106b033f783f9a7bcbe8e741494/tiktoken-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94f984c9831fd32688aef4348803b0905d4ae9c432303087bae370dc1381a2b8", size = 1244057, upload-time = "2025-08-08T23:57:57.628Z" }, - { url = "https://files.pythonhosted.org/packages/50/79/bcf350609f3a10f09fe4fc207f132085e497fdd3612f3925ab24d86a0ca0/tiktoken-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2177ffda31dec4023356a441793fed82f7af5291120751dee4d696414f54db0c", size = 883901, upload-time = "2025-08-08T23:57:59.359Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b6/81c5799ab77a9580c6d840cf77d4717e929193a42190fd623a080c647aa6/tiktoken-0.11.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:13220f12c9e82e399377e768640ddfe28bea962739cc3a869cad98f42c419a89", size = 1061648, upload-time = "2025-08-08T23:58:00.753Z" }, - { url = "https://files.pythonhosted.org/packages/50/89/faa668066b2a4640534ef5797c09ecd0a48b43367502129b217339dfaa97/tiktoken-0.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f2db627f5c74477c0404b4089fd8a28ae22fa982a6f7d9c7d4c305c375218f3", size = 1000950, upload-time = "2025-08-08T23:58:01.855Z" }, - { url = "https://files.pythonhosted.org/packages/aa/7f/5f950528b54cb3025af4bc3522c23dbfb691afe8ffb292aa1e8dc2e6bddf/tiktoken-0.11.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2302772f035dceb2bcf8e55a735e4604a0b51a6dd50f38218ff664d46ec43807", size = 1130777, upload-time = "2025-08-08T23:58:03.256Z" }, - { url = "https://files.pythonhosted.org/packages/27/a4/e82ddf0773835ba24536ac8c0dce561e697698ec020a93212a1e041d39b4/tiktoken-0.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20b977989afe44c94bcc50db1f76971bb26dca44218bd203ba95925ef56f8e7a", size = 1185692, upload-time = "2025-08-08T23:58:04.476Z" }, - { url = "https://files.pythonhosted.org/packages/1b/c2/06361e41d176e62797ae65fa678111cdd30553321cf4d83e7b84107ea95f/tiktoken-0.11.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:669a1aa1ad6ebf1b3c26b45deb346f345da7680f845b5ea700bba45c20dea24c", size = 1246518, upload-time = "2025-08-08T23:58:06.126Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ad/ca37e15c46741ebb3904d562d03194e845539a08f7751a6df0f391757312/tiktoken-0.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:e363f33c720a055586f730c00e330df4c7ea0024bf1c83a8a9a9dbc054c4f304", size = 884702, upload-time = "2025-08-08T23:58:07.534Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/e4/e5/5f3cb2159769d0f4324c0e9e87f9de3c4b1cd45848a96b2eb3566ad5ca77/tiktoken-0.13.0.tar.gz", hash = "sha256:c9435714c3a84c2319499de9a300c0e604449dd0799ff246458b3bb6a7f433c1", size = 38986, upload-time = "2026-05-15T04:51:27.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/e3/03c90dadcf5b3f82b83cee9adee60ef666b329c654f58c066af44eae0287/tiktoken-0.13.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:47b1df8d73390a24f94980c75158cdd5c56d256f16d55f30cb49c230caba9ba4", size = 1036627, upload-time = "2026-05-15T04:50:11.229Z" }, + { url = "https://files.pythonhosted.org/packages/5e/30/760463e5b2e8ad2bc229ae0a17ecb06727b6cbc094f08d8f65844315632e/tiktoken-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7d40c6c5aab171dcd6eb8455bc567bde404bb9def60cdb8c1299cc782b242bb9", size = 984699, upload-time = "2026-05-15T04:50:12.874Z" }, + { url = "https://files.pythonhosted.org/packages/de/8a/8895f342a6b6aabd1a358e672f6f077b3ae51d0c63ca605d142db3bcd8ab/tiktoken-0.13.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:9b842981fa91accdffd48ff6408a977b7a91c3fbda55d353c3c68114d5c9d69e", size = 1118690, upload-time = "2026-05-15T04:50:14.234Z" }, + { url = "https://files.pythonhosted.org/packages/51/e0/92557768fb0801f0d9dd9243cb9b6d342900b05e4b1006d4771f49ce233e/tiktoken-0.13.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ed5a30027cb4d8c7ca8b273d4766f3db3cf58fad9e9f3b1a68a351ffb54873d5", size = 1138423, upload-time = "2026-05-15T04:50:15.668Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b9/a3d99feeedb032ffd09cd6652077f86bdee9a70dd0b990b2b272b445d4c3/tiktoken-0.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7ab10f4a21c2999846940113f6dbd72e0fa06a24119feddd74cc47e85818e06d", size = 1185077, upload-time = "2026-05-15T04:50:17.19Z" }, + { url = "https://files.pythonhosted.org/packages/cc/93/bab868277d475dc6d2aaacd34cdd239c282f4908dcc8702e0a3311a8e032/tiktoken-0.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a2937ad042d49d50eac6e1ba07c5661d4bd3942a5b1e0c0d08475c4df83676e1", size = 1241702, upload-time = "2026-05-15T04:50:18.772Z" }, + { url = "https://files.pythonhosted.org/packages/c3/16/27e9f7e0ed76e501cfefc9fb2112df4c7bf70ca96945b15ecb7615aac860/tiktoken-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:44733b99bfd72b590cd0936b1c01b3b4dd73122db2d544bc1ceeb18a7678c910", size = 876565, upload-time = "2026-05-15T04:50:20.268Z" }, + { url = "https://files.pythonhosted.org/packages/1a/4c/1bc81f4cd53e827c4ee67ca951b5935724716049452d8dfa09b8b82372bb/tiktoken-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7bfe1849caa65d1e1d9871817170ec497bbb7984e182012e1bdce72f66608cdb", size = 1036353, upload-time = "2026-05-15T04:50:21.757Z" }, + { url = "https://files.pythonhosted.org/packages/75/91/10b9c7076bc02c246c853201fdbbe300a4b8c5ed7b84c25f7403f4e32655/tiktoken-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:91c180fe255bd5a86d8316210d2833a1d4d33d026cd86a67812f4773743c8d26", size = 984644, upload-time = "2026-05-15T04:50:23.256Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e4/fceae98015fab47fcd49b8bd7f46145bcd187a47e0add1e5378ed67ef980/tiktoken-0.13.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:059c8ecf554eb5b41e6e054ba467b871b03277d267dee7244380aca4359747d4", size = 1119261, upload-time = "2026-05-15T04:50:24.348Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/fe42ad00de01a8c4a49ad8649a2c8a316835a9cad5961b11d21eac0020a5/tiktoken-0.13.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:36217497eaffc158607a3b26f065300db2aefd43b115263f3b9688ce38146173", size = 1138253, upload-time = "2026-05-15T04:50:25.505Z" }, + { url = "https://files.pythonhosted.org/packages/03/c4/ccee1ecccca107e9a16efcecdeeb964c325305038554d466ece65b42338f/tiktoken-0.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:303f7d91b4fce3baddbcde05c139091d4caa5026ac7214c1dc7ff7a71ee429ff", size = 1185747, upload-time = "2026-05-15T04:50:27.02Z" }, + { url = "https://files.pythonhosted.org/packages/9d/03/cd0cba295522b91eb55c6b2704f1df895f8226cfe60ab10d4d51d0cc9e69/tiktoken-0.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5d48843bee149630eb735a99e1f4a85b47308d21868ea63163f6e87768d3cfed", size = 1241265, upload-time = "2026-05-15T04:50:28.815Z" }, + { url = "https://files.pythonhosted.org/packages/7e/25/a10efd564402d82c2ff50d12057353ace447aa8007deceaa48641f63d35c/tiktoken-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:fc1c44cd37b43fc46bae593129164f4f281e82ea116b57a85aa81bda57eafc94", size = 876509, upload-time = "2026-05-15T04:50:30.026Z" }, + { url = "https://files.pythonhosted.org/packages/85/8e/144bde4e01df66b34bb865557c7cd754ed08b036217ebd79c9db5e9048a9/tiktoken-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:32ac870a806cfb260a02d0cb70426aef02e038297f8ad50df5040bb5af360791", size = 1034888, upload-time = "2026-05-15T04:50:31.579Z" }, + { url = "https://files.pythonhosted.org/packages/36/18/d4ac9d20956cdebca04841316660ed584c2fecdc2b81722a28bc7ad3b1e4/tiktoken-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d9980f11429ed2d737c463bb1fb78cf330caa026adf002f714aced7849a687b", size = 982970, upload-time = "2026-05-15T04:50:32.961Z" }, + { url = "https://files.pythonhosted.org/packages/74/ed/6bb8d05b9f731f749fee5c6f5ca63e981143c826a5985877330507bd13b7/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3f277ebea5edd7b8bf03c6f9431e1d67d517530115572b2dc1d465326e8f88c7", size = 1115741, upload-time = "2026-05-15T04:50:34.475Z" }, + { url = "https://files.pythonhosted.org/packages/34/de/2ca96b07a82d972b74fe4b46de055b79c904e45c7eab699354a0bfa697dc/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a116178fa7e1b4065bff05214360373a65cac22f965be7b3f73d00a0dbfe7649", size = 1136523, upload-time = "2026-05-15T04:50:35.782Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/9dafec002c2d4424378563cf4cf5c7fb93631d2a55013c8b87554ee4012c/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c397ddda233208345b01bd30f2fca79ff730e55731d0108a603f9bc57f6af3b", size = 1181954, upload-time = "2026-05-15T04:50:36.99Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d0/1f8578c45b2f24759b46f0b50d31878c63c73e6bf0f2227e10ec5c5408dc/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:95097e4f89b06403976e498abf61a0ee73a7497e73fb599cb211d8197a054d91", size = 1240069, upload-time = "2026-05-15T04:50:38.221Z" }, + { url = "https://files.pythonhosted.org/packages/aa/90/28d7f154888610aa9237e541986beb62b479df29d193a5a0617dbb1514d0/tiktoken-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:8f2d16e7a7c783ad81f36e457d046d1f1c8af70b22aec8a13238efe531977c41", size = 874748, upload-time = "2026-05-15T04:50:39.587Z" }, + { url = "https://files.pythonhosted.org/packages/9c/83/b096c859c2a47c11731bf2f5885f4028b809dfe2396582883eed9cae372f/tiktoken-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5df5d1507bd245f1ccad4a074698240021239e455eb0bb4ced4e3d7181872154", size = 1034228, upload-time = "2026-05-15T04:50:40.988Z" }, + { url = "https://files.pythonhosted.org/packages/53/61/c68e123b6d753e3fc2751e9b18e732c9d8bf1e1926762e736eee935d931c/tiktoken-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fe806a50664e83a6ffd56cbd1e4f5dcc6cd32a3e7538f70dc38b1a271384545", size = 982978, upload-time = "2026-05-15T04:50:42.195Z" }, + { url = "https://files.pythonhosted.org/packages/ef/8b/96cc178cc584e65d363134500f297790b06cd48cdeb1e8fcf7bbe60f4715/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:125bc05005e747f993a83dc67934249932d6e4209854452cd4c0b1d53fba3ba2", size = 1116355, upload-time = "2026-05-15T04:50:43.564Z" }, + { url = "https://files.pythonhosted.org/packages/86/f5/bab735d2c72ea55404b295d02d092644eb5f7cc6205e34d35eb9abfb9ab2/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5e6358911cab4adee6712da27d65573496a4f68cf8a2b5fca6a4ad10fc5748cf", size = 1135772, upload-time = "2026-05-15T04:50:44.782Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b9/6de04ebdf904edfaad87788011b3735087a0c9ea671b9027e1e4e965e8c8/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:975cbd78d085d75d26b59660e262736dcaed1e35f8f142cd6291025c01d25486", size = 1182415, upload-time = "2026-05-15T04:50:46.422Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9c/470a05f3b1caf038f44880e334d47ab674e0c80d514c66b375d14d5afa10/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ab9bc99fa020a4c283424590ecd7f3afd70c1c281cb3fa3192a6c3af9f9615", size = 1239879, upload-time = "2026-05-15T04:50:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/42/a6/c1936d16055436cb32e6c6128d68629622e00f4768562f55653752d34768/tiktoken-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:6b1615f0ff71953d19729ceb18865429c185b0a23c5353f1bbca34a394bf60f7", size = 874829, upload-time = "2026-05-15T04:50:49.202Z" }, + { url = "https://files.pythonhosted.org/packages/d6/07/acb5992c3772b5a36284f742cfb7a5895aa4471d1848ac31464ad50d7fdf/tiktoken-0.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6eb4a5bfbc6426938026b1a334e898ac53541360d62d8c689870160cc80abd67", size = 1033600, upload-time = "2026-05-15T04:50:50.4Z" }, + { url = "https://files.pythonhosted.org/packages/14/e9/742e9aec30f59b9f161f7ff7cd072e02ea836c9e1c0854a8076dfcd40d5c/tiktoken-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:43cee3e5400573b2046fbf092cc7a5bc30164f9e4c95ce20714da929df48737a", size = 982516, upload-time = "2026-05-15T04:50:52.03Z" }, + { url = "https://files.pythonhosted.org/packages/72/74/ca1541b053e7648254d2e4b42a253e1bb4359f2c91a0a8d49228c794e1a0/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:7de52e3f566d19b3b11bd37eea552c6c305ad74081f736882bd44d148ed4c48d", size = 1115518, upload-time = "2026-05-15T04:50:53.543Z" }, + { url = "https://files.pythonhosted.org/packages/46/e3/93825eaf5a4a504795b787e5d5dea07fbeb3dabf97aa7b450be8bde59c89/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:51384448aa508e4df84c0f7c1dc3211c7f7b8096325660ee5fc82f3e11b381ce", size = 1136867, upload-time = "2026-05-15T04:50:55.191Z" }, + { url = "https://files.pythonhosted.org/packages/8c/46/002b68de6827091d5ae90b048f326e8aad8d953520950e5ce1508879414f/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e28157350f7ebf35008dd8e9e0fdb621f976e4230c881099c85e8cf07eaa50e2", size = 1181826, upload-time = "2026-05-15T04:50:56.296Z" }, + { url = "https://files.pythonhosted.org/packages/db/c6/d393e3185a276505182f7abd93fe714f3c444a2be9180798fa052347504e/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:165cf1820ea4a354985c2490a5205d4cc74661c934aca79dd0368232fff94e0f", size = 1239489, upload-time = "2026-05-15T04:50:57.918Z" }, + { url = "https://files.pythonhosted.org/packages/b7/4d/bc07d1f1635d4897a202acc0ae11c2886eaa7325c359ba4741b47bf8e225/tiktoken-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6c43a675ca14f6f2749ba7f12075d37456015a24b859f2517b9beb4ef30807ec", size = 873820, upload-time = "2026-05-15T04:50:59.528Z" }, + { url = "https://files.pythonhosted.org/packages/8c/93/0dd6adca026a616c3a92974566b43381eea4b475ce1f36c062b8271a9ac5/tiktoken-0.13.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaaaef47c2406277181d2086484c317bf7fc433e2d5d03ff94f56b0dcec87471", size = 1034977, upload-time = "2026-05-15T04:51:00.957Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5ec6e6bc5b30bed6d93f7f2162d8f6b32437b3ba27cb527cfe004f6109c9/tiktoken-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ca8b310bd93b3772cb1b7922d915446864860f562bdfe4825c63a0aed3fb28cd", size = 983635, upload-time = "2026-05-15T04:51:02.629Z" }, + { url = "https://files.pythonhosted.org/packages/94/b0/c8ae9aff00d625c50659b4513e707a0462c4bf5d4d6cc1b802103225c02e/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:32e0c12305105002c047b3bb1070b0dd9a73b0cb3b2856a8972b810e7a4f5881", size = 1116036, upload-time = "2026-05-15T04:51:04.082Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ac/6a5dddd1d0a6018ecb389bd0353e6b4a515eb4d2286611bd0ace1937b9e1/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:5ba5fd62507a932d1241346179e3b39bc7bf7408f03c272652d93b3bedf5db24", size = 1135544, upload-time = "2026-05-15T04:51:05.229Z" }, + { url = "https://files.pythonhosted.org/packages/f4/b8/585032b4384b2f7dcdaddcb52865c83a701a420d09e3c2b4a2be1c450c57/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d108bc2d470fc53c8ecd24f2c0fd2b5f98c33e87cdb6aa2e9b8c5dced703d273", size = 1182217, upload-time = "2026-05-15T04:51:06.517Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b6/993ff1ded3958215fd341a847b8e5ffeb5de473f435296870d314fc91ac4/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cb99cb5127449f58d0a2d5f5ccfb390d8dbdfd919c221246caaee29d8725ed51", size = 1239404, upload-time = "2026-05-15T04:51:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3d/fef7e06e3b33e7538db0ced734cf9fe23b6832d2ac4990c119c377aec55e/tiktoken-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:115c4f26ffa11caac8b54eea35c2ad38c612c20a48d35dd15d70a02ac6f51f58", size = 918686, upload-time = "2026-05-15T04:51:08.925Z" }, + { url = "https://files.pythonhosted.org/packages/c1/82/a7fc44582bc32ab00de988a2299bf77c077f59068b233109e34b7d6ca7e6/tiktoken-0.13.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:472527e9132952f2fbf77cd290658bacf003d4d5a3fabc18e5fbd407cbae4d9b", size = 1034454, upload-time = "2026-05-15T04:51:10.035Z" }, + { url = "https://files.pythonhosted.org/packages/37/d0/24d8a890c14f432a05cea669c17bebeaa99f96a7c79523b590f564246411/tiktoken-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e2f67d27c9626cdd25fe33d9313c5cdb3d8d82da646b68d6eb8e7e9c20e6448", size = 982976, upload-time = "2026-05-15T04:51:11.23Z" }, + { url = "https://files.pythonhosted.org/packages/49/b7/2ab43f62788a9266187a9bfc1d3af99ad83e5eaa25fbef168a69cd5ad14f/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2b920b35805cd64585a37c3dc7ce65fba4d2d36016be01e1d7942482ca29093a", size = 1115526, upload-time = "2026-05-15T04:51:12.608Z" }, + { url = "https://files.pythonhosted.org/packages/64/39/1494321ed323ce7a14d88e3cd6cb9058625977df1c6961ddc492bd10a9f3/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:493af3aa28a4aaf2e3d2600a2ee717252c9bf5ab38fff94eb5a02db5ab77e5ad", size = 1136466, upload-time = "2026-05-15T04:51:13.926Z" }, + { url = "https://files.pythonhosted.org/packages/96/d9/dfd086aa2d918c563a140720e0ce296cada1634efd2783d5cf51e05f984e/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6644c9c2b5cf3916f5a3641d7d12fdb3f006a7b3d9ff6acdaec44e29ab1ff91e", size = 1181863, upload-time = "2026-05-15T04:51:15.025Z" }, + { url = "https://files.pythonhosted.org/packages/2f/68/a18b4f307086954fdae32714cb4f85562e34f9d34ab206e61f1816aa6018/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cb65b60b9408563676d874a3a4ee573370066f0dc4e29d84e82e989c6517424", size = 1239218, upload-time = "2026-05-15T04:51:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/16/5b/f2aa703a4fc5d2dff73460a7d46cc2f3f44aa0f3dd8eeb20d2a0ecf68862/tiktoken-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:85b78cc3a2c3d48723ca751fa981f1fedccd54194ca0471b957364353a898b07", size = 918110, upload-time = "2026-05-15T04:51:17.237Z" }, ] [[package]] name = "tokenizers" -version = "0.22.0" +version = "0.23.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/b4/c1ce3699e81977da2ace8b16d2badfd42b060e7d33d75c4ccdbf9dc920fa/tokenizers-0.22.0.tar.gz", hash = "sha256:2e33b98525be8453f355927f3cab312c36cd3e44f4d7e9e97da2fa94d0a49dcb", size = 362771, upload-time = "2025-08-29T10:25:33.914Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c1/60/21f715d9faba5f5407ff759472ade058ec4a507ad62bcea47cb847239a73/tokenizers-0.23.1.tar.gz", hash = "sha256:1feeeadf865a7915adc25445dea30e9933e593c31bb96c277cee36de227c8bfa", size = 365748, upload-time = "2026-04-27T14:43:25.606Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/b1/18c13648edabbe66baa85fe266a478a7931ddc0cd1ba618802eb7b8d9865/tokenizers-0.22.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:eaa9620122a3fb99b943f864af95ed14c8dfc0f47afa3b404ac8c16b3f2bb484", size = 3081954, upload-time = "2025-08-29T10:25:24.993Z" }, - { url = "https://files.pythonhosted.org/packages/c2/02/c3c454b641bd7c4f79e4464accfae9e7dfc913a777d2e561e168ae060362/tokenizers-0.22.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:71784b9ab5bf0ff3075bceeb198149d2c5e068549c0d18fe32d06ba0deb63f79", size = 2945644, upload-time = "2025-08-29T10:25:23.405Z" }, - { url = "https://files.pythonhosted.org/packages/55/02/d10185ba2fd8c2d111e124c9d92de398aee0264b35ce433f79fb8472f5d0/tokenizers-0.22.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec5b71f668a8076802b0241a42387d48289f25435b86b769ae1837cad4172a17", size = 3254764, upload-time = "2025-08-29T10:25:12.445Z" }, - { url = "https://files.pythonhosted.org/packages/13/89/17514bd7ef4bf5bfff58e2b131cec0f8d5cea2b1c8ffe1050a2c8de88dbb/tokenizers-0.22.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ea8562fa7498850d02a16178105b58803ea825b50dc9094d60549a7ed63654bb", size = 3161654, upload-time = "2025-08-29T10:25:15.493Z" }, - { url = "https://files.pythonhosted.org/packages/5a/d8/bac9f3a7ef6dcceec206e3857c3b61bb16c6b702ed7ae49585f5bd85c0ef/tokenizers-0.22.0-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4136e1558a9ef2e2f1de1555dcd573e1cbc4a320c1a06c4107a3d46dc8ac6e4b", size = 3511484, upload-time = "2025-08-29T10:25:20.477Z" }, - { url = "https://files.pythonhosted.org/packages/aa/27/9c9800eb6763683010a4851db4d1802d8cab9cec114c17056eccb4d4a6e0/tokenizers-0.22.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdf5954de3962a5fd9781dc12048d24a1a6f1f5df038c6e95db328cd22964206", size = 3712829, upload-time = "2025-08-29T10:25:17.154Z" }, - { url = "https://files.pythonhosted.org/packages/10/e3/b1726dbc1f03f757260fa21752e1921445b5bc350389a8314dd3338836db/tokenizers-0.22.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8337ca75d0731fc4860e6204cc24bb36a67d9736142aa06ed320943b50b1e7ed", size = 3408934, upload-time = "2025-08-29T10:25:18.76Z" }, - { url = "https://files.pythonhosted.org/packages/d4/61/aeab3402c26874b74bb67a7f2c4b569dde29b51032c5384db592e7b216f4/tokenizers-0.22.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a89264e26f63c449d8cded9061adea7b5de53ba2346fc7e87311f7e4117c1cc8", size = 3345585, upload-time = "2025-08-29T10:25:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/bc/d3/498b4a8a8764cce0900af1add0f176ff24f475d4413d55b760b8cdf00893/tokenizers-0.22.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:790bad50a1b59d4c21592f9c3cf5e5cf9c3c7ce7e1a23a739f13e01fb1be377a", size = 9322986, upload-time = "2025-08-29T10:25:26.607Z" }, - { url = "https://files.pythonhosted.org/packages/a2/62/92378eb1c2c565837ca3cb5f9569860d132ab9d195d7950c1ea2681dffd0/tokenizers-0.22.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:76cf6757c73a10ef10bf06fa937c0ec7393d90432f543f49adc8cab3fb6f26cb", size = 9276630, upload-time = "2025-08-29T10:25:28.349Z" }, - { url = "https://files.pythonhosted.org/packages/eb/f0/342d80457aa1cda7654327460f69db0d69405af1e4c453f4dc6ca7c4a76e/tokenizers-0.22.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:1626cb186e143720c62c6c6b5371e62bbc10af60481388c0da89bc903f37ea0c", size = 9547175, upload-time = "2025-08-29T10:25:29.989Z" }, - { url = "https://files.pythonhosted.org/packages/14/84/8aa9b4adfc4fbd09381e20a5bc6aa27040c9c09caa89988c01544e008d18/tokenizers-0.22.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:da589a61cbfea18ae267723d6b029b84598dc8ca78db9951d8f5beff72d8507c", size = 9692735, upload-time = "2025-08-29T10:25:32.089Z" }, - { url = "https://files.pythonhosted.org/packages/bf/24/83ee2b1dc76bfe05c3142e7d0ccdfe69f0ad2f1ebf6c726cea7f0874c0d0/tokenizers-0.22.0-cp39-abi3-win32.whl", hash = "sha256:dbf9d6851bddae3e046fedfb166f47743c1c7bd11c640f0691dd35ef0bcad3be", size = 2471915, upload-time = "2025-08-29T10:25:36.411Z" }, - { url = "https://files.pythonhosted.org/packages/d1/9b/0e0bf82214ee20231845b127aa4a8015936ad5a46779f30865d10e404167/tokenizers-0.22.0-cp39-abi3-win_amd64.whl", hash = "sha256:c78174859eeaee96021f248a56c801e36bfb6bd5b067f2e95aa82445ca324f00", size = 2680494, upload-time = "2025-08-29T10:25:35.14Z" }, + { url = "https://files.pythonhosted.org/packages/87/39/b87a87d5bb9470610b80a2d31df42fcffeaf35118b8b97952b2aff598cc7/tokenizers-0.23.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e03d6ffcbe0d56ee9c1ccd070e70a13fa750727c0277e138152acbc0252c2224", size = 3146732, upload-time = "2026-04-27T14:43:15.427Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6a/068ed9f6e444c9d7e9d55ce134181325700f3d7f30410721bdc8f848d727/tokenizers-0.23.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0948bbb1ac1d7cdfc9fb6d62c596e3b7550036ad60ecd654a66ad273326324e", size = 3054954, upload-time = "2026-04-27T14:43:13.745Z" }, + { url = "https://files.pythonhosted.org/packages/6c/36/e006edf031154cba92b8416057d92c3abe3635e4c4b0aa0b5b9bb39dde70/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bf13402aff9bc533c89cb849ec3b412dc3fbeacc9744840e423d7bf3f7dc0e3", size = 3374081, upload-time = "2026-04-27T14:43:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ef/7735d226f9c7f874a6bee5e3f27fb25ecabdf207d37b8cf45286d0795893/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f836ca703b89ae07919a309f9651f7a88fd5a33d5f718ba5ad0870ec0256bad6", size = 3247641, upload-time = "2026-04-27T14:43:03.856Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d9/24827036f6e21297bfffda0768e58eb6096a4f411e932964a01707857931/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae848657742035523fdf261773630cb819a26995fcd3d9ecae0c1daf6e5a4959", size = 3585624, upload-time = "2026-04-27T14:43:10.664Z" }, + { url = "https://files.pythonhosted.org/packages/0c/9a/22f3582b3a4f49358293a5206e25317621ee4526bfe9cdaa0f07a12e770e/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53b09e85775d5187941e7bab30e941b4134ab4a7dd8c68e783d231fb7ca27c51", size = 3844062, upload-time = "2026-04-27T14:43:05.643Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/b8f8814eef95800f20721384136d9a1d22241d50b2874357cb70542c392f/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea5a0ce170074329faaa8ea3f6400ecde604b6678192688533af80980daae71a", size = 3460098, upload-time = "2026-04-27T14:43:08.854Z" }, + { url = "https://files.pythonhosted.org/packages/0d/d5/1353e5f677ec27c2494fb6a6725e82d56c985f53e90ec511369e7e4f02c6/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b405006415ea148a992d093699c66eb01952bf59f4d5727089a98bda45a4", size = 3346235, upload-time = "2026-04-27T14:43:12.377Z" }, + { url = "https://files.pythonhosted.org/packages/71/89/39b6b8fc073fb6d413d0147aa333dc7eff7be65639ac9d19930a0b21bf33/tokenizers-0.23.1-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:56f3a77de629917652f876294dc9fe6bad4a0c43bc229dc72e59bb23a0f4729a", size = 3426398, upload-time = "2026-04-27T14:43:07.264Z" }, + { url = "https://files.pythonhosted.org/packages/0f/80/127c854da64827e5b79264ce524993a90dddcb320e5cd42412c5c02f9e8a/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9d10a6d957ef01896dc274e890eee27d41bd0e74ef31e60616f0fc311345184e", size = 9823279, upload-time = "2026-04-27T14:43:17.222Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ba/44c2502feb1a058f096ddfb4e0996ef3225a01a388e1a9b094e91689fe93/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1974288a609c343774f1b897c8b482c791ab17b75ab5c8c2b1737565c1d82288", size = 9644986, upload-time = "2026-04-27T14:43:19.45Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c1/464019a9fb059870bfe4eebb4ba12208f3042035e258bf5e782906bd3847/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:120468fb4c24faf0543c835a4fabafa4deb3f20a035c9b6e83d0b553a97615d4", size = 9976181, upload-time = "2026-04-27T14:43:21.463Z" }, + { url = "https://files.pythonhosted.org/packages/79/94/3ac1432bda31626071e9b6a12709b97ae05131c804b94c8f3ac622c5da32/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e3d8f40ea6268047de7046906326abed5134f27d4e8447b23763afe5808c8a96", size = 10113853, upload-time = "2026-04-27T14:43:23.617Z" }, + { url = "https://files.pythonhosted.org/packages/6a/dd/631b21433c771b1382535326f0eca80b9c9cee2e64961dd993bc9ac4669e/tokenizers-0.23.1-cp310-abi3-win32.whl", hash = "sha256:93120a930b919416da7cd10a2f606ac9919cc69cacae7980fa2140e277660948", size = 2536263, upload-time = "2026-04-27T14:43:29.888Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/2553f72aaf65a2797d4229e37fa7fbe38ffbf3e32912d31bdd78b3323e59/tokenizers-0.23.1-cp310-abi3-win_amd64.whl", hash = "sha256:e7bfaf995c1bdbbd21d13539decb6650967013759318627d85daeb7881af16b7", size = 2798223, upload-time = "2026-04-27T14:43:28.51Z" }, + { url = "https://files.pythonhosted.org/packages/cd/2b/2be299bab55fc595e3d38567edb1a87f86e594842968fa9515a07bdcf422/tokenizers-0.23.1-cp310-abi3-win_arm64.whl", hash = "sha256:a26197957d8e4425dfba746315f3c425ea00cfa8367c5fbc4ec73447893dcea9", size = 2664127, upload-time = "2026-04-27T14:43:26.949Z" }, ] [[package]] @@ -2959,53 +4990,68 @@ wheels = [ [[package]] name = "tomli" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, - { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, - { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, - { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, - { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, - { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, - { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, - { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, - { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, - { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, - { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, - { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, - { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, - { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, - { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, - { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, - { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, - { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, - { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, - { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, - { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, - { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, - { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, - { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, - { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, - { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, - { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, - { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, - { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] [[package]] name = "tqdm" -version = "4.67.1" +version = "4.68.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/5f/57ff8b434839e70dab45601284ea413e947a63799891b7553e5960a793a8/tqdm-4.68.4.tar.gz", hash = "sha256:19829c9673638f2a0b8617da4cdcb927e831cd88bcfcb6e78d42a4d1af131520", size = 792418, upload-time = "2026-07-07T09:58:18.369Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/5e5e750890ada51017d18d0d4c30da696e5b5bd3180947729927628fc3cb/tqdm-4.68.4-py3-none-any.whl", hash = "sha256:5168118b2368f48c561afda8020fd79195b1bdb0bdf8086b88442c267a315dc2", size = 676612, upload-time = "2026-07-07T09:58:16.256Z" }, ] [[package]] @@ -3030,7 +5076,7 @@ wheels = [ [[package]] name = "twisted" -version = "25.5.0" +version = "26.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -3041,238 +5087,867 @@ dependencies = [ { name = "typing-extensions" }, { name = "zope-interface" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/13/0f/82716ed849bf7ea4984c21385597c949944f0f9b428b5710f79d0afc084d/twisted-25.5.0.tar.gz", hash = "sha256:1deb272358cb6be1e3e8fc6f9c8b36f78eb0fa7c2233d2dbe11ec6fee04ea316", size = 3545725, upload-time = "2025-06-07T09:52:24.858Z" } +sdist = { url = "https://files.pythonhosted.org/packages/db/97/6e9beb1e78247ae6dc34114f27d538cf2cb183c4afcd3609dfdf2b0439c8/twisted-26.4.0.tar.gz", hash = "sha256:dbfd0fe1ee409d0243fdd7a6a6ff14f4948cec1fd78e0376291f805e1501fae9", size = 3575095, upload-time = "2026-05-11T11:24:51.861Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/66/ab7efd8941f0bc7b2bd555b0f0471bff77df4c88e0cc31120c82737fec77/twisted-25.5.0-py3-none-any.whl", hash = "sha256:8559f654d01a54a8c3efe66d533d43f383531ebf8d81d9f9ab4769d91ca15df7", size = 3204767, upload-time = "2025-06-07T09:52:21.428Z" }, + { url = "https://files.pythonhosted.org/packages/a6/57/bcf4e2370dd218c9aa68a9140a65d86729c73f1d529f7e94786c2766fc72/twisted-26.4.0-py3-none-any.whl", hash = "sha256:dc25ea0ebf6511c24f03232ee9f4afa54b291c5d897990e3a39cc4d14a1ef4c0", size = 3230362, upload-time = "2026-05-11T11:24:49.5Z" }, ] [[package]] -name = "types-protobuf" -version = "6.30.2.20250822" +name = "types-aioboto3" +version = "15.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore-stubs" }, + { name = "types-aiobotocore" }, + { name = "types-s3transfer" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/76/e162ea2ef8d414d4f36f28a6e0b6078ccef3f2f9d5f957859f303995c528/types_aioboto3-15.5.0.tar.gz", hash = "sha256:5769a1c3df7ca1abedf3656ddf0b970c9b0436d0f88cf4686040b55cd2a02925", size = 81059, upload-time = "2025-10-31T01:11:54.445Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/1d/e187fbe9771dffb5f0801e315ac23a6c383c14d1cbb90da6ca3ad1ea9b06/types_aioboto3-15.5.0-py3-none-any.whl", hash = "sha256:8aed7c9b6fe9b59e6ce74f7a6db7b8a9912a34c8f80ed639fac1fa59d6b20aa1", size = 42521, upload-time = "2025-10-31T01:11:47.832Z" }, +] + +[package.optional-dependencies] +s3 = [ + { name = "types-aiobotocore-s3" }, +] + +[[package]] +name = "types-aiobotocore" +version = "3.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/61/68/0c7144be5c6dc16538e79458839fc914ea494481c7e64566de4ecc0c3682/types_protobuf-6.30.2.20250822.tar.gz", hash = "sha256:faacbbe87bd8cba4472361c0bd86f49296bd36f7761e25d8ada4f64767c1bde9", size = 62379, upload-time = "2025-08-22T03:01:56.572Z" } +dependencies = [ + { name = "botocore-stubs" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/e8/ef1fcb876937dbdddc0f01b5df4ed53f33b166a6367d80a9014d5e5f091d/types_aiobotocore-3.7.0.tar.gz", hash = "sha256:fe35de52c12e5fdb89ca60b3989766e7fe827e3d2e95fcf4583e91581945205c", size = 87992, upload-time = "2026-05-10T03:19:32.353Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/52/64/b926a6355993f712d7828772e42b9ae942f2d306d25072329805c374e729/types_protobuf-6.30.2.20250822-py3-none-any.whl", hash = "sha256:5584c39f7e36104b5f8bdfd31815fa1d5b7b3455a79ddddc097b62320f4b1841", size = 76523, upload-time = "2025-08-22T03:01:55.157Z" }, + { url = "https://files.pythonhosted.org/packages/f9/68/0cdfd7df415ee3e769c8e8f9bd8013c64c88cdd7306f72453a02123c58f9/types_aiobotocore-3.7.0-py3-none-any.whl", hash = "sha256:ff4139b3eae22d242b6b39ba56048344b2b86f67daeeca4680da1a6e191681fd", size = 54804, upload-time = "2026-05-10T03:19:29.487Z" }, ] [[package]] -name = "types-requests" -version = "2.32.4.20250809" +name = "types-aiobotocore-s3" +version = "2.25.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "urllib3" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/c6/9bb91a44eed1114690edb15d8251f32392e355dfa0a5b8e1c190b4cf89a4/types_aiobotocore_s3-2.25.2.tar.gz", hash = "sha256:678aa425491af19bd6d011d59ecdbbb7ae7e95800efddcf4fd559ab72c94e194", size = 75955, upload-time = "2025-11-12T01:52:06.536Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/0a/d0d9faefd7caa8536eb97647c38c711e73ab83341a65119d08c2cb20957d/types_aiobotocore_s3-2.25.2-py3-none-any.whl", hash = "sha256:151301e84bb2f1cbf30f0d1ef791bb75c141cfbfe47b93fd317b7f1ba3eb35e4", size = 83626, upload-time = "2025-11-12T01:52:04.763Z" }, +] + +[[package]] +name = "types-awscrt" +version = "0.34.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/59/44409a8fc06b444ab1a6f71dcb29d49a6e17e02424345eb51b051bebb345/types_awscrt-0.34.1.tar.gz", hash = "sha256:559aa04250f6a419a617dfb788f3e10903aaf74700ef23e521b64a411b83b803", size = 19062, upload-time = "2026-06-05T04:40:10.689Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/b1/214b12162b452ed6acd230065e6c587cde6b96871e3ce6d653f40888f8df/types_awscrt-0.34.1-py3-none-any.whl", hash = "sha256:20c752b6031544d8f694803c35174aee129f1be5ddf886ae46d22f7ffd9b7d75", size = 45688, upload-time = "2026-06-05T04:40:09.198Z" }, +] + +[[package]] +name = "types-protobuf" +version = "7.34.1.20260518" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/59/e2b13b499d15e6720150c4b1a8d91e31fcacf716b432397475b3151ff7e4/types_protobuf-7.34.1.20260518.tar.gz", hash = "sha256:28cfaded25889cb83ebfb63cfb0a43628f0b6f3785767bec17287dc6468795f2", size = 68936, upload-time = "2026-05-18T06:01:47.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/1f/ec5caf72c2e3b688ca3927e0979a04ddad19e1afc4bf1c199bd743e0f419/types_protobuf-7.34.1.20260518-py3-none-any.whl", hash = "sha256:a0a5337413347166439c0e07cbc26c6164d091401c6f01b1dfd8cdb966c4dd8f", size = 85992, upload-time = "2026-05-18T06:01:45.696Z" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ed/b0/9355adb86ec84d057fea765e4c49cce592aaf3d5117ce5609a95a7fc3dac/types_requests-2.32.4.20250809.tar.gz", hash = "sha256:d8060de1c8ee599311f56ff58010fb4902f462a1470802cf9f6ed27bc46c4df3", size = 23027, upload-time = "2025-08-09T03:17:10.664Z" } + +[[package]] +name = "types-s3transfer" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/64/42689150509eb3e6e82b33ee3d89045de1592488842ddf23c56957786d05/types_s3transfer-0.16.0.tar.gz", hash = "sha256:b4636472024c5e2b62278c5b759661efeb52a81851cde5f092f24100b1ecb443", size = 13557, upload-time = "2025-12-08T08:13:09.928Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/6f/ec0012be842b1d888d46884ac5558fd62aeae1f0ec4f7a581433d890d4b5/types_requests-2.32.4.20250809-py3-none-any.whl", hash = "sha256:f73d1832fb519ece02c85b1f09d5f0dd3108938e7d47e7f94bbfa18a6782b163", size = 20644, upload-time = "2025-08-09T03:17:09.716Z" }, + { url = "https://files.pythonhosted.org/packages/98/27/e88220fe6274eccd3bdf95d9382918716d312f6f6cef6a46332d1ee2feff/types_s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:1c0cd111ecf6e21437cb410f5cddb631bfb2263b77ad973e79b9c6d0cb24e0ef", size = 19247, upload-time = "2025-12-08T08:13:08.426Z" }, ] [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.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" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] [[package]] name = "typing-inspection" -version = "0.4.1" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +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/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, + { 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" +name = "tzdata" +version = "2026.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, +] + +[[package]] +name = "tzlocal" +version = "5.4.4" 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" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/5b/879b2f932adfa7a053c360d50bc896c977fa6426109185f7c12ebdd0cb9d/tzlocal-5.4.4.tar.gz", hash = "sha256:8dbb8660838688a7b6ba4fed31d18dedf842afb4d47ca050d6d891c2c15f3be4", size = 31170, upload-time = "2026-06-29T08:03:40.026Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/9e/a4/017a7a6cbe387d961a688ec31364ae60a5c4e22c96ae9921b79a947c855d/tzlocal-5.4.4-py3-none-any.whl", hash = "sha256:aae09f0126a8a86fa736be266eb4a471380d26a0de3bc14844e7821fee3e2a15", size = 18115, upload-time = "2026-06-29T08:03:38.666Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "uuid-utils" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/91/63938e0e7e7876658e5e40178e7c0735b53527886fe11797a11699c55edd/uuid_utils-0.17.0.tar.gz", hash = "sha256:abb5667a36119019b3fa320c4d10c21ebccfcc87c8a739e6a0056cee7f48dde2", size = 43220, upload-time = "2026-07-09T13:49:58.433Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/60/659104207938f2ac62508b9aa595fc0515ac7452dd515c8e1d47d0b91169/uuid_utils-0.17.0-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d2d9a63a9e6f2416ace8c109043a9280d6b34f34bb2e5421903e149403db40a6", size = 564038, upload-time = "2026-07-09T13:47:51.731Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e7/e0d048a268b4163058bdd2f07a45bbe13c29e3cc6b7b88f8f00b001617ce/uuid_utils-0.17.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b776c7fc8755c7de06dd5a22b47c40ae84f67d13277ebb233cc84933ba4dcbcd", size = 286680, upload-time = "2026-07-09T13:47:53.141Z" }, + { url = "https://files.pythonhosted.org/packages/84/83/e3606dc9b4224d0c9a6675d9347e7e0da7e67fa30e061bfdb686138844d0/uuid_utils-0.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1edf2f8732e4ed95bd7b65f2658f4aa072efaaff321144f4e0d4bf6a22709263", size = 323533, upload-time = "2026-07-09T13:47:54.433Z" }, + { url = "https://files.pythonhosted.org/packages/22/f8/aec5c34fa80c9fef09a506a098015e728080076494b72b9e8e5cfc9669c4/uuid_utils-0.17.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:84ed3a2d5cd3ae6db87af20bfed3331116195ba4757ad7177fc8f12c1bbce2a9", size = 330691, upload-time = "2026-07-09T13:47:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/08/73/85776566863514f37b0a761648368e96b07d64981a9b6c391220aa2563a9/uuid_utils-0.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bf4d9cd1e80e73922073b9b27c143bedeb109d65f94cd12712e2c87118f2b7d", size = 444094, upload-time = "2026-07-09T13:47:56.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/06/e0424b4268c0932e0ff8257303d70de4053f05958843268fac4cb0f79b57/uuid_utils-0.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52db0e471d3d2632d35445af352591f40a8f32959a412981d9f51e068bb9514b", size = 324548, upload-time = "2026-07-09T13:47:58.217Z" }, + { url = "https://files.pythonhosted.org/packages/db/d2/a0cb3a69ef6d9becc30a6a0594ddf6f798f6204953dfa85073cbec875b94/uuid_utils-0.17.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:344f7c755e280ea0ba6aeb08022190d867a80000b1715cacded54fc4b5633607", size = 350307, upload-time = "2026-07-09T13:47:59.418Z" }, + { url = "https://files.pythonhosted.org/packages/82/81/d82766af7db541e4a78b920bc1c4303d44995f841805d1498934088cd12c/uuid_utils-0.17.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:589d9da7de8fa7f739bb970ac4632c9a268213117d634e1c4a58c1c1e821ca05", size = 500661, upload-time = "2026-07-09T13:48:00.726Z" }, + { url = "https://files.pythonhosted.org/packages/10/71/b261cd0d38497ed8c2cce0263c5607ec9cd2bbace0f73cb19a6fc2060b6e/uuid_utils-0.17.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cee808b405e9095506f4e4e89924bec7ea77eac3129b6fe36eda04364b3b343b", size = 606577, upload-time = "2026-07-09T13:48:02.539Z" }, + { url = "https://files.pythonhosted.org/packages/3b/63/9e48512bb235e9533adbb25c30fd0c9cef09f6ecefe131ba392b98572b40/uuid_utils-0.17.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:53ce348ef4c6e98c02c19c522af01334fe94476ce9af0db8c4482f9f142ae9c1", size = 567054, upload-time = "2026-07-09T13:48:03.833Z" }, + { url = "https://files.pythonhosted.org/packages/b2/cc/d7bad8799a37ec33fc21b29fcb459d63d9f88aa09056d0c3e58903ba2fb0/uuid_utils-0.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9e753e81457241e2200c56a898e268e8fa25796271af0489c608f24d8e631eed", size = 529682, upload-time = "2026-07-09T13:48:05.097Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3b/59b1e07ada8aadd3c046c97fe9814d85e770abb7e8cf68d5d86538bf62e9/uuid_utils-0.17.0-cp310-cp310-win32.whl", hash = "sha256:c589f5023d471ce75dd2cce61acb25ed6347e562041588a1a366808f22d7176c", size = 170595, upload-time = "2026-07-09T13:48:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5c/23a2d0253ada2ee8c497d541d4ef0dd5576c3d2454ec2f9d0b8a06af9304/uuid_utils-0.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:981cc10163988defea96e8d6c507df151eab8f483e7df9ae543d5a41a4be073b", size = 177225, upload-time = "2026-07-09T13:48:07.561Z" }, + { url = "https://files.pythonhosted.org/packages/d7/b2/8f03b61f0aa4afc687855c4f00db35f4d3e58c480cd885abc46f6e41308f/uuid_utils-0.17.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f9b093cb3b6c9d6233ef45a05cab064d2aa0a8cb3c5777084c9e20fcb77c2371", size = 563901, upload-time = "2026-07-09T13:48:08.961Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cb/88b909ffb9ac11f88d2e6ceabc592ccc660b5830b06dbcbd290ab8981f1f/uuid_utils-0.17.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0bc4c431ccd59c764080ceb43b126043325fe17861b87759d026a0cdd8423bb2", size = 286383, upload-time = "2026-07-09T13:48:10.2Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b8/bc5b64e9898867227c535cd0366c571c580a736748e81329437c1773e442/uuid_utils-0.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c00d182e31034250690f417b9068b78eab423c10d76766664e82d9860c340479", size = 323244, upload-time = "2026-07-09T13:48:11.477Z" }, + { url = "https://files.pythonhosted.org/packages/13/d9/8a17462ce066fbf89670fb737a3f0c93a77816736d2a4d134787e759d8ea/uuid_utils-0.17.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:570db214f6d8507587a8faa968a3fe65e957daeb7bc48b27dc7f69bc3ecdd6f1", size = 330466, upload-time = "2026-07-09T13:48:13.092Z" }, + { url = "https://files.pythonhosted.org/packages/43/37/0c65d0db3bae45183419756d938f1791a82c835fd92bf234eb4f008d2e02/uuid_utils-0.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:351462debd866f1f25e4d4f5c7fac89525b52151f0102a1bdfe94a999b046f5f", size = 443806, upload-time = "2026-07-09T13:48:14.372Z" }, + { url = "https://files.pythonhosted.org/packages/32/d5/7e698466d1f5254620b5ee0d711fdd20a0e9c2acd7040740c37193a8f673/uuid_utils-0.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:622cdde768300591ac79bfcd7bb3468e4b191b1105d5dbfe8d87c39d8f63dd46", size = 324261, upload-time = "2026-07-09T13:48:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/5d/48/3a5b242d7f0b8e3ca77dcd7177f3cf73e0280cee32e2349d9796ca27f183/uuid_utils-0.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:75d7411e8eb9259764dd60310738540649057cda4509b4af14b36b7f663bfeb0", size = 350657, upload-time = "2026-07-09T13:48:17.273Z" }, + { url = "https://files.pythonhosted.org/packages/95/f4/f32ea82a89efed2eafee2f1d925d64687a81e550a9951933fb1b75c95ca6/uuid_utils-0.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1019476b6bdc047216ef7414be5babe0fa5ccfde977c0cac4fd6c75ddec66ff7", size = 500613, upload-time = "2026-07-09T13:48:18.459Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5c/c7b73ec4bbe28db162a4841d352c6eda582801e0dd9fe72f6ad5cc584ee4/uuid_utils-0.17.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:04452640d8b6920c480c16e5afe91ff896d236e0c972830f9247e0898d38c803", size = 606306, upload-time = "2026-07-09T13:48:19.726Z" }, + { url = "https://files.pythonhosted.org/packages/63/95/8a2777204e8691b4961e6aa619001c3e5175aa430ab43da3079142e8d310/uuid_utils-0.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:793229621e1ad6cac55f015cfa9f4eff102accbc3da25d607b91c6b0bec167fb", size = 567231, upload-time = "2026-07-09T13:48:21.024Z" }, + { url = "https://files.pythonhosted.org/packages/1a/6f/1d778ca3ed6d2cf35f22088e2de714675416747ab41be510f22c141043a7/uuid_utils-0.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03815cea572c8a693cab5475b9d750cc161470961c7defa27e9286cad62f38f5", size = 529373, upload-time = "2026-07-09T13:48:22.312Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d3/9ad1ab64b3bed0a0237d1db89dc6f5001d6116a82766753da4ac4496f979/uuid_utils-0.17.0-cp311-cp311-win32.whl", hash = "sha256:c4f845166b09acc65c5213a35551a7f81c17fa010ab467229b5813f79d17fe13", size = 169930, upload-time = "2026-07-09T13:48:23.504Z" }, + { url = "https://files.pythonhosted.org/packages/c2/1a/e01417f52eae6e2cb412260bb332b4ee4b37af2982d9c38cff4b68b2e899/uuid_utils-0.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:14dc2f46abb1091260c0d203fcbdf4e045042cc07e49183fd3b255904b95eb70", size = 177242, upload-time = "2026-07-09T13:48:24.723Z" }, + { url = "https://files.pythonhosted.org/packages/35/20/396c27f996add19f8ac31e49cc4570824e51a97719087dabf94694d25bc4/uuid_utils-0.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:29179ffb7b317239b6d6afb100d14c439c728770460718280b9c0a42d2561ec2", size = 177023, upload-time = "2026-07-09T13:48:25.834Z" }, + { url = "https://files.pythonhosted.org/packages/20/80/a7e685968e3cec99d6fe2fb25d0f5726310e1bba356da68c13dfd8b7d140/uuid_utils-0.17.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:9205068badf453d2f0821fd5d340389b4679992d7ff79d4f3e5608996dd1b287", size = 556403, upload-time = "2026-07-09T13:48:27.022Z" }, + { url = "https://files.pythonhosted.org/packages/56/47/3102d93bcb7b0bfe6bede63ff8f221a7f91348e10a37f682773be27c56d9/uuid_utils-0.17.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0fcca4e838af9ac9243b3358d7c14afa4dca286a87781124c272d6c4cad9c968", size = 285608, upload-time = "2026-07-09T13:48:28.769Z" }, + { url = "https://files.pythonhosted.org/packages/55/fb/d59695f0f8db065b93c63316eaafa05a22d75a0486978a33736c52c646d5/uuid_utils-0.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f3729e839209f3457d0d8b6a35a376fdf65577a5aecaf4cc3587d3305759ba6", size = 319926, upload-time = "2026-07-09T13:48:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/5a/03/62fabcd1e990e07a0e220e8d552af45bc16f107fa8e55c2014a706bb1a1e/uuid_utils-0.17.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3dac0ad0cd9a2818d1775215365a4e8c2f8ada215529dd26f3f8cceeb67a6988", size = 327172, upload-time = "2026-07-09T13:48:31.187Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/a5081391338b459e2f8d8b12581f00f8caa6317fab510e0e85c18c59e938/uuid_utils-0.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e671b2322ef09106ecb1ca0f4c398b134d5e2c1f80d7a4f3336847a3072c0e94", size = 439075, upload-time = "2026-07-09T13:48:32.295Z" }, + { url = "https://files.pythonhosted.org/packages/59/30/91795bd01e17a13661280d4899fbf38fb05e3f38e873f9aaec106ec30aa0/uuid_utils-0.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8eb3e5caca8d3a6f72ea4cce024583f989f6f2e9186f98800213fff0176e8bcc", size = 320247, upload-time = "2026-07-09T13:48:33.64Z" }, + { url = "https://files.pythonhosted.org/packages/e5/11/09102b78303e4eb62069d6d88ef9fd661dc523e8f429e1fd67eaa78a6f44/uuid_utils-0.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b72c2002202038666bf647f9a790906214c7c11cd0d6efef77b7d07bef3034a", size = 344738, upload-time = "2026-07-09T13:48:34.786Z" }, + { url = "https://files.pythonhosted.org/packages/74/f9/be95bad6954b60328878c3800258f01a6accd24fd75112d13f023462d53f/uuid_utils-0.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e2ac1c0b56f2c91b6f158e29ed96b1503223fe8aa6e79b1be1dc55bd8a5131c", size = 496845, upload-time = "2026-07-09T13:48:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/2d/02/8a19a34e0530d987488a068a71576a236f5c8c746630b870b57f71eb24ef/uuid_utils-0.17.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6c142bd0cb4dba31c10babe00d59f7ef6460f0ef55eaa9c1a9da270684af996a", size = 603233, upload-time = "2026-07-09T13:48:37.512Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a8/b1abab36ff73b0248d82179816467f6d39a2e80fd64329a895ca94f3508e/uuid_utils-0.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e252db239eb41c32248e096e0d170bce5896a4fd3405556362bc3dd83d912206", size = 561401, upload-time = "2026-07-09T13:48:38.977Z" }, + { url = "https://files.pythonhosted.org/packages/61/91/70e7b528b351cc03a9ca43e6116371cdde31bb12bcead7ca2ca1367366cc/uuid_utils-0.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:237722b6581bb5b4eb4cefbcbe5c6e2980a440aabe781fbe50ebf1cb71eee4cc", size = 525314, upload-time = "2026-07-09T13:48:40.599Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f6/9167e90cf9937d6558f92d022ff3024a69d938a514d9c8faa4080f73b001/uuid_utils-0.17.0-cp312-cp312-win32.whl", hash = "sha256:46a73cacdf512f473a81f65dbf84186e08cfe6e9118fa582b6c6b33a8288a30d", size = 166831, upload-time = "2026-07-09T13:48:41.862Z" }, + { url = "https://files.pythonhosted.org/packages/5c/7d/0b889654d9ee3413f810cf4685e241285f650d98a4103ac9f3c6bcc95f29/uuid_utils-0.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:e59b60a0a4cb7541480e02090d37dc2df3b72df4c2e776fff64ce3a4e3dd4637", size = 172944, upload-time = "2026-07-09T13:48:42.992Z" }, + { url = "https://files.pythonhosted.org/packages/be/35/8c6e1bf65e4d400352885dadc656ad6d0af96e89231e3f04686bc2197128/uuid_utils-0.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:d561a4c5747a1e6c7fa7c49a0292e78b4e8c456332caa084fc7abad8de828652", size = 172459, upload-time = "2026-07-09T13:48:44.271Z" }, + { url = "https://files.pythonhosted.org/packages/d2/dd/614fb9912157ac0128e6050859ccf06d9f13df9a944a803e8f80f6157e38/uuid_utils-0.17.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d11a7bc1e02da8984d32e6de9e0826c6edac00eac17de270f372bf32f9a0af63", size = 557259, upload-time = "2026-07-09T13:48:45.664Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/d072711704de3d21bec08b6c2f36a215200ca1d5e01a390ea1ac434080a0/uuid_utils-0.17.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7a49f47ac26df3e431c56b825c1bae8e6d3d591fdbb7438c227cc9845a7e3d73", size = 286271, upload-time = "2026-07-09T13:48:47.018Z" }, + { url = "https://files.pythonhosted.org/packages/18/6d/8a63e5eb2d5a6ba69a6c2036e305075bd6f5a022e7ea25fc6ce0eb7c51d2/uuid_utils-0.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32df1944808877702ceea398c103881c09a679bb672a215e01c2a84231266bf9", size = 320025, upload-time = "2026-07-09T13:48:48.208Z" }, + { url = "https://files.pythonhosted.org/packages/f7/2d/bdc2caf9719d9090d7c46043242ae6136cba4f7a7ee384992ab905ad9aa1/uuid_utils-0.17.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:98c88d3edd08e7245562e9815996dbc6f0bd4745e1c76462f24af5ae4e187dd1", size = 327931, upload-time = "2026-07-09T13:48:49.673Z" }, + { url = "https://files.pythonhosted.org/packages/b6/33/9219d09d51ead282b578b2a4e0a515c2cce3ec52076cada8bfb7e35727d5/uuid_utils-0.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a4370089c8b2e42f1db51d76408c7fa8eaa2934bf854d17983d16179c07c098", size = 438537, upload-time = "2026-07-09T13:48:50.842Z" }, + { url = "https://files.pythonhosted.org/packages/d8/79/e8e0f8b3955f2081c116157119d87659937893242eb834aa170da04d660b/uuid_utils-0.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09a55b7a5ae764985cb46467496a1787678d0a1400356157a080ad95b1a36869", size = 320656, upload-time = "2026-07-09T13:48:52.164Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5e/d1ceddc430ff04b6e21704b2030d4438074a2f478b265dab43da957791c1/uuid_utils-0.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56aa6488b931246fae11924e4bd0e2b32677e63945eecb71c29e3c2ca0dc3131", size = 345310, upload-time = "2026-07-09T13:48:54.076Z" }, + { url = "https://files.pythonhosted.org/packages/d5/62/89438e12f389a843e626b7e37691319a057b3d6b80914609106891faadda/uuid_utils-0.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:309a35f12d99dde19032bc2259cda6431c85eeac0879134dc777cc3087d7e1cb", size = 496771, upload-time = "2026-07-09T13:48:55.365Z" }, + { url = "https://files.pythonhosted.org/packages/87/d2/eedcd99f522d60e238ead03844f0d51743ba84d33044959e230b756bf212/uuid_utils-0.17.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:21c79b61ff750abcf057163dd764ccb6196cde7a26cda1b31b45cd97769e03b3", size = 603631, upload-time = "2026-07-09T13:48:56.746Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a8/bb1b38aaddd7243b6e562c6694f499bf094800918316192fd8cb2cdc2620/uuid_utils-0.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4134353bfe3026ddab8e886002dc52bc5a0ab04611aabb0eaae23c32e6e57f64", size = 562008, upload-time = "2026-07-09T13:48:58.241Z" }, + { url = "https://files.pythonhosted.org/packages/b4/77/5f7ed930dc105e293845c09e4d5bd84076318a12f45a46783e1af64906d7/uuid_utils-0.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7c89359affecebe2e39e6a116d069b363c936511a9572b308402489a26957d89", size = 525527, upload-time = "2026-07-09T13:48:59.784Z" }, + { url = "https://files.pythonhosted.org/packages/fd/25/1b55697adf6811a6f92cff6340e6b03e31fd6bc51066a5c10698c29b3679/uuid_utils-0.17.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:6a019a31bc4db89a0903a3e4f6b218571f3a6ff0ad4b3d3fe1c8f91a05ff6e3e", size = 97965, upload-time = "2026-07-09T13:49:01.217Z" }, + { url = "https://files.pythonhosted.org/packages/26/bf/cd729343de4684230be8a966bad7bfc2cf10ce3e643b1189a8b5370dbe35/uuid_utils-0.17.0-cp313-cp313-win32.whl", hash = "sha256:b3131a82d0c7611f0aa480a6d36929e001a3f54ba0fc029a8118a5863cce513c", size = 167316, upload-time = "2026-07-09T13:49:02.354Z" }, + { url = "https://files.pythonhosted.org/packages/76/f0/e602ae0a1b139a7826e5189b93d91902564def06d5006324fd2faf82c8fc/uuid_utils-0.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:9e311f908d2f842fca4c7dcebc4f10306b8089b204ef04cf6704b4332c9ff6ff", size = 173630, upload-time = "2026-07-09T13:49:03.529Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/024ebece265b387154115dc4f1d9727174ef82623069f4bec8b7ed7e73f7/uuid_utils-0.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:c351737e2e65497c7200ab4ffb8af97e9f48be6488309abdd265fe08d66ee92f", size = 173214, upload-time = "2026-07-09T13:49:04.836Z" }, + { url = "https://files.pythonhosted.org/packages/56/44/e2fd3fdf356e1b55d2acf1b956b4f3f29ffb215a99c387eba04b1c5fba66/uuid_utils-0.17.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:673d89cc434cc9b97a0b4cf61272f6fca70a81f64eb0afbface2a0d9f77f06cd", size = 562232, upload-time = "2026-07-09T13:49:06.201Z" }, + { url = "https://files.pythonhosted.org/packages/19/28/65e0980d668a6d44e699f59d1acf43d6b5d4893592c115ce7c680bb4dfa1/uuid_utils-0.17.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:387cf7437c94ddec08651a0f1081381299c7075bc48a6251d8922bf39973378a", size = 287858, upload-time = "2026-07-09T13:49:07.45Z" }, + { url = "https://files.pythonhosted.org/packages/8f/8d/5e97bcebc90fb6a10f98af3dc1ba552e04183aba59e2edc0b9cf486dd998/uuid_utils-0.17.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:220b52746d99e11964badac3c0869016e0c24bafb70a7dd5c2c072a6be3da9cc", size = 321587, upload-time = "2026-07-09T13:49:09.489Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d7/88b2a2370cc3d455ba0515fb6f5c8f7ac0c0f55a86801b6e56a432f22c17/uuid_utils-0.17.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0ab4a66e7a035ad6625cfc1fbdb34f5c2d25a80ae1ef4bfee458ea2036333c6d", size = 328964, upload-time = "2026-07-09T13:49:11.292Z" }, + { url = "https://files.pythonhosted.org/packages/bd/0f/181c5da673953dfc0958cb4fb3a4984a9098673ddb05cac68e994bc8511b/uuid_utils-0.17.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5641071337eb11d61a001ea08793bf72216f3241f0a433ed2764804b2a3e3cc7", size = 442909, upload-time = "2026-07-09T13:49:12.644Z" }, + { url = "https://files.pythonhosted.org/packages/ec/38/5c5e665af542884a8fd3c61725c38453239e13940326b5b70f3ef8881a97/uuid_utils-0.17.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9082e709014946b1f6e96ae6ecd93652efca2d2a6a3ab67dbe151c8b4bf193a4", size = 323076, upload-time = "2026-07-09T13:49:13.897Z" }, + { url = "https://files.pythonhosted.org/packages/f5/35/7de97de18cbf226c2a4f2104ad15e56ca4491717c81c0b71795c0c585b4e/uuid_utils-0.17.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1fd6f0e8a162dc0e9255b6aebe3cd175e76c33202f1bf39da9e6294b93db0099", size = 347360, upload-time = "2026-07-09T13:49:15.237Z" }, + { url = "https://files.pythonhosted.org/packages/26/a1/9915d5dd59fdd1957ded5d188c0ea0b9db5a1d84d42c8d8828a7b83b366e/uuid_utils-0.17.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d63010803d7c368963bbe6f7ec379593e76dd581d7db0f29118d88713c9e0354", size = 499267, upload-time = "2026-07-09T13:49:16.774Z" }, + { url = "https://files.pythonhosted.org/packages/c0/05/88108405262ec850cea0f95733445d6873e5772af3292baabd9ef8457740/uuid_utils-0.17.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a46bedc273b6f58f11dee816ff74999625ef8d007890f411b7a4975bf1c89330", size = 604940, upload-time = "2026-07-09T13:49:18.147Z" }, + { url = "https://files.pythonhosted.org/packages/89/d5/6dbcd300de47cc443cff2656cd5327a385751213dcb2101cfee7388170b2/uuid_utils-0.17.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:405233a5f625b3d995648f4647fa6befa4567cf3f74e1f6b9837e16f7310f0e0", size = 564172, upload-time = "2026-07-09T13:49:19.593Z" }, + { url = "https://files.pythonhosted.org/packages/ab/94/e8057f2288a415fba8a978bca4b589f5cb6b91a028a5dc07a1775938b33f/uuid_utils-0.17.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b6c5d2d71e1f17329150ad9427d27f4a3f29a01792e7ecdc64a98ac5368fc4d5", size = 528533, upload-time = "2026-07-09T13:49:21.075Z" }, + { url = "https://files.pythonhosted.org/packages/f0/6b/31713148c77e48e62f51aa042a98a54a8be0396912ea5130f83f52ae722d/uuid_utils-0.17.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f7e9b8728ba07a3cb2f29d5aa1a266c2664eb8ef0fd43afa34627c92f7fac8f0", size = 99197, upload-time = "2026-07-09T13:49:22.351Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f3/ca6f6ac5428312df8ed632f6dd9f9e6aba23090471fcdeae53eab027e8b3/uuid_utils-0.17.0-cp314-cp314-win32.whl", hash = "sha256:58838921e377791ef22c64cc92141bfae030f43651ff9272f0f28a208a9e6a5a", size = 169540, upload-time = "2026-07-09T13:49:23.563Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cd/7ede0db66411fa09817d79b680f7454ea9bee2d374e1922e4efd065760a3/uuid_utils-0.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:42275ebd0e8e74e32cdbfb8bd88fc99576567d51d54a508020611fd8f4f463a0", size = 175984, upload-time = "2026-07-09T13:49:24.703Z" }, + { url = "https://files.pythonhosted.org/packages/f0/81/533b5f80cd4918c0693f4e1b7b90ceb1caa45f4266ae8b528135d7ecca5d/uuid_utils-0.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:b5d11cccba076a32321ef1380dea956821f0b51794ef59df64e58fb1cd543aae", size = 174749, upload-time = "2026-07-09T13:49:25.886Z" }, + { url = "https://files.pythonhosted.org/packages/a0/13/f400ac39d06fd8be5b099c09e41bb975205926722a3e8d53348817cb7ff9/uuid_utils-0.17.0-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:fae8b282f0cb22a5de222999f7723f4e5ec04f6fcdf4aaef879b5b36625ae2b0", size = 562610, upload-time = "2026-07-09T13:49:27.374Z" }, + { url = "https://files.pythonhosted.org/packages/03/8c/c71c8312304c56f6d0bcba87cd402fa79bec35d18ffc8c41954196ca68e5/uuid_utils-0.17.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:967955620df45e6cffe2e9950cb9903cb455649396f896b26b04363a91a5054b", size = 289473, upload-time = "2026-07-09T13:49:28.989Z" }, + { url = "https://files.pythonhosted.org/packages/bb/cd/522117e2e5184ca1d4f0f85ee833e9e21bd8c6b99eff8a4d1a8e5a194e33/uuid_utils-0.17.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375cde148430d60a4a07c03abaa0774c4fddfdd90de99b4ba02f24088bc9d750", size = 321600, upload-time = "2026-07-09T13:49:30.4Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f4/0d81f9bd346fc717bc561c08fa6457e0328966eb76e536b938fe77d56459/uuid_utils-0.17.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:975c17da26c5b9d46c336b03c52a057ac28378d6f9d98b58d32a038589bb3912", size = 329569, upload-time = "2026-07-09T13:49:31.732Z" }, + { url = "https://files.pythonhosted.org/packages/5e/41/26e1363f36a94c9e8ec2dd21d5f63088d3e7c723adbb12dcc8fdc77be417/uuid_utils-0.17.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3150d836290c88f1d26eb59c4db280d87417dd3bfaadd2889c77416c8f0ff6fa", size = 442051, upload-time = "2026-07-09T13:49:33.024Z" }, + { url = "https://files.pythonhosted.org/packages/2b/a7/2c1ed1b34d7df7fdcc11c28fd26d94d44843b37d9af2435ff9fd8abdbc08/uuid_utils-0.17.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9472a8de37faf8bd216c628e0e68c8f6bef730d3ba0a5060f3b0fa460c992ac2", size = 324372, upload-time = "2026-07-09T13:49:34.554Z" }, + { url = "https://files.pythonhosted.org/packages/78/bf/328d3c6bb22c496944a1b3b732207d71aa6964eb604e5e3b9dcb91ed0a00/uuid_utils-0.17.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d27c531edb8d1f38ca2eddaa1fa24913a460aeb721f2efd4ef42a124ce94e354", size = 348548, upload-time = "2026-07-09T13:49:35.898Z" }, + { url = "https://files.pythonhosted.org/packages/3e/76/a07de5cb7b90582fdbbc830fd19be129cbbb9897cfe239fef469d7bd2d09/uuid_utils-0.17.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5670c52a438e21483ce715776144914a4e2a2a5c62d9dee15f8a3e90cf128ae6", size = 498985, upload-time = "2026-07-09T13:49:37.142Z" }, + { url = "https://files.pythonhosted.org/packages/f4/62/9966e46ae34fcec6b06119631fb3c09705ea78835035ce3a82d3348eb61a/uuid_utils-0.17.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:6f29689a76fe7a49cbd629a794d0ec1eab48814e323a00a146a741b0195bde68", size = 605183, upload-time = "2026-07-09T13:49:38.648Z" }, + { url = "https://files.pythonhosted.org/packages/d7/4e/bb962ba0fe31e903b199f22cf4c1a6cba35a8987aef526d287277ab8ca8b/uuid_utils-0.17.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4441600447d340ae103a353f01dbcd22ff680e5ee1a22988efe8d7b791d8fdb3", size = 565412, upload-time = "2026-07-09T13:49:40.115Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9e/122adfeeeae8a84ccfd43bce627b104d12a2180a93bffd2c0e1b54dad7a6/uuid_utils-0.17.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7b04935a79c03c41ad08d0a5f390aac968bfb561f1268897bc5b0f077971efd", size = 529885, upload-time = "2026-07-09T13:49:41.513Z" }, + { url = "https://files.pythonhosted.org/packages/b3/4f/257304dded339dc35fc9bf35722ac68fd4fdb930f255b8f7bccdf74ebba9/uuid_utils-0.17.0-cp314-cp314t-win32.whl", hash = "sha256:239d8a281fe10bae33205b5d43185834d556b18434e0a113b5dc1dfb2fd97e91", size = 169472, upload-time = "2026-07-09T13:49:42.871Z" }, + { url = "https://files.pythonhosted.org/packages/35/c8/e78c06db7e9ce317ce7b8759ff2058333eac75caa8c22b75f0059589c9be/uuid_utils-0.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e288a06cbbbcd01b44386e767985c9e21d2ad9bf59829aa7058d9a2a494804ab", size = 176271, upload-time = "2026-07-09T13:49:44.105Z" }, + { url = "https://files.pythonhosted.org/packages/a7/11/bd1c70e1ad3301163cebe66c8d26de26e6814d52f642a849448bd2833626/uuid_utils-0.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1776a80d16369999b21627028cc5dbce819be83e1e079fdd7a51b587d2916db9", size = 175004, upload-time = "2026-07-09T13:49:45.591Z" }, + { url = "https://files.pythonhosted.org/packages/ee/14/4ae708968b15cac7b68d5b854bfce724b21faa1c7a5147fb96d87f468a45/uuid_utils-0.17.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7b9044ce4acbf392d4b3a503fe377641f4deff82e6c341c36ef27af0dea76cdf", size = 567823, upload-time = "2026-07-09T13:49:46.902Z" }, + { url = "https://files.pythonhosted.org/packages/4c/e2/d3af9c3d1dc6efb9ee1cffab30f3f2aacacc3892b21b495d78d34c6696bc/uuid_utils-0.17.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9a91c4814c7150a4d798da691b7804eacd78c4b84fb392a60fa0de21341861eb", size = 288763, upload-time = "2026-07-09T13:49:48.491Z" }, + { url = "https://files.pythonhosted.org/packages/bc/c2/f1b183e412387529893015a94a8447633c665f6d0392de20e245680e636a/uuid_utils-0.17.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dd4a21baaac9a88486f0dd166c5793feb101a0bb9f006f2c401657fff5a1343", size = 324919, upload-time = "2026-07-09T13:49:49.972Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3c/d32c799bdd51f3b08b6ee95f9de921b59c69075a96767f937fab55014813/uuid_utils-0.17.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32abaafc8e91928b3d9f4d82e42d2094041e38ad6bb964066faadff28e4162f1", size = 332689, upload-time = "2026-07-09T13:49:51.402Z" }, + { url = "https://files.pythonhosted.org/packages/6f/90/b4cd455619ff276dc3c3262a7420ead63aa1e531362f00df4cdb07d90e0a/uuid_utils-0.17.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd741c73440b328f937dc53b344ecadc46bc4f0cec0333a8f42b55f3468ce7ec", size = 445726, upload-time = "2026-07-09T13:49:52.757Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f1/5cc042a37932aa9a66eb8ab4a9a5b31d80261ae4565ff0193d8cc1fb9392/uuid_utils-0.17.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89a0980d49683c00539c59cd9f46b1908c538e6b5b0a48ad12187bb856d0f391", size = 325610, upload-time = "2026-07-09T13:49:54.191Z" }, + { url = "https://files.pythonhosted.org/packages/5e/72/9e800c41d766484484e97845a7a7f677ba94462df86c97183e0290229d16/uuid_utils-0.17.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:de1064663aa7c839286488a319d2b3b478ca5ab5b2091ade888ed0eeca11a98a", size = 352672, upload-time = "2026-07-09T13:49:55.748Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8e/86ce2c03a1d9674530f6649e49067f7c69929600127077731de590d12132/uuid_utils-0.17.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2db386941cfdecdd0b5a8ceeed5cf7479c83d1730dcf64a48d43cfa018cc3310", size = 178681, upload-time = "2026-07-09T13:49:57.096Z" }, ] [[package]] name = "uvicorn" -version = "0.35.0" +version = "0.51.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "h11", marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/65/b7c6c443ccc58678c91e1e973bbe2a878591538655d6e1d47f24ba1c51f3/uvicorn-0.51.0.tar.gz", hash = "sha256:f6f4b69b657c312f516dd2d268ab9ae6f254b11e4bac504f37b2ab58b24dd0b0", size = 94412, upload-time = "2026-07-08T10:59:05.962Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/ec/dbb7e5a6b91f86bfb9eb7d2988a2730907b6a729875b949c7f022e8b88fa/uvicorn-0.51.0-py3-none-any.whl", hash = "sha256:5d38af6cd620f2ae3849fb44fd4879e0890aa1febe8d47eb355fb45d93fe6a5b", size = 73219, upload-time = "2026-07-08T10:59:04.44Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" }, + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + +[[package]] +name = "wcmatch" +version = "11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bracex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/25/1da725838132221e33568973da484ff43813662ccc06ebf7f6e3abddfcd5/wcmatch-11.0.tar.gz", hash = "sha256:55d95c2447789712774b198ceec72939e88b5618f1f8f0a9b605bf7740b63b96", size = 141360, upload-time = "2026-07-10T05:50:24.183Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/12/f38b6fee116274d7221743caab07d765032e1370bb54cad8714f87aeb0e8/wcmatch-11.0-py3-none-any.whl", hash = "sha256:3a5977ace27e075eef67eb03d539563f1a19018b62881949a42932cf66926934", size = 42914, upload-time = "2026-07-10T05:50:22.995Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" }, + { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" }, + { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" }, + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" }, + { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" }, + { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, +] + +[[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 = "xmltodict" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/70/80f3b7c10d2630aa66414bf23d210386700aa390547278c789afa994fd7e/xmltodict-1.0.4.tar.gz", hash = "sha256:6d94c9f834dd9e44514162799d344d815a3a4faec913717a9ecbfa5be1bb8e61", size = 26124, upload-time = "2026-02-22T02:21:22.074Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/34/98a2f52245f4d47be93b580dae5f9861ef58977d73a79eb47c58f1ad1f3a/xmltodict-1.0.4-py3-none-any.whl", hash = "sha256:a4a00d300b0e1c59fc2bfccb53d7b2e88c32f200df138a0dd2229f842497026a", size = 13580, upload-time = "2026-02-22T02:21:21.039Z" }, +] + +[[package]] +name = "xxhash" +version = "3.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/63/71aa56b151a1b28770037a61bd4e461c2619cfc8866a4fcaf1548605e325/xxhash-3.8.1.tar.gz", hash = "sha256:b0de4bf3aa66363552d52c6a89003c479911f12098cd48a53d44a0f7a25f7c46", size = 86223, upload-time = "2026-07-06T10:49:58.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/97/1a8cebf0a6650417f08a18231590e2515aacd5ce39c3ad8b9e013ebd437d/xxhash-3.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:27a9e475157f7315826118e3f3127909a0fe25f1b43d3d3be9c584f9d265f937", size = 34695, upload-time = "2026-07-06T10:43:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/2f/cf/745b9bc0dd9c341bc074b5fc700db7bbef0f3b69ab21446492296ab37e50/xxhash-3.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b2ce44bf8f4a1d01f418b3110ff8dff32fd3f3e836c0e06333c3725f243fa6c", size = 32376, upload-time = "2026-07-06T10:43:41.97Z" }, + { url = "https://files.pythonhosted.org/packages/65/a4/8512a901b1d6ad4a9838d1b40385907a879d7e005a5afbec5d39526b69f6/xxhash-3.8.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:942bc86e9be6fdd6e1175048f5fe8f8fdaaf2309dd1323ef1e155a69cd346780", size = 217470, upload-time = "2026-07-06T10:43:43.572Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ad/0ffd8094ea29579bb2dc42fa74d08570e9ea3d95db561e6b1105e69b9ca6/xxhash-3.8.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0204701e6d01f64254e0e5ff4255812b1febe027ddd7dda63372e27f98b5e91f", size = 237799, upload-time = "2026-07-06T10:43:45.248Z" }, + { url = "https://files.pythonhosted.org/packages/b3/90/783c6b3f9336bd07449fe672be32cef6833633936bbfda8d3b23ee18d202/xxhash-3.8.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7dc4bdf008f77c88d544849c48c1a40faf25a5eff6cc466de2e8edc37c191fce", size = 262587, upload-time = "2026-07-06T10:43:46.733Z" }, + { url = "https://files.pythonhosted.org/packages/c4/77/ba0316a7c3e661b86830a47ae4987798616ce1b15af8d2a6358e2d89ef60/xxhash-3.8.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c566b123dce7e4867ca518434cdfb9f84e5023771235b2e3107a26c9a41cbd8", size = 238484, upload-time = "2026-07-06T10:43:48.453Z" }, + { url = "https://files.pythonhosted.org/packages/09/79/33001037c1cba90f4ced38b257161c13452024c0db44208f883e2e47f3fc/xxhash-3.8.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9f23083e1bd9d901f844af7a126727c486e7eada9a1a6791c8f7e73f94fac656", size = 469909, upload-time = "2026-07-06T10:43:50.188Z" }, + { url = "https://files.pythonhosted.org/packages/45/90/237eded9dd6ae638083294e5a9f77b317aaebd480a330806b39c192a0de1/xxhash-3.8.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64af54dd1c3a45a27c04942f9a1a4683322bdd127f4745cca4e02549c1d2d2bb", size = 217166, upload-time = "2026-07-06T10:43:51.816Z" }, + { url = "https://files.pythonhosted.org/packages/0b/6a/8cb439dc9920e1468e1c2d69ef77cbeb4be3b1ae9f4b5344c07a2b59af18/xxhash-3.8.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8ea8a141eeced4f6262ab6dd71c681ac546a558c30bb586abe087d814b5f85ea", size = 307593, upload-time = "2026-07-06T10:43:53.436Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c6/c0607d373c8affea92101a3926c4fc8b026bcf8983e05fd58f3a0380ebf8/xxhash-3.8.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a98b2f95cab589e0f5e92c48431afb4d56238b8bf6668edcc66166180e9b509b", size = 234702, upload-time = "2026-07-06T10:43:55.042Z" }, + { url = "https://files.pythonhosted.org/packages/5b/cb/f4cfd456624c1f017858168b7ba9443dad810da8aac779a612658450e827/xxhash-3.8.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1b86ae798a976ccbc1d02af6ccb98f5b4d24756b1f65e995f11d10fe071f486f", size = 265749, upload-time = "2026-07-06T10:43:56.749Z" }, + { url = "https://files.pythonhosted.org/packages/33/f3/9006669c04b01206e21b2177425c649461ba188930a052c2f1728d6ec6a8/xxhash-3.8.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:81f4ed9ca9644bc95cd976bfe10f7a4cafab8ffdc3aed52877d4600e445be7ef", size = 221992, upload-time = "2026-07-06T10:43:58.12Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0b/7e6f3eaa05df5e0b6c94aa452b0672801f7031e602081f07fd441aaaaed5/xxhash-3.8.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:cb3fe820c27593f170770d6c8d791936cf6275d9269405fbb7b30a55363c10c8", size = 236899, upload-time = "2026-07-06T10:43:59.562Z" }, + { url = "https://files.pythonhosted.org/packages/da/cc/bbaee4987f3aab1d7b33bb430bb49e940646160af448b9167431c931126d/xxhash-3.8.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:7345007c12780985de4fd740148776d1eee18c0d41407c6fa1e48c5450304fe5", size = 297934, upload-time = "2026-07-06T10:44:01.132Z" }, + { url = "https://files.pythonhosted.org/packages/a7/97/6bee358660eb8b4f73c00b00b00bc616ebde00e1ab4b67c63486ce360648/xxhash-3.8.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:12eaeaa9ab8b9e6033a1fa5f6b338aaf55ff4df4bee11b59fd6ee03b19186ee4", size = 439315, upload-time = "2026-07-06T10:44:02.878Z" }, + { url = "https://files.pythonhosted.org/packages/c6/50/7e35275f39256bedace0c3cd5be3c72d4ac9d5aecf5e5fdc3530337cd263/xxhash-3.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2a845687219ba3214126f14a8a5861f97c9e065a7d0b8252adb6df13eea86fb", size = 214038, upload-time = "2026-07-06T10:44:04.504Z" }, + { url = "https://files.pythonhosted.org/packages/59/2d/69d02d096ee50bdf3ef0d208d874f52c71b1aa6906066bce3c52fedb8bc6/xxhash-3.8.1-cp310-cp310-win32.whl", hash = "sha256:656256c9f9303e47f07d5cb8ae4468285370adfafd7ba48aea33a458e7697626", size = 31939, upload-time = "2026-07-06T10:44:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1d/e06fca9844919ca91c6587d530cfa1e745830ec73ad38f44f04b25d1bfb7/xxhash-3.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:27cfc2f1ed76f956f36dfe0c56e5f5a3e94cd91eb78b893f63e2ef2ae404fcdf", size = 32729, upload-time = "2026-07-06T10:44:07.621Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c2/800648d99039927b5a86d8ae02cd86a556a5ee1678d388216f6b44c8966c/xxhash-3.8.1-cp310-cp310-win_arm64.whl", hash = "sha256:c85949d02c85adf6d786eb94858e124989a632a4e65739835b2fc5761827fac3", size = 29215, upload-time = "2026-07-06T10:44:08.916Z" }, + { url = "https://files.pythonhosted.org/packages/8a/5a/05eaa129555f85476a3e16ff869e95f81a78bbe4647eef9d0229f515a317/xxhash-3.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602efcad4a42c184e81d43a2b7e6e4f524d619878f2b6ee2ba469011f47c8147", size = 34699, upload-time = "2026-07-06T10:44:10.14Z" }, + { url = "https://files.pythonhosted.org/packages/80/59/0df1133958b2228929355e022aab1e958c7b2c43e27bf7f59bc9edfa8a54/xxhash-3.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:131324f719957b988861714de7d6ddf57b47abec3b0cc691302ffeaba0e05e10", size = 32373, upload-time = "2026-07-06T10:44:11.353Z" }, + { url = "https://files.pythonhosted.org/packages/3e/bf/1cfda5b5e6bf26617812b4a31662ef2220d2ad04e0a55b8ff9eb36e56a5c/xxhash-3.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:db77278a6eddadbf44ce5aae2fee5ebb4d061f026b1ce2130d058cd4d7a7b670", size = 220284, upload-time = "2026-07-06T10:44:12.683Z" }, + { url = "https://files.pythonhosted.org/packages/70/93/45dc0ad7913b69e5b08bd039236cf628380e4c9cc76a8a4c6625a328e058/xxhash-3.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c332dd48b8cb050da2bb2a3c96d72b1664168650a250ef9718e423df7989e05", size = 240980, upload-time = "2026-07-06T10:44:14.297Z" }, + { url = "https://files.pythonhosted.org/packages/e9/02/f28ba7d17f2c1410ee397982c817ab1bd5b2701070c2d2c373539aad000a/xxhash-3.8.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a5cd96f6dcdf4fa657b2d95668d71d58455248f98712ecffaa9c528edf40ccae", size = 264526, upload-time = "2026-07-06T10:44:16.017Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d0/f10651cec2c7981b20d693deae6bdfc438427d92be2db4ccabb6181f0021/xxhash-3.8.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c959f88160b13b4e730b0d75b459b7929fc0d2225c284c9683ac95d6feeeac6a", size = 241369, upload-time = "2026-07-06T10:44:17.698Z" }, + { url = "https://files.pythonhosted.org/packages/ff/40/136e0cbaf5db51e191423b1c98643593189f02b6cd90837bf64b19113d70/xxhash-3.8.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:027dee4355f3fcc41481650d846cf6cfc895c85a1ab7acd063063821a0df5b4c", size = 473186, upload-time = "2026-07-06T10:44:19.354Z" }, + { url = "https://files.pythonhosted.org/packages/4b/3f/6aa808a96bdc43dba9a740dec56c744526ee3c0019e32c75e810fa90ae4d/xxhash-3.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad52a0e4bcc0ba956a953a169d1feec2734a64981d689e4fc8f490f7bf91af60", size = 220092, upload-time = "2026-07-06T10:44:20.956Z" }, + { url = "https://files.pythonhosted.org/packages/47/28/a8675e78a9ced96dab853416162268e10e05b452e95db7888cf69f58ac5f/xxhash-3.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5d3dfb1f0ff146da7952867a9414f0c7a29762f8825a84879592612fd6139342", size = 309846, upload-time = "2026-07-06T10:44:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/0f/7fe4d4ef4e69f0033e012396ee2a115886bca7b10b7e45ce398626436bfc/xxhash-3.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4482380b462ca9e59994d072a877ecadd1cf51102daeeab2db696f96ab763723", size = 237659, upload-time = "2026-07-06T10:44:24.135Z" }, + { url = "https://files.pythonhosted.org/packages/38/8f/83e9e31d4ed57fe963b99cb5b13a23e3e0f0dad1885aa0ebd2a7819dd423/xxhash-3.8.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:950ac754d16daea42038f38e7465eb84cda4d08d7343c1c915771b29470f065a", size = 268737, upload-time = "2026-07-06T10:44:25.875Z" }, + { url = "https://files.pythonhosted.org/packages/57/79/7e7de46dbe5d1f49afc96a0bc42e6b8df24eae3d6bad6007b99e42f48430/xxhash-3.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0418ec8b2331b9d4d575fc9284427e8e69449d7172e99e1a86fcdd1f51a0a937", size = 224955, upload-time = "2026-07-06T10:44:27.777Z" }, + { url = "https://files.pythonhosted.org/packages/ec/34/b8540839e958d5ef5c6101af6f16032109e7099698ae8edbc8dcefe4d8f4/xxhash-3.8.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:32a94ad2763e0263d9102037d349002c3d3c401e42770542c3eeb4801f311661", size = 239653, upload-time = "2026-07-06T10:44:29.422Z" }, + { url = "https://files.pythonhosted.org/packages/ce/87/a735d05f7f859354acadabe470ff40e2c46672275f96dcf096a761904def/xxhash-3.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:89b11a5cdd441aa463f6d34ca0241602bc09b001a76994b6059828494108c673", size = 300213, upload-time = "2026-07-06T10:44:31.401Z" }, + { url = "https://files.pythonhosted.org/packages/98/31/3e1cb020237b68117fc212dc5f9753b87f865b4dfee7c1ce62d0836955b5/xxhash-3.8.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:09a204dd4bb0823daf938cdd0dc8057d5f1e14fe3cbde929424255f23f9de872", size = 442508, upload-time = "2026-07-06T10:44:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/23/bf/f80090622141cc734b039ce1d15ce3ff6dced375e9680249bf5b9b8c6bf9/xxhash-3.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e710ad822c493fb80a4fbc1e3d0a807b1422cb90adbe64378f98291b7fa48fef", size = 216853, upload-time = "2026-07-06T10:44:34.983Z" }, + { url = "https://files.pythonhosted.org/packages/a6/a3/60157acecc307b238d3651c2483168e224b48b23a36ae6d6903588341d80/xxhash-3.8.1-cp311-cp311-win32.whl", hash = "sha256:5013be3bea7612852c62a7437f3302c1cfb91ca7e703b194459db0b2b2e0d792", size = 31936, upload-time = "2026-07-06T10:44:36.542Z" }, + { url = "https://files.pythonhosted.org/packages/59/5c/ef70c418d878d187b8da56d4cdc06aea6cf5e456b301e96e51e1d2cc8625/xxhash-3.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:f377012b86c0a23a1df0cf5a1b05aa7187649e472f71c7892e5f2c2815bbe74f", size = 32724, upload-time = "2026-07-06T10:44:38.177Z" }, + { url = "https://files.pythonhosted.org/packages/2c/25/f008db952cec6b2a26445b456eeed2ebebd65e08e848ebe09ed6ac0634e6/xxhash-3.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:836f11d4474d3228e9909d97216faa4f7505df41cfaf3927eb29809de785a78d", size = 29212, upload-time = "2026-07-06T10:44:39.577Z" }, + { url = "https://files.pythonhosted.org/packages/42/91/f65c34a7aa7b4e7cf4854f8e6ef3f7ee32ceac41d4f008da0780db0612f6/xxhash-3.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e6e49370822c1f4d8d90e678b06dbcb08b51a026a7c4b55479e7d467f2e813bc", size = 34680, upload-time = "2026-07-06T10:44:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/57/04/b10a245a4c09a9cfa88f8e9ae755029413ad1ac17047f9a61906e5ae0799/xxhash-3.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:220d68130f83f7cc86d6edfdeab176adc73d7200bf3a8ec10c629e8cf605c215", size = 32397, upload-time = "2026-07-06T10:44:42.196Z" }, + { url = "https://files.pythonhosted.org/packages/3a/75/45ab795b5945b6388583bd75202106af505537935566c15a1577797a0e08/xxhash-3.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d365ee1892c1fa803536f8c6ce21d24b29c9718ec75eb856095c07830f8c478", size = 220549, upload-time = "2026-07-06T10:44:43.603Z" }, + { url = "https://files.pythonhosted.org/packages/13/44/5ba2bd0a14ddf4193fc7d8ec29625f659f22c06d60b28f04bf46305d8330/xxhash-3.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:852bfe059720632e2f16a6a4745e41d20937b2bf2a42a401e2412046bb6971cc", size = 241186, upload-time = "2026-07-06T10:44:45.534Z" }, + { url = "https://files.pythonhosted.org/packages/23/32/c4147def4d1e4538b906f82731e0ba23424377fc50a7cddd03cd284c8f63/xxhash-3.8.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2f8c25a7061d952de589bd0ea0eaadee32378ff83dd6a677b267f9cd86f401f8", size = 264852, upload-time = "2026-07-06T10:44:47.199Z" }, + { url = "https://files.pythonhosted.org/packages/6c/bd/71ed14f4f0318bb7fd7b2ec51999413487fa8da8d41208e84d50d1ef0f98/xxhash-3.8.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:868a8dcaff1a84ba78038e1cef14fc88ccf84d9b4d12ea604696e0693296aa56", size = 242663, upload-time = "2026-07-06T10:44:48.846Z" }, + { url = "https://files.pythonhosted.org/packages/91/09/70af22c565a8473b3f2ae73f88e7721af281bc4a575236dbd1970c9f76f6/xxhash-3.8.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6536d8677d2fff7e64cd0b98b976df9de7aee0e69590044c2af5f51b76b7a170", size = 473510, upload-time = "2026-07-06T10:44:50.695Z" }, + { url = "https://files.pythonhosted.org/packages/18/96/34db781c8f0cf99c544ca1f2bc2e5bf55426e1eb4ca6de8ea5da56a9f352/xxhash-3.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:82c0cedd280eab2e8291270e6c04894dbc096f8159a39dcf1807429f026ca3cc", size = 220469, upload-time = "2026-07-06T10:44:52.422Z" }, + { url = "https://files.pythonhosted.org/packages/93/5f/9a184f615fa5a4dce30c01534f62946ce5a11ce40f73785cbd356ccabaa9/xxhash-3.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daa86e4b68221d38e669bb236ba112d0335353829fb627c82e5909e4bbe8694c", size = 310290, upload-time = "2026-07-06T10:44:54.142Z" }, + { url = "https://files.pythonhosted.org/packages/a9/dc/9b9a9789011ee153723a5eb9e7dd7fcbae2ba9b3fe7a729249ca7c252056/xxhash-3.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2bc7113e6f2b6b3922dd61796ca9f36af09da3773898e7003038dc992fc83b8d", size = 238173, upload-time = "2026-07-06T10:44:55.693Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4d/71c6005ada9dcb608a4e1902e8475ecadb5f3fbfa04e1e244d276a2d0c43/xxhash-3.8.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5eed32dad81d6ba8e62dc7b9ffa0500199385d7810a8dd9d4eafaceb8c6e20bb", size = 269026, upload-time = "2026-07-06T10:44:57.424Z" }, + { url = "https://files.pythonhosted.org/packages/2f/87/d6c036ba25dfbd9c8633be5aa86fc9474bbb9e2c68212a841d090abe7344/xxhash-3.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:83697b0ea1f10e7f5d8b26a4906fa851393c61546c63839643a2b7fe2d868061", size = 224970, upload-time = "2026-07-06T10:44:59.085Z" }, + { url = "https://files.pythonhosted.org/packages/48/62/4c1f035a41c5752aa05e195b6c904c07b94fe9061a16de61e72a6e6b135f/xxhash-3.8.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:36fc69160465ae75c6ec4ac9f781bb2aa16ae7ff869e73c26fee85fbb11b9887", size = 240820, upload-time = "2026-07-06T10:45:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/da/14/d39d565069b87e86d21a2af2a31d04db79249d25aa8d5b62959056a89857/xxhash-3.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:445e0f5a31f2f3546ae0895d4811e159518cdc9d824c11419898d40cfadb677e", size = 300619, upload-time = "2026-07-06T10:45:02.716Z" }, + { url = "https://files.pythonhosted.org/packages/13/22/75467acc887edc8cf71c97ab1708feb3df7a88bda589b9f399765c6387d2/xxhash-3.8.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:dfe0580fbfd5e4af87d0cc52d2044f155d55ebd8c8a93568758a2ea7d8e15975", size = 443267, upload-time = "2026-07-06T10:45:04.653Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b6/1da3baa5fa6ef705e3425fddd382be7dfc4dfba2686df90a20f16e9c7b1b/xxhash-3.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:095e1323fa108be1292c54c86da3ef3c7a7dc015b105a52133973bc07a6ad11a", size = 217338, upload-time = "2026-07-06T10:45:06.304Z" }, + { url = "https://files.pythonhosted.org/packages/78/dd/b5295a9f97484e7a1c2b283a742ca45e3104991c55a1ef670dde161829ba/xxhash-3.8.1-cp312-cp312-win32.whl", hash = "sha256:bf28f55e427e0483acb1f666bd0d869b6d5e5a716680c216ad7befe3d4cfba2e", size = 31970, upload-time = "2026-07-06T10:45:07.823Z" }, + { url = "https://files.pythonhosted.org/packages/ec/31/3fa0b807d7e21515cd975e7fe5c039d52ac3e9401a96d6ad68dae6305215/xxhash-3.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:2256e80e4960ee282f63428adb349cb7f8bd8efe4db770d88eb815f4b9860724", size = 32741, upload-time = "2026-07-06T10:45:09.42Z" }, + { url = "https://files.pythonhosted.org/packages/b8/05/86feada74e239600e6875aa507afb40482a89b92700aa74a92da83bdcb77/xxhash-3.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:9df56e6df96a60590935e22373041cccc91fd55858763dcffb55bf63b3a2b396", size = 29234, upload-time = "2026-07-06T10:45:10.809Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8c/446bb782cd0d27007a917b5569a08dd73219c3e8d6e459014db104b27bdb/xxhash-3.8.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:3c682fcd96eb4bf64be32a4d95f96107e1588005831bd8a741b324fdda01b913", size = 38562, upload-time = "2026-07-06T10:45:12.425Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ec/c0c45627eaa6be7a5d6117423adf8f7a15b17ee74b4b17072cca5959a225/xxhash-3.8.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:036a024d8b9c01f70782e09ed98d532e76fd23f950ae7154bd950fe94e90ebec", size = 36656, upload-time = "2026-07-06T10:45:13.932Z" }, + { url = "https://files.pythonhosted.org/packages/f6/94/8324c04cc7597154caaeba6c094e01fbd2e7601d01e7a13eea9f5420e77b/xxhash-3.8.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d6a5c0bce213b23b0166fe0d35bcbbe23ce4b968f257cc7eb6fd57cb8e1e6297", size = 31169, upload-time = "2026-07-06T10:45:15.687Z" }, + { url = "https://files.pythonhosted.org/packages/40/a4/beb6bb26e1184e126dbe7a5682330214ef54dcfbf882078aa9f4b5428d42/xxhash-3.8.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:5177aa44eddaa97c6ef0cc00c6d540edb64d51781d2f8fb941612ec61a92c9ed", size = 32177, upload-time = "2026-07-06T10:45:17.035Z" }, + { url = "https://files.pythonhosted.org/packages/56/0f/fc4c92a5a528f839b34b6419b2e53c8597f2a629d5a1f5d721f65bfa1fd6/xxhash-3.8.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7801b7223db017b9c0c9ccf37e44524edb35a1544a1c032add22c061c6af0276", size = 34642, upload-time = "2026-07-06T10:45:18.39Z" }, + { url = "https://files.pythonhosted.org/packages/d4/58/edbfb141d4000767ac6a9694f8ac0763e2c2e983e65c9e31620ba56e2667/xxhash-3.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9e80238259655bf69d7bcd08226a970d7f42605f3157786bfa76dd13472d7fa0", size = 34684, upload-time = "2026-07-06T10:45:20.033Z" }, + { url = "https://files.pythonhosted.org/packages/07/3f/5072f1f0f5714186f0ac2a0b5a4929ce30d4b845e94886b6c01b6ebda0be/xxhash-3.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bcab50a389cc04d87f90092af78a6adba2ab3deca63175a3344ca83514045315", size = 32401, upload-time = "2026-07-06T10:45:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/49/c7/802ea2f9c2ed59219934d6d65c470d502b1788043eae277a52af8658bda6/xxhash-3.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a2489d3a776fa380cb8e71f54c7fda268a9baf3de9b1395093fd280f95735907", size = 220617, upload-time = "2026-07-06T10:45:23.234Z" }, + { url = "https://files.pythonhosted.org/packages/99/a8/e10488efd31fcb13fcd6acbc6e788f10c6f8e3a0cc4ae3eb89dc19c55a12/xxhash-3.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32ab1e5432690276e71192be7401b55f96db2d0eedea5d44eb1f164505669cc0", size = 241295, upload-time = "2026-07-06T10:45:25.364Z" }, + { url = "https://files.pythonhosted.org/packages/18/cc/14180b17d44892a631f8ae7323c30bfbb1328efc8209e528a480293528ac/xxhash-3.8.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b30e01a0b97a4bc3f519a4d7a82da3dc53251fb0de5eeea8660dcd4ff094c0c2", size = 264688, upload-time = "2026-07-06T10:45:27.09Z" }, + { url = "https://files.pythonhosted.org/packages/a9/72/a14019d0c5f6c41ee407a503036ae32787c91325ca218a96a9b5627be651/xxhash-3.8.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f44275ddb0978b67a58a951501903f04d49335a91f7681c9ce122ecb8ccb329", size = 242740, upload-time = "2026-07-06T10:45:28.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/08/92550e556c6fcfcb96c6a336945eb53a431ed43120ed749636debb16c5cf/xxhash-3.8.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3b87cbd974512c0c5fc7b469c36b2cdc9ee6d76e4ec78bccb2c7184611c49b0", size = 473599, upload-time = "2026-07-06T10:45:30.524Z" }, + { url = "https://files.pythonhosted.org/packages/29/83/e361d3c1acd1b21e1d489616de6fa4aaf843365d8179f612e3743eac20a9/xxhash-3.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98ee81b4b7f3023c9cb04a78cc67610baffcb5812d92f2096cb5a5efc6f19437", size = 220559, upload-time = "2026-07-06T10:45:32.979Z" }, + { url = "https://files.pythonhosted.org/packages/05/01/006a4243c2c2a6831827f9999f6d1c23feeef100eb023c1f886022a00bf3/xxhash-3.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2666f059a1588a99267e33605365ed89cea92f424b3522806a9f4bd8ad2e3d62", size = 310383, upload-time = "2026-07-06T10:45:35.875Z" }, + { url = "https://files.pythonhosted.org/packages/d8/20/af388e8bf9f9a0f89eeef7d2a1935d176ee1c20bc6adeda05035879379cf/xxhash-3.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0093cf7eeb91b84776e8742113afa4bdf47533d36cf719179aaaf1f56f6f8bf", size = 238228, upload-time = "2026-07-06T10:45:38.02Z" }, + { url = "https://files.pythonhosted.org/packages/63/6b/4666579a87eebd1744663c404297355fa0658617b015cedfa58810ee7036/xxhash-3.8.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3a800912a2e5e975d4128969d645c4a2a80aa886ccd6c9b1c6f44529e327e8cf", size = 269137, upload-time = "2026-07-06T10:45:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/de/d3/e963a8a46f900a137d91b02144d8ea07a8f812971b138204a3b2f8b8e55c/xxhash-3.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0fe37f72a207223d22a4eddc3149d4298993385aa9daef25c039246ca5a309f3", size = 225068, upload-time = "2026-07-06T10:45:41.718Z" }, + { url = "https://files.pythonhosted.org/packages/aa/80/9d181dbcde4b0fe48375f48833a5832d4b8cd2b349b15110c92ee472d874/xxhash-3.8.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5db43f249b4be9f99ef4b967863f37094fb40e67effafb78ba4f0356b6396104", size = 240874, upload-time = "2026-07-06T10:45:43.414Z" }, + { url = "https://files.pythonhosted.org/packages/39/15/ce3ab5a1cd27ead25a5196e55a7284220f6ad6e316da494ffd900b2b600f/xxhash-3.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c4ed42965c2cd9081f011be22f69d0e65d3b6165fe7734072fd0c232840bbd4e", size = 300702, upload-time = "2026-07-06T10:45:45.135Z" }, + { url = "https://files.pythonhosted.org/packages/96/c0/2281a8ab5f2a62dbf57a23c58a01ccc1d98abf40f71193c8a81f59e759b5/xxhash-3.8.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3557bec8fcb11738a8920eeb68974bc76b75262f6947998d3147954ce0a4b893", size = 443351, upload-time = "2026-07-06T10:45:47.188Z" }, + { url = "https://files.pythonhosted.org/packages/81/2e/071a58c1a53a52d4f7a3aa0987be0c396dffd40da8204805fe1b130a81f4/xxhash-3.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:00de40f3b42240db23a82a5c682b55d7263d84a26a953240c1aee463409660e3", size = 217396, upload-time = "2026-07-06T10:45:48.925Z" }, + { url = "https://files.pythonhosted.org/packages/68/44/36ab58134badd9d3433fc7b53c4ca8d113d8e807782885628640f8297a4d/xxhash-3.8.1-cp313-cp313-win32.whl", hash = "sha256:b5196cc2574cfec572a5f3fb7cfa5ade27305ae3d06516a082132441aff4c83a", size = 31974, upload-time = "2026-07-06T10:45:50.591Z" }, + { url = "https://files.pythonhosted.org/packages/96/2a/2a0b84798448e766f7b89ceed073cb0cb5a43fc9ebbacbdea74a38de18e3/xxhash-3.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:538f5f865df6cd8c32dd63158a0e5b4f5dd08d732a7da8b7228a5a0776c8ce55", size = 32739, upload-time = "2026-07-06T10:45:52.221Z" }, + { url = "https://files.pythonhosted.org/packages/d4/60/bb51dbf7c363ff88a7cbd50b7959718219577ef44d7cf255929ffc4a2194/xxhash-3.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:a6617f30641ba0d8baa1635fbefb1dffc5165ec36d26921bd5cee13497cd937a", size = 29239, upload-time = "2026-07-06T10:45:53.714Z" }, + { url = "https://files.pythonhosted.org/packages/56/d3/827ca123c2ee5443a6aaed3c5dd199237dc2f010e2bebd7ec09ef36f3a5f/xxhash-3.8.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:bfcd82852c62a60e314670a9602de354c4460f8adad916e2e42a20860c7870bc", size = 34964, upload-time = "2026-07-06T10:45:55.535Z" }, + { url = "https://files.pythonhosted.org/packages/05/67/67ae2a3ccdeb8b8ef025d35aee9edd1d26c3abe5051d47da9286232afbf8/xxhash-3.8.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:08ea2081f5e88615fec8622a9f87fbe21b8ea58d88cfc02163ca11026ee62a92", size = 32697, upload-time = "2026-07-06T10:45:57.288Z" }, + { url = "https://files.pythonhosted.org/packages/38/5a/3d3994346e1f45493679cb5c1ffc2bf454e410e9d1e8a662d253becee91e/xxhash-3.8.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2e32855b6f9e5b18f449e59d45e3d5778bdeb660632ef2693cca267a11246c75", size = 225954, upload-time = "2026-07-06T10:45:58.897Z" }, + { url = "https://files.pythonhosted.org/packages/3f/2c/53169270309b7cd8e05504e07fe123bac053b89d00ac63617faacf0a2ec0/xxhash-3.8.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6e088bd7870775624256a0d84c2a6714afd223b2eeb56b0ca58398e52a32fda", size = 249776, upload-time = "2026-07-06T10:46:00.977Z" }, + { url = "https://files.pythonhosted.org/packages/70/e0/5c551d8d592f944506f7c5185e210255c15e672a3c6008c156a1bd9b775e/xxhash-3.8.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:72eb5ae575cc7ae2b23f6f8064a8b10f638c7149819ae9cc6d20ebd4d37a1629", size = 274776, upload-time = "2026-07-06T10:46:02.869Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2a/d3a762270cee2d7bcd0e25e28c623e5f3f5c0dc637b66e3e47dd5b0bb3f0/xxhash-3.8.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d0b48cdf690a64cedf7258c3dc9506cc41fc86edd7739c40e3098952265dc068", size = 252056, upload-time = "2026-07-06T10:46:04.688Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/b78e4373b2cb6d1c42af60ea2d7e9146ad0710b239ac7f706d5d31d5bb98/xxhash-3.8.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb9e256a357dfcede7818c6d34e70db2d6b664394803d1de4b6984d2de76c0f1", size = 482108, upload-time = "2026-07-06T10:46:06.498Z" }, + { url = "https://files.pythonhosted.org/packages/e6/0d/642d923336ea61a15f8ce64fc7e078729e6e06c3a026e517fa79b2c23b7a/xxhash-3.8.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51f71a6e2ad071e70c937e41fcb6c19f82c3f9f49831eba850ed4a106ffbb647", size = 226739, upload-time = "2026-07-06T10:46:08.598Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0a/a37d6da6427d45a8d23e3ee3a0ca9c9d4a90364849c6637fe2963a755f9b/xxhash-3.8.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4a6443968c4e8dc69967e12776776a5952c119cc1bd94168ad1c5ad667c2be1", size = 319658, upload-time = "2026-07-06T10:46:10.504Z" }, + { url = "https://files.pythonhosted.org/packages/4a/51/ebbd40da8a3f1bc53b4b7a9a87f8e28bd95c5f21bc14b8a57860cf367d1b/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:714503083a1f2065c9ad15340dd49ac8a8e948a505a705ffa1750cb951519113", size = 246059, upload-time = "2026-07-06T10:46:12.634Z" }, + { url = "https://files.pythonhosted.org/packages/24/4c/d9014030147e1f0bb26e7da47aa240dd9ec61c763c573e558111d869f8e1/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:77f74e45a1e5574bbbf80181c8027b3a4c65c2248fffbd557bd596fff13102f9", size = 275535, upload-time = "2026-07-06T10:46:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/caee2db41fadcd5a25aa4323213f9afec5a8586d4e419241e3d659362bd7/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4e0e1b0fb0259c1b75d1251ac0bb4d7ab675d36f7a6bf4ba6aa630dae94f9ffa", size = 231292, upload-time = "2026-07-06T10:46:16.452Z" }, + { url = "https://files.pythonhosted.org/packages/0b/60/f52f08bcdc904c4514ea5c25caa19e9f3214144434a6ff96dc82dc1cbddd/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:10e4393ec33633c2f05ad01869e546ad080b1a18f2650503731f153774608b31", size = 250490, upload-time = "2026-07-06T10:46:18.318Z" }, + { url = "https://files.pythonhosted.org/packages/24/a0/94dc7ae310838f250669c6ad7168e6d6fca17d49dac1053f06dc232c4a56/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b3ba794c3d885803db6c3116686923f1ec13bc86e621e169a375282b63ea1cc6", size = 309861, upload-time = "2026-07-06T10:46:20.503Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f9/adeead7d0eb28cdfc2832544ea639ffbc6749ccde47a8e228d667459182e/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:57189a69c0891e4818853feaa521c972d22c880a001453addea015f48e3c3398", size = 448739, upload-time = "2026-07-06T10:46:22.79Z" }, + { url = "https://files.pythonhosted.org/packages/04/a4/22ec0e07db57d901c9298ae98aa3cf2be45bafded6f07c13131e85b89032/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d59e71153fe9ff85648d00e18649b07e9b22c797291abb7e27274fa06df8b838", size = 223657, upload-time = "2026-07-06T10:46:24.831Z" }, + { url = "https://files.pythonhosted.org/packages/94/32/8a9531f37b59e5a013003db7cb7414baf4ce7e0e1268e0d5947cd3d6a2df/xxhash-3.8.1-cp313-cp313t-win32.whl", hash = "sha256:5b96f0024e9840f449bd91b2d005c921a4b666055a0d1b6492463799f32aae22", size = 32377, upload-time = "2026-07-06T10:46:26.86Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/2ca45fd7f671de5f81fc297ef1c95080b40c86ec6be0cc6034b8f7707ac8/xxhash-3.8.1-cp313-cp313t-win_amd64.whl", hash = "sha256:37d5a56c36dcc0b9a87b814cd992598d33863ff683749de6c86081f278d5e629", size = 33274, upload-time = "2026-07-06T10:46:28.39Z" }, + { url = "https://files.pythonhosted.org/packages/5a/54/20d7163463ddb6438b73a427d1655a77a502cf9b9b0c3ada3599629d9c0a/xxhash-3.8.1-cp313-cp313t-win_arm64.whl", hash = "sha256:6696c8752aded28ff3b16f33ef28ce28fb5d209b80c206746f943199fcf5fd65", size = 29375, upload-time = "2026-07-06T10:46:29.962Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8b/df2ba04f22a6cd6b39f96a6577329a8471a55c90ef8d8e2f7c102363613f/xxhash-3.8.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:9db455cb649dcfe4504d6d68a6d83a7315a99a3ca59871dc3ff840671f99adba", size = 38430, upload-time = "2026-07-06T10:46:31.496Z" }, + { url = "https://files.pythonhosted.org/packages/b2/4f/6a059e8ad3ca8deedc91dfe335b211204900895152212c03ebbe721de68b/xxhash-3.8.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:affb37f152e55b5e4494bb9d0107f7bb08515c6704fbed82d9f61214d74adc17", size = 36558, upload-time = "2026-07-06T10:46:33.078Z" }, + { url = "https://files.pythonhosted.org/packages/cb/95/40be178205acce092ae418feb20ac737b32a02c7b864926ed0717354c9f8/xxhash-3.8.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:460261045936975193bfd20549a0de1cd52a33b405cbb972f0d80940c42266cd", size = 31181, upload-time = "2026-07-06T10:46:34.793Z" }, + { url = "https://files.pythonhosted.org/packages/3f/89/2da4dbf051bafa156c0e3f12012db2b0ac3b84ff37ca1f021f6bfffcdfbb/xxhash-3.8.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:38c887aedb696ef8bca19983206d270848558cfae4a91afa6a2fb05dde58ffc5", size = 32192, upload-time = "2026-07-06T10:46:36.393Z" }, + { url = "https://files.pythonhosted.org/packages/7c/4e/e000bbae3566bc8e0be771a8a0f294aa99075e3f0bc4ef43922ebffdebc8/xxhash-3.8.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:594131ce1aad18db3689781f806db1b065cdaa04f4df36b4c038d2013aefd0bf", size = 34691, upload-time = "2026-07-06T10:46:38.1Z" }, + { url = "https://files.pythonhosted.org/packages/b4/4a/ea954aacc7d1c8711880ac2b55da94429a9b4296b151c4fc0966549ca1ee/xxhash-3.8.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:78c794b643d214f1522e7a288bcf5a2de120d26cd170516749a4009dc92722c9", size = 34807, upload-time = "2026-07-06T10:46:39.647Z" }, + { url = "https://files.pythonhosted.org/packages/ca/29/df598e738ff37558ac627264deb2e560902d9bf7f46d3bd5175c9eee593e/xxhash-3.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af0c9fedc4a2c24e8664953882fe8185f3790b8338c9c700f76f5ad660817711", size = 32410, upload-time = "2026-07-06T10:46:41.359Z" }, + { url = "https://files.pythonhosted.org/packages/59/9c/81ab40e7d33ada0b3df5d1bc884894d15dbf4f805cd645b685e4606bb8e0/xxhash-3.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:115772daeb71b2f3b9381177017f53e6cf3f3439c840737fdabd21aba6e54920", size = 220564, upload-time = "2026-07-06T10:46:43.463Z" }, + { url = "https://files.pythonhosted.org/packages/fd/6f/62ae6f5c8606320a0e2a41c2dc8c6d91cc5d63d0f84dd9582e9543779dd8/xxhash-3.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:000435984a0469b0f822fe76f35bddea0f96a4d6521b3339a60a6428cdee1edc", size = 241462, upload-time = "2026-07-06T10:46:45.509Z" }, + { url = "https://files.pythonhosted.org/packages/15/a1/9c3a0ec6cb524396f551eddd102a76690a795494eb9784fc67542b0daa37/xxhash-3.8.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2f1c68394818e0595569c2ff3cbc1e6d5a36a434e796f5c526b987b80c8a8c62", size = 264491, upload-time = "2026-07-06T10:46:47.655Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/700a4674e4308eb59d2fdb973977e82eae231bea5044753fee5c9eec0e0c/xxhash-3.8.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:46b39976d008e2a845758650f0ff7136bca004f40da0c8798bd37ac37860154f", size = 242905, upload-time = "2026-07-06T10:46:49.857Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8a/72d9874375c8d4cbc64a8cd1d659d5695a8765c3db82efa82dc5bd9f14d0/xxhash-3.8.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d5006c65ec507a333479e76e00e2c368781f16c24ededa764763956b32a0e93e", size = 473873, upload-time = "2026-07-06T10:46:51.953Z" }, + { url = "https://files.pythonhosted.org/packages/03/f0/6db07590ed7e0a77f186ef0bcea8d52553bf1ba57833e09467a2411f0f2d/xxhash-3.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31a2649bcf1fe97cf11c79848d761df33ac46b3896942d31b640557b486ff6b", size = 220765, upload-time = "2026-07-06T10:46:55.41Z" }, + { url = "https://files.pythonhosted.org/packages/8f/10/00d12d8b8beabbf49a8bbc626fb9f40445145a8887eb41a6acfb69149ac4/xxhash-3.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f759eed402448c2bdbb492e4fba1f20668ffe29688605ea61f0f67f9e4e386d", size = 310478, upload-time = "2026-07-06T10:46:57.729Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f9/12a82394eefb0f185d15a7f7b9f627c61c475a72dd83718436a5b84b42ac/xxhash-3.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5f97ecfede10d5b2870383620e2d25c8561e217c7bf9081073802b54248d2b", size = 238393, upload-time = "2026-07-06T10:46:59.87Z" }, + { url = "https://files.pythonhosted.org/packages/20/f3/53f963e320b9ce678337aa7273f39ce692ded8b99e3d22a866ec722159ab/xxhash-3.8.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1da930bbcac3e8fbe2191850e2abb57977a99348c12c4b385e1058ac1b0a9ecc", size = 268704, upload-time = "2026-07-06T10:47:01.806Z" }, + { url = "https://files.pythonhosted.org/packages/0a/50/5b5badbd87c82d9f9b5f58ac74a3f29ef08f6fc387b324b8fd482450b862/xxhash-3.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:747476436f6891b9773374ce8d48edcc8b12cb5b61b67c6fb6289633747d088f", size = 225015, upload-time = "2026-07-06T10:47:03.784Z" }, + { url = "https://files.pythonhosted.org/packages/30/93/3ca68265afe7b4e69435e08a7b6a1d9d0f2a071e889da1f8041ed00fe878/xxhash-3.8.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef09bbc2519a93cd0f95f2ceb5f7b85919dffea643278e02362bf40e3c4bed1", size = 240951, upload-time = "2026-07-06T10:47:05.816Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a6/27e19670c40f46b5e76e11f2f4713d21054804568425d870670e757172ad/xxhash-3.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a5eed9d41995a83f3332b4e3396abb7f433cac584222bd7e305b606d8353861e", size = 300751, upload-time = "2026-07-06T10:47:07.95Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fb/b33e27689959fe7ed2ae0b830af41560d65213943983afa9db3a8d481bce/xxhash-3.8.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:53f3ed9118397074ff63a79b66b7fec1c84c782eecde35c5bc94e420a971c231", size = 443480, upload-time = "2026-07-06T10:47:10Z" }, + { url = "https://files.pythonhosted.org/packages/26/60/0e0d973be5fe280753ef02fbc89349492ad6e903bf1dcb870b668f94b662/xxhash-3.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d247b34bf433c92b41689318fd25d246313cab2275a6a47e2efac178b80d6efe", size = 217657, upload-time = "2026-07-06T10:47:12.196Z" }, + { url = "https://files.pythonhosted.org/packages/ad/68/c9e3ecef4a9a417d464cb5bd200aa12f73192dee677901b9e08e0ad0d1bb/xxhash-3.8.1-cp314-cp314-win32.whl", hash = "sha256:d58ce8b6cfa9c4d2f230557f69caf7c06369e318015d0b19485095bc2c5963ab", size = 32690, upload-time = "2026-07-06T10:47:14.204Z" }, + { url = "https://files.pythonhosted.org/packages/d7/99/e9e44588c0b62837bbec5ba7927816de0afa03406b1a0b6c7a7e1d1a30a0/xxhash-3.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:6cee733fe4ccb1737e0997135283c82341e5cfa9cf214b165f9087fb663aaf4f", size = 33460, upload-time = "2026-07-06T10:47:16.021Z" }, + { url = "https://files.pythonhosted.org/packages/45/2b/64f36d86380b3657ad9031967ab814f3ef31307174650853f69c18932ebc/xxhash-3.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:58346024d47e84f7d8b3e7f5d6faa1d58acbbe49a8771497872059f58c1d8ea5", size = 30092, upload-time = "2026-07-06T10:47:17.81Z" }, + { url = "https://files.pythonhosted.org/packages/92/cb/18b64bff88c58a0ca209dc533e63cf02d7ae5aa6b1b9a9fd14e81b5dbd60/xxhash-3.8.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:01cab782f8a0a05ecad2c63d7ef10f7ab475f660e0d6419d069418c14d88de7c", size = 35024, upload-time = "2026-07-06T10:47:19.821Z" }, + { url = "https://files.pythonhosted.org/packages/af/1d/72d8a70520e5dcddb472ea0486d299da3240745a10658290cd7b5690ede2/xxhash-3.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:717b12fdc51819833704e85e6926d76981ffa3f780ef92e33ebb8b26d46bb230", size = 32697, upload-time = "2026-07-06T10:47:21.649Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b8/e041f555903c56db3d0a731b3d72a6575d75e0ed868b1bd2e5176111ca44/xxhash-3.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ec55d80e9b8a519d742669e0b49e8ce9e6747be42bf3c138158b6543a9c8e489", size = 226044, upload-time = "2026-07-06T10:47:23.612Z" }, + { url = "https://files.pythonhosted.org/packages/3a/7e/5cdcf06bf6ec4b5d2ac073feb23432ec1d603fd438864cbd2c09c7cb45e1/xxhash-3.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98d8ac1129b4dd39098cffed94d1284aceb61c3aa396757ccc736ac392e4cee5", size = 249899, upload-time = "2026-07-06T10:47:25.812Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c0/eb7e059cb5e1dba11fd30d2fdf882f56e5a417a3eaa43669d43623767f45/xxhash-3.8.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3bc0fa90830df1e1277f33cc6e55de9990b83c0319fd8c7412866cfde38b025e", size = 274892, upload-time = "2026-07-06T10:47:27.931Z" }, + { url = "https://files.pythonhosted.org/packages/66/74/a600aaf7cd39957fd1510adeedb1749c1e7eb82bd632a1153d9c664c3135/xxhash-3.8.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c73b6f652f0745425aa6378319c331293b5341756262e9408ed3d45f183375e6", size = 252243, upload-time = "2026-07-06T10:47:30.288Z" }, + { url = "https://files.pythonhosted.org/packages/ad/04/78d88fa75a6763e5d09bf1b947a392a27988903381b219006f92f3c68fc8/xxhash-3.8.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6114692261eff4266386cdec0f7d87eee24e317ab397c218b7ae6a76b4c6339", size = 482191, upload-time = "2026-07-06T10:47:32.45Z" }, + { url = "https://files.pythonhosted.org/packages/7f/06/07a8aea1108d682de8791ce608cdf367d75ff4e7e57cd3c154bdc6f47b23/xxhash-3.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4df57c0b161ec1b3ed0526a67b0db0914b557e86ee8aae51887aec941b261542", size = 226877, upload-time = "2026-07-06T10:47:34.705Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b5/86bade5618a524d2c06c4041aa2fe8e5749ce16e88afba60d67c1684a21f/xxhash-3.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9043877a917be88ccf230aa5667c1bd059bce80f4c2727e4defa1b29b7f48b08", size = 319794, upload-time = "2026-07-06T10:47:37.08Z" }, + { url = "https://files.pythonhosted.org/packages/23/69/9b1a2b89b1621bb740fbcb7beb512f60f99480c1bdc680c0c90e1f56ff75/xxhash-3.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:559e3cabe522231909f9de98ef06929edbd53782046bd21aae0c72db6f2a0775", size = 246202, upload-time = "2026-07-06T10:47:39.676Z" }, + { url = "https://files.pythonhosted.org/packages/08/ea/662ed6cb49f1d34078b6a3a3e0f3d29ff93fd7b5a03c0bc9ecfd9b2159c3/xxhash-3.8.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:264710bd335016f303763ce1275c6486df30bb57c2245c91b224c983d7ac39b8", size = 275628, upload-time = "2026-07-06T10:47:41.99Z" }, + { url = "https://files.pythonhosted.org/packages/13/f5/49fc9e4c6728a5a3bd8fe639199d2fa67609b3a84f938aff6e8568dd3e4f/xxhash-3.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e14800b9b10bb39d7a60ad4a310e403164d7b8988a27ae933d4e40618a44088e", size = 231390, upload-time = "2026-07-06T10:47:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/64/9d/3acaf8f599c0e0b30e910a3a11ba32929da53c86dc73c7c55fe6a010b4e9/xxhash-3.8.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:ea6a3e734b0fd41b82784a400be946821900daebe610c050a5e0760838a34f99", size = 250600, upload-time = "2026-07-06T10:47:47.611Z" }, + { url = "https://files.pythonhosted.org/packages/23/64/8acab4c5ec60dbe664b5b9858fd44c2413b07e535b09556a0a5022e78aa6/xxhash-3.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cf399fac542a1c7a4734a435b93df2c55e858c7d31abf6c1bdf46f9ae67fbfd0", size = 310032, upload-time = "2026-07-06T10:47:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/56/47/a0288d7329b1fe63e2734a32d19d444a96ae2b4810f545bc61e561224917/xxhash-3.8.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:44c89d915a75c11d2547eaee9098fcd80398987c4bff2974a0497a925bf92c07", size = 448882, upload-time = "2026-07-06T10:47:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/01/e7/3071dfd3beb5c38204ce1cf56bf7749fce08de900fa92714b81d1d8ca1f2/xxhash-3.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:358650d5bda9c635da699c53adf4e8134af492ecc79c960f917eebf088bb6799", size = 223728, upload-time = "2026-07-06T10:47:55.093Z" }, + { url = "https://files.pythonhosted.org/packages/12/11/b99949f0ba2b07e9f9ffe83b9c86faa685f9080725dc21a916a607313be5/xxhash-3.8.1-cp314-cp314t-win32.whl", hash = "sha256:c240939e963653054fc7e4a17c382829cda4aa88a7daf0af841715dbded1b497", size = 33150, upload-time = "2026-07-06T10:47:57.274Z" }, + { url = "https://files.pythonhosted.org/packages/54/1c/09703eb341f8416e74e58d6c6732d4b5c46de59c942363203cb237cc95b0/xxhash-3.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:7258ee276e8772599bc19e14b36f6260306e21b637190cd7cb489a2449d48684", size = 34005, upload-time = "2026-07-06T10:47:59.434Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f9/6ed7251bb6a8af10ac73b1821c60583d2826e5b2064e45a979c935287c98/xxhash-3.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:8f454166c2ffed45636c8d501741e649851ba2f346c4eb73a64c07ac00428f20", size = 30239, upload-time = "2026-07-06T10:48:01.874Z" }, + { url = "https://files.pythonhosted.org/packages/99/e4/4d8040435aeac814fc69ba63621565fbeb19229a138e2568324a26b2a45c/xxhash-3.8.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:39c9d5b61508b0bb68f29e54546de0ed2a74943c6a18585535a7e37356f1dd12", size = 32687, upload-time = "2026-07-06T10:49:42.803Z" }, + { url = "https://files.pythonhosted.org/packages/da/6a/975f1f2318c760e5bcec109ed379713ae645d8d856c2a3b9ec5d26857087/xxhash-3.8.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:83b9130b80b216d56fdf9e87131946b353c9627930c061955a101ea82b09fed9", size = 29879, upload-time = "2026-07-06T10:49:45.172Z" }, + { url = "https://files.pythonhosted.org/packages/08/0b/40a2a55ff52cf635bfdc5eae67a772bec85b4f44c6c737f73f6f528d51d1/xxhash-3.8.1-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8304be0982130954b7fd3aad18e2c6f8ee40254bc3d2e635991c16d77c91e2bd", size = 43246, upload-time = "2026-07-06T10:49:47.905Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6d/56ed2b6b200f26fb474f3fd387d95d0601efcd5bb33430c90c68924bdd77/xxhash-3.8.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b512261801b1e5fde7b6ebf2fef7977339c620cbbca88a0040ad9ad134f4d02", size = 38202, upload-time = "2026-07-06T10:49:50.59Z" }, + { url = "https://files.pythonhosted.org/packages/0d/a3/56864d895d1161a9f17502088e9c1fb7c06bde2c2efdde620d22bb7a9c43/xxhash-3.8.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49aa8692507835dcc1e8ad8021f20c74c2dc13d83b5112e87877faa2a0035b20", size = 34448, upload-time = "2026-07-06T10:49:53.242Z" }, + { url = "https://files.pythonhosted.org/packages/6b/57/5c6e0908a47f61dca96d01c8ee6fce01ed1050611eb779083ba8758fed81/xxhash-3.8.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:345b07b78e2bf583d71682aa34ae5b5fab575f7a1cb31e10263ebbc6f89f8c42", size = 32869, upload-time = "2026-07-06T10:49:55.972Z" }, ] [[package]] name = "yarl" -version = "1.20.1" +version = "1.24.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428, upload-time = "2025-06-10T00:46:09.923Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/65/7fed0d774abf47487c64be14e9223749468922817b5e8792b8a64792a1bb/yarl-1.20.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6032e6da6abd41e4acda34d75a816012717000fa6839f37124a47fcefc49bec4", size = 132910, upload-time = "2025-06-10T00:42:31.108Z" }, - { url = "https://files.pythonhosted.org/packages/8a/7b/988f55a52da99df9e56dc733b8e4e5a6ae2090081dc2754fc8fd34e60aa0/yarl-1.20.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2c7b34d804b8cf9b214f05015c4fee2ebe7ed05cf581e7192c06555c71f4446a", size = 90644, upload-time = "2025-06-10T00:42:33.851Z" }, - { url = "https://files.pythonhosted.org/packages/f7/de/30d98f03e95d30c7e3cc093759982d038c8833ec2451001d45ef4854edc1/yarl-1.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c869f2651cc77465f6cd01d938d91a11d9ea5d798738c1dc077f3de0b5e5fed", size = 89322, upload-time = "2025-06-10T00:42:35.688Z" }, - { url = "https://files.pythonhosted.org/packages/e0/7a/f2f314f5ebfe9200724b0b748de2186b927acb334cf964fd312eb86fc286/yarl-1.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62915e6688eb4d180d93840cda4110995ad50c459bf931b8b3775b37c264af1e", size = 323786, upload-time = "2025-06-10T00:42:37.817Z" }, - { url = "https://files.pythonhosted.org/packages/15/3f/718d26f189db96d993d14b984ce91de52e76309d0fd1d4296f34039856aa/yarl-1.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:41ebd28167bc6af8abb97fec1a399f412eec5fd61a3ccbe2305a18b84fb4ca73", size = 319627, upload-time = "2025-06-10T00:42:39.937Z" }, - { url = "https://files.pythonhosted.org/packages/a5/76/8fcfbf5fa2369157b9898962a4a7d96764b287b085b5b3d9ffae69cdefd1/yarl-1.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21242b4288a6d56f04ea193adde174b7e347ac46ce6bc84989ff7c1b1ecea84e", size = 339149, upload-time = "2025-06-10T00:42:42.627Z" }, - { url = "https://files.pythonhosted.org/packages/3c/95/d7fc301cc4661785967acc04f54a4a42d5124905e27db27bb578aac49b5c/yarl-1.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bea21cdae6c7eb02ba02a475f37463abfe0a01f5d7200121b03e605d6a0439f8", size = 333327, upload-time = "2025-06-10T00:42:44.842Z" }, - { url = "https://files.pythonhosted.org/packages/65/94/e21269718349582eee81efc5c1c08ee71c816bfc1585b77d0ec3f58089eb/yarl-1.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f8a891e4a22a89f5dde7862994485e19db246b70bb288d3ce73a34422e55b23", size = 326054, upload-time = "2025-06-10T00:42:47.149Z" }, - { url = "https://files.pythonhosted.org/packages/32/ae/8616d1f07853704523519f6131d21f092e567c5af93de7e3e94b38d7f065/yarl-1.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd803820d44c8853a109a34e3660e5a61beae12970da479cf44aa2954019bf70", size = 315035, upload-time = "2025-06-10T00:42:48.852Z" }, - { url = "https://files.pythonhosted.org/packages/48/aa/0ace06280861ef055855333707db5e49c6e3a08840a7ce62682259d0a6c0/yarl-1.20.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b982fa7f74c80d5c0c7b5b38f908971e513380a10fecea528091405f519b9ebb", size = 338962, upload-time = "2025-06-10T00:42:51.024Z" }, - { url = "https://files.pythonhosted.org/packages/20/52/1e9d0e6916f45a8fb50e6844f01cb34692455f1acd548606cbda8134cd1e/yarl-1.20.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:33f29ecfe0330c570d997bcf1afd304377f2e48f61447f37e846a6058a4d33b2", size = 335399, upload-time = "2025-06-10T00:42:53.007Z" }, - { url = "https://files.pythonhosted.org/packages/f2/65/60452df742952c630e82f394cd409de10610481d9043aa14c61bf846b7b1/yarl-1.20.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:835ab2cfc74d5eb4a6a528c57f05688099da41cf4957cf08cad38647e4a83b30", size = 338649, upload-time = "2025-06-10T00:42:54.964Z" }, - { url = "https://files.pythonhosted.org/packages/7b/f5/6cd4ff38dcde57a70f23719a838665ee17079640c77087404c3d34da6727/yarl-1.20.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:46b5e0ccf1943a9a6e766b2c2b8c732c55b34e28be57d8daa2b3c1d1d4009309", size = 358563, upload-time = "2025-06-10T00:42:57.28Z" }, - { url = "https://files.pythonhosted.org/packages/d1/90/c42eefd79d0d8222cb3227bdd51b640c0c1d0aa33fe4cc86c36eccba77d3/yarl-1.20.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:df47c55f7d74127d1b11251fe6397d84afdde0d53b90bedb46a23c0e534f9d24", size = 357609, upload-time = "2025-06-10T00:42:59.055Z" }, - { url = "https://files.pythonhosted.org/packages/03/c8/cea6b232cb4617514232e0f8a718153a95b5d82b5290711b201545825532/yarl-1.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76d12524d05841276b0e22573f28d5fbcb67589836772ae9244d90dd7d66aa13", size = 350224, upload-time = "2025-06-10T00:43:01.248Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a3/eaa0ab9712f1f3d01faf43cf6f1f7210ce4ea4a7e9b28b489a2261ca8db9/yarl-1.20.1-cp310-cp310-win32.whl", hash = "sha256:6c4fbf6b02d70e512d7ade4b1f998f237137f1417ab07ec06358ea04f69134f8", size = 81753, upload-time = "2025-06-10T00:43:03.486Z" }, - { url = "https://files.pythonhosted.org/packages/8f/34/e4abde70a9256465fe31c88ed02c3f8502b7b5dead693a4f350a06413f28/yarl-1.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:aef6c4d69554d44b7f9d923245f8ad9a707d971e6209d51279196d8e8fe1ae16", size = 86817, upload-time = "2025-06-10T00:43:05.231Z" }, - { url = "https://files.pythonhosted.org/packages/b1/18/893b50efc2350e47a874c5c2d67e55a0ea5df91186b2a6f5ac52eff887cd/yarl-1.20.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47ee6188fea634bdfaeb2cc420f5b3b17332e6225ce88149a17c413c77ff269e", size = 133833, upload-time = "2025-06-10T00:43:07.393Z" }, - { url = "https://files.pythonhosted.org/packages/89/ed/b8773448030e6fc47fa797f099ab9eab151a43a25717f9ac043844ad5ea3/yarl-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0f6500f69e8402d513e5eedb77a4e1818691e8f45e6b687147963514d84b44b", size = 91070, upload-time = "2025-06-10T00:43:09.538Z" }, - { url = "https://files.pythonhosted.org/packages/e3/e3/409bd17b1e42619bf69f60e4f031ce1ccb29bd7380117a55529e76933464/yarl-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a8900a42fcdaad568de58887c7b2f602962356908eedb7628eaf6021a6e435b", size = 89818, upload-time = "2025-06-10T00:43:11.575Z" }, - { url = "https://files.pythonhosted.org/packages/f8/77/64d8431a4d77c856eb2d82aa3de2ad6741365245a29b3a9543cd598ed8c5/yarl-1.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bad6d131fda8ef508b36be3ece16d0902e80b88ea7200f030a0f6c11d9e508d4", size = 347003, upload-time = "2025-06-10T00:43:14.088Z" }, - { url = "https://files.pythonhosted.org/packages/8d/d2/0c7e4def093dcef0bd9fa22d4d24b023788b0a33b8d0088b51aa51e21e99/yarl-1.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:df018d92fe22aaebb679a7f89fe0c0f368ec497e3dda6cb81a567610f04501f1", size = 336537, upload-time = "2025-06-10T00:43:16.431Z" }, - { url = "https://files.pythonhosted.org/packages/f0/f3/fc514f4b2cf02cb59d10cbfe228691d25929ce8f72a38db07d3febc3f706/yarl-1.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f969afbb0a9b63c18d0feecf0db09d164b7a44a053e78a7d05f5df163e43833", size = 362358, upload-time = "2025-06-10T00:43:18.704Z" }, - { url = "https://files.pythonhosted.org/packages/ea/6d/a313ac8d8391381ff9006ac05f1d4331cee3b1efaa833a53d12253733255/yarl-1.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:812303eb4aa98e302886ccda58d6b099e3576b1b9276161469c25803a8db277d", size = 357362, upload-time = "2025-06-10T00:43:20.888Z" }, - { url = "https://files.pythonhosted.org/packages/00/70/8f78a95d6935a70263d46caa3dd18e1f223cf2f2ff2037baa01a22bc5b22/yarl-1.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98c4a7d166635147924aa0bf9bfe8d8abad6fffa6102de9c99ea04a1376f91e8", size = 348979, upload-time = "2025-06-10T00:43:23.169Z" }, - { url = "https://files.pythonhosted.org/packages/cb/05/42773027968968f4f15143553970ee36ead27038d627f457cc44bbbeecf3/yarl-1.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12e768f966538e81e6e7550f9086a6236b16e26cd964cf4df35349970f3551cf", size = 337274, upload-time = "2025-06-10T00:43:27.111Z" }, - { url = "https://files.pythonhosted.org/packages/05/be/665634aa196954156741ea591d2f946f1b78ceee8bb8f28488bf28c0dd62/yarl-1.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe41919b9d899661c5c28a8b4b0acf704510b88f27f0934ac7a7bebdd8938d5e", size = 363294, upload-time = "2025-06-10T00:43:28.96Z" }, - { url = "https://files.pythonhosted.org/packages/eb/90/73448401d36fa4e210ece5579895731f190d5119c4b66b43b52182e88cd5/yarl-1.20.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8601bc010d1d7780592f3fc1bdc6c72e2b6466ea34569778422943e1a1f3c389", size = 358169, upload-time = "2025-06-10T00:43:30.701Z" }, - { url = "https://files.pythonhosted.org/packages/c3/b0/fce922d46dc1eb43c811f1889f7daa6001b27a4005587e94878570300881/yarl-1.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:daadbdc1f2a9033a2399c42646fbd46da7992e868a5fe9513860122d7fe7a73f", size = 362776, upload-time = "2025-06-10T00:43:32.51Z" }, - { url = "https://files.pythonhosted.org/packages/f1/0d/b172628fce039dae8977fd22caeff3eeebffd52e86060413f5673767c427/yarl-1.20.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:03aa1e041727cb438ca762628109ef1333498b122e4c76dd858d186a37cec845", size = 381341, upload-time = "2025-06-10T00:43:34.543Z" }, - { url = "https://files.pythonhosted.org/packages/6b/9b/5b886d7671f4580209e855974fe1cecec409aa4a89ea58b8f0560dc529b1/yarl-1.20.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:642980ef5e0fa1de5fa96d905c7e00cb2c47cb468bfcac5a18c58e27dbf8d8d1", size = 379988, upload-time = "2025-06-10T00:43:36.489Z" }, - { url = "https://files.pythonhosted.org/packages/73/be/75ef5fd0fcd8f083a5d13f78fd3f009528132a1f2a1d7c925c39fa20aa79/yarl-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:86971e2795584fe8c002356d3b97ef6c61862720eeff03db2a7c86b678d85b3e", size = 371113, upload-time = "2025-06-10T00:43:38.592Z" }, - { url = "https://files.pythonhosted.org/packages/50/4f/62faab3b479dfdcb741fe9e3f0323e2a7d5cd1ab2edc73221d57ad4834b2/yarl-1.20.1-cp311-cp311-win32.whl", hash = "sha256:597f40615b8d25812f14562699e287f0dcc035d25eb74da72cae043bb884d773", size = 81485, upload-time = "2025-06-10T00:43:41.038Z" }, - { url = "https://files.pythonhosted.org/packages/f0/09/d9c7942f8f05c32ec72cd5c8e041c8b29b5807328b68b4801ff2511d4d5e/yarl-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:26ef53a9e726e61e9cd1cda6b478f17e350fb5800b4bd1cd9fe81c4d91cfeb2e", size = 86686, upload-time = "2025-06-10T00:43:42.692Z" }, - { url = "https://files.pythonhosted.org/packages/5f/9a/cb7fad7d73c69f296eda6815e4a2c7ed53fc70c2f136479a91c8e5fbdb6d/yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9", size = 133667, upload-time = "2025-06-10T00:43:44.369Z" }, - { url = "https://files.pythonhosted.org/packages/67/38/688577a1cb1e656e3971fb66a3492501c5a5df56d99722e57c98249e5b8a/yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a", size = 91025, upload-time = "2025-06-10T00:43:46.295Z" }, - { url = "https://files.pythonhosted.org/packages/50/ec/72991ae51febeb11a42813fc259f0d4c8e0507f2b74b5514618d8b640365/yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2", size = 89709, upload-time = "2025-06-10T00:43:48.22Z" }, - { url = "https://files.pythonhosted.org/packages/99/da/4d798025490e89426e9f976702e5f9482005c548c579bdae792a4c37769e/yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee", size = 352287, upload-time = "2025-06-10T00:43:49.924Z" }, - { url = "https://files.pythonhosted.org/packages/1a/26/54a15c6a567aac1c61b18aa0f4b8aa2e285a52d547d1be8bf48abe2b3991/yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819", size = 345429, upload-time = "2025-06-10T00:43:51.7Z" }, - { url = "https://files.pythonhosted.org/packages/d6/95/9dcf2386cb875b234353b93ec43e40219e14900e046bf6ac118f94b1e353/yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16", size = 365429, upload-time = "2025-06-10T00:43:53.494Z" }, - { url = "https://files.pythonhosted.org/packages/91/b2/33a8750f6a4bc224242a635f5f2cff6d6ad5ba651f6edcccf721992c21a0/yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6", size = 363862, upload-time = "2025-06-10T00:43:55.766Z" }, - { url = "https://files.pythonhosted.org/packages/98/28/3ab7acc5b51f4434b181b0cee8f1f4b77a65919700a355fb3617f9488874/yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd", size = 355616, upload-time = "2025-06-10T00:43:58.056Z" }, - { url = "https://files.pythonhosted.org/packages/36/a3/f666894aa947a371724ec7cd2e5daa78ee8a777b21509b4252dd7bd15e29/yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a", size = 339954, upload-time = "2025-06-10T00:43:59.773Z" }, - { url = "https://files.pythonhosted.org/packages/f1/81/5f466427e09773c04219d3450d7a1256138a010b6c9f0af2d48565e9ad13/yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38", size = 365575, upload-time = "2025-06-10T00:44:02.051Z" }, - { url = "https://files.pythonhosted.org/packages/2e/e3/e4b0ad8403e97e6c9972dd587388940a032f030ebec196ab81a3b8e94d31/yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef", size = 365061, upload-time = "2025-06-10T00:44:04.196Z" }, - { url = "https://files.pythonhosted.org/packages/ac/99/b8a142e79eb86c926f9f06452eb13ecb1bb5713bd01dc0038faf5452e544/yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f", size = 364142, upload-time = "2025-06-10T00:44:06.527Z" }, - { url = "https://files.pythonhosted.org/packages/34/f2/08ed34a4a506d82a1a3e5bab99ccd930a040f9b6449e9fd050320e45845c/yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8", size = 381894, upload-time = "2025-06-10T00:44:08.379Z" }, - { url = "https://files.pythonhosted.org/packages/92/f8/9a3fbf0968eac704f681726eff595dce9b49c8a25cd92bf83df209668285/yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a", size = 383378, upload-time = "2025-06-10T00:44:10.51Z" }, - { url = "https://files.pythonhosted.org/packages/af/85/9363f77bdfa1e4d690957cd39d192c4cacd1c58965df0470a4905253b54f/yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004", size = 374069, upload-time = "2025-06-10T00:44:12.834Z" }, - { url = "https://files.pythonhosted.org/packages/35/99/9918c8739ba271dcd935400cff8b32e3cd319eaf02fcd023d5dcd487a7c8/yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5", size = 81249, upload-time = "2025-06-10T00:44:14.731Z" }, - { url = "https://files.pythonhosted.org/packages/eb/83/5d9092950565481b413b31a23e75dd3418ff0a277d6e0abf3729d4d1ce25/yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698", size = 86710, upload-time = "2025-06-10T00:44:16.716Z" }, - { url = "https://files.pythonhosted.org/packages/8a/e1/2411b6d7f769a07687acee88a062af5833cf1966b7266f3d8dfb3d3dc7d3/yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", size = 131811, upload-time = "2025-06-10T00:44:18.933Z" }, - { url = "https://files.pythonhosted.org/packages/b2/27/584394e1cb76fb771371770eccad35de400e7b434ce3142c2dd27392c968/yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", size = 90078, upload-time = "2025-06-10T00:44:20.635Z" }, - { url = "https://files.pythonhosted.org/packages/bf/9a/3246ae92d4049099f52d9b0fe3486e3b500e29b7ea872d0f152966fc209d/yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", size = 88748, upload-time = "2025-06-10T00:44:22.34Z" }, - { url = "https://files.pythonhosted.org/packages/a3/25/35afe384e31115a1a801fbcf84012d7a066d89035befae7c5d4284df1e03/yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691", size = 349595, upload-time = "2025-06-10T00:44:24.314Z" }, - { url = "https://files.pythonhosted.org/packages/28/2d/8aca6cb2cabc8f12efcb82749b9cefecbccfc7b0384e56cd71058ccee433/yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31", size = 342616, upload-time = "2025-06-10T00:44:26.167Z" }, - { url = "https://files.pythonhosted.org/packages/0b/e9/1312633d16b31acf0098d30440ca855e3492d66623dafb8e25b03d00c3da/yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28", size = 361324, upload-time = "2025-06-10T00:44:27.915Z" }, - { url = "https://files.pythonhosted.org/packages/bc/a0/688cc99463f12f7669eec7c8acc71ef56a1521b99eab7cd3abb75af887b0/yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653", size = 359676, upload-time = "2025-06-10T00:44:30.041Z" }, - { url = "https://files.pythonhosted.org/packages/af/44/46407d7f7a56e9a85a4c207724c9f2c545c060380718eea9088f222ba697/yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5", size = 352614, upload-time = "2025-06-10T00:44:32.171Z" }, - { url = "https://files.pythonhosted.org/packages/b1/91/31163295e82b8d5485d31d9cf7754d973d41915cadce070491778d9c9825/yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02", size = 336766, upload-time = "2025-06-10T00:44:34.494Z" }, - { url = "https://files.pythonhosted.org/packages/b4/8e/c41a5bc482121f51c083c4c2bcd16b9e01e1cf8729e380273a952513a21f/yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53", size = 364615, upload-time = "2025-06-10T00:44:36.856Z" }, - { url = "https://files.pythonhosted.org/packages/e3/5b/61a3b054238d33d70ea06ebba7e58597891b71c699e247df35cc984ab393/yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc", size = 360982, upload-time = "2025-06-10T00:44:39.141Z" }, - { url = "https://files.pythonhosted.org/packages/df/a3/6a72fb83f8d478cb201d14927bc8040af901811a88e0ff2da7842dd0ed19/yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04", size = 369792, upload-time = "2025-06-10T00:44:40.934Z" }, - { url = "https://files.pythonhosted.org/packages/7c/af/4cc3c36dfc7c077f8dedb561eb21f69e1e9f2456b91b593882b0b18c19dc/yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4", size = 382049, upload-time = "2025-06-10T00:44:42.854Z" }, - { url = "https://files.pythonhosted.org/packages/19/3a/e54e2c4752160115183a66dc9ee75a153f81f3ab2ba4bf79c3c53b33de34/yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b", size = 384774, upload-time = "2025-06-10T00:44:45.275Z" }, - { url = "https://files.pythonhosted.org/packages/9c/20/200ae86dabfca89060ec6447649f219b4cbd94531e425e50d57e5f5ac330/yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1", size = 374252, upload-time = "2025-06-10T00:44:47.31Z" }, - { url = "https://files.pythonhosted.org/packages/83/75/11ee332f2f516b3d094e89448da73d557687f7d137d5a0f48c40ff211487/yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7", size = 81198, upload-time = "2025-06-10T00:44:49.164Z" }, - { url = "https://files.pythonhosted.org/packages/ba/ba/39b1ecbf51620b40ab402b0fc817f0ff750f6d92712b44689c2c215be89d/yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c", size = 86346, upload-time = "2025-06-10T00:44:51.182Z" }, - { url = "https://files.pythonhosted.org/packages/43/c7/669c52519dca4c95153c8ad96dd123c79f354a376346b198f438e56ffeb4/yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d", size = 138826, upload-time = "2025-06-10T00:44:52.883Z" }, - { url = "https://files.pythonhosted.org/packages/6a/42/fc0053719b44f6ad04a75d7f05e0e9674d45ef62f2d9ad2c1163e5c05827/yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf", size = 93217, upload-time = "2025-06-10T00:44:54.658Z" }, - { url = "https://files.pythonhosted.org/packages/4f/7f/fa59c4c27e2a076bba0d959386e26eba77eb52ea4a0aac48e3515c186b4c/yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3", size = 92700, upload-time = "2025-06-10T00:44:56.784Z" }, - { url = "https://files.pythonhosted.org/packages/2f/d4/062b2f48e7c93481e88eff97a6312dca15ea200e959f23e96d8ab898c5b8/yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d", size = 347644, upload-time = "2025-06-10T00:44:59.071Z" }, - { url = "https://files.pythonhosted.org/packages/89/47/78b7f40d13c8f62b499cc702fdf69e090455518ae544c00a3bf4afc9fc77/yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c", size = 323452, upload-time = "2025-06-10T00:45:01.605Z" }, - { url = "https://files.pythonhosted.org/packages/eb/2b/490d3b2dc66f52987d4ee0d3090a147ea67732ce6b4d61e362c1846d0d32/yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1", size = 346378, upload-time = "2025-06-10T00:45:03.946Z" }, - { url = "https://files.pythonhosted.org/packages/66/ad/775da9c8a94ce925d1537f939a4f17d782efef1f973039d821cbe4bcc211/yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce", size = 353261, upload-time = "2025-06-10T00:45:05.992Z" }, - { url = "https://files.pythonhosted.org/packages/4b/23/0ed0922b47a4f5c6eb9065d5ff1e459747226ddce5c6a4c111e728c9f701/yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3", size = 335987, upload-time = "2025-06-10T00:45:08.227Z" }, - { url = "https://files.pythonhosted.org/packages/3e/49/bc728a7fe7d0e9336e2b78f0958a2d6b288ba89f25a1762407a222bf53c3/yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be", size = 329361, upload-time = "2025-06-10T00:45:10.11Z" }, - { url = "https://files.pythonhosted.org/packages/93/8f/b811b9d1f617c83c907e7082a76e2b92b655400e61730cd61a1f67178393/yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16", size = 346460, upload-time = "2025-06-10T00:45:12.055Z" }, - { url = "https://files.pythonhosted.org/packages/70/fd/af94f04f275f95da2c3b8b5e1d49e3e79f1ed8b6ceb0f1664cbd902773ff/yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513", size = 334486, upload-time = "2025-06-10T00:45:13.995Z" }, - { url = "https://files.pythonhosted.org/packages/84/65/04c62e82704e7dd0a9b3f61dbaa8447f8507655fd16c51da0637b39b2910/yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f", size = 342219, upload-time = "2025-06-10T00:45:16.479Z" }, - { url = "https://files.pythonhosted.org/packages/91/95/459ca62eb958381b342d94ab9a4b6aec1ddec1f7057c487e926f03c06d30/yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390", size = 350693, upload-time = "2025-06-10T00:45:18.399Z" }, - { url = "https://files.pythonhosted.org/packages/a6/00/d393e82dd955ad20617abc546a8f1aee40534d599ff555ea053d0ec9bf03/yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458", size = 355803, upload-time = "2025-06-10T00:45:20.677Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ed/c5fb04869b99b717985e244fd93029c7a8e8febdfcffa06093e32d7d44e7/yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e", size = 341709, upload-time = "2025-06-10T00:45:23.221Z" }, - { url = "https://files.pythonhosted.org/packages/24/fd/725b8e73ac2a50e78a4534ac43c6addf5c1c2d65380dd48a9169cc6739a9/yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d", size = 86591, upload-time = "2025-06-10T00:45:25.793Z" }, - { url = "https://files.pythonhosted.org/packages/94/c3/b2e9f38bc3e11191981d57ea08cab2166e74ea770024a646617c9cddd9f6/yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f", size = 93003, upload-time = "2025-06-10T00:45:27.752Z" }, - { url = "https://files.pythonhosted.org/packages/01/75/0d37402d208d025afa6b5b8eb80e466d267d3fd1927db8e317d29a94a4cb/yarl-1.20.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e42ba79e2efb6845ebab49c7bf20306c4edf74a0b20fc6b2ccdd1a219d12fad3", size = 134259, upload-time = "2025-06-10T00:45:29.882Z" }, - { url = "https://files.pythonhosted.org/packages/73/84/1fb6c85ae0cf9901046f07d0ac9eb162f7ce6d95db541130aa542ed377e6/yarl-1.20.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:41493b9b7c312ac448b7f0a42a089dffe1d6e6e981a2d76205801a023ed26a2b", size = 91269, upload-time = "2025-06-10T00:45:32.917Z" }, - { url = "https://files.pythonhosted.org/packages/f3/9c/eae746b24c4ea29a5accba9a06c197a70fa38a49c7df244e0d3951108861/yarl-1.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f5a5928ff5eb13408c62a968ac90d43f8322fd56d87008b8f9dabf3c0f6ee983", size = 89995, upload-time = "2025-06-10T00:45:35.066Z" }, - { url = "https://files.pythonhosted.org/packages/fb/30/693e71003ec4bc1daf2e4cf7c478c417d0985e0a8e8f00b2230d517876fc/yarl-1.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30c41ad5d717b3961b2dd785593b67d386b73feca30522048d37298fee981805", size = 325253, upload-time = "2025-06-10T00:45:37.052Z" }, - { url = "https://files.pythonhosted.org/packages/0f/a2/5264dbebf90763139aeb0b0b3154763239398400f754ae19a0518b654117/yarl-1.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:59febc3969b0781682b469d4aca1a5cab7505a4f7b85acf6db01fa500fa3f6ba", size = 320897, upload-time = "2025-06-10T00:45:39.962Z" }, - { url = "https://files.pythonhosted.org/packages/e7/17/77c7a89b3c05856489777e922f41db79ab4faf58621886df40d812c7facd/yarl-1.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d2b6fb3622b7e5bf7a6e5b679a69326b4279e805ed1699d749739a61d242449e", size = 340696, upload-time = "2025-06-10T00:45:41.915Z" }, - { url = "https://files.pythonhosted.org/packages/6d/55/28409330b8ef5f2f681f5b478150496ec9cf3309b149dab7ec8ab5cfa3f0/yarl-1.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:749d73611db8d26a6281086f859ea7ec08f9c4c56cec864e52028c8b328db723", size = 335064, upload-time = "2025-06-10T00:45:43.893Z" }, - { url = "https://files.pythonhosted.org/packages/85/58/cb0257cbd4002828ff735f44d3c5b6966c4fd1fc8cc1cd3cd8a143fbc513/yarl-1.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9427925776096e664c39e131447aa20ec738bdd77c049c48ea5200db2237e000", size = 327256, upload-time = "2025-06-10T00:45:46.393Z" }, - { url = "https://files.pythonhosted.org/packages/53/f6/c77960370cfa46f6fb3d6a5a79a49d3abfdb9ef92556badc2dcd2748bc2a/yarl-1.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff70f32aa316393eaf8222d518ce9118148eddb8a53073c2403863b41033eed5", size = 316389, upload-time = "2025-06-10T00:45:48.358Z" }, - { url = "https://files.pythonhosted.org/packages/64/ab/be0b10b8e029553c10905b6b00c64ecad3ebc8ace44b02293a62579343f6/yarl-1.20.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c7ddf7a09f38667aea38801da8b8d6bfe81df767d9dfc8c88eb45827b195cd1c", size = 340481, upload-time = "2025-06-10T00:45:50.663Z" }, - { url = "https://files.pythonhosted.org/packages/c5/c3/3f327bd3905a4916029bf5feb7f86dcf864c7704f099715f62155fb386b2/yarl-1.20.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:57edc88517d7fc62b174fcfb2e939fbc486a68315d648d7e74d07fac42cec240", size = 336941, upload-time = "2025-06-10T00:45:52.554Z" }, - { url = "https://files.pythonhosted.org/packages/d1/42/040bdd5d3b3bb02b4a6ace4ed4075e02f85df964d6e6cb321795d2a6496a/yarl-1.20.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:dab096ce479d5894d62c26ff4f699ec9072269d514b4edd630a393223f45a0ee", size = 339936, upload-time = "2025-06-10T00:45:54.919Z" }, - { url = "https://files.pythonhosted.org/packages/0d/1c/911867b8e8c7463b84dfdc275e0d99b04b66ad5132b503f184fe76be8ea4/yarl-1.20.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14a85f3bd2d7bb255be7183e5d7d6e70add151a98edf56a770d6140f5d5f4010", size = 360163, upload-time = "2025-06-10T00:45:56.87Z" }, - { url = "https://files.pythonhosted.org/packages/e2/31/8c389f6c6ca0379b57b2da87f1f126c834777b4931c5ee8427dd65d0ff6b/yarl-1.20.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c89b5c792685dd9cd3fa9761c1b9f46fc240c2a3265483acc1565769996a3f8", size = 359108, upload-time = "2025-06-10T00:45:58.869Z" }, - { url = "https://files.pythonhosted.org/packages/7f/09/ae4a649fb3964324c70a3e2b61f45e566d9ffc0affd2b974cbf628957673/yarl-1.20.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:69e9b141de5511021942a6866990aea6d111c9042235de90e08f94cf972ca03d", size = 351875, upload-time = "2025-06-10T00:46:01.45Z" }, - { url = "https://files.pythonhosted.org/packages/8d/43/bbb4ed4c34d5bb62b48bf957f68cd43f736f79059d4f85225ab1ef80f4b9/yarl-1.20.1-cp39-cp39-win32.whl", hash = "sha256:b5f307337819cdfdbb40193cad84978a029f847b0a357fbe49f712063cfc4f06", size = 82293, upload-time = "2025-06-10T00:46:03.763Z" }, - { url = "https://files.pythonhosted.org/packages/d7/cd/ce185848a7dba68ea69e932674b5c1a42a1852123584bccc5443120f857c/yarl-1.20.1-cp39-cp39-win_amd64.whl", hash = "sha256:eae7bfe2069f9c1c5b05fc7fe5d612e5bbc089a39309904ee8b829e322dcad00", size = 87385, upload-time = "2025-06-10T00:46:05.655Z" }, - { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload-time = "2025-06-10T00:46:07.521Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/79/12/1e8f37460ea0f7eb59c221fdaf0ed75e7ac43e97f8093b9c6f411df50a78/yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8", size = 210798, upload-time = "2026-05-19T21:31:05.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/df/f1c7a3de0831cd83194f1a85c5bb431b13f81e6b45079314c86d1c4ef3f2/yarl-1.24.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5249a113065c2b7a958bc699759e359cd61cfc81e3069662208f48f191b7ed12", size = 129057, upload-time = "2026-05-19T21:27:47.564Z" }, + { url = "https://files.pythonhosted.org/packages/48/41/7daafb32dd7562bf45b1ce56562e7e1a9146f6479b6456873eb8a3413c40/yarl-1.24.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7f4425fa244fbf530b006d0c5f79ce920114cfff5b4f5f6056e669f8e160fdc0", size = 91545, upload-time = "2026-05-19T21:27:50.089Z" }, + { url = "https://files.pythonhosted.org/packages/a8/8f/7b3ec212f1ea0683f55f978e3246bc313c38818664edfc97a9f349a4901e/yarl-1.24.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15c0b5e49d3c44e2a0b93e6a49476c5edad0a7686b92c395765a7ea775572a75", size = 91380, upload-time = "2026-05-19T21:27:51.953Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1b/8bafab7db23b0567ae9db749099b329d91e3b82bc6028b2050ba583e116c/yarl-1.24.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:246d32a53a947c8f0189f5d699cbd4c7036de45d9359e13ba238d1239678c727", size = 105957, upload-time = "2026-05-19T21:27:53.98Z" }, + { url = "https://files.pythonhosted.org/packages/7f/77/21030c2f8d21d21559719beafc772ada2014be933418ed1eaed9cc800e42/yarl-1.24.2-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:64480fb3e4d4ed9ed71c48a91a477384fc342a50ca30071d2f8a88d51d9c9413", size = 97242, upload-time = "2026-05-19T21:27:55.981Z" }, + { url = "https://files.pythonhosted.org/packages/50/d8/f9ea63d1b6aa910a866e089d871fff6cbd49caab29b86b35221a62dfa0d5/yarl-1.24.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:349de4701dc3760b6e876628423a8f147ef4f5599d10aba1e10702075d424ed9", size = 114719, upload-time = "2026-05-19T21:27:58.037Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a3/04e0ee98ac58a249ea7ed75223f5f901ba81a834f0b4921b58e5cec11757/yarl-1.24.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d162677af8d5d3d6ebab8394b021f4d041ac107a4b705873148a77a49dc9e1b2", size = 112140, upload-time = "2026-05-19T21:27:59.618Z" }, + { url = "https://files.pythonhosted.org/packages/02/ad/0b9cc9f38a7324a7eb1d80f834eaa5283d17e9271bbda3186e598dddaeac/yarl-1.24.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5f5c6ec23a9043f2d139cc072f53dd23168d202a334b9b2fda8de4c3e890d90", size = 106721, upload-time = "2026-05-19T21:28:02.586Z" }, + { url = "https://files.pythonhosted.org/packages/65/e7/a52478ebfc66ec989e085c6ae038b9f1bfa4190baa193b133b669c709e2f/yarl-1.24.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:60de6742447fbbf697f16f070b8a443f1b5fe6ca3826fbef9fe70ecd5328e643", size = 106478, upload-time = "2026-05-19T21:28:04.523Z" }, + { url = "https://files.pythonhosted.org/packages/04/d8/5508530fea8472542de00013ae280765fc938ee196fc4030c43a498afb36/yarl-1.24.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acf93187c3710e422368eb768aee98db551ec7c85adc250207a95c16548ab7ac", size = 105423, upload-time = "2026-05-19T21:28:06.515Z" }, + { url = "https://files.pythonhosted.org/packages/84/f1/ece28505e9628e8b756e11bb4f28864a17cc33b6b44db4d2aaf0622bf630/yarl-1.24.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f4b0352fd41fd34b6651934606268816afd6914d09626f9bcbbf018edb0afb3f", size = 99878, upload-time = "2026-05-19T21:28:08.637Z" }, + { url = "https://files.pythonhosted.org/packages/3f/52/fb5d34529b46dd84013afcfb30b8d2bc2832ed03d412736f577d604fa393/yarl-1.24.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6b208bb939099b4b297438da4e9b25357f0b1c791888669b963e45b203ea9f36", size = 114025, upload-time = "2026-05-19T21:28:10.64Z" }, + { url = "https://files.pythonhosted.org/packages/43/f0/ff9d31aaab024f7a251c0ed308a98ae29bf9f7dc344e78f28b1322431ca2/yarl-1.24.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4b85b8825e631295ff4bc8943f7471d54c533a9360bbe15ebb38e018b555bb8a", size = 105613, upload-time = "2026-05-19T21:28:12.784Z" }, + { url = "https://files.pythonhosted.org/packages/31/7d/3296fb3f3ecd52bf9ae6c16b0895c1cda7e9170a2083861552b683f70264/yarl-1.24.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e26acf20c26cb4fefc631fdb75aca2a6b8fa8b7b5d7f204fb6a8f1e63c706f53", size = 111665, upload-time = "2026-05-19T21:28:14.393Z" }, + { url = "https://files.pythonhosted.org/packages/1a/74/77aa6ddaca4fbf42e45e675a465c43956dd40702281049975a2aa04eae59/yarl-1.24.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:819ca24f8eafcfb683c1bd5f44f2f488cea1274eb8944731ffd2e1f10f619342", size = 106914, upload-time = "2026-05-19T21:28:15.893Z" }, + { url = "https://files.pythonhosted.org/packages/d8/02/7611f22cd1d4ed7373eb7f9ee21fde1046edba2e7c0e514880d760352f48/yarl-1.24.2-cp310-cp310-win_amd64.whl", hash = "sha256:5cb0f995a901c36be096ccbf4c673591c2faabbe96279598ffaec8c030f85bf4", size = 92658, upload-time = "2026-05-19T21:28:17.471Z" }, + { url = "https://files.pythonhosted.org/packages/91/00/671d0add79938127292839ae44506ce2f7fe8909c72d5a931864f128fd0b/yarl-1.24.2-cp310-cp310-win_arm64.whl", hash = "sha256:f408eace7e22a68b467a0562e0d27d322f91fe3eaaa6f466b962c6cfaea9fa39", size = 87887, upload-time = "2026-05-19T21:28:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c5/1ce244152ff2839645e7cae92f90e7bafcb2c52bea7ff586ac714f14f5df/yarl-1.24.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:36348bebb147b83818b9d7e673ea4debc75970afc6ffdc7e3975ad05ce5a58c1", size = 128971, upload-time = "2026-05-19T21:28:20.543Z" }, + { url = "https://files.pythonhosted.org/packages/87/5a/00f36967203ed89cb3acd2c8ed526cc3fed9418eb70ce128160a911c8499/yarl-1.24.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a97e42c8a2233f2f279ecadd9e4a037bcb5d813b78435e8eedd4db5a9e9708c", size = 91507, upload-time = "2026-05-19T21:28:22.556Z" }, + { url = "https://files.pythonhosted.org/packages/31/d0/1fb0c1cd27288f39f6974da4318c32768d72c9890984541fdf1e2e32a51d/yarl-1.24.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8d027d56f1035e339d1001ac33eceab5b2ec8e42e449787bb75e289fb9a5cd1d", size = 91343, upload-time = "2026-05-19T21:28:24.092Z" }, + { url = "https://files.pythonhosted.org/packages/03/ce/d4a646508bed2f8dec6435b40166fe9308dd191262033d3f307b2bbcaecd/yarl-1.24.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a6377060e7927187a42b7eb202090cbe2b34933a4eeaf90e3bd9e33432e5cae", size = 105704, upload-time = "2026-05-19T21:28:25.872Z" }, + { url = "https://files.pythonhosted.org/packages/4b/07/b3278e82d8bc41485bcf6d856cd0433262593de615b1d3dc43bd3f5bead4/yarl-1.24.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:17076578bce0049a5ce57d14ad1bded391b68a3b213e9b81b0097b090244999a", size = 97281, upload-time = "2026-05-19T21:28:27.352Z" }, + { url = "https://files.pythonhosted.org/packages/17/5b/4cee6e7c92e487bebe7afc797da0aa54a248ab4e776a68fe369ec29665a5/yarl-1.24.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:50713f1d4d6be6375bb178bb43d140ee1acb8abe589cd723320b7925a275be1e", size = 114020, upload-time = "2026-05-19T21:28:29.458Z" }, + { url = "https://files.pythonhosted.org/packages/5c/82/111076571545a7d4f9cca3fbd5c6f40615af58642be09f12328f48022468/yarl-1.24.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:34263e2fa8fb5bb63a0d97706cda38edbad62fddb58c7f12d6acbc092812aa50", size = 111450, upload-time = "2026-05-19T21:28:31.262Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ec/08f671f69a444d704aeecebf92af659b67b97a869942411d0a578b08c334/yarl-1.24.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49016d82f032b1bd1e10b01078a7d29ae71bf468eeae0ea22df8bab691e60003", size = 106384, upload-time = "2026-05-19T21:28:32.856Z" }, + { url = "https://files.pythonhosted.org/packages/e5/86/ce41e7a7a199340b2330d52b60f25c4074b6636dd0e60b1a80d31a9db042/yarl-1.24.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3f6d2c216318f8f32038ca3f72501ba08536f0fd18a36e858836b121b2deed9f", size = 106153, upload-time = "2026-05-19T21:28:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5d/31be8a729531ab3e55ac3e7e5c800be8c89ea98947f418b2f6ea259fb6ee/yarl-1.24.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:08d3a33218e0c64393e7610284e770409a9c31c429b078bcb24096ed0a783b8f", size = 105322, upload-time = "2026-05-19T21:28:36.642Z" }, + { url = "https://files.pythonhosted.org/packages/47/9b/b57afb22b386ae87ac9940f09878b98d8c333f89113e6fc96fcf4ca9eb64/yarl-1.24.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5d699376c4ca3cba49bbfae3a05b5b70ded572937171ce1e0b8d87118e2ba294", size = 99057, upload-time = "2026-05-19T21:28:38.386Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4f/06348c27c8389256c313e8a57d796808fc0264c915dd5e7cfd3c0e314dc7/yarl-1.24.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a1cab588b4fa14bea2e55ebea27478adfb05372f47573738e1acc4a36c0b05d2", size = 113502, upload-time = "2026-05-19T21:28:40.091Z" }, + { url = "https://files.pythonhosted.org/packages/5f/1c/284f307b298e4a17b7943b07d9d7ecc4151537f8d137ba51f3bb6c31ca20/yarl-1.24.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ec87ccc31bd21db7ad009d8572c127c1000f268517618a4cc09adba3c2a7f21c", size = 105253, upload-time = "2026-05-19T21:28:41.987Z" }, + { url = "https://files.pythonhosted.org/packages/c8/bf/0de123bec8619e45c80cbded9085f61b5b4a9eddb8abe6d25d28ee1ec866/yarl-1.24.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d1dd47a22843b212baa8d74f37796815d43bd046b42a0f41e9da433386c3136b", size = 111345, upload-time = "2026-05-19T21:28:43.93Z" }, + { url = "https://files.pythonhosted.org/packages/90/af/0248eb065e51129d2a9b2436cd1b5c772c19a6b04e5b6a186955671e3319/yarl-1.24.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7b54b9c67c2b06bd7b9a77253d242124b9c95d2c02def5a1144001ee547dd9d5", size = 106558, upload-time = "2026-05-19T21:28:45.806Z" }, + { url = "https://files.pythonhosted.org/packages/21/3c/f960d7a65ef97d8ba9b424fb5128796a4bc710fc6df2ddbbd7dfdc3bbd20/yarl-1.24.2-cp311-cp311-win_amd64.whl", hash = "sha256:f8fdbcff8b2c7c9284e60c196f693588598ddcee31e11c18e14949ce44519d45", size = 92808, upload-time = "2026-05-19T21:28:48.465Z" }, + { url = "https://files.pythonhosted.org/packages/03/1a/49fb03750e4de4d2284cd5b885a383133c34eef45bd59631b2bb8b7e81e8/yarl-1.24.2-cp311-cp311-win_arm64.whl", hash = "sha256:b32c37a7a337e90822c45797bf3d79d60875cfcccd3ecc80e9f453d87026c122", size = 87610, upload-time = "2026-05-19T21:28:50.07Z" }, + { url = "https://files.pythonhosted.org/packages/f0/da/866bcb01076ba49d2b42b309867bed3826421f1c479655eb7a607b44f20b/yarl-1.24.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b975866c184564c827e0877380f0dae57dcca7e52782128381b72feff6dfceb8", size = 129957, upload-time = "2026-05-19T21:28:51.695Z" }, + { url = "https://files.pythonhosted.org/packages/bf/1d/fcefb70922ea2268a8971d8e5874d9a8218644200fb8465f1dcad55e6851/yarl-1.24.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3b075301a2836a0e297b1b658cb6d6135df535d62efefdd60366bd589c2c82f2", size = 92164, upload-time = "2026-05-19T21:28:53.242Z" }, + { url = "https://files.pythonhosted.org/packages/29/b6/170e2b8d4e3bc30e6bfdcca53556537f5bf595e938632dfcb059311f3ff6/yarl-1.24.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ae44649b00947634ab0dab2a374a638f52923a6e67083f2c156cd5cbd1a881d", size = 91688, upload-time = "2026-05-19T21:28:54.865Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a5/c9f655d5553ea0b99fdac9d6a99ad3f9b3e73b8e5758bb46f58c9831f74c/yarl-1.24.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507cc19f0b45454e2d6dcd62ff7d062b9f77a2812404e62dbdaec05b50faa035", size = 102902, upload-time = "2026-05-19T21:28:56.963Z" }, + { url = "https://files.pythonhosted.org/packages/5d/bc/6b9664d815d79af4ee553337f9d606c56bbf269186ada9172de45f1b5f60/yarl-1.24.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4c17bad5a530912d2111825d3f05e89bab2dd376aaa8cbc77e449e6db63e576", size = 97931, upload-time = "2026-05-19T21:28:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/98/ec/32ba48acae30fecd60928f5791188b80a9d6ee3840507ffda29fecd37b71/yarl-1.24.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5f0cbb112838a4a293985b6ed73948a547dadcc1ba6d2089938e7abdedceef8", size = 111030, upload-time = "2026-05-19T21:29:00.148Z" }, + { url = "https://files.pythonhosted.org/packages/82/5a/6f4cd081e5f4934d2ae3a8ef4abe3afacc010d26f0035ee91b35cd7d7c37/yarl-1.24.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ec8356b8a6afcf81fc7aeeef13b1ff7a49dec00f313394bbb9e83830d32ccd7", size = 110392, upload-time = "2026-05-19T21:29:02.155Z" }, + { url = "https://files.pythonhosted.org/packages/7a/da/323a01c349bd5fb01bb6652e314d9bb218cee630a736bdb810ad50e4013f/yarl-1.24.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e7ebcdef69dec6c6451e616f32b622a6d4a2e92b445c992f7c8e5274a6bbc4c", size = 105612, upload-time = "2026-05-19T21:29:04.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/80/264ab684f181e1a876389374519ff05d10248725535ae2ac4e8ac4e563d6/yarl-1.24.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:47a55d6cf6db2f401017a9e96e5288844e5051911fb4e0c8311a3980f5e59a7d", size = 104487, upload-time = "2026-05-19T21:29:06.491Z" }, + { url = "https://files.pythonhosted.org/packages/41/07/efabe5df87e96d7ad5959760b888344be48cd6884db127b407c6b5503adc/yarl-1.24.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3065657c80a2321225e804048597ad55658a7e76b32d6f5ee4074d04c50401db", size = 102333, upload-time = "2026-05-19T21:29:08.267Z" }, + { url = "https://files.pythonhosted.org/packages/44/0c/bcf7c42603e1009295f586d8890f2ba032c8b53310e815adf0a202c73d9f/yarl-1.24.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cb84b80d88e19ede158619b80813968713d8d008b0e2497a576e6a0557d50712", size = 99025, upload-time = "2026-05-19T21:29:10.682Z" }, + { url = "https://files.pythonhosted.org/packages/4f/82/84482ab1a57a0f21a08afe6a7004c61d741f8f2ecc3b05c321577c612164/yarl-1.24.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:990de4f680b1c217e77ff0d6aa0029f9eb79889c11fb3e9a3942c7eba29c1996", size = 110507, upload-time = "2026-05-19T21:29:12.954Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8d/a546ba1dfe1b0f290e05fef145cd07614c0f15df1a707195e512d1e39d1d/yarl-1.24.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:abb8ec0323b80161e3802da3150ef660b41d0e9be2048b76a363d93eee992c2b", size = 103719, upload-time = "2026-05-19T21:29:14.893Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b6/267f2a09213138473adfce6b8a6e17791d7fee70bd4d9003218e4dec58b0/yarl-1.24.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e7977781f83638a4c73e0f88425563d70173e0dfd90ac006a45c65036293ee3c", size = 110438, upload-time = "2026-05-19T21:29:16.485Z" }, + { url = "https://files.pythonhosted.org/packages/48/2d/1c8d89c7c5f9cad9fb2902445d94e2ab1d7aa35de029afbb8ae95c42d00f/yarl-1.24.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e30dd55825dc554ec5b66a94953b8eda8745926514c5089dfcacecb9c99b5bd1", size = 105719, upload-time = "2026-05-19T21:29:18.367Z" }, + { url = "https://files.pythonhosted.org/packages/a7/25/722e3b93bd687009afb2d59a35e13d30ddd8f80571445bb0c4e4ce26ec66/yarl-1.24.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dafe10c12ddd4d120d528c4b5599c953bd7b12845347d507b95451195bb6cad", size = 92901, upload-time = "2026-05-19T21:29:20.014Z" }, + { url = "https://files.pythonhosted.org/packages/39/47/4486ccfb674c04854a1ef8aa77868b6a6f765feaf69633409d7ca4f02cb8/yarl-1.24.2-cp312-cp312-win_arm64.whl", hash = "sha256:044a09d8401fcf8681977faef6d286b8ade1e2d2e9dceda175d1cfa5ca496f30", size = 87229, upload-time = "2026-05-19T21:29:22.1Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/fcf0ce677f17e5c471c06311dd25964be38a4c586993632910d2e75278bc/yarl-1.24.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:491ac9141decf49ee8030199e1ee251cdff0e131f25678817ff6aa5f837a3536", size = 128978, upload-time = "2026-05-19T21:29:23.83Z" }, + { url = "https://files.pythonhosted.org/packages/d3/58/8e63299bb71ed61a834121d9d3fe6c9fcf2a6a5d09754ff4f20f2d20baf5/yarl-1.24.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e89418f65eda18f99030386305bd44d7d504e328a7945db1ead514fbe03a0607", size = 91733, upload-time = "2026-05-19T21:29:25.375Z" }, + { url = "https://files.pythonhosted.org/packages/c1/24/16748d5dab6daec8b0ed81ccec639a1cded0f18dcc62a4f696b4fe366c37/yarl-1.24.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cdfcce633b4a4bb8281913c57fcafd4b5933fbc19111a5e3930bbd299d6102f1", size = 91113, upload-time = "2026-05-19T21:29:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/b63fff7b71211e866624b21432d5943cbb633eb0c2872d9ee3070648f22c/yarl-1.24.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:863297ddede92ee49024e9a9b11ecb59f310ca85b60d8537f56bed9bbb5b1986", size = 103899, upload-time = "2026-05-19T21:29:28.842Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/ba1974b8533909636f7733fe86cf677e3619527c3c2fa913e0ea89c48757/yarl-1.24.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:374423f70754a2c96942ede36a29d37dc6b0cb8f92f8d009ddf3ed78d3da5488", size = 97862, upload-time = "2026-05-19T21:29:31.086Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a5/123ac993b5c2ba6f554a140305620cb8f150fa543711bbc49be3ec0a65a4/yarl-1.24.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33a29b5d00ccbf3219bb3e351d7875739c19481e030779f48cc46a7a71681a9b", size = 111060, upload-time = "2026-05-19T21:29:32.657Z" }, + { url = "https://files.pythonhosted.org/packages/23/37/c472d3af3509688392134a88a825276770a187f1daa4de3f6dc0a327a751/yarl-1.24.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9532c57211730c515341af11fef6e9b61d157487272a096d0c04da445642592", size = 110613, upload-time = "2026-05-19T21:29:34.379Z" }, + { url = "https://files.pythonhosted.org/packages/df/88/09c28dad91e662ccfaa1b78f1c57badde74fc9d0b23e74aef644750ecd73/yarl-1.24.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91e72cf093fd833483a97ee648e0c053c7c629f51ff4a0e7edd84f806b0c5617", size = 107012, upload-time = "2026-05-19T21:29:36.216Z" }, + { url = "https://files.pythonhosted.org/packages/07/ab/9d4f69d571a94f4d112fa7e2e007200f5a54d319f58c82ac7b7baa61f5c6/yarl-1.24.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b3177bc0a768ef3bacceb4f272632990b7bea352f1b2f1eee9d6d6ff16516f92", size = 105887, upload-time = "2026-05-19T21:29:38.746Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9a/000b2b66c0d772a499fc531d21dab92dfeb73b640a12eed6ba89f49bb2d0/yarl-1.24.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e196952aacaf3b232e265ff02980b64d483dc0972bd49bcb061171ff22ac203a", size = 103620, upload-time = "2026-05-19T21:29:40.368Z" }, + { url = "https://files.pythonhosted.org/packages/41/7c/7c1050f73450fbdaa3f0c72017059f00ce5e13366692f3dba25275a1083d/yarl-1.24.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:204e7a61ce99919c0de1bf904ab5d7aa188a129ea8f690a8f76cfb6e2844dc44", size = 100599, upload-time = "2026-05-19T21:29:42.66Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b1/29e5756b3926705f5f6089bd5b9f50a56eaac550da6e260bf713ead44d04/yarl-1.24.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b156914620f0b9d78dc1adb3751141daee561cfec796088abb89ed49d220f1a", size = 110604, upload-time = "2026-05-19T21:29:44.632Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4b/8415bc96e9b150cde942fbac9a8182985e58f40ce5c54c34ed015407d3ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8372a2b976cf70654b2be6619ab6068acabb35f724c0fda7b277fbf53d66a5cf", size = 105161, upload-time = "2026-05-19T21:29:46.755Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d4/cde059abfa229553b7298a2eadde2752e723d50aeedaef86ce59da2718ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f9a1e9b622ca284143aab5d885848686dcd85453bb1ca9abcdb7503e64dc0056", size = 110619, upload-time = "2026-05-19T21:29:48.972Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2c/d6a6c9a61549f7b6c7e6dc6937d195bcf069582b47b7200dcd0e7b256acf/yarl-1.24.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:810e19b685c8c3c5862f6a38160a1f4e4c0916c9390024ec347b6157a45a0992", size = 107362, upload-time = "2026-05-19T21:29:51Z" }, + { url = "https://files.pythonhosted.org/packages/92/dd/3ae5fe417e9d1c353a548553326eb9935e76b6b727161563b424cc296df3/yarl-1.24.2-cp313-cp313-win_amd64.whl", hash = "sha256:7d37fb7c38f2b6edab0f845c4f85148d4c44204f52bc127021bd2bc9fdbf1656", size = 92667, upload-time = "2026-05-19T21:29:52.743Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/a7beb239f78f27fca1b053c8e8595e4179c02e62249b4687ec218c370c50/yarl-1.24.2-cp313-cp313-win_arm64.whl", hash = "sha256:1e831894be7c2954240e49791fa4b50c05a0dc881de2552cfe3ffd8631c7f461", size = 87069, upload-time = "2026-05-19T21:29:54.442Z" }, + { url = "https://files.pythonhosted.org/packages/40/0e/e08087695fc12789263821c5dc0f8dc52b5b17efd0887cacf419f8a43ba3/yarl-1.24.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f9312b3c02d9b3d23840f67952913c9c8721d7f1b7db305289faefa878f364c2", size = 129670, upload-time = "2026-05-19T21:29:56.631Z" }, + { url = "https://files.pythonhosted.org/packages/3a/98/ab4b5ed1b1b5cd973c8a3eb994c3a6aefb6ce6d399e21bb5f0316c33815c/yarl-1.24.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a4f4d6cd615823bfc7fb7e9b5987c3f41666371d870d51058f77e2680fbe9630", size = 91916, upload-time = "2026-05-19T21:29:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b1/5297bb6a7df4782f7605bffc43b31f5044070935fbbcaa6c705a07e6ac65/yarl-1.24.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0c3063e5c0a8e8e62fae6c2596fa01da1561e4cd1da6fec5789f5cf99a8aefd8", size = 91625, upload-time = "2026-05-19T21:30:00.412Z" }, + { url = "https://files.pythonhosted.org/packages/02/a7/45baabfff76829264e623b185cff0c340d7e11bf3e1cd9ea37e7d17934bd/yarl-1.24.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fecd17873a096036c1c87ab3486f1aef7f269ada7f23f7f856f93b1cc7744f14", size = 104574, upload-time = "2026-05-19T21:30:02.544Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/3a5ab144d3d650ca37d4f4b57e56169be8af3ca34c448793e064b30baaed/yarl-1.24.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a46d1ab4ba4d32e6dc80daf8a28ce0bd83d08df52fbc32f3e288663427734535", size = 97534, upload-time = "2026-05-19T21:30:04.319Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b5/5658fef3681fb5776b4513b052bec750009f47b3a592251c705d75375798/yarl-1.24.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73e68edf6dfd5f73f9ca127d84e2a6f9213c65bdffb736bda19524c0564fcd14", size = 111481, upload-time = "2026-05-19T21:30:05.988Z" }, + { url = "https://files.pythonhosted.org/packages/4c/06/fdcd7dde037f00866dce123ed4ba23dba94beb56fc4cf561668d27be37f2/yarl-1.24.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a296ca617f2d25fbceafb962b88750d627e5984e75732c712154d058ae8d79a3", size = 111529, upload-time = "2026-05-19T21:30:07.738Z" }, + { url = "https://files.pythonhosted.org/packages/c2/53/d81269aaafccea0d33396c03035de997b743f11e648e6e27a0df99c72980/yarl-1.24.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51b2cf5ec89a8b8470177641ed62a3ba22d74e1e898e06ad53aa77972487208", size = 107338, upload-time = "2026-05-19T21:30:09.713Z" }, + { url = "https://files.pythonhosted.org/packages/ae/04/23049463f729bd899df203a7960505a75333edd499cda8aa1d5a82b64df5/yarl-1.24.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:310fc687f7b2044ec54e372c8cbe923bb88f5c37bded0d3079e5791c2fc3cf50", size = 106147, upload-time = "2026-05-19T21:30:11.365Z" }, + { url = "https://files.pythonhosted.org/packages/14/18/04a4b5830b43ed5e4c5015b40e9f6241ad91487d71611061b4e111d6ac80/yarl-1.24.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:297a2fe352ecf858b30a98f87948746ec16f001d279f84aebdbd3bd965e2f1bd", size = 104272, upload-time = "2026-05-19T21:30:12.978Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f7/8cffdf319aee7a7c1dbd07b61d91c3e3fda460c7a93b5f93e445f3806c4c/yarl-1.24.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2a263e76b97bc42bdcd7c5f4953dec1f7cd62a1112fa7f869e57255229390d67", size = 99962, upload-time = "2026-05-19T21:30:15.001Z" }, + { url = "https://files.pythonhosted.org/packages/d7/39/b3cce3b7dbef64ac700ad4cea156a207d01bede0f507587616c364b5468e/yarl-1.24.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:822519b64cf0b474f1a0aaef1dc621438ea46bb77c94df97a5b4d213a7d8a8b1", size = 111063, upload-time = "2026-05-19T21:30:16.683Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ea/100818505e7ebf165c7242ff17fdf7d9fee79e27234aeca871c1082920d7/yarl-1.24.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b6067060d9dc594899ba83e6db6c48c68d1e494a6dab158156ed86977ca7bcb1", size = 105438, upload-time = "2026-05-19T21:30:18.769Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d2/e075a0b32aa6625087de9e653087df0759fed5de4a435fef594181102a77/yarl-1.24.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0063adad533e57171b79db3943b229d40dfafeeee579767f96541f106bac5f1b", size = 111458, upload-time = "2026-05-19T21:30:21.024Z" }, + { url = "https://files.pythonhosted.org/packages/e6/5c/ceea7ba98b65c8eb8d947fdc52f9bedfcd43c6a57c9e3c90c17be8f324a3/yarl-1.24.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ee8e3fb34513e8dc082b586ef4910c98335d43a6fab688cd44d4851bacfce3e8", size = 107589, upload-time = "2026-05-19T21:30:23.412Z" }, + { url = "https://files.pythonhosted.org/packages/fa/d9/5582d57e2b2db9b85eb6663a22efdd78e08805f3f5389566e9fcad254d1b/yarl-1.24.2-cp314-cp314-win_amd64.whl", hash = "sha256:afb00d7fd8e0f285ca29a44cc50df2d622ff2f7a6d933fa641577b5f9d5f3db0", size = 94424, upload-time = "2026-05-19T21:30:25.425Z" }, + { url = "https://files.pythonhosted.org/packages/92/10/7dc07a0e22806a9280f42a57361395506e800c64e22737cd7b0886feab42/yarl-1.24.2-cp314-cp314-win_arm64.whl", hash = "sha256:68cf6eacd6028ef1142bc4b48376b81566385ca6f9e7dde3b0fa91be08ffcb57", size = 88690, upload-time = "2026-05-19T21:30:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/9e/13/d5b8e2c8667db955bcb3de233f18798fefe7edf1d7429c2c9d4f9c401114/yarl-1.24.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:221ce1dd921ac4f603957f17d7c18c5cc0797fbb52f156941f92e04605d1d67b", size = 136248, upload-time = "2026-05-19T21:30:29.297Z" }, + { url = "https://files.pythonhosted.org/packages/de/46/a4a97c05c9c9b8fd266bb2a0df12992c7fbd02391eb9640583411b6dab32/yarl-1.24.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5f3224db28173a00d7afacdee07045cc4673dfab2b15492c7ae10deddbece761", size = 95084, upload-time = "2026-05-19T21:30:31.031Z" }, + { url = "https://files.pythonhosted.org/packages/95/b2/845cf2074a015e6fe0d0808cf1a2d9e868386c4220d657ebd8302b199043/yarl-1.24.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c557165320d6244ebe3a02431b2a201a20080e02f41f0cfa0ccc47a183765da8", size = 95272, upload-time = "2026-05-19T21:30:33.062Z" }, + { url = "https://files.pythonhosted.org/packages/fe/16/e69d4aa244aef45235ddfebc0e04036a6829842bc5a6a795aedc6c998d23/yarl-1.24.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:904065e6e85b1fa54d0d87438bd58c14c0bad97aad654ad1077fd9d87e8478ed", size = 101497, upload-time = "2026-05-19T21:30:34.842Z" }, + { url = "https://files.pythonhosted.org/packages/15/94/c07107715d621076863ee88b3ddf183fa5e9d4aba5769623c9979828410a/yarl-1.24.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cec2a38d70edc10e0e856ceda886af5327a017ccbde8e1de1bd44d300357543", size = 94002, upload-time = "2026-05-19T21:30:37.724Z" }, + { url = "https://files.pythonhosted.org/packages/a9/35/fc1bbdd895b5e4010b8fdd037f7ed3aa289d3863e08231b30231ca9a0815/yarl-1.24.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7484b9361ed222ee1ca5b4337aa4cbdcc4618ce5aff57d9ef1582fd95893fc0", size = 106524, upload-time = "2026-05-19T21:30:40.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/32b66d0a4ba47c296cf86d03e2c67bff58399fe6d6d84d5205c04c66cc6d/yarl-1.24.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:84f9670b89f34db07f81e53aee83e0b938a3412329d51c8f922488be7fcc4024", size = 106165, upload-time = "2026-05-19T21:30:41.888Z" }, + { url = "https://files.pythonhosted.org/packages/95/47/37cb5ff50c5e825d4d38e81bb04d1b7e96bf960f7ab89f9850b162f3f114/yarl-1.24.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:abb2759733d63a28b4956500a5dd57140f26486c92b2caedfb964ab7d9b79dbf", size = 103010, upload-time = "2026-05-19T21:30:43.985Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d2/4597912315096f7bb359e46e13bf8b60994fcbb2db29b804c0902ef4eff5/yarl-1.24.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:081c2bf54efe03774d0311172bc04fedf9ca01e644d4cd8c805688e527209bdc", size = 101128, upload-time = "2026-05-19T21:30:46.291Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d5/c8e86e120521e646013d02a8e3b8884392e28494be8f392366e50d208efc/yarl-1.24.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:86746bef442aa479107fe28132e1277237f9c24c2f00b0b0cf22b3ee0904f2bb", size = 101382, upload-time = "2026-05-19T21:30:48.085Z" }, + { url = "https://files.pythonhosted.org/packages/fa/98/70b229236118f89dbeb739b76f10225bbf53b5497725502594c9a01d699a/yarl-1.24.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:2d07d21d0bc4b17558e8de0b02fbfdf1e347d3bb3699edd00bb92e7c57925420", size = 95964, upload-time = "2026-05-19T21:30:49.785Z" }, + { url = "https://files.pythonhosted.org/packages/87/f8/56c386981e3c8648d279fdef2397ffec577e8320fd5649745e34d54faeb7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4fb1ac3fc5fecd8ae7453ea237e4d22b49befa70266dfe1629924245c21a0c7f", size = 106204, upload-time = "2026-05-19T21:30:51.862Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1e/765afe97811ca35933e2a7de70ac57b1997ea2e4ee895719ee7a231fb7e5/yarl-1.24.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4da31a5512ed1729ca8d8aacde3f7faeb8843cde3165d6bcf7f88f74f17bb8aa", size = 101510, upload-time = "2026-05-19T21:30:53.62Z" }, + { url = "https://files.pythonhosted.org/packages/ee/78/393913f4b9039e1edd09ae8a9bbb9d539be909a8abf6d8a2084585bed4b7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:533ded4dceb5f1f3da7906244f4e82cf46cfd40d84c69a1faf5ac506aa65ecbe", size = 105584, upload-time = "2026-05-19T21:30:55.962Z" }, + { url = "https://files.pythonhosted.org/packages/78/87/deb17b7049bbe74ea11a713b86f8f27800cc1c8648b0b797243ebb4830ba/yarl-1.24.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7b3a85525f6e7eeabcfdd372862b21ee1915db1b498a04e8bf0e389b607ff0bd", size = 103410, upload-time = "2026-05-19T21:30:57.962Z" }, + { url = "https://files.pythonhosted.org/packages/8f/be/f9f7594e23b5b93affff0318e4593c1920331bcaefda326cabcad94296a1/yarl-1.24.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a7624b1ca46ca5d7b864ef0d2f8efe3091454085ee1855b4e992314529972215", size = 102980, upload-time = "2026-05-19T21:30:59.735Z" }, + { url = "https://files.pythonhosted.org/packages/65/a4/ba80dccd3593ff1f01051a818694d07b58cb8232677ee9a22a5a1f93a9fc/yarl-1.24.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e434a45ce2e7a947f951fc5a8944c8cc080b7e59f9c50ae80fd39107cf88126d", size = 91219, upload-time = "2026-05-19T21:31:01.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" }, ] [[package]] name = "zipp" -version = "3.23.0" +version = "4.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, ] [[package]] name = "zope-interface" -version = "7.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "setuptools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/30/93/9210e7606be57a2dfc6277ac97dcc864fd8d39f142ca194fdc186d596fda/zope.interface-7.2.tar.gz", hash = "sha256:8b49f1a3d1ee4cdaf5b32d2e738362c7f5e40ac8b46dd7d1a65e82a4872728fe", size = 252960, upload-time = "2024-11-28T08:45:39.224Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/71/e6177f390e8daa7e75378505c5ab974e0bf59c1d3b19155638c7afbf4b2d/zope.interface-7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce290e62229964715f1011c3dbeab7a4a1e4971fd6f31324c4519464473ef9f2", size = 208243, upload-time = "2024-11-28T08:47:29.781Z" }, - { url = "https://files.pythonhosted.org/packages/52/db/7e5f4226bef540f6d55acfd95cd105782bc6ee044d9b5587ce2c95558a5e/zope.interface-7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05b910a5afe03256b58ab2ba6288960a2892dfeef01336dc4be6f1b9ed02ab0a", size = 208759, upload-time = "2024-11-28T08:47:31.908Z" }, - { url = "https://files.pythonhosted.org/packages/28/ea/fdd9813c1eafd333ad92464d57a4e3a82b37ae57c19497bcffa42df673e4/zope.interface-7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:550f1c6588ecc368c9ce13c44a49b8d6b6f3ca7588873c679bd8fd88a1b557b6", size = 254922, upload-time = "2024-11-28T09:18:11.795Z" }, - { url = "https://files.pythonhosted.org/packages/3b/d3/0000a4d497ef9fbf4f66bb6828b8d0a235e690d57c333be877bec763722f/zope.interface-7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ef9e2f865721553c6f22a9ff97da0f0216c074bd02b25cf0d3af60ea4d6931d", size = 249367, upload-time = "2024-11-28T08:48:24.238Z" }, - { url = "https://files.pythonhosted.org/packages/3e/e5/0b359e99084f033d413419eff23ee9c2bd33bca2ca9f4e83d11856f22d10/zope.interface-7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27f926f0dcb058211a3bb3e0e501c69759613b17a553788b2caeb991bed3b61d", size = 254488, upload-time = "2024-11-28T08:48:28.816Z" }, - { url = "https://files.pythonhosted.org/packages/7b/90/12d50b95f40e3b2fc0ba7f7782104093b9fd62806b13b98ef4e580f2ca61/zope.interface-7.2-cp310-cp310-win_amd64.whl", hash = "sha256:144964649eba4c5e4410bb0ee290d338e78f179cdbfd15813de1a664e7649b3b", size = 211947, upload-time = "2024-11-28T08:48:18.831Z" }, - { url = "https://files.pythonhosted.org/packages/98/7d/2e8daf0abea7798d16a58f2f3a2bf7588872eee54ac119f99393fdd47b65/zope.interface-7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1909f52a00c8c3dcab6c4fad5d13de2285a4b3c7be063b239b8dc15ddfb73bd2", size = 208776, upload-time = "2024-11-28T08:47:53.009Z" }, - { url = "https://files.pythonhosted.org/packages/a0/2a/0c03c7170fe61d0d371e4c7ea5b62b8cb79b095b3d630ca16719bf8b7b18/zope.interface-7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:80ecf2451596f19fd607bb09953f426588fc1e79e93f5968ecf3367550396b22", size = 209296, upload-time = "2024-11-28T08:47:57.993Z" }, - { url = "https://files.pythonhosted.org/packages/49/b4/451f19448772b4a1159519033a5f72672221e623b0a1bd2b896b653943d8/zope.interface-7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:033b3923b63474800b04cba480b70f6e6243a62208071fc148354f3f89cc01b7", size = 260997, upload-time = "2024-11-28T09:18:13.935Z" }, - { url = "https://files.pythonhosted.org/packages/65/94/5aa4461c10718062c8f8711161faf3249d6d3679c24a0b81dd6fc8ba1dd3/zope.interface-7.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a102424e28c6b47c67923a1f337ede4a4c2bba3965b01cf707978a801fc7442c", size = 255038, upload-time = "2024-11-28T08:48:26.381Z" }, - { url = "https://files.pythonhosted.org/packages/9f/aa/1a28c02815fe1ca282b54f6705b9ddba20328fabdc37b8cf73fc06b172f0/zope.interface-7.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25e6a61dcb184453bb00eafa733169ab6d903e46f5c2ace4ad275386f9ab327a", size = 259806, upload-time = "2024-11-28T08:48:30.78Z" }, - { url = "https://files.pythonhosted.org/packages/a7/2c/82028f121d27c7e68632347fe04f4a6e0466e77bb36e104c8b074f3d7d7b/zope.interface-7.2-cp311-cp311-win_amd64.whl", hash = "sha256:3f6771d1647b1fc543d37640b45c06b34832a943c80d1db214a37c31161a93f1", size = 212305, upload-time = "2024-11-28T08:49:14.525Z" }, - { url = "https://files.pythonhosted.org/packages/68/0b/c7516bc3bad144c2496f355e35bd699443b82e9437aa02d9867653203b4a/zope.interface-7.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:086ee2f51eaef1e4a52bd7d3111a0404081dadae87f84c0ad4ce2649d4f708b7", size = 208959, upload-time = "2024-11-28T08:47:47.788Z" }, - { url = "https://files.pythonhosted.org/packages/a2/e9/1463036df1f78ff8c45a02642a7bf6931ae4a38a4acd6a8e07c128e387a7/zope.interface-7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:21328fcc9d5b80768bf051faa35ab98fb979080c18e6f84ab3f27ce703bce465", size = 209357, upload-time = "2024-11-28T08:47:50.897Z" }, - { url = "https://files.pythonhosted.org/packages/07/a8/106ca4c2add440728e382f1b16c7d886563602487bdd90004788d45eb310/zope.interface-7.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6dd02ec01f4468da0f234da9d9c8545c5412fef80bc590cc51d8dd084138a89", size = 264235, upload-time = "2024-11-28T09:18:15.56Z" }, - { url = "https://files.pythonhosted.org/packages/fc/ca/57286866285f4b8a4634c12ca1957c24bdac06eae28fd4a3a578e30cf906/zope.interface-7.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e7da17f53e25d1a3bde5da4601e026adc9e8071f9f6f936d0fe3fe84ace6d54", size = 259253, upload-time = "2024-11-28T08:48:29.025Z" }, - { url = "https://files.pythonhosted.org/packages/96/08/2103587ebc989b455cf05e858e7fbdfeedfc3373358320e9c513428290b1/zope.interface-7.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cab15ff4832580aa440dc9790b8a6128abd0b88b7ee4dd56abacbc52f212209d", size = 264702, upload-time = "2024-11-28T08:48:37.363Z" }, - { url = "https://files.pythonhosted.org/packages/5f/c7/3c67562e03b3752ba4ab6b23355f15a58ac2d023a6ef763caaca430f91f2/zope.interface-7.2-cp312-cp312-win_amd64.whl", hash = "sha256:29caad142a2355ce7cfea48725aa8bcf0067e2b5cc63fcf5cd9f97ad12d6afb5", size = 212466, upload-time = "2024-11-28T08:49:14.397Z" }, - { url = "https://files.pythonhosted.org/packages/c6/3b/e309d731712c1a1866d61b5356a069dd44e5b01e394b6cb49848fa2efbff/zope.interface-7.2-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:3e0350b51e88658d5ad126c6a57502b19d5f559f6cb0a628e3dc90442b53dd98", size = 208961, upload-time = "2024-11-28T08:48:29.865Z" }, - { url = "https://files.pythonhosted.org/packages/49/65/78e7cebca6be07c8fc4032bfbb123e500d60efdf7b86727bb8a071992108/zope.interface-7.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15398c000c094b8855d7d74f4fdc9e73aa02d4d0d5c775acdef98cdb1119768d", size = 209356, upload-time = "2024-11-28T08:48:33.297Z" }, - { url = "https://files.pythonhosted.org/packages/11/b1/627384b745310d082d29e3695db5f5a9188186676912c14b61a78bbc6afe/zope.interface-7.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:802176a9f99bd8cc276dcd3b8512808716492f6f557c11196d42e26c01a69a4c", size = 264196, upload-time = "2024-11-28T09:18:17.584Z" }, - { url = "https://files.pythonhosted.org/packages/b8/f6/54548df6dc73e30ac6c8a7ff1da73ac9007ba38f866397091d5a82237bd3/zope.interface-7.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb23f58a446a7f09db85eda09521a498e109f137b85fb278edb2e34841055398", size = 259237, upload-time = "2024-11-28T08:48:31.71Z" }, - { url = "https://files.pythonhosted.org/packages/b6/66/ac05b741c2129fdf668b85631d2268421c5cd1a9ff99be1674371139d665/zope.interface-7.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a71a5b541078d0ebe373a81a3b7e71432c61d12e660f1d67896ca62d9628045b", size = 264696, upload-time = "2024-11-28T08:48:41.161Z" }, - { url = "https://files.pythonhosted.org/packages/0a/2f/1bccc6f4cc882662162a1158cda1a7f616add2ffe322b28c99cb031b4ffc/zope.interface-7.2-cp313-cp313-win_amd64.whl", hash = "sha256:4893395d5dd2ba655c38ceb13014fd65667740f09fa5bb01caa1e6284e48c0cd", size = 212472, upload-time = "2024-11-28T08:49:56.587Z" }, - { url = "https://files.pythonhosted.org/packages/8c/2c/1f49dc8b4843c4f0848d8e43191aed312bad946a1563d1bf9e46cf2816ee/zope.interface-7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bd449c306ba006c65799ea7912adbbfed071089461a19091a228998b82b1fdb", size = 208349, upload-time = "2024-11-28T08:49:28.872Z" }, - { url = "https://files.pythonhosted.org/packages/ed/7d/83ddbfc8424c69579a90fc8edc2b797223da2a8083a94d8dfa0e374c5ed4/zope.interface-7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a19a6cc9c6ce4b1e7e3d319a473cf0ee989cbbe2b39201d7c19e214d2dfb80c7", size = 208799, upload-time = "2024-11-28T08:49:30.616Z" }, - { url = "https://files.pythonhosted.org/packages/36/22/b1abd91854c1be03f5542fe092e6a745096d2eca7704d69432e119100583/zope.interface-7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72cd1790b48c16db85d51fbbd12d20949d7339ad84fd971427cf00d990c1f137", size = 254267, upload-time = "2024-11-28T09:18:21.059Z" }, - { url = "https://files.pythonhosted.org/packages/2a/dd/fcd313ee216ad0739ae00e6126bc22a0af62a74f76a9ca668d16cd276222/zope.interface-7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52e446f9955195440e787596dccd1411f543743c359eeb26e9b2c02b077b0519", size = 248614, upload-time = "2024-11-28T08:48:41.953Z" }, - { url = "https://files.pythonhosted.org/packages/88/d4/4ba1569b856870527cec4bf22b91fe704b81a3c1a451b2ccf234e9e0666f/zope.interface-7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ad9913fd858274db8dd867012ebe544ef18d218f6f7d1e3c3e6d98000f14b75", size = 253800, upload-time = "2024-11-28T08:48:46.637Z" }, - { url = "https://files.pythonhosted.org/packages/69/da/c9cfb384c18bd3a26d9fc6a9b5f32ccea49ae09444f097eaa5ca9814aff9/zope.interface-7.2-cp39-cp39-win_amd64.whl", hash = "sha256:1090c60116b3da3bfdd0c03406e2f14a1ff53e5771aebe33fec1edc0a350175d", size = 211980, upload-time = "2024-11-28T08:50:35.681Z" }, +version = "8.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/08/dc/50550cfcbb2ea3cbca5f1d7ed05c8aa840f831a0f2d63aec0a953f7c590e/zope_interface-8.5.tar.gz", hash = "sha256:7a3ba1c5877f0f3e3906b02ddf793abed2becc2948116414ce0e1dd820b68d6d", size = 257957, upload-time = "2026-05-26T06:50:14.574Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/43/9cd98bee951d23848de690ba2809f87e3b22c67c370987acc960da15ad37/zope_interface-8.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0c8aa2bf8f3911ef37b87deb1bbe225a310e6eb6522a16d77f5d8330c4f6fbe", size = 210951, upload-time = "2026-05-26T06:49:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/17/0f/8f1a29966bcf863e3a2121edcafb81c55715de7886bcc9544749cc79e7da/zope_interface-8.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:efe234a0fafb4b6b1602e9be9245b97c2bf06d67c07af5a4bc3c0438978b555c", size = 211309, upload-time = "2026-05-26T06:49:02.732Z" }, + { url = "https://files.pythonhosted.org/packages/9f/9f/37e564eaaf85e3abc1ada40a79fa43f2ab45bdb67431b0ec0fe29e4763e2/zope_interface-8.5-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:dabeb6fe1228d411994f300811edc6866fff0cdcbc9cef98a78f05ea0da42e37", size = 254881, upload-time = "2026-05-26T06:49:04.303Z" }, + { url = "https://files.pythonhosted.org/packages/06/61/e6501d8ea7a2cac3217e03f404e1f98c1df7191d83cfe86b1895fbba5dac/zope_interface-8.5-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:147a9442dcc2b7339ecdb1be2b3cdb098e90462e39425054053ebfb50d99125a", size = 259811, upload-time = "2026-05-26T06:49:06.373Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/bfa25ef480b02af6e9452c478483fec75e87c9e2b60c407fd0b1f6054b9c/zope_interface-8.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a17e681224267880707c9ec9e730ad9a1ad2d65c371256843efba6cf48711b58", size = 260358, upload-time = "2026-05-26T06:49:08.317Z" }, + { url = "https://files.pythonhosted.org/packages/64/51/2b518072fea76242da64451d501c69b7b5ccdef9b57fead584ccf1c180d5/zope_interface-8.5-cp310-cp310-win_amd64.whl", hash = "sha256:d178968a1a611df30549a717d1624cb38ca810347339e3e37b7baa6f6781a170", size = 214822, upload-time = "2026-05-26T06:49:10.441Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f1/83ad110fb847413affe71609bb50e59e1aa082e1236030122227c7c283d3/zope_interface-8.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:afc66ccaef2a3c0bef6ca02aad40d29a39276389dad16a8eac36f9f385e4d057", size = 211426, upload-time = "2026-05-26T06:49:12.595Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a7/6b6e0c31ac240cb9fc015ae9ed45ca54be886c18fcf7bfa2377a4d7a8785/zope_interface-8.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c28044972187245d7a309e4699319bfdbd2ffcbf7176d1d4ddf5adffb2dea80f", size = 211850, upload-time = "2026-05-26T06:49:14.474Z" }, + { url = "https://files.pythonhosted.org/packages/37/36/7599ecabcf80ce4fef2e1ef3c5ac0d4696b61f03f724cc44022f4d226af9/zope_interface-8.5-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:03bbecc7982af713d7499d4084bc03916413d17ffd45f89009348cc0c1d9e376", size = 260711, upload-time = "2026-05-26T06:49:16.568Z" }, + { url = "https://files.pythonhosted.org/packages/03/3e/1774b0ee46ccbb5498ee3c33ece40315b6ef58bc71957be94bd345340bc1/zope_interface-8.5-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf917009a4a7457c7290225a019f4a0aa706d96accd2cfdba2418d3bc1fcde2f", size = 265277, upload-time = "2026-05-26T06:49:18.656Z" }, + { url = "https://files.pythonhosted.org/packages/b6/09/e533b2ffabaae4e5d5730d6768a591cf335defe8e37bec2ad905d09be656/zope_interface-8.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:31cff25b2aaedb5267e6e77b1e9be6b0ec4f622032de8a069202b8ffacda7dc2", size = 266369, upload-time = "2026-05-26T06:49:20.174Z" }, + { url = "https://files.pythonhosted.org/packages/49/4a/3ebe6a4c122b2d5340db45cbe7e490663d3228b172710ec71060cd5d541e/zope_interface-8.5-cp311-cp311-win_amd64.whl", hash = "sha256:17a3114bbdddb5e75e5784cdf318944636190cbbc72d357ef9fb1a8b0351f955", size = 215161, upload-time = "2026-05-26T06:49:21.799Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/056ad97af5b16db1975ee98ec7ab03d2ce3f3355efad904ced1dbce0e39f/zope_interface-8.5-cp311-cp311-win_arm64.whl", hash = "sha256:aab6bb5bee10f38ea688b95ba054396b67f613552d2c8378be7fcb2d2fba7646", size = 213481, upload-time = "2026-05-26T06:49:25.085Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/b84123a948f3162a34623e188922827cd845244fdd043ed20f8d02228caa/zope_interface-8.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8e6ee90c2e6de7c37058d5fa41f123c8b13a312db8d1e0fb5840d7f4bcdff9c9", size = 212165, upload-time = "2026-05-26T06:49:26.566Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/cbceec44f1b27208a76c1a688c131302685852406a23df5aab68324109cc/zope_interface-8.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c1adc90d3576b3b4c4de4953e6002c37bef28b78d7fa54c1bbfd0c50f022fe7c", size = 212341, upload-time = "2026-05-26T06:49:28.182Z" }, + { url = "https://files.pythonhosted.org/packages/e1/c3/005032195ff3b210c139b7c560ed5c534e844b0907d8e44d2b3d8919305e/zope_interface-8.5-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:e6347b8d8d12c5eca6502450a92be30079b7acfade2c4f693efa0deb8871b06e", size = 265296, upload-time = "2026-05-26T06:49:29.741Z" }, + { url = "https://files.pythonhosted.org/packages/c5/66/1036543d6a66bc04c19df3cf650f3ad938a002ab0a443c24e23e8de5e8b9/zope_interface-8.5-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5e970dabea777a24b0b0bbf9dae3ab75ce8b2d8e948edf4875627034b21f3560", size = 270689, upload-time = "2026-05-26T06:49:31.767Z" }, + { url = "https://files.pythonhosted.org/packages/30/4c/8b56259558cace4414e753ca6740396a1f59d4a95ddb55b4658600408670/zope_interface-8.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f0b48ccadaa9839e09ff81e969703cecb3f402c813bfe8b958652e699bea69f5", size = 270280, upload-time = "2026-05-26T06:49:33.489Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ea/649908c83aa8fdb7faf2ddca4d3cf6fb8f2157121267dc56e8f72681e26c/zope_interface-8.5-cp312-cp312-win_amd64.whl", hash = "sha256:e0e311f1277468c08fd59a2b41f71b43d25dff639789d364747acd1705c0df6e", size = 215019, upload-time = "2026-05-26T06:49:35.607Z" }, + { url = "https://files.pythonhosted.org/packages/9f/97/da13037b4c563e4df32eedbc819f8c00b754af494f68211e3dffd48d52da/zope_interface-8.5-cp312-cp312-win_arm64.whl", hash = "sha256:652b73107a04159ec6c020db6c1543d4f1e8f4d069bd2aac88a947820923517b", size = 213569, upload-time = "2026-05-26T06:49:37.317Z" }, + { url = "https://files.pythonhosted.org/packages/f4/8c/4c15755d701f2ec0e80d64a18e1ebaf5be2c584c0ec153fd516f5d13eada/zope_interface-8.5-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:28e80457c134d1fa57a7d758004dece348654e1b1467ac22dcdc20fc1d127c52", size = 212512, upload-time = "2026-05-26T06:49:38.996Z" }, + { url = "https://files.pythonhosted.org/packages/9a/2e/4360c54c465db042cc8fbeeec92abac28b4cedbf6ba63c1f092fd08a190f/zope_interface-8.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:09495ce9d559c06b70f2d4855b3e4f48a822a9ddc8be1d30c5b4e5be14ae1ace", size = 212541, upload-time = "2026-05-26T06:49:41.186Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a5/692a2b8d70f78e848793231d5fae5fecbf8d0cccd73430fdc34802a6d3c1/zope_interface-8.5-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:7849ad8fa90763cc1087f4dda78ca3a233e950b3e08fac7079297c9cafbbd7bb", size = 265191, upload-time = "2026-05-26T06:49:43.449Z" }, + { url = "https://files.pythonhosted.org/packages/70/8d/454a9cfc7a050c394ab4f11b3371f7897828b7415e096afff724637e65e0/zope_interface-8.5-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5578c9421ca409a1f39f153d6f7803e4cde01da592ec75a9ac5e1b777d18d33b", size = 270626, upload-time = "2026-05-26T06:49:45.425Z" }, + { url = "https://files.pythonhosted.org/packages/51/8c/db8409cfa3575b8e9b4800babd7d49f8228433cd1f0c56814bd0ada49c33/zope_interface-8.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e1bd7d96b4ca5fa311f54c9eac16dce4886b428c1531dbe06067763ccdf123b4", size = 270444, upload-time = "2026-05-26T06:49:47.025Z" }, + { url = "https://files.pythonhosted.org/packages/4a/df/a386940e41469ef615e100a216d8b386521e9e598817147f87932ca203c4/zope_interface-8.5-cp313-cp313-win_amd64.whl", hash = "sha256:0c8123d2a4dfde2a613c7cb772605477724782c20bc2e0ad1d9435376a6a44a3", size = 215021, upload-time = "2026-05-26T06:49:48.478Z" }, + { url = "https://files.pythonhosted.org/packages/89/75/477eb5669b6b2a7a843decd1a075e9b1971a8720017654143a7183abd3d9/zope_interface-8.5-cp313-cp313-win_arm64.whl", hash = "sha256:6d02be14f3173c6c7288bc2fdf530090c01c3cf8764ad46c68024686f364278e", size = 213610, upload-time = "2026-05-26T06:49:50.01Z" }, + { url = "https://files.pythonhosted.org/packages/d4/19/5032e954827fdf02db2d2f49737ac4378bb9cfc2cd95a8f2e2a5ae2ec01a/zope_interface-8.5-cp314-cp314-macosx_10_9_x86_64.whl", hash = "sha256:ffaecf013251a89d0de6feb49a46eba48ad8cbbf8a40aeb6045e459e7bec6784", size = 212597, upload-time = "2026-05-26T06:49:51.63Z" }, + { url = "https://files.pythonhosted.org/packages/f1/53/3ef644012cf8a6a234a2d6134aab5a5c65ac5467c86296865501d4fbc406/zope_interface-8.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:126fa9d1c52295ae076d4cf968634f0a1826afa408a20808b57ff72877b8f69f", size = 212626, upload-time = "2026-05-26T06:49:53.236Z" }, + { url = "https://files.pythonhosted.org/packages/32/67/bc8b4f465d388039255003e230c284a175cedf1203c692f23cb7bff64efe/zope_interface-8.5-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:3090e3a663d20194756a59a272e0c8508b889341e31d5894223331fe6b4f9b21", size = 266827, upload-time = "2026-05-26T06:49:54.873Z" }, + { url = "https://files.pythonhosted.org/packages/a7/eb/37d05b935ede53d79690fecc8d201440084418e590bcfc05f384451c7593/zope_interface-8.5-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9342fb74e2afefdb081bf1df727d209ea56995c6e13f5a0540e6d7aff4beafb8", size = 270139, upload-time = "2026-05-26T06:49:57.116Z" }, + { url = "https://files.pythonhosted.org/packages/8b/0b/fd0c54579e2ce8dc6cf1a757903f3374bc6fbda929a46af9e0f53cb0e5f0/zope_interface-8.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6c54725d818f1b57a7efb8b16528326e1f3c257b602b32393fd255c45af8799d", size = 270338, upload-time = "2026-05-26T06:49:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/c1/1d/c420dcd777bb761067ea92879ac766694a5ca78608185f1aecea64cbfc11/zope_interface-8.5-cp314-cp314-win_amd64.whl", hash = "sha256:29d74febbae1afeb6834c4ccbf42e242a673c860060f09e53142825270456140", size = 215789, upload-time = "2026-05-26T06:50:00.405Z" }, + { url = "https://files.pythonhosted.org/packages/62/94/50b5eb8f94e527edceac14f9955e58917424ea79bb572ddc18548561cbc2/zope_interface-8.5-cp314-cp314-win_arm64.whl", hash = "sha256:633c8c49396f38df030340797c533e9fe460d1b5d1e42d88e55e938e525f548c", size = 213757, upload-time = "2026-05-26T06:50:01.973Z" }, + { url = "https://files.pythonhosted.org/packages/17/6f/5d5f32c4dfcdb16ce2ec5363da686840f13c13e1a1214cb70b49e1cd6d9f/zope_interface-8.5-cp314-cp314t-macosx_10_9_x86_64.whl", hash = "sha256:133999820fdbae513c36c03d6f29ef87317aaa3edef39112222b155083664714", size = 213591, upload-time = "2026-05-26T06:50:03.529Z" }, + { url = "https://files.pythonhosted.org/packages/f3/55/de0c3459ff717fce3342f9a29464c281fdeb0d36c3171ee88d119d5f0650/zope_interface-8.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8bd75c96966e573232f0599deaff717564828031c7f05563ccc1ac35c5ee0304", size = 213733, upload-time = "2026-05-26T06:50:05.101Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/d97430abd5ae9677e8b9295b58720c0064a5b557dbb6b8bf5928484cf0d8/zope_interface-8.5-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:14b0e9799351d4c34fe99afd67f0cdd76e55ba15c66a98699d5fc22ea8241e08", size = 294905, upload-time = "2026-05-26T06:50:07.384Z" }, + { url = "https://files.pythonhosted.org/packages/41/ec/a0f8f3dad6e74992f4654bdd94802be0929eabca7b871cac3b6fbb5e961b/zope_interface-8.5-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0cd6a732ac84b94eb1ef9222a117347a27efd294ee16810ffdf7ecd307677ed5", size = 300885, upload-time = "2026-05-26T06:50:08.997Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/6881b48803a0ee8d23eb5efa30fce3ed218a2bd9de5758ce489d224fee81/zope_interface-8.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:798b7c87d0e59a7d5d086d642208d0d8700ff0d55c4029134b3c479c3bfb110f", size = 304672, upload-time = "2026-05-26T06:50:10.563Z" }, + { url = "https://files.pythonhosted.org/packages/2e/0e/b4c01320859ff1d585438bc231fd60bd258d096359bccf6654fecdf0cffb/zope_interface-8.5-cp314-cp314t-win_amd64.whl", hash = "sha256:0fc3a9d45f114d27eaa1e53beeb144533689edca8a9f66505b1e8e8b3f075e42", size = 217241, upload-time = "2026-05-26T06:50:12.171Z" }, +] + +[[package]] +name = "zstandard" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/7a/28efd1d371f1acd037ac64ed1c5e2b41514a6cc937dd6ab6a13ab9f0702f/zstandard-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e59fdc271772f6686e01e1b3b74537259800f57e24280be3f29c8a0deb1904dd", size = 795256, upload-time = "2025-09-14T22:15:56.415Z" }, + { url = "https://files.pythonhosted.org/packages/96/34/ef34ef77f1ee38fc8e4f9775217a613b452916e633c4f1d98f31db52c4a5/zstandard-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4d441506e9b372386a5271c64125f72d5df6d2a8e8a2a45a0ae09b03cb781ef7", size = 640565, upload-time = "2025-09-14T22:15:58.177Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1b/4fdb2c12eb58f31f28c4d28e8dc36611dd7205df8452e63f52fb6261d13e/zstandard-0.25.0-cp310-cp310-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:ab85470ab54c2cb96e176f40342d9ed41e58ca5733be6a893b730e7af9c40550", size = 5345306, upload-time = "2025-09-14T22:16:00.165Z" }, + { url = "https://files.pythonhosted.org/packages/73/28/a44bdece01bca027b079f0e00be3b6bd89a4df180071da59a3dd7381665b/zstandard-0.25.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e05ab82ea7753354bb054b92e2f288afb750e6b439ff6ca78af52939ebbc476d", size = 5055561, upload-time = "2025-09-14T22:16:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/e9/74/68341185a4f32b274e0fc3410d5ad0750497e1acc20bd0f5b5f64ce17785/zstandard-0.25.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:78228d8a6a1c177a96b94f7e2e8d012c55f9c760761980da16ae7546a15a8e9b", size = 5402214, upload-time = "2025-09-14T22:16:04.109Z" }, + { url = "https://files.pythonhosted.org/packages/8b/67/f92e64e748fd6aaffe01e2b75a083c0c4fd27abe1c8747fee4555fcee7dd/zstandard-0.25.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b6bd67528ee8b5c5f10255735abc21aa106931f0dbaf297c7be0c886353c3d0", size = 5449703, upload-time = "2025-09-14T22:16:06.312Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e5/6d36f92a197c3c17729a2125e29c169f460538a7d939a27eaaa6dcfcba8e/zstandard-0.25.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4b6d83057e713ff235a12e73916b6d356e3084fd3d14ced499d84240f3eecee0", size = 5556583, upload-time = "2025-09-14T22:16:08.457Z" }, + { url = "https://files.pythonhosted.org/packages/d7/83/41939e60d8d7ebfe2b747be022d0806953799140a702b90ffe214d557638/zstandard-0.25.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9174f4ed06f790a6869b41cba05b43eeb9a35f8993c4422ab853b705e8112bbd", size = 5045332, upload-time = "2025-09-14T22:16:10.444Z" }, + { url = "https://files.pythonhosted.org/packages/b3/87/d3ee185e3d1aa0133399893697ae91f221fda79deb61adbe998a7235c43f/zstandard-0.25.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:25f8f3cd45087d089aef5ba3848cd9efe3ad41163d3400862fb42f81a3a46701", size = 5572283, upload-time = "2025-09-14T22:16:12.128Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1d/58635ae6104df96671076ac7d4ae7816838ce7debd94aecf83e30b7121b0/zstandard-0.25.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3756b3e9da9b83da1796f8809dd57cb024f838b9eeafde28f3cb472012797ac1", size = 4959754, upload-time = "2025-09-14T22:16:14.225Z" }, + { url = "https://files.pythonhosted.org/packages/75/d6/57e9cb0a9983e9a229dd8fd2e6e96593ef2aa82a3907188436f22b111ccd/zstandard-0.25.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:81dad8d145d8fd981b2962b686b2241d3a1ea07733e76a2f15435dfb7fb60150", size = 5266477, upload-time = "2025-09-14T22:16:16.343Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a9/ee891e5edf33a6ebce0a028726f0bbd8567effe20fe3d5808c42323e8542/zstandard-0.25.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a5a419712cf88862a45a23def0ae063686db3d324cec7edbe40509d1a79a0aab", size = 5440914, upload-time = "2025-09-14T22:16:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/58/08/a8522c28c08031a9521f27abc6f78dbdee7312a7463dd2cfc658b813323b/zstandard-0.25.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e7360eae90809efd19b886e59a09dad07da4ca9ba096752e61a2e03c8aca188e", size = 5819847, upload-time = "2025-09-14T22:16:20.559Z" }, + { url = "https://files.pythonhosted.org/packages/6f/11/4c91411805c3f7b6f31c60e78ce347ca48f6f16d552fc659af6ec3b73202/zstandard-0.25.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:75ffc32a569fb049499e63ce68c743155477610532da1eb38e7f24bf7cd29e74", size = 5363131, upload-time = "2025-09-14T22:16:22.206Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d6/8c4bd38a3b24c4c7676a7a3d8de85d6ee7a983602a734b9f9cdefb04a5d6/zstandard-0.25.0-cp310-cp310-win32.whl", hash = "sha256:106281ae350e494f4ac8a80470e66d1fe27e497052c8d9c3b95dc4cf1ade81aa", size = 436469, upload-time = "2025-09-14T22:16:25.002Z" }, + { url = "https://files.pythonhosted.org/packages/93/90/96d50ad417a8ace5f841b3228e93d1bb13e6ad356737f42e2dde30d8bd68/zstandard-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea9d54cc3d8064260114a0bbf3479fc4a98b21dffc89b3459edd506b69262f6e", size = 506100, upload-time = "2025-09-14T22:16:23.569Z" }, + { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" }, + { url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" }, + { url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" }, + { url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" }, + { url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" }, + { url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" }, + { url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" }, + { url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" }, + { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" }, + { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" }, + { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" }, + { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" }, + { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" }, + { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" }, + { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" }, + { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" }, + { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" }, + { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" }, + { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" }, + { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" }, + { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" }, + { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" }, + { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" }, + { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" }, + { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" }, + { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" }, + { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" }, + { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, + { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, ]